diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 6de8f9c297..ed63908ab2 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -35,8 +35,8 @@ (def token-types #{:boolean :border-radius - :stroke-width :box-shadow + :color :dimensions :numeric :opacity @@ -45,6 +45,7 @@ :sizing :spacing :string + :stroke-width :typography}) (defn valid-token-type? @@ -66,6 +67,12 @@ [:description {:optional true} :string] [:modified-at {:optional true} ::sm/inst]]) +(sm/register! ::color + [:map + [:color {:optional true} token-name-ref]]) + +(def color-keys (schema-keys ::color)) + (sm/register! ::border-radius [:map [:rx {:optional true} token-name-ref] diff --git a/frontend/package.json b/frontend/package.json index c7d620fc82..047329918a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -116,6 +116,7 @@ "source-map-support": "^0.5.21", "style-dictionary": "patch:style-dictionary@npm%3A4.0.0-prerelease.36#~/.yarn/patches/style-dictionary-npm-4.0.0-prerelease.36-55c0fc33bd.patch", "tdigest": "^0.1.2", + "tinycolor2": "npm:^1.6.0", "ua-parser-js": "^1.0.38", "xregexp": "^5.1.1" } diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 2170ebfebd..a762475384 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -52,6 +52,25 @@ ;; --- Color Picker Modal +(defn use-color-picker-css-variables! [node-ref current-color] + (mf/with-effect [current-color] + (let [node (mf/ref-val node-ref) + {:keys [r g b h v]} current-color + rgb [r g b] + hue-rgb (cc/hsv->rgb [h 1.0 255]) + hsl-from (cc/hsv->hsl [h 0.0 v]) + hsl-to (cc/hsv->hsl [h 1.0 v]) + + format-hsl (fn [[h s l]] + (str/fmt "hsl(%s, %s, %s)" + h + (str (* s 100) "%") + (str (* l 100) "%")))] + (dom/set-css-property! node "--color" (str/join ", " rgb)) + (dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb)) + (dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from)) + (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))))) + (mf/defc colorpicker {::mf/props :obj} [{:keys [data disable-gradient disable-opacity disable-image on-change on-accept]}] @@ -220,23 +239,7 @@ (st/emit! (dc/update-colorpicker data))) ;; Updates the CSS color variable when there is a change in the color - (mf/with-effect [current-color] - (let [node (mf/ref-val node-ref) - {:keys [r g b h v]} current-color - rgb [r g b] - hue-rgb (cc/hsv->rgb [h 1.0 255]) - hsl-from (cc/hsv->hsl [h 0.0 v]) - hsl-to (cc/hsv->hsl [h 1.0 v]) - - format-hsl (fn [[h s l]] - (str/fmt "hsl(%s, %s, %s)" - h - (str (* s 100) "%") - (str (* l 100) "%")))] - (dom/set-css-property! node "--color" (str/join ", " rgb)) - (dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb)) - (dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from)) - (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))) + (use-color-picker-css-variables! node-ref current-color) ;; Updates color when pixel picker is used (mf/with-effect [picking-color? picked-color picked-color-select] diff --git a/frontend/src/app/main/ui/workspace/tokens/changes.cljs b/frontend/src/app/main/ui/workspace/tokens/changes.cljs index 95243ee513..e1438da95e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/changes.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/changes.cljs @@ -8,6 +8,7 @@ (:require [app.common.types.shape.radius :as ctsr] [app.common.types.token :as ctt] + [app.main.data.workspace.colors :as wdc] [app.main.data.workspace :as udw] [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shapes :as dwsh] @@ -18,7 +19,8 @@ [app.main.ui.workspace.tokens.token :as wtt] [beicon.v2.core :as rx] [clojure.set :as set] - [potok.v2.core :as ptk])) + [potok.v2.core :as ptk] + [app.main.ui.workspace.tokens.tinycolor :as tinycolor])) ;; Token Updates --------------------------------------------------------------- @@ -123,6 +125,14 @@ {:reg-objects? true :attrs [:strokes]})) +(defn update-color + [value shape-ids] + (let [color (some->> value + (tinycolor/valid-color) + (tinycolor/->hex) + (str "#"))] + (wdc/change-fill shape-ids {:color color} 0))) + (defn update-shape-dimensions [value shape-ids attributes] (ptk/reify ::update-shape-dimensions ptk/WatchEvent diff --git a/frontend/src/app/main/ui/workspace/tokens/common.cljs b/frontend/src/app/main/ui/workspace/tokens/common.cljs index 7f1c787796..ea0b8bc65f 100644 --- a/frontend/src/app/main/ui/workspace/tokens/common.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/common.cljs @@ -125,7 +125,7 @@ (mf/defc labeled-input {::mf/wrap-props false} - [{:keys [label input-props auto-complete? error? icon render-right]}] + [{:keys [label input-props auto-complete? error? render-right]}] (let [input-props (cond-> input-props :always camel-keys ;; Disable auto-complete on form fields for proprietary password managers 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 1eba57205f..6e4156d034 100644 --- a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs @@ -222,10 +222,10 @@ (defn selection-actions [{:keys [type token] :as context-data}] (let [with-actions (get shape-attribute-actions-map (or type (:type token))) - attribute-actions (with-actions context-data)] + attribute-actions (if with-actions (with-actions context-data) [])] (concat attribute-actions - [:separator] + (when (seq attribute-actions) [:separator]) (default-actions context-data)))) ;; Components ------------------------------------------------------------------ diff --git a/frontend/src/app/main/ui/workspace/tokens/errors.cljs b/frontend/src/app/main/ui/workspace/tokens/errors.cljs new file mode 100644 index 0000000000..65beb37741 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/errors.cljs @@ -0,0 +1,42 @@ +(ns app.main.ui.workspace.tokens.errors + (:require + [cuerdas.core :as str])) + +(def error-codes + {:error.token/direct-self-reference + {:error/code :error.token/direct-self-reference + :error/message "Token has self reference"} + + :error.token/invalid-color + {:error/code :error.token/invalid-color + :error/fn #(str "Invalid color value: " %)} + + :error.style-dictionary/missing-reference + {:error/code :error.style-dictionary/missing-reference + :error/fn #(str "Missing token references: " (str/join " " %))} + + :error.style-dictionary/invalid-token-value + {:error/code :error.style-dictionary/invalid-token-value + :error/fn #(str "Invalid token value: " %)} + + :error/unknown + {:error/code :error/unknown + :error/message "Unknown error"}}) + +(defn get-error-code [error-key] + (get error-codes error-key (:error/unknown error-codes))) + +(defn error-with-value [error-key error-value] + (-> (get-error-code error-key) + (assoc :error/value error-value))) + +(defn has-error-code? [error-key errors] + (some #(= (:error/code %) error-key) errors)) + +(defn humanize-errors [errors] + (->> errors + (map (fn [err] + (cond + (:error/fn err) ((:error/fn err) (:error/value err)) + (:error/message err) (:error/message err) + :else err))))) diff --git a/frontend/src/app/main/ui/workspace/tokens/form.cljs b/frontend/src/app/main/ui/workspace/tokens/form.cljs index d58cca1423..b202c3ac80 100644 --- a/frontend/src/app/main/ui/workspace/tokens/form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/form.cljs @@ -8,13 +8,19 @@ (:require-macros [app.main.style :as stl]) (:require ["lodash.debounce" :as debounce] + [app.common.colors :as c] [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.components.color-bullet :refer [color-bullet]] + [app.main.ui.workspace.colorpicker :as colorpicker] + [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector]] [app.main.ui.workspace.tokens.common :as tokens.common] + [app.main.ui.workspace.tokens.errors :as wte] [app.main.ui.workspace.tokens.style-dictionary :as sd] + [app.main.ui.workspace.tokens.tinycolor :as tinycolor] [app.main.ui.workspace.tokens.token :as wtt] [app.main.ui.workspace.tokens.update :as wtu] [app.util.dom :as dom] @@ -84,32 +90,39 @@ Token names should only contain letters and digits separated by . characters.")} ;; Component ------------------------------------------------------------------- +(defn token-self-reference? + [token-name input] + (let [token-references (wtt/find-token-references input) + self-reference? (get token-references token-name)] + self-reference?)) + (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 (wtt/find-token-references input) - direct-self-reference? (get token-references token-name)] + (let [ ;; 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)] (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-name merge {:id token-id - :value input - :name token-name})] - (-> (sd/resolve-tokens+ new-tokens {:names-map? true}) - (p/then - (fn [resolved-tokens] - (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) - :else (p/rejected :error/unknown-error)))))))))) + (empty? (str/trim input)) + (p/rejected {:errors [{:error/code :error/empty-input}]}) + + (token-self-reference? token-name input) + (p/rejected {:errors [(wte/get-error-code :error.token/direct-self-reference)]}) + + :else + (let [token-id (or (:id token) (random-uuid)) + new-tokens (update tokens token-name merge {:id token-id + :value input + :name token-name + :type (:type token)})] + (-> (sd/resolve-tokens+ new-tokens {:names-map? true}) + (p/then + (fn [resolved-tokens] + (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)] + (cond + resolved-value (p/resolved resolved-token) + :else (p/rejected {:errors (or errors (wte/get-error-code :error/unknown-error))})))))))))) (defn use-debonced-resolve-callback "Resolves a token values using `StyleDictionary`. @@ -120,31 +133,74 @@ Token names should only contain letters and digits separated by . characters.")} debounced-resolver-callback (mf/use-callback (mf/deps token callback tokens) - (fn [event] - (let [input (dom/get-target-val event) - timeout-id (js/Symbol) + (fn [value] + (let [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 + (-> (validate-token-value+ {:input value :name-value @name-ref :token token :tokens tokens}) - (p/finally (fn [x err] - (when-not (timeout-outdated-cb?) - (callback (or err x)))))))) + (p/finally + (fn [x err] + (when-not (timeout-outdated-cb?) + (callback (or err x)))))))) timeout))))] debounced-resolver-callback)) (defonce form-token-cache-atom (atom nil)) +(mf/defc ramp + [{:keys [color on-change]}] + (let [wrapper-node-ref (mf/use-ref nil) + dragging? (mf/use-state) + hex->value (fn [hex] + (when-let [tc (tinycolor/valid-color hex)] + (let [hex (str "#" (tinycolor/->hex tc)) + [r g b] (c/hex->rgb hex) + [h s v] (c/hex->hsv hex)] + {:hex hex + :r r :g g :b b + :h h :s s :v v + :alpha 1}))) + value (mf/use-state (hex->value color)) + on-change' (fn [{:keys [hex]}] + (reset! value (hex->value hex)) + (when-not (and @dragging? hex) + (on-change hex)))] + (colorpicker/use-color-picker-css-variables! wrapper-node-ref @value) + [:div {:ref wrapper-node-ref} + [:& ramp-selector + {:color @value + :disable-opacity true + :on-start-drag #(reset! dragging? true) + :on-finish-drag #(reset! dragging? false) + :on-change on-change'}]])) + +(mf/defc token-value-or-errors + [{:keys [result-or-errors]}] + (let [{:keys [errors]} result-or-errors + empty-message? (or (nil? result-or-errors) + (wte/has-error-code? :error/empty-input errors))] + [:div {:class (stl/css-case :resolved-value true + :resolved-value-placeholder empty-message? + :resolved-value-error (seq errors))} + (cond + empty-message? "Enter token value" + errors (->> (wte/humanize-errors errors) + (str/join "\n")) + :else [:p result-or-errors])])) + (mf/defc form {::mf/wrap-props false} - [{:keys [token token-type] :as _args}] - (let [tokens (mf/deref refs/workspace-ordered-token-sets-tokens) + [{:keys [token token-type]}] + (let [validate-name? (mf/use-state (not (:id token))) + token (or token {:type token-type}) + color? (wtt/color-token? token) selected-set-tokens (mf/deref refs/workspace-selected-token-set-tokens) active-theme-tokens (mf/deref refs/workspace-active-theme-sets-tokens) resolved-tokens (sd/use-resolved-tokens active-theme-tokens {:names-map? true @@ -153,7 +209,7 @@ Token names should only contain letters and digits separated by . characters.")} (mf/deps (:name token)) #(wtt/token-name->path (:name token))) selected-set-tokens-tree (mf/use-memo - (mf/deps token-path tokens) + (mf/deps token-path selected-set-tokens) (fn [] (-> (wtt/token-names-tree selected-set-tokens) ;; Allow setting editing token to it's own path @@ -172,7 +228,10 @@ Token names should only contain letters and digits separated by . characters.")} (debounce (fn [e] (let [value (dom/get-target-val e) errors (validate-name value)] - (reset! name-errors errors))))) + ;; Prevent showing error when just going to another field on a new token + (when-not (and validate-name? (str/empty? value)) + (reset! validate-name? false) + (reset! name-errors errors)))))) on-update-name (mf/use-callback (mf/deps on-update-name-debounced) (fn [e] @@ -183,23 +242,33 @@ Token names should only contain letters and digits separated by . characters.")} (valid-name? @name-ref)) ;; Value + color (mf/use-state (when color? (:value token))) + color-ramp-open? (mf/use-state false) + value-input-ref (mf/use-ref nil) value-ref (mf/use-var (:value token)) 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 - (= 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))] + (let [error? (:errors token-or-err) + v (if error? + token-or-err + (:resolved-value token-or-err))] + (when color? (reset! color (if error? nil v))) (reset! token-resolve-result v)))) on-update-value-debounced (use-debonced-resolve-callback name-ref token active-theme-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")) + (let [value (dom/get-target-val e)] + (reset! value-ref value) + (on-update-value-debounced value)))) + on-update-color (mf/use-callback + (mf/deps on-update-value-debounced) + (fn [hex-value] + (reset! value-ref hex-value) + (set! (.-value (mf/ref-val value-input-ref)) hex-value) + (on-update-value-debounced hex-value))) + value-error? (seq (:errors @token-resolve-result)) valid-value-field? (and (not value-error?) (valid-value? @token-resolve-result)) @@ -257,7 +326,8 @@ Token names should only contain letters and digits separated by . characters.")} (st/emit! (wtu/update-workspace-tokens)) (modal/hide!)))))))))] [:form - {:on-submit on-submit} + {:class (stl/css :form-wrapper) + :on-submit on-submit} [:div {:class (stl/css :token-rows)} [:div [:& tokens.common/labeled-input {:label "Name" @@ -275,16 +345,22 @@ Token names should only contain letters and digits separated by . characters.")} [:& 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])] + :on-change on-update-value + :ref value-input-ref} + :render-right (when color? + (mf/fnc [] + [:div {:class (stl/css :color-bullet) + :on-click #(swap! color-ramp-open? not)} + (if-let [hex (some-> @color tinycolor/valid-color tinycolor/->hex)] + [:& color-bullet {:color hex + :mini? true}] + [:div {:class (stl/css :color-bullet-placeholder)}])]))}] + (when @color-ramp-open? + [:& ramp {:color (some-> (or @token-resolve-result (:value token)) + (tinycolor/valid-color)) + :on-change on-update-color}]) + [:& token-value-or-errors {:result-or-errors @token-resolve-result}] + [:div [:& tokens.common/labeled-input {:label "Description" :input-props {:default-value @description-ref diff --git a/frontend/src/app/main/ui/workspace/tokens/form.scss b/frontend/src/app/main/ui/workspace/tokens/form.scss index cb67c90017..4d1b37c86b 100644 --- a/frontend/src/app/main/ui/workspace/tokens/form.scss +++ b/frontend/src/app/main/ui/workspace/tokens/form.scss @@ -7,6 +7,10 @@ @import "refactor/common-refactor.scss"; @import "./common.scss"; +.form-wrapper { + width: $s-260; +} + .button-row { display: flex; flex-direction: column; @@ -30,7 +34,7 @@ @include bodySmallTypography; padding: $s-4 $s-6; font-weight: medium; - height: $s-24; + min-height: 1lh; color: var(--color-foreground-primary); border: 1px solid color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent); @@ -48,3 +52,19 @@ .resolved-value-error { color: var(--status-color-error-500); } + +.color-bullet { + margin-right: $s-8; + cursor: pointer; +} + +.color-bullet-placeholder { + width: var(--bullet-size, $s-16); + height: var(--bullet-size, $s-16); + min-width: var(--bullet-size, $s-16); + min-height: var(--bullet-size, $s-16); + margin-top: 0; + background-color: color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent); + border-radius: $br-4; + cursor: pointer; +} diff --git a/frontend/src/app/main/ui/workspace/tokens/modals.cljs b/frontend/src/app/main/ui/workspace/tokens/modals.cljs index 30a45c49f9..5e4358d341 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals.cljs @@ -68,6 +68,12 @@ [properties] [:& token-update-create-modal properties]) +(mf/defc color-modal + {::mf/register modal/components + ::mf/register-as :tokens/color} + [properties] + [:& token-update-create-modal properties]) + (mf/defc stroke-width-modal {::mf/register modal/components ::mf/register-as :tokens/stroke-width} diff --git a/frontend/src/app/main/ui/workspace/tokens/modals.scss b/frontend/src/app/main/ui/workspace/tokens/modals.scss index c0e045448d..cb16e1ec21 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals.scss +++ b/frontend/src/app/main/ui/workspace/tokens/modals.scss @@ -8,6 +8,9 @@ .shadow { @extend .modal-container-base; + width: auto; + max-width: auto; + min-width: auto; @include menuShadow; position: absolute; z-index: 11; diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index 879ef97af9..b76b8e1df8 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -13,6 +13,7 @@ [app.main.data.tokens :as wdt] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.color-bullet :refer [color-bullet]] [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.icons :as i] @@ -57,18 +58,22 @@ [{:keys [on-click token theme-token highlighted? on-context-menu] :as props}] (let [{:keys [name value resolved-value errors]} token errors? (and (seq errors) (seq (:errors theme-token)))] - [:button {:class (stl/css-case :token-pill true - :token-pill-highlighted highlighted? - :token-pill-invalid errors?) - :title (cond - errors? (sd/humanize-errors token) - :else (->> [(str "Token: " name) - (str "Original value: " value) - (str "Resolved value: " resolved-value)] - (str/join "\n"))) - :on-click on-click - :on-context-menu on-context-menu - :disabled errors?} + [:button + {:class (stl/css-case :token-pill true + :token-pill-highlighted highlighted? + :token-pill-invalid errors?) + :title (cond + errors? (sd/humanize-errors token) + :else (->> [(str "Token: " name) + (str "Original value: " value) + (str "Resolved value: " resolved-value)] + (str/join "\n"))) + :on-click on-click + :on-context-menu on-context-menu + :disabled errors?} + (when-let [color (wtt/resolved-value-hex token)] + [:& color-bullet {:color color + :mini? true}]) name])) (mf/defc token-section-icon @@ -77,6 +82,7 @@ (case type :border-radius i/corner-radius :numeric [:span {:class (stl/css :section-text-icon)} "123"] + :color i/drop-icon :boolean i/boolean-difference :opacity [:span {:class (stl/css :section-text-icon)} "%"] :rotation i/rotation diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss index 092a8bc862..ed2f1a9b77 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss @@ -72,6 +72,7 @@ .token-pill { @extend .button-secondary; + gap: $s-8; padding: $s-4 $s-8; border-radius: $br-6; font-size: $fs-14; 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 62b068a327..72e1337309 100644 --- a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs @@ -2,13 +2,17 @@ (:require ["@tokens-studio/sd-transforms" :as sd-transforms] ["style-dictionary$default" :as sd] - [app.common.data :refer [ordered-map]] + [app.common.logging :as l] [app.main.refs :as refs] + [app.main.ui.workspace.tokens.errors :as wte] + [app.main.ui.workspace.tokens.tinycolor :as tinycolor] [app.main.ui.workspace.tokens.token :as wtt] [cuerdas.core :as str] [promesa.core :as p] [rumext.v2 :as mf])) +(l/set-level! "app.main.ui.workspace.tokens.style-dictionary" :warn) + (def StyleDictionary "Initiates the global StyleDictionary instance with transforms from tokens-studio used to parse and resolved token values." @@ -24,7 +28,7 @@ (defn tokens->style-dictionary+ "Resolves references and math expressions using StyleDictionary. Returns a promise with the resolved dictionary." - [tokens {:keys [debug?]}] + [tokens] (let [data (cond-> {:tokens tokens :platforms {:json {:transformGroup "tokens-studio" :files [{:format "custom/json" @@ -33,66 +37,68 @@ :warnings "silent" :errors {:brokenReferences "console"}} :preprocessors ["tokens-studio"]} - debug? (update :log merge {:verbosity "verbose" - :warnings "warn"})) + (l/enabled? "app.main.ui.workspace.tokens.style-dictionary" :debug) + (update :log merge {:verbosity "verbose" + :warnings "warn"})) js-data (clj->js data)] - (when debug? - (js/console.log "Input Data" js-data)) + (l/debug :hint "Input Data" :js/data js-data) (sd. js-data))) (defn resolve-sd-tokens+ "Resolves references and math expressions using StyleDictionary. Returns a promise with the resolved dictionary." - [tokens & {:keys [debug?] :as config}] + [tokens] (let [performance-start (js/performance.now) - sd (tokens->style-dictionary+ tokens config)] - (when debug? - (js/console.log "StyleDictionary" sd)) + sd (tokens->style-dictionary+ tokens)] + (l/debug :hint "StyleDictionary" :js/style-dictionary sd) (-> sd (.buildAllPlatforms "json") - (.catch js/console.error) + (.catch #(l/error :hint "Styledictionary build error" :js/error %)) (.then (fn [^js resp] (let [performance-end (js/performance.now) duration-ms (- performance-end performance-start) resolved-tokens (.-allTokens resp)] - (when debug? - (js/console.log "Time elapsed" duration-ms "ms") - (js/console.log "Resolved tokens" resolved-tokens)) + (l/debug :hint (str "Time elapsed" duration-ms "ms") :duration duration-ms) + (l/debug :hint "Resolved tokens" :js/tokens resolved-tokens) resolved-tokens)))))) (defn humanize-errors [{:keys [errors value] :as _token}] (->> (map (fn [err] (case err - :style-dictionary/missing-reference (str "Could not resolve reference token with the name: " value) + :error.style-dictionary/missing-reference (str "Could not resolve reference token with the name: " value) nil)) errors) (str/join "\n"))) -(defn missing-reference-error? - [errors] - (and (set? errors) - (get errors :style-dictionary/missing-reference))) - (defn resolve-tokens+ - [tokens & {:keys [names-map? debug?] :as config}] + [tokens & {:keys [names-map?] :as config}] (p/let [sd-tokens (-> (wtt/token-names-tree tokens) - (resolve-sd-tokens+ config))] + (resolve-sd-tokens+))] (let [resolved-tokens (reduce (fn [acc ^js cur] (let [identifier (if names-map? (.. cur -original -name) (uuid (.-uuid (.-id cur)))) - origin-token (get tokens identifier) - 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))) + {:keys [type] :as origin-token} (get tokens identifier) + value (.-value cur) + token-or-err (case type + :color (if-let [tc (tinycolor/valid-color value)] + {:value value :unit (tinycolor/color-format tc)} + {:errors [(wte/error-with-value :error.token/invalid-color value)]}) + (or (wtt/parse-token-value value) + (if-let [references (-> (wtt/find-token-references value) + (seq))] + {:errors [(wte/error-with-value :error.style-dictionary/missing-reference references)] + :references references} + {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}))) + output-token (if (:errors token-or-err) + (merge origin-token token-or-err) + (assoc origin-token + :resolved-value (:value token-or-err) + :unit (:unit token-or-err)))] + (assoc acc (wtt/token-identifier output-token) output-token))) {} sd-tokens)] - (when debug? - (js/console.log "Resolved tokens" resolved-tokens)) + (l/debug :hint "Resolved tokens" :js/tokens resolved-tokens) resolved-tokens))) ;; Hooks ----------------------------------------------------------------------- diff --git a/frontend/src/app/main/ui/workspace/tokens/tinycolor.cljs b/frontend/src/app/main/ui/workspace/tokens/tinycolor.cljs new file mode 100644 index 0000000000..91fac6bdf1 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/tinycolor.cljs @@ -0,0 +1,27 @@ +(ns app.main.ui.workspace.tokens.tinycolor + "Bindings for tinycolor2 which supports a wide range of css compatible colors. + + This library was chosen as it is already used by StyleDictionary, + so there is no extra dependency cost and there was no clojure alternatives with all the necessary features." + (:require + ["tinycolor2" :as tinycolor])) + +(defn tinycolor? [x] + (and (instance? tinycolor x) (.isValid x))) + +(defn valid-color [color-str] + (let [tc (tinycolor color-str)] + (when (.isValid tc) tc))) + +(defn ->hex [tc] + (assert (tinycolor? tc)) + (.toHex tc)) + +(defn color-format [tc] + (assert (tinycolor? tc)) + (.getFormat tc)) + +(comment + (some-> (valid-color "red") ->hex) + (some-> (valid-color "red") color-format) + nil) diff --git a/frontend/src/app/main/ui/workspace/tokens/token.cljs b/frontend/src/app/main/ui/workspace/tokens/token.cljs index aba9ae9a0a..5f1a957668 100644 --- a/frontend/src/app/main/ui/workspace/tokens/token.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/token.cljs @@ -2,7 +2,8 @@ (:require [app.common.data :as d] [clojure.set :as set] - [cuerdas.core :as str])) + [cuerdas.core :as str] + [app.main.ui.workspace.tokens.tinycolor :as tinycolor])) (defn get-workspace-tokens [state] @@ -38,11 +39,6 @@ (defn token-identifier [{:keys [name] :as _token}] name) -(defn resolve-token-value [{:keys [value resolved-value] :as _token}] - (or - resolved-value - (d/parse-double value))) - (defn attributes-map "Creats an attributes map using collection of `attributes` for `id`." [attributes token] @@ -81,11 +77,6 @@ [token shapes token-attributes] (some #(token-applied? token % token-attributes) shapes)) -(defn shapes-token-applied-all? - "Test if `token` is applied to to any of `shapes` with at least one of the one of the given `token-attributes`." - [token shapes token-attributes] - (some #(token-applied? token % token-attributes) shapes)) - (defn shapes-ids-by-applied-attributes [token shapes token-attributes] (reduce (fn [acc shape] (let [applied-ids-by-attribute (->> (map #(when (token-attribute-applied? token shape %) @@ -99,24 +90,6 @@ (defn shapes-applied-all? [ids-by-attributes shape-ids attributes] (every? #(set/superset? (get ids-by-attributes %) shape-ids) attributes)) -(defn group-shapes-by-all-applied - [token shapes token-attributes] - (reduce - (fn [acc cur-shape] - (let [applied-attrs (token-applied-attributes token cur-shape token-attributes)] - (cond - (empty? applied-attrs) (update acc :none (fnil conj []) cur-shape) - (= applied-attrs token-attributes) (update acc :all (fnil conj []) cur-shape) - :else (reduce (fn [acc' cur'] - (update-in acc' [:some cur'] (fnil conj []) cur-shape)) - acc applied-attrs)))) - {} shapes)) - -(defn group-shapes-by-all-applied-all? [grouped-shapes] - (and (seq (:all grouped-shapes)) - (empty? (:other grouped-shapes)) - (empty? (:some grouped-shapes)))) - (defn token-name->path "Splits token-name into a path vector split by `.` characters. @@ -184,3 +157,12 @@ :else (-> (get path-target selector) (seq) (boolean))))) + +(defn color-token? [token] + (= (:type token) :color)) + +(defn resolved-value-hex [{:keys [resolved-value] :as token}] + (when (and resolved-value (color-token? token)) + (some->> (tinycolor/valid-color resolved-value) + (tinycolor/->hex) + (str "#")))) diff --git a/frontend/src/app/main/ui/workspace/tokens/token_types.cljs b/frontend/src/app/main/ui/workspace/tokens/token_types.cljs index 2faadab692..ce4c5cbf31 100644 --- a/frontend/src/app/main/ui/workspace/tokens/token_types.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/token_types.cljs @@ -20,6 +20,14 @@ :modal {:key :tokens/border-radius :fields [{:label "Border Radius" :key :border-radius}]}} + + :color + {:title "Color" + :attributes ctt/color-keys + :on-update-shape wtch/update-color + :modal {:key :tokens/color + :fields [{:label "Color" :key :color}]}} + :stroke-width {:title "Stroke Width" :attributes ctt/stroke-width-keys diff --git a/frontend/src/app/main/ui/workspace/tokens/update.cljs b/frontend/src/app/main/ui/workspace/tokens/update.cljs index 2f64aa65b1..5f0b581a97 100644 --- a/frontend/src/app/main/ui/workspace/tokens/update.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/update.cljs @@ -19,6 +19,7 @@ (def attributes->shape-update {#{:rx :ry} (fn [v ids _] (wtch/update-shape-radius-all v ids)) #{:r1 :r2 :r3 :r4} wtch/update-shape-radius-single-corner + ctt/color-keys wtch/update-color ctt/stroke-width-keys wtch/update-stroke-width ctt/sizing-keys wtch/update-shape-dimensions ctt/opacity-keys wtch/update-opacity diff --git a/frontend/yarn.lock b/frontend/yarn.lock index ebde9b8cf3..b13ad3a1a8 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6966,6 +6966,7 @@ __metadata: style-dictionary: "patch:style-dictionary@npm%3A4.0.0-prerelease.36#~/.yarn/patches/style-dictionary-npm-4.0.0-prerelease.36-55c0fc33bd.patch" svg-sprite: "npm:^2.0.4" tdigest: "npm:^0.1.2" + tinycolor2: "npm:^1.6.0" typescript: "npm:^5.4.5" ua-parser-js: "npm:^1.0.38" vite: "npm:^5.1.4"