diff --git a/CHANGES.md b/CHANGES.md index a6f7459acf..e65534835f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) diff --git a/common/src/app/common/types/color.cljc b/common/src/app/common/types/color.cljc index ae56250d96..f626e48ae3 100644 --- a/common/src/app/common/types/color.cljc +++ b/common/src/app/common/types/color.cljc @@ -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 diff --git a/common/test/common_tests/types/color_test.cljc b/common/test/common_tests/types/color_test.cljc index 9a3ab00ac9..deb0f24346 100644 --- a/common/test/common_tests/types/color_test.cljc +++ b/common/test/common_tests/types/color_test.cljc @@ -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))))) diff --git a/frontend/playwright/ui/specs/colorpicker.spec.js b/frontend/playwright/ui/specs/colorpicker.spec.js index 225c7da433..75dad9c97f 100644 --- a/frontend/playwright/ui/specs/colorpicker.spec.js +++ b/frontend/playwright/ui/specs/colorpicker.spec.js @@ -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 }) => { diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index cf9af4aacd..7b4c5bfa62 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -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}] diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs index 09ad2d0e8c..69c466669c 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss index fe5b93d679..3a3034bc95 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss @@ -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); } diff --git a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs index 807d976314..802f59c8c2 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs @@ -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"] diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs index f125b6368b..7021db6767 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss index 4473eaface..09b9942e22 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss @@ -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%;