From 7dd61968b59df7b7dde3625bb474af44e3e97e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Schr=C3=B6dl?= Date: Thu, 3 Jul 2025 12:22:04 +0200 Subject: [PATCH] :sparkles: Implement object type specific tokens (#6816) * :sparkles: Allow token applying for supported shape types only * :bug: Remove x/y attribute keys from spacing token * :sparkles: Shape specific context-menu * :sparkles: Only apply tokens to supported shapes when doing multi selection apply * :sparkles: Handle groups not supported by tokens yet * :bug: Fix outdated tests * :recycle: Commentary * :sparkles: Add helper functions for attribute applicability checks * :recycle: Groups don't have own attributes * :recycle: Remove unused function * :recycle: Move attribute logic to common.types.token --- common/src/app/common/types/token.cljc | 61 ++++- .../common_tests/logic/token_apply_test.cljc | 51 ++-- .../data/workspace/tokens/application.cljs | 7 +- .../tokens/management/context_menu.cljs | 249 ++++++++++-------- .../tokens/management/token_pill.cljs | 22 +- .../tokens/management/token_pill.scss | 5 + .../tokens/context_menu_test.cljs | 203 ++++++++++++++ .../tokens/logic/token_actions_test.cljs | 41 +-- 8 files changed, 470 insertions(+), 169 deletions(-) create mode 100644 frontend/test/frontend_tests/tokens/context_menu_test.cljs diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index e961ddc282..7e2a06e178 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -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)) diff --git a/common/test/common_tests/logic/token_apply_test.cljc b/common/test/common_tests/logic/token_apply_test.cljc index e75a284f85..c8d41f53af 100644 --- a/common/test/common_tests/logic/token_apply_test.cljc +++ b/common/test/common_tests/logic/token_apply_test.cljc @@ -95,38 +95,35 @@ (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 - :shape $ - :attributes [:r1 :r2 :r3 :r4]}) - (cto/maybe-apply-token-to-shape {:token token-rotation - :shape $ - :attributes [:rotation]}) - (cto/maybe-apply-token-to-shape {:token token-opacity - :shape $ - :attributes [:opacity]}) - (cto/maybe-apply-token-to-shape {:token token-stroke-width - :shape $ - :attributes [:stroke-width]}) - (cto/maybe-apply-token-to-shape {:token token-color - :shape $ - :attributes [:stroke-color]}) - (cto/maybe-apply-token-to-shape {:token token-color - :shape $ - :attributes [:fill]}) - (cto/maybe-apply-token-to-shape {:token token-dimensions - :shape $ - :attributes [:width :height]}))) + (cto/apply-token-to-shape {:token token-radius + :shape $ + :attributes [:r1 :r2 :r3 :r4]}) + (cto/apply-token-to-shape {:token token-rotation + :shape $ + :attributes [:rotation]}) + (cto/apply-token-to-shape {:token token-opacity + :shape $ + :attributes [:opacity]}) + (cto/apply-token-to-shape {:token token-stroke-width + :shape $ + :attributes [:stroke-width]}) + (cto/apply-token-to-shape {:token token-color + :shape $ + :attributes [:stroke-color]}) + (cto/apply-token-to-shape {:token token-color + :shape $ + :attributes [:fill]}) + (cto/apply-token-to-shape {:token token-dimensions + :shape $ + :attributes [:width :height]}))) (:objects page) {}) (cls/generate-update-shapes [(:id text1)] (fn [shape] (as-> shape $ - (cto/maybe-apply-token-to-shape {:token token-font-size - :shape $ - :attributes [:font-size]}))) + (cto/apply-token-to-shape {:token token-font-size + :shape $ + :attributes [:font-size]}))) (:objects page) {})) diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index 21c3bc193d..778398fc11 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -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])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs index 8496a254f2..bc96b1ad76 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs @@ -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,37 +74,38 @@ (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 [all-selected? selected-pred shape-ids]} (attribute-actions token selected-shapes attributes) - all-action (let [props {:attributes attributes - :token token - :shape-ids shape-ids}] - {:title (tr "labels.all") - :selected? all-selected? - :hint hint - :action #(if all-selected? - (st/emit! (dwta/unapply-token props)) - (st/emit! (dwta/apply-token (assoc props :on-update-shape (or on-update-shape-all on-update-shape)))))}) - single-actions (map (fn [[attr title]] - (let [selected? (selected-pred attr)] - {:title title - :selected? (and (not all-selected?) selected?) - :action #(let [props {:attributes #{attr} - :token token - :shape-ids shape-ids} - event (cond - all-selected? (-> (assoc props :attributes-to-remove attributes) - (dwta/apply-token)) - selected? (dwta/unapply-token props) - :else (-> (assoc props :on-update-shape on-update-shape) - (dwta/apply-token)))] - (st/emit! event))})) - attribute-labels)] - (concat [all-action] single-actions))) + {: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 + :shape-ids shape-ids}] + {:title (tr "labels.all") + :selected? all-selected? + :hint hint + :action #(if all-selected? + (st/emit! (dwta/unapply-token props)) + (st/emit! (dwta/apply-token (assoc props :on-update-shape (or on-update-shape-all on-update-shape)))))}) + single-actions (map (fn [[attr title]] + (let [selected? (selected-pred attr)] + {:title title + :selected? (and (not all-selected?) selected?) + :action #(let [props {:attributes #{attr} + :token token + :shape-ids shape-ids} + event (cond + all-selected? (-> (assoc props :attributes-to-remove attributes) + (dwta/apply-token)) + selected? (dwta/unapply-token props) + :else (-> (assoc props :on-update-shape on-update-shape) + (dwta/apply-token)))] + (st/emit! event))})) + attribute-labels)] + (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,61 +188,69 @@ (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" - :p2 "Padding right" - :p3 "Padding bottom" - :p4 "Padding left"} - :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" - :m2 "Margin right" - :m3 "Margin bottom" - :m4 "Margin left"} - :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}) + + +(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-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})) 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"} - :hint (tr "workspace.tokens.size") - :on-update-shape dwta/update-shape-dimensions} - context-data) - [:separator] - (all-or-separate-actions {:attribute-labels {:layout-item-min-w "Min Width" - :layout-item-min-h "Min Height"} - :hint (tr "workspace.tokens.min-size") - :on-update-shape dwta/update-layout-sizing-limits} - context-data) - [:separator] - (all-or-separate-actions {:attribute-labels {:layout-item-max-w "Max Width" - :layout-item-max-h "Max Height"} - :hint (tr "workspace.tokens.max-size") - :on-update-shape dwta/update-layout-sizing-limits} - context-data))) + (->> + (concat + (all-or-separate-actions {:attribute-labels {:width "Width" + :height "Height"} + :hint (tr "workspace.tokens.size") + :on-update-shape dwta/update-shape-dimensions} + context-data) + [:separator] + (all-or-separate-actions {:attribute-labels {:layout-item-min-w "Min Width" + :layout-item-min-h "Min Height"} + :hint (tr "workspace.tokens.min-size") + :on-update-shape dwta/update-layout-sizing-limits} + context-data) + [:separator] + (all-or-separate-actions {:attribute-labels {:layout-item-max-w "Max Width" + :layout-item-max-h "Max Height"} + :hint (tr "workspace.tokens.max-size") + :on-update-shape dwta/update-layout-sizing-limits} + 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" - :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}) + 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})] + {: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}] - [: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))))})) + (-> (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))) + (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 ------------------------------------------------------------------ diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs index 085597795e..fa4890d9f0 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs @@ -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?) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.scss b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.scss index 6379e77f81..f6e276ecf5 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.scss @@ -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); diff --git a/frontend/test/frontend_tests/tokens/context_menu_test.cljs b/frontend/test/frontend_tests/tokens/context_menu_test.cljs new file mode 100644 index 0000000000..5341bf52b8 --- /dev/null +++ b/frontend/test/frontend_tests/tokens/context_menu_test.cljs @@ -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"]))))) diff --git a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs index b717fb8b3d..8c0ed6e59c 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -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" - :value "100" - :type :spacing} + (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')))))))))))