Implement object type specific tokens (#6816)

*  Allow token applying for supported shape types only

* 🐛 Remove x/y attribute keys from spacing token

*  Shape specific context-menu

*  Only apply tokens to supported shapes when doing multi selection apply

*  Handle groups not supported by tokens yet

* 🐛 Fix outdated tests

* ♻️ Commentary

*  Add helper functions for attribute applicability checks

* ♻️ Groups don't have own attributes

* ♻️ Remove unused function

* ♻️ Move attribute logic to common.types.token
This commit is contained in:
Florian Schrödl 2025-07-03 12:22:04 +02:00 committed by GitHub
parent 669d6d9ae2
commit 7dd61968b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 470 additions and 169 deletions

View file

@ -102,9 +102,7 @@
[:m1 {:optional true} token-name-ref]
[:m2 {:optional true} token-name-ref]
[:m3 {:optional true} token-name-ref]
[:m4 {:optional true} token-name-ref]
[:x {:optional true} token-name-ref]
[:y {:optional true} token-name-ref]])
[:m4 {:optional true} token-name-ref]])
(def spacing-keys (schema-keys schema:spacing))
@ -204,6 +202,56 @@
:stroke-width :strokes
token-attr))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKEN SHAPE ATTRIBUTES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def position-attributes #{:x :y})
(def generic-attributes
(set/union color-keys
stroke-width-keys
rotation-keys
sizing-keys
opacity-keys
position-attributes))
(def rect-attributes
(set/union generic-attributes
border-radius-keys))
(def frame-attributes
(set/union rect-attributes
spacing-keys))
(def text-attributes
(set/union generic-attributes
typography-keys
number-keys))
(defn shape-type->attributes
[type]
(case type
:bool generic-attributes
:circle generic-attributes
:rect rect-attributes
:frame frame-attributes
:image rect-attributes
:path generic-attributes
:svg-raw generic-attributes
:text text-attributes
nil))
(defn appliable-attrs
"Returns intersection of shape `attributes` for `token-type`."
[attributes token-type]
(set/intersection attributes (shape-type->attributes token-type)))
(defn any-appliable-attr?
"Checks if `token-type` supports given shape `attributes`."
[attributes token-type]
(seq (appliable-attrs attributes token-type)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS IN SHAPES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -230,13 +278,6 @@
:attributes attributes})]
(update shape :applied-tokens #(merge % applied-tokens))))
(defn maybe-apply-token-to-shape
"When the passed `:token` is non-nil apply it to the `:applied-tokens` on a shape."
[{:keys [shape token _attributes] :as props}]
(if token
(apply-token-to-shape props)
shape))
(defn unapply-token-id [shape attributes]
(update shape :applied-tokens d/without-keys attributes))

View file

@ -95,28 +95,25 @@
(cls/generate-update-shapes [(:id frame1)]
(fn [shape]
(as-> shape $
(cto/maybe-apply-token-to-shape {:token nil ; test nil case
:shape $
:attributes []})
(cto/maybe-apply-token-to-shape {:token token-radius
(cto/apply-token-to-shape {:token token-radius
:shape $
:attributes [:r1 :r2 :r3 :r4]})
(cto/maybe-apply-token-to-shape {:token token-rotation
(cto/apply-token-to-shape {:token token-rotation
:shape $
:attributes [:rotation]})
(cto/maybe-apply-token-to-shape {:token token-opacity
(cto/apply-token-to-shape {:token token-opacity
:shape $
:attributes [:opacity]})
(cto/maybe-apply-token-to-shape {:token token-stroke-width
(cto/apply-token-to-shape {:token token-stroke-width
:shape $
:attributes [:stroke-width]})
(cto/maybe-apply-token-to-shape {:token token-color
(cto/apply-token-to-shape {:token token-color
:shape $
:attributes [:stroke-color]})
(cto/maybe-apply-token-to-shape {:token token-color
(cto/apply-token-to-shape {:token token-color
:shape $
:attributes [:fill]})
(cto/maybe-apply-token-to-shape {:token token-dimensions
(cto/apply-token-to-shape {:token token-dimensions
:shape $
:attributes [:width :height]})))
(:objects page)
@ -124,7 +121,7 @@
(cls/generate-update-shapes [(:id text1)]
(fn [shape]
(as-> shape $
(cto/maybe-apply-token-to-shape {:token token-font-size
(cto/apply-token-to-shape {:token token-font-size
:shape $
:attributes [:font-size]})))
(:objects page)

View file

@ -7,7 +7,6 @@
(ns app.main.data.workspace.tokens.application
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.tokens :as cft]
[app.common.text :as txt]
[app.common.types.shape.layout :as ctsl]
@ -55,7 +54,8 @@
objects (dsh/lookup-page-objects state)
shape-ids (or (->> (select-keys objects shape-ids)
(filter (fn [[_ shape]] (not= (:type shape) :group)))
(filter (fn [[_ shape]]
(ctt/any-appliable-attr? attributes (:type shape))))
(keys))
[])
@ -455,6 +455,3 @@
(defn get-token-properties [token]
(get token-properties (:type token)))
(defn token-attributes [token-type]
(dm/get-in token-properties [token-type :attributes]))

View file

@ -10,6 +10,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.tokens :as cft]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.main.data.modal :as modal]
[app.main.data.workspace.shape-layout :as dwsl]
@ -27,6 +28,20 @@
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
;; Helpers ---------------------------------------------------------------------
(defn- key-in-map? [ks m]
(some #(contains? m %) ks))
(defn clean-separators
"Cleans up `:separator` inside of `items`
Will clean consecutive items like `[:separator :separator {}]`
And will return nil for lists consisting only of `:separator` items."
[items]
(let [items' (dedupe items)]
(when-not (every? #(= % :separator) items')
items')))
;; Actions ---------------------------------------------------------------------
(defn attribute-actions [token selected-shapes attributes]
@ -36,14 +51,15 @@
:shape-ids shape-ids
:selected-pred #(seq (% ids-by-attributes))}))
(defn generic-attribute-actions [attributes title {:keys [token selected-shapes on-update-shape hint]}]
(let [on-update-shape-fn
(defn generic-attribute-actions [attributes title {:keys [token selected-shapes on-update-shape hint allowed-shape-attributes]}]
(let [allowed-attributes (set/intersection attributes allowed-shape-attributes)
on-update-shape-fn
(or on-update-shape
(-> (dwta/get-token-properties token)
(:on-update-shape)))
{:keys [selected-pred shape-ids]}
(attribute-actions token selected-shapes attributes)]
(attribute-actions token selected-shapes allowed-attributes)]
(map (fn [attribute]
(let [selected? (selected-pred attribute)
@ -58,11 +74,12 @@
(if selected?
(st/emit! (dwta/unapply-token props))
(st/emit! (dwta/apply-token (assoc props :on-update-shape on-update-shape-fn)))))}))
attributes)))
allowed-attributes)))
(defn all-or-separate-actions [{:keys [attribute-labels on-update-shape-all on-update-shape hint]}
{:keys [token selected-shapes]}]
(let [attributes (set (keys attribute-labels))
{:keys [token selected-shapes allowed-shape-attributes]}]
(when-let [attribute-labels (seq (select-keys attribute-labels allowed-shape-attributes))]
(let [attributes (-> (keys attribute-labels) (set))
{:keys [all-selected? selected-pred shape-ids]} (attribute-actions token selected-shapes attributes)
all-action (let [props {:attributes attributes
:token token
@ -88,7 +105,7 @@
(dwta/apply-token)))]
(st/emit! event))}))
attribute-labels)]
(concat [all-action] single-actions)))
(concat (when all-action [all-action]) single-actions))))
(defn layout-spacing-items [{:keys [token selected-shapes all-attr-labels horizontal-attr-labels vertical-attr-labels on-update-shape hint]}]
(let [horizontal-attrs (into #{} (keys horizontal-attr-labels))
@ -171,43 +188,50 @@
(dwsl/update-layout shape-ids {:layout-item-margin-type :multiple}))
(dwta/update-layout-item-margin value shape-ids attributes)))
(defn spacing-attribute-actions [{:keys [token selected-shapes] :as context-data}]
(let [padding-items (layout-spacing-items {:token token
:selected-shapes selected-shapes
:all-attr-labels {:p1 "Padding top"
(defn spacing-attribute-actions [{:keys [token selected-shapes allowed-shape-attributes] :as context-data}]
(let [padding-attr-labels {:p1 "Padding top"
:p2 "Padding right"
:p3 "Padding bottom"
:p4 "Padding left"}
padding-items (when (key-in-map? allowed-shape-attributes padding-attr-labels)
(layout-spacing-items {:token token
:selected-shapes selected-shapes
:all-attr-labels padding-attr-labels
:hint (tr "workspace.tokens.paddings")
:horizontal-attr-labels {:p2 "Padding right"
:p4 "Padding left"}
:vertical-attr-labels {:p1 "Padding top"
:p3 "Padding bottom"}
:on-update-shape update-shape-layout-padding})
margin-items (layout-spacing-items {:token token
:selected-shapes selected-shapes
:all-attr-labels {:m1 "Margin top"
:on-update-shape update-shape-layout-padding}))
margin-attr-labels {:m1 "Margin top"
:m2 "Margin right"
:m3 "Margin bottom"
:m4 "Margin left"}
margin-items (when (key-in-map? allowed-shape-attributes margin-attr-labels)
(layout-spacing-items {:token token
:selected-shapes selected-shapes
:all-attr-labels margin-attr-labels
:hint (tr "workspace.tokens.margins")
:horizontal-attr-labels {:m2 "Margin right"
:m4 "Margin left"}
:vertical-attr-labels {:m1 "Margin top"
:m3 "Margin bottom"}
:on-update-shape update-shape-layout-margin})
:on-update-shape update-shape-layout-margin}))
gap-items (all-or-separate-actions {:attribute-labels {:column-gap "Column Gap"
:row-gap "Row Gap"}
:hint (tr "workspace.tokens.gaps")
:on-update-shape dwta/update-layout-spacing}
context-data)]
(concat gap-items
[:separator]
(when padding-items [:separator])
padding-items
[:separator]
(when margin-items [:separator])
margin-items)))
(defn sizing-attribute-actions [context-data]
(->>
(concat
(all-or-separate-actions {:attribute-labels {:width "Width"
:height "Height"}
@ -225,7 +249,8 @@
:layout-item-max-h "Max Height"}
:hint (tr "workspace.tokens.max-size")
:on-update-shape dwta/update-layout-sizing-limits}
context-data)))
context-data))
(clean-separators)))
(defn update-shape-radius-for-corners [value shape-ids attributes]
(st/emit!
@ -234,37 +259,44 @@
(def shape-attribute-actions-map
(let [stroke-width (partial generic-attribute-actions #{:stroke-width} "Stroke Width")
font-size (partial generic-attribute-actions #{:font-size} "Font Size")]
{:border-radius (partial all-or-separate-actions {:attribute-labels {:r1 "Top Left"
font-size (partial generic-attribute-actions #{:font-size} "Font Size")
line-height #(generic-attribute-actions #{:line-height} "Line Height" (assoc % :on-update-shape dwta/update-line-height))
border-radius (partial all-or-separate-actions {:attribute-labels {:r1 "Top Left"
:r2 "Top Right"
:r4 "Bottom Left"
:r3 "Bottom Right"}
:hint (tr "workspace.tokens.radius")
:on-update-shape-all dwta/update-shape-radius-all
:on-update-shape update-shape-radius-for-corners})
:on-update-shape update-shape-radius-for-corners})]
{:border-radius border-radius
:color (fn [context-data]
[(generic-attribute-actions #{:fill} "Fill" (assoc context-data :on-update-shape dwta/update-fill :hint (tr "workspace.tokens.color")))
(generic-attribute-actions #{:stroke-color} "Stroke" (assoc context-data :on-update-shape dwta/update-stroke-color))])
(concat
(generic-attribute-actions #{:fill} "Fill" (assoc context-data :on-update-shape dwta/update-fill :hint (tr "workspace.tokens.color")))
(generic-attribute-actions #{:stroke-color} "Stroke" (assoc context-data :on-update-shape dwta/update-stroke-color))))
:spacing spacing-attribute-actions
:sizing sizing-attribute-actions
:rotation (partial generic-attribute-actions #{:rotation} "Rotation")
:opacity (partial generic-attribute-actions #{:opacity} "Opacity")
:number (fn [context-data]
[(generic-attribute-actions #{:rotation} "Rotation" (assoc context-data :on-update-shape dwta/update-rotation))
(generic-attribute-actions #{:line-height} "Line Height" (assoc context-data :on-update-shape dwta/update-line-height))])
(concat
(generic-attribute-actions #{:rotation} "Rotation" (assoc context-data :on-update-shape dwta/update-rotation))
(let [line-height (line-height context-data)]
(when (seq line-height) line-height))))
:stroke-width stroke-width
:font-size font-size
:dimensions (fn [context-data]
(concat
[{:title "Sizing" :submenu :sizing}
{:title "Spacing" :submenu :spacing}
:separator
{:title "Border Radius" :submenu :border-radius}]
(-> (concat
(when (seq (sizing-attribute-actions context-data)) [{:title "Sizing" :submenu :sizing}])
(when (seq (spacing-attribute-actions context-data)) [{:title "Spacing" :submenu :spacing}])
[:separator]
(when (seq (border-radius context-data))
[{:title "Border Radius" :submenu :border-radius}])
[:separator]
(stroke-width (assoc context-data :on-update-shape dwta/update-stroke-width))
[:separator]
(generic-attribute-actions #{:x} "X" (assoc context-data :on-update-shape dwta/update-shape-position :hint (tr "workspace.tokens.axis")))
(generic-attribute-actions #{:y} "Y" (assoc context-data :on-update-shape dwta/update-shape-position))))}))
(generic-attribute-actions #{:y} "Y" (assoc context-data :on-update-shape dwta/update-shape-position)))
(clean-separators)))}))
(defn default-actions [{:keys [token selected-token-set-name]}]
(let [{:keys [modal]} (dwta/get-token-properties token)]
@ -290,19 +322,24 @@
(ctob/prefixed-set-path-string->set-name-string selected-token-set-name)
(:name token)))}]))
(defn selection-actions [{:keys [type token] :as context-data}]
(let [with-actions (get shape-attribute-actions-map (or type (:type token)))
(defn- allowed-shape-attributes [shapes]
(reduce into #{} (map #(ctt/shape-type->attributes (:type %)) shapes)))
(defn menu-actions [{:keys [type token selected-shapes] :as context-data}]
(let [context-data (assoc context-data :allowed-shape-attributes (allowed-shape-attributes selected-shapes))
with-actions (get shape-attribute-actions-map (or type (:type token)))
attribute-actions (if with-actions (with-actions context-data) [])]
attribute-actions))
(defn selection-actions [context-data]
(let [attribute-actions (menu-actions context-data)]
(concat
attribute-actions
(when (seq attribute-actions) [:separator])
(default-actions context-data))))
(defn submenu-actions-selection-actions [{:keys [type token] :as context-data}]
(let [with-actions (get shape-attribute-actions-map (or type (:type token)))
attribute-actions (if with-actions (with-actions context-data) [])]
(concat
attribute-actions)))
(defn submenu-actions-selection-actions [context-data]
(menu-actions context-data))
;; Components ------------------------------------------------------------------

View file

@ -12,6 +12,7 @@
[app.common.data :as d]
[app.common.files.helpers :as cfh]
[app.common.files.tokens :as cft]
[app.common.types.token :as ctt]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.color :as dwtc]
[app.main.refs :as refs]
@ -162,6 +163,12 @@
shape-ids (into #{} xf:map-id selected-shapes)]
(cft/shapes-applied-all? ids-by-attributes shape-ids attributes)))
(defn attributes-match-selection?
[selected-shapes attrs]
(some (fn [shape]
(ctt/any-appliable-attr? attrs (:type shape)))
selected-shapes))
(def token-types-with-status-icon
#{:color :border-radius :rotation :sizing :dimensions :opacity :spacing :stroke-width})
@ -174,22 +181,28 @@
is-reference? (cft/is-reference? token)
contains-path? (str/includes? name ".")
{:keys [attributes all-attributes]}
(get dwta/token-properties type)
attributes (as-> (get dwta/token-properties type) $
(d/nilv (:all-attributes $) (:attributes $)))
full-applied?
(if has-selected?
(applied-all-attributes? token selected-shapes (d/nilv all-attributes attributes))
(applied-all-attributes? token selected-shapes attributes)
true)
applied?
(if has-selected?
(cft/shapes-token-applied? token selected-shapes (d/nilv all-attributes attributes))
(cft/shapes-token-applied? token selected-shapes attributes)
false)
half-applied?
(and applied? (not full-applied?))
disabled? (and
has-selected?
(not applied?)
(not half-applied?)
(not (attributes-match-selection? selected-shapes attributes)))
;; FIXME: move to context or props
can-edit? (:can-edit (deref refs/permissions))
@ -260,6 +273,7 @@
:token-pill true
:token-pill-no-icon (and (not status-icon?) (not errors?))
:token-pill-default can-edit?
:token-pill-disabled disabled?
:token-pill-applied (and can-edit? has-selected? (or half-applied? full-applied?))
:token-pill-invalid (and can-edit? errors?)
:token-pill-invalid-applied (and full-applied? errors? can-edit?)

View file

@ -85,6 +85,11 @@
}
}
.token-pill-disabled {
opacity: 0.4;
cursor: default;
}
.token-pill-applied {
--token-pill-background: var(--color-token-background);
--token-pill-foreground: var(--color-token-foreground);

View file

@ -0,0 +1,203 @@
(ns frontend-tests.tokens.context-menu-test
(:require
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.shapes :as ths]
[app.common.test-helpers.tokens :as tht]
[app.common.types.tokens-lib :as ctob]
[app.main.ui.workspace.tokens.management.context-menu :as wtcm]
[clojure.test :as t]))
(defn setup-file []
(-> (thf/sample-file :file-1)
(tht/add-tokens-lib)
(tht/update-tokens-lib #(-> %
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
(ctob/add-theme (ctob/make-token-theme :name "test-theme"
:sets #{"test-token-set"}))
(ctob/set-active-themes #{"/test-theme"})
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-radius"
:type :border-radius
:value 10))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-color"
:type :color
:value "red"))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-spacing"
:type :spacing
:value 10))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-sizing"
:type :sizing
:value 10))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-rotation"
:type :rotation
:value 10))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-opacity"
:type :opacity
:value 10))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-dimensions"
:type :dimensions
:value 10))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-number"
:type :number
:value 10))))
;; app.main.data.workspace.tokens.application/generic-attributes
(tho/add-group :group1)
;; app.main.data.workspace.tokens.application/rect-attributes
(tho/add-rect :rect1)
;; app.main.data.workspace.tokens.application/frame-attributes
(tho/add-frame :frame1)
;; app.main.data.workspace.tokens.application/text-attributes
(tho/add-text :text1 "Hello World!")))
(defn token-menu-actions [shape-names token-name]
(let [file (setup-file)
token-set "test-token-set"
token (tht/get-token file token-set token-name)
selected-shapes (map #(ths/get-shape file %) shape-names)]
(wtcm/menu-actions
{:token token
:selected-shapes selected-shapes})))
(defn token-menu-action-labels [actions]
(mapv #(if (keyword? %) % (:title %)) actions))
(t/deftest border-radius-items
(t/testing "shows radius items for selection of supported shapes"
(let [actions (token-menu-actions [:frame1 :rect1] "token-radius")
action-titles (mapv :title actions)]
(t/is (= action-titles ["All" "Top Right" "Bottom Right" "Top Left" "Bottom Left"]))))
(t/testing "shows radius items for mixed selection"
(let [actions (token-menu-actions [:frame1 :text1] "token-radius")
action-titles (mapv :title actions)]
(t/is (= action-titles ["All" "Top Right" "Bottom Right" "Top Left" "Bottom Left"]))))
(t/testing "hides radius for unrelated shapes"
(let [actions (token-menu-actions [:text1 :group1] "token-radius")]
(t/is (empty? actions)))))
(t/deftest color-items
(t/testing "shows color items for selection of all shapes"
(let [actions (token-menu-actions [:frame1 :rect1 :group1 :text1] "token-color")
action-titles (mapv :title actions)]
(t/is (= action-titles ["Fill" "Stroke"])))))
(t/deftest spacing-items
(t/testing "shows spacing items for selection of supported shapes"
(let [actions (token-menu-actions [:frame1] "token-spacing")
action-titles (mapv #(if (keyword? %) % (:title %)) actions)]
(t/is (= action-titles ["All" "Column Gap" "Row Gap"
:separator
"All" "Horizontal" "Vertical"
"Padding top" "Padding right" "Padding bottom" "Padding left"
:separator
"All" "Horizontal" "Vertical"
"Margin top" "Margin right" "Margin bottom" "Margin left"]))))
(t/testing "shows radius items for mixed selection"
(let [actions (token-menu-actions [:frame1 :text1] "token-spacing")
action-titles (mapv #(if (keyword? %) % (:title %)) actions)]
(t/is (= action-titles ["All" "Column Gap" "Row Gap"
:separator
"All" "Horizontal" "Vertical"
"Padding top" "Padding right" "Padding bottom" "Padding left"
:separator
"All" "Horizontal" "Vertical"
"Margin top" "Margin right" "Margin bottom" "Margin left"]))))
(t/testing "hides radius for unrelated shapes"
(let [actions (token-menu-actions [:text1 :group1] "token-radius")]
(t/is (empty? actions)))))
(t/deftest sizing-items
(t/testing "shows sizing items for selection of all shapes"
(let [actions (token-menu-actions [:frame1 :rect1 :group1 :text1] "token-sizing")
action-titles (mapv #(if (keyword? %) % (:title %)) actions)]
(t/is (= action-titles ["All" "Width" "Height"
:separator
"All" "Min Width" "Min Height"
:separator
"All" "Max Width" "Max Height"]))))
(t/testing "shows no sizing items for groups"
(let [actions (token-menu-actions [:group1] "token-sizing")]
(t/is (nil? actions)))))
(t/deftest rotation-items
(t/testing "shows color items for selection of all shapes"
(let [actions (token-menu-actions [:frame1 :rect1 :group1 :text1] "token-rotation")
action-titles (mapv :title actions)]
(t/is (= action-titles ["Rotation"])))))
(t/deftest dimensions-items
(t/testing "shows `rect-attributes` dimension items for rect"
(let [actions (token-menu-actions [:rect1] "token-dimensions")
action-titles (mapv #(if (keyword? %) % (select-keys % [:title :submenu])) actions)]
(t/is (= action-titles [{:title "Sizing", :submenu :sizing}
:separator
{:title "Border Radius", :submenu :border-radius}
:separator
{:title "Stroke Width"}
:separator
{:title "X"}
{:title "Y"}]))))
(t/testing "shows all attribute dimension items for frame"
(let [actions (token-menu-actions [:frame1] "token-dimensions")
action-titles (mapv #(if (keyword? %) % (select-keys % [:title :submenu])) actions)]
(t/is (= action-titles [{:title "Sizing", :submenu :sizing}
{:title "Spacing", :submenu :spacing}
:separator
{:title "Border Radius", :submenu :border-radius}
:separator
{:title "Stroke Width"}
:separator
{:title "X"}
{:title "Y"}]))))
(t/testing "shows `text-attributes` dimension items for text"
(let [actions (token-menu-actions [:text1] "token-dimensions")
action-titles (mapv #(if (keyword? %) % (select-keys % [:title :submenu])) actions)]
(t/is (= action-titles [{:title "Sizing", :submenu :sizing}
:separator
{:title "Stroke Width"}
:separator
{:title "X"}
{:title "Y"}]))))
(t/testing "not attributes for groups as they are not supported yet"
(let [actions (token-menu-actions [:group1] "token-dimensions")]
(t/is (nil? actions)))))
(t/deftest number-items
(t/testing "shows all number attribute items for text"
(let [actions (token-menu-actions [:text1] "token-number")
action-titles (mapv :title actions)]
(t/is (= action-titles ["Rotation" "Line Height"]))))
(t/testing "shows non text attributes for non text shapes"
(let [actions (token-menu-actions [:frame1 :rect1 :group1] "token-number")
action-titles (mapv :title actions)]
(t/is (= action-titles ["Rotation"])))))
(t/deftest stroke-width-items
(t/testing "shows stroke width items for all shapes"
(let [actions (token-menu-actions [:frame1 :rect1 :group1 :text1] "token-dimensions")
stroke-width-action (first (filter #(and (map? %) (= (:title %) "Stroke Width")) actions))]
(t/is (some? stroke-width-action))
(t/is (= (:title stroke-width-action) "Stroke Width")))))
(t/deftest opacity-items
(t/testing "shows opacity items for all shapes"
(let [actions (token-menu-actions [:frame1 :rect1 :group1 :text1] "token-opacity")
action-titles (mapv :title actions)]
(t/is (= action-titles ["Opacity"])))))

View file

@ -195,7 +195,7 @@
rect-1 (cths/get-shape file :rect-1)
rect-2 (cths/get-shape file :rect-2)
events [(dwta/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:color}
:attributes #{:fill}
:token (toht/get-token file "color.primary")
:on-update-shape dwta/update-fill})
(dwta/apply-token {:shape-ids [(:id rect-1)]
@ -203,7 +203,7 @@
:token (toht/get-token file "color.primary")
:on-update-shape dwta/update-stroke-color})
(dwta/apply-token {:shape-ids [(:id rect-2)]
:attributes #{:color}
:attributes #{:fill}
:token (toht/get-token file "color.secondary")
:on-update-shape dwta/update-fill})
(dwta/apply-token {:shape-ids [(:id rect-2)]
@ -214,25 +214,25 @@
store done events
(fn [new-state]
(let [file' (ths/get-file-from-state new-state)
token-target' (toht/get-token file' "rotation.medium")
primary-target (toht/get-token file' "color.primary")
secondary-target (toht/get-token file' "color.secondary")
rect-1' (cths/get-shape file' :rect-1)
rect-2' (cths/get-shape file' :rect-2)]
(t/testing "regular color"
(t/is (some? (:applied-tokens rect-1')))
(t/is (= (:fill (:applied-tokens rect-1')) (:name token-target')))
(t/is (= (:fill (:applied-tokens rect-1')) (:name primary-target)))
(t/is (= (get-in rect-1' [:fills 0 :fill-color]) "#ff0000"))
(t/is (= (:stroke (:applied-tokens rect-1')) (:name token-target')))
(t/is (= (:stroke-color (:applied-tokens rect-1')) (:name primary-target)))
(t/is (= (get-in rect-1' [:strokes 0 :stroke-color]) "#ff0000")))
(t/testing "color with alpha channel"
(t/is (some? (:applied-tokens rect-2')))
(t/is (= (:fill (:applied-tokens rect-2')) (:name token-target')))
(t/is (= (:fill (:applied-tokens rect-2')) (:name secondary-target)))
(t/is (= (get-in rect-2' [:fills 0 :fill-color]) "#ff0000"))
(t/is (= (get-in rect-2' [:fills 0 :fill-opacity]) 0.5))
(t/is (= (:stroke (:applied-tokens rect-2')) (:name token-target')))
(t/is (= (:stroke-color (:applied-tokens rect-2')) (:name secondary-target)))
(t/is (= (get-in rect-2' [:strokes 0 :stroke-color]) "#ff0000"))
(t/is (= (get-in rect-2' [:strokes 0 :stroke-opacity]) 0.5))))))))))
@ -270,33 +270,40 @@
(t/testing "applies padding token to shapes with layout"
(t/async
done
(let [dimensions-token {:name "padding.sm"
(let [spacing-token {:name "padding.sm"
:value "100"
:type :spacing}
file (-> (setup-file-with-tokens)
(ctho/add-frame :frame-1)
(ctho/add-frame :frame-2 {:layout :grid})
(update-in [:data :tokens-lib]
#(ctob/add-token-in-set % "Set A" (ctob/make-token dimensions-token))))
#(ctob/add-token-in-set % "Set A" (ctob/make-token spacing-token))))
store (ths/setup-store file)
frame-1 (cths/get-shape file :frame-1)
frame-2 (cths/get-shape file :frame-2)
events [(dwta/apply-token {:shape-ids [(:id frame-1) (:id frame-2)]
:attributes #{:padding}
:attributes #{:p1 :p2 :p3 :p4}
:token (toht/get-token file "padding.sm")
:on-update-shape dwta/update-layout-padding})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-state new-state)
token-target' (toht/get-token file' "dimensions.sm")
token-target' (toht/get-token file' "padding.sm")
frame-1' (cths/get-shape file' :frame-1)
frame-2' (cths/get-shape file' :frame-2)]
(t/testing "shape `:applied-tokens` got updated"
(t/is (= (:spacing (:applied-tokens frame-1')) (:name token-target')))
(t/is (= (:spacing (:applied-tokens frame-2')) (:name token-target'))))
(t/is (= (:p1 (:applied-tokens frame-1')) (:name token-target')))
(t/is (= (:p2 (:applied-tokens frame-1')) (:name token-target')))
(t/is (= (:p3 (:applied-tokens frame-1')) (:name token-target')))
(t/is (= (:p4 (:applied-tokens frame-1')) (:name token-target')))
(t/is (= (:p1 (:applied-tokens frame-2')) (:name token-target')))
(t/is (= (:p2 (:applied-tokens frame-2')) (:name token-target')))
(t/is (= (:p3 (:applied-tokens frame-2')) (:name token-target')))
(t/is (= (:p4 (:applied-tokens frame-2')) (:name token-target'))))
(t/testing "shapes padding got updated"
(t/is (= (:layout-padding frame-2') {:padding 100})))
(t/is (= (:layout-padding frame-2') {:p1 100 :p2 100 :p3 100 :p4 100})))
(t/testing "shapes without layout get ignored"
(t/is (nil? (:layout-padding frame-1')))))))))))