diff --git a/frontend/package.json b/frontend/package.json index 877357e28..a56170864 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,7 +5,9 @@ "author": "Kaleidos INC", "private": true, "packageManager": "yarn@4.2.2", - "browserslist": ["defaults"], + "browserslist": [ + "defaults" + ], "type": "module", "repository": { "type": "git", diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index f1a90d8b0..3cfc57b7b 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -30,7 +30,6 @@ [app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]] [app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]] [app.main.ui.workspace.sidebar.history :refer [history-toolbox]] - [app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]] [app.main.ui.workspace.tokens.modals] [app.main.ui.workspace.viewport :refer [viewport]] [app.util.debug :as dbg] @@ -206,7 +205,6 @@ :style {:background-color background-color :touch-action "none"}} [:& context-menu] - [:& token-context-menu] (if ^boolean file-ready? [:& workspace-page {:page-id page-id diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index 47e7904c1..b2f71ae23 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -14,10 +14,8 @@ [app.config :as cf] [app.main.data.events :as-alias ev] [app.main.data.workspace :as udw] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.grid-layout.editor :as dwge] [app.main.data.workspace.shape-layout :as dwsl] - [app.main.data.workspace.undo :as dwu] [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] @@ -31,6 +29,7 @@ [app.main.ui.icons :as i] [app.main.ui.workspace.tokens.core :as wtc] [app.main.ui.workspace.tokens.editable-select :refer [editable-select]] + [app.main.ui.workspace.tokens.token-types :as wtty] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -864,7 +863,7 @@ (wtc/tokens-name-map->select-options {:shape shape :tokens spacing-tokens - :attributes (wtc/token-attributes :spacing) + :attributes (wtty/token-attributes :spacing) :selected-attributes #{:spacing-column}}))) spacing-row-options (mf/use-memo @@ -873,7 +872,7 @@ (wtc/tokens-name-map->select-options {:shape shape :tokens spacing-tokens - :attributes (wtc/token-attributes :spacing) + :attributes (wtty/token-attributes :spacing) :selected-attributes #{:spacing-row}}))) padding-x-options (mf/use-memo @@ -882,7 +881,7 @@ (wtc/tokens-name-map->select-options {:shape shape :tokens spacing-tokens - :attributes (wtc/token-attributes :spacing) + :attributes (wtty/token-attributes :spacing) :selected-attributes #{:padding-p1 :padding-p3}}))) padding-y-options (mf/use-memo @@ -891,7 +890,7 @@ (wtc/tokens-name-map->select-options {:shape shape :tokens spacing-tokens - :attributes (wtc/token-attributes :spacing) + :attributes (wtty/token-attributes :spacing) :selected-attributes #{:padding-p2 :padding-p4}}))) on-add-layout diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index ef8eb85e3..63d4c6c77 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -27,6 +27,7 @@ [app.main.ui.workspace.tokens.core :as wtc] [app.main.ui.workspace.tokens.editable-select :refer [editable-select]] [app.main.ui.workspace.tokens.style-dictionary :as sd] + [app.main.ui.workspace.tokens.token-types :as wtty] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [clojure.set :refer [rename-keys union]] @@ -109,21 +110,21 @@ #(wtc/tokens-name-map->select-options {:shape shape :tokens border-radius-tokens - :attributes (wtc/token-attributes :border-radius)})) + :attributes (wtty/token-attributes :border-radius)})) sizing-tokens (:sizing tokens-by-type) width-options (mf/use-memo (mf/deps shape sizing-tokens) #(wtc/tokens-name-map->select-options {:shape shape :tokens sizing-tokens - :attributes (wtc/token-attributes :sizing) + :attributes (wtty/token-attributes :sizing) :selected-attributes #{:width}})) height-options (mf/use-memo (mf/deps shape sizing-tokens) #(wtc/tokens-name-map->select-options {:shape shape :tokens sizing-tokens - :attributes (wtc/token-attributes :sizing) + :attributes (wtty/token-attributes :sizing) :selected-attributes #{:height}})) flex-child? (->> selection-parents (some ctl/flex-layout?)) @@ -330,7 +331,7 @@ (let [token-value (wtc/maybe-resolve-token-value token)] (st/emit! (change-radius (fn [shape] - (-> (dt/unapply-token-id shape (wtc/token-attributes :border-radius)) + (-> (dt/unapply-token-id shape (wtty/token-attributes :border-radius)) (ctsr/set-radius-1 token-value)))))))) on-radius-1-change @@ -342,7 +343,7 @@ (change-radius (fn [shape] (-> (dt/maybe-apply-token-to-shape {:token (when token-value value) :shape shape - :attributes (wtc/token-attributes :border-radius)}) + :attributes (wtty/token-attributes :border-radius)}) (ctsr/set-radius-1 (or token-value value))))))))) on-radius-multi-change diff --git a/frontend/src/app/main/ui/workspace/tokens/CHANGELOG.md b/frontend/src/app/main/ui/workspace/tokens/CHANGELOG.md index 5574e332c..7347888c4 100644 --- a/frontend/src/app/main/ui/workspace/tokens/CHANGELOG.md +++ b/frontend/src/app/main/ui/workspace/tokens/CHANGELOG.md @@ -18,6 +18,25 @@ If possible add video here from PR as well ## Changes +### 2024-07-25 - UX Improvements for the context menu + +[Link to PR](https://github.com/tokens-studio/tokens-studio-for-penpot/pull/224) + +Changes context menu behavior according to [Specs](https://github.com/tokens-studio/obsidian-docs/blob/31f0d7f98ff5ac922970f3009fe877cc02d6d0cd/Products/TS%20for%20Penpot/Specs/Token%20State%20Specs.md) + +- Removing a token wont update the shape +- Mixed selection (shapes with applied, shapes without applied) will always unapply token +- Multi selection of shapes without token will apply the token to all +- Every shape change and token applying should be one undo step now +- Prevent token applying when nothign is selected +- `All` is a toggle instead of a checkbox if all tokens have been applied + - For instance with border radius the context menu can `:r1 :r2 :r3 :r4` which will highlight `All` + - If one attribute is missing it will check the single attributes + - Clicking a single attribute after clicking `All` will remove the other attributes +- Fixed some issues for switching between split and uniform border radius +- Clicking a token wont apply all attributes anymore. We apply only a select collection of attributes, which makes most sense. For instance on `sizing` we only apply `width` and `height` instead of all (`max-width`, `max-height`, `min-heigt`, `min-width`) + + ### 2024-07-05 - UX Improvements when applying tokens [Link to PR](https://github.com/tokens-studio/tokens-studio-for-penpot/compare/token-studio-develop...ux-improvements?body=&expand=1) diff --git a/frontend/src/app/main/ui/workspace/tokens/changes.cljs b/frontend/src/app/main/ui/workspace/tokens/changes.cljs new file mode 100644 index 000000000..915216bef --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/changes.cljs @@ -0,0 +1,176 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.changes + (:require + [app.common.types.shape.radius :as ctsr] + [app.common.types.token :as ctt] + [app.main.data.tokens :as dt] + [app.main.data.workspace :as udw] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.shape-layout :as dwsl] + [app.main.data.workspace.state-helpers :as wsh] + [app.main.data.workspace.transforms :as dwt] + [app.main.data.workspace.undo :as dwu] + [app.main.store :as st] + [app.main.ui.workspace.tokens.style-dictionary :as sd] + [app.main.ui.workspace.tokens.token :as wtt] + [beicon.v2.core :as rx] + [clojure.set :as set] + [potok.v2.core :as ptk] + [promesa.core :as p])) + +;; Token Updates --------------------------------------------------------------- + +(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] :as _props}] + (ptk/reify ::apply-token + ptk/WatchEvent + (watch [_ state _] + (->> (rx/from (sd/resolve-tokens+ (get-in state [:workspace-data :tokens]))) + (rx/mapcat + (fn [sd-tokens] + (let [undo-id (js/Symbol) + resolved-value (-> (get sd-tokens (:id token)) + (wtt/resolve-token-value)) + tokenized-attributes (wtt/attributes-map attributes (:id token))] + (rx/of + (dwu/start-undo-transaction undo-id) + (dch/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 % (wtt/remove-attributes-for-token-id attributes (:id token) %))] + (dch/update-shapes + shape-ids + (fn [shape] + (update shape :applied-tokens remove-token)))))))) + +(defn toggle-token + [{:keys [token-type-props token shapes] :as _props}] + (ptk/reify ::on-toggle-token + ptk/WatchEvent + (watch [_ _ _] + (let [{:keys [attributes all-attributes on-update-shape]} token-type-props + unapply-tokens? (wtt/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}))))))) + +;; Shape Updates --------------------------------------------------------------- + +(defn update-shape-radius-all [value shape-ids] + (dch/update-shapes shape-ids + (fn [shape] + (when (ctsr/has-radius? shape) + (ctsr/set-radius-1 shape value))) + {:reg-objects? true + :attrs ctt/border-radius-keys})) + +(defn update-opacity [value shape-ids] + (dch/update-shapes shape-ids #(assoc % :opacity value))) + +(defn update-rotation [value shape-ids] + (ptk/reify ::update-shape-dimensions + ptk/WatchEvent + (watch [_ _ _] + (rx/of + (udw/trigger-bounding-box-cloaking shape-ids) + (udw/increase-rotation shape-ids value))))) + +(defn update-shape-radius-single-corner [value shape-ids attributes] + (dch/update-shapes shape-ids + (fn [shape] + (when (ctsr/has-radius? shape) + (cond-> shape + (:rx shape) (ctsr/switch-to-radius-4) + :always (ctsr/set-radius-4 (first attributes) value)))) + {:reg-objects? true + :attrs [:rx :ry :r1 :r2 :r3 :r4]})) + +(defn update-stroke-width + [value shape-ids] + (dch/update-shapes shape-ids (fn [shape] + (when (seq (:strokes shape)) + (assoc-in shape [:strokes 0 :stroke-width] value))))) + +(defn update-shape-dimensions [value shape-ids attributes] + (ptk/reify ::update-shape-dimensions + ptk/WatchEvent + (watch [_ _ _] + (rx/of + (when (:width attributes) (dwt/update-dimensions shape-ids :width value)) + (when (:height attributes) (dwt/update-dimensions shape-ids :height value)))))) + +(defn- attributes->layout-gap [attributes value] + (let [layout-gap (-> (set/intersection attributes #{:column-gap :row-gap}) + (zipmap (repeat value)))] + {:layout-gap layout-gap})) + +(defn update-layout-spacing [value shape-ids attributes] + (ptk/reify ::update-layout-spacing + ptk/WatchEvent + (watch [_ state _] + (let [layout-shape-ids (->> (wsh/lookup-shapes state shape-ids) + (eduction + (filter :layout) + (map :id))) + layout-attributes (attributes->layout-gap attributes value)] + (rx/of + (dwsl/update-layout layout-shape-ids layout-attributes)))))) + +(defn update-layout-spacing-column [value shape-ids] + (ptk/reify ::update-layout-spacing-column + ptk/WatchEvent + (watch [_ _ _] + (rx/concat + (for [shape-id shape-ids] + (let [layout-update {:layout-gap {:column-gap value :row-gap value}}] + (dwsl/update-layout [shape-id] layout-update))))))) + +(defn update-shape-position [value shape-ids attributes] + (ptk/reify ::update-shape-position + ptk/WatchEvent + (watch [_ _ _] + (rx/concat + (map #(dwt/update-position % (zipmap attributes (repeat value))) shape-ids))))) + +(defn update-layout-sizing-limits [value shape-ids attributes] + (ptk/reify ::update-layout-sizing-limits + ptk/WatchEvent + (watch [_ _ _] + (let [props (-> {:layout-item-min-w value + :layout-item-min-h value + :layout-item-max-w value + :layout-item-max-h value} + (select-keys attributes))] + (dwsl/update-layout-child shape-ids props))))) diff --git a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs index 803613ea9..615f758d4 100644 --- a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs @@ -8,27 +8,228 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.types.shape.radius :as ctsr] [app.main.data.modal :as modal] - [app.main.data.shortcuts :as scd] [app.main.data.tokens :as dt] - [app.main.data.workspace :as dw] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.shape-layout :as dwsl] - [app.main.data.workspace.transforms :as dwt] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.icons :as i] - [app.main.ui.workspace.tokens.core :as wtc] + [app.main.ui.workspace.tokens.changes :as wtch] [app.main.ui.workspace.tokens.token :as wtt] + [app.main.ui.workspace.tokens.token-types :as wtty] [app.util.dom :as dom] [app.util.timers :as timers] - [clojure.set :as set] [okulary.core :as l] [rumext.v2 :as mf])) +;; Actions --------------------------------------------------------------------- + +(defn attribute-actions [token selected-shapes attributes] + (let [ids-by-attributes (wtt/shapes-ids-by-applied-attributes token selected-shapes attributes) + shape-ids (into #{} (map :id selected-shapes))] + {:all-selected? (wtt/shapes-applied-all? ids-by-attributes shape-ids attributes) + :shape-ids shape-ids + :selected-pred #(seq (% ids-by-attributes))})) + +(defn generic-attribute-actions [attributes title {:keys [token selected-shapes on-update-shape]}] + (let [on-update-shape-fn (or on-update-shape + (-> (wtty/get-token-properties token) + (:on-update-shape))) + {:keys [selected-pred shape-ids]} (attribute-actions token selected-shapes attributes)] + (map (fn [attribute] + (let [selected? (selected-pred attribute) + props {:attributes #{attribute} + :token token + :shape-ids shape-ids}] + + {:title title + :selected? selected? + :action (fn [] + (if selected? + (st/emit! (wtch/unapply-token props)) + (st/emit! (wtch/apply-token (assoc props :on-update-shape on-update-shape-fn)))))})) + attributes))) + +(defn all-or-sepearate-actions [{:keys [attribute-labels on-update-shape-all on-update-shape]} + {: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 "All" + :selected? all-selected? + :action #(if all-selected? + (st/emit! (wtch/unapply-token props)) + (st/emit! (wtch/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) + (wtch/apply-token)) + selected? (wtch/unapply-token props) + :else (-> (assoc props :on-update-shape on-update-shape) + (wtch/apply-token)))] + (st/emit! event))})) + attribute-labels)] + (concat [all-action] single-actions))) + +(defn spacing-attribute-actions [{:keys [token selected-shapes] :as context-data}] + (let [on-update-shape (fn [resolved-value shape-ids attrs] + (dwsl/update-layout shape-ids {:layout-padding (zipmap attrs (repeat resolved-value))})) + padding-attrs {:p1 "Top" + :p2 "Right" + :p3 "Bottom" + :p4 "Left"} + all-padding-attrs (into #{} (keys padding-attrs)) + {:keys [all-selected? selected-pred shape-ids]} (attribute-actions token selected-shapes all-padding-attrs) + horizontal-attributes #{:p1 :p3} + horizontal-padding-selected? (and + (not all-selected?) + (every? selected-pred horizontal-attributes)) + vertical-attributes #{:p2 :p4} + vertical-padding-selected? (and + (not all-selected?) + (every? selected-pred vertical-attributes)) + padding-items [{:title "All" + :selected? all-selected? + :action (fn [] + (let [props {:attributes all-padding-attrs + :token token + :shape-ids shape-ids}] + (if all-selected? + (st/emit! (wtch/unapply-token props)) + (st/emit! (wtch/apply-token (assoc props :on-update-shape on-update-shape))))))} + {:title "Horizontal" + :selected? horizontal-padding-selected? + :action (fn [] + (let [props {:token token + :shape-ids shape-ids} + event (cond + all-selected? (wtch/apply-token (assoc props :attributes-to-remove vertical-attributes)) + horizontal-padding-selected? (wtch/apply-token (assoc props :attributes-to-remove horizontal-attributes)) + :else (wtch/apply-token (assoc props + :attributes horizontal-attributes + :on-update-shape on-update-shape)))] + (st/emit! event)))} + {:title "Vertical" + :selected? vertical-padding-selected? + :action (fn [] + (let [props {:token token + :shape-ids shape-ids} + event (cond + all-selected? (wtch/apply-token (assoc props :attributes-to-remove vertical-attributes)) + vertical-padding-selected? (wtch/apply-token (assoc props :attributes-to-remove vertical-attributes)) + :else (wtch/apply-token (assoc props + :attributes vertical-attributes + :on-update-shape on-update-shape)))] + (st/emit! event)))}] + single-padding-items (->> padding-attrs + (map (fn [[attr title]] + (let [same-axis-selected? (cond + (get horizontal-attributes attr) horizontal-padding-selected? + (get vertical-attributes attr) vertical-padding-selected? + :else true) + selected? (and + (not all-selected?) + (not same-axis-selected?) + (selected-pred attr))] + {:title title + :selected? selected? + :action #(let [props {:attributes #{attr} + :token token + :shape-ids shape-ids} + event (cond + all-selected? (-> (assoc props :attributes-to-remove all-padding-attrs) + (wtch/apply-token)) + selected? (wtch/unapply-token props) + :else (-> (assoc props :on-update-shape on-update-shape) + (wtch/apply-token)))] + (st/emit! event))})))) + gap-items (all-or-sepearate-actions {:attribute-labels {:column-gap "Column Gap" + :row-gap "Row Gap"} + :on-update-shape wtch/update-layout-spacing} + context-data)] + (concat padding-items + single-padding-items + [:separator] + gap-items))) + +(defn sizing-attribute-actions [context-data] + (concat + (all-or-sepearate-actions {:attribute-labels {:width "Width" + :height "Height"} + :on-update-shape wtch/update-shape-dimensions} + context-data) + [:separator] + (all-or-sepearate-actions {:attribute-labels {:layout-item-min-w "Min Width" + :layout-item-min-h "Min Height"} + :on-update-shape wtch/update-layout-sizing-limits} + context-data) + [:separator] + (all-or-sepearate-actions {:attribute-labels {:layout-item-max-w "Max Width" + :layout-item-max-h "Max Height"} + :on-update-shape wtch/update-layout-sizing-limits} + context-data))) + +(def shape-attribute-actions-map + (let [stroke-width (partial generic-attribute-actions #{:stroke-width} "Stroke Width")] + {:border-radius (partial all-or-sepearate-actions {:attribute-labels {:r1 "Top Left" + :r2 "Top Right" + :r4 "Bottom Left" + :r3 "Bottom Right"} + :on-update-shape-all wtch/update-shape-radius-all + :on-update-shape wtch/update-shape-radius-single-corner}) + :spacing spacing-attribute-actions + :sizing sizing-attribute-actions + :rotation (partial generic-attribute-actions #{:rotation} "Rotation") + :opacity (partial generic-attribute-actions #{:opacity} "Opacity") + :stroke-width stroke-width + :dimensions (fn [context-data] + (concat + [{:title "Spacing" :submenu :spacing} + {:title "Sizing" :submenu :sizing} + :separator + {:title "Border Radius" :submenu :border-radius}] + (stroke-width context-data) + [:separator] + (generic-attribute-actions #{:x} "X" (assoc context-data :on-update-shape wtch/update-shape-position)) + (generic-attribute-actions #{:y} "Y" (assoc context-data :on-update-shape wtch/update-shape-position))))})) + +(defn default-actions [{:keys [token]}] + (let [{:keys [modal]} (wtty/get-token-properties token)] + [{:title "Delete Token" + :action #(st/emit! (dt/delete-token (:id token)))} + {:title "Duplicate Token" + :action #(st/emit! (dt/duplicate-token (:id token)))} + {:title "Edit Token" + :action (fn [event] + (let [{:keys [key fields]} modal + token (dt/get-token-data-from-token-id (:id token))] + (st/emit! dt/hide-token-context-menu) + (dom/stop-propagation event) + (modal/show! key {:x (.-clientX ^js event) + :y (.-clientY ^js event) + :position :right + :fields fields + :token token})))}])) + +(defn selection-actions [{:keys [type token] :as context-data}] + (let [with-actions (get shape-attribute-actions-map (or type (:type token))) + attribute-actions (with-actions context-data)] + (concat + attribute-actions + [:separator] + (default-actions context-data)))) + +;; Components ------------------------------------------------------------------ + (def tokens-menu-ref (l/derived :token-context-menu refs/workspace-local)) @@ -39,312 +240,92 @@ (mf/defc menu-entry {::mf/props :obj} - [{:keys [title shortcut on-click on-pointer-enter on-pointer-leave - on-unmount children selected? icon disabled value]}] + [{:keys [title value on-click selected? children submenu-offset]}] (let [submenu-ref (mf/use-ref nil) hovering? (mf/use-ref false) on-pointer-enter (mf/use-callback (fn [] (mf/set-ref-val! hovering? true) - (let [submenu-node (mf/ref-val submenu-ref)] - (when (some? submenu-node) - (dom/set-css-property! submenu-node "display" "block"))) - (when on-pointer-enter (on-pointer-enter)))) - + (when-let [submenu-node (mf/ref-val submenu-ref)] + (dom/set-css-property! submenu-node "display" "block")))) on-pointer-leave (mf/use-callback (fn [] (mf/set-ref-val! hovering? false) - (let [submenu-node (mf/ref-val submenu-ref)] - (when (some? submenu-node) - (timers/schedule - 50 - #(when-not (mf/ref-val hovering?) - (dom/set-css-property! submenu-node "display" "none"))))) - (when on-pointer-leave (on-pointer-leave)))) - + (when-let [submenu-node (mf/ref-val submenu-ref)] + (timers/schedule 50 #(when-not (mf/ref-val hovering?) + (dom/set-css-property! submenu-node "display" "none")))))) set-dom-node (mf/use-callback (fn [dom] (let [submenu-node (mf/ref-val submenu-ref)] (when (and (some? dom) (some? submenu-node)) (dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))] + [:li + {:class (stl/css :context-menu-item) + :ref set-dom-node + :data-value value + :on-click on-click + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} + (when selected? + [:span {:class (stl/css :icon-wrapper)} + [:span {:class (stl/css :selected-icon)} i/tick]]) + [:span {:class (stl/css :title)} title] + (when children + [:* + [:span {:class (stl/css :submenu-icon)} i/arrow] + [:ul {:class (stl/css :token-context-submenu) + :ref submenu-ref + :style {:display "none" + :top 0 + :left (str submenu-offset "px")} + :on-context-menu prevent-default} + children]])])) - (mf/use-effect - (mf/deps on-unmount) - (constantly on-unmount)) - - (if icon - [:li {:class (stl/css :icon-menu-item) - :disabled disabled - :data-value value - :ref set-dom-node - :on-click on-click - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave} - [:span - {:class (stl/css :icon-wrapper)} - (if selected? [:span {:class (stl/css :selected-icon)} - i/tick] - [:span {:class (stl/css :selected-icon)}]) - [:span {:class (stl/css :shape-icon)} icon]] - [:span {:class (stl/css :title)} title]] - [:li {:class (stl/css :context-menu-item) - :disabled disabled - :ref set-dom-node - :data-value value - :on-click on-click - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave} - [:span {:class (stl/css :title)} title] - (when shortcut - [:span {:class (stl/css :shortcut)} - (for [[idx sc] (d/enumerate (scd/split-sc shortcut))] - [:span {:key (dm/str shortcut "-" idx) - :class (stl/css :shortcut-key)} sc])]) - - (when (> (count children) 1) - [:span {:class (stl/css :submenu-icon)} i/arrow]) - - (when (> (count children) 1) - [:ul {:class (stl/css :token-context-submenu) - :ref submenu-ref - :style {:display "none" :left 235} - :on-context-menu prevent-default} - children])]))) - -(mf/defc menu-separator - [] - [:li {:class (stl/css :separator)}]) - -(defn update-shape-radius-single-corner [value shape-ids attribute] - (st/emit! - (dch/update-shapes shape-ids - (fn [shape] - (when (ctsr/has-radius? shape) - (ctsr/set-radius-4 shape (first attribute) value))) - {:reg-objects? true - :attrs [:rx :ry :r1 :r2 :r3 :r4]}))) - -(defn apply-border-radius-token [{:keys [token-id token-type-props selected-shapes]} attributes] - (let [token (dt/get-token-data-from-token-id token-id) - updated-token-type-props (if (set/superset? #{:r1 :r2 :r3 :r4} attributes) - (assoc token-type-props - :on-update-shape update-shape-radius-single-corner - :attributes attributes) - token-type-props)] - (wtc/on-apply-token {:token token - :token-type-props updated-token-type-props - :selected-shapes selected-shapes}))) - -(defn update-layout-spacing [value selected-shapes attributes] - (doseq [shape selected-shapes] - (let [shape-id (:id shape)] - (if-let [layout-gap (cond - (:row-gap attributes) {:row-gap value} - (:column-gap attributes) {:column-gap value})] - (st/emit! (dwsl/update-layout [shape-id] {:layout-gap layout-gap})) - (when (:layout shape) - (st/emit! (dwsl/update-layout [shape-id] {:layout-padding (zipmap attributes (repeat value))}))))))) - - -(defn apply-spacing-token [{:keys [token-id token-type-props selected-shapes]} attributes] - (let [token (dt/get-token-data-from-token-id token-id) - attributes (set attributes) - updated-token-type-props (assoc token-type-props - :on-update-shape (fn [value shape-ids] - (update-layout-spacing value selected-shapes attributes)) - :attributes attributes)] - (wtc/on-apply-token {:token token - :token-type-props updated-token-type-props - :selected-shapes selected-shapes}))) - -(defn update-shape-position [value shape-ids attributes] - (doseq [shape-id shape-ids] - (st/emit! (dw/update-position shape-id {(first attributes) value})))) - -(defn apply-dimensions-token [{:keys [token-id token-type-props selected-shapes]} attributes] - (let [token (dt/get-token-data-from-token-id token-id) - attributes (set attributes) - updated-token-type-props (cond - (set/superset? #{:x :y} attributes) - (assoc token-type-props - :on-update-shape update-shape-position - :attributes attributes) - - (set/superset? #{:stroke-width} attributes) - (assoc token-type-props - :on-update-shape wtc/update-stroke-width - :attributes attributes) - - :else token-type-props)] - (wtc/on-apply-token {:token token - :token-type-props updated-token-type-props - :selected-shapes selected-shapes}))) - -(defn update-shape-dimensions [value shape-ids attributes] - (st/emit! (dwt/update-dimensions shape-ids (first attributes) value))) - -(defn update-layout-sizing-limits [value shape-ids attributes] - (st/emit! (dwsl/update-layout-child shape-ids {(first attributes) value}))) - -(defn apply-sizing-token [{:keys [token-id token-type-props selected-shapes]} attributes] - (let [token (dt/get-token-data-from-token-id token-id) - updated-token-type-props (cond - (set/superset? #{:width :height} attributes) - (assoc token-type-props - :on-update-shape update-shape-dimensions - :attributes attributes) - - (set/superset? #{:layout-item-min-w :layout-item-max-w - :layout-item-min-h :layout-item-max-h} attributes) - (assoc token-type-props - :on-update-shape update-layout-sizing-limits - :attributes attributes) - - :else token-type-props)] - (wtc/on-apply-token {:token token - :token-type-props updated-token-type-props - :selected-shapes selected-shapes}))) - -(defn apply-rotation-opacity-stroke-token [{:keys [token-id token-type-props selected-shapes]} attributes] - (let [token (dt/get-token-data-from-token-id token-id)] - (wtc/on-apply-token {:token token - :token-type-props token-type-props - :selected-shapes selected-shapes}))) - -(defn additional-actions [{:keys [token-id token-type selected-shapes] :as context-data}] - (let [attributes->actions (fn [update-fn coll] - (for [{:keys [attributes] :as item} coll] - (let [selected? (wtt/shapes-token-applied? {:id token-id} selected-shapes attributes)] - (assoc item - :action #(update-fn context-data attributes) - :selected? selected?))))] - (case token-type - :border-radius (attributes->actions - apply-border-radius-token - [{:title "All" :attributes #{:r1 :r2 :r3 :r4}} - {:title "Top Left" :attributes #{:r1}} - {:title "Top Right" :attributes #{:r2}} - {:title "Bottom Right" :attributes #{:r3}} - {:title "Bottom Left" :attributes #{:r4}}]) - :spacing (attributes->actions - apply-spacing-token - [{:title "All" :attributes #{:p1 :p2 :p3 :p4}} - {:title "Column Gap" :attributes #{:column-gap}} - {:title "Vertical padding" :attributes #{:p1 :p3}} - {:title "Horizontal padding" :attributes #{:p2 :p4}} - {:title "Row Gap" :attributes #{:row-gap}} - {:title "Top" :attributes #{:p1}} - {:title "Right" :attributes #{:p2}} - {:title "Bottom" :attributes #{:p3}} - {:title "Left" :attributes #{:p4}}]) - - :sizing (attributes->actions - apply-sizing-token - [{:title "All" :attributes #{:width :height :layout-item-min-w :layout-item-max-w :layout-item-min-h :layout-item-max-h}} - {:title "Width" :attributes #{:width}} - {:title "Height" :attributes #{:height}} - {:title "Min width" :attributes #{:layout-item-min-w}} - {:title "Max width" :attributes #{:layout-item-max-w}} - {:title "Min height" :attributes #{:layout-item-min-h}} - {:title "Max height" :attributes #{:layout-item-max-h}}]) - - :dimensions (attributes->actions - apply-dimensions-token - [{:title "Spacing" :submenu :spacing} - {:title "Sizing" :submenu :sizing} - {:title "Border Radius" :submenu :border-radius} - {:title "Border Width" :attributes #{:stroke-width}} - {:title "x" :attributes #{:x}} - {:title "y" :attributes #{:y}}]) - ;;TODO: Background blur {:title "Background blur" :attributes #{:width}}]) - - :opacity (attributes->actions - apply-rotation-opacity-stroke-token - [{:title "opacity" :attributes #{:opacity}}]) - - :rotation (attributes->actions - apply-rotation-opacity-stroke-token - [{:title "rotation" :attributes #{:rotation}}]) - - :stroke-width (attributes->actions - apply-rotation-opacity-stroke-token - [{:title "stroke width" :attributes #{:stroke-width}}]) - - []))) - -(defn generate-menu-entries [{:keys [token-id token-type-props token-type selected-shapes] :as context-data}] - (let [{:keys [modal]} token-type-props - default-actions [{:title "Delete Token" :action #(st/emit! (dt/delete-token token-id))} - {:title "Duplicate Token" :action #(st/emit! (dt/duplicate-token token-id))} - {:title "Edit Token" :action (fn [event] - (let [{:keys [key fields]} modal - token (dt/get-token-data-from-token-id token-id)] - (st/emit! dt/hide-token-context-menu) - (dom/stop-propagation event) - (modal/show! key {:x (.-clientX ^js event) - :y (.-clientY ^js event) - :position :right - :fields fields - :token token})))}] - specific-actions (additional-actions context-data) - all-actions (concat specific-actions default-actions)] - all-actions)) - -(mf/defc token-pill-context-menu - [context-data] - (let [menu-entries (generate-menu-entries context-data)] - (for [[index {:keys [title action selected? children submenu]}] (d/enumerate menu-entries)] - [:& menu-entry (cond-> {:key index - :title title} - (not submenu) (assoc :on-click action - ;; TODO: Allow selected items wihtout an icon for the context menu - :icon (mf/html [:div {:class (stl/css-case :empty-icon true - :hidden-icon (not selected?))}]) - :selected? selected?)) - (when submenu - (let [submenu-entries (additional-actions (assoc context-data :token-type submenu))] - (for [[index {:keys [title action selected?]}] (d/enumerate submenu-entries)] - [:& menu-entry {:key index - :title title - :on-click action - :icon (mf/html [:div {:class (stl/css-case :empty-icon true - :hidden-icon (not selected?))}]) - :selected? selected?}])))]))) +(mf/defc menu-tree + [{:keys [selected-shapes] :as context-data}] + (let [entries (if (seq selected-shapes) + (selection-actions context-data) + (default-actions context-data))] + (for [[index {:keys [title action selected? submenu] :as entry}] (d/enumerate entries)] + [:* {:key (str title " " index)} + (cond + (= :separator entry) [:li {:class (stl/css :separator)}] + submenu [:& menu-entry {:title title + :submenu-offset (:submenu-offset context-data)} + [:& menu-tree (assoc context-data :type submenu)]] + :else [:& menu-entry + {:title title + :on-click action + :selected? selected?}])]))) (mf/defc token-context-menu [] - (let [mdata (mf/deref tokens-menu-ref) - top (- (get-in mdata [:position :y]) 20) - left (get-in mdata [:position :x]) - dropdown-ref (mf/use-ref) + (let [mdata (mf/deref tokens-menu-ref) + top (+ (get-in mdata [:position :y]) 5) + left (+ (get-in mdata [:position :x]) 5) + width (mf/use-state 0) + dropdown-ref (mf/use-ref) objects (mf/deref refs/workspace-page-objects) selected (mf/deref refs/selected-shapes) - selected-shapes (into [] (keep (d/getf objects)) selected)] - + selected-shapes (into [] (keep (d/getf objects)) selected) + token-id (:token-id mdata) + token (get (mf/deref refs/workspace-tokens) token-id)] (mf/use-effect (mf/deps mdata) - #(let [dropdown (mf/ref-val dropdown-ref)] - (when dropdown - (let [bounding-rect (dom/get-bounding-rect dropdown) - window-size (dom/get-window-size) - delta-x (max (- (+ (:right bounding-rect) 250) (:width window-size)) 0) - delta-y (max (- (:bottom bounding-rect) (:height window-size)) 0) - new-style (str "top: " (- top delta-y) "px; " - "left: " (- left delta-x) "px;")] - (when (or (> delta-x 0) (> delta-y 0)) - (.setAttribute ^js dropdown "style" new-style)))))) + (fn [] + (when-let [node (mf/ref-val dropdown-ref)] + (reset! width (.-offsetWidth node))))) [:& dropdown {:show (boolean mdata) :on-close #(st/emit! dt/hide-token-context-menu)} [:div {:class (stl/css :token-context-menu) :ref dropdown-ref :style {:top top :left left} :on-context-menu prevent-default} - (when (= :token (:type mdata)) + (when token [:ul {:class (stl/css :context-list)} - [:& token-pill-context-menu {:token-id (:token-id mdata) - :token-type-props (:token-type-props mdata) - :token-type (:token-type mdata) - :selected-shapes selected-shapes}]])]])) + [:& menu-tree {:submenu-offset @width + :token token + :selected-shapes selected-shapes}]])]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/context_menu.scss index 3062483bc..c1d6cc573 100644 --- a/frontend/src/app/main/ui/workspace/tokens/context_menu.scss +++ b/frontend/src/app/main/ui/workspace/tokens/context_menu.scss @@ -8,8 +8,6 @@ .token-context-menu { position: absolute; - top: $s-40; - left: $s-736; z-index: $z-index-4; } @@ -38,13 +36,14 @@ } .separator { - height: $s-12; + @include bodySmallTypography; + margin: $s-6; + border-block-start: $s-1 solid var(--panel-border-color); } .context-menu-item { display: flex; align-items: center; - justify-content: space-between; height: $s-28; width: 100%; padding: $s-6; @@ -52,27 +51,34 @@ cursor: pointer; .title { + flex-grow: 1; @include bodySmallTypography; color: var(--menu-foreground-color); margin-left: calc(($s-32 + $s-28) / 2); } - .shortcut { - @include flexCenter; - gap: $s-2; - color: var(--menu-shortcut-foreground-color); - .shortcut-key { - @include bodySmallTypography; - @include flexCenter; - height: $s-20; - padding: $s-2 $s-6; - border-radius: $br-6; - background-color: var(--menu-shortcut-background-color); + + .icon-wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + } + + .icon-wrapper + .title { + margin-left: $s-6; + } + + .selected-icon { + svg { + @extend .button-icon-small; + stroke: var(--menu-foreground-color); } } - .submenu-icon svg { - @extend .button-icon-small; - stroke: var(--menu-foreground-color); + .submenu-icon { + margin-left: $s-2; + svg { + @extend .button-icon-small; + stroke: var(--menu-foreground-color); + } } &:hover { @@ -84,60 +90,14 @@ color: var(--menu-shortcut-foreground-color-hover); } } + &:focus { border: 1px solid var(--menu-border-color-focus); background-color: var(--menu-background-color-focus); } -} -.icon-menu-item { - display: flex; - justify-content: flex-start; - align-items: center; - height: $s-28; - padding: $s-6; - border-radius: $br-8; - &:hover { - background-color: var(--menu-background-color-hover); - } - - span.title { - margin-left: $s-6; - } - - .selected-icon { - svg { - @extend .button-icon-small; - stroke: var(--menu-foreground-color); - } - } - - .shape-icon { - margin-left: $s-2; - svg { - @extend .button-icon-small; - stroke: var(--menu-foreground-color); - } - } - - .icon-wrapper { - display: grid; - grid-template-columns: 1fr 1fr; - margin: 0; + &[disabled] { + pointer-events: none; + opacity: 0.6; } } - -.icon-menu-item[disabled], -.context-menu-item[disabled] { - pointer-events: none; - opacity: 0.6; -} - -// TODO: Allow selected items wihtout an icon for the context menu -.empty-icon { - width: 0; - height: 0; -} -.hidden-icon { - width: 11px; -} diff --git a/frontend/src/app/main/ui/workspace/tokens/core.cljs b/frontend/src/app/main/ui/workspace/tokens/core.cljs index 93e367a8a..a68d7e046 100644 --- a/frontend/src/app/main/ui/workspace/tokens/core.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/core.cljs @@ -6,29 +6,16 @@ (ns app.main.ui.workspace.tokens.core (:require - [app.common.data :as d :refer [ordered-map]] - [app.common.types.shape.radius :as ctsr] - [app.common.types.token :as ctt] - [app.main.data.tokens :as dt] - [app.main.data.workspace :as udw] - [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.shape-layout :as dwsl] - [app.main.data.workspace.transforms :as dwt] - [app.main.data.workspace.undo :as dwu] + [app.common.data :as d] [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.workspace.tokens.style-dictionary :as sd] [app.main.ui.workspace.tokens.token :as wtt] [app.util.dom :as dom] [app.util.webapi :as wapi] - [beicon.v2.core :as rx] - [cuerdas.core :as str] - [potok.v2.core :as ptk] - [promesa.core :as p])) + [cuerdas.core :as str])) ;; Helpers --------------------------------------------------------------------- -(defn resolve-token-value [{:keys [value resolved-value] :as token}] +(defn resolve-token-value [{:keys [value resolved-value] :as _token}] (or resolved-value (d/parse-double value))) @@ -48,128 +35,6 @@ (cond-> (assoc item :label name) (wtt/token-applied? item shape (or selected-attributes attributes)) (assoc :selected? true)))))) -;; Shape Update Functions ------------------------------------------------------ - -(defn update-shape-radius [value shape-ids] - (dch/update-shapes shape-ids - (fn [shape] - (when (ctsr/has-radius? shape) - (ctsr/set-radius-1 shape value))) - {:reg-objects? true - :attrs ctt/border-radius-keys})) - -(defn update-shape-dimensions [value shape-ids] - (ptk/reify ::update-shape-dimensions - ptk/WatchEvent - (watch [_ _ _] - (rx/of - (dwt/update-dimensions shape-ids :width value) - (dwt/update-dimensions shape-ids :height value))))) - -(defn update-opacity [value shape-ids] - (dch/update-shapes shape-ids #(assoc % :opacity value))) - -(defn update-stroke-width - [value shape-ids] - (dch/update-shapes shape-ids (fn [shape] - (if (seq (:strokes shape)) - (assoc-in shape [:strokes 0 :stroke-width] value) - shape)))) - -(defn update-rotation [value shape-ids] - (ptk/reify ::update-shape-dimensions - ptk/WatchEvent - (watch [_ _ _] - (rx/of - (udw/trigger-bounding-box-cloaking shape-ids) - (udw/increase-rotation shape-ids value))))) - -(defn update-layout-spacing-column [value shape-ids] - (ptk/reify ::update-layout-spacing-column - ptk/WatchEvent - (watch [_ state _] - (rx/concat - (for [shape-id shape-ids] - (let [shape (dt/get-shape-from-state shape-id state) - layout-direction (:layout-flex-dir shape) - layout-update (if (or (= layout-direction :row-reverse) (= layout-direction :row)) - {:layout-gap {:column-gap value}} - {:layout-gap {:row-gap value}})] - (dwsl/update-layout [shape-id] layout-update))))))) - -;; Events ---------------------------------------------------------------------- - -(defn apply-token - [{:keys [attributes shape-ids token on-update-shape] :as _props}] - (ptk/reify ::apply-token - ptk/WatchEvent - (watch [_ state _] - (->> (rx/from (sd/resolve-tokens+ (get-in state [:workspace-data :tokens]))) - (rx/mapcat - (fn [sd-tokens] - (let [undo-id (js/Symbol) - resolved-value (-> (get sd-tokens (:id token)) - (resolve-token-value)) - tokenized-attributes (wtt/attributes-map attributes (:id token))] - (rx/of - (dwu/start-undo-transaction undo-id) - (dch/update-shapes shape-ids (fn [shape] - (update shape :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 % (wtt/remove-attributes-for-token-id attributes (:id token) %))] - (dch/update-shapes - shape-ids - (fn [shape] - (update shape :applied-tokens remove-token)))))))) - -(defn toggle-token - [{:keys [token-type-props token shapes] :as _props}] - (ptk/reify ::on-toggle-token - ptk/WatchEvent - (watch [_ _ _] - (let [{:keys [attributes on-update-shape]} token-type-props - unapply-tokens? (wtt/shapes-token-applied? token shapes (:attributes token-type-props)) - shape-ids (map :id shapes)] - (if unapply-tokens? - (rx/of - (unapply-token {: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}))))))) - -(defn on-apply-token [{:keys [token token-type-props selected-shapes] :as _props}] - (let [{:keys [attributes on-apply on-update-shape] - :or {on-apply dt/update-token-from-attributes}} token-type-props - shape-ids (->> selected-shapes - (eduction - (remove #(wtt/shapes-token-applied? token % attributes)) - (map :id)))] - - (p/let [sd-tokens (sd/resolve-workspace-tokens+ {:debug? true})] - (let [resolved-token (get sd-tokens (:id token)) - resolved-token-value (resolve-token-value resolved-token)] - (doseq [shape selected-shapes] - (st/emit! (on-apply {:token-id (:id token) - :shape-id (:id shape) - :attributes attributes})) - (on-update-shape resolved-token-value shape-ids attributes)))))) - ;; JSON export functions ------------------------------------------------------- (defn encode-tokens @@ -196,104 +61,3 @@ (let [all-tokens (deref refs/workspace-tokens) transformed-tokens-json (transform-tokens-into-json-format all-tokens)] (export-tokens-file transformed-tokens-json))) - -;; Token types ----------------------------------------------------------------- - -(def token-types - (ordered-map - [:border-radius - {:title "Border Radius" - :attributes ctt/border-radius-keys - :on-update-shape update-shape-radius - :modal {:key :tokens/border-radius - :fields [{:label "Border Radius" - :key :border-radius}]}}] - [:stroke-width - {:title "Stroke Width" - :attributes ctt/stroke-width-keys - :on-update-shape update-stroke-width - :modal {:key :tokens/stroke-width - :fields [{:label "Stroke Width" - :key :stroke-width}]}}] - - [:sizing - {:title "Sizing" - :attributes #{:width :height} - :on-update-shape update-shape-dimensions - :modal {:key :tokens/sizing - :fields [{:label "Sizing" - :key :sizing}]}}] - [:dimensions - {:title "Dimensions" - :attributes #{:width :height} - :on-update-shape update-shape-dimensions - :modal {:key :tokens/dimensions - :fields [{:label "Dimensions" - :key :dimensions}]}}] - - [:opacity - {:title "Opacity" - :attributes ctt/opacity-keys - :on-update-shape update-opacity - :modal {:key :tokens/opacity - :fields [{:label "Opacity" - :key :opacity}]}}] - - [:rotation - {:title "Rotation" - :attributes ctt/rotation-keys - :on-update-shape update-rotation - :modal {:key :tokens/rotation - :fields [{:label "Rotation" - :key :rotation}]}}] - [:spacing - {:title "Spacing" - :attributes ctt/spacing-keys - :on-update-shape update-layout-spacing-column - :modal {:key :tokens/spacing - :fields [{:label "Spacing" - :key :spacing}]}}] - (comment - [:boolean - {:title "Boolean" - :modal {:key :tokens/boolean - :fields [{:label "Boolean"}]}}] - - [:box-shadow - {:title "Box Shadow" - :modal {:key :tokens/box-shadow - :fields [{:label "Box shadows" - :key :box-shadow - :type :box-shadow}]}}] - - [:numeric - {:title "Numeric" - :modal {:key :tokens/numeric - :fields [{:label "Numeric" - :key :numeric}]}}] - - [:other - {:title "Other" - :modal {:key :tokens/other - :fields [{:label "Other" - :key :other}]}}] - [:string - {:title "String" - :modal {:key :tokens/string - :fields [{:label "String" - :key :string}]}}] - [:typography - {:title "Typography" - :modal {:key :tokens/typography - :fields [{:label "Font" :key :font-family} - {:label "Weight" :key :weight} - {:label "Font Size" :key :font-size} - {:label "Line Height" :key :line-height} - {:label "Letter Spacing" :key :letter-spacing} - {:label "Paragraph Spacing" :key :paragraph-spacing} - {:label "Paragraph Indent" :key :paragraph-indent} - {:label "Text Decoration" :key :text-decoration} - {:label "Text Case" :key :text-case}]}}]))) - -(defn token-attributes [token-type] - (get-in token-types [token-type :attributes])) diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index 4edabdfcc..7cab3ea9a 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -14,9 +14,12 @@ [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.assets.common :as cmm] + [app.main.ui.workspace.tokens.changes :as wtch] + [app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]] [app.main.ui.workspace.tokens.core :as wtc] [app.main.ui.workspace.tokens.style-dictionary :as sd] [app.main.ui.workspace.tokens.token :as wtt] + [app.main.ui.workspace.tokens.token-types :as wtty] [app.util.dom :as dom] [cuerdas.core :as str] [okulary.core :as l] @@ -70,7 +73,7 @@ [{:keys [type tokens selected-shapes token-type-props]}] (let [open? (mf/deref (-> (l/key type) (l/derived lens:token-type-open-status))) - {:keys [modal attributes title]} token-type-props + {:keys [modal attributes all-attributes title]} token-type-props on-context-menu (mf/use-fn (fn [event token] @@ -78,8 +81,6 @@ (dom/stop-propagation event) (st/emit! (dt/show-token-context-menu {:type :token :position (dom/get-client-position event) - :token-type-props token-type-props - :token-type type :token-id (:id token)})))) on-toggle-open-click (mf/use-fn @@ -99,10 +100,11 @@ (mf/deps selected-shapes token-type-props) (fn [event token] (dom/stop-propagation event) - (st/emit! - (wtc/toggle-token {:token token - :shapes selected-shapes - :token-type-props token-type-props})))) + (when (seq selected-shapes) + (st/emit! + (wtch/toggle-token {:token token + :shapes selected-shapes + :token-type-props token-type-props}))))) tokens-count (count tokens)] [:div {:on-click on-toggle-open-click} [:& cmm/asset-section {:icon (mf/fnc icon-wrapper [_] @@ -123,7 +125,7 @@ [:& token-pill {:key (:id token) :token token - :highlighted? (wtt/shapes-token-applied? token selected-shapes attributes) + :highlighted? (wtt/shapes-token-applied? token selected-shapes (or all-attributes attributes)) :on-click #(on-token-pill-click % token) :on-context-menu #(on-context-menu % token)}])]])]])) @@ -132,7 +134,7 @@ Sort each group alphabetically (by their `:token-key`)." [tokens] (let [tokens-by-type (wtc/group-tokens-by-type tokens) - {:keys [empty filled]} (->> wtc/token-types + {:keys [empty filled]} (->> wtty/token-types (map (fn [[token-key token-type-props]] {:token-key token-key :token-type-props token-type-props @@ -154,6 +156,7 @@ token-groups (mf/with-memo [tokens] (sorted-token-groups tokens))] [:article + [:& token-context-menu] [:div.assets-bar (for [{:keys [token-key token-type-props tokens]} (concat (:filled token-groups) (:empty token-groups))] diff --git a/frontend/src/app/main/ui/workspace/tokens/token.cljs b/frontend/src/app/main/ui/workspace/tokens/token.cljs index 79fbb0e2d..b0d12d220 100644 --- a/frontend/src/app/main/ui/workspace/tokens/token.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/token.cljs @@ -1,7 +1,14 @@ (ns app.main.ui.workspace.tokens.token (:require + [app.common.data :as d] + [clojure.set :as set] [cuerdas.core :as str])) +(defn resolve-token-value [{:keys [value resolved-value] :as _token}] + (or + resolved-value + (d/parse-double value))) + (defn attributes-map "Creats an attributes map using collection of `attributes` for `id`." [attributes id] @@ -18,20 +25,64 @@ applied-tokens) (into {})))) +(defn token-attribute-applied? + "Test if `token` is applied to a `shape` on single `token-attribute`." + [token shape token-attribute] + (when-let [id (get-in shape [:applied-tokens token-attribute])] + (= (:id token) id))) + (defn token-applied? - "Test if `token` is applied to a `shape` with the given `token-attributes`." + "Test if `token` is applied to a `shape` with at least one of the one of the given `token-attributes`." [token shape token-attributes] - (let [{:keys [id]} token - applied-tokens (get shape :applied-tokens {})] - (some (fn [attr] - (= (get applied-tokens attr) id)) - token-attributes))) + (some #(token-attribute-applied? token shape %) token-attributes)) + +(defn token-applied-attributes + "Return a set of which `token-attributes` are applied with `token`." + [token shape token-attributes] + (-> (filter #(token-attribute-applied? token shape %) token-attributes) + (set))) (defn shapes-token-applied? - "Test if `token` is applied to to any of `shapes` with the given `token-attributes`." + "Test if `token` is applied to to any of `shapes` with at least one of the one of the given `token-attributes`." [token shapes token-attributes] (some #(token-applied? token % token-attributes) shapes)) +(defn shapes-token-applied-all? + "Test if `token` is applied to to any of `shapes` with at least one of the one of the given `token-attributes`." + [token shapes token-attributes] + (some #(token-applied? token % token-attributes) shapes)) + +(defn shapes-ids-by-applied-attributes [token shapes token-attributes] + (reduce (fn [acc shape] + (let [applied-ids-by-attribute (->> (map #(when (token-attribute-applied? token shape %) + [% #{(:id shape)}]) + token-attributes) + (filter some?) + (into {}))] + (merge-with into acc applied-ids-by-attribute))) + {} shapes)) + +(defn shapes-applied-all? [ids-by-attributes shape-ids attributes] + (every? #(set/superset? (get ids-by-attributes %) shape-ids) attributes)) + +(defn group-shapes-by-all-applied + [token shapes token-attributes] + (reduce + (fn [acc cur-shape] + (let [applied-attrs (token-applied-attributes token cur-shape token-attributes)] + (cond + (empty? applied-attrs) (update acc :none (fnil conj []) cur-shape) + (= applied-attrs token-attributes) (update acc :all (fnil conj []) cur-shape) + :else (reduce (fn [acc' cur'] + (update-in acc' [:some cur'] (fnil conj []) cur-shape)) + acc applied-attrs)))) + {} shapes)) + +(defn group-shapes-by-all-applied-all? [grouped-shapes] + (and (seq (:all grouped-shapes)) + (empty? (:other grouped-shapes)) + (empty? (:some grouped-shapes)))) + (defn token-name->path "Splits token-name into a path vector split by `.` characters. diff --git a/frontend/src/app/main/ui/workspace/tokens/token_types.cljs b/frontend/src/app/main/ui/workspace/tokens/token_types.cljs new file mode 100644 index 000000000..6b6c16916 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/token_types.cljs @@ -0,0 +1,120 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.token-types + (:require + [app.common.data :as d :refer [ordered-map]] + [app.common.types.token :as ctt] + [app.main.ui.workspace.tokens.changes :as wtch] + [clojure.set :as set])) + +(def token-types + (ordered-map + [:border-radius + {:title "Border Radius" + :attributes ctt/border-radius-keys + :on-update-shape wtch/update-shape-radius-all + :modal {:key :tokens/border-radius + :fields [{:label "Border Radius" + :key :border-radius}]}}] + [:stroke-width + {:title "Stroke Width" + :attributes ctt/stroke-width-keys + :on-update-shape wtch/update-stroke-width + :modal {:key :tokens/stroke-width + :fields [{:label "Stroke Width" + :key :stroke-width}]}}] + + [:sizing + {:title "Sizing" + :attributes #{:width :height} + :all-attributes ctt/sizing-keys + :on-update-shape wtch/update-shape-dimensions + :modal {:key :tokens/sizing + :fields [{:label "Sizing" + :key :sizing}]}}] + [:dimensions + {:title "Dimensions" + :attributes #{:width :height} + :all-attributes (set/union + ctt/spacing-keys + ctt/sizing-keys + ctt/border-radius-keys + ctt/stroke-width-keys) + :on-update-shape wtch/update-shape-dimensions + :modal {:key :tokens/dimensions + :fields [{:label "Dimensions" + :key :dimensions}]}}] + + [:opacity + {:title "Opacity" + :attributes ctt/opacity-keys + :on-update-shape wtch/update-opacity + :modal {:key :tokens/opacity + :fields [{:label "Opacity" + :key :opacity}]}}] + + [:rotation + {:title "Rotation" + :attributes ctt/rotation-keys + :on-update-shape wtch/update-rotation + :modal {:key :tokens/rotation + :fields [{:label "Rotation" + :key :rotation}]}}] + [:spacing + {:title "Spacing" + :attributes ctt/spacing-keys + :on-update-shape wtch/update-layout-spacing + :modal {:key :tokens/spacing + :fields [{:label "Spacing" + :key :spacing}]}}] + (comment + [:boolean + {:title "Boolean" + :modal {:key :tokens/boolean + :fields [{:label "Boolean"}]}}] + + [:box-shadow + {:title "Box Shadow" + :modal {:key :tokens/box-shadow + :fields [{:label "Box shadows" + :key :box-shadow + :type :box-shadow}]}}] + + [:numeric + {:title "Numeric" + :modal {:key :tokens/numeric + :fields [{:label "Numeric" + :key :numeric}]}}] + + [:other + {:title "Other" + :modal {:key :tokens/other + :fields [{:label "Other" + :key :other}]}}] + [:string + {:title "String" + :modal {:key :tokens/string + :fields [{:label "String" + :key :string}]}}] + [:typography + {:title "Typography" + :modal {:key :tokens/typography + :fields [{:label "Font" :key :font-family} + {:label "Weight" :key :weight} + {:label "Font Size" :key :font-size} + {:label "Line Height" :key :line-height} + {:label "Letter Spacing" :key :letter-spacing} + {:label "Paragraph Spacing" :key :paragraph-spacing} + {:label "Paragraph Indent" :key :paragraph-indent} + {:label "Text Decoration" :key :text-decoration} + {:label "Text Case" :key :text-case}]}}]))) + +(defn get-token-properties [token] + (get token-types (:type token))) + +(defn token-attributes [token-type] + (get-in token-types [token-type :attributes])) diff --git a/frontend/test/token_tests/logic/token_actions_test.cljs b/frontend/test/token_tests/logic/token_actions_test.cljs index 3fadf0cdd..1b6565d00 100644 --- a/frontend/test/token_tests/logic/token_actions_test.cljs +++ b/frontend/test/token_tests/logic/token_actions_test.cljs @@ -3,6 +3,7 @@ [app.common.test-helpers.compositions :as ctho] [app.common.test-helpers.files :as cthf] [app.common.test-helpers.shapes :as cths] + [app.main.ui.workspace.tokens.changes :as wtch] [app.main.ui.workspace.tokens.core :as wtc] [cljs.test :as t :include-macros true] [frontend-tests.helpers.pages :as thp] @@ -27,155 +28,189 @@ :type :border-radius}))) (t/deftest test-apply-token - (t/testing "applying a token twice with the same attributes will override") - (t/async - done - (let [file (setup-file) - store (ths/setup-store file) - rect-1 (cths/get-shape file :rect-1) - events [(wtc/apply-token {:shape-ids [(:id rect-1)] - :attributes #{:rx :ry} - :token (toht/get-token file :token-1) - :on-update-shape wtc/update-shape-radius}) - (wtc/apply-token {:shape-ids [(:id rect-1)] - :attributes #{:rx :ry} - :token (toht/get-token file :token-2) - :on-update-shape wtc/update-shape-radius})]] - (tohs/run-store-async - store done events - (fn [new-state] - (let [file' (ths/get-file-from-store new-state) - token-2' (toht/get-token file' :token-2) - rect-1' (cths/get-shape file' :rect-1)] - (t/is (some? (:applied-tokens rect-1'))) - (t/is (= (:rx (:applied-tokens rect-1')) (:id token-2'))) - (t/is (= (:ry (:applied-tokens rect-1')) (:id token-2'))) - (t/is (= (:rx rect-1') 24)) - (t/is (= (:ry rect-1') 24)))))))) + (t/testing "applying a token twice with the same attributes will override the previous applied token" + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [(wtch/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:rx :ry} + :token (toht/get-token file :token-1) + :on-update-shape wtch/update-shape-radius-all}) + (wtch/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:rx :ry} + :token (toht/get-token file :token-2) + :on-update-shape wtch/update-shape-radius-all})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-2' (toht/get-token file' :token-2) + rect-1' (cths/get-shape file' :rect-1)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:rx (:applied-tokens rect-1')) (:id token-2'))) + (t/is (= (:ry (:applied-tokens rect-1')) (:id token-2'))) + (t/is (= (:rx rect-1') 24)) + (t/is (= (:ry rect-1') 24))))))))) + +(t/deftest test-apply-token-overwrite + (t/testing "removes old token attributes and applies only single attribute" + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [;; Apply `:token-1` to all border radius attributes + (wtch/apply-token {:attributes #{:rx :ry :r1 :r2 :r3 :r4} + :token (toht/get-token file :token-1) + :shape-ids [(:id rect-1)] + :on-update-shape wtch/update-shape-radius-all}) + ;; Apply single `:r1` attribute to same shape + ;; while removing other attributes from the border-radius set + ;; but keep `:r4` for testing purposes + (wtch/apply-token {:attributes #{:r1} + :attributes-to-remove #{:rx :ry :r1 :r2 :r3} + :token (toht/get-token file :token-2) + :shape-ids [(:id rect-1)] + :on-update-shape wtch/update-shape-radius-all})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-1' (toht/get-token file' :token-1) + token-2' (toht/get-token file' :token-2) + rect-1' (cths/get-shape file' :rect-1)] + (t/testing "other border-radius attributes got removed" + (t/is (nil? (:rx (:applied-tokens rect-1'))))) + (t/testing "r1 got applied with :token-2" + (t/is (= (:r1 (:applied-tokens rect-1')) (:id token-2')))) + (t/testing "while :r4 was kept" + (t/is (= (:r4 (:applied-tokens rect-1')) (:id token-1')))))))))));))))))))))) (t/deftest test-apply-border-radius - (t/testing "applies radius token and updates the shapes radius") - (t/async - done - (let [file (setup-file) - store (ths/setup-store file) - rect-1 (cths/get-shape file :rect-1) - events [(wtc/apply-token {:shape-ids [(:id rect-1)] - :attributes #{:rx :ry} - :token (toht/get-token file :token-2) - :on-update-shape wtc/update-shape-radius})]] - (tohs/run-store-async - store done events - (fn [new-state] - (let [file' (ths/get-file-from-store new-state) - token-2' (toht/get-token file' :token-2) - rect-1' (cths/get-shape file' :rect-1)] - (t/is (some? (:applied-tokens rect-1'))) - (t/is (= (:rx (:applied-tokens rect-1')) (:id token-2'))) - (t/is (= (:ry (:applied-tokens rect-1')) (:id token-2'))) - (t/is (= (:rx rect-1') 24)) - (t/is (= (:ry rect-1') 24)))))))) + (t/testing "applies radius token and updates the shapes radius" + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [(wtch/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:rx :ry} + :token (toht/get-token file :token-2) + :on-update-shape wtch/update-shape-radius-all})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-2' (toht/get-token file' :token-2) + rect-1' (cths/get-shape file' :rect-1)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:rx (:applied-tokens rect-1')) (:id token-2'))) + (t/is (= (:ry (:applied-tokens rect-1')) (:id token-2'))) + (t/is (= (:rx rect-1') 24)) + (t/is (= (:ry rect-1') 24))))))))) (t/deftest test-apply-dimensions - (t/testing "applies dimensions token and updates the shapes width and height") - (t/async - done - (let [file (-> (setup-file) - (toht/add-token :token-target {:value "100" - :name "dimensions.sm" - :type :dimensions})) - store (ths/setup-store file) - rect-1 (cths/get-shape file :rect-1) - events [(wtc/apply-token {:shape-ids [(:id rect-1)] - :attributes #{:width :height} - :token (toht/get-token file :token-target) - :on-update-shape wtc/update-shape-dimensions})]] - (tohs/run-store-async - store done events - (fn [new-state] - (let [file' (ths/get-file-from-store new-state) - token-target' (toht/get-token file' :token-target) - rect-1' (cths/get-shape file' :rect-1)] - (t/is (some? (:applied-tokens rect-1'))) - (t/is (= (:width (:applied-tokens rect-1')) (:id token-target'))) - (t/is (= (:height (:applied-tokens rect-1')) (:id token-target'))) - (t/is (= (:width rect-1') 100)) - (t/is (= (:height rect-1') 100)))))))) + (t/testing "applies dimensions token and updates the shapes width and height" + (t/async + done + (let [file (-> (setup-file) + (toht/add-token :token-target {:value "100" + :name "dimensions.sm" + :type :dimensions})) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [(wtch/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:width :height} + :token (toht/get-token file :token-target) + :on-update-shape wtch/update-shape-dimensions})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-target' (toht/get-token file' :token-target) + rect-1' (cths/get-shape file' :rect-1)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:width (:applied-tokens rect-1')) (:id token-target'))) + (t/is (= (:height (:applied-tokens rect-1')) (:id token-target'))) + (t/is (= (:width rect-1') 100)) + (t/is (= (:height rect-1') 100))))))))) (t/deftest test-apply-sizing - (t/testing "applies sizing token and updates the shapes width and height") - (t/async - done - (let [file (-> (setup-file) - (toht/add-token :token-target {:value "100" - :name "sizing.sm" - :type :sizing})) - store (ths/setup-store file) - rect-1 (cths/get-shape file :rect-1) - events [(wtc/apply-token {:shape-ids [(:id rect-1)] - :attributes #{:width :height} - :token (toht/get-token file :token-target) - :on-update-shape wtc/update-shape-dimensions})]] - (tohs/run-store-async - store done events - (fn [new-state] - (let [file' (ths/get-file-from-store new-state) - token-target' (toht/get-token file' :token-target) - rect-1' (cths/get-shape file' :rect-1)] - (t/is (some? (:applied-tokens rect-1'))) - (t/is (= (:width (:applied-tokens rect-1')) (:id token-target'))) - (t/is (= (:height (:applied-tokens rect-1')) (:id token-target'))) - (t/is (= (:width rect-1') 100)) - (t/is (= (:height rect-1') 100)))))))) + (t/testing "applies sizing token and updates the shapes width and height" + (t/async + done + (let [file (-> (setup-file) + (toht/add-token :token-target {:value "100" + :name "sizing.sm" + :type :sizing})) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [(wtch/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:width :height} + :token (toht/get-token file :token-target) + :on-update-shape wtch/update-shape-dimensions})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-target' (toht/get-token file' :token-target) + rect-1' (cths/get-shape file' :rect-1)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:width (:applied-tokens rect-1')) (:id token-target'))) + (t/is (= (:height (:applied-tokens rect-1')) (:id token-target'))) + (t/is (= (:width rect-1') 100)) + (t/is (= (:height rect-1') 100))))))))) (t/deftest test-apply-opacity - (t/testing "applies opacity token and updates the shapes opacity") - (t/async - done - (let [file (-> (setup-file) - (toht/add-token :token-target {:value "0.5" - :name "opacity.medium" - :type :opacity})) - store (ths/setup-store file) - rect-1 (cths/get-shape file :rect-1) - events [(wtc/apply-token {:shape-ids [(:id rect-1)] - :attributes #{:opacity} - :token (toht/get-token file :token-target) - :on-update-shape wtc/update-opacity})]] - (tohs/run-store-async - store done events - (fn [new-state] - (let [file' (ths/get-file-from-store new-state) - token-target' (toht/get-token file' :token-target) - rect-1' (cths/get-shape file' :rect-1)] - (t/is (some? (:applied-tokens rect-1'))) - (t/is (= (:opacity (:applied-tokens rect-1')) (:id token-target'))) - ;; TODO Fix opacity shape update not working? - #_(t/is (= (:opacity rect-1') 0.5)))))))) + (t/testing "applies opacity token and updates the shapes opacity" + (t/async + done + (let [file (-> (setup-file) + (toht/add-token :token-target {:value "0.5" + :name "opacity.medium" + :type :opacity})) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [(wtch/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:opacity} + :token (toht/get-token file :token-target) + :on-update-shape wtch/update-opacity})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-target' (toht/get-token file' :token-target) + rect-1' (cths/get-shape file' :rect-1)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:opacity (:applied-tokens rect-1')) (:id token-target'))) + ;; TODO Fix opacity shape update not working? + #_(t/is (= (:opacity rect-1') 0.5))))))))) (t/deftest test-apply-rotation - (t/testing "applies rotation token and updates the shapes rotation") - (t/async - done - (let [file (-> (setup-file) - (toht/add-token :token-target {:value "120" - :name "rotation.medium" - :type :rotation})) - store (ths/setup-store file) - rect-1 (cths/get-shape file :rect-1) - events [(wtc/apply-token {:shape-ids [(:id rect-1)] - :attributes #{:rotation} - :token (toht/get-token file :token-target) - :on-update-shape wtc/update-rotation})]] - (tohs/run-store-async - store done events - (fn [new-state] - (let [file' (ths/get-file-from-store new-state) - token-target' (toht/get-token file' :token-target) - rect-1' (cths/get-shape file' :rect-1)] - (t/is (some? (:applied-tokens rect-1'))) - (t/is (= (:rotation (:applied-tokens rect-1')) (:id token-target'))) - (t/is (= (:rotation rect-1') 120)))))))) + (t/testing "applies rotation token and updates the shapes rotation" + (t/async + done + (let [file (-> (setup-file) + (toht/add-token :token-target {:value "120" + :name "rotation.medium" + :type :rotation})) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [(wtch/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:rotation} + :token (toht/get-token file :token-target) + :on-update-shape wtch/update-rotation})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-target' (toht/get-token file' :token-target) + rect-1' (cths/get-shape file' :rect-1)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:rotation (:applied-tokens rect-1')) (:id token-target'))) + (t/is (= (:rotation rect-1') 120))))))))) (t/deftest test-toggle-token-none (t/testing "should apply token to all selected items, where no item has the token applied" @@ -185,10 +220,10 @@ store (ths/setup-store file) rect-1 (cths/get-shape file :rect-1) rect-2 (cths/get-shape file :rect-2) - events [(wtc/toggle-token {:shapes [rect-1 rect-2] - :token-type-props {:attributes #{:rx :ry} - :on-update-shape wtc/update-shape-radius} - :token (toht/get-token file :token-2)})]] + events [(wtch/toggle-token {:shapes [rect-1 rect-2] + :token-type-props {:attributes #{:rx :ry} + :on-update-shape wtch/update-shape-radius-all} + :token (toht/get-token file :token-2)})]] (tohs/run-store-async store done events (fn [new-state] @@ -218,9 +253,9 @@ rect-without-token (cths/get-shape file :rect-2) rect-with-other-token (cths/get-shape file :rect-3) - events [(wtc/toggle-token {:shapes [rect-with-token rect-without-token rect-with-other-token] - :token (toht/get-token file :token-1) - :token-type-props {:attributes #{:rx :ry}}})]] + events [(wtch/toggle-token {:shapes [rect-with-token rect-without-token rect-with-other-token] + :token (toht/get-token file :token-1) + :token-type-props {:attributes #{:rx :ry}}})]] (tohs/run-store-async store done events (fn [new-state] @@ -252,9 +287,9 @@ rect-without-token (cths/get-shape file :rect-2) rect-with-other-token-2 (cths/get-shape file :rect-3) - events [(wtc/toggle-token {:shapes [rect-with-other-token-1 rect-without-token rect-with-other-token-2] - :token (toht/get-token file :token-1) - :token-type-props {:attributes #{:rx :ry}}})]] + events [(wtch/toggle-token {:shapes [rect-with-other-token-1 rect-without-token rect-with-other-token-2] + :token (toht/get-token file :token-1) + :token-type-props {:attributes #{:rx :ry}}})]] (tohs/run-store-async store done events (fn [new-state] diff --git a/frontend/test/token_tests/token_test.cljs b/frontend/test/token_tests/token_test.cljs index 255adf28e..bcf86fcc9 100644 --- a/frontend/test/token_tests/token_test.cljs +++ b/frontend/test/token_tests/token_test.cljs @@ -25,6 +25,40 @@ (t/testing "doesn't match passed `:token-attributes`" (t/is (nil? (wtt/token-applied? {:id :a} {:applied-tokens {:x :a}} #{:y}))))) +(t/deftest token-applied-attributes + (t/is (= #{:x} (wtt/token-applied-attributes {:id :a} + {:applied-tokens {:x :a :y :b}} + #{:x :missing})))) +(t/deftest shapes-ids-by-applied-attributes + (t/testing "Returns set of matched attributes that fit the applied token" + (let [attributes #{:x :y :z} + shape-applied-x {:id :shape-applied-x + :applied-tokens {:x 1}} + shape-applied-y {:id :shape-applied-y + :applied-tokens {:y 1}} + shape-applied-x-y {:id :shape-applied-x-y + :applied-tokens {:x 1 :y 1}} + shape-applied-none {:id :shape-applied-none + :applied-tokens {}} + shape-applied-all {:id :shape-applied-all + :applied-tokens {:x 1 :y 1 :z 1}} + ids-set (fn [& xs] (into #{} (map :id xs))) + shapes [shape-applied-x + shape-applied-y + shape-applied-x-y + shape-applied-all + shape-applied-none] + expected (wtt/shapes-ids-by-applied-attributes {:id 1} shapes attributes)] + (t/is (= (:x expected) (ids-set shape-applied-x + shape-applied-x-y + shape-applied-all))) + (t/is (= (:y expected) (ids-set shape-applied-y + shape-applied-x-y + shape-applied-all))) + (t/is (= (:z expected) (ids-set shape-applied-all))) + (t/is (true? (wtt/shapes-applied-all? expected (ids-set shape-applied-all) attributes))) + (t/is (false? (wtt/shapes-applied-all? expected (apply ids-set shapes) attributes)))))) + (t/deftest tokens-applied-test (t/testing "is true when single shape matches the token and attributes" (t/is (true? (wtt/shapes-token-applied? {:id :a} [{:applied-tokens {:x :a}}