mirror of
https://github.com/penpot/penpot.git
synced 2025-07-18 14:57:12 +02:00
✨ 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:
parent
669d6d9ae2
commit
7dd61968b5
8 changed files with 470 additions and 169 deletions
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)
|
||||
{}))
|
||||
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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 ------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -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?)
|
||||
|
|
|
@ -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);
|
||||
|
|
203
frontend/test/frontend_tests/tokens/context_menu_test.cljs
Normal file
203
frontend/test/frontend_tests/tokens/context_menu_test.cljs
Normal 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"])))))
|
|
@ -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')))))))))))
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue