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 b2f71ae23b..294185fe6f 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 @@ -29,6 +29,8 @@ [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 :as wtt] + [app.main.ui.workspace.tokens.changes :as wtch] [app.main.ui.workspace.tokens.token-types :as wtty] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -985,18 +987,25 @@ (mf/use-fn (mf/deps ids) (fn [type prop value] - (let [token-value (wtc/maybe-resolve-token-value value) - val (or token-value (mth/finite value 0))] + (let [token-identifier (wtt/token-identifier value) + val (or token-identifier (mth/finite value 0)) + on-update-shape wtch/update-layout-padding] (cond (and (= type :simple) (= prop :p1)) - (st/emit! (dwsl/update-layout ids {:layout-padding {:p1 val :p3 val} - :applied-tokens {:padding-p1 (if token-value (:id value) nil) - :padding-p3 (if token-value (:id value) nil)}})) + (if token-identifier + (st/emit! (wtch/apply-token {:shape-ids ids + :attributes #{:p1 :p3} + :token value + :on-update-shape on-update-shape})) + (st/emit! (on-update-shape value ids #{:p1 :p3}))) (and (= type :simple) (= prop :p2)) - (st/emit! (dwsl/update-layout ids {:layout-padding {:p2 val :p4 val} - :applied-tokens {:padding-p2 (if token-value (:id value) nil) - :padding-p4 (if token-value (:id value) nil)}})) + (if token-identifier + (st/emit! (wtch/apply-token {:shape-ids ids + :attributes #{:p2 :p4} + :token value + :on-update-shape on-update-shape})) + (st/emit! (on-update-shape value ids #{:p2 :p4}))) (some? prop) (st/emit! (dwsl/update-layout ids {:layout-padding {prop val}})))))) diff --git a/frontend/src/app/main/ui/workspace/tokens/changes.cljs b/frontend/src/app/main/ui/workspace/tokens/changes.cljs index 45ff044df4..95243ee513 100644 --- a/frontend/src/app/main/ui/workspace/tokens/changes.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/changes.cljs @@ -136,6 +136,9 @@ (zipmap (repeat value)))] {:layout-gap layout-gap})) +(defn update-layout-padding [value shape-ids attrs] + (dwsl/update-layout shape-ids {:layout-padding (zipmap attrs (repeat value))})) + (defn update-layout-spacing [value shape-ids attributes] (ptk/reify ::update-layout-spacing ptk/WatchEvent @@ -148,15 +151,6 @@ (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 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 615f758d41..57e6311d10 100644 --- a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs @@ -81,8 +81,7 @@ (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))})) + (let [on-update-shape-padding wtch/update-layout-padding padding-attrs {:p1 "Top" :p2 "Right" :p3 "Bottom" @@ -105,7 +104,7 @@ :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))))))} + (st/emit! (wtch/apply-token (assoc props :on-update-shape on-update-shape-padding))))))} {:title "Horizontal" :selected? horizontal-padding-selected? :action (fn [] @@ -116,7 +115,7 @@ 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)))] + :on-update-shape on-update-shape-padding)))] (st/emit! event)))} {:title "Vertical" :selected? vertical-padding-selected? @@ -128,7 +127,7 @@ 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)))] + :on-update-shape on-update-shape-padding)))] (st/emit! event)))}] single-padding-items (->> padding-attrs (map (fn [[attr title]] @@ -149,7 +148,7 @@ 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) + :else (-> (assoc props :on-update-shape on-update-shape-padding) (wtch/apply-token)))] (st/emit! event))})))) gap-items (all-or-sepearate-actions {:attribute-labels {:column-gap "Column Gap" diff --git a/frontend/src/app/main/ui/workspace/tokens/editable_select.scss b/frontend/src/app/main/ui/workspace/tokens/editable_select.scss index c0905cb8db..c404919ec8 100644 --- a/frontend/src/app/main/ui/workspace/tokens/editable_select.scss +++ b/frontend/src/app/main/ui/workspace/tokens/editable_select.scss @@ -14,6 +14,7 @@ width: 100%; padding: $s-8; border-radius: $br-8; + position: relative; cursor: pointer; background: transparent; diff --git a/frontend/src/app/main/ui/workspace/tokens/form.cljs b/frontend/src/app/main/ui/workspace/tokens/form.cljs index 4521906289..5e3351940f 100644 --- a/frontend/src/app/main/ui/workspace/tokens/form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/form.cljs @@ -8,14 +8,15 @@ (:require-macros [app.main.style :as stl]) (:require ["lodash.debounce" :as debounce] - [app.main.ui.workspace.tokens.update :as wtu] [app.common.data :as d] [app.main.data.modal :as modal] [app.main.data.tokens :as dt] + [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.workspace.tokens.common :as tokens.common] [app.main.ui.workspace.tokens.style-dictionary :as sd] [app.main.ui.workspace.tokens.token :as wtt] + [app.main.ui.workspace.tokens.update :as wtu] [app.util.dom :as dom] [cuerdas.core :as str] [malli.core :as m] @@ -141,14 +142,15 @@ Token names should only contain letters and digits separated by . characters.")} (mf/defc form {::mf/wrap-props false} [{:keys [token token-type] :as _args}] - (let [tokens (sd/use-resolved-workspace-tokens) + (let [tokens (mf/deref refs/workspace-tokens) + resolved-tokens (sd/use-resolved-tokens tokens) token-path (mf/use-memo (mf/deps (:name token)) #(wtt/token-name->path (:name token))) tokens-tree (mf/use-memo - (mf/deps token-path tokens) + (mf/deps token-path resolved-tokens) (fn [] - (-> (wtt/token-names-tree tokens) + (-> (wtt/token-names-tree resolved-tokens) ;; Allow setting editing token to it's own path (d/dissoc-in token-path)))) @@ -177,7 +179,7 @@ Token names should only contain letters and digits separated by . characters.")} ;; Value value-ref (mf/use-var (:value token)) - token-resolve-result (mf/use-state (get-in tokens [(:id token) :resolved-value])) + token-resolve-result (mf/use-state (get-in resolved-tokens [(wtt/token-identifier token) :resolved-value])) set-resolve-value (mf/use-callback (fn [token-or-err] (let [v (cond @@ -219,7 +221,7 @@ Token names should only contain letters and digits separated by . characters.")} (not valid-description-field?)) on-submit (mf/use-callback - (mf/deps validate-name validate-descripion token tokens) + (mf/deps validate-name validate-descripion token resolved-tokens) (fn [e] (dom/prevent-default e) ;; We have to re-validate the current form values before submitting @@ -236,7 +238,7 @@ Token names should only contain letters and digits separated by . characters.")} (validate-token-value+ {:input final-value :name-value final-name :token token - :tokens tokens})]) + :tokens resolved-tokens})]) (p/finally (fn [result err] ;; The result should be a vector of all resolved validations ;; We do not handle the error case as it will be handled by the components validations diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs new file mode 100644 index 0000000000..01a2c212e5 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -0,0 +1,101 @@ +;; 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.sets + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data.macros :as dm] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [okulary.core :as l] + [potok.v2.core :as ptk] + [rumext.v2 :as mf])) + +(def active-sets #{#uuid "2858b330-828e-4131-86ed-e4d1c0f4b3e3" + #uuid "d608877b-842a-473b-83ca-b5f8305caf83"}) + +(def sets-root-order [#uuid "2858b330-828e-4131-86ed-e4d1c0f4b3e3" + #uuid "9c5108aa-bdb4-409c-a3c8-c3dfce2f8bf8" + #uuid "0381446e-1f1d-423f-912c-ab577d61b79b"]) + +(def sets {#uuid "9c5108aa-bdb4-409c-a3c8-c3dfce2f8bf8" {:type :group + :name "Group A" + :children [#uuid "d1754e56-3510-493f-8287-5ef3417d4141" + #uuid "d608877b-842a-473b-83ca-b5f8305caf83"]} + #uuid "d608877b-842a-473b-83ca-b5f8305caf83" {:type :set + :name "Set A / 1"} + #uuid "d1754e56-3510-493f-8287-5ef3417d4141" {:type :group + :name "Group A / B" + :children [#uuid "f608877b-842a-473b-83ca-b5f8305caf83" + #uuid "7cc05389-9391-426e-bc0e-ba5cb8f425eb"]} + #uuid "f608877b-842a-473b-83ca-b5f8305caf83" {:type :set + :name "Set A / B / 1"} + #uuid "7cc05389-9391-426e-bc0e-ba5cb8f425eb" {:type :set + :name "Set A / B / 2"} + #uuid "2858b330-828e-4131-86ed-e4d1c0f4b3e3" {:type :set + :name "Set Root 1"} + #uuid "0381446e-1f1d-423f-912c-ab577d61b79b" {:type :set + :name "Set Root 2"}}) + +(def ^:private chevron-icon + (i/icon-xref :arrow (stl/css :chevron-icon))) + +(defn set-selected-set + [set-id] + (dm/assert! (uuid? set-id)) + (ptk/reify ::set-selected-set + ptk/UpdateEvent + (update [_ state] + (assoc state :selected-set-id set-id)))) + +(mf/defc sets-tree + [{:keys [selected-set-id set-id]}] + (let [set (get sets set-id)] + (when set + (let [{:keys [type name children]} set + visible? (mf/use-state (contains? active-sets set-id)) + collapsed? (mf/use-state false) + icon (if (= type :set) i/document i/group) + selected? (mf/use-state (= set-id selected-set-id)) + + on-click + (mf/use-fn + (mf/deps type set-id) + (fn [event] + (dom/stop-propagation event) + (st/emit! (set-selected-set set-id))))] + [:div {:class (stl/css :set-item-container) + :on-click on-click} + [:div {:class (stl/css-case :set-item-group (= type :group) + :set-item-set (= type :set) + :selected-set (and (= type :set) @selected?))} + (when (= type :group) + [:span {:class (stl/css-case + :collapsabled-icon true + :collapsed @collapsed?) + :on-click #(when (= type :group) (swap! collapsed? not))} + chevron-icon]) + [:span {:class (stl/css :icon)} icon] + [:div {:class (stl/css :set-name)} name] + (when (= type :set) + [:span {:class (stl/css :action-btn) + :on-click #(swap! visible? not)} + (if @visible? + i/shown + i/hide)])] + (when (and children (not @collapsed?)) + [:div {:class (stl/css :set-children)} + (for [child-id children] + [:& sets-tree {:key child-id :set-id child-id :selected-set-id selected-set-id}])])])))) + +(mf/defc sets-list + [{:keys [selected-set-id]}] + [:ul {:class (stl/css :sets-list)} + (for [set-id sets-root-order] + [:& sets-tree {:key set-id + :set-id set-id + :selected-set-id selected-set-id}])]) diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.scss b/frontend/src/app/main/ui/workspace/tokens/sets.scss new file mode 100644 index 0000000000..5c32bf199b --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/sets.scss @@ -0,0 +1,101 @@ +// 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 + +@import "refactor/common-refactor.scss"; + +.sets-list { + width: 100%; + margin-bottom: $s-12; + overflow-y: auto; +} + +.set-item-container { + width: 100%; + cursor: pointer; + color: var(--layer-row-foreground-color); + padding-left: $s-20; +} + +.set-item-set, +.set-item-group { + @include bodySmallTypography; + display: flex; + align-items: center; + min-height: $s-32; + width: 100%; + cursor: pointer; + color: var(--layer-row-foreground-color); + .set-name { + @include textEllipsis; + flex-grow: 1; + padding-left: $s-2; + } + + .icon { + display: flex; + align-items: center; + width: $s-20; + height: $s-20; + padding-right: $s-4; + + svg { + height: $s-20; + width: $s-20; + color: white; + fill: none; + stroke: var(--icon-foreground); + } + } +} + +.set-item-set { + &:hover { + background-color: var(--layer-row-background-color-hover); + color: var(--layer-row-foreground-color-hover); + box-shadow: -100px 0 0 0 var(--layer-row-background-color-hover); + } +} + +.selected-set { + background-color: var(--layer-row-background-color-selected); + color: var(--layer-row-foreground-color-selected); + box-shadow: -100px 0 0 0 var(--layer-row-background-color-selected); +} + +.action-btn { + @extend .button-tertiary; + height: $s-28; + width: $s-28; + svg { + @extend .button-icon; + width: 12px; + height: 12px; + } +} + +.collapsabled-icon { + @include buttonStyle; + @include flexCenter; + height: $s-24; + border-radius: $br-8; + + --chevron-icon-rotation: 90deg; + + &.collapsed { + --chevron-icon-rotation: 0deg; + } + + &:hover { + --chevron-icon-color: var(--title-foreground-color-hover); + } +} + +.chevron-icon { + @extend .button-icon-small; + margin-right: $s-6; + transform: rotate(var(--chevron-icon-rotation)); + stroke: var(--icon-foreground); +} diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index 7e3069493b..d8dcfc4076 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -13,12 +13,14 @@ [app.main.data.tokens :as wdt] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.title-bar :refer [title-bar]] [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.common :refer [labeled-input]] [app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]] [app.main.ui.workspace.tokens.core :as wtc] + [app.main.ui.workspace.tokens.sets :refer [sets-list]] [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] @@ -34,6 +36,9 @@ (def ^:private download-icon (i/icon-xref :download (stl/css :download-icon))) +(def selected-set-id + (l/derived :selected-set-id st/state)) + (mf/defc token-pill {::mf/wrap-props false} [{:keys [on-click token highlighted? on-context-menu]}] @@ -260,13 +265,35 @@ :tokens tokens :token-type-props token-type-props}])]])) +(mf/defc sets-sidebar + [] + (let [selected-set-id (mf/deref selected-set-id) + open? (mf/use-state true)] + [:div {:class (stl/css :sets-sidebar)} + [:div {:class (stl/css :sidebar-header)} + [:& title-bar {:collapsable true + :collapsed (not @open?) + :all-clickable true + :title "SETS" + :on-collapsed #(swap! open? not)}] + [:button {:class (stl/css :add-set) + :on-click #(println "Add Set")} + i/add]] + (when @open? + [:& sets-list {:selected-set-id selected-set-id}])])) + (mf/defc tokens-sidebar-tab {::mf/wrap [mf/memo] ::mf/wrap-props false} [_props] - [:div {:class (stl/css :sidebar-tab-wrapper)} - [:& tokens-explorer] - [:button {:class (stl/css :download-json-button) - :on-click wtc/download-tokens-as-json} - download-icon - "Export JSON"]]) + (let [show-sets-section? false] ;; temporarily added this variable to see/hide the sets section till we have it working end to end + [:div {:class (stl/css :sidebar-tab-wrapper)} + (when show-sets-section? + [:div {:class (stl/css :sets-section-wrapper)} + [:& sets-sidebar]]) + [:div {:class (stl/css :tokens-section-wrapper)} + [:& tokens-explorer]] + [:button {:class (stl/css :download-json-button) + :on-click wtc/download-tokens-as-json} + download-icon + "Export JSON"]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss index 67ff95ee03..e90b12d3cf 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss @@ -5,11 +5,57 @@ // Copyright (c) KALEIDOS INC @import "refactor/common-refactor.scss"; - @import "./common.scss"; .sidebar-tab-wrapper { - padding: $s-12; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.sets-section-wrapper { + display: flex; + flex-direction: column; + margin-bottom: $s-8; +} + +.sets-sidebar { + position: relative; +} + +.sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-left: $s-8; + padding-top: $s-12; + color: var(--layer-row-foreground-color); +} + +.add-set { + @extend .button-tertiary; + height: $s-32; + width: $s-28; + padding: 0; + margin-right: $s-12; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + transform: rotate(90deg); + } +} + +.tokens-section-wrapper { + flex: 1; + padding-top: $s-12; + padding-left: $s-12; + overflow-y: auto; +} + +// TODO Remove once sets are available to public +.sets-section-wrapper + .tokens-section-wrapper { + padding-top: 0; } .token-pills-wrapper { diff --git a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs index bed8546951..d309645a7a 100644 --- a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs @@ -10,9 +10,8 @@ [rumext.v2 :as mf])) (def StyleDictionary - "The global StyleDictionary instance used as an external library for now, - as the package would need webpack to be bundled, - because shadow-cljs doesn't support some of the more modern bundler features." + "Initiates the global StyleDictionary instance with transforms + from tokens-studio used to parse and resolved token values." (do (sd-transforms/registerTransforms sd) (.registerFormat sd #js {:name "custom/json" diff --git a/frontend/test/token_tests/style_dictionary_test.cljs b/frontend/test/token_tests/style_dictionary_test.cljs new file mode 100644 index 0000000000..cc96abe1b8 --- /dev/null +++ b/frontend/test/token_tests/style_dictionary_test.cljs @@ -0,0 +1,37 @@ +(ns token-tests.style-dictionary-test + (:require + [app.main.ui.workspace.tokens.style-dictionary :as sd] + [cljs.test :as t :include-macros true] + [promesa.core :as p])) + +(def border-radius-token + {:id #uuid "8c868278-7c8d-431b-bbc9-7d8f15c8edb9" + :value "12px" + :name "borderRadius.sm" + :type :border-radius}) + +(def reference-border-radius-token + {:id #uuid "b9448d78-fd5b-4e3d-aa32-445904063f5b" + :value "{borderRadius.sm} * 2" + :name "borderRadius.md-with-dashes" + :type :border-radius}) + +(def tokens {(:id border-radius-token) border-radius-token + (:id reference-border-radius-token) reference-border-radius-token}) + +(t/deftest resolve-tokens-test + (t/async + done + (t/testing "resolves tokens using style-dictionary" + (-> (sd/resolve-tokens+ tokens) + (p/finally (fn [resolved-tokens] + (let [expected-tokens {"borderRadius.sm" + (assoc border-radius-token + :resolved-value 12 + :resolved-unit "px") + "borderRadius.md-with-dashes" + (assoc reference-border-radius-token + :resolved-value 24 + :resolved-unit "px")}] + (t/is (= expected-tokens resolved-tokens)) + (done))))))))