From 44f6c2f83ce97b5b4b1beef7a07045b74e98116e Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 3 Oct 2025 11:19:01 +0200 Subject: [PATCH] :tada: Show tokens on color inputs (#7377) * :tada: Add tokens to color row * :tada: Add color-token to stroke input * :bug: FIx change token on multiselection with groups * :wrench: Add config flag * :bug: Fix comments --- common/src/app/common/types/token.cljc | 4 +- .../playwright/ui/specs/colorpicker.spec.js | 4 +- frontend/playwright/ui/specs/tokens.spec.js | 40 +- .../main/ui/components/reorder_handler.scss | 14 +- frontend/src/app/main/ui/ds/_sizes.scss | 1 - .../src/app/main/ui/ds/product/loader.scss | 6 +- frontend/src/app/main/ui/ds/spacing.scss | 4 - .../src/app/main/ui/ds/utilities/swatch.cljs | 11 +- .../src/app/main/ui/ds/utilities/swatch.scss | 5 +- .../app/main/ui/workspace/colorpicker.cljs | 20 +- .../workspace/colorpicker/color_tokens.cljs | 17 +- .../ui/workspace/colorpicker/gradients.cljs | 6 +- .../src/app/main/ui/workspace/libraries.scss | 2 +- .../main/ui/workspace/sidebar/options.cljs | 2 +- .../options/menus/color_selection.cljs | 1 + .../workspace/sidebar/options/menus/fill.cljs | 48 ++- .../sidebar/options/menus/stroke.cljs | 18 +- .../sidebar/options/rows/color_row.cljs | 331 +++++++++++----- .../sidebar/options/rows/color_row.scss | 358 ++++++++++++------ .../sidebar/options/rows/stroke_row.cljs | 62 ++- .../sidebar/options/shapes/bool.cljs | 11 +- .../sidebar/options/shapes/circle.cljs | 9 +- .../sidebar/options/shapes/frame.cljs | 8 +- .../sidebar/options/shapes/group.cljs | 19 +- .../sidebar/options/shapes/multiple.cljs | 59 ++- .../sidebar/options/shapes/path.cljs | 8 +- .../sidebar/options/shapes/rect.cljs | 8 +- .../sidebar/options/shapes/svg_raw.cljs | 8 +- .../sidebar/options/shapes/text.cljs | 8 +- frontend/translations/en.po | 6 +- frontend/translations/es.po | 6 +- 31 files changed, 789 insertions(+), 315 deletions(-) diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index a83d5b919a..41b0149e44 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -534,4 +534,6 @@ :sided-margins #{:spacing :dimensions} :line-height #{:line-height :number} :font-size #{:font-size} - :letter-spacing #{:letter-spacing}}) + :letter-spacing #{:letter-spacing} + :fill #{:color} + :stroke-color #{:color}}) diff --git a/frontend/playwright/ui/specs/colorpicker.spec.js b/frontend/playwright/ui/specs/colorpicker.spec.js index db2e3ccdfe..535cbc3a47 100644 --- a/frontend/playwright/ui/specs/colorpicker.spec.js +++ b/frontend/playwright/ui/specs/colorpicker.spec.js @@ -93,7 +93,7 @@ test("Create a LINEAR gradient", async ({ page }) => { await expect(inputOpacityGlobal).toHaveValue("50"); await expect(inputOpacityGlobal).toBeVisible(); - await expect(workspacePage.page.getByText("Linear gradient")).toBeVisible(); + await expect(workspacePage.page.getByText("Linear gradient").nth(1)).toBeVisible(); }); test("Create a RADIAL gradient", async ({ page }) => { @@ -175,7 +175,7 @@ test("Create a RADIAL gradient", async ({ page }) => { await expect(inputOpacityGlobal).toHaveValue("50"); await expect(inputOpacityGlobal).toBeVisible(); - await expect(workspacePage.page.getByText("Radial gradient")).toBeVisible(); + await expect(workspacePage.page.getByText("Radial gradient").nth(1)).toBeVisible(); }); test("Gradient stops limit", async ({ page }) => { diff --git a/frontend/playwright/ui/specs/tokens.spec.js b/frontend/playwright/ui/specs/tokens.spec.js index f5dd883cfd..19f5f6594b 100644 --- a/frontend/playwright/ui/specs/tokens.spec.js +++ b/frontend/playwright/ui/specs/tokens.spec.js @@ -46,7 +46,7 @@ const setupTokensFile = async (page, options = {}) => { } = options; const workspacePage = new WorkspacePage(page); - if (flags.length) { + if (flags.length > 0) { await workspacePage.mockConfigFlags(flags); } @@ -879,7 +879,10 @@ test.describe("Tokens: Themes modal", () => { }); test.describe("Tokens: Apply token", () => { - test("User applies color token to a shape", async ({ page }) => { + // When deleting the "enable-token-color" flag, permanently remove this test. + test("User applies color token to a shape without tokens on design-tab", async ({ + page, + }) => { const { workspacePage, tokensSidebar, tokenContextMenuForToken } = await setupTokensFile(page); @@ -909,6 +912,35 @@ test.describe("Tokens: Themes modal", () => { await expect(inputColor).toHaveValue("000000"); }); + test("User applies color token to a shape", async ({ page }) => { + const { workspacePage, tokensSidebar, tokenContextMenuForToken } = + await setupTokensFile(page, { flags: ["enable-token-color"] }); + + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers + .getByTestId("layer-row") + .filter({ hasText: "Button" }) + .click(); + + const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); + await tokensTabButton.click(); + + await tokensSidebar + .getByRole("button") + .filter({ hasText: "Color" }) + .click(); + + await tokensSidebar + .getByRole("button", { name: "colors.black" }) + .click({ button: "right" }); + await tokenContextMenuForToken.getByText("Fill").click(); + + await expect( + workspacePage.page.getByLabel("Name: colors.black"), + ).toBeVisible(); + }); + test("User applies typography token to a text shape", async ({ page }) => { const { workspacePage, tokensSidebar, tokenContextMenuForToken } = await setupTypographyTokensFile(page); @@ -1024,9 +1056,7 @@ test.describe("Tokens: Themes modal", () => { await expect(letterSpacingField).toHaveValue( originalValues.letterSpacing, ); - await expect(lineHeightField).toHaveValue( - originalValues.lineHeight, - ); + await expect(lineHeightField).toHaveValue(originalValues.lineHeight); await expect(textCaseField).toHaveValue(originalValues.textCase); await expect(textDecorationField).toHaveValue( originalValues.textDecoration, diff --git a/frontend/src/app/main/ui/components/reorder_handler.scss b/frontend/src/app/main/ui/components/reorder_handler.scss index 692a24c156..499ff56ad5 100644 --- a/frontend/src/app/main/ui/components/reorder_handler.scss +++ b/frontend/src/app/main/ui/components/reorder_handler.scss @@ -8,16 +8,16 @@ cursor: grab; display: flex; flex-direction: column; - height: calc(100% + var(--sp-m)); + block-size: calc(100% + var(--sp-m)); justify-content: center; - left: var(--reorder-left-position, calc(-1 * var(--sp-l))); + inset-inline-start: var(--reorder-left-position, -11px); position: absolute; - top: calc(-1 * (var(--sp-m) / 2)); + inset-block-start: calc(-1 * (var(--sp-m) / 2)); z-index: var(--z-index-panels); } .reorder-icon { - height: var(--sp-l); + block-size: var(--sp-l); pointer-events: none; visibility: var(--reorder-icon-visibility, hidden); --icon-stroke-color: var(--color-foreground-secondary); @@ -28,15 +28,15 @@ border-color: var(--color-accent-primary); margin: 0; position: absolute; - width: 100%; + inline-size: 100%; } .reorder-separator-top { display: var(--reorder-top-display, none); - top: calc(-1 * var(--sp-xxs)); + inset-block-start: calc(-1 * var(--sp-xxs)); } .reorder-separator-bottom { display: var(--reorder-bottom-display, none); - bottom: calc(-1 * var(--sp-xxs)); + inset-block-end: calc(-1 * var(--sp-xxs)); } diff --git a/frontend/src/app/main/ui/ds/_sizes.scss b/frontend/src/app/main/ui/ds/_sizes.scss index b06a80882b..17afdf4b20 100644 --- a/frontend/src/app/main/ui/ds/_sizes.scss +++ b/frontend/src/app/main/ui/ds/_sizes.scss @@ -18,7 +18,6 @@ $sz-32: px2rem(32); $sz-36: px2rem(36); $sz-40: px2rem(40); $sz-48: px2rem(48); -$sz-80: px2rem(80); $sz-88: px2rem(88); $sz-120: px2rem(120); $sz-154: px2rem(154); diff --git a/frontend/src/app/main/ui/ds/product/loader.scss b/frontend/src/app/main/ui/ds/product/loader.scss index 4eb1f3abde..772049d573 100644 --- a/frontend/src/app/main/ui/ds/product/loader.scss +++ b/frontend/src/app/main/ui/ds/product/loader.scss @@ -4,6 +4,8 @@ // // Copyright (c) KALEIDOS INC +@use "ds/_utils.scss" as *; + @keyframes line-pencil { 0% { transform: translateY(0); @@ -44,8 +46,8 @@ // Tips container .tips-container { text-align: center; - min-width: var(--sp-600); - max-width: var(--sp-800); + min-width: px2rem(600); + max-width: px2rem(800); display: flex; flex-direction: column; gap: var(--sp-s); diff --git a/frontend/src/app/main/ui/ds/spacing.scss b/frontend/src/app/main/ui/ds/spacing.scss index 2c242a056b..796699dd27 100644 --- a/frontend/src/app/main/ui/ds/spacing.scss +++ b/frontend/src/app/main/ui/ds/spacing.scss @@ -14,8 +14,6 @@ $_sp-16: px2rem(16); $_sp-20: px2rem(20); $_sp-24: px2rem(24); $_sp-32: px2rem(32); -$_sp-600: px2rem(600); -$_sp-800: px2rem(800); :global(:root) { --sp-xxs: #{$_sp-2}; @@ -26,6 +24,4 @@ $_sp-800: px2rem(800); --sp-xl: #{$_sp-20}; --sp-xxl: #{$_sp-24}; --sp-xxxl: #{$_sp-32}; - --sp-600: #{$_sp-600}; - --sp-800: #{$_sp-800}; } diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.cljs b/frontend/src/app/main/ui/ds/utilities/swatch.cljs index 214166d275..8eda95fd3a 100644 --- a/frontend/src/app/main/ui/ds/utilities/swatch.cljs +++ b/frontend/src/app/main/ui/ds/utilities/swatch.cljs @@ -63,11 +63,12 @@ [:class {:optional true} :string] [:size {:optional true} [:enum "small" "medium" "large"]] [:active {:optional true} ::sm/boolean] + [:has-errors {:optional true} [:maybe ::sm/boolean]] [:on-click {:optional true} ::sm/fn]]) (mf/defc swatch* {::mf/schema (sm/schema schema:swatch)} - [{:keys [background on-click size active class tooltip-content] + [{:keys [background on-click size active class tooltip-content has-errors] :rest props}] (let [;; NOTE: this code is only relevant for storybook, because ;; storybook is unable to pass in a comfortable way a complex @@ -92,6 +93,12 @@ image (:image background) format (if id? "rounded" "square") element-id (mf/use-id) + on-click + (mf/use-fn + (mf/deps background on-click) + (fn [event] + (when (fn? on-click) + (^function on-click background event)))) class (dm/str class " " (stl/css-case @@ -124,6 +131,8 @@ (let [uri (cfg/resolve-file-media image)] [:span {:class (stl/css :swatch-image) :style {:background-image (str/ffmt "url(%)" uri)}}]) + has-errors + [:span {:class (stl/css :swatch-error)}] :else [:span {:class (stl/css :swatch-opacity)} diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.scss b/frontend/src/app/main/ui/ds/utilities/swatch.scss index 2fdf815472..37ffe33e4e 100644 --- a/frontend/src/app/main/ui/ds/utilities/swatch.scss +++ b/frontend/src/app/main/ui/ds/utilities/swatch.scss @@ -20,7 +20,6 @@ border: $b-1 solid var(--border-color); border-radius: var(--border-radius); overflow: hidden; - &:focus { --border-color: var(--color-accent-primary); } @@ -109,3 +108,7 @@ flex: 1; display: block; } + +.swatch-error { + background: var(--color-background-primary); +} diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 6964d194c8..76d075edf6 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -47,7 +47,8 @@ [cuerdas.core :as str] [okulary.core :as l] [potok.v2.core :as ptk] - [rumext.v2 :as mf])) + [rumext.v2 :as mf] + [rumext.v2.util :as mfu])) ;; --- Refs @@ -93,13 +94,13 @@ (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))))) (mf/defc colorpicker - [{:keys [data disable-gradient disable-opacity disable-image on-change on-accept origin combined-tokens color-origin on-token-change]}] + [{:keys [data disable-gradient disable-opacity disable-image on-change on-accept origin combined-tokens color-origin on-token-change tab]}] (let [state (mf/deref refs/colorpicker) node-ref (mf/use-ref) should-update? (mf/use-var true) token-color (contains? cfg/flags :token-color) - color-style* (mf/use-state :direct-color) + color-style* (mf/use-state (d/nilv tab :direct-color)) color-style (deref color-style*) toggle-token-color (mf/use-fn @@ -364,7 +365,7 @@ :icon i/hsva :id "hsva"}]) - show-tokens? (contains? #{:fill :stroke :color-selection} color-origin)] + show-tokens? (contains? #{:fill :stroke-color :color-selection} color-origin)] ;; Initialize colorpicker state (mf/with-effect [] @@ -726,12 +727,16 @@ color-origin on-token-change on-close + tab on-accept]}] (let [vport (mf/deref viewport) dirty? (mf/use-var false) last-change (mf/use-var nil) position (d/nilv position :left) style (calculate-position vport position x y (some? (:gradient data))) + active-tokens (if (object? active-tokens) + (mfu/bean active-tokens) + active-tokens) on-change' (mf/use-fn @@ -752,6 +757,10 @@ (some-> tokens-lib (ctob/get-active-themes-set-names))) + active-tokens (if (delay? active-tokens) + @active-tokens + active-tokens) + color-tokens (:color active-tokens) grouped-tokens-by-set @@ -762,7 +771,7 @@ (filter-active-sets active-sets-names) (filter-non-empty-sets) (group-sets) - (combine-groups-with-resolved color-tokens)))] + (combine-groups-with-resolved color-tokens)))] (mf/with-effect [] (st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts))) @@ -783,5 +792,6 @@ :on-token-change on-token-change :on-change on-change' :origin origin + :tab tab :color-origin color-origin :on-accept on-accept}]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs index 605680ca67..bbe16bfffc 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs @@ -135,24 +135,11 @@ on-token-pill-click (mf/use-fn - (mf/deps selected-shapes selected color-origin) + (mf/deps selected-shapes) (fn [event token] (dom/stop-propagation event) (when (seq selected-shapes) - (if (= :color-selection color-origin) - (on-token-change event token) - (let [attributes (if (= color-origin :stroke) #{:stroke-color} #{:fill}) - shape-ids (into #{} (map :id selected-shapes))] - (if (or - (and (= (:name token) has-stroke-tokens?) (= color-origin :stroke)) - (and (= (:name token) has-color-tokens?) (= color-origin :fill))) - (st/emit! (dwta/unapply-token {:attributes attributes - :token token - :shape-ids shape-ids})) - (st/emit! (dwta/apply-token {:shape-ids shape-ids - :attributes attributes - :token token - :on-update-shape dwta/update-fill-stroke})))))))) + (on-token-change event token)))) create-token-on-set (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs index 39263a1d5d..b88bb3042c 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs @@ -52,6 +52,10 @@ (str/join ", ") (str/ffmt "linear-gradient(90deg, %1)"))) +(defn- stop->hex-color + [stop] + (select-keys stop [:color :opacity])) + (mf/defc stop-input-row* {::mf/private true} [{:keys [stop @@ -156,7 +160,7 @@ [:> color-row* {:disable-gradient true :disable-picker true - :color stop + :color (stop->hex-color stop) :index index :origin :gradient :on-change handle-change-stop-color diff --git a/frontend/src/app/main/ui/workspace/libraries.scss b/frontend/src/app/main/ui/workspace/libraries.scss index c604371535..2cd2a047b2 100644 --- a/frontend/src/app/main/ui/workspace/libraries.scss +++ b/frontend/src/app/main/ui/workspace/libraries.scss @@ -454,7 +454,7 @@ .sample-library-button { @include t.use-typography("headline-small"); height: $sz-32; - width: $sz-80; + width: px2rem(80); margin: 0; border-radius: $br-8; white-space: nowrap; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 37510ba166..1b73dbbc34 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -46,7 +46,7 @@ (mf/defc single-shape-options* {::mf/private true} - [{:keys [shape page-id file-id libraries] :as props}] + [{:keys [shape page-id file-id libraries] :rest props}] (let [shape-type (dm/get-prop shape :type) shape-id (dm/get-prop shape :id) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs index b9963e67d8..a785230dac 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs @@ -108,6 +108,7 @@ color-operations (get groups color) ids (into (d/ordered-set) xf:map-shape-id color-operations)] (st/emit! (dws/select-shapes ids))))) + on-token-change (mf/use-fn (fn [_ token old-color] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 6d6293c451..a9d4d194aa 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -13,6 +13,7 @@ [app.config :as cfg] [app.main.data.workspace :as udw] [app.main.data.workspace.colors :as dc] + [app.main.data.workspace.tokens.application :as dwta] [app.main.store :as st] [app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] @@ -68,16 +69,24 @@ n-vals (unchecked-get n-props "values") o-fills (get o-vals :fills) n-fills (get n-vals :fills) + o-objects (get o-vals :objects) + n-objects (get n-vals :objects) + o-applied-tokens (get o-vals :applied-tokens) + n-applied-tokens (get n-vals :applied-tokens) o-hide (get o-vals :hide-fill-on-export) n-hide (get n-vals :hide-fill-on-export)] (and (identical? o-hide n-hide) - (identical? o-fills n-fills))))) + (identical? o-applied-tokens n-applied-tokens) + (identical? o-fills n-fills) + (identical? o-objects n-objects))))) (mf/defc fill-menu* {::mf/wrap [#(mf/memo' % check-props)]} - [{:keys [ids type values]}] + [{:keys [ids type values applied-tokens shapes objects]}] + (let [fills (get values :fills) hide-on-export (get values :hide-fill-on-export false) + fill-token-applied (:fill applied-tokens) ^boolean multiple? (= :multiple fills) @@ -172,7 +181,37 @@ #(reset! disable-drag* true)) on-blur - (mf/use-fn #(reset! disable-drag* false))] + (mf/use-fn #(reset! disable-drag* false)) + + on-token-change + (mf/use-fn + (mf/deps shapes objects) + (fn [_ token] + (let [expanded-shapes + (if (= 1 (count shapes)) + (let [shape (first shapes)] + (if (= (:type shape) :group) + (keep objects (:shapes shape)) + [shape])) + + (mapcat (fn [shape] + (if (= (:type shape) :group) + (keep objects (:shapes shape)) + [shape])) + shapes))] + + (st/emit! + (dwta/toggle-token {:token token + :attrs #{:fill} + :shapes expanded-shapes}))))) + + on-detach-token + (mf/use-fn + (mf/deps ids) + (fn [token] + (st/emit! (dwta/unapply-token {:attributes #{:fill} + :token token + :shape-ids ids}))))] (mf/with-layout-effect [hide-on-export] (when-let [checkbox (mf/ref-val checkbox-ref)] @@ -223,9 +262,12 @@ :on-change on-change :on-reorder on-reorder :on-detach on-detach + :on-detach-token on-detach-token :on-remove on-remove :disable-drag disable-drag? :on-focus on-focus + :applied-token fill-token-applied + :on-token-change on-token-change :origin :fill :select-on-focus (not disable-drag?) :on-blur on-blur}]))]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index 9ef4f7a658..a7b2403333 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -12,6 +12,7 @@ [app.common.types.stroke :as cts] [app.main.data.workspace :as udw] [app.main.data.workspace.colors :as dc] + [app.main.data.workspace.tokens.application :as dwta] [app.main.store :as st] [app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] @@ -37,8 +38,8 @@ :stroke-cap-end]) (mf/defc stroke-menu - {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "show-caps"]))]} - [{:keys [ids type values show-caps disable-stroke-style] :as props}] + {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "show-caps" "applied-tokens" "shapes" "objects"]))]} + [{:keys [ids type values show-caps disable-stroke-style applied-tokens shapes objects] :as props}] (let [label (case type :multiple (tr "workspace.options.selection-stroke") :group (tr "workspace.options.group-stroke") @@ -167,7 +168,14 @@ (reset! disable-drag true)) on-blur (fn [_] - (reset! disable-drag false))] + (reset! disable-drag false)) + on-detach-token + (mf/use-fn + (mf/deps ids) + (fn [token attrs] + (st/emit! (dwta/unapply-token {:attributes attrs + :token token + :shape-ids ids}))))] [:div {:class (stl/css :element-set)} [:div {:class (stl/css :element-title)} @@ -201,6 +209,8 @@ :stroke value :title (tr "workspace.options.stroke-color") :index index + :shapes shapes + :objects objects :show-caps show-caps :on-color-change on-color-change :on-color-detach on-color-detach @@ -212,6 +222,8 @@ :on-stroke-cap-start-change on-stroke-cap-start-change :on-stroke-cap-end-change on-stroke-cap-end-change :on-stroke-cap-switch on-stroke-cap-switch + :applied-tokens applied-tokens + :on-detach-token on-detach-token :on-remove on-remove :on-reorder (handle-reorder index) :disable-drag disable-drag diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 9e1dca9dcc..8ac990deec 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -11,28 +11,27 @@ [app.common.data.macros :as dm] [app.common.types.color :as clr] [app.common.types.shape.attrs :refer [default-color]] + [app.common.types.token :as tk] + [app.config :as cfg] [app.main.data.modal :as modal] [app.main.data.workspace.colors :as dwc] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.color-bullet :as cb] [app.main.ui.components.color-input :refer [color-input*]] [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.reorder-handler :refer [reorder-handler*]] [app.main.ui.context :as ctx] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.ds.tooltip.tooltip :refer [tooltip*]] + [app.main.ui.ds.utilities.swatch :refer [swatch*]] [app.main.ui.formats :as fmt] [app.main.ui.hooks :as h] - [app.main.ui.icons :as deprecated-icon] [app.util.color :as uc] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) -(def ^:private detach-icon - (deprecated-icon/icon-xref :detach (stl/css :detach-icon))) - (defn opacity->string [opacity] (if (= opacity :multiple) @@ -42,45 +41,139 @@ (* 100) (fmt/format-number))))) -(defn remove-multiple - [v] - (if (= v :multiple) nil v)) +(mf/defc color-info-wrapper* + {::mf/private true} + [{:keys [color class handle-click-color children select-on-focus opacity on-focus on-blur on-opacity-change]}] + [:div {:class (stl/css :color-info)} + [:div {:class class} + [:div {:class (stl/css :color-bullet-wrapper)} + [:> swatch* {:background color + :on-click handle-click-color + :size "small"}]] + children] + (when opacity + [:div {:class (stl/css :opacity-element-wrapper)} + [:span {:class (stl/css :icon-text)} "%"] + [:> numeric-input* {:value (-> color :opacity opacity->string) + :class (stl/css :opacity-input) + :placeholder "--" + :select-on-focus select-on-focus + :on-focus on-focus + :on-blur on-blur + :on-change on-opacity-change + :data-testid "opacity-input" + :default 100 + :min 0 + :max 100}]])]) + +(mf/defc color-token-row* + {::mf/private true} + [{:keys [active-tokens color-token color on-swatch-click-token detach-token open-modal-from-token]}] + (let [;; `active-tokens` may be provided as a `delay` (lazy computation). + ;; In that case we must deref it (`@active-tokens`) to force evaluation + ;; and obtain the actual value. If it’s already realized (not a delay), + ;; we just use it directly. + active-tokens (if (delay? active-tokens) + @active-tokens + active-tokens) + + color-tokens (:color active-tokens) + + token (some #(when (= (:name %) color-token) %) color-tokens) + + on-detach-token + (mf/use-fn + (mf/deps detach-token token) + #(detach-token token)) + + has-errors (some? (:errors token)) + token-name (:name token) + resolved (:resolved-value token) + not-active (and (some? active-tokens) (nil? token)) + id (dm/str (:id token) "-name") + swatch-tooltip-content (cond + not-active + (tr "ds.inputs.token-field.no-active-token-option") + has-errors + (tr "color-row.token-color-row.deleted-token") + :else + (tr "workspace.tokens.resolved-value" resolved)) + name-tooltip-content (cond + not-active + (tr "ds.inputs.token-field.no-active-token-option") + has-errors + (tr "color-row.token-color-row.deleted-token") + :else + (mf/html + [:div + [:span (dm/str (tr "workspace.tokens.token-name") ": ")] + [:span {:class (stl/css :token-name-tooltip)} color-token]]))] + + [:div {:class (stl/css :color-info)} + [:div {:class (stl/css-case :token-color-wrapper true + :token-color-with-errors has-errors + :token-color-not-active not-active)} + [:div {:class (stl/css :color-bullet-wrapper)} + (when (or has-errors not-active) + [:div {:class (stl/css :error-dot)}]) + [:> swatch* {:background color + :tooltip-content swatch-tooltip-content + :on-click on-swatch-click-token + :has-errors (or has-errors not-active) + :size "small"}]] + [:> tooltip* {:content name-tooltip-content + :id id + :class (stl/css :token-tooltip)} + [:div {:class (stl/css :token-name) + :aria-labelledby id} + (or token-name color-token)]] + [:div {:class (stl/css :token-actions)} + [:> icon-button* + {:variant "action" + :aria-label (tr "ds.inputs.token-field.detach-token") + :on-click on-detach-token + :icon i/detach}] + [:> icon-button* + {:variant "action" + :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown") + :on-click open-modal-from-token + :icon i/tokens}]]]])) (mf/defc color-row* [{:keys [index color class disable-gradient disable-opacity disable-image disable-picker hidden - on-change on-reorder on-detach on-open on-close on-remove origin - disable-drag on-focus on-blur select-only select-on-focus on-token-change]}] - (let [libraries (mf/deref refs/files) + on-change on-reorder on-detach on-open on-close on-remove origin on-detach-token + disable-drag on-focus on-blur select-only select-on-focus on-token-change applied-token]}] + (let [token-color (contains? cfg/flags :token-color) + libraries (mf/deref refs/files) on-change (h/use-ref-callback on-change) on-token-change (h/use-ref-callback on-token-change) + color-without-hash (mf/use-memo + (mf/deps color) + #(-> color :color clr/remove-hash)) file-id (or (:ref-file color) (:file-id color)) color-id (or (:ref-id color) (:id color)) src-colors (dm/get-in libraries [file-id :data :colors]) color-name (dm/get-in src-colors [color-id :name]) - multiple-colors? (uc/multiple? color) - library-color? (and (or (:id color) (:ref-id color)) color-name (not multiple-colors?)) - gradient-color? (and (not multiple-colors?) + has-multiple-colors (uc/multiple? color) + library-color? (and (or (:id color) (:ref-id color)) color-name (not has-multiple-colors)) + gradient-color? (and (not has-multiple-colors) (:gradient color) (dm/get-in color [:gradient :type])) - image-color? (and (not multiple-colors?) + image-color? (and (not has-multiple-colors) (:image color)) editing-text* (mf/use-state false) - editing-text? (deref editing-text*) - - class (if (some? class) (dm/str class " ") "") + is-editing-text (deref editing-text*) active-tokens* (mf/use-ctx ctx/active-tokens-by-type) - active-tokens (if active-tokens* - @active-tokens* - {}) - opacity? - (and (not multiple-colors?) - (not library-color?) - (not disable-opacity)) + tokens (mf/with-memo [active-tokens* origin] + (delay + (-> (deref active-tokens*) + (select-keys (get tk/tokens-by-input origin)) + (not-empty)))) on-focus' (mf/use-fn @@ -138,12 +231,12 @@ (st/emit! (dwc/add-recent-color color) (on-change color index))))) - handle-click-color + open-modal (mf/use-fn - (mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open active-tokens) - (fn [color event] + (mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open tokens) + (fn [color pos tab] (let [color (cond - multiple-colors? + has-multiple-colors {:color default-color :opacity 1} @@ -152,13 +245,8 @@ :else color) - - cpos (dom/get-client-position event) - x (dm/get-prop cpos :x) - y (dm/get-prop cpos :y) - - props {:x x - :y y + props {:x (:x pos) + :y (:y pos) :disable-gradient disable-gradient :disable-opacity disable-opacity :disable-image disable-image @@ -168,8 +256,9 @@ :on-close (fn [value opacity id file-id] (when on-close (on-close value opacity id file-id))) - :active-tokens active-tokens + :active-tokens tokens :color-origin origin + :tab tab :origin :sidebar :data color}] @@ -179,6 +268,38 @@ (when-not disable-picker (modal/show! :colorpicker props))))) + handle-click-color + (mf/use-fn + (mf/deps open-modal) + (fn [color event] + (let [cpos (dom/get-client-position event)] + (open-modal color cpos nil)))) + + open-modal-from-token + (mf/use-fn + (mf/deps open-modal color) + (fn [event] + (let [cpos (dom/get-client-position event) + x (:x cpos) + y (:y cpos) + pos {:x (- x 215) + :y y}] + (open-modal color pos :token-color)))) + + on-swatch-click-token + (mf/use-fn + (mf/deps open-modal) + (fn [color event] + (let [cpos (dom/get-client-position event)] + (open-modal color cpos :token-color)))) + + detach-token + (mf/use-fn + (mf/deps on-detach-token) + (fn [token] + (when on-detach-token + (on-detach-token token)))) + on-remove' (mf/use-fn (mf/deps index) @@ -207,88 +328,95 @@ :name (str "Color row" index)}) [nil nil]) - row-class (stl/css-case :color-data true :hidden hidden :dnd-over-top (= (:over dprops) :top) :dnd-over-bot (= (:over dprops) :bot))] - (mf/with-effect [color prev-color disable-picker] (when (and (not disable-picker) (not= prev-color color)) (modal/update-props! :colorpicker {:data (parse-color color)}))) [:div {:class [class row-class]} - ;; Drag handler (when (some? on-reorder) [:> reorder-handler* {:ref dref}]) + (cond + (and token-color applied-token) + [:> color-token-row* {:active-tokens tokens + :color-token applied-token + :color (dissoc color :ref-id :ref-file) + :on-swatch-click-token on-swatch-click-token + :detach-token detach-token + :open-modal-from-token open-modal-from-token}] - [:div {:class (stl/css :color-info)} - [:div {:class (stl/css-case :color-name-wrapper true - :no-opacity (or disable-opacity - (not opacity?)) - :library-name-wrapper library-color? - :editing editing-text? - :gradient-name-wrapper gradient-color?)} - [:div {:class (stl/css :color-bullet-wrapper)} - [:& cb/color-bullet {:color (cond-> color - (nil? color-name) (dissoc :ref-id :ref-file)) - :mini true - :on-click handle-click-color}]] - (cond - ;; Rendering a color with ID - library-color? - [:* - [:div {:class (stl/css :color-name) - :title (str color-name)} + library-color? + [:> color-info-wrapper* {:class (stl/css-case :color-name-wrapper true + :library-name-wrapper true) + :handle-click-color handle-click-color + :color color} + [:* + [:div {:class (stl/css :color-name) + :title (str color-name)} + (str color-name)] + [:> icon-button* + {:variant "ghost" + :class (stl/css :detach-btn) + :aria-label (tr "settings.detach") + :on-click detach-value + :icon i/detach}]]] - (str color-name)] - (when on-detach - [:button - {:class (stl/css :detach-btn) - :title (tr "settings.detach") - :on-click detach-value} - detach-icon])] + gradient-color? + [:> color-info-wrapper* {:class (stl/css-case :color-name-wrapper true + :no-opacity disable-opacity + :gradient-name-wrapper true) + :handle-click-color handle-click-color + :color color + :opacity true + :select-on-focus select-on-focus + :on-focus on-focus' + :on-blur on-blur' + :on-opacity-change on-opacity-change} + [:div {:class (stl/css :color-name)} + (uc/gradient-type->string (dm/get-in color [:gradient :type]))]] - ;; Rendering a gradient - gradient-color? - [:div {:class (stl/css :color-name)} - (uc/gradient-type->string (dm/get-in color [:gradient :type]))] + image-color? + [:> color-info-wrapper* {:class (stl/css-case :color-name-wrapper true + :no-opacity disable-opacity) + :handle-click-color handle-click-color + :color color + :opacity true + :select-on-focus select-on-focus + :on-focus on-focus' + :on-blur on-blur' + :on-opacity-change on-opacity-change} + [:div {:class (stl/css :color-name)} + (tr "media.image")]] - ;; Rendering an image - image-color? - [:div {:class (stl/css :color-name)} - (tr "media.image")] + :else + [:> color-info-wrapper* {:class (stl/css-case :color-name-wrapper true + :no-opacity (or disable-opacity + has-multiple-colors) + :editing is-editing-text) + :handle-click-color handle-click-color + :color color + :opacity true + :select-on-focus select-on-focus + :on-focus on-focus' + :on-blur on-blur' + :on-opacity-change on-opacity-change} - ;; Rendering a plain color - :else - [:span {:class (stl/css :color-input-wrapper)} - [:> color-input* {:value (if multiple-colors? - "" - (-> color :color clr/remove-hash)) - :placeholder (tr "settings.multiple") - :data-index index - :class (stl/css :color-input) - :on-focus on-focus' - :on-blur on-blur' - :on-change on-color-change}]])] - - (when opacity? - [:div {:class (stl/css :opacity-element-wrapper)} - [:span {:class (stl/css :icon-text)} "%"] - [:> numeric-input* {:value (-> color :opacity opacity->string) - :class (stl/css :opacity-input) - :placeholder "--" - :select-on-focus select-on-focus - :on-focus on-focus' - :on-blur on-blur' - :on-change on-opacity-change - :data-testid "opacity-input" - :default 100 - :min 0 - :max 100}]])] + [:span {:class (stl/css :color-input-wrapper)} + [:> color-input* {:value (if has-multiple-colors + "" + color-without-hash) + :placeholder (tr "settings.multiple") + :data-index index + :class (stl/css :color-input) + :on-focus on-focus' + :on-blur on-blur' + :on-change on-color-change}]]]) (when (some? on-remove) [:> icon-button* {:variant "ghost" @@ -299,5 +427,4 @@ [:> icon-button* {:variant "ghost" :aria-label (tr "settings.select-this-color") :on-click handle-select - :icon i/move}])])) - + :icon i/move}])])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss index 719d188409..98e71e9794 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss @@ -4,15 +4,18 @@ // // Copyright (c) KALEIDOS INC -@use "refactor/common-refactor.scss" as deprecated; +@use "ds/typography.scss" as t; +@use "ds/_sizes.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/mixins.scss" as *; +@use "ds/_utils.scss" as *; .color-data { - @include deprecated.flexRow; - + display: flex; + align-items: center; + gap: var(--sp-xs); position: relative; - --reorder-left-position: calc(-1 * var(--sp-m) - var(--sp-xxs)); - &:hover { --reorder-icon-visibility: visible; } @@ -34,156 +37,289 @@ --detach-icon-foreground-color: none; display: grid; - flex: 1; grid-template-columns: 1fr auto; align-items: center; - gap: deprecated.$s-2; - border-radius: deprecated.$s-8; - background-color: var(--input-details-color); - height: deprecated.$s-32; + flex: 1; + gap: var(--sp-xxs); + block-size: $sz-32; + border-radius: $br-8; + background-color: var(--color-background-primary); &:hover { - --detach-icon-foreground-color: var(--input-foreground-color-active); - - .detach-btn, - .select-btn { - background-color: transparent; - } + --detach-icon-foreground-color: var(--color-foreground-primary); } } .color-name-wrapper { - @extend .input-element; - @include deprecated.bodySmallTypography; + --color-name-wrapper-background-color: var(--color-background-tertiary); + --color-name-wrapper-foreground-color: var(--color-foreground-primary); + --color-name-wrapper-boder-color: var(--color-background-tertiary); + @include t.use-typography("body-small"); + @include textEllipsis; + display: flex; + align-items: center; flex-grow: 1; - width: 100%; - min-width: 0; - border-radius: deprecated.$br-8 0 0 deprecated.$br-8; + gap: var(--sp-xs); + block-size: $sz-32; + min-inline-size: 0; + inline-size: 100%; padding: 0; margin-inline-end: 0; - gap: deprecated.$s-4; + border: $b-1 solid var(--color-name-wrapper-boder-color); + border-radius: $br-8; + background-color: var(--color-name-wrapper-background-color); + color: var(--color-name-wrapper-foreground-color); + border-radius: $br-8 0 0 $br-8; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - input { - padding: 0; - } - .color-bullet-wrapper { - height: deprecated.$s-28; - padding: 0 deprecated.$s-2 0 deprecated.$s-8; - border-radius: deprecated.$br-8 0 0 deprecated.$br-8; - background-color: transparent; - display: flex; - align-items: center; - &:hover { - background-color: transparent; - } - } - .color-name { - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; - padding-inline: deprecated.$s-6; - border-radius: deprecated.$br-8; - color: var(--input-foreground-color-active); - } - .detach-btn { - @extend .button-tertiary; - height: deprecated.$s-28; - width: deprecated.$s-28; - margin-inline-start: auto; - border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; - display: none; - } - .detach-icon { - @extend .button-icon; - stroke: var(--detach-icon-foreground-color); - } - .color-input-wrapper { - @include deprecated.bodySmallTypography; - display: flex; - align-items: center; - height: deprecated.$s-28; - padding: 0 deprecated.$s-0; - width: 100%; - margin: 0; - flex-grow: 1; - background-color: var(--input-background-color); - color: var(--input-foreground-color); - border-radius: deprecated.$br-0; - } &.no-opacity { - border-radius: deprecated.$br-8; + border-radius: $br-8; .color-input-wrapper { - border-radius: deprecated.$br-8; + border-radius: $br-8; } } + &:hover { --detach-icon-foreground-color: var(--input-foreground-color-active); + --color-name-wrapper-background-color: var(--color-background-quaternary); + --color-name-wrapper-boder-color: var(--color-background-quaternary); - background-color: var(--input-background-color-hover); - border: deprecated.$s-1 solid var(--input-border-color-hover); - .color-bullet-wrapper, - .color-name, - .detach-btn, - .color-input-wrapper { - background-color: var(--input-background-color-hover); - } .detach-btn { - display: flex; + display: grid; } &.editing { - background-color: var(--input-background-color-active); - .color-bullet-wrapper, - .color-name, - .detach-btn, - .color-input-wrapper { - background-color: var(--input-background-color-active); - } + --color-name-wrapper-background-color: var(--color-background-primary); } + &:focus, &:focus-within { - background-color: var(--input-background-color-focus); - border: deprecated.$s-1 solid var(--input-border-color-focus); + --color-name-wrapper-background-color: var(--color-background-tertiary); + --color-name-wrapper-boder-color: var(--color-accent-primary); } } &:focus, &:focus-within { - background-color: var(--input-background-color-focus); - border: deprecated.$s-1 solid var(--input-border-color-focus); + --color-name-wrapper-background-color: var(--color-background-tertiary); + --color-name-wrapper-boder-color: var(--color-accent-primary); &:hover { - background-color: var(--input-background-color-hover); - border: deprecated.$s-1 solid var(--input-border-color-focus); + --color-name-wrapper-background-color: var(--color-background-quaternary); } } &.editing { - background-color: var(--input-background-color-active); + --color-name-wrapper-background-color: var(--color-background-primary); &:hover { - border: deprecated.$s-1 solid var(--input-border-color-active); + --color-name-wrapper-boder-color: var(--color-accent-primary); } } } +.detach-btn { + display: none; + background: var(--color-name-wrapper-background-color); +} + +.color-input-wrapper { + @include t.use-typography("body-small"); + display: flex; + align-items: center; + flex-grow: 1; + block-size: $sz-28; + inline-size: 100%; + padding: 0; + margin: 0; + background-color: var(--color-name-wrapper-background-color); + color: var(--color-name-wrapper-foreground-color); + border-radius: 0; +} + +.color-name { + @include t.use-typography("body-small"); + @include textEllipsis; + flex-grow: 1; + padding-inline: px2rem(6); + border-radius: $br-8; + color: var(--color-name-wrapper-foreground-color); +} + +.color-bullet-wrapper { + display: flex; + align-items: center; + position: relative; + block-size: $sz-28; + padding: 0 var(--sp-xxs) 0 var(--sp-s); + border-radius: $br-8 0 0 $br-8; + background-color: transparent; + &:hover { + background-color: transparent; + } +} + +.color-input { + @include textEllipsis; + border: none; + background: none; + outline: none; + block-size: $sz-28; + inline-size: 100%; + flex-grow: 1; + margin: var(--sp-xxs) 0; + padding: 0 0 0 px2rem(6); + border-radius: $br-8; + color: var(--input-foreground-color-active); + &[disabled] { + opacity: 0.5; + pointer-events: none; + } +} + .library-name-wrapper { - border-radius: deprecated.$br-8; + border-radius: $br-8; } .opacity-element-wrapper { - @extend .input-element; - @include deprecated.bodySmallTypography; - width: deprecated.$s-60; - border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; - .opacity-input { - padding: 0; - border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; - min-width: deprecated.$s-28; + --opacity-input-background-color: var(--color-background-tertiary); + --opacity-input-boder-color: var(--color-background-tertiary); + + @include t.use-typography("body-small"); + display: flex; + align-items: center; + block-size: $sz-32; + inline-size: px2rem(60); + border-radius: 0 $br-8 $br-8 0; + border: $b-1 solid var(--opacity-input-boder-color); + background-color: var(--opacity-input-background-color); + + &:hover { + --opacity-input-background-color: var(--color-background-quaternary); + --opacity-input-boder-color: var(--color-background-quaternary); + + .detach-btn { + display: grid; + } + &.editing { + --opacity-input-background-color: var(--color-background-primary); + } + + &:focus, + &:focus-within { + --opacity-input-background-color: var(--color-background-tertiary); + --opacity-input-boder-color: var(--color-accent-primary); + } } - .icon-text { - @include deprecated.flexCenter; - height: deprecated.$s-32; - margin-inline-end: deprecated.$s-4; - margin-block-start: deprecated.$s-2; + + &:focus, + &:focus-within { + --opacity-input-background-color: var(--color-background-tertiary); + --opacity-input-boder-color: var(--color-accent-primary); + &:hover { + --opacity-input-background-color: var(--color-background-quaternary); + } + } + + &.editing { + --opacity-input-background-color: var(--color-background-primary); + &:hover { + --opacity-input-boder-color: var(--color-accent-primary); + } } } + +.opacity-input { + @include textEllipsis; + block-size: $sz-28; + min-inline-size: $sz-28; + flex-grow: 1; + inline-size: 100%; + padding: 0; + border-radius: 0 $br-8 $br-8 0; + border: none; + background: none; + outline: none; + margin: var(--sp-xxs) 0; + padding: 0 0 0 px2rem(6); + color: var(--color-foreground-primary); + &[disabled] { + opacity: 0.5; + pointer-events: none; + } +} + +.icon-text { + display: flex; + justify-content: center; + align-items: center; + block-size: $sz-32; + margin-inline-end: var(--sp-xs); + margin-block-start: var(--sp-xxs); + color: var(--color-foreground-secondary); +} + +// TOKEN ROW + +.token-color-wrapper { + --token-color-wrapper-background-color: var(--color-background-tertiary); + --token-color-wrapper-foreground-color: var(--color-token-foreground); + --token-color-wrapper-border-color: var(--color-token-border); + --token-actions-display: none; + display: grid; + grid-template-columns: auto 1fr auto; + gap: var(--sp-xs); + block-size: $sz-32; + min-inline-size: 0; + inline-size: 100%; + padding: 0; + margin-inline-end: 0; + background: var(--token-color-wrapper-background-color); + border: $b-1 solid var(--token-color-wrapper-border-color); + border-radius: $br-8; + &:hover { + --token-color-wrapper-background-color: var(--color-token-background); + --token-color-wrapper-foreground-color: var(--color-foreground-primary); + --token-color-wrapper-border-color: var(--color-token-background); + --token-actions-display: flex; + } +} + +.token-color-with-errors, +.token-color-not-active { + --token-color-wrapper-background-color: var(--color-background-primary); + --token-color-wrapper-foreground-color: var(--color-foreground-secondary); + --token-color-wrapper-border-color: var(--color-token-border); + &:hover { + --token-color-wrapper-background-color: var(--color-background-primary); + --token-color-wrapper-foreground-color: var(--color-foreground-secondary); + --token-color-wrapper-border-color: var(--color-token-background); + --token-actions-display: flex; + } +} + +.token-name { + @include t.use-typography("body-small"); + @include textEllipsis; + color: var(--token-color-wrapper-foreground-color); + block-size: $sz-32; + display: flex; + align-items: center; +} + +.token-name-tooltip { + color: var(--color-foreground-primary); +} + +.token-actions { + display: var(--token-actions-display); + justify-self: flex-end; + align-items: center; +} + +.error-dot { + inline-size: px2rem(4); + block-size: px2rem(4); + border-radius: 50%; + background-color: var(--color-foreground-error); + margin-inline-start: var(--sp-xs); + position: absolute; + inset-inline-end: px2rem(1); + inset-block-start: px2rem(5); +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index 323ac06283..3bd1eb7a41 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -9,6 +9,8 @@ (:require [app.common.data :as d] [app.common.types.color :as ctc] + [app.main.data.workspace.tokens.application :as dwta] + [app.main.store :as st] [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.reorder-handler :refer [reorder-handler*]] [app.main.ui.components.select :refer [select]] @@ -37,8 +39,12 @@ disable-drag on-focus on-blur + applied-tokens + on-detach-token disable-stroke-style - select-on-focus]}] + select-on-focus + shapes + objects]}] (let [on-drop (fn [_ data] @@ -55,27 +61,29 @@ :name (str "Border row" index)}) [nil nil]) + stroke-color-token (:stroke-color applied-tokens) + on-color-change-refactor - (mf/use-callback + (mf/use-fn (mf/deps index on-color-change) (fn [color] (on-color-change index color))) on-color-detach - (mf/use-callback + (mf/use-fn (mf/deps index on-color-detach) (fn [color] (on-color-detach index color))) on-remove - (mf/use-callback + (mf/use-fn (mf/deps index on-remove) #(on-remove index)) stroke-width (:stroke-width stroke) on-width-change - (mf/use-callback + (mf/use-fn (mf/deps index on-stroke-width-change) #(on-stroke-width-change index %)) @@ -91,12 +99,35 @@ {:value :outer :label (tr "workspace.options.stroke.outer")}])) on-alignment-change - (mf/use-callback + (mf/use-fn (mf/deps index on-stroke-alignment-change) #(on-stroke-alignment-change index (keyword %))) + on-token-change + (mf/use-fn + (mf/deps shapes objects) + (fn [_ token] + (let [expanded-shapes + (if (= 1 (count shapes)) + (let [shape (first shapes)] + (if (= (:type shape) :group) + (keep objects (:shapes shape)) + [shape])) + + (mapcat (fn [shape] + (if (= (:type shape) :group) + (keep objects (:shapes shape)) + [shape])) + shapes))] + + (st/emit! + (dwta/toggle-token {:token token + :attrs #{:stroke-color} + :shapes expanded-shapes}))))) + stroke-style (or (:stroke-style stroke) :solid) + stroke-style-options (mf/with-memo [stroke-style] (d/concat-vec @@ -108,20 +139,26 @@ {:value :mixed :label (tr "workspace.options.stroke.mixed")}])) on-style-change - (mf/use-callback + (mf/use-fn (mf/deps index on-stroke-style-change) #(on-stroke-style-change index (keyword %))) on-caps-start-change - (mf/use-callback + (mf/use-fn (mf/deps index on-stroke-cap-start-change) #(on-stroke-cap-start-change index (keyword %))) on-caps-end-change - (mf/use-callback + (mf/use-fn (mf/deps index on-stroke-cap-end-change) #(on-stroke-cap-end-change index (keyword %))) + on-detach-token-color + (mf/use-fn + (mf/deps on-detach-token) + (fn [token] + (on-detach-token token #{:stroke-color}))) + stroke-caps-options [{:value nil :label (tr "workspace.options.stroke-cap.none")} :separator @@ -135,7 +172,7 @@ {:value :square :label (tr "workspace.options.stroke-cap.square") :icon :stroke-squared}] on-cap-switch - (mf/use-callback + (mf/use-fn (mf/deps index on-stroke-cap-switch) #(on-stroke-cap-switch index))] @@ -156,8 +193,11 @@ :on-detach on-color-detach :on-remove on-remove :disable-drag disable-drag + :applied-token stroke-color-token + :on-detach-token on-detach-token-color + :on-token-change on-token-change :on-focus on-focus - :origin :stroke + :origin :stroke-color :select-on-focus select-on-focus :on-blur on-blur}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs index c0bba81644..8311104d68 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs @@ -30,7 +30,8 @@ shapes (mf/with-memo [shape] [shape]) applied-tokens - (get shape :applied-tokens) + (when (seq (get shape :applied-tokens)) + (get shape :applied-tokens)) measure-values (select-keys shape measure-attrs) @@ -120,12 +121,16 @@ [:> fill/fill-menu* {:ids ids :type type - :values shape}] + :values shape + :shapes shapes + :applied-tokens applied-tokens}] [:& stroke-menu {:ids ids :type type :show-caps true - :values stroke-values}] + :values stroke-values + :shapes shapes + :applied-tokens applied-tokens}] [:> shadow-menu* {:ids ids :values (get shape :shadow)}] [:& blur-menu {:ids ids diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs index 35d9586e6e..b21f0f4c5c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs @@ -120,10 +120,15 @@ [:> fill/fill-menu* {:ids ids :type type - :values shape}] + :values shape + :shapes shapes + :applied-tokens applied-tokens}] + [:& stroke-menu {:ids ids :type type - :values stroke-values}] + :values stroke-values + :shapes shapes + :applied-tokens applied-tokens}] [:> shadow-menu* {:ids ids :values (get shape :shadow)}] [:& blur-menu {:ids ids :values (select-keys shape [:blur])}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs index fcba591c5d..dec42cb3da 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs @@ -143,11 +143,15 @@ [:> fill/fill-menu* {:ids ids :type shape-type - :values shape}] + :values shape + :shapes shapes + :applied-tokens applied-tokens}] [:& stroke-menu {:ids ids :type shape-type - :values stroke-values}] + :values stroke-values + :shapes shapes + :applied-tokens applied-tokens}] [:> color-selection-menu* {:type shape-type :shapes shapes-with-children :file-id file-id diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs index ae3fb4e65f..e9d29affcc 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs @@ -90,7 +90,7 @@ [constraint-ids constraint-values] (get-attrs shapes objects :constraint) - [fill-ids fill-values] + [fill-ids fill-values fill-tokens] (get-attrs shapes objects :fill) [shadow-ids] @@ -99,7 +99,7 @@ [blur-ids blur-values] (get-attrs shapes objects :blur) - [stroke-ids stroke-values] + [stroke-ids stroke-values stroke-tokens] (get-attrs shapes objects :stroke) [text-ids text-values] @@ -143,10 +143,21 @@ [:& constraints-menu {:ids constraint-ids :values constraint-values}]) (when-not (empty? fill-ids) - [:> fill/fill-menu* {:type type :ids fill-ids :values fill-values}]) + [:> fill/fill-menu* + {:type type + :ids fill-ids + :values fill-values + :shapes shapes + :objects objects + :applied-tokens fill-tokens}]) (when-not (empty? stroke-ids) - [:& stroke-menu {:type type :ids stroke-ids :values stroke-values}]) + [:& stroke-menu {:type type + :ids stroke-ids + :values stroke-values + :shapes shapes + :objects objects + :applied-tokens stroke-tokens}]) [:> color-selection-menu* {:type type diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index 11247646ea..4887cdc324 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -17,6 +17,7 @@ [app.common.types.shape.attrs :refer [editable-attrs]] [app.common.types.shape.layout :as ctl] [app.common.types.text :as txt] + [app.common.types.token :as tt] [app.common.weak :as weak] [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-attrs blur-menu]] @@ -211,18 +212,34 @@ (= attr-group :blur) (attrs/get-attrs-multi [v1 v2] attrs blur-eq blur-sel) :else (attrs/get-attrs-multi [v1 v2] attrs))) + merge-attr + (fn [acc applied-tokens t-attr] + "Merges a single token attribute (`t-attr`) into the accumulator map. + - If the attribute is not present, associates it with the new value. + - If the existing value equals the new value, keeps the accumulator unchanged. + - If there is a conflict, sets the value to `:multiple`." + (let [new-val (get applied-tokens t-attr) + existing (get acc t-attr ::not-found)] + (cond + (= existing ::not-found) (assoc acc t-attr new-val) + (= existing new-val) acc + :else (assoc acc t-attr :multiple)))) + + merge-shape-attr + (fn [acc applied-tokens shape-attr] + "Merges all token attributes derived from a single shape attribute + into the accumulator map using `merge-attr`." + (let [token-attrs (tt/shape-attr->token-attrs shape-attr)] + (reduce #(merge-attr %1 applied-tokens %2) acc token-attrs))) + merge-token-values - (fn [acc keys attrs] - (reduce - (fn [accum key] - (let [new-val (get attrs key) - existing (get accum key ::not-found)] - (cond - (= existing ::not-found) (assoc accum key new-val) - (= existing new-val) accum - :else (assoc accum key :multiple)))) - acc - keys)) + (fn [acc shape-attrs applied-tokens] + "Merges token values across all shape attributes. + For each shape attribute, its corresponding token attributes are merged + into the accumulator. If applied tokens are empty, the accumulator is returned unchanged." + (if (seq applied-tokens) + (reduce #(merge-shape-attr %1 applied-tokens %2) acc shape-attrs) + acc)) extract-attrs (fn [[ids values token-acc] {:keys [id type applied-tokens] :as shape}] @@ -263,8 +280,8 @@ :children (let [children (->> (:shapes shape []) (map #(get objects %))) - [new-ids new-values] (get-attrs* children objects attr-group)] - [(d/concat-vec ids new-ids) (merge-attrs values new-values) {}]) + [new-ids new-values tokens] (get-attrs* children objects attr-group)] + [(d/concat-vec ids new-ids) (merge-attrs values new-values) tokens]) [])))] @@ -376,7 +393,7 @@ [constraint-ids constraint-values] (get-attrs shapes objects :constraint) - [fill-ids fill-values] + [fill-ids fill-values fill-tokens] (get-attrs shapes objects :fill) [shadow-ids shadow-values] @@ -385,7 +402,7 @@ [blur-ids blur-values] (get-attrs shapes objects :blur) - [stroke-ids stroke-values] + [stroke-ids stroke-values stroke-tokens] (get-attrs shapes objects :stroke) [exports-ids exports-values] @@ -463,14 +480,22 @@ [:& ot/text-menu {:type type :ids text-ids :values text-values}]) (when-not (empty? fill-ids) - [:> fill/fill-menu* {:type type :ids fill-ids :values fill-values}]) + [:> fill/fill-menu* {:type type + :ids fill-ids + :values fill-values + :shapes shapes + :objects objects + :applied-tokens fill-tokens}]) (when-not (empty? stroke-ids) [:& stroke-menu {:type type :ids stroke-ids :show-caps show-caps? :values stroke-values - :disable-stroke-style has-text?}]) + :shapes shapes + :objects objects + :disable-stroke-style has-text? + :applied-tokens stroke-tokens}]) (when-not (empty? shapes) [:> color-selection-menu* diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs index e632809ca7..c1ef859746 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs @@ -120,12 +120,16 @@ [:> fill/fill-menu* {:ids ids :type type - :values shape}] + :values shape + :shapes shapes + :applied-tokens applied-tokens}] [:& stroke-menu {:ids ids :type type :show-caps true - :values stroke-values}] + :values stroke-values + :shapes shapes + :applied-tokens applied-tokens}] [:> shadow-menu* {:ids ids :values (get shape :shadow)}] [:& blur-menu {:ids ids :values (select-keys shape [:blur])}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs index 6e7df675b4..6019e37f7b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs @@ -120,11 +120,15 @@ [:> fill/fill-menu* {:ids ids :type type - :values shape}] + :shapes shapes + :values shape + :applied-tokens applied-tokens}] [:& stroke-menu {:ids ids :type type - :values stroke-values}] + :shapes shapes + :values stroke-values + :applied-tokens applied-tokens}] [:> shadow-menu* {:ids ids :values (get shape :shadow)}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs index daf01c4dc5..0b85529c76 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs @@ -188,11 +188,15 @@ [:> fill/fill-menu* {:ids ids :type type - :values fill-values}] + :values fill-values + :shapes shapes + :applied-tokens applied-tokens}] [:& stroke-menu {:ids ids :type type - :values stroke-values}] + :values stroke-values + :shapes shapes + :applied-tokens applied-tokens}] [:> shadow-menu* {:ids ids :values (get shape :shadow)}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index 51642f9d29..025217501a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -173,12 +173,16 @@ [:> fill/fill-menu* {:ids ids :type type - :values fill-values}] + :values fill-values + :shapes shapes + :applied-tokens applied-tokens}] [:& stroke-menu {:ids ids :type type :values stroke-values - :disable-stroke-style true}] + :shapes shapes + :disable-stroke-style true + :applied-tokens applied-tokens}] (when (= :multiple (:fills fill-values)) [:> color-selection-menu* diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e4f35c9086..25b06304b4 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1184,7 +1184,11 @@ msgstr "Detach token" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:39 msgid "ds.inputs.token-field.no-active-token-option" -msgstr "This token is not available in any active set or theme." +msgstr "This token is not in any active set or has an invalid value." + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +msgid "color-row.token-color-row.deleted-token" +msgstr "This token does not exists or has been deleted." #: src/app/main/data/auth.cljs:314 msgid "errors.auth-provider-not-allowed" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index e0830db85d..ff8b86aa0b 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1196,7 +1196,11 @@ msgstr "Desvincular token" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:39 msgid "ds.inputs.token-field.no-active-token-option" -msgstr "Este token no está disponible en ningún set ni tema activo." +msgstr "Este token no está disponible en ningún set o tiene un valor inválido." + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +msgid "color-row.token-color-row.deleted-token" +msgstr "Este token no existe o ha sido borrado." #: src/app/main/data/auth.cljs:314 msgid "errors.auth-provider-not-allowed"