From 045aa7c788a4cfc9b12357debf76f1e9e38761d8 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 14 Oct 2025 11:17:14 +0200 Subject: [PATCH] :tada: Add tokens to color selection (#7447) * :tada: Add tokens to color row * :tada: Add color-token to stroke input * :bug: Fix change token on multiselection with groups * :tada: Create token colors on selected-colors section * :recycle: Fix comments --- .../test/common_tests/logic/token_test.cljc | 1 - .../src/app/main/data/workspace/colors.cljs | 118 ++++++++++-- .../src/app/main/ui/ds/utilities/swatch.cljs | 1 - .../options/menus/color_selection.cljs | 177 +++++++++++++----- .../workspace/sidebar/options/menus/fill.cljs | 4 +- .../sidebar/options/rows/color_row.cljs | 32 ++-- .../sidebar/options/rows/stroke_row.cljs | 1 - frontend/translations/en.po | 6 +- frontend/translations/es.po | 6 +- 9 files changed, 261 insertions(+), 85 deletions(-) diff --git a/common/test/common_tests/logic/token_test.cljc b/common/test/common_tests/logic/token_test.cljc index aa58adfa05..02a4599f39 100644 --- a/common/test/common_tests/logic/token_test.cljc +++ b/common/test/common_tests/logic/token_test.cljc @@ -34,7 +34,6 @@ (pcb/with-library-data (:data file)) (clt/generate-toggle-token-set (tht/get-tokens-lib file) "foo/bar")) - _ (prn "changes" changes) redo (thf/apply-changes file changes) redo-lib (tht/get-tokens-lib redo) undo (thf/apply-undo-changes redo changes) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 1eb4b5ad0f..bab73f42d6 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -1137,16 +1137,20 @@ ref-id (:stroke-color-ref-id stroke) colors (-> libraries - (get ref-file) + (get ref-id) (get :data) (ctl/get-colors)) + shared? (contains? colors ref-id) + has-color? (:stroke-color stroke) - is-shared? (contains? colors ref-id) - has-color? (or (:stroke-color stroke) - (:stroke-color-gradient stroke)) - attrs (cond-> (clr/stroke->color stroke) - (not (or is-shared? (= ref-file file-id))) - (dissoc :ref-id :ref-file))] + base-attrs (cond-> (clr/stroke->color stroke) + (not (or shared? (= ref-file file-id))) + (dissoc :ref-file :ref-id)) + + attrs (cond-> base-attrs + (:has-token-applied stroke) + (assoc :has-token-applied true + :token-name (:token-name stroke)))] (when has-color? {:attrs attrs @@ -1154,7 +1158,25 @@ :shape-id (:shape-id stroke) :index (:index stroke)}))) -(defn- shadow->color-att +(defn- shadow->color-attr + "Given a stroke map enriched with :shape-id, :index, and optionally + :has-token-applied / :token-name, returns a color attribute map. + + If :has-token-applied is true, adds token metadata to :attrs: + {:has-token-applied true + :token-name } + + Args: + - stroke: map with stroke info, including :shape-id and :index + - file-id: current file UUID + - libraries: map of shared color libraries + + Returns: + A map like: + {:attrs {...color data...} + :prop :stroke + :shape-id + :index }" [shadow file-id libraries] (let [color (get shadow :color) ref-file (get color :ref-file) @@ -1202,6 +1224,24 @@ (map #(text->color-att % file-id libraries))))) (defn- fill->color-att + "Given a fill map enriched with :shape-id, :index, and optionally + :has-token-applied / :token-name, returns a color attribute map. + + If :has-token-applied is true, adds token metadata to :attrs: + {:has-token-applied true + :token-name } + + Args: + - fill: map with fill info, including :shape-id and :index + - file-id: current file UUID + - libraries: map of shared color libraries + + Returns: + A map like: + {:attrs {...color data...} + :prop :fill + :shape-id + :index }" [fill file-id libraries] (let [ref-file (:fill-color-ref-file fill) ref-id (:fill-color-ref-id fill) @@ -1213,9 +1253,15 @@ shared? (contains? colors ref-id) has-color? (or (:fill-color fill) (:fill-color-gradient fill)) - attrs (cond-> (types.fills/fill->color fill) + + base-attrs (cond-> (types.fills/fill->color fill) (not (or shared? (= ref-file file-id))) - (dissoc :ref-file :ref-id))] + (dissoc :ref-file :ref-id)) + + attrs (cond-> base-attrs + (:has-token-applied fill) + (assoc :has-token-applied true + :token-name (:token-name fill)))] (when has-color? {:attrs attrs @@ -1224,21 +1270,55 @@ :index (:index fill)}))) (defn extract-all-colors + "Extracts color information from a list of shapes, including fills, strokes, and shadows. + If a shape has applied tokens of type :fill or :stroke-color, the first fill or stroke + will include extra attributes in its :attrs map: + {:has-token-applied true + :token-name } + + Args: + - shapes: vector of shape maps + - file-id: current file UUID + - libraries: map of shared color libraries + + Returns: + A vector of color attribute maps with metadata for each shape." [shapes file-id libraries] (reduce (fn [result shape] - (let [fill-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:fills shape)) - stroke-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:strokes shape)) - shadow-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:shadow shape))] + (let [applied-tokens (:applied-tokens shape) + applied-fill (get applied-tokens :fill) + applied-stroke (get applied-tokens :stroke-color) + fills (:fills shape) + strokes (:strokes shape) + shadows (:shadow shape) + shape-id (:id shape) + + fills* (map-indexed + (fn [index fill] + (cond-> (assoc fill :shape-id shape-id :index index) + (and (zero? index) applied-fill) + (assoc :has-token-applied true + :token-name applied-fill))) + fills) + + strokes* (map-indexed + (fn [index stroke] + (cond-> (assoc stroke :shape-id shape-id :index index) + (and (zero? index) applied-stroke) + (assoc :has-token-applied true + :token-name applied-stroke))) + strokes) + + shadows* (map-indexed #(assoc %2 :shape-id shape-id :index %1) shadows)] (if (= :text (:type shape)) (-> result - (into (keep #(stroke->color-att % file-id libraries)) stroke-obj) - (into (map #(shadow->color-att % file-id libraries)) shadow-obj) + (into (keep #(stroke->color-att % file-id libraries)) strokes*) + (into (map #(shadow->color-attr % file-id libraries)) shadows*) (into (extract-text-colors shape file-id libraries))) - (-> result - (into (keep #(fill->color-att % file-id libraries)) fill-obj) - (into (keep #(stroke->color-att % file-id libraries)) stroke-obj) - (into (map #(shadow->color-att % file-id libraries)) shadow-obj))))) + (into (keep #(fill->color-att % file-id libraries)) fills*) + (into (keep #(stroke->color-att % file-id libraries)) strokes*) + (into (map #(shadow->color-attr % file-id libraries)) shadows*))))) [] shapes)) diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.cljs b/frontend/src/app/main/ui/ds/utilities/swatch.cljs index 1021c086c6..2ac26b6d94 100644 --- a/frontend/src/app/main/ui/ds/utilities/swatch.cljs +++ b/frontend/src/app/main/ui/ds/utilities/swatch.cljs @@ -133,7 +133,6 @@ :style {:background-image (str/ffmt "url(%)" uri)}}]) has-errors [:span {:class (stl/css :swatch-error)}] - :else [:span {:class (stl/css :swatch-opacity)} [:span {:class (stl/css :swatch-solid-side) 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 a785230dac..86622173c8 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 @@ -13,44 +13,93 @@ [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.hooks :as h] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) (defn- prepare-colors - [shapes file-id libraries] - (let [data (into [] (remove nil? (dwc/extract-all-colors shapes file-id libraries))) - groups (d/group-by :attrs #(dissoc % :attrs) data) - all-colors (distinct (mapv :attrs data)) + "Prepares and groups extracted color information from shapes. + Input: + - shapes: vector of shape maps + - file-id: current file UUID + - libraries: shared color libraries - tmp (group-by #(some? (:id %)) all-colors) - library-colors (get tmp true) - colors (get tmp false)] + Output: + {:groups explained below + :all-colors vector of all color maps (unique attrs) + :colors vector of normal colors (without ref-id or token) + :library-colors vector of colors linked to libraries (with ref-id) + :token-colors vector of colors linked to applied tokens + :tokens placeholder for future token data} + + :groups structure + + A map where: + - Each **key** is a color descriptor map representing a unique color instance. + Depending on the color type, it can contain: + • :color → hex string (e.g. \"#9f2929\") + • :opacity → numeric value between 0-1 + • :ref-id and :ref-file → if the color comes from a library + • :token-name \"some-token\" → if the color + originates from an applied token + + - Each **value** is a vector of one or more maps describing *where* that + color is used. Each entry corresponds to a specific shape and color + property in the document: + • :prop → the property type (:fill, :stroke, :shadow, etc.) + • :shape-id → the UUID of the shape using this color + • :index → index of the color in the shape's fill/stroke list + + Example of groups: + { + {:color \"#9f2929\", :opacity 0.3, :token-name \"asd2\" :has-token-applied true} + [{:prop :fill, :shape-id #uuid \"d0231035-25c9-80d5-8006-eae4c3dff32e\", :index 0}] + + {:color \"#1b54b6\", :opacity 1} + [{:prop :fill, :shape-id #uuid \"aab34f9a-98c1-801a-8006-eae5e8236f1b\", :index 0}] + } + + This structure allows fast lookups of all shapes using the same visual color, + regardless of whether it comes from local fills, strokes or shadow-colors." + + [shapes file-id libraries] + (let [data (into [] (remove nil?) (dwc/extract-all-colors shapes file-id libraries)) + groups (d/group-by :attrs #(dissoc % :attrs) data) + + ;; Unique color attribute maps + all-colors (distinct (mapv :attrs data)) + + ;; Split into: library colors, token colors, and plain colors + library-colors (filterv :ref-id all-colors) + token-colors (filterv :token-name all-colors) + colors (filterv #(and (nil? (:ref-id %)) + (not (:token-name %))) + all-colors)] {:groups groups :all-colors all-colors :colors colors + :token-colors token-colors :library-colors library-colors})) (def xf:map-shape-id (map :shape-id)) -(defn- generate-color-operations +(defn- retrieve-color-operations [groups old-color prev-colors] - (let [old-color (-> old-color - (dissoc :name :path) - (d/without-nils)) - prev-color (d/seek (partial get groups) prev-colors) + (let [old-color (-> old-color + (dissoc :name :path) + (d/without-nils)) + prev-color (d/seek (partial get groups) prev-colors) color-operations-old (get groups old-color) color-operations-prev (get groups prev-colors) color-operations (or color-operations-prev color-operations-old) - old-color (or prev-color old-color)] + old-color (or prev-color old-color)] [color-operations old-color])) (mf/defc color-selection-menu* {::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]} [{:keys [shapes file-id libraries]}] - (let [{:keys [groups library-colors colors]} + (let [{:keys [groups library-colors colors token-colors]} (mf/with-memo [file-id shapes libraries] (prepare-colors shapes file-id libraries)) @@ -63,21 +112,19 @@ expand-lib-color (mf/use-state false) expand-color (mf/use-state false) + expand-token-color (mf/use-state false) - groups-ref (h/use-ref-value groups) + ;; TODO: Review if this is still necessary. prev-colors-ref (mf/use-ref nil) on-change (mf/use-fn - (fn [new-color old-color from-picker?] - (let [;; When dragging on the color picker sometimes all - ;; the shapes hasn't updated the color to the prev - ;; value so we need this extra calculation - groups (mf/ref-val groups-ref) - prev-colors (mf/ref-val prev-colors-ref) - - [color-operations old-color] (generate-color-operations groups old-color prev-colors)] + (mf/deps groups) + (fn [old-color new-color from-picker?] + (let [prev-colors (mf/ref-val prev-colors-ref) + [color-operations old-color] (retrieve-color-operations groups old-color prev-colors)] + ;; TODO: Review if this is still necessary. (when from-picker? (let [color (-> new-color (dissoc :name :path) @@ -85,7 +132,7 @@ (mf/set-ref-val! prev-colors-ref (conj prev-colors color)))) - (st/emit! (dwc/change-color-in-selected color-operations new-color old-color))))) + (st/emit! (dwc/change-color-in-selected color-operations new-color (dissoc old-color :token-name :has-token-applied)))))) on-open (mf/use-fn #(mf/set-ref-val! prev-colors-ref [])) @@ -95,31 +142,52 @@ on-detach (mf/use-fn + (mf/deps groups) (fn [color] - (let [groups (mf/ref-val groups-ref) - color-operations (get groups color) - color' (dissoc color :id :file-id)] + (let [color-operations (get groups color) + color' (dissoc color :ref-id :ref-file)] (st/emit! (dwc/change-color-in-selected color-operations color' color))))) + on-detach-token + (mf/use-fn + (mf/deps token-colors groups) + (fn [token] + (let [prev-colors (mf/ref-val prev-colors-ref) + token-color (some #(when (= (:token-name %) (:name token)) %) token-colors) + + [color-operations _] (retrieve-color-operations groups token-color prev-colors)] + (doseq [op color-operations] + (let [attr (if (= (:prop op) :stroke) + #{:stroke-color} + #{:fill}) + color (-> token-color + (dissoc :token-name :has-token-applied) + (d/without-nils))] + (mf/set-ref-val! prev-colors-ref + (conj prev-colors color)) + (st/emit! (dwta/unapply-token {:attributes attr + :token token + :shape-ids [(:shape-id op)]}))))))) + select-only (mf/use-fn + (mf/deps groups) (fn [color] - (let [groups (mf/ref-val groups-ref) - color-operations (get groups color) + (let [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 + (mf/deps groups) (fn [_ token old-color] - (let [groups (mf/ref-val groups-ref) - prev-colors (mf/ref-val prev-colors-ref) + (let [prev-colors (mf/ref-val prev-colors-ref) resolved-value (:resolved-value token) new-color (dwta/value->color resolved-value) color (-> new-color (dissoc :name :path) (d/without-nils)) - [color-operations _] (generate-color-operations groups old-color prev-colors)] + [color-operations _] (retrieve-color-operations groups old-color prev-colors)] (mf/set-ref-val! prev-colors-ref (conj prev-colors color)) (st/emit! (dwta/apply-token-on-selected color-operations token)))))] @@ -135,22 +203,15 @@ (when open? [:div {:class (stl/css :element-content)} [:div {:class (stl/css :selected-color-group)} - ;; The hidden color is to solve a problem with the color picker. When a color is changed - ;; and is no longer a library color it disapears from the list of library colors. Because - ;; we need to keep the color picker open we need to maintain that color. The easier way - ;; is to render the color elements so even if the library color is no longer we have still - ;; the component to change it from the color picker. - (let [lib-colors (cond->> library-colors (not @expand-lib-color) (take 3)) - lib-colors (concat lib-colors colors)] - (for [[index color] (d/enumerate lib-colors)] + (let [library-colors-extract (cond->> library-colors (not @expand-lib-color) (take 3))] + (for [[index color] (d/enumerate library-colors-extract)] [:> color-row* {:key index :color color :index index - :hidden (not (:id color)) - :on-detach on-detach + :on-detach #(on-detach color %) :select-only select-only - :on-change #(on-change %1 color %2) + :on-change #(on-change color %1 %2) :on-token-change #(on-token-change %1 %2 color) :on-open on-open :origin :color-selection @@ -167,7 +228,7 @@ :color color :index index :select-only select-only - :on-change #(on-change %1 color %2) + :on-change #(on-change color %1 %2) :origin :color-selection :on-token-change #(on-token-change %1 %2 color) :on-open on-open @@ -176,4 +237,28 @@ (when (and (false? @expand-color) (< 3 (count colors))) [:button {:class (stl/css :more-colors-btn) :on-click #(reset! expand-color true)} - (tr "workspace.options.more-colors")])]])])) + (tr "workspace.options.more-colors")])] + + [:div {:class (stl/css :selected-color-group)} + (let [token-color-extract (cond->> token-colors (not @expand-token-color) (take 3))] + (for [[index token-color] (d/enumerate token-color-extract)] + (let [color {:color (:color token-color) + :opacity (:opacity token-color)}] + [:> color-row* + {:key index + :color color + :index index + :select-only select-only + :on-change #(on-change token-color %1 %2) + :origin :color-selection + :applied-token (:token-name token-color) + :on-detach-token on-detach-token + :on-token-change #(on-token-change %1 %2 token-color) + :on-open on-open + :on-close on-close}]))) + + (when (and (false? @expand-token-color) + (< 3 (count token-colors))) + [:button {:class (stl/css :more-colors-btn) + :on-click #(reset! expand-token-color true)} + (tr "workspace.options.more-token-colors")])]])])) 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 422d163b61..708c53adca 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 @@ -256,7 +256,9 @@ :on-remove on-remove :disable-drag disable-drag? :on-focus on-focus - :applied-token fill-token-applied + :applied-token (if (= index 0) + fill-token-applied + nil) :on-token-change on-token-change :origin :fill :select-on-focus (not 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 397415718b..bfcf309d3b 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 @@ -69,9 +69,9 @@ (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), + (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 @@ -143,10 +143,10 @@ [{: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 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)) @@ -169,11 +169,12 @@ active-tokens* (mf/use-ctx ctx/active-tokens-by-type) - tokens (mf/with-memo [active-tokens* origin] - (delay - (-> (deref active-tokens*) - (select-keys (get tk/tokens-by-input origin)) - (not-empty)))) + tokens (mf/with-memo [active-tokens* origin] + (let [origin (if (= :color-selection origin) :fill origin)] + (delay + (-> (deref active-tokens*) + (select-keys (get tk/tokens-by-input origin)) + (not-empty))))) on-focus' (mf/use-fn @@ -205,9 +206,12 @@ handle-select (mf/use-fn - (mf/deps select-only color) + (mf/deps select-only color applied-token) (fn [] - (select-only color))) + (let [color (if applied-token + (assoc color :has-token-applied true :token-name applied-token) + color)] + (select-only color)))) on-color-change (mf/use-fn @@ -233,7 +237,7 @@ open-modal (mf/use-fn - (mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open tokens) + (mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open tokens index) (fn [color pos tab] (let [color (cond has-multiple-colors @@ -427,4 +431,4 @@ [:> icon-button* {:variant "ghost" :aria-label (tr "settings.select-this-color") :on-click handle-select - :icon i/move}])])) \ No newline at end of file + :icon i/move}])])) 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 3bd1eb7a41..4fc7e4d9de 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 @@ -127,7 +127,6 @@ stroke-style (or (:stroke-style stroke) :solid) - stroke-style-options (mf/with-memo [stroke-style] (d/concat-vec diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 18f9a1804e..06365e430c 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -6514,7 +6514,11 @@ msgstr "Top" msgid "workspace.options.more-colors" msgstr "More colors" -#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:161 +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.more-token-colors" +msgstr "More color tokens" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:140 msgid "workspace.options.more-lib-colors" msgstr "More library colors" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 6e5f6415ae..9633d73d95 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -6457,7 +6457,11 @@ msgstr "Arriba" msgid "workspace.options.more-colors" msgstr "Más colores" -#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:161 +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.more-token-colors" +msgstr "Más tokens de color" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:140 msgid "workspace.options.more-lib-colors" msgstr "Más colores de la biblioteca"