mirror of
https://github.com/penpot/penpot.git
synced 2025-06-21 20:47:10 +02:00
Merge pull request #168 from tokens-studio/style-dictionary
References & Expressions in Tokens
This commit is contained in:
commit
83515250da
12 changed files with 1341 additions and 42 deletions
|
@ -26,6 +26,7 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.tokens.core :as wtc]
|
||||
[app.main.ui.workspace.tokens.editable-select :refer [editable-select]]
|
||||
[app.main.ui.workspace.tokens.style-dictionary :as sd]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[clojure.set :refer [rename-keys union]]
|
||||
|
@ -98,7 +99,8 @@
|
|||
selection-parents-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids))
|
||||
selection-parents (mf/deref selection-parents-ref)
|
||||
|
||||
tokens (mf/deref refs/workspace-tokens)
|
||||
tokens (-> (mf/deref refs/workspace-tokens)
|
||||
(sd/use-resolved-tokens))
|
||||
tokens-by-type (mf/use-memo (mf/deps tokens) #(wtc/group-tokens-by-type tokens))
|
||||
|
||||
border-radius-tokens (:border-radius tokens-by-type)
|
||||
|
|
|
@ -36,12 +36,12 @@
|
|||
{::mf/wrap-props false}
|
||||
[{:keys [label input-props auto-complete?]}]
|
||||
(let [input-props (cond-> input-props
|
||||
:always camel-keys
|
||||
;; Disable auto-complete on form fields for proprietary password managers
|
||||
;; https://github.com/orgs/tokens-studio/projects/69/views/11?pane=issue&itemId=63724204
|
||||
(not auto-complete?) (assoc "data-1p-ignore" true
|
||||
"data-lpignore" true
|
||||
:auto-complete "off")
|
||||
:always camel-keys)]
|
||||
:auto-complete "off"))]
|
||||
[:label {:class (stl/css :labeled-input)}
|
||||
[:span {:class (stl/css :label)} label]
|
||||
[:& :input input-props]]))
|
||||
|
|
|
@ -14,7 +14,9 @@
|
|||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.shape-layout :as dwsl]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.store :as st]))
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.workspace.tokens.style-dictionary :as sd]
|
||||
[promesa.core :as p]))
|
||||
|
||||
;; Helpers ---------------------------------------------------------------------
|
||||
|
||||
|
@ -32,10 +34,10 @@
|
|||
[token shapes token-attributes]
|
||||
(some #(token-applied? token % token-attributes) shapes))
|
||||
|
||||
(defn resolve-token-value [{:keys [value] :as token}]
|
||||
(if-let [int-or-double (d/parse-double value)]
|
||||
int-or-double
|
||||
(throw (ex-info (str "Implement token value resolve for " value) token))))
|
||||
(defn resolve-token-value [{:keys [value resolved-value] :as token}]
|
||||
(or
|
||||
resolved-value
|
||||
(d/parse-double value)))
|
||||
|
||||
(defn maybe-resolve-token-value [{:keys [value] :as token}]
|
||||
(when value (resolve-token-value token)))
|
||||
|
@ -77,13 +79,15 @@
|
|||
shape-ids (->> selected-shapes
|
||||
(eduction
|
||||
(remove #(tokens-applied? token % attributes))
|
||||
(map :id)))
|
||||
token-value (resolve-token-value token)]
|
||||
(doseq [shape selected-shapes]
|
||||
(st/emit! (on-apply {:token-id (:id token)
|
||||
:shape-id (:id shape)
|
||||
:attributes attributes}))
|
||||
(on-update-shape token-value shape-ids attributes))))
|
||||
(map :id)))]
|
||||
(p/let [sd-tokens (sd/resolve-workspace-tokens+ {:debug? true})]
|
||||
(let [resolved-token (get sd-tokens (:id token))
|
||||
resolved-token-value (resolve-token-value resolved-token)]
|
||||
(doseq [shape selected-shapes]
|
||||
(st/emit! (on-apply {:token-id (:id token)
|
||||
:shape-id (:id shape)
|
||||
:attributes attributes}))
|
||||
(on-update-shape resolved-token-value shape-ids attributes))))))
|
||||
|
||||
(defn update-shape-radius [value shape-ids]
|
||||
(st/emit!
|
||||
|
|
|
@ -0,0 +1,580 @@
|
|||
{
|
||||
"core": {
|
||||
"dimension": {
|
||||
"scale": {
|
||||
"value": "2",
|
||||
"type": "dimension"
|
||||
},
|
||||
"xs": {
|
||||
"value": "4",
|
||||
"type": "dimension"
|
||||
},
|
||||
"sm": {
|
||||
"value": "{dimension.xs} * {dimension.scale}",
|
||||
"type": "dimension"
|
||||
},
|
||||
"md": {
|
||||
"value": "{dimension.sm} * {dimension.scale}",
|
||||
"type": "dimension"
|
||||
},
|
||||
"lg": {
|
||||
"value": "{dimension.md} * {dimension.scale}",
|
||||
"type": "dimension"
|
||||
},
|
||||
"xl": {
|
||||
"value": "{dimension.lg} * {dimension.scale}",
|
||||
"type": "dimension"
|
||||
}
|
||||
},
|
||||
"spacing": {
|
||||
"xs": {
|
||||
"value": "{dimension.xs}",
|
||||
"type": "spacing"
|
||||
},
|
||||
"sm": {
|
||||
"value": "{dimension.sm}",
|
||||
"type": "spacing"
|
||||
},
|
||||
"md": {
|
||||
"value": "{dimension.md}",
|
||||
"type": "spacing"
|
||||
},
|
||||
"lg": {
|
||||
"value": "{dimension.lg}",
|
||||
"type": "spacing"
|
||||
},
|
||||
"xl": {
|
||||
"value": "{dimension.xl}",
|
||||
"type": "spacing"
|
||||
},
|
||||
"multi-value": {
|
||||
"value": "{dimension.sm} {dimension.xl}",
|
||||
"type": "spacing",
|
||||
"description": "You can have multiple values in a single spacing token. Read more on these: https://docs.tokens.studio/available-tokens/spacing-tokens#multi-value-spacing-tokens"
|
||||
}
|
||||
},
|
||||
"borderRadius": {
|
||||
"sm": {
|
||||
"value": "4",
|
||||
"type": "borderRadius"
|
||||
},
|
||||
"lg": {
|
||||
"value": "8",
|
||||
"type": "borderRadius"
|
||||
},
|
||||
"xl": {
|
||||
"value": "16",
|
||||
"type": "borderRadius"
|
||||
},
|
||||
"multi-value": {
|
||||
"value": "{borderRadius.sm} {borderRadius.lg}",
|
||||
"type": "borderRadius",
|
||||
"description": "You can have multiple values in a single radius token. Read more on these: https://docs.tokens.studio/available-tokens/border-radius-tokens#single--multiple-values"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"black": {
|
||||
"value": "#000000",
|
||||
"type": "color"
|
||||
},
|
||||
"white": {
|
||||
"value": "#ffffff",
|
||||
"type": "color"
|
||||
},
|
||||
"gray": {
|
||||
"100": {
|
||||
"value": "#f7fafc",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#edf2f7",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#e2e8f0",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#cbd5e0",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#a0aec0",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#718096",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#4a5568",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#2d3748",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#1a202c",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"red": {
|
||||
"100": {
|
||||
"value": "#fff5f5",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#fed7d7",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#feb2b2",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#fc8181",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#f56565",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#e53e3e",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#c53030",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#9b2c2c",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#742a2a",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"orange": {
|
||||
"100": {
|
||||
"value": "#fffaf0",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#feebc8",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#fbd38d",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#f6ad55",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#ed8936",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#dd6b20",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#c05621",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#9c4221",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#7b341e",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"yellow": {
|
||||
"100": {
|
||||
"value": "#fffff0",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#fefcbf",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#faf089",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#f6e05e",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#ecc94b",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#d69e2e",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#b7791f",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#975a16",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#744210",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"green": {
|
||||
"100": {
|
||||
"value": "#f0fff4",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#c6f6d5",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#9ae6b4",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#68d391",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#48bb78",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#38a169",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#2f855a",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#276749",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#22543d",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"teal": {
|
||||
"100": {
|
||||
"value": "#e6fffa",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#b2f5ea",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#81e6d9",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#4fd1c5",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#38b2ac",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#319795",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#2c7a7b",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#285e61",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#234e52",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"blue": {
|
||||
"100": {
|
||||
"value": "#ebf8ff",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#bee3f8",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#90cdf4",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#63b3ed",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#4299e1",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#3182ce",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#2b6cb0",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#2c5282",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#2a4365",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"indigo": {
|
||||
"100": {
|
||||
"value": "#ebf4ff",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#c3dafe",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#a3bffa",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#7f9cf5",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#667eea",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#5a67d8",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#4c51bf",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#434190",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#3c366b",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"purple": {
|
||||
"100": {
|
||||
"value": "#faf5ff",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#e9d8fd",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#d6bcfa",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#b794f4",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#9f7aea",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#805ad5",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#6b46c1",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#553c9a",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#44337a",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"pink": {
|
||||
"100": {
|
||||
"value": "#fff5f7",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#fed7e2",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#fbb6ce",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#f687b3",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#ed64a6",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#d53f8c",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#b83280",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#97266d",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#702459",
|
||||
"type": "color"
|
||||
}
|
||||
}
|
||||
},
|
||||
"opacity": {
|
||||
"low": {
|
||||
"value": "10%",
|
||||
"type": "opacity"
|
||||
},
|
||||
"md": {
|
||||
"value": "50%",
|
||||
"type": "opacity"
|
||||
},
|
||||
"high": {
|
||||
"value": "90%",
|
||||
"type": "opacity"
|
||||
}
|
||||
},
|
||||
"fontFamilies": {
|
||||
"heading": {
|
||||
"value": "Inter",
|
||||
"type": "fontFamilies"
|
||||
},
|
||||
"body": {
|
||||
"value": "Roboto",
|
||||
"type": "fontFamilies"
|
||||
}
|
||||
},
|
||||
"lineHeights": {
|
||||
"heading": {
|
||||
"value": "110%",
|
||||
"type": "lineHeights"
|
||||
},
|
||||
"body": {
|
||||
"value": "140%",
|
||||
"type": "lineHeights"
|
||||
}
|
||||
},
|
||||
"letterSpacing": {
|
||||
"default": {
|
||||
"value": "0",
|
||||
"type": "letterSpacing"
|
||||
},
|
||||
"increased": {
|
||||
"value": "150%",
|
||||
"type": "letterSpacing"
|
||||
},
|
||||
"decreased": {
|
||||
"value": "-5%",
|
||||
"type": "letterSpacing"
|
||||
}
|
||||
},
|
||||
"paragraphSpacing": {
|
||||
"h1": {
|
||||
"value": "32",
|
||||
"type": "paragraphSpacing"
|
||||
},
|
||||
"h2": {
|
||||
"value": "26",
|
||||
"type": "paragraphSpacing"
|
||||
}
|
||||
},
|
||||
"fontWeights": {
|
||||
"headingRegular": {
|
||||
"value": "Regular",
|
||||
"type": "fontWeights"
|
||||
},
|
||||
"headingBold": {
|
||||
"value": "Bold",
|
||||
"type": "fontWeights"
|
||||
},
|
||||
"bodyRegular": {
|
||||
"value": "Regular",
|
||||
"type": "fontWeights"
|
||||
},
|
||||
"bodyBold": {
|
||||
"value": "Bold",
|
||||
"type": "fontWeights"
|
||||
}
|
||||
},
|
||||
"fontSizes": {
|
||||
"h1": {
|
||||
"value": "{fontSizes.h2} * 1.25",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"h2": {
|
||||
"value": "{fontSizes.h3} * 1.25",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"h3": {
|
||||
"value": "{fontSizes.h4} * 1.25",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"h4": {
|
||||
"value": "{fontSizes.h5} * 1.25",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"h5": {
|
||||
"value": "{fontSizes.h6} * 1.25",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"h6": {
|
||||
"value": "{fontSizes.body} * 1",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"body": {
|
||||
"value": "16",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"sm": {
|
||||
"value": "{fontSizes.body} * 0.85",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"xs": {
|
||||
"value": "{fontSizes.body} * 0.65",
|
||||
"type": "fontSizes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.tokens.core :as wtc]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.keyboard :as kbd]
|
||||
|
@ -92,7 +93,9 @@
|
|||
(cond
|
||||
(= :separator item) [:li {:class (stl/css :separator)
|
||||
:key (dm/str element-id "-" index)}]
|
||||
:else (let [{:keys [value label selected?]} item
|
||||
;; Remove items with missing references
|
||||
(seq (:errors item)) nil
|
||||
:else (let [{:keys [label selected? errors]} item
|
||||
highlighted? (= highlighted index)]
|
||||
[:li
|
||||
{:key (str element-id "-" index)
|
||||
|
@ -100,9 +103,10 @@
|
|||
:is-selected selected?
|
||||
:is-highlighted highlighted?)
|
||||
:data-label label
|
||||
:disabled (seq errors)
|
||||
:on-click #(on-select item)}
|
||||
[:span {:class (stl/css :label)} label]
|
||||
[:span {:class (stl/css :value)} value]
|
||||
[:span {:class (stl/css :value)} (wtc/resolve-token-value item)]
|
||||
[:span {:class (stl/css :check-icon)} i/tick]])))]]]))
|
||||
|
||||
(mf/defc editable-select
|
||||
|
@ -255,7 +259,7 @@
|
|||
(when-let [{:keys [label value]} token]
|
||||
[:div {:title (str label ": " value)
|
||||
:class (stl/css :token-pill)}
|
||||
value])
|
||||
(wtc/resolve-token-value token)])
|
||||
(cond
|
||||
token [:& :input (merge input-props
|
||||
{:value (or (:token-value state) "")
|
||||
|
|
|
@ -61,9 +61,8 @@
|
|||
state (mf/use-state initial-fields)
|
||||
|
||||
on-update-state-field (fn [idx e]
|
||||
(->> (dom/get-target-val e)
|
||||
(assoc-in @state [idx :value])
|
||||
(reset! state)))
|
||||
(let [value (dom/get-target-val e)]
|
||||
(swap! state assoc-in [idx :value] value)))
|
||||
|
||||
on-submit (fn [e]
|
||||
(dom/prevent-default e)
|
||||
|
|
|
@ -15,9 +15,12 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.sidebar.assets.common :as cmm]
|
||||
[app.main.ui.workspace.tokens.core :as wtc]
|
||||
[app.main.ui.workspace.tokens.style-dictionary :as sd]
|
||||
[app.util.dom :as dom]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
[rumext.v2 :as mf]
|
||||
[shadow.resource]))
|
||||
|
||||
(def lens:token-type-open-status
|
||||
(l/derived (l/in [:workspace-tokens :open-status]) st/state))
|
||||
|
@ -25,16 +28,20 @@
|
|||
(mf/defc token-pill
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [on-click token highlighted? on-context-menu]}]
|
||||
(let [{:keys [name value]} token
|
||||
resolved-value (try
|
||||
(wtc/resolve-token-value token)
|
||||
(catch js/Error _ nil))]
|
||||
[:div {:class (stl/css-case :token-pill true
|
||||
:token-pill-highlighted highlighted?
|
||||
:token-pill-invalid (not resolved-value))
|
||||
:title (str (if resolved-value "Token value: " "Invalid token value: ") value)
|
||||
:on-click on-click
|
||||
:on-context-menu on-context-menu}
|
||||
(let [{:keys [name value resolved-value errors]} token
|
||||
errors? (seq 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?}
|
||||
name]))
|
||||
|
||||
(mf/defc token-section-icon
|
||||
|
@ -137,7 +144,8 @@
|
|||
selected (mf/deref refs/selected-shapes)
|
||||
selected-shapes (into [] (keep (d/getf objects)) selected)
|
||||
|
||||
tokens (mf/deref refs/workspace-tokens)
|
||||
tokens (-> (mf/deref refs/workspace-tokens)
|
||||
(sd/use-resolved-tokens))
|
||||
token-groups (mf/with-memo [tokens]
|
||||
(sorted-token-groups tokens))]
|
||||
[:article
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
}
|
||||
|
||||
&.token-pill-invalid {
|
||||
background-color: var(--button-secondary-background-color-rest);
|
||||
color: var(--status-color-error-500);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
|
165
frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs
Normal file
165
frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs
Normal file
|
@ -0,0 +1,165 @@
|
|||
(ns app.main.ui.workspace.tokens.style-dictionary
|
||||
(:require
|
||||
["@tokens-studio/sd-transforms" :as sd-transforms]
|
||||
["style-dictionary$default" :as sd]
|
||||
[app.common.data :as d]
|
||||
[app.main.refs :as refs]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]
|
||||
[shadow.resource]))
|
||||
|
||||
(def StyleDictionary
|
||||
"The global StyleDictionary instance used as an external library for now,
|
||||
as the package would need webpack to be bundled,
|
||||
because shadow-cljs doesn't support some of the more modern bundler features."
|
||||
(do
|
||||
(sd-transforms/registerTransforms sd)
|
||||
(.registerFormat sd #js {:name "custom/json"
|
||||
:format (fn [res]
|
||||
(.-tokens (.-dictionary res)))})
|
||||
sd))
|
||||
|
||||
;; Functions -------------------------------------------------------------------
|
||||
|
||||
(defn tokens->style-dictionary+
|
||||
"Resolves references and math expressions using StyleDictionary.
|
||||
Returns a promise with the resolved dictionary."
|
||||
[tokens {:keys [debug?]}]
|
||||
(let [data (cond-> {:tokens tokens
|
||||
:platforms {:json {:transformGroup "tokens-studio"
|
||||
:files [{:format "custom/json"
|
||||
:destination "fake-filename"}]}}
|
||||
:log {:verbosity "silent"
|
||||
:warnings "silent"
|
||||
:errors {:brokenReferences "console"}}
|
||||
:preprocessors ["tokens-studio"]}
|
||||
debug? (update :log merge {:verbosity "verbose"
|
||||
:warnings "warn"}))
|
||||
js-data (clj->js data)]
|
||||
(when debug?
|
||||
(js/console.log "Input 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}]
|
||||
(let [performance-start (js/window.performance.now)
|
||||
sd (tokens->style-dictionary+ tokens config)]
|
||||
(when debug?
|
||||
(js/console.log "StyleDictionary" sd))
|
||||
(-> sd
|
||||
(.buildAllPlatforms "json")
|
||||
(.catch js/console.error)
|
||||
(.then (fn [^js resp]
|
||||
(let [performance-end (js/window.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))
|
||||
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)
|
||||
nil))
|
||||
errors)
|
||||
(str/join "\n")))
|
||||
|
||||
(defn tokens-name-map [tokens]
|
||||
(->> tokens
|
||||
(map (fn [[_ x]] [(:name x) x]))
|
||||
(into {})))
|
||||
|
||||
(defn resolve-tokens+
|
||||
[tokens & {:keys [debug?] :as config}]
|
||||
(p/let [sd-tokens (-> (tokens-name-map tokens)
|
||||
(clj->js)
|
||||
(resolve-sd-tokens+ config))]
|
||||
(let [resolved-tokens (reduce
|
||||
(fn [acc ^js cur]
|
||||
(let [value (.-value cur)
|
||||
resolved-value (d/parse-integer (.-value cur))
|
||||
original-value (-> cur .-original .-value)
|
||||
id (uuid (.-uuid (.-id cur)))
|
||||
missing-reference? (and (not resolved-value)
|
||||
(re-find #"\{" value)
|
||||
(= value original-value))]
|
||||
(cond-> (assoc-in acc [id :resolved-value] resolved-value)
|
||||
missing-reference? (update-in [id :errors] (fnil conj #{}) :style-dictionary/missing-reference))))
|
||||
tokens sd-tokens)]
|
||||
(when debug?
|
||||
(js/console.log "Resolved tokens" resolved-tokens))
|
||||
resolved-tokens)))
|
||||
|
||||
(defn resolve-workspace-tokens+
|
||||
[& {:keys [debug?] :as config}]
|
||||
(when-let [workspace-tokens @refs/workspace-tokens]
|
||||
(resolve-tokens+ workspace-tokens)))
|
||||
|
||||
;; Hooks -----------------------------------------------------------------------
|
||||
|
||||
(defonce !tokens-cache (atom nil))
|
||||
|
||||
(defn use-resolved-tokens
|
||||
"The StyleDictionary process function is async, so we can't use resolved values directly.
|
||||
|
||||
This hook will return the unresolved tokens as state until they are processed,
|
||||
then the state will be updated with the resolved tokens."
|
||||
[tokens & {:keys [cache-atom]
|
||||
:or {cache-atom !tokens-cache}}]
|
||||
(let [tokens-state (mf/use-state (get @cache-atom tokens tokens))]
|
||||
(mf/use-effect
|
||||
(mf/deps tokens)
|
||||
(fn []
|
||||
(let [cached (get @cache-atom tokens)]
|
||||
(cond
|
||||
;; The tokens are already processing somewhere
|
||||
(p/promise? cached) (p/then cached #(reset! tokens-state %))
|
||||
;; Get the cached entry
|
||||
(some? cached) (reset! tokens-state cached)
|
||||
;; No cached entry, start processing
|
||||
:else (let [promise+ (resolve-tokens+ tokens)]
|
||||
(swap! cache-atom assoc tokens promise+)
|
||||
(p/then promise+ (fn [resolved-tokens]
|
||||
(swap! cache-atom assoc tokens resolved-tokens)
|
||||
(reset! tokens-state resolved-tokens))))))))
|
||||
@tokens-state))
|
||||
|
||||
(defn use-resolved-workspace-tokens
|
||||
([] (use-resolved-tokens nil))
|
||||
([options]
|
||||
(-> (mf/deref refs/workspace-tokens)
|
||||
(use-resolved-tokens options))))
|
||||
|
||||
;; Testing ---------------------------------------------------------------------
|
||||
|
||||
(defn tokens-studio-example []
|
||||
(-> (shadow.resource/inline "./data/example-tokens-set.json")
|
||||
(js/JSON.parse)
|
||||
.-core))
|
||||
|
||||
(comment
|
||||
|
||||
(defonce !output (atom nil))
|
||||
|
||||
@!output
|
||||
|
||||
(-> (resolve-workspace-tokens+ {:debug? true})
|
||||
(p/then #(reset! !output %)))
|
||||
|
||||
(->> @refs/workspace-tokens
|
||||
(resolve-tokens+))
|
||||
|
||||
(->
|
||||
(clj->js {"a" {:name "a" :value "5"}
|
||||
"b" {:name "b" :value "{a} * 2"}})
|
||||
(#(resolve-sd-tokens+ % {:debug? true})))
|
||||
|
||||
(-> (tokens-studio-example)
|
||||
(resolve-sd-tokens+ {:debug? true}))
|
||||
|
||||
nil)
|
Loading…
Add table
Add a link
Reference in a new issue