🎉 Add tokens to color selection (#7447)

* 🎉 Add tokens to color row

* 🎉 Add color-token to stroke input

* 🐛 Fix change token on multiselection with groups

* 🎉 Create token colors on selected-colors section

* ♻️ Fix comments
This commit is contained in:
Eva Marco
2025-10-14 11:17:14 +02:00
committed by GitHub
parent 0f3ca67773
commit 045aa7c788
9 changed files with 261 additions and 85 deletions

View File

@@ -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 <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 <uuid>
:index <int>}"
[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 <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 <uuid>
:index <int>}"
[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 <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))

View File

@@ -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)

View File

@@ -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")])]])]))

View File

@@ -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?)

View File

@@ -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 its 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 its 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}])]))
:icon i/move}])]))

View File

@@ -127,7 +127,6 @@
stroke-style (or (:stroke-style stroke) :solid)
stroke-style-options
(mf/with-memo [stroke-style]
(d/concat-vec

View File

@@ -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"

View File

@@ -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"