diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 2e8a4e2cc..104948105 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -47,10 +47,12 @@ :string :typography}) +(def token-name-ref :string) + (sm/register! ::token [:map {:title "Token"} [:id ::sm/uuid] - [:name :string] + [:name token-name-ref] [:type [::sm/one-of token-types]] [:value :any] [:description {:optional true} :string] @@ -58,48 +60,48 @@ (sm/register! ::border-radius [:map - [:rx {:optional true} ::sm/uuid] - [:ry {:optional true} ::sm/uuid] - [:r1 {:optional true} ::sm/uuid] - [:r2 {:optional true} ::sm/uuid] - [:r3 {:optional true} ::sm/uuid] - [:r4 {:optional true} ::sm/uuid]]) + [:rx {:optional true} token-name-ref] + [:ry {:optional true} token-name-ref] + [:r1 {:optional true} token-name-ref] + [:r2 {:optional true} token-name-ref] + [:r3 {:optional true} token-name-ref] + [:r4 {:optional true} token-name-ref]]) (def border-radius-keys (schema-keys ::border-radius)) (sm/register! ::stroke-width [:map - [:stroke-width {:optional true} ::sm/uuid]]) + [:stroke-width {:optional true} token-name-ref]]) (def stroke-width-keys (schema-keys ::stroke-width)) (sm/register! ::sizing [:map - [:width {:optional true} ::sm/uuid] - [:height {:optional true} ::sm/uuid] - [:layout-item-min-w {:optional true} ::sm/uuid] - [:layout-item-max-w {:optional true} ::sm/uuid] - [:layout-item-min-h {:optional true} ::sm/uuid] - [:layout-item-max-h {:optional true} ::sm/uuid]]) + [:width {:optional true} token-name-ref] + [:height {:optional true} token-name-ref] + [:layout-item-min-w {:optional true} token-name-ref] + [:layout-item-max-w {:optional true} token-name-ref] + [:layout-item-min-h {:optional true} token-name-ref] + [:layout-item-max-h {:optional true} token-name-ref]]) (def sizing-keys (schema-keys ::sizing)) (sm/register! ::opacity [:map - [:opacity {:optional true} ::sm/uuid]]) + [:opacity {:optional true} token-name-ref]]) (def opacity-keys (schema-keys ::opacity)) (sm/register! ::spacing [:map - [:row-gap {:optional true} ::sm/uuid] - [:column-gap {:optional true} ::sm/uuid] - [:p1 {:optional true} ::sm/uuid] - [:p2 {:optional true} ::sm/uuid] - [:p3 {:optional true} ::sm/uuid] - [:p4 {:optional true} ::sm/uuid] - [:x {:optional true} ::sm/uuid] - [:y {:optional true} ::sm/uuid]]) + [:row-gap {:optional true} token-name-ref] + [:column-gap {:optional true} token-name-ref] + [:p1 {:optional true} token-name-ref] + [:p2 {:optional true} token-name-ref] + [:p3 {:optional true} token-name-ref] + [:p4 {:optional true} token-name-ref] + [:x {:optional true} token-name-ref] + [:y {:optional true} token-name-ref]]) (def spacing-keys (schema-keys ::spacing)) @@ -113,7 +115,7 @@ (sm/register! ::rotation [:map - [:rotation {:optional true} ::sm/uuid]]) + [:rotation {:optional true} token-name-ref]]) (def rotation-keys (schema-keys ::rotation)) diff --git a/frontend/src/app/main/data/tokens.cljs b/frontend/src/app/main/data/tokens.cljs index 84c83ef0a..a27c7f30c 100644 --- a/frontend/src/app/main/data/tokens.cljs +++ b/frontend/src/app/main/data/tokens.cljs @@ -16,6 +16,7 @@ [app.main.data.workspace.shapes :as dwsh] [app.main.refs :as refs] [app.main.ui.workspace.tokens.common :refer [workspace-shapes]] + [app.main.ui.workspace.tokens.token :as wtt] [beicon.v2.core :as rx] [clojure.data :as data] [cuerdas.core :as str] @@ -55,22 +56,22 @@ (first))] shape)) -(defn token-from-attributes [token-id attributes] - (->> (map (fn [attr] [attr token-id]) attributes) +(defn token-from-attributes [token attributes] + (->> (map (fn [attr] [attr (wtt/token-identifier token)]) attributes) (into {}))) (defn unapply-token-id [shape attributes] (update shape :applied-tokens d/without-keys attributes)) -(defn apply-token-id-to-attributes [{:keys [shape token-id attributes]}] - (let [token (token-from-attributes token-id attributes)] +(defn apply-token-to-attributes [{:keys [shape token attributes]}] + (let [token (token-from-attributes token attributes)] (toggle-or-apply-token shape token))) (defn apply-token-to-shape [{:keys [shape token attributes] :as _props}] - (let [applied-tokens (apply-token-id-to-attributes {:shape shape - :token-id (:id token) - :attributes attributes})] + (let [applied-tokens (apply-token-to-attributes {:shape shape + :token token + :attributes attributes})] (update shape :applied-tokens #(merge % applied-tokens)))) (defn maybe-apply-token-to-shape @@ -80,17 +81,6 @@ (apply-token-to-shape props) shape)) -(defn update-token-from-attributes - [{:keys [token-id shape-id attributes]}] - (ptk/reify ::update-token-from-attributes - ptk/WatchEvent - (watch [_ state _] - (let [shape (get-shape-from-state shape-id state) - applied-tokens (apply-token-id-to-attributes {:shape shape - :token-id token-id - :attributes attributes})] - (rx/of (update-shape shape-id {:applied-tokens applied-tokens})))))) - (defn get-token-data-from-token-id [id] (let [workspace-data (deref refs/workspace-data)] 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 b2f71ae23..294185fe6 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/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index 05efc28a4..e786b1141 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 @@ -300,7 +300,7 @@ (update-fn shape) shape)) {:reg-objects? true - :attrs [:rx :ry :r1 :r2 :r3 :r4]}))) + :attrs [:rx :ry :r1 :r2 :r3 :r4 :applied-tokens]}))) on-switch-to-radius-1 (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/tokens/changes.cljs b/frontend/src/app/main/ui/workspace/tokens/changes.cljs index af53ee46c..95243ee51 100644 --- a/frontend/src/app/main/ui/workspace/tokens/changes.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/changes.cljs @@ -34,11 +34,10 @@ (watch [_ state _] (->> (rx/from (sd/resolve-tokens+ (get-in state [:workspace-data :tokens]))) (rx/mapcat - (fn [sd-tokens] + (fn [resolved-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))] + resolved-value (get-in resolved-tokens [(wtt/token-identifier token) :resolved-value]) + tokenized-attributes (wtt/attributes-map attributes token)] (rx/of (dwu/start-undo-transaction undo-id) (dwsh/update-shapes shape-ids (fn [shape] @@ -58,7 +57,7 @@ ptk/WatchEvent (watch [_ _ _] (rx/of - (let [remove-token #(when % (wtt/remove-attributes-for-token-id attributes (:id token) %))] + (let [remove-token #(when % (wtt/remove-attributes-for-token attributes token %))] (dwsh/update-shapes shape-ids (fn [shape] @@ -137,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 @@ -149,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 615f758d4..57e6311d1 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 c0905cb8d..c404919ec 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 5094f29ef..5e3351940 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] @@ -92,7 +93,7 @@ Token names should only contain letters and digits separated by . characters.")} ;; When creating a new token we dont have a token name yet, ;; so we use a temporary token name that hopefully doesn't clash with any of the users token names. token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value) - token-references (sd/find-token-references input) + token-references (wtt/find-token-references input) direct-self-reference? (get token-references token-name)] (cond empty-input? (p/rejected nil) @@ -104,7 +105,7 @@ Token names should only contain letters and digits separated by . characters.")} (-> (sd/resolve-tokens+ new-tokens #_ {:debug? true}) (p/then (fn [resolved-tokens] - (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-id)] + (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)] (cond resolved-value (p/resolved resolved-token) (sd/missing-reference-error? errors) (p/rejected :error/token-missing-reference) @@ -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/style_dictionary.cljs b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs index 738cdbc1e..3ce334a02 100644 --- a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs @@ -2,18 +2,15 @@ (:require ["@tokens-studio/sd-transforms" :as sd-transforms] ["style-dictionary$default" :as sd] - [app.common.data :as d] [app.main.refs :as refs] [app.main.ui.workspace.tokens.token :as wtt] [cuerdas.core :as str] [promesa.core :as p] - [rumext.v2 :as mf] - [shadow.resource])) + [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" @@ -23,13 +20,6 @@ ;; Functions ------------------------------------------------------------------- -(defn find-token-references - "Finds token reference values in `value-string` and returns a set with all contained namespaces." - [value-string] - (some->> (re-seq #"\{([^}]*)\}" value-string) - (map second) - (into #{}))) - (defn tokens->style-dictionary+ "Resolves references and math expressions using StyleDictionary. Returns a promise with the resolved dictionary." @@ -88,16 +78,16 @@ (resolve-sd-tokens+ config))] (let [resolved-tokens (reduce (fn [acc ^js cur] - (let [value (.-value cur) - resolved-value (d/parse-double (.-value cur)) - original-value (-> cur .-original .-value) - id (uuid (.-uuid (.-id cur))) - missing-reference? (and (not resolved-value) - (re-find #"\{" value) - (= value original-value))] - (cond-> (assoc-in acc [id :resolved-value] resolved-value) - missing-reference? (update-in [id :errors] (fnil conj #{}) :style-dictionary/missing-reference)))) - tokens sd-tokens)] + (let [id (uuid (.-uuid (.-id cur))) + origin-token (get tokens id) + parsed-value (wtt/parse-token-value (.-value cur)) + resolved-token (if (not parsed-value) + (assoc origin-token :errors [:style-dictionary/missing-reference]) + (assoc origin-token + :resolved-value (:value parsed-value) + :resolved-unit (:unit parsed-value)))] + (assoc acc (wtt/token-identifier resolved-token) resolved-token))) + {} sd-tokens)] (when debug? (js/console.log "Resolved tokens" resolved-tokens)) resolved-tokens))) @@ -148,20 +138,31 @@ (comment (defonce !output (atom nil)) + (-> @refs/workspace-tokens + (resolve-tokens+ {:debug? false}) + (.then js/console.log)) + (-> (resolve-workspace-tokens+ {:debug? true}) (p/then #(reset! !output %))) + @!output + (->> @refs/workspace-tokens - (resolve-tokens+)) + (resolve-tokens+) + (#(doto % js/console.log))) (-> (clj->js {"a" {:name "a" :value "5"} "b" {:name "b" :value "{a} * 2"}}) + (#(resolve-sd-tokens+ % {:debug? true}))) + (defonce output (atom nil)) + (require '[shadow.resource]) (let [example (-> (shadow.resource/inline "./data/example-tokens-set.json") (js/JSON.parse) .-core)] - (resolve-sd-tokens+ example {:debug? true})) + (.then (resolve-sd-tokens+ example {:debug? true}) + #(reset! output %))) nil) diff --git a/frontend/src/app/main/ui/workspace/tokens/token.cljs b/frontend/src/app/main/ui/workspace/tokens/token.cljs index b0d12d220..9c0efd356 100644 --- a/frontend/src/app/main/ui/workspace/tokens/token.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/token.cljs @@ -4,6 +4,32 @@ [clojure.set :as set] [cuerdas.core :as str])) +(def parseable-token-value-regexp + "Regexp that can be used to parse a number value out of resolved token value. + This regexp also trims whitespace around the value." + #"^\s*(-?[0-9]+\.?[0-9]*)(px|%)?\s*$") + +(defn parse-token-value + "Parses a resolved value and separates the unit from the value. + Returns a map of {:value `number` :unit `string`}." + [value] + (cond + (number? value) {:value value} + (string? value) (when-let [[_ value unit] (re-find parseable-token-value-regexp value)] + (when-let [parsed-value (d/parse-double value)] + {:value parsed-value + :unit unit})))) + +(defn find-token-references + "Finds token reference values in `value-string` and returns a set with all contained namespaces." + [value-string] + (some->> (re-seq #"\{([^}]*)\}" value-string) + (map second) + (into #{}))) + +(defn token-identifier [{:keys [name] :as _token}] + name) + (defn resolve-token-value [{:keys [value resolved-value] :as _token}] (or resolved-value @@ -11,17 +37,17 @@ (defn attributes-map "Creats an attributes map using collection of `attributes` for `id`." - [attributes id] - (->> (map (fn [attr] {attr id}) attributes) + [attributes token] + (->> (map (fn [attr] [attr (token-identifier token)]) attributes) (into {}))) -(defn remove-attributes-for-token-id +(defn remove-attributes-for-token "Removes applied tokens with `token-id` for the given `attributes` set from `applied-tokens`." - [attributes token-id applied-tokens] + [attributes token applied-tokens] (let [attr? (set attributes)] (->> (remove (fn [[k v]] (and (attr? k) - (= v token-id))) + (= v (token-identifier token)))) applied-tokens) (into {})))) @@ -29,7 +55,7 @@ "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))) + (= (token-identifier token) id))) (defn token-applied? "Test if `token` is applied to a `shape` with at least one of the one of the given `token-attributes`." diff --git a/frontend/test/token_tests/helpers/tokens.cljs b/frontend/test/token_tests/helpers/tokens.cljs index 716dd0755..87db316fb 100644 --- a/frontend/test/token_tests/helpers/tokens.cljs +++ b/frontend/test/token_tests/helpers/tokens.cljs @@ -15,8 +15,8 @@ (defn apply-token-to-shape [file shape-label token-label attributes] (let [first-page-id (get-in file [:data :pages 0]) shape-id (thi/id shape-label) - token-id (thi/id token-label) - applied-attributes (wtt/attributes-map attributes token-id)] + token (get-token file token-label) + applied-attributes (wtt/attributes-map attributes token)] (update-in file [:data :pages-index first-page-id :objects shape-id diff --git a/frontend/test/token_tests/logic/token_actions_test.cljs b/frontend/test/token_tests/logic/token_actions_test.cljs index beef09847..e0d3fac38 100644 --- a/frontend/test/token_tests/logic/token_actions_test.cljs +++ b/frontend/test/token_tests/logic/token_actions_test.cljs @@ -5,6 +5,7 @@ [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.token :as wtt] [cljs.test :as t :include-macros true] [frontend-tests.helpers.pages :as thp] [frontend-tests.helpers.state :as ths] @@ -31,31 +32,58 @@ :type :border-radius}))) (t/deftest test-apply-token - (t/testing "applying a token twice with the same attributes will override the previous applied token" + (t/testing "applies token to shape and updates shape attributes to resolved value" (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))))))))) + 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/testing "shape `:applied-tokens` got updated" + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:rx (:applied-tokens rect-1')) (wtt/token-identifier token-2'))) + (t/is (= (:ry (:applied-tokens rect-1')) (wtt/token-identifier token-2')))) + (t/testing "shape radius got update to the resolved token value." + (t/is (= (:rx rect-1') 24)) + (t/is (= (:ry rect-1') 24)))))))))) + +(t/deftest test-apply-multiple-tokens + (t/testing "applying a token twice with the same attributes will override the previously applied tokens values" + (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/testing "shape `:applied-tokens` got updated" + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:rx (:applied-tokens rect-1')) (wtt/token-identifier token-2'))) + (t/is (= (:ry (:applied-tokens rect-1')) (wtt/token-identifier token-2')))) + (t/testing "shape radius got update to the resolved token value." + (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" @@ -87,58 +115,37 @@ (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/is (= (:r1 (:applied-tokens rect-1')) (wtt/token-identifier 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 [(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/is (= (:r4 (:applied-tokens rect-1')) (wtt/token-identifier token-1')))))))))));))))))))))) (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 [(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))))))))) + 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/testing "shape `:applied-tokens` got updated" + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:width (:applied-tokens rect-1')) (wtt/token-identifier token-target'))) + (t/is (= (:height (:applied-tokens rect-1')) (wtt/token-identifier token-target')))) + (t/testing "shapes width and height got updated" + (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" @@ -160,11 +167,13 @@ (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 "shape `:applied-tokens` got updated" + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:width (:applied-tokens rect-1')) (wtt/token-identifier token-target'))) + (t/is (= (:height (:applied-tokens rect-1')) (wtt/token-identifier token-target')))) + (t/testing "shapes width and height got updated" + (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" @@ -207,13 +216,13 @@ token-opacity-percent (toht/get-token file' :opacity-percent) token-opacity-invalid (toht/get-token file' :opacity-invalid)] (t/testing "float value got translated to float and applied to opacity" - (t/is (= (:opacity (:applied-tokens rect-1')) (:id token-opacity-float))) + (t/is (= (:opacity (:applied-tokens rect-1')) (wtt/token-identifier token-opacity-float))) (t/is (= (:opacity rect-1') 0.3))) (t/testing "percentage value got translated to float and applied to opacity" - (t/is (= (:opacity (:applied-tokens rect-2')) (:id token-opacity-percent))) + (t/is (= (:opacity (:applied-tokens rect-2')) (wtt/token-identifier token-opacity-percent))) (t/is (= (:opacity rect-2') 0.4))) (t/testing "invalid opacity value got applied but did not change shape" - (t/is (= (:opacity (:applied-tokens rect-3')) (:id token-opacity-invalid))) + (t/is (= (:opacity (:applied-tokens rect-3')) (wtt/token-identifier token-opacity-invalid))) (t/is (nil? (:opacity rect-3'))))))))))) (t/deftest test-apply-rotation @@ -237,7 +246,7 @@ 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 (:applied-tokens rect-1')) (wtt/token-identifier token-target'))) (t/is (= (:rotation rect-1') 120))))))))) (t/deftest test-apply-stroke-width @@ -267,10 +276,10 @@ rect-with-stroke' (cths/get-shape file' :rect-1) rect-without-stroke' (cths/get-shape file' :rect-2)] (t/testing "token got applied to rect with stroke and shape stroke got updated" - (t/is (= (:stroke-width (:applied-tokens rect-with-stroke')) (:id token-target'))) + (t/is (= (:stroke-width (:applied-tokens rect-with-stroke')) (wtt/token-identifier token-target'))) (t/is (= (get-in rect-with-stroke' [:strokes 0 :stroke-width]) 10))) (t/testing "token got applied to rect without stroke but shape didnt get updated" - (t/is (= (:stroke-width (:applied-tokens rect-without-stroke')) (:id token-target'))) + (t/is (= (:stroke-width (:applied-tokens rect-without-stroke')) (wtt/token-identifier token-target'))) (t/is (empty? (:strokes rect-without-stroke'))))))))))) (t/deftest test-toggle-token-none @@ -294,10 +303,10 @@ rect-2' (cths/get-shape file' :rect-2)] (t/is (some? (:applied-tokens rect-1'))) (t/is (some? (:applied-tokens rect-2'))) - (t/is (= (:rx (:applied-tokens rect-1')) (:id token-2'))) - (t/is (= (:rx (:applied-tokens rect-2')) (:id token-2'))) - (t/is (= (:ry (:applied-tokens rect-1')) (:id token-2'))) - (t/is (= (:ry (:applied-tokens rect-2')) (:id token-2'))) + (t/is (= (:rx (:applied-tokens rect-1')) (wtt/token-identifier token-2'))) + (t/is (= (:rx (:applied-tokens rect-2')) (wtt/token-identifier token-2'))) + (t/is (= (:ry (:applied-tokens rect-1')) (wtt/token-identifier token-2'))) + (t/is (= (:ry (:applied-tokens rect-2')) (wtt/token-identifier token-2'))) (t/is (= (:rx rect-1') 24)) (t/is (= (:rx rect-2') 24))))))))) @@ -361,10 +370,10 @@ rect-with-other-token-2' (cths/get-shape file' :rect-3)] (t/testing "token got applied to all shapes" - (t/is (= (:rx (:applied-tokens rect-with-other-token-1')) (:id target-token))) - (t/is (= (:rx (:applied-tokens rect-without-token')) (:id target-token))) - (t/is (= (:rx (:applied-tokens rect-with-other-token-2')) (:id target-token))) + (t/is (= (:rx (:applied-tokens rect-with-other-token-1')) (wtt/token-identifier target-token))) + (t/is (= (:rx (:applied-tokens rect-without-token')) (wtt/token-identifier target-token))) + (t/is (= (:rx (:applied-tokens rect-with-other-token-2')) (wtt/token-identifier target-token))) - (t/is (= (:ry (:applied-tokens rect-with-other-token-1')) (:id target-token))) - (t/is (= (:ry (:applied-tokens rect-without-token')) (:id target-token))) - (t/is (= (:ry (:applied-tokens rect-with-other-token-2')) (:id target-token))))))))))) + (t/is (= (:ry (:applied-tokens rect-with-other-token-1')) (wtt/token-identifier target-token))) + (t/is (= (:ry (:applied-tokens rect-without-token')) (wtt/token-identifier target-token))) + (t/is (= (:ry (:applied-tokens rect-with-other-token-2')) (wtt/token-identifier target-token))))))))))) diff --git a/frontend/test/token_tests/style_dictionary_test.cljs b/frontend/test/token_tests/style_dictionary_test.cljs index ff03ba16c..cc96abe1b 100644 --- a/frontend/test/token_tests/style_dictionary_test.cljs +++ b/frontend/test/token_tests/style_dictionary_test.cljs @@ -1,20 +1,37 @@ -;; 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 token-tests.style-dictionary-test (:require - [app.main.ui.workspace.tokens.style-dictionary :as wtsd] - [cljs.test :as t :include-macros true])) + [app.main.ui.workspace.tokens.style-dictionary :as sd] + [cljs.test :as t :include-macros true] + [promesa.core :as p])) -(t/deftest test-find-token-references - ;; Return references - (t/is (= #{"foo" "bar"} (wtsd/find-token-references "{foo} + {bar}"))) - ;; Ignore non reference text - (t/is (= #{"foo.bar.baz"} (wtsd/find-token-references "{foo.bar.baz} + something"))) - ;; No references found - (t/is (nil? (wtsd/find-token-references "1 + 2"))) - ;; Edge-case: Ignore unmatched closing parens - (t/is (= #{"foo" "bar"} (wtsd/find-token-references "{foo}} + {bar}")))) +(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)))))))) diff --git a/frontend/test/token_tests/token_test.cljs b/frontend/test/token_tests/token_test.cljs index bcf86fcc9..4b38c889c 100644 --- a/frontend/test/token_tests/token_test.cljs +++ b/frontend/test/token_tests/token_test.cljs @@ -9,67 +9,95 @@ [app.main.ui.workspace.tokens.token :as wtt] [cljs.test :as t :include-macros true])) +(t/deftest test-parse-token-value + (t/testing "parses double from a token value" + (t/is (= {:value 100.1 :unit nil} (wtt/parse-token-value "100.1"))) + (t/is (= {:value -9 :unit nil} (wtt/parse-token-value "-9")))) + (t/testing "trims white-space" + (t/is (= {:value -1.3 :unit nil} (wtt/parse-token-value " -1.3 ")))) + (t/testing "parses unit: px" + (t/is (= {:value 70.3 :unit "px"} (wtt/parse-token-value " 70.3px ")))) + (t/testing "parses unit: %" + (t/is (= {:value -10 :unit "%"} (wtt/parse-token-value "-10%")))) + (t/testing "parses unit: px") + (t/testing "returns nil for any invalid characters" + (t/is (nil? (wtt/parse-token-value " -1.3a ")))) + (t/testing "doesnt accept invalid double" + (t/is (nil? (wtt/parse-token-value ".3"))))) + +(t/deftest find-token-references + (t/testing "finds references inside curly braces in a string" + (t/is (= #{"foo" "bar"} (wtt/find-token-references "{foo} + {bar}"))) + (t/testing "ignores extra text" + (t/is (= #{"foo.bar.baz"} (wtt/find-token-references "{foo.bar.baz} + something")))) + (t/testing "ignores string without references" + (t/is (nil? (wtt/find-token-references "1 + 2")))) + (t/testing "handles edge-case for extra curly braces" + (t/is (= #{"foo" "bar"} (wtt/find-token-references "{foo}} + {bar}")))))) + (t/deftest remove-attributes-for-token-id - (t/testing "removes attributes matching the `token-id`, keeps other attributes" - (t/is (= {:ry :b} - (wtt/remove-attributes-for-token-id - #{:rx :ry} :a {:rx :a :ry :b}))))) + (t/testing "removes attributes matching the `token`, keeps other attributes" + (t/is (= {:ry "b"} + (wtt/remove-attributes-for-token #{:rx :ry} {:name "a"} {:rx "a" :ry "b"}))))) (t/deftest token-applied-test (t/testing "matches passed token with `:token-attributes`" - (t/is (true? (wtt/token-applied? {:id :a} {:applied-tokens {:x :a}} #{:x})))) + (t/is (true? (wtt/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x})))) (t/testing "doesn't match empty token" - (t/is (nil? (wtt/token-applied? {} {:applied-tokens {:x :a}} #{:x})))) + (t/is (nil? (wtt/token-applied? {} {:applied-tokens {:x "a"}} #{:x})))) (t/testing "does't match passed token `:id`" - (t/is (nil? (wtt/token-applied? {:id :b} {:applied-tokens {:x :a}} #{:x})))) + (t/is (nil? (wtt/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x})))) (t/testing "doesn't match passed `:token-attributes`" - (t/is (nil? (wtt/token-applied? {:id :a} {:applied-tokens {:x :a}} #{:y}))))) + (t/is (nil? (wtt/token-applied? {:name "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}} + (t/is (= #{:x} (wtt/token-applied-attributes {:name "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 + 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))) + shape-applied-all {:id "shape-applied-all" + :applied-tokens {:x "1" :y "1" :z "1"}} + shape-ids (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)))))) + expected (wtt/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)] + (t/is (= (:x expected) (shape-ids shape-applied-x + shape-applied-x-y + shape-applied-all))) + (t/is (= (:y expected) (shape-ids shape-applied-y + shape-applied-x-y + shape-applied-all))) + (t/is (= (:z expected) (shape-ids shape-applied-all))) + (t/is (true? (wtt/shapes-applied-all? expected (shape-ids shape-applied-all) attributes))) + (t/is (false? (wtt/shapes-applied-all? expected (apply shape-ids shapes) attributes))) + (shape-ids shape-applied-x + shape-applied-x-y + shape-applied-all)))) (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}} - {:applied-tokens {:x :b}}] + (t/is (true? (wtt/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}} + {:applied-tokens {:x "b"}}] #{:x})))) (t/testing "is false when no shape matches the token or attributes" - (t/is (nil? (wtt/shapes-token-applied? {:id :a} [{:applied-tokens {:x :b}} - {:applied-tokens {:x :b}}] + (t/is (nil? (wtt/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}} + {:applied-tokens {:x "b"}}] #{:x}))) - (t/is (nil? (wtt/shapes-token-applied? {:id :a} [{:applied-tokens {:x :a}} - {:applied-tokens {:x :a}}] + (t/is (nil? (wtt/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}} + {:applied-tokens {:x "a"}}] #{:y}))))) (t/deftest name->path-test