mirror of
https://github.com/penpot/penpot.git
synced 2025-01-21 06:02:32 -05:00
commit
fb38e4378a
19 changed files with 347 additions and 147 deletions
|
@ -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]
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ------------------------------------------------------------------
|
||||
|
|
42
frontend/src/app/main/ui/workspace/tokens/errors.cljs
Normal file
42
frontend/src/app/main/ui/workspace/tokens/errors.cljs
Normal 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)))))
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
|
||||
.shadow {
|
||||
@extend .modal-container-base;
|
||||
width: auto;
|
||||
max-width: auto;
|
||||
min-width: auto;
|
||||
@include menuShadow;
|
||||
position: absolute;
|
||||
z-index: 11;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 -----------------------------------------------------------------------
|
||||
|
|
27
frontend/src/app/main/ui/workspace/tokens/tinycolor.cljs
Normal file
27
frontend/src/app/main/ui/workspace/tokens/tinycolor.cljs
Normal 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)
|
|
@ -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 "#"))))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue