diff --git a/frontend/package.json b/frontend/package.json index a373539f99..c4b5b3c950 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,10 @@ "test:run": "node target/tests.cjs", "test:watch": "clojure -M:dev:shadow-cljs watch test", "test": "yarn run test:compile && yarn run test:run", + "token-test:compile": "clojure -M:dev:shadow-cljs compile test-esm --config-merge '{:autorun false}'", + "token-test:run": "bun target/tests-esm.cjs", + "token-test:watch": "clojure -M:dev:shadow-cljs watch test-esm", + "token-test": "yarn run token-test:compile && yarn run token-test:run", "translations:validate": "node ./scripts/validate-translations.js", "translations:find-unused": "node ./scripts/find-unused-translations.js", "compile": "node ./scripts/compile.js", diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index 7a5b4c4ff1..7201b1dcb7 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -106,8 +106,8 @@ :warnings {:fn-deprecated false}}} :lib-penpot - {:target :esm - :output-dir "resources/public/libs" + {:target :esm + :output-dir "resources/public/libs" :modules {:penpot {:exports {:renderPage app.libs.render/render-page-export @@ -158,5 +158,17 @@ :source-map-detail-level :all :warnings {:fn-deprecated false}}} - }} + :test-esm + {:target :node-test + :output-to "target/tests-esm.cjs" + :output-dir "target/test-esm" + :ns-regexp "^token-tests.*-test$" + :autorun true + :compiler-options + {:output-feature-set :es2020 + :output-wrapper false + :source-map true + :source-map-include-sources-content true + :source-map-detail-level :all + :warnings {:fn-deprecated false}}}}} diff --git a/frontend/src/app/main/ui/workspace/tokens/CHANGELOG.md b/frontend/src/app/main/ui/workspace/tokens/CHANGELOG.md index 26f32ec3ed..95291feb56 100644 --- a/frontend/src/app/main/ui/workspace/tokens/CHANGELOG.md +++ b/frontend/src/app/main/ui/workspace/tokens/CHANGELOG.md @@ -2,6 +2,9 @@ Add changes that are meaningful to the user here after each PR so they can be updated in feature base. +
+Template + ## Template ### - @@ -11,9 +14,22 @@ Add changes that are meaningful to the user here after each PR so they can be up If possible add video here from PR as well - Outline of changes +
## Changes +### 2024-06-25 - Token Insert/Edit Validation + Value Preview + +[Video](https://private-user-images.githubusercontent.com/1898374/342781533-06054a7e-3efb-4f48-a063-8b03f4b8fe5c.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTkzMjgwNzYsIm5iZiI6MTcxOTMyNzc3NiwicGF0aCI6Ii8xODk4Mzc0LzM0Mjc4MTUzMy0wNjA1NGE3ZS0zZWZiLTRmNDgtYTA2My04YjAzZjRiOGZlNWMubXA0P1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDYyNSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA2MjVUMTUwMjU2WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZDliZmUwMzU1MWY3NWQ2NWZkYzA0ODYxYzYzMTYzMjMyOGZjZGMzZDNhMWJmZGI4ZmM3NmU2NzNjYjY2MTdmMCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.44rKA1h3Cvw-vDWevnx7xVUeuZ1ezV4pqEtekVXgVds) + +https://github.com/tokens-studio/tokens-studio-for-penpot/pull/194 + +Adds validation to the token create/edit field + + - Name duplication is not allowed and takes a min/max length + - Value has to be a resolvable value + - Description has max value + ### 2024-06-19 - Added CHANGELOG.md Added template for changelog diff --git a/frontend/src/app/main/ui/workspace/tokens/common.cljs b/frontend/src/app/main/ui/workspace/tokens/common.cljs index 03b3d4239a..cd955330c4 100644 --- a/frontend/src/app/main/ui/workspace/tokens/common.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/common.cljs @@ -34,7 +34,7 @@ (mf/defc labeled-input {::mf/wrap-props false} - [{:keys [label input-props auto-complete?]}] + [{:keys [label input-props auto-complete? error?]}] (let [input-props (cond-> input-props :always camel-keys ;; Disable auto-complete on form fields for proprietary password managers @@ -42,6 +42,7 @@ (not auto-complete?) (assoc "data-1p-ignore" true "data-lpignore" true :auto-complete "off"))] - [:label {:class (stl/css :labeled-input)} + [:label {:class (stl/css-case :labeled-input true + :labeled-input-error error?)} [:span {:class (stl/css :label)} label] [:& :input input-props]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/common.scss b/frontend/src/app/main/ui/workspace/tokens/common.scss index 30d611be7e..9398a2bb23 100644 --- a/frontend/src/app/main/ui/workspace/tokens/common.scss +++ b/frontend/src/app/main/ui/workspace/tokens/common.scss @@ -18,6 +18,10 @@ } } +.labeled-input-error { + border: 1px solid var(--status-color-error-500) !important; +} + .button { @extend .button-primary; } diff --git a/frontend/src/app/main/ui/workspace/tokens/form.cljs b/frontend/src/app/main/ui/workspace/tokens/form.cljs new file mode 100644 index 0000000000..9dd1930a4b --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/form.cljs @@ -0,0 +1,265 @@ +;; 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.form + (:require-macros [app.main.style :as stl]) + (:require + ["lodash.debounce" :as debounce] + [app.main.data.modal :as modal] + [app.main.data.tokens :as dt] + [app.main.store :as st] + [app.main.ui.workspace.tokens.common :as tokens.common] + [app.main.ui.workspace.tokens.style-dictionary :as sd] + [app.util.dom :as dom] + [cuerdas.core :as str] + [malli.core :as m] + [malli.error :as me] + [promesa.core :as p] + [rumext.v2 :as mf])) + +;; Schemas --------------------------------------------------------------------- + +(defn token-name-schema + "Generate a dynamic schema validation to check if a token name already exists. + `existing-token-names` should be a set of strings." + [existing-token-names] + (let [non-existing-token-schema + (m/-simple-schema + {:type :token/name-exists + :pred #(not (get existing-token-names %)) + :type-properties {:error/fn #(str (:value %) " is an already existing token name") + :existing-token-names existing-token-names}})] + (m/schema + [:and + [:string {:min 1 :max 255}] + non-existing-token-schema]))) + +(def token-description-schema + (m/schema + [:string {:max 2048}])) + +;; Helpers --------------------------------------------------------------------- + +(defn finalize-name [name] + (str/trim name)) + +(defn valid-name? [name] + (seq (finalize-name (str name)))) + +(defn finalize-value [value] + (-> (str value) + (str/trim))) + +(defn valid-value? [value] + (seq (finalize-value value))) + +(defn schema-validation->promise [validated] + (if (:errors validated) + (p/rejected validated) + (p/resolved validated))) + +;; Component ------------------------------------------------------------------- + +(defn validate-token-value+ + "Validates token value by resolving the value `input` using `StyleDictionary`. + Returns a promise of either resolved tokens or rejects with an error state." + [{:keys [input name-value token tokens]}] + (let [empty-input? (empty? (str/trim input)) + ;; Check if the given value contains a reference that is the current token-name + ;; 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) + direct-self-reference? (get token-references token-name)] + (cond + empty-input? (p/rejected nil) + direct-self-reference? (p/rejected :error/token-direct-self-reference) + :else (let [token-id (or (:id token) (random-uuid)) + new-tokens (update tokens token-id merge {:id token-id + :value input + :name token-name})] + (-> (sd/resolve-tokens+ new-tokens) + (p/then + (fn [resolved-tokens] + (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-id)] + (cond + resolved-value (p/resolved resolved-token) + (sd/missing-reference-error? errors) (p/rejected :error/token-missing-reference) + :else (p/rejected :error/unknown-error)))))))))) + +(defn use-debonced-resolve-callback + "Resolves a token values using `StyleDictionary`. + This function is debounced as the resolving might be an expensive calculation. + Uses a custom debouncing logic, as the resolve function is async." + [name-ref token tokens callback & {:keys [timeout] :or {timeout 160}}] + (let [timeout-id-ref (mf/use-ref nil) + debounced-resolver-callback + (mf/use-callback + (mf/deps token callback tokens) + (fn [event] + (let [input (dom/get-target-val event) + timeout-id (js/Symbol) + ;; Dont execute callback when the timout-id-ref is outdated because this function got called again + timeout-outdated-cb? #(not= (mf/ref-val timeout-id-ref) timeout-id)] + (mf/set-ref-val! timeout-id-ref timeout-id) + (js/setTimeout + (fn [] + (when (not (timeout-outdated-cb?)) + (-> (validate-token-value+ {:input input + :name-value @name-ref + :token token + :tokens tokens}) + (p/finally (fn [x err] + (when-not (timeout-outdated-cb?) + (callback (or err x)))))))) + timeout))))] + debounced-resolver-callback)) + +(mf/defc form + {::mf/wrap-props false} + [{:keys [token token-type] :as _args}] + (let [tokens (sd/use-resolved-workspace-tokens) + existing-token-names (mf/use-memo + (mf/deps tokens) + (fn [] + (-> (into #{} (map (fn [[_ {:keys [name]}]] name) tokens)) + ;; Remove the currently editing token name, + ;; as we don't want it to show when checking for duplicate names. + (disj (:name token))))) + + ;; Name + name-ref (mf/use-var (:name token)) + name-errors (mf/use-state nil) + validate-name (mf/use-callback + (mf/deps existing-token-names) + (fn [value] + (let [schema (token-name-schema existing-token-names)] + (m/explain schema (finalize-name value))))) + on-update-name-debounced (mf/use-callback + (debounce (fn [e] + (let [value (dom/get-target-val e) + errors (validate-name value)] + (reset! name-errors errors))))) + on-update-name (mf/use-callback + (mf/deps on-update-name-debounced) + (fn [e] + (reset! name-ref (dom/get-target-val e)) + (on-update-name-debounced e))) + valid-name-field? (and + (not @name-errors) + (valid-name? @name-ref)) + + ;; Value + value-ref (mf/use-var (:value token)) + token-resolve-result (mf/use-state (get-in tokens [(:id token) :resolved-value])) + set-resolve-value (mf/use-callback + (fn [token-or-err] + (let [v (cond + (= token-or-err :error/token-direct-self-reference) token-or-err + (= token-or-err :error/token-missing-reference) token-or-err + (:resolved-value token-or-err) (:resolved-value token-or-err))] + (reset! token-resolve-result v)))) + on-update-value-debounced (use-debonced-resolve-callback name-ref token tokens set-resolve-value) + on-update-value (mf/use-callback + (mf/deps on-update-value-debounced) + (fn [e] + (reset! value-ref (dom/get-target-val e)) + (on-update-value-debounced e))) + value-error? (when (keyword? @token-resolve-result) + (= (namespace @token-resolve-result) "error")) + valid-value-field? (and + (not value-error?) + (valid-value? @token-resolve-result)) + + ;; Description + description-ref (mf/use-var (:description token)) + description-errors (mf/use-state nil) + validate-descripion (mf/use-callback #(m/explain token-description-schema %)) + on-update-description-debounced (mf/use-callback + (debounce (fn [e] + (let [value (dom/get-target-val e) + errors (validate-descripion value)] + (reset! description-errors errors))))) + on-update-description (mf/use-callback + (mf/deps on-update-description-debounced) + (fn [e] + (reset! description-ref (dom/get-target-val e)) + (on-update-description-debounced e))) + valid-description-field? (not @description-errors) + + ;; Form + disabled? (or (not valid-name-field?) + (not valid-value-field?) + (not valid-description-field?)) + + on-submit (mf/use-callback + (mf/deps validate-name validate-descripion token tokens) + (fn [e] + (dom/prevent-default e) + ;; We have to re-validate the current form values before submitting + ;; because the validation is asynchronous/debounced + ;; and the user might have edited a valid form to make it invalid, + ;; and press enter before the next validations could return. + (let [final-name (finalize-name @name-ref) + valid-name?+ (-> (validate-name final-name) schema-validation->promise) + final-value (finalize-value @value-ref) + final-description @description-ref + valid-description?+ (some-> final-description validate-descripion schema-validation->promise)] + (-> (p/all [valid-name?+ + valid-description?+ + (validate-token-value+ {:input final-value + :name-value final-name + :token token + :tokens 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 + (when (and (seq result) (not err)) + (let [token (cond-> {:name final-name + :type (or (:type token) token-type) + :value final-value} + final-description (assoc :description final-description) + (:id token) (assoc :id (:id token)))] + (st/emit! (dt/add-token token)) + (modal/hide!)))))))))] + [:form + {:on-submit on-submit} + [:div {:class (stl/css :token-rows)} + [:div + [:& tokens.common/labeled-input {:label "Name" + :error? @name-errors + :input-props {:default-value @name-ref + :auto-focus true + :on-blur on-update-name + :on-change on-update-name}}] + (when @name-errors + [:p {:class (stl/css :error)} + (me/humanize @name-errors)])] + [:& tokens.common/labeled-input {:label "Value" + :input-props {:default-value @value-ref + :on-blur on-update-value + :on-change on-update-value}}] + [:div {:class (stl/css-case :resolved-value true + :resolved-value-placeholder (nil? @token-resolve-result) + :resolved-value-error value-error?)} + (case @token-resolve-result + :error/token-direct-self-reference "Token has self reference" + :error/token-missing-reference "Token has missing reference" + :error/unknown-error "" + nil "Enter token value" + [:p @token-resolve-result])] + [:div + [:& tokens.common/labeled-input {:label "Description" + :input-props {:default-value @description-ref + :on-change on-update-description}}] + (when @description-errors + [:p {:class (stl/css :error)} + (me/humanize @description-errors)])] + [:div {:class (stl/css :button-row)} + [:button {:class (stl/css :button) + :type "submit" + :disabled disabled?} + "Save"]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/form.scss b/frontend/src/app/main/ui/workspace/tokens/form.scss new file mode 100644 index 0000000000..cb67c90017 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/form.scss @@ -0,0 +1,50 @@ +// 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"; +@import "./common.scss"; + +.button-row { + display: flex; + flex-direction: column; + margin-top: $s-16; +} + +.token-rows { + display: flex; + flex-direction: column; + gap: $s-8; +} + +.error { + @include bodySmallTypography; + margin-top: $s-6; + margin-bottom: 0; + color: var(--status-color-error-500); +} + +.resolved-value { + @include bodySmallTypography; + padding: $s-4 $s-6; + font-weight: medium; + height: $s-24; + + color: var(--color-foreground-primary); + border: 1px solid color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent); + + p { + font-size: $fs-12; + margin: 0; + } +} + +.resolved-value-placeholder { + color: var(--color-foreground-secondary); +} + +.resolved-value-error { + color: var(--status-color-error-500); +} diff --git a/frontend/src/app/main/ui/workspace/tokens/modal.cljs b/frontend/src/app/main/ui/workspace/tokens/modal.cljs deleted file mode 100644 index 755aa7a54f..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/modal.cljs +++ /dev/null @@ -1,102 +0,0 @@ -;; 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.modal - (:require-macros [app.main.style :as stl]) - (:require - [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.util.dom :as dom] - [okulary.core :as l] - [rumext.v2 :as mf])) - -(defn calculate-position - "Calculates the style properties for the given coordinates and position" - [{vh :height} position x y] - (let [;; picker height in pixels - h 510 - ;; Checks for overflow outside the viewport height - overflow-fix (max 0 (+ y (- 50) h (- vh))) - - x-pos 325] - (cond - (or (nil? x) (nil? y)) {:left "auto" :right "16rem" :top "4rem"} - (= position :left) {:left (str (- x x-pos) "px") - :top (str (- y 50 overflow-fix) "px")} - :else {:left (str (+ x 80) "px") - :top (str (- y 70 overflow-fix) "px")}))) - -(def viewport - (l/derived :vport refs/workspace-local)) - -(defn fields->map [fields] - (->> (map (fn [{:keys [key] :as field}] - [key (:value field)]) fields) - (into {}))) - -(mf/defc tokens-properties-form - {::mf/wrap-props false} - [{:keys [token-type x y position fields token]}] - (let [vport (mf/deref viewport) - style (calculate-position vport position x y) - - name (mf/use-var (or (:name token) "")) - on-update-name #(reset! name (dom/get-target-val %)) - - token-value (mf/use-var (or (:value token) "")) - - description (mf/use-var (or (:description token) "")) - on-update-description #(reset! description (dom/get-target-val %)) - - initial-fields (mapv (fn [field] - (assoc field :value (or (:value token) ""))) - fields) - state (mf/use-state initial-fields) - - on-update-state-field (fn [idx e] - (let [value (dom/get-target-val e)] - (swap! state assoc-in [idx :value] value))) - - on-submit (fn [e] - (dom/prevent-default e) - (let [token-value (-> (fields->map @state) - (first) - (val)) - token (cond-> {:name @name - :type (or (:type token) token-type) - :value token-value} - @description (assoc :description @description) - (:id token) (assoc :id (:id token)))] - (st/emit! (dt/add-token token)) - (modal/hide!)))] - - [:form - {:class (stl/css :shadow) - :style (clj->js style) - :on-submit on-submit} - [:div {:class (stl/css :token-rows)} - [:& tokens.common/labeled-input {:label "Name" - :input-props {:default-value @name - :auto-focus true - :on-change on-update-name}}] - (for [[idx {:keys [type label]}] (d/enumerate @state)] - [:* {:key (str "form-field-" idx)} - (case type - :box-shadow [:p "TODO BOX SHADOW"] - [:& tokens.common/labeled-input {:label label - :input-props {:default-value @token-value - :on-change #(on-update-state-field idx %)}}])]) - [:& tokens.common/labeled-input {:label "Description" - :input-props {:default-value @description - :on-change #(on-update-description %)}}] - [:div {:class (stl/css :button-row)} - [:button {:class (stl/css :button) - :type "submit"} - "Save"]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/modal.scss b/frontend/src/app/main/ui/workspace/tokens/modal.scss deleted file mode 100644 index d40a861dd1..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/modal.scss +++ /dev/null @@ -1,66 +0,0 @@ -// 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"; -@import "./common.scss"; - -.button-row { - display: flex; - flex-direction: column; - margin-top: $s-16; -} - -.token-rows { - display: flex; - flex-direction: column; - gap: $s-8; -} - -.shadow { - @extend .modal-container-base; - @include menuShadow; - position: absolute; - z-index: 11; - overflow-y: auto; - overflow-x: hidden; - - &-select-wrapper { - display: flex; - grid-gap: $s-4; - } - - &-properties { - display: flex; - flex-direction: column; - grid-gap: $s-4; - } - - .inputs-grid { - display: grid; - grid-template-areas: - "x blur blur spread spread" - "y color color color color"; - grid-template-columns: repeat(5, 1fr); - grid-template-rows: repeat(2, 1fr); - grid-gap: $s-4; - - label:nth-child(1) { - grid-area: x; - } - label:nth-child(2) { - grid-area: y; - } - label:nth-child(3) { - grid-area: blur; - } - label:nth-child(4) { - grid-area: spread; - } - label:nth-child(5) { - grid-area: color; - } - } -} diff --git a/frontend/src/app/main/ui/workspace/tokens/modals.cljs b/frontend/src/app/main/ui/workspace/tokens/modals.cljs index 9404c9c07d..23edf75ede 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals.cljs @@ -5,85 +5,124 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.tokens.modals + (:require-macros [app.main.style :as stl]) (:require [app.main.data.modal :as modal] - [app.main.ui.workspace.tokens.modal :refer [tokens-properties-form]] + [app.main.refs :as refs] + [app.main.ui.workspace.tokens.form :refer [form]] + [okulary.core :as l] [rumext.v2 :as mf])) +;; Component ------------------------------------------------------------------- + +(defn calculate-position + "Calculates the style properties for the given coordinates and position" + [{vh :height} position x y] + (let [;; picker height in pixels + h 510 + ;; Checks for overflow outside the viewport height + overflow-fix (max 0 (+ y (- 50) h (- vh))) + + x-pos 325] + (cond + (or (nil? x) (nil? y)) {:left "auto" :right "16rem" :top "4rem"} + (= position :left) {:left (str (- x x-pos) "px") + :top (str (- y 50 overflow-fix) "px")} + :else {:left (str (+ x 80) "px") + :top (str (- y 70 overflow-fix) "px")}))) + +(defn use-viewport-position-style [x y position] + (let [vport (-> (l/derived :vport refs/workspace-local) + (mf/deref))] + (-> (calculate-position vport position x y) + (clj->js)))) + +(mf/defc modal + {::mf/wrap-props false} + [{:keys [x y position token token-type] :as _args}] + (let [wrapper-style (use-viewport-position-style x y position)] + [:div + {:class (stl/css :shadow) + :style wrapper-style} + [:& form {:token token + :token-type token-type}]])) + +;; Modals ---------------------------------------------------------------------- + (mf/defc boolean-modal {::mf/register modal/components ::mf/register-as :tokens/boolean} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) (mf/defc border-radius-modal {::mf/register modal/components ::mf/register-as :tokens/border-radius} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) (mf/defc stroke-width-modal {::mf/register modal/components ::mf/register-as :tokens/stroke-width} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) (mf/defc box-shadow-modal {::mf/register modal/components ::mf/register-as :tokens/box-shadow} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) (mf/defc sizing-modal {::mf/register modal/components ::mf/register-as :tokens/sizing} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) (mf/defc dimensions-modal {::mf/register modal/components ::mf/register-as :tokens/dimensions} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) (mf/defc numeric-modal {::mf/register modal/components ::mf/register-as :tokens/numeric} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) (mf/defc opacity-modal {::mf/register modal/components ::mf/register-as :tokens/opacity} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) (mf/defc other-modal {::mf/register modal/components ::mf/register-as :tokens/other} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) (mf/defc rotation-modal {::mf/register modal/components ::mf/register-as :tokens/rotation} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) (mf/defc spacing-modal {::mf/register modal/components ::mf/register-as :tokens/spacing} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) (mf/defc string-modal {::mf/register modal/components ::mf/register-as :tokens/string} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) (mf/defc typography-modal {::mf/register modal/components ::mf/register-as :tokens/typography} [properties] - [:& tokens-properties-form properties]) + [:& modal properties]) diff --git a/frontend/src/app/main/ui/workspace/tokens/modals.scss b/frontend/src/app/main/ui/workspace/tokens/modals.scss new file mode 100644 index 0000000000..c0e045448d --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/modals.scss @@ -0,0 +1,16 @@ +// 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"; + +.shadow { + @extend .modal-container-base; + @include menuShadow; + position: absolute; + z-index: 11; + overflow-y: auto; + overflow-x: hidden; +} 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 d3c6ceef9c..d9d3c4a0fa 100644 --- a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs @@ -4,6 +4,7 @@ ["style-dictionary$default" :as sd] [app.common.data :as d] [app.main.refs :as refs] + [app.util.dom :as dom] [cuerdas.core :as str] [promesa.core :as p] [rumext.v2 :as mf] @@ -16,12 +17,19 @@ (do (sd-transforms/registerTransforms sd) (.registerFormat sd #js {:name "custom/json" - :format (fn [res] + :format (fn [^js res] (.-tokens (.-dictionary res)))}) sd)) ;; 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." @@ -69,6 +77,11 @@ errors) (str/join "\n"))) +(defn missing-reference-error? + [errors] + (and (set? errors) + (get errors :style-dictionary/missing-reference))) + (defn tokens-name-map [tokens] (->> tokens (map (fn [[_ x]] [(:name x) x])) @@ -77,7 +90,6 @@ (defn resolve-tokens+ [tokens & {:keys [debug?] :as config}] (p/let [sd-tokens (-> (tokens-name-map tokens) - (clj->js) (resolve-sd-tokens+ config))] (let [resolved-tokens (reduce (fn [acc ^js cur] @@ -129,25 +141,15 @@ (reset! tokens-state resolved-tokens)))))))) @tokens-state)) -(defn use-resolved-workspace-tokens - ([] (use-resolved-tokens nil)) - ([options] - (-> (mf/deref refs/workspace-tokens) - (use-resolved-tokens options)))) +(defn use-resolved-workspace-tokens [& {:as config}] + (-> (mf/deref refs/workspace-tokens) + (use-resolved-tokens config))) ;; Testing --------------------------------------------------------------------- -(defn tokens-studio-example [] - (-> (shadow.resource/inline "./data/example-tokens-set.json") - (js/JSON.parse) - .-core)) - (comment - (defonce !output (atom nil)) - @!output - (-> (resolve-workspace-tokens+ {:debug? true}) (p/then #(reset! !output %))) @@ -159,7 +161,9 @@ "b" {:name "b" :value "{a} * 2"}}) (#(resolve-sd-tokens+ % {:debug? true}))) - (-> (tokens-studio-example) - (resolve-sd-tokens+ {:debug? true})) + (let [example (-> (shadow.resource/inline "./data/example-tokens-set.json") + (js/JSON.parse) + .-core)] + (resolve-sd-tokens+ example {:debug? true})) nil) 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..ff03ba16c8 --- /dev/null +++ b/frontend/test/token_tests/style_dictionary_test.cljs @@ -0,0 +1,20 @@ +;; 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])) + +(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}"))))