diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 9ef1571e53..afd7279808 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -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 diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index 0ae82fd980..b457963c01 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -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, diff --git a/frontend/src/app/main/ui/workspace/tokens/management.cljs b/frontend/src/app/main/ui/workspace/tokens/management.cljs index bfc533364b..19d606c955 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management.cljs @@ -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 []}])])) 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 bc96b1ad76..86a054e336 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.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 [] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs index c96ed54cdf..2a55e65960 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -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}])]])]])) 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 fa4890d9f0..669eb6056d 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 @@ -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))