Fix spacing token for frame children

This commit is contained in:
Florian Schroedl 2025-07-23 14:52:39 +02:00 committed by Andrés Moya
parent f7627e515a
commit 1f15e9b81e
6 changed files with 182 additions and 106 deletions

View file

@ -92,19 +92,32 @@
(def opacity-keys (schema-keys schema:opacity))
(def ^:private schema:spacing
(def ^:private schema:spacing-gap
[:map
[:row-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]])
(def ^:private schema:spacing-padding
[:map
[:p1 {:optional true} token-name-ref]
[:p2 {:optional true} token-name-ref]
[:p3 {:optional true} token-name-ref]
[:p4 {:optional true} token-name-ref]
[:p4 {:optional true} token-name-ref]])
(def ^:private schema:spacing-margin
[:map
[:m1 {:optional true} token-name-ref]
[:m2 {:optional true} token-name-ref]
[:m3 {:optional true} token-name-ref]
[:m4 {:optional true} token-name-ref]])
(def ^:private schema:spacing
(reduce mu/union [schema:spacing-gap
schema:spacing-padding
schema:spacing-margin]))
(def spacing-margin-keys (schema-keys schema:spacing-margin))
(def spacing-keys (schema-keys schema:spacing))
(def ^:private schema:dimensions

View file

@ -31,88 +31,6 @@
(declare token-properties)
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
(defn apply-token
"Apply `attributes` that match `token` for `shape-ids`.
Optionally remove attributes from `attributes-to-remove`,
this is useful for applying a single attribute from an attributes set
while removing other applied tokens from this set."
[{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}]
(ptk/reify ::apply-token
ptk/WatchEvent
(watch [_ state _]
;; We do not allow to apply tokens while text editor is open.
(when (empty? (get state :workspace-editor-state))
(when-let [tokens (some-> (dsh/lookup-file-data state)
(get :tokens-lib)
(ctob/get-tokens-in-active-sets))]
(->> (sd/resolve-tokens tokens)
(rx/mapcat
(fn [resolved-tokens]
(let [undo-id (js/Symbol)
objects (dsh/lookup-page-objects state)
shape-ids (or (->> (select-keys objects shape-ids)
(filter (fn [[_ shape]]
(ctt/any-appliable-attr? attributes (:type shape))))
(keys))
[])
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
tokenized-attributes (cft/attributes-map attributes token)]
(rx/of
(st/emit! (ptk/event ::ev/event {::ev/name "apply-tokens"}))
(dwu/start-undo-transaction undo-id)
(dwsh/update-shapes shape-ids (fn [shape]
(cond-> shape
attributes-to-remove
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
:always
(update :applied-tokens merge tokenized-attributes))))
(when on-update-shape
(on-update-shape resolved-value shape-ids attributes))
(dwu/commit-undo-transaction undo-id)))))))))))
(defn unapply-token
"Removes `attributes` that match `token` for `shape-ids`.
Doesn't update shape attributes."
[{:keys [attributes token shape-ids] :as _props}]
(ptk/reify ::unapply-token
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
(dwsh/update-shapes
shape-ids
(fn [shape]
(update shape :applied-tokens remove-token))))))))
(defn toggle-token
[{:keys [token shapes]}]
(ptk/reify ::on-toggle-token
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [attributes all-attributes on-update-shape]}
(get token-properties (:type token))
unapply-tokens?
(cft/shapes-token-applied? token shapes (or all-attributes attributes))
shape-ids (map :id shapes)]
(if unapply-tokens?
(rx/of
(unapply-token {:attributes (or all-attributes attributes)
:token token
:shape-ids shape-ids}))
(rx/of
(apply-token {:attributes attributes
:token token
:shape-ids shape-ids
:on-update-shape on-update-shape})))))))
;; Events to update the value of attributes with applied tokens ---------------------------------------------------------
;; (note that dwsh/update-shapes function returns an event)
@ -380,6 +298,123 @@
{:ignore-touched true
:page-id page-id})))))
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
(defn apply-token
"Apply `attributes` that match `token` for `shape-ids`.
Optionally remove attributes from `attributes-to-remove`,
this is useful for applying a single attribute from an attributes set
while removing other applied tokens from this set."
[{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}]
(ptk/reify ::apply-token
ptk/WatchEvent
(watch [_ state _]
;; We do not allow to apply tokens while text editor is open.
(when (empty? (get state :workspace-editor-state))
(when-let [tokens (some-> (dsh/lookup-file-data state)
(get :tokens-lib)
(ctob/get-tokens-in-active-sets))]
(->> (sd/resolve-tokens tokens)
(rx/mapcat
(fn [resolved-tokens]
(let [undo-id (js/Symbol)
objects (dsh/lookup-page-objects state)
selected-shapes (select-keys objects shape-ids)
shape-ids (or (->> selected-shapes
(filter (fn [[_ shape]]
(or
(and (ctsl/any-layout-immediate-child? objects shape)
(some ctt/spacing-margin-keys attributes))
(ctt/any-appliable-attr? attributes (:type shape)))))
(keys))
[])
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
tokenized-attributes (cft/attributes-map attributes token)]
(rx/of
(st/emit! (ptk/event ::ev/event {::ev/name "apply-tokens"}))
(dwu/start-undo-transaction undo-id)
(dwsh/update-shapes shape-ids (fn [shape]
(cond-> shape
attributes-to-remove
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
:always
(update :applied-tokens merge tokenized-attributes))))
(when on-update-shape
(on-update-shape resolved-value shape-ids attributes))
(dwu/commit-undo-transaction undo-id)))))))))))
(defn apply-spacing-token
"Handles edge-case for spacing token when applying token via toggle button.
Splits out `shape-ids` into seperate default actions:
- Layouts take the `default` update function
- Shapes inside layout will only take margin"
[{:keys [token shapes]}]
(ptk/reify ::apply-spacing-token
ptk/WatchEvent
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
{:keys [attributes on-update-shape]}
(get token-properties (:type token))
{:keys [other frame-children]}
(group-by #(if (ctsl/any-layout-immediate-child? objects %) :frame-children :other) shapes)]
(rx/of
(apply-token {:attributes attributes
:token token
:shape-ids (map :id other)
:on-update-shape on-update-shape})
(apply-token {:attributes ctt/spacing-margin-keys
:token token
:shape-ids (map :id frame-children)
:on-update-shape update-layout-item-margin}))))))
(defn unapply-token
"Removes `attributes` that match `token` for `shape-ids`.
Doesn't update shape attributes."
[{:keys [attributes token shape-ids] :as _props}]
(ptk/reify ::unapply-token
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
(dwsh/update-shapes
shape-ids
(fn [shape]
(update shape :applied-tokens remove-token))))))))
(defn toggle-token
[{:keys [token shapes]}]
(ptk/reify ::on-toggle-token
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [attributes all-attributes on-update-shape]}
(get token-properties (:type token))
unapply-tokens?
(cft/shapes-token-applied? token shapes (or all-attributes attributes))
shape-ids (map :id shapes)]
(if unapply-tokens?
(rx/of
(unapply-token {:attributes (or all-attributes attributes)
:token token
:shape-ids shape-ids}))
(rx/of
(case (:type token)
:spacing
(apply-spacing-token {:token token
:shapes shapes})
(apply-token {:attributes attributes
:token token
:shape-ids shape-ids
:on-update-shape on-update-shape}))))))))
;; Map token types to different properties used along the cokde ---------------------------------------------
;; FIXME: the values should be lazy evaluated, probably a function,

View file

@ -2,6 +2,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.types.shape.layout :as ctsl]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
@ -61,6 +62,10 @@
(mf/with-memo [selected objects]
(into [] (keep (d/getf objects)) selected))
is-selected-inside-layout
(mf/with-memo [selected-shapes objects]
(some #(ctsl/any-layout-immediate-child? objects %) selected-shapes))
active-theme-tokens
(mf/with-memo [tokens-lib]
(if tokens-lib
@ -148,6 +153,7 @@
:is-open (get open-status type false)
:type type
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens'
:tokens tokens}]))
@ -155,5 +161,6 @@
[:> token-group* {:key (name type)
:type type
:selected-shapes selected-shapes
:is-selected-inside-layout :is-selected-inside-layout
:active-theme-tokens active-theme-tokens'
:tokens []}])]))

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.shape.layout :as ctsl]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.main.data.modal :as modal]
@ -34,11 +35,13 @@
(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."
"Cleans up `:separator` inside of `items` with these rules:
- Clean consecutive items like `[:separator :separator {}]`
- Returns nil for lists consisting only of `:separator` items.
- Removes `:separator` at the beginning of the `items`"
[items]
(let [items' (dedupe items)]
(let [items' (->> (dedupe items)
(drop-while #(= % :separator)))]
(when-not (every? #(= % :separator) items')
items')))
@ -190,7 +193,7 @@
(defn spacing-attribute-actions [{:keys [token selected-shapes allowed-shape-attributes] :as context-data}]
(defn spacing-attribute-actions [{:keys [token selected-shapes allowed-shape-attributes is-selected-inside-layout] :as context-data}]
(let [padding-attr-labels {:p1 "Padding top"
:p2 "Padding right"
:p3 "Padding bottom"
@ -209,7 +212,9 @@
:m2 "Margin right"
:m3 "Margin bottom"
:m4 "Margin left"}
margin-items (when (key-in-map? allowed-shape-attributes margin-attr-labels)
margin-items (when (or
is-selected-inside-layout
(key-in-map? allowed-shape-attributes margin-attr-labels))
(layout-spacing-items {:token token
:selected-shapes selected-shapes
:all-attr-labels margin-attr-labels
@ -224,11 +229,13 @@
:hint (tr "workspace.tokens.gaps")
:on-update-shape dwta/update-layout-spacing}
context-data)]
(concat gap-items
(when padding-items [:separator])
padding-items
(when margin-items [:separator])
margin-items)))
(->> (concat
gap-items
[:separator]
padding-items
[:separator]
margin-items)
(clean-separators))))
(defn sizing-attribute-actions [context-data]
(->>
@ -446,9 +453,17 @@
(mf/defc token-context-menu-tree
[{:keys [width errors] :as mdata}]
(let [objects (mf/deref refs/workspace-page-objects)
(let [objects (mf/deref refs/workspace-page-objects)
selected (mf/deref refs/selected-shapes)
selected-shapes (into [] (keep (d/getf objects)) selected)
selected-shapes
(mf/with-memo [selected objects]
(into [] (keep (d/getf objects)) selected))
is-selected-inside-layout
(mf/with-memo [selected-shapes objects]
(some #(ctsl/any-layout-immediate-child? objects %) selected-shapes))
token-name (:token-name mdata)
token (mf/deref (refs/workspace-token-in-selected-set token-name))
selected-token-set-name (mf/deref refs/selected-token-set-name)]
@ -457,7 +472,8 @@
:token token
:errors errors
:selected-token-set-name selected-token-set-name
:selected-shapes selected-shapes}]]))
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout}]]))
(mf/defc token-context-menu
[]

View file

@ -42,7 +42,7 @@
(mf/defc token-group*
{::mf/private true}
[{:keys [type tokens selected-shapes active-theme-tokens is-open]}]
[{:keys [type tokens selected-shapes is-selected-inside-layout active-theme-tokens is-open]}]
(let [{:keys [modal title]}
(get dwta/token-properties type)
editing-ref (mf/deref refs/workspace-editor-state)
@ -115,6 +115,7 @@
{:key (:name token)
:token token
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens
:on-click on-token-pill-click
:on-context-menu on-context-menu}])]])]]))

View file

@ -21,6 +21,7 @@
[app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[clojure.set :as set]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@ -164,17 +165,20 @@
(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))
[selected-shapes attrs & {:keys [selected-inside-layout?]}]
(or
;; Edge-case for allowing margin attribute on shapes inside layout parent
(and selected-inside-layout? (set/subset? ctt/spacing-margin-keys 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})
(mf/defc token-pill*
{::mf/wrap [mf/memo]}
[{:keys [on-click token on-context-menu selected-shapes active-theme-tokens]}]
[{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}]
(let [{:keys [name value errors type]} token
has-selected? (pos? (count selected-shapes))
@ -201,7 +205,7 @@
has-selected?
(not applied?)
(not half-applied?)
(not (attributes-match-selection? selected-shapes attributes)))
(not (attributes-match-selection? selected-shapes attributes {:selected-inside-layout? is-selected-inside-layout})))
;; FIXME: move to context or props
can-edit? (:can-edit (deref refs/permissions))