0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-21 06:02:32 -05:00

Merge pull request #273 from tokens-studio/color-token

Color token
This commit is contained in:
Florian Schrödl 2024-09-18 18:10:42 +02:00 committed by GitHub
commit fb38e4378a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 347 additions and 147 deletions

View file

@ -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]

View file

@ -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"
}

View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -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 ------------------------------------------------------------------

View file

@ -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)))))

View file

@ -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

View file

@ -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;
}

View file

@ -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}

View file

@ -8,6 +8,9 @@
.shadow {
@extend .modal-container-base;
width: auto;
max-width: auto;
min-width: auto;
@include menuShadow;
position: absolute;
z-index: 11;

View file

@ -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

View file

@ -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;

View file

@ -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 -----------------------------------------------------------------------

View file

@ -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)

View file

@ -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 "#"))))

View file

@ -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

View file

@ -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

View file

@ -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"