mirror of
https://github.com/penpot/penpot.git
synced 2026-04-30 13:49:06 +00:00
✨ Add HEX, HSB, and HSL support in the third color tab (#9134)
* ✨ Add HEX, HSB, and HSL support in the third color tab Relabel the existing HSVA tab to HSBA (the math was already HSB) and add an inline HSB ↔ HSL model toggle inside the tab, matching Figma's color panel. Sliders, gradients, and labels update dynamically per mode; HSL values roundtrip through RGB/HSV so no color-storage changes are needed. Model choice persists across sessions. * 💄 Fix lint errors Signed-off-by: juan-flores077 <toptalent399@gmail.com> * 🐛 Fix Plugin API token application for JS array of strings (#9166) * 🐛 Fix Plugin API token application for JS array of strings Plugin code calling `shape.applyToken(token, ["fill"])` or `token.applyToShapes([rect], ["fill"])` from JavaScript supplies a JS array of strings. The plugin proxies expected a Clojure set of keywords, and two coupled defects made the calls silently no-op (or, with `throwValidationErrors` enabled, throw "check error"): 1. `token-attr-plugin->token-attr` only consulted its alias map when the input was already a keyword. String inputs like "fill" fell through to the identity branch, so the downstream `cto/token-attr?` predicate (which checks against a set of keywords) returned false for every string. Coerce strings to keywords first. 2. The `applyToken` / `applyToShapes` / `applyToSelected` schemas used plain `[:set ...]`, which has no `:decode/json` transformer for JS array → Clojure set coercion. Switch to the registered `[::sm/set ...]` (in `app.common.schema`) which provides the array → set decoder. After the switch, the standard JSON pipeline converts `["fill"]` to `#{"fill"}`, then the inner `[:and ::sm/keyword [:fn token-attr?]]` decodes each element to a keyword and validates it. Also extends the docstring on `token-attr-plugin->token-attr` to make the string-friendly contract explicit, and registers a new `tokens-test` ns under `frontend/test/frontend_tests/plugins/` with six `deftest` blocks covering: - known keywords passing through unchanged - keyword aliases (`:r1` → `:border-radius-top-left`, etc.) - string inputs coerced to keywords (regression for #9162) - `token-attr?` accepting both keyword and string inputs - `token-attr?` rejecting unknown attrs and nil Closes #9162 * 🐛 Fix wrong direction in plugin-name alias tests The added tests in tokens_test.cljs and the new docstring in tokens.cljs described the alias resolution in the wrong direction. The map is {:r1 :border-radius-top-left, …} then map-invert'd, so token-attr-plugin->token-attr maps verbose plugin-side names (:border-radius-top-left) to canonical internal short names (:r1), not the other way around. Inputs already in canonical form (:r1, :fill, "fill", …) pass through unchanged. Flipped the alias-resolution test expectations and the keyword/string-input cases, refreshed the docstring and the regression-coverage comment to match. --------- Co-authored-by: Andrey Antukh <niwi@niwi.nz> * 💄 Fix sucess typo in subscription dialog i18n keys (#9204) Rename subscription.settings.sucess.dialog.{title,footer} to subscription.settings.success.dialog.{title,footer} in en.po and update the three callsites in subscription.cljs. Closes #9203 Signed-off-by: jack-stormentswe <crazycoder131@gmail.com> * 🐛 Fix HSVA → HSBA test rename and Prettier formatting Signed-off-by: juan-flores077 <toptalent399@gmail.com> * 🐛 Fix CI failures and address review feedback for HSB color tab Signed-off-by: juan-flores077 <toptalent399@gmail.com> * 💄 Resolve Conflicts Signed-off-by: juan-flores077 <toptalent399@gmail.com> --------- Signed-off-by: juan-flores077 <toptalent399@gmail.com> Signed-off-by: jack-stormentswe <crazycoder131@gmail.com> Signed-off-by: Andrey Antukh <niwi@niwi.nz> Co-authored-by: Andrey Antukh <niwi@niwi.nz> Co-authored-by: boskodev790 <boskomaljkovic790@outlook.com> Co-authored-by: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com>
This commit is contained in:
parent
9f94566005
commit
4902037c7d
@ -50,6 +50,7 @@
|
||||
- Preserve vector content when pasting from external tools such as Inkscape: recognise SVG sent as text/plain (with optional XML declaration and HTML comments), skip the raster preview when an SVG sibling is on the clipboard, and ignore empty SVG blobs that some tools advertise alongside the real payload, so pasted graphics arrive editable without spurious "SVG is invalid" warnings [Github #546](https://github.com/penpot/penpot/issues/546)
|
||||
- Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457)
|
||||
- Adds a **Pixel grid color** picker in the viewport settings, next to the existing canvas color control [Github #7750](https://github.com/penpot/penpot/issues/7750)
|
||||
- Add HEX, HSB and HSL support to the third color tab via an inline model switcher: relabel the existing HSVA tab as HSBA (the math was already HSB-equivalent), add an HSB ↔ HSL pill toggle that updates input labels, slider gradients and round-trip values without changing how colors are stored, and persist the chosen model across sessions [Github #9133](https://github.com/penpot/penpot/issues/9133)
|
||||
- Show specific invitation-link error messages instead of a single generic "Invite invalid" page: distinguish expired invitations, email-mismatch (signed in with the wrong account) and corrupted/invalid tokens, each with an actionable recovery hint [Github #9220](https://github.com/penpot/penpot/issues/9220)
|
||||
- Show detailed messages on file import errors to help diagnose why a file could not be imported (by @jsdevninja) [Github #9004](https://github.com/penpot/penpot/issues/9004)
|
||||
- Add read-only preview mode for saved versions — click a version name to open a dedicated preview view (by @wdeveloper16) [Github #8976](https://github.com/penpot/penpot/issues/8976)
|
||||
|
||||
@ -610,6 +610,36 @@
|
||||
[hsv]
|
||||
(-> hsv hsv->hex hex->hsl))
|
||||
|
||||
;; HSB (Hue, Saturation, Brightness) — same color model as HSV but with
|
||||
;; the brightness component normalized to a 0-100 range, matching Figma,
|
||||
;; Sketch, and Adobe XD conventions. Internally we reuse the HSV math and
|
||||
;; only rescale the brightness axis.
|
||||
|
||||
(defn rgb->hsb
|
||||
[rgb]
|
||||
(let [[h s v] (rgb->hsv rgb)]
|
||||
[h s (* (/ v 255.0) 100.0)]))
|
||||
|
||||
(defn hsb->rgb
|
||||
[[h s b]]
|
||||
(hsv->rgb [h s (int (* (/ b 100.0) 255.0))]))
|
||||
|
||||
(defn hex->hsb
|
||||
[v]
|
||||
(-> v hex->rgb rgb->hsb))
|
||||
|
||||
(defn hsb->hex
|
||||
[hsb]
|
||||
(-> hsb hsb->rgb rgb->hex))
|
||||
|
||||
(defn hsv->hsb
|
||||
[[h s v]]
|
||||
[h s (* (/ v 255.0) 100.0)])
|
||||
|
||||
(defn hsb->hsv
|
||||
[[h s b]]
|
||||
[h s (int (* (/ b 100.0) 255.0))])
|
||||
|
||||
(defn expand-hex
|
||||
[v]
|
||||
(cond
|
||||
|
||||
@ -164,3 +164,78 @@
|
||||
{:color "#ffffff" :opacity 1.0 :offset 0.5}]
|
||||
result (colors/interpolate-gradient stops 1.0)]
|
||||
(t/is (= "#ffffff" (:color result))))))
|
||||
|
||||
(t/deftest rgb-to-hsb
|
||||
;; Achromatic black: brightness 0
|
||||
(let [[h s b] (colors/rgb->hsb [0 0 0])]
|
||||
(t/is (= 0 h))
|
||||
(t/is (= 0 s))
|
||||
(t/is (mth/close? b 0.0)))
|
||||
;; Pure red: hue 0, full saturation, brightness 100
|
||||
(let [[h s b] (colors/rgb->hsb [255 0 0])]
|
||||
(t/is (mth/close? h 0.0))
|
||||
(t/is (mth/close? s 1.0))
|
||||
(t/is (mth/close? b 100.0)))
|
||||
;; Pure white: brightness 100
|
||||
(let [[_ _ b] (colors/rgb->hsb [255 255 255])]
|
||||
(t/is (mth/close? b 100.0)))
|
||||
;; Mid gray: brightness ~50.2
|
||||
(let [[_ _ b] (colors/rgb->hsb [128 128 128])]
|
||||
(t/is (mth/close? b (* (/ 128.0 255.0) 100.0)))))
|
||||
|
||||
(t/deftest hsb-to-rgb
|
||||
(t/is (= [0 0 0] (colors/hsb->rgb [0 0 0])))
|
||||
(t/is (= [255 255 255] (colors/hsb->rgb [0 0 100])))
|
||||
;; Pure red from HSB
|
||||
(let [[r g b] (colors/hsb->rgb [0 1 100])]
|
||||
(t/is (= 255 r))
|
||||
(t/is (= 0 g))
|
||||
(t/is (= 0 b))))
|
||||
|
||||
(t/deftest hex-to-hsb
|
||||
;; Black
|
||||
(let [[h s b] (colors/hex->hsb "#000000")]
|
||||
(t/is (= 0 h))
|
||||
(t/is (= 0 s))
|
||||
(t/is (mth/close? b 0.0)))
|
||||
;; White: brightness 100
|
||||
(let [[_ _ b] (colors/hex->hsb "#ffffff")]
|
||||
(t/is (mth/close? b 100.0)))
|
||||
;; Red
|
||||
(let [[h s b] (colors/hex->hsb "#ff0000")]
|
||||
(t/is (mth/close? h 0.0))
|
||||
(t/is (mth/close? s 1.0))
|
||||
(t/is (mth/close? b 100.0))))
|
||||
|
||||
(t/deftest hsb-to-hex
|
||||
(t/is (= "#000000" (colors/hsb->hex [0 0 0])))
|
||||
(t/is (= "#ffffff" (colors/hsb->hex [0 0 100]))))
|
||||
|
||||
(t/deftest hsv-hsb-roundtrip
|
||||
;; HSV brightness is 0-255, HSB brightness is 0-100. Round-trip
|
||||
;; should reach the same triple within ±1 (integer rounding).
|
||||
(let [orig [210.0 0.5 128]
|
||||
hsb (colors/hsv->hsb orig)
|
||||
result (colors/hsb->hsv hsb)]
|
||||
(t/is (mth/close? (nth orig 0) (nth result 0)))
|
||||
(t/is (mth/close? (nth orig 1) (nth result 1)))
|
||||
(t/is (< (mth/abs (- (nth orig 2) (nth result 2))) 2))))
|
||||
|
||||
(t/deftest rgb-hsb-roundtrip
|
||||
;; RGB → HSB → RGB should land within ±1 per channel
|
||||
(let [orig [100 150 200]
|
||||
hsb (colors/rgb->hsb orig)
|
||||
result (colors/hsb->rgb hsb)]
|
||||
(t/is (every? true? (map #(< (mth/abs (- %1 %2)) 2) orig result)))))
|
||||
|
||||
(t/deftest hex-hsb-roundtrip
|
||||
;; HEX → HSB → HEX should preserve the color across the model swap
|
||||
(let [orig "#fabada"
|
||||
hsb (colors/hex->hsb orig)
|
||||
result (colors/hsb->hex hsb)]
|
||||
;; Allow ±1 per channel after the round-trip due to integer rounding
|
||||
(let [[r1 g1 b1] (colors/hex->rgb orig)
|
||||
[r2 g2 b2] (colors/hex->rgb result)]
|
||||
(t/is (< (mth/abs (- r1 r2)) 2))
|
||||
(t/is (< (mth/abs (- g1 g2)) 2))
|
||||
(t/is (< (mth/abs (- b1 b2)) 2)))))
|
||||
|
||||
@ -196,7 +196,7 @@ test("Gradient stops limit", async ({ page }) => {
|
||||
});
|
||||
|
||||
// Fix for https://tree.taiga.io/project/penpot/issue/9900
|
||||
test("Bug 9900 - Color picker has no inputs for HSV values", async ({
|
||||
test("Bug 9900 - Color picker has no inputs for HSB values", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
@ -207,12 +207,12 @@ test("Bug 9900 - Color picker has no inputs for HSV values", async ({
|
||||
const swatch = workspacePage.page.getByRole("button", { name: "E8E9EA" });
|
||||
await swatch.click();
|
||||
|
||||
const HSVA = await workspacePage.page.getByLabel("HSVA");
|
||||
await HSVA.click();
|
||||
const HSBA = await workspacePage.page.getByLabel("HSBA");
|
||||
await HSBA.click();
|
||||
|
||||
await workspacePage.page.getByLabel("H", { exact: true }).isVisible();
|
||||
await workspacePage.page.getByLabel("S", { exact: true }).isVisible();
|
||||
await workspacePage.page.getByLabel("V", { exact: true }).isVisible();
|
||||
await workspacePage.page.getByLabel("B(V)", { exact: true }).isVisible();
|
||||
});
|
||||
|
||||
test("Bug 10089 - Cannot change alpha", async ({ page }) => {
|
||||
|
||||
@ -82,6 +82,15 @@
|
||||
hsl-from (cc/hsv->hsl [h 0.0 v])
|
||||
hsl-to (cc/hsv->hsl [h 1.0 v])
|
||||
|
||||
;; HSL-mode gradients. For S: fix current lightness, sweep
|
||||
;; saturation 0 → 1. For L: fix current saturation, sweep
|
||||
;; lightness 0 → 0.5 (pure hue) → 1. All computed at the
|
||||
;; current hue.
|
||||
[_ cur-hsl-s cur-hsl-l] (cc/rgb->hsl rgb)
|
||||
hsl-sat-from [h 0.0 cur-hsl-l]
|
||||
hsl-sat-to [h 1.0 cur-hsl-l]
|
||||
lightness-mid [h cur-hsl-s 0.5]
|
||||
|
||||
format-hsl (fn [[h s l]]
|
||||
(str/fmt "hsl(%s, %s, %s)"
|
||||
h
|
||||
@ -90,7 +99,10 @@
|
||||
(dom/set-css-property! node "--color" (str/join ", " rgb))
|
||||
(dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb))
|
||||
(dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from))
|
||||
(dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))))
|
||||
(dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))
|
||||
(dom/set-css-property! node "--hsl-saturation-grad-from" (format-hsl hsl-sat-from))
|
||||
(dom/set-css-property! node "--hsl-saturation-grad-to" (format-hsl hsl-sat-to))
|
||||
(dom/set-css-property! node "--lightness-grad-mid" (format-hsl lightness-mid)))))
|
||||
|
||||
(mf/defc colorpicker*
|
||||
[{:keys [data disable-gradient disable-opacity disable-image on-change on-accept origin combined-tokens color-origin on-token-change tab applied-token]}]
|
||||
@ -128,10 +140,15 @@
|
||||
active-color-tab* (hooks/use-persisted-state ::color-tab "ramp")
|
||||
active-color-tab (deref active-color-tab*)
|
||||
|
||||
;; Inline HSB/HSL toggle inside the HSBA tab — shared between
|
||||
;; the slider selector (for labels) and the numeric inputs.
|
||||
hsb-mode* (hooks/use-persisted-state ::hsb-mode :hsb)
|
||||
hsb-mode (deref hsb-mode*)
|
||||
|
||||
drag?* (mf/use-state false)
|
||||
drag? (deref drag?*)
|
||||
|
||||
type (if (= active-color-tab "hsva") :hsv :rgb)
|
||||
type (if (= active-color-tab "hsva") :hsb :rgb)
|
||||
|
||||
fill-image-ref (mf/use-ref nil)
|
||||
|
||||
@ -351,7 +368,7 @@
|
||||
{:aria-label "Harmony"
|
||||
:icon i/rgba-complementary
|
||||
:id "harmony"}
|
||||
{:aria-label "HSVA"
|
||||
{:aria-label "HSBA"
|
||||
:icon i/hsva
|
||||
:id "hsva"}])
|
||||
|
||||
@ -505,6 +522,7 @@
|
||||
[:> hsva-selector*
|
||||
{:color current-color
|
||||
:disable-opacity disable-opacity
|
||||
:mode hsb-mode
|
||||
:on-change handle-change-color
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]))]]
|
||||
@ -512,6 +530,8 @@
|
||||
[:> color-inputs*
|
||||
{:type type
|
||||
:disable-opacity disable-opacity
|
||||
:mode hsb-mode
|
||||
:on-mode-change #(reset! hsb-mode* %)
|
||||
:color current-color
|
||||
:on-change handle-change-color}]
|
||||
|
||||
|
||||
@ -28,11 +28,23 @@
|
||||
[val]
|
||||
(* (/ val 255) 100))
|
||||
|
||||
(mf/defc color-inputs* [{:keys [type color disable-opacity on-change]}]
|
||||
(mf/defc color-inputs* [{:keys [type color disable-opacity mode on-mode-change on-change]}]
|
||||
(let [{red :r green :g blue :b
|
||||
hue :h saturation :s value :v
|
||||
hex :hex alpha :alpha} color
|
||||
|
||||
;; Sub-model selector for the HSB tab: users can toggle between
|
||||
;; HSB and HSL input display without leaving the tab. State is
|
||||
;; lifted to the colorpicker parent so the slider labels stay
|
||||
;; in sync with the inputs.
|
||||
hsb-mode (or mode :hsb)
|
||||
|
||||
;; Compute HSL from current RGB (derived; not stored on the color map)
|
||||
[_hsl-h hsl-s hsl-l]
|
||||
(if (and red green blue)
|
||||
(cc/rgb->hsl [red green blue])
|
||||
[0 0 0])
|
||||
|
||||
refs {:hex (mf/use-ref nil)
|
||||
:r (mf/use-ref nil)
|
||||
:g (mf/use-ref nil)
|
||||
@ -40,6 +52,8 @@
|
||||
:h (mf/use-ref nil)
|
||||
:s (mf/use-ref nil)
|
||||
:v (mf/use-ref nil)
|
||||
:hsl-s (mf/use-ref nil)
|
||||
:hsl-l (mf/use-ref nil)
|
||||
:alpha (mf/use-ref nil)}
|
||||
|
||||
setup-hex-color
|
||||
@ -73,6 +87,7 @@
|
||||
(let [val (case property
|
||||
:s (/ val 100)
|
||||
:v (value->hsv-value val)
|
||||
(:hsl-s :hsl-l) (/ val 100)
|
||||
:alpha (/ val 100)
|
||||
val)]
|
||||
(cond
|
||||
@ -87,6 +102,18 @@
|
||||
:h h :s s :v v
|
||||
:r r :g g :b b}))
|
||||
|
||||
;; HSL changes: recompute RGB/HSV from the new HSL triple,
|
||||
;; reusing the current hue when only S or L changes.
|
||||
(#{:hsl-s :hsl-l} property)
|
||||
(let [new-s (if (= property :hsl-s) val hsl-s)
|
||||
new-l (if (= property :hsl-l) val hsl-l)
|
||||
[r g b] (cc/hsl->rgb [hue new-s new-l])
|
||||
hex (cc/rgb->hex [r g b])
|
||||
[h s v] (cc/hex->hsv hex)]
|
||||
(on-change {:hex hex
|
||||
:h h :s s :v v
|
||||
:r r :g g :b b}))
|
||||
|
||||
:else
|
||||
(let [{:keys [h s v]} (merge color (hash-map property val))
|
||||
hex (cc/hsv->hex [h s v])
|
||||
@ -126,10 +153,13 @@
|
||||
|
||||
;; Updates the inputs values when a property is changed in the parent
|
||||
(mf/use-effect
|
||||
(mf/deps color type)
|
||||
(mf/deps color type hsb-mode)
|
||||
(fn []
|
||||
(doseq [ref-key (keys refs)]
|
||||
(let [property-val (get color ref-key)
|
||||
(let [property-val (case ref-key
|
||||
:hsl-s hsl-s
|
||||
:hsl-l hsl-l
|
||||
(get color ref-key))
|
||||
property-ref (get refs ref-key)]
|
||||
(when (and property-val property-ref)
|
||||
(when-let [node (mf/ref-val property-ref)]
|
||||
@ -137,14 +167,32 @@
|
||||
(case ref-key
|
||||
(:s :alpha) (mth/precision (* property-val 100) 2)
|
||||
:v (mth/precision (hsv-value->value property-val) 2)
|
||||
(:hsl-s :hsl-l) (mth/precision (* property-val 100) 2)
|
||||
property-val)]
|
||||
(dom/set-value! node new-val))))))))
|
||||
|
||||
[:div {:class (stl/css-case :color-values true
|
||||
:disable-opacity disable-opacity)}
|
||||
|
||||
;; Inline HSB/HSL switcher — only shown on the HSB tab so that
|
||||
;; designers can pick whichever hue-based model matches their
|
||||
;; workflow (HSB matches Figma/Sketch/XD, HSL matches CSS).
|
||||
(when (and (not= type :rgb) on-mode-change)
|
||||
[:div {:class (stl/css :model-switcher)}
|
||||
[:button {:type "button"
|
||||
:class (stl/css-case :model-pill true
|
||||
:model-pill-active (= hsb-mode :hsb))
|
||||
:on-click #(on-mode-change :hsb)}
|
||||
"HSB"]
|
||||
[:button {:type "button"
|
||||
:class (stl/css-case :model-pill true
|
||||
:model-pill-active (= hsb-mode :hsl))
|
||||
:on-click #(on-mode-change :hsl)}
|
||||
"HSL"]])
|
||||
|
||||
[:div {:class (stl/css :colors-row)}
|
||||
(if (= type :rgb)
|
||||
(cond
|
||||
(= type :rgb)
|
||||
[:*
|
||||
[:div {:class (stl/css :input-wrapper)}
|
||||
[:label {:for "red-value" :class (stl/css :input-label)} "R"]
|
||||
@ -177,6 +225,42 @@
|
||||
:on-change (on-change-property :b 255)
|
||||
:on-key-down (on-key-down-property :b 255)}]]]
|
||||
|
||||
(= hsb-mode :hsl)
|
||||
[:*
|
||||
[:div {:class (stl/css :input-wrapper)}
|
||||
[:label {:for "hue-value" :class (stl/css :input-label)} "H"]
|
||||
[:input {:id "hue-value"
|
||||
:ref (:h refs)
|
||||
:type "number"
|
||||
:min 0
|
||||
:max 360
|
||||
:default-value hue
|
||||
:on-change (on-change-property :h 360)
|
||||
:on-key-down (on-key-down-property :h 360)}]]
|
||||
[:div {:class (stl/css :input-wrapper)}
|
||||
[:label {:for "hsl-saturation-value" :class (stl/css :input-label)} "S"]
|
||||
[:input {:id "hsl-saturation-value"
|
||||
:ref (:hsl-s refs)
|
||||
:type "number"
|
||||
:min 0
|
||||
:max 100
|
||||
:step 1
|
||||
:default-value (mth/precision (* hsl-s 100) 2)
|
||||
:on-change (on-change-property :hsl-s 100)
|
||||
:on-key-down (on-key-down-property :hsl-s 100)}]]
|
||||
[:div {:class (stl/css :input-wrapper)}
|
||||
[:label {:for "lightness-value" :class (stl/css :input-label)} "L"]
|
||||
[:input {:id "lightness-value"
|
||||
:ref (:hsl-l refs)
|
||||
:type "number"
|
||||
:min 0
|
||||
:max 100
|
||||
:step 1
|
||||
:default-value (mth/precision (* hsl-l 100) 2)
|
||||
:on-change (on-change-property :hsl-l 100)
|
||||
:on-key-down (on-key-down-property :hsl-l 100)}]]]
|
||||
|
||||
:else
|
||||
[:*
|
||||
[:div {:class (stl/css :input-wrapper)}
|
||||
[:label {:for "hue-value" :class (stl/css :input-label)} "H"]
|
||||
@ -200,8 +284,8 @@
|
||||
:on-change (on-change-property :s 100)
|
||||
:on-key-down (on-key-down-property :s 100)}]]
|
||||
[:div {:class (stl/css :input-wrapper)}
|
||||
[:label {:for "value-value" :class (stl/css :input-label)} "V"]
|
||||
[:input {:id "value-value"
|
||||
[:label {:for "brightness-value" :class (stl/css :input-label)} "B(V)"]
|
||||
[:input {:id "brightness-value"
|
||||
:ref (:v refs)
|
||||
:type "number"
|
||||
:min 0
|
||||
|
||||
@ -11,6 +11,36 @@
|
||||
|
||||
margin-top: deprecated.$s-8;
|
||||
|
||||
.model-switcher {
|
||||
display: flex;
|
||||
gap: deprecated.$s-4;
|
||||
margin-bottom: deprecated.$s-8;
|
||||
padding: deprecated.$s-2;
|
||||
background-color: var(--color-background-tertiary);
|
||||
border-radius: deprecated.$s-6;
|
||||
align-self: flex-start;
|
||||
|
||||
.model-pill {
|
||||
@include deprecated.body-small-typography;
|
||||
|
||||
padding: deprecated.$s-2 deprecated.$s-8;
|
||||
border: none;
|
||||
border-radius: deprecated.$s-4;
|
||||
background: transparent;
|
||||
color: var(--color-foreground-secondary);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
&.model-pill-active {
|
||||
background-color: var(--color-background-primary);
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disable-opacity {
|
||||
grid-template-columns: 3.5rem repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@ -11,17 +11,45 @@
|
||||
[app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector*]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc hsva-selector* [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}]
|
||||
(let [{hue :h saturation :s value :v alpha :alpha} color
|
||||
handle-change-slider (fn [key]
|
||||
(fn [new-value]
|
||||
(let [change (hash-map key new-value)
|
||||
{:keys [h s v]} (merge color change)
|
||||
hex (cc/hsv->hex [h s v])
|
||||
[r g b] (cc/hex->rgb hex)]
|
||||
(on-change (merge change
|
||||
{:hex hex
|
||||
:r r :g g :b b})))))
|
||||
(mf/defc hsva-selector* [{:keys [color disable-opacity mode on-change on-start-drag on-finish-drag]}]
|
||||
(let [{hue :h saturation :s value :v alpha :alpha
|
||||
r-val :r g-val :g b-val :b} color
|
||||
hsl-mode? (= mode :hsl)
|
||||
|
||||
;; Current HSL derived from RGB — used as the starting point
|
||||
;; for HSL saturation/lightness slider values and for
|
||||
;; recomputing the color when either is dragged.
|
||||
[_ hsl-s hsl-l] (if (and r-val g-val b-val)
|
||||
(cc/rgb->hsl [r-val g-val b-val])
|
||||
[0 0 0])
|
||||
|
||||
;; HSB math — current default behavior.
|
||||
handle-change-hsv
|
||||
(fn [key]
|
||||
(fn [new-value]
|
||||
(let [change (hash-map key new-value)
|
||||
{:keys [h s v]} (merge color change)
|
||||
hex (cc/hsv->hex [h s v])
|
||||
[r g b] (cc/hex->rgb hex)]
|
||||
(on-change (merge change
|
||||
{:hex hex
|
||||
:r r :g g :b b})))))
|
||||
|
||||
;; HSL math — when the user drags the S or L slider in HSL mode,
|
||||
;; we recompute RGB from the updated HSL triple and derive HSV
|
||||
;; for the canonical color representation.
|
||||
handle-change-hsl
|
||||
(fn [key]
|
||||
(fn [new-value]
|
||||
(let [new-s (if (= key :hsl-s) new-value hsl-s)
|
||||
new-l (if (= key :hsl-l) new-value hsl-l)
|
||||
[r g b] (cc/hsl->rgb [hue new-s new-l])
|
||||
hex (cc/rgb->hex [r g b])
|
||||
[h s v] (cc/hex->hsv hex)]
|
||||
(on-change {:hex hex
|
||||
:h h :s s :v v
|
||||
:r r :g g :b b}))))
|
||||
|
||||
on-change-opacity (fn [new-alpha] (on-change {:alpha new-alpha}))]
|
||||
[:div {:class (stl/css :hsva-selector)}
|
||||
[:div {:class (stl/css :hsva-row)}
|
||||
@ -31,29 +59,47 @@
|
||||
:type :hue
|
||||
:max-value 360
|
||||
:value hue
|
||||
:on-change (handle-change-slider :h)
|
||||
:on-change (handle-change-hsv :h)
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]]
|
||||
[:div {:class (stl/css :hsva-row)}
|
||||
[:span {:class (stl/css :hsva-selector-label)} "S"]
|
||||
[:> slider-selector*
|
||||
{:class (stl/css :hsva-bar)
|
||||
:type :saturation
|
||||
:max-value 1
|
||||
:value saturation
|
||||
:on-change (handle-change-slider :s)
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]]
|
||||
(if hsl-mode?
|
||||
[:> slider-selector*
|
||||
{:class (stl/css :hsva-bar)
|
||||
:type :hsl-saturation
|
||||
:max-value 1
|
||||
:value hsl-s
|
||||
:on-change (handle-change-hsl :hsl-s)
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]
|
||||
[:> slider-selector*
|
||||
{:class (stl/css :hsva-bar)
|
||||
:type :saturation
|
||||
:max-value 1
|
||||
:value saturation
|
||||
:on-change (handle-change-hsv :s)
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}])]
|
||||
[:div {:class (stl/css :hsva-row)}
|
||||
[:span {:class (stl/css :hsva-selector-label)} "V"]
|
||||
[:> slider-selector*
|
||||
{:class (stl/css :hsva-bar)
|
||||
:type :value
|
||||
:max-value 255
|
||||
:value value
|
||||
:on-change (handle-change-slider :v)
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]]
|
||||
[:span {:class (stl/css :hsva-selector-label)} (if hsl-mode? "L" "B(V)")]
|
||||
(if hsl-mode?
|
||||
[:> slider-selector*
|
||||
{:class (stl/css :hsva-bar)
|
||||
:type :lightness
|
||||
:max-value 1
|
||||
:value hsl-l
|
||||
:on-change (handle-change-hsl :hsl-l)
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]
|
||||
[:> slider-selector*
|
||||
{:class (stl/css :hsva-bar)
|
||||
:type :value
|
||||
:max-value 255
|
||||
:value value
|
||||
:on-change (handle-change-hsv :v)
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}])]
|
||||
(when (not disable-opacity)
|
||||
[:div {:class (stl/css :hsva-row)}
|
||||
[:span {:class (stl/css :hsva-selector-label)} "A"]
|
||||
|
||||
@ -53,7 +53,9 @@
|
||||
:slider-selector true
|
||||
:hue (= type :hue)
|
||||
:opacity (= type :opacity)
|
||||
:value (= type :value)))
|
||||
:value (= type :value)
|
||||
:hsl-saturation (= type :hsl-saturation)
|
||||
:lightness (= type :lightness)))
|
||||
:data-testid (when (= type :opacity) "slider-opacity")
|
||||
:on-pointer-down handle-start-drag
|
||||
:on-pointer-up handle-stop-drag
|
||||
|
||||
@ -72,6 +72,18 @@
|
||||
background: linear-gradient(var(--gradient-direction), #000 0%, #fff 100%);
|
||||
}
|
||||
|
||||
&.hsl-saturation {
|
||||
background: linear-gradient(
|
||||
var(--gradient-direction),
|
||||
var(--hsl-saturation-grad-from) 0%,
|
||||
var(--hsl-saturation-grad-to) 100%
|
||||
);
|
||||
}
|
||||
|
||||
&.lightness {
|
||||
background: linear-gradient(var(--gradient-direction), #000 0%, var(--lightness-grad-mid) 50%, #fff 100%);
|
||||
}
|
||||
|
||||
.handler {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user