0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-18 18:51:29 -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 (def token-types
#{:boolean #{:boolean
:border-radius :border-radius
:stroke-width
:box-shadow :box-shadow
:color
:dimensions :dimensions
:numeric :numeric
:opacity :opacity
@ -45,6 +45,7 @@
:sizing :sizing
:spacing :spacing
:string :string
:stroke-width
:typography}) :typography})
(defn valid-token-type? (defn valid-token-type?
@ -66,6 +67,12 @@
[:description {:optional true} :string] [:description {:optional true} :string]
[:modified-at {:optional true} ::sm/inst]]) [: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 (sm/register! ::border-radius
[:map [:map
[:rx {:optional true} token-name-ref] [:rx {:optional true} token-name-ref]

View file

@ -116,6 +116,7 @@
"source-map-support": "^0.5.21", "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", "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", "tdigest": "^0.1.2",
"tinycolor2": "npm:^1.6.0",
"ua-parser-js": "^1.0.38", "ua-parser-js": "^1.0.38",
"xregexp": "^5.1.1" "xregexp": "^5.1.1"
} }

View file

@ -52,6 +52,25 @@
;; --- Color Picker Modal ;; --- 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/defc colorpicker
{::mf/props :obj} {::mf/props :obj}
[{:keys [data disable-gradient disable-opacity disable-image on-change on-accept]}] [{:keys [data disable-gradient disable-opacity disable-image on-change on-accept]}]
@ -220,23 +239,7 @@
(st/emit! (dc/update-colorpicker data))) (st/emit! (dc/update-colorpicker data)))
;; Updates the CSS color variable when there is a change in the color ;; Updates the CSS color variable when there is a change in the color
(mf/with-effect [current-color] (use-color-picker-css-variables! node-ref 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))))
;; Updates color when pixel picker is used ;; Updates color when pixel picker is used
(mf/with-effect [picking-color? picked-color picked-color-select] (mf/with-effect [picking-color? picked-color picked-color-select]

View file

@ -8,6 +8,7 @@
(:require (:require
[app.common.types.shape.radius :as ctsr] [app.common.types.shape.radius :as ctsr]
[app.common.types.token :as ctt] [app.common.types.token :as ctt]
[app.main.data.workspace.colors :as wdc]
[app.main.data.workspace :as udw] [app.main.data.workspace :as udw]
[app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.shapes :as dwsh]
@ -18,7 +19,8 @@
[app.main.ui.workspace.tokens.token :as wtt] [app.main.ui.workspace.tokens.token :as wtt]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[clojure.set :as set] [clojure.set :as set]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]))
;; Token Updates --------------------------------------------------------------- ;; Token Updates ---------------------------------------------------------------
@ -123,6 +125,14 @@
{:reg-objects? true {:reg-objects? true
:attrs [:strokes]})) :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] (defn update-shape-dimensions [value shape-ids attributes]
(ptk/reify ::update-shape-dimensions (ptk/reify ::update-shape-dimensions
ptk/WatchEvent ptk/WatchEvent

View file

@ -125,7 +125,7 @@
(mf/defc labeled-input (mf/defc labeled-input
{::mf/wrap-props false} {::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 (let [input-props (cond-> input-props
:always camel-keys :always camel-keys
;; Disable auto-complete on form fields for proprietary password managers ;; 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}] (defn selection-actions [{:keys [type token] :as context-data}]
(let [with-actions (get shape-attribute-actions-map (or type (:type token))) (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 (concat
attribute-actions attribute-actions
[:separator] (when (seq attribute-actions) [:separator])
(default-actions context-data)))) (default-actions context-data))))
;; Components ------------------------------------------------------------------ ;; 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-macros [app.main.style :as stl])
(:require (:require
["lodash.debounce" :as debounce] ["lodash.debounce" :as debounce]
[app.common.colors :as c]
[app.common.data :as d] [app.common.data :as d]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.tokens :as dt] [app.main.data.tokens :as dt]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [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.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.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.token :as wtt]
[app.main.ui.workspace.tokens.update :as wtu] [app.main.ui.workspace.tokens.update :as wtu]
[app.util.dom :as dom] [app.util.dom :as dom]
@ -84,32 +90,39 @@ Token names should only contain letters and digits separated by . characters.")}
;; Component ------------------------------------------------------------------- ;; 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+ (defn validate-token-value+
"Validates token value by resolving the value `input` using `StyleDictionary`. "Validates token value by resolving the value `input` using `StyleDictionary`.
Returns a promise of either resolved tokens or rejects with an error state." Returns a promise of either resolved tokens or rejects with an error state."
[{:keys [input name-value token tokens]}] [{:keys [input name-value token tokens]}]
(let [empty-input? (empty? (str/trim input)) (let [ ;; When creating a new token we dont have a token name yet,
;; Check if the given value contains a reference that is the current token-name ;; so we use a temporary token name that hopefully doesn't clash with any of the users token names
;; When creating a new token we dont have a token name yet, token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value)]
;; 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)]
(cond (cond
empty-input? (p/rejected nil) (empty? (str/trim input))
direct-self-reference? (p/rejected :error/token-direct-self-reference) (p/rejected {:errors [{:error/code :error/empty-input}]})
:else (let [token-id (or (:id token) (random-uuid))
(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 new-tokens (update tokens token-name merge {:id token-id
:value input :value input
:name token-name})] :name token-name
:type (:type token)})]
(-> (sd/resolve-tokens+ new-tokens {:names-map? true}) (-> (sd/resolve-tokens+ new-tokens {:names-map? true})
(p/then (p/then
(fn [resolved-tokens] (fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)] (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)]
(cond (cond
resolved-value (p/resolved resolved-token) resolved-value (p/resolved resolved-token)
(sd/missing-reference-error? errors) (p/rejected :error/token-missing-reference) :else (p/rejected {:errors (or errors (wte/get-error-code :error/unknown-error))}))))))))))
:else (p/rejected :error/unknown-error))))))))))
(defn use-debonced-resolve-callback (defn use-debonced-resolve-callback
"Resolves a token values using `StyleDictionary`. "Resolves a token values using `StyleDictionary`.
@ -120,20 +133,20 @@ Token names should only contain letters and digits separated by . characters.")}
debounced-resolver-callback debounced-resolver-callback
(mf/use-callback (mf/use-callback
(mf/deps token callback tokens) (mf/deps token callback tokens)
(fn [event] (fn [value]
(let [input (dom/get-target-val event) (let [timeout-id (js/Symbol)
timeout-id (js/Symbol)
;; Dont execute callback when the timout-id-ref is outdated because this function got called again ;; 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)] timeout-outdated-cb? #(not= (mf/ref-val timeout-id-ref) timeout-id)]
(mf/set-ref-val! timeout-id-ref timeout-id) (mf/set-ref-val! timeout-id-ref timeout-id)
(js/setTimeout (js/setTimeout
(fn [] (fn []
(when (not (timeout-outdated-cb?)) (when (not (timeout-outdated-cb?))
(-> (validate-token-value+ {:input input (-> (validate-token-value+ {:input value
:name-value @name-ref :name-value @name-ref
:token token :token token
:tokens tokens}) :tokens tokens})
(p/finally (fn [x err] (p/finally
(fn [x err]
(when-not (timeout-outdated-cb?) (when-not (timeout-outdated-cb?)
(callback (or err x)))))))) (callback (or err x))))))))
timeout))))] timeout))))]
@ -141,10 +154,53 @@ Token names should only contain letters and digits separated by . characters.")}
(defonce form-token-cache-atom (atom nil)) (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/defc form
{::mf/wrap-props false} {::mf/wrap-props false}
[{:keys [token token-type] :as _args}] [{:keys [token token-type]}]
(let [tokens (mf/deref refs/workspace-ordered-token-sets-tokens) (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) selected-set-tokens (mf/deref refs/workspace-selected-token-set-tokens)
active-theme-tokens (mf/deref refs/workspace-active-theme-sets-tokens) active-theme-tokens (mf/deref refs/workspace-active-theme-sets-tokens)
resolved-tokens (sd/use-resolved-tokens active-theme-tokens {:names-map? true 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)) (mf/deps (:name token))
#(wtt/token-name->path (:name token))) #(wtt/token-name->path (:name token)))
selected-set-tokens-tree (mf/use-memo selected-set-tokens-tree (mf/use-memo
(mf/deps token-path tokens) (mf/deps token-path selected-set-tokens)
(fn [] (fn []
(-> (wtt/token-names-tree selected-set-tokens) (-> (wtt/token-names-tree selected-set-tokens)
;; Allow setting editing token to it's own path ;; 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] (debounce (fn [e]
(let [value (dom/get-target-val e) (let [value (dom/get-target-val e)
errors (validate-name value)] 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 on-update-name (mf/use-callback
(mf/deps on-update-name-debounced) (mf/deps on-update-name-debounced)
(fn [e] (fn [e]
@ -183,23 +242,33 @@ Token names should only contain letters and digits separated by . characters.")}
(valid-name? @name-ref)) (valid-name? @name-ref))
;; Value ;; 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)) value-ref (mf/use-var (:value token))
token-resolve-result (mf/use-state (get-in resolved-tokens [(wtt/token-identifier 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 set-resolve-value (mf/use-callback
(fn [token-or-err] (fn [token-or-err]
(let [v (cond (let [error? (:errors token-or-err)
(= token-or-err :error/token-direct-self-reference) token-or-err v (if error?
(= token-or-err :error/token-missing-reference) token-or-err token-or-err
(:resolved-value token-or-err) (:resolved-value token-or-err))] (:resolved-value token-or-err))]
(when color? (reset! color (if error? nil v)))
(reset! token-resolve-result 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-debounced (use-debonced-resolve-callback name-ref token active-theme-tokens set-resolve-value)
on-update-value (mf/use-callback on-update-value (mf/use-callback
(mf/deps on-update-value-debounced) (mf/deps on-update-value-debounced)
(fn [e] (fn [e]
(reset! value-ref (dom/get-target-val e)) (let [value (dom/get-target-val e)]
(on-update-value-debounced e))) (reset! value-ref value)
value-error? (when (keyword? @token-resolve-result) (on-update-value-debounced value))))
(= (namespace @token-resolve-result) "error")) 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 valid-value-field? (and
(not value-error?) (not value-error?)
(valid-value? @token-resolve-result)) (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)) (st/emit! (wtu/update-workspace-tokens))
(modal/hide!)))))))))] (modal/hide!)))))))))]
[:form [:form
{:on-submit on-submit} {:class (stl/css :form-wrapper)
:on-submit on-submit}
[:div {:class (stl/css :token-rows)} [:div {:class (stl/css :token-rows)}
[:div [:div
[:& tokens.common/labeled-input {:label "Name" [:& 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" [:& tokens.common/labeled-input {:label "Value"
:input-props {:default-value @value-ref :input-props {:default-value @value-ref
:on-blur on-update-value :on-blur on-update-value
:on-change on-update-value}}] :on-change on-update-value
[:div {:class (stl/css-case :resolved-value true :ref value-input-ref}
:resolved-value-placeholder (nil? @token-resolve-result) :render-right (when color?
:resolved-value-error value-error?)} (mf/fnc []
(case @token-resolve-result [:div {:class (stl/css :color-bullet)
:error/token-direct-self-reference "Token has self reference" :on-click #(swap! color-ramp-open? not)}
:error/token-missing-reference "Token has missing reference" (if-let [hex (some-> @color tinycolor/valid-color tinycolor/->hex)]
:error/unknown-error "" [:& color-bullet {:color hex
nil "Enter token value" :mini? true}]
[:p @token-resolve-result])] [: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 [:div
[:& tokens.common/labeled-input {:label "Description" [:& tokens.common/labeled-input {:label "Description"
:input-props {:default-value @description-ref :input-props {:default-value @description-ref

View file

@ -7,6 +7,10 @@
@import "refactor/common-refactor.scss"; @import "refactor/common-refactor.scss";
@import "./common.scss"; @import "./common.scss";
.form-wrapper {
width: $s-260;
}
.button-row { .button-row {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -30,7 +34,7 @@
@include bodySmallTypography; @include bodySmallTypography;
padding: $s-4 $s-6; padding: $s-4 $s-6;
font-weight: medium; font-weight: medium;
height: $s-24; min-height: 1lh;
color: var(--color-foreground-primary); color: var(--color-foreground-primary);
border: 1px solid color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent); border: 1px solid color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent);
@ -48,3 +52,19 @@
.resolved-value-error { .resolved-value-error {
color: var(--status-color-error-500); 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] [properties]
[:& token-update-create-modal 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/defc stroke-width-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :tokens/stroke-width} ::mf/register-as :tokens/stroke-width}

View file

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

View file

@ -13,6 +13,7 @@
[app.main.data.tokens :as wdt] [app.main.data.tokens :as wdt]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [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.components.title-bar :refer [title-bar]]
[app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.hooks.resize :refer [use-resize-hook]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
@ -57,7 +58,8 @@
[{:keys [on-click token theme-token highlighted? on-context-menu] :as props}] [{:keys [on-click token theme-token highlighted? on-context-menu] :as props}]
(let [{:keys [name value resolved-value errors]} token (let [{:keys [name value resolved-value errors]} token
errors? (and (seq errors) (seq (:errors theme-token)))] errors? (and (seq errors) (seq (:errors theme-token)))]
[:button {:class (stl/css-case :token-pill true [:button
{:class (stl/css-case :token-pill true
:token-pill-highlighted highlighted? :token-pill-highlighted highlighted?
:token-pill-invalid errors?) :token-pill-invalid errors?)
:title (cond :title (cond
@ -69,6 +71,9 @@
:on-click on-click :on-click on-click
:on-context-menu on-context-menu :on-context-menu on-context-menu
:disabled errors?} :disabled errors?}
(when-let [color (wtt/resolved-value-hex token)]
[:& color-bullet {:color color
:mini? true}])
name])) name]))
(mf/defc token-section-icon (mf/defc token-section-icon
@ -77,6 +82,7 @@
(case type (case type
:border-radius i/corner-radius :border-radius i/corner-radius
:numeric [:span {:class (stl/css :section-text-icon)} "123"] :numeric [:span {:class (stl/css :section-text-icon)} "123"]
:color i/drop-icon
:boolean i/boolean-difference :boolean i/boolean-difference
:opacity [:span {:class (stl/css :section-text-icon)} "%"] :opacity [:span {:class (stl/css :section-text-icon)} "%"]
:rotation i/rotation :rotation i/rotation

View file

@ -72,6 +72,7 @@
.token-pill { .token-pill {
@extend .button-secondary; @extend .button-secondary;
gap: $s-8;
padding: $s-4 $s-8; padding: $s-4 $s-8;
border-radius: $br-6; border-radius: $br-6;
font-size: $fs-14; font-size: $fs-14;

View file

@ -2,13 +2,17 @@
(:require (:require
["@tokens-studio/sd-transforms" :as sd-transforms] ["@tokens-studio/sd-transforms" :as sd-transforms]
["style-dictionary$default" :as sd] ["style-dictionary$default" :as sd]
[app.common.data :refer [ordered-map]] [app.common.logging :as l]
[app.main.refs :as refs] [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] [app.main.ui.workspace.tokens.token :as wtt]
[cuerdas.core :as str] [cuerdas.core :as str]
[promesa.core :as p] [promesa.core :as p]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(l/set-level! "app.main.ui.workspace.tokens.style-dictionary" :warn)
(def StyleDictionary (def StyleDictionary
"Initiates the global StyleDictionary instance with transforms "Initiates the global StyleDictionary instance with transforms
from tokens-studio used to parse and resolved token values." from tokens-studio used to parse and resolved token values."
@ -24,7 +28,7 @@
(defn tokens->style-dictionary+ (defn tokens->style-dictionary+
"Resolves references and math expressions using StyleDictionary. "Resolves references and math expressions using StyleDictionary.
Returns a promise with the resolved dictionary." Returns a promise with the resolved dictionary."
[tokens {:keys [debug?]}] [tokens]
(let [data (cond-> {:tokens tokens (let [data (cond-> {:tokens tokens
:platforms {:json {:transformGroup "tokens-studio" :platforms {:json {:transformGroup "tokens-studio"
:files [{:format "custom/json" :files [{:format "custom/json"
@ -33,66 +37,68 @@
:warnings "silent" :warnings "silent"
:errors {:brokenReferences "console"}} :errors {:brokenReferences "console"}}
:preprocessors ["tokens-studio"]} :preprocessors ["tokens-studio"]}
debug? (update :log merge {:verbosity "verbose" (l/enabled? "app.main.ui.workspace.tokens.style-dictionary" :debug)
(update :log merge {:verbosity "verbose"
:warnings "warn"})) :warnings "warn"}))
js-data (clj->js data)] js-data (clj->js data)]
(when debug? (l/debug :hint "Input Data" :js/data js-data)
(js/console.log "Input Data" js-data))
(sd. js-data))) (sd. js-data)))
(defn resolve-sd-tokens+ (defn resolve-sd-tokens+
"Resolves references and math expressions using StyleDictionary. "Resolves references and math expressions using StyleDictionary.
Returns a promise with the resolved dictionary." Returns a promise with the resolved dictionary."
[tokens & {:keys [debug?] :as config}] [tokens]
(let [performance-start (js/performance.now) (let [performance-start (js/performance.now)
sd (tokens->style-dictionary+ tokens config)] sd (tokens->style-dictionary+ tokens)]
(when debug? (l/debug :hint "StyleDictionary" :js/style-dictionary sd)
(js/console.log "StyleDictionary" sd))
(-> sd (-> sd
(.buildAllPlatforms "json") (.buildAllPlatforms "json")
(.catch js/console.error) (.catch #(l/error :hint "Styledictionary build error" :js/error %))
(.then (fn [^js resp] (.then (fn [^js resp]
(let [performance-end (js/performance.now) (let [performance-end (js/performance.now)
duration-ms (- performance-end performance-start) duration-ms (- performance-end performance-start)
resolved-tokens (.-allTokens resp)] resolved-tokens (.-allTokens resp)]
(when debug? (l/debug :hint (str "Time elapsed" duration-ms "ms") :duration duration-ms)
(js/console.log "Time elapsed" duration-ms "ms") (l/debug :hint "Resolved tokens" :js/tokens resolved-tokens)
(js/console.log "Resolved tokens" resolved-tokens))
resolved-tokens)))))) resolved-tokens))))))
(defn humanize-errors [{:keys [errors value] :as _token}] (defn humanize-errors [{:keys [errors value] :as _token}]
(->> (map (fn [err] (->> (map (fn [err]
(case 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)) nil))
errors) errors)
(str/join "\n"))) (str/join "\n")))
(defn missing-reference-error?
[errors]
(and (set? errors)
(get errors :style-dictionary/missing-reference)))
(defn resolve-tokens+ (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) (p/let [sd-tokens (-> (wtt/token-names-tree tokens)
(resolve-sd-tokens+ config))] (resolve-sd-tokens+))]
(let [resolved-tokens (reduce (let [resolved-tokens (reduce
(fn [acc ^js cur] (fn [acc ^js cur]
(let [identifier (if names-map? (let [identifier (if names-map?
(.. cur -original -name) (.. cur -original -name)
(uuid (.-uuid (.-id cur)))) (uuid (.-uuid (.-id cur))))
origin-token (get tokens identifier) {:keys [type] :as origin-token} (get tokens identifier)
parsed-value (wtt/parse-token-value (.-value cur)) value (.-value cur)
resolved-token (if (not parsed-value) token-or-err (case type
(assoc origin-token :errors [:style-dictionary/missing-reference]) :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 (assoc origin-token
:resolved-value (:value parsed-value) :resolved-value (:value token-or-err)
:resolved-unit (:unit parsed-value)))] :unit (:unit token-or-err)))]
(assoc acc (wtt/token-identifier resolved-token) resolved-token))) (assoc acc (wtt/token-identifier output-token) output-token)))
{} sd-tokens)] {} sd-tokens)]
(when debug? (l/debug :hint "Resolved tokens" :js/tokens resolved-tokens)
(js/console.log "Resolved tokens" resolved-tokens))
resolved-tokens))) resolved-tokens)))
;; Hooks ----------------------------------------------------------------------- ;; 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 (:require
[app.common.data :as d] [app.common.data :as d]
[clojure.set :as set] [clojure.set :as set]
[cuerdas.core :as str])) [cuerdas.core :as str]
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]))
(defn get-workspace-tokens (defn get-workspace-tokens
[state] [state]
@ -38,11 +39,6 @@
(defn token-identifier [{:keys [name] :as _token}] (defn token-identifier [{:keys [name] :as _token}]
name) name)
(defn resolve-token-value [{:keys [value resolved-value] :as _token}]
(or
resolved-value
(d/parse-double value)))
(defn attributes-map (defn attributes-map
"Creats an attributes map using collection of `attributes` for `id`." "Creats an attributes map using collection of `attributes` for `id`."
[attributes token] [attributes token]
@ -81,11 +77,6 @@
[token shapes token-attributes] [token shapes token-attributes]
(some #(token-applied? token % token-attributes) shapes)) (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] (defn shapes-ids-by-applied-attributes [token shapes token-attributes]
(reduce (fn [acc shape] (reduce (fn [acc shape]
(let [applied-ids-by-attribute (->> (map #(when (token-attribute-applied? token 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] (defn shapes-applied-all? [ids-by-attributes shape-ids attributes]
(every? #(set/superset? (get 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 (defn token-name->path
"Splits token-name into a path vector split by `.` characters. "Splits token-name into a path vector split by `.` characters.
@ -184,3 +157,12 @@
:else (-> (get path-target selector) :else (-> (get path-target selector)
(seq) (seq)
(boolean))))) (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 :modal {:key :tokens/border-radius
:fields [{:label "Border Radius" :fields [{:label "Border Radius"
:key :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 :stroke-width
{:title "Stroke Width" {:title "Stroke Width"
:attributes ctt/stroke-width-keys :attributes ctt/stroke-width-keys

View file

@ -19,6 +19,7 @@
(def attributes->shape-update (def attributes->shape-update
{#{:rx :ry} (fn [v ids _] (wtch/update-shape-radius-all v ids)) {#{:rx :ry} (fn [v ids _] (wtch/update-shape-radius-all v ids))
#{:r1 :r2 :r3 :r4} wtch/update-shape-radius-single-corner #{: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/stroke-width-keys wtch/update-stroke-width
ctt/sizing-keys wtch/update-shape-dimensions ctt/sizing-keys wtch/update-shape-dimensions
ctt/opacity-keys wtch/update-opacity 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" 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" svg-sprite: "npm:^2.0.4"
tdigest: "npm:^0.1.2" tdigest: "npm:^0.1.2"
tinycolor2: "npm:^1.6.0"
typescript: "npm:^5.4.5" typescript: "npm:^5.4.5"
ua-parser-js: "npm:^1.0.38" ua-parser-js: "npm:^1.0.38"
vite: "npm:^5.1.4" vite: "npm:^5.1.4"