Implement new token-type :font-families

This commit is contained in:
Florian Schroedl (aider) 2025-07-01 09:34:40 +02:00 committed by Andrés Moya
parent 2cddc6fb5b
commit d788a4d252
16 changed files with 544 additions and 196 deletions

View file

@ -10,6 +10,7 @@
[app.common.schema :as sm]
[clojure.data :as data]
[clojure.set :as set]
[cuerdas.core :as str]
[malli.util :as mu]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -29,20 +30,21 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def token-type->dtcg-token-type
{:boolean "boolean"
:border-radius "borderRadius"
:color "color"
:dimensions "dimension"
:font-size "fontSizes"
{:boolean "boolean"
:border-radius "borderRadius"
:color "color"
:dimensions "dimension"
:font-family "fontFamilies"
:font-size "fontSizes"
:letter-spacing "letterSpacing"
:number "number"
:opacity "opacity"
:other "other"
:rotation "rotation"
:sizing "sizing"
:spacing "spacing"
:string "string"
:stroke-width "strokeWidth"})
:number "number"
:opacity "opacity"
:other "other"
:rotation "rotation"
:sizing "sizing"
:spacing "spacing"
:string "string"
:stroke-width "strokeWidth"})
(def dtcg-token-type->token-type
(set/map-invert token-type->dtcg-token-type))
@ -133,7 +135,13 @@
(def letter-spacing-keys (schema-keys schema:letter-spacing))
(def typography-keys (set/union font-size-keys letter-spacing-keys))
(def ^:private schema:font-family
[:map
[:font-family {:optional true} token-name-ref]])
(def font-family-keys (schema-keys schema:font-family))
(def typography-keys (set/union font-size-keys letter-spacing-keys font-family-keys))
;; TODO: Created to extract the font-size feature from the typography feature flag.
;; Delete this once the typography feature flag is removed.
@ -169,6 +177,7 @@
schema:number
schema:font-size
schema:letter-spacing
schema:font-family
schema:dimensions])
(defn shape-attr->token-attrs
@ -198,6 +207,7 @@
(font-size-keys shape-attr) #{shape-attr}
(letter-spacing-keys shape-attr) #{shape-attr}
(font-family-keys shape-attr) #{shape-attr}
(border-radius-keys shape-attr) #{shape-attr}
(sizing-keys shape-attr) #{shape-attr}
(opacity-keys shape-attr) #{shape-attr}
@ -291,3 +301,23 @@
(defn unapply-token-id [shape attributes]
(update shape :applied-tokens d/without-keys attributes))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TYPOGRAPHY
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn split-font-family
"Splits font family `value` string from into vector of font families.
Doesn't handle possible edge-case of font-families with `,` in their font family name."
[font-value]
(let [families (str/split font-value ",")
xform (comp
(map str/trim)
(remove str/empty?))]
(into [] xform families)))
(defn join-font-family
"Joins font family `value` into a string to be edited with a single input."
[font-families]
(str/join ", " font-families))

View file

@ -1333,13 +1333,25 @@ Will return a value that matches this schema:
(walk/postwalk
(fn [node]
(cond-> node
;; Handle sequential values that are objects with type
(and (map? node)
(contains? node "value")
(sequential? (get node "value")))
(sequential? (get node "value"))
(map? (first (get node "value"))))
(update "value"
(fn [seq-value]
(map #(set/rename-keys % {"type" "$type"}) seq-value)))
;; Keep array of font families
(and (map? node)
(contains? node "type")
(= "fontFamilies" (get node "type"))
(contains? node "value")
(sequential? (get node "value"))
(not (map? (first (get node "value")))))
identity
;; Rename keys for all token nodes
(and (map? node)
(and (contains? node "type")
(contains? node "value")))
@ -1371,7 +1383,16 @@ Will return a value that matches this schema:
(assoc tokens child-path (make-token
:name child-path
:type token-type
:value (get v "$value")
:value (cond-> (get v "$value")
;; Split string of font-families
(and (= :font-family token-type)
(string? (get v "$value")))
cto/split-font-family
;; Keep array of font-families
(and (= :font-family token-type)
(sequential? (get v "$value")))
identity)
:description (get v "$description")))
;; Discard unknown type tokens
tokens)))))

View file

@ -62,7 +62,11 @@
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-letter-spacing"
:type :letter-spacing
:value 2))))
:value 2))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :name "token-font-family"
:type :font-family
:value ["Helvetica" "Arial" "sans-serif"]))))
(tho/add-frame :frame1)
(tho/add-text :text1 "Hello World!")))
@ -77,7 +81,8 @@
(tht/apply-token-to-shape :frame1 "token-color" [:fill] [:fill] "#00ff00")
(tht/apply-token-to-shape :frame1 "token-dimensions" [:width :height] [:width :height] 100)
(tht/apply-token-to-shape :text1 "token-font-size" [:font-size] [:font-size] 24)
(tht/apply-token-to-shape :text1 "token-letter-spacing" [:letter-spacing] [:letter-spacing] 2)))
(tht/apply-token-to-shape :text1 "token-letter-spacing" [:letter-spacing] [:letter-spacing] 2)
(tht/apply-token-to-shape :text1 "token-font-family" [:font-family] [:font-family] ["Helvetica" "Arial" "sans-serif"])))
(t/deftest apply-tokens-to-shape
(let [;; ==== Setup
@ -93,6 +98,7 @@
token-dimensions (tht/get-token file "test-token-set" "token-dimensions")
token-font-size (tht/get-token file "test-token-set" "token-font-size")
token-letter-spacing (tht/get-token file "test-token-set" "token-letter-spacing")
token-font-family (tht/get-token file "test-token-set" "token-font-family")
;; ==== Action
changes (-> (-> (pcb/empty-changes nil)
@ -132,7 +138,10 @@
:attributes [:font-size]})
(cto/apply-token-to-shape {:token token-letter-spacing
:shape $
:attributes [:letter-spacing]})))
:attributes [:letter-spacing]})
(cto/apply-token-to-shape {:token token-font-family
:shape $
:attributes [:font-family]})))
(:objects page)
{}))
@ -157,9 +166,10 @@
(t/is (= (:fill applied-tokens') "token-color"))
(t/is (= (:width applied-tokens') "token-dimensions"))
(t/is (= (:height applied-tokens') "token-dimensions"))
(t/is (= (count text1-applied-tokens) 2))
(t/is (= (count text1-applied-tokens) 3))
(t/is (= (:font-size text1-applied-tokens) "token-font-size"))
(t/is (= (:letter-spacing text1-applied-tokens) "token-letter-spacing"))))
(t/is (= (:letter-spacing text1-applied-tokens) "token-letter-spacing"))
(t/is (= (:font-family text1-applied-tokens) "token-font-family"))))
(t/deftest unapply-tokens-from-shape
(let [;; ==== Setup
@ -189,7 +199,8 @@
(fn [shape]
(-> shape
(cto/unapply-token-id [:font-size])
(cto/unapply-token-id [:letter-spacing])))
(cto/unapply-token-id [:letter-spacing])
(cto/unapply-token-id [:font-family])))
(:objects page)
{}))
@ -240,7 +251,8 @@
d/txt-merge
{:fills (ths/sample-fills-color :fill-color "#fabada")
:font-size "1"
:letter-spacing "0"}))
:letter-spacing "0"
:font-family "Arial"}))
(:objects page)
{}))

View file

@ -195,6 +195,7 @@
value (.-value sd-token)
has-references? (str/includes? (:value origin-token) "{")
parsed-token-value (case (:type origin-token)
:font-family {:value (-> (js->clj value) (flatten))}
:color (parse-sd-token-color-value value)
:opacity (parse-sd-token-opacity-value value has-references?)
:stroke-width (parse-sd-token-stroke-width-value value has-references?)

View file

@ -14,6 +14,7 @@
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.common.types.typography :as cty]
[app.common.uuid :as uuid]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
[app.main.data.style-dictionary :as sd]
@ -24,6 +25,7 @@
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.transforms :as dwtr]
[app.main.data.workspace.undo :as dwu]
[app.main.fonts :as fonts]
[app.main.store :as st]
[beicon.v2.core :as rx]
[clojure.set :as set]
@ -364,6 +366,26 @@
{:ignore-touched true
:page-id page-id})))))
(defn update-font-family
([value shape-ids attributes] (update-font-family value shape-ids attributes nil))
([value shape-ids _attributes page-id]
(let [font-family (first value)
font (some-> font-family
(fonts/find-font-family))
text-attrs (if font
{:font-id (:id font)
:font-family (:family font)}
{:font-id (str uuid/zero)
:font-family font-family})
update-node? (fn [node]
(or (txt/is-text-node? node)
(txt/is-paragraph-node? node)))]
(when text-attrs
(dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? d/txt-merge text-attrs)
{:ignore-touched true
:page-id page-id})))))
(defn update-font-size
([value shape-ids attributes] (update-font-size value shape-ids attributes nil))
([value shape-ids _attributes page-id]
@ -421,6 +443,14 @@
:fields [{:label "Letter Spacing"
:key :letter-spacing}]}}
:font-family
{:title "Font Family"
:attributes ctt/font-family-keys
:on-update-shape update-font-family
:modal {:key :tokens/font-family
:fields [{:label "Font Family"
:key :font-family}]}}
:stroke-width
{:title "Stroke Width"
:attributes ctt/stroke-width-keys

View file

@ -35,6 +35,7 @@
#{:line-height} dwta/update-line-height
#{:font-size} dwta/update-font-size
#{:letter-spacing} dwta/update-letter-spacing
#{:font-family} dwta/update-font-family
#{:x :y} dwta/update-shape-position
#{:p1 :p2 :p3 :p4} dwta/update-layout-padding
#{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin

View file

@ -78,6 +78,15 @@
data))
(vals @fontsdb)))
(defn find-font-family
"Case insensitive lookup of font-family."
[family]
(let [family' (str/lower family)]
(d/seek
(fn [{:keys [family]}]
(= family' (str/lower family)))
(vals @fontsdb))))
(defn resolve-variants
[id]
(get-in @fontsdb [id :variants]))

View file

@ -20,7 +20,7 @@
[:class {:optional true} :string]
[:id :string]
[:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]]
[:maybe [:and :string [:fn #(contains? icon-list %)]]]]
[:has-hint {:optional true} :boolean]
[:hint-type {:optional true} [:maybe [:enum "hint" "error" "warning"]]]
[:type {:optional true} :string]

View file

@ -11,6 +11,7 @@
[app.common.data.macros :as dm]
[app.common.files.tokens :as cft]
[app.common.types.color :as c]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
[app.main.data.modal :as modal]
@ -21,9 +22,11 @@
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.tokens.propagation :as dwtp]
[app.main.data.workspace.tokens.warnings :as wtw]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
[app.main.ui.ds.foundations.assets.icon :as i]
@ -31,6 +34,8 @@
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.workspace.colorpicker :as colorpicker]
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]]
[app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]]
[app.main.ui.workspace.tokens.management.create.input-token-color-bullet :refer [input-token-color-bullet*]]
[app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-tokens-value*]]
[app.util.dom :as dom]
[app.util.functions :as uf]
@ -94,7 +99,17 @@
(defn valid-value? [value]
(seq (finalize-value value)))
;; Component -------------------------------------------------------------------
;; Validation ------------------------------------------------------------------
(defn validate-empty-input [value]
(if (sequential? value)
(empty? value)
(empty? (str/trim value))))
(defn validate-self-reference? [token-name value]
(if (sequential? value)
(some #(ctob/token-value-self-reference? token-name %) value)
(ctob/token-value-self-reference? token-name value)))
(defn validate-token-value
"Validates token value by resolving the value `input` using `StyleDictionary`.
@ -104,17 +119,19 @@
;; 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? (str/trim value))
(validate-empty-input value)
(rx/throw {:errors [(wte/get-error-code :error.token/empty-input)]})
(ctob/token-value-self-reference? token-name value)
(validate-self-reference? token-name value)
(rx/throw {:errors [(wte/get-error-code :error.token/direct-self-reference)]})
:else
(let [tokens' (cond-> tokens
;; Remove previous token when renaming a token
(not= name-value (:name token)) (dissoc (:name token))
:always (update token-name #(ctob/make-token (merge % {:value value
:always (update token-name #(ctob/make-token (merge % {:value (cond
(= (:type token) :font-family) (ctt/split-font-family value)
:else value)
:name token-name
:type (:type token)}))))]
(->> tokens'
@ -156,59 +173,7 @@
(defonce form-token-cache-atom (atom nil))
;; FIXME: this function has confusing name
(defn- hex->value
[hex]
(when-let [tc (tinycolor/valid-color hex)]
(let [hex (tinycolor/->hex-string tc)
alpha (tinycolor/alpha 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 alpha})))
(mf/defc ramp*
[{:keys [color on-change]}]
(let [wrapper-node-ref (mf/use-ref nil)
dragging-ref (mf/use-ref false)
on-start-drag
(mf/use-fn #(mf/set-ref-val! dragging-ref true))
on-finish-drag
(mf/use-fn #(mf/set-ref-val! dragging-ref false))
internal-color*
(mf/use-state #(hex->value color))
internal-color
(deref internal-color*)
on-change'
(mf/use-fn
(mf/deps on-change)
(fn [{:keys [hex alpha] :as selector-color}]
(let [dragging? (mf/ref-val dragging-ref)]
(when-not (and dragging? hex)
(reset! internal-color* selector-color)
(on-change hex alpha)))))]
(mf/use-effect
(mf/deps color)
(fn []
;; Update internal color when user changes input value
(when-let [color (tinycolor/valid-color color)]
(when-not (= (tinycolor/->hex-string color) (:hex internal-color))
(reset! internal-color* (hex->value color))))))
(colorpicker/use-color-picker-css-variables! wrapper-node-ref internal-color)
[:div {:ref wrapper-node-ref}
[:> ramp-selector*
{:color internal-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag
:on-change on-change'}]]))
;; Component -------------------------------------------------------------------
(mf/defc token-value-hint
[{:keys [result]}]
@ -232,13 +197,11 @@
:class (stl/css-case :resolved-value (not (or empty-message? (seq warnings) (seq errors))))
:type type}]))
(mf/defc form
{::mf/wrap-props false}
[{:keys [token token-type action selected-token-set-name on-display-colorpicker]}]
(mf/defc form*
[{:keys [token token-type action selected-token-set-name transform-value on-value-resolve custom-input-token-value custom-input-token-value-props]}]
(let [create? (not (instance? ctob/Token token))
token (or token {:type token-type})
token-properties (dwta/get-token-properties token)
is-color-token (cft/color-token? token)
tokens-in-selected-set (mf/deref refs/workspace-all-tokens-in-selected-set)
active-theme-tokens (cond-> (mf/deref refs/workspace-active-theme-sets-tokens)
@ -246,7 +209,11 @@
;; even if the name has been overriden by a token with the same name
;; in another set below.
(and (:name token) (:value token))
(assoc (:name token) token))
(assoc (:name token) token)
;; Style dictionary resolver needs font families to be an array of strings
(= :font-family (or (:type token) token-type))
(update-in [(:name token) :value] ctt/split-font-family))
resolved-tokens (sd/use-resolved-tokens active-theme-tokens {:cache-atom form-token-cache-atom
:interactive? true})
@ -260,13 +227,6 @@
(-> (ctob/tokens-tree tokens-in-selected-set)
;; Allow setting editing token to it's own path
(d/dissoc-in token-path))))
cancel-ref (mf/use-ref nil)
on-cancel-ref
(mf/use-fn
(fn [node]
(mf/set-ref-val! cancel-ref node)))
;; Name
touched-name* (mf/use-state false)
touched-name? (deref touched-name*)
@ -286,16 +246,13 @@
on-blur-name
(mf/use-fn
(mf/deps cancel-ref touched-name? warning-name-change?)
(mf/deps touched-name? warning-name-change?)
(fn [e]
(let [node (dom/get-related-target e)
on-cancel-btn (= node (mf/ref-val cancel-ref))]
(when-not on-cancel-btn
(let [value (dom/get-target-val e)
errors (validate-name value)]
(when touched-name?
(reset! warning-name-change* true))
(reset! name-errors errors))))))
(let [value (dom/get-target-val e)
errors (validate-name value)]
(when touched-name?
(reset! warning-name-change* true))
(reset! name-errors errors))))
on-update-name-debounced
(mf/use-fn
@ -321,10 +278,6 @@
(valid-name? @token-name-ref))
;; Value
color* (mf/use-state (when is-color-token (:value token)))
color (deref color*)
color-ramp-open* (mf/use-state false)
color-ramp-open? (deref color-ramp-open*)
value-input-ref (mf/use-ref nil)
value-ref (mf/use-ref (:value token))
@ -333,68 +286,46 @@
set-resolve-value
(mf/use-fn
(mf/deps on-value-resolve)
(fn [token-or-err]
(let [error? (:errors token-or-err)
warnings? (:warnings token-or-err)
v (cond
error?
token-or-err
(do
(when on-value-resolve (on-value-resolve nil))
token-or-err)
warnings?
(:warnings {:warnings token-or-err})
:else
(:resolved-value token-or-err))]
(when is-color-token (reset! color* (if error? nil v)))
(cond-> (:resolved-value token-or-err)
on-value-resolve on-value-resolve))]
(reset! token-resolve-result* v))))
on-update-value-debounced (use-debonced-resolve-callback token-name-ref token active-theme-tokens set-resolve-value)
on-update-value (mf/use-fn
(mf/deps on-update-value-debounced)
(fn [e]
(let [value (dom/get-target-val e)
;; Automatically add # for hex values
value' (if (and is-color-token (tinycolor/hex-without-hash-prefix? value))
(let [hex (dm/str "#" value)]
(dom/set-value! (mf/ref-val value-input-ref) hex)
hex)
value)]
(mf/set-ref-val! value-ref value')
(on-update-value-debounced value'))))
on-update-color (mf/use-fn
(mf/deps color on-update-value-debounced)
(fn [hex-value alpha]
(let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field
prev-input-color (some-> (dom/get-value (mf/ref-val value-input-ref))
(tinycolor/valid-color))
;; If the input is a reference we will take the format from the computed value
prev-computed-color (when-not prev-input-color
(some-> color (tinycolor/valid-color)))
prev-format (some-> (or prev-input-color prev-computed-color)
(tinycolor/color-format))
to-rgba? (and
(< alpha 1)
(or (= prev-format "hex") (not prev-format)))
to-hex? (and (not prev-format) (= alpha 1))
format (cond
to-rgba? "rgba"
to-hex? "hex"
prev-format prev-format
:else "hex")
color-value (-> (tinycolor/valid-color hex-value)
(tinycolor/set-alpha (or alpha 1))
(tinycolor/->string format))]
(mf/set-ref-val! value-ref color-value)
(dom/set-value! (mf/ref-val value-input-ref) color-value)
(on-update-value-debounced color-value))))
on-display-colorpicker'
on-update-value
(mf/use-fn
(mf/deps color-ramp-open? on-display-colorpicker)
(fn []
(let [open? (not color-ramp-open?)]
(reset! color-ramp-open* open?)
(on-display-colorpicker open?))))
(mf/deps on-update-value-debounced transform-value)
(fn [e]
(let [value (dom/get-target-val e)
value' (if (fn? transform-value)
(transform-value value)
value)]
;; Value got updated in transform, update the dom node
(when (not= value value')
(dom/set-value! (mf/ref-val value-input-ref) value'))
(mf/set-ref-val! value-ref value)
(on-update-value-debounced value))))
on-external-update-value
(mf/use-fn
(mf/deps on-update-value-debounced)
(fn [next-value]
(dom/set-value! (mf/ref-val value-input-ref) next-value)
(mf/set-ref-val! value-ref next-value)
(on-update-value-debounced next-value)))
value-error? (seq (:errors token-resolve-result))
@ -431,7 +362,6 @@
(mf/deps validate-name validate-descripion token active-theme-tokens)
(fn [e]
(dom/prevent-default e)
(mf/set-ref-val! cancel-ref nil)
;; We have to re-validate the current form values before submitting
;; because the validation is asynchronous/debounced
;; and the user might have edited a valid form to make it invalid,
@ -440,7 +370,11 @@
valid-name? (try
(not (:errors (validate-name final-name)))
(catch js/Error _ nil))
final-value (finalize-value (mf/ref-val value-ref))
final-value (let [value (mf/ref-val value-ref)
font-family? (= :font-family (or (:type token) token-type))]
(if font-family?
(ctt/split-font-family value)
(finalize-value value)))
final-description @description-ref
valid-description? (if final-description
(try
@ -479,9 +413,9 @@
on-cancel
(mf/use-fn
(fn [e]
(mf/set-ref-val! cancel-ref nil)
(dom/prevent-default e)
(modal/hide!)))
handle-key-down-delete
(mf/use-fn
(mf/deps on-delete-token)
@ -555,20 +489,31 @@
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
[:div {:class (stl/css :input-row)}
[:> input-tokens-value*
{:placeholder (tr "workspace.tokens.token-value-enter")
:label (tr "workspace.tokens.token-value")
:default-value (mf/ref-val value-ref)
:ref value-input-ref
:is-color-token is-color-token
:color color
:on-change on-update-value
:error (not (nil? (:errors token-resolve-result)))
:display-colorpicker on-display-colorpicker'
:on-blur on-update-value}]
(when color-ramp-open?
[:> ramp* {:color (some-> color (tinycolor/valid-color))
:on-change on-update-color}])
(let [placeholder (tr "workspace.tokens.token-value-enter")
label (tr "workspace.tokens.token-value")
default-value (mf/ref-val value-ref)
ref value-input-ref
error (not (nil? (:errors token-resolve-result)))
on-blur on-update-value]
(if (fn? custom-input-token-value)
[:> custom-input-token-value
{:placeholder placeholder
:label label
:default-value default-value
:input-ref ref
:error error
:on-blur on-blur
:on-update-value on-update-value
:on-external-update-value on-external-update-value
:custom-input-token-value-props custom-input-token-value-props}]
[:> input-tokens-value*
{:placeholder placeholder
:label label
:default-value default-value
:ref ref
:error error
:on-blur on-blur
:on-change on-update-value}]))
[:& token-value-hint {:result token-resolve-result}]]
[:div {:class (stl/css :input-row)}
[:> input* {:label (tr "workspace.tokens.token-description")
@ -593,7 +538,6 @@
[:> button* {:on-click on-cancel
:on-key-down handle-key-down-cancel
:type "button"
:on-ref on-cancel-ref
:id "token-modal-cancel"
:variant "secondary"}
(tr "labels.cancel")]
@ -602,3 +546,247 @@
:variant "primary"
:disabled disabled?}
(tr "labels.save")]]]]))
;; FIXME: this function has confusing name
(defn- hex->value
[hex]
(when-let [tc (tinycolor/valid-color hex)]
(let [hex (tinycolor/->hex-string tc)
alpha (tinycolor/alpha 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 alpha})))
(mf/defc ramp*
[{:keys [color on-change]}]
(let [wrapper-node-ref (mf/use-ref nil)
dragging-ref (mf/use-ref false)
on-start-drag
(mf/use-fn #(mf/set-ref-val! dragging-ref true))
on-finish-drag
(mf/use-fn #(mf/set-ref-val! dragging-ref false))
internal-color*
(mf/use-state #(hex->value color))
internal-color
(deref internal-color*)
on-change'
(mf/use-fn
(mf/deps on-change)
(fn [{:keys [hex alpha] :as selector-color}]
(let [dragging? (mf/ref-val dragging-ref)]
(when-not (and dragging? hex)
(reset! internal-color* selector-color)
(on-change hex alpha)))))]
(mf/use-effect
(mf/deps color)
(fn []
;; Update internal color when user changes input value
(when-let [color (tinycolor/valid-color color)]
(when-not (= (tinycolor/->hex-string color) (:hex internal-color))
(reset! internal-color* (hex->value color))))))
(colorpicker/use-color-picker-css-variables! wrapper-node-ref internal-color)
[:div {:ref wrapper-node-ref}
[:> ramp-selector*
{:color internal-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag
:on-change on-change'}]]))
(mf/defc color-picker*
[{:keys [placeholder label default-value input-ref error on-blur on-update-value on-external-update-value custom-input-token-value-props]}]
(let [{:keys [color on-display-colorpicker]} custom-input-token-value-props
color-ramp-open* (mf/use-state false)
color-ramp-open? (deref color-ramp-open*)
on-click-swatch
(mf/use-fn
(mf/deps color-ramp-open? on-display-colorpicker)
(fn []
(let [open? (not color-ramp-open?)]
(reset! color-ramp-open* open?)
(on-display-colorpicker open?))))
swatch
(mf/html
[:> input-token-color-bullet*
{:color color
:class (stl/css :slot-start)
:on-click on-click-swatch}])
on-change'
(mf/use-fn
(mf/deps color on-external-update-value)
(fn [hex-value alpha]
(let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field
prev-input-color (some-> (dom/get-value (mf/ref-val input-ref))
(tinycolor/valid-color))
;; If the input is a reference we will take the format from the computed value
prev-computed-color (when-not prev-input-color
(some-> color (tinycolor/valid-color)))
prev-format (some-> (or prev-input-color prev-computed-color)
(tinycolor/color-format))
to-rgba? (and
(< alpha 1)
(or (= prev-format "hex") (not prev-format)))
to-hex? (and (not prev-format) (= alpha 1))
format (cond
to-rgba? "rgba"
to-hex? "hex"
prev-format prev-format
:else "hex")
color-value (-> (tinycolor/valid-color hex-value)
(tinycolor/set-alpha (or alpha 1))
(tinycolor/->string format))]
(on-external-update-value color-value))))]
[:*
[:> input-tokens-value*
{:placeholder placeholder
:label label
:default-value default-value
:ref input-ref
:error error
:on-blur on-blur
:on-change on-update-value
:slot-start swatch}]
(when color-ramp-open?
[:> ramp*
{:color (some-> color (tinycolor/valid-color))
:on-change on-change'}])]))
(mf/defc color-form*
[{:keys [token on-display-colorpicker] :rest props}]
(let [color* (mf/use-state (:value token))
color (deref color*)
on-value-resolve (mf/use-fn
(mf/deps color)
(fn [value]
(reset! color* value)
value))
custom-input-token-value-props
(mf/use-memo
(mf/deps color on-display-colorpicker)
(fn []
{:color color
:on-display-colorpicker on-display-colorpicker}))
transform-value
(mf/use-fn
(fn [value]
(if (tinycolor/hex-without-hash-prefix? value)
(dm/str "#" value)
value)))]
[:> form*
(mf/spread-props props {:token token
:transform-value transform-value
:on-value-resolve on-value-resolve
:custom-input-token-value color-picker*
:custom-input-token-value-props custom-input-token-value-props})]))
(mf/defc font-selector-wrapper*
[{:keys [font input-ref on-select-font on-close-font-selector]}]
(let [current-font* (mf/use-state (or font
(some-> (mf/ref-val input-ref)
(dom/get-value)
(ctt/split-font-family)
(first)
(fonts/find-font-family))))
current-font (deref current-font*)]
[:div {:class (stl/css :font-select-wrapper)}
[:> font-selector* {:current-font current-font
:on-select on-select-font
:on-close on-close-font-selector
:full-size true}]]))
(mf/defc font-picker*
[{:keys [default-value input-ref error on-blur on-update-value on-external-update-value]}]
(let [font* (mf/use-state (fonts/find-font-family default-value))
font (deref font*)
set-font (mf/use-fn
(mf/deps font)
#(reset! font* %))
font-selector-open* (mf/use-state false)
font-selector-open? (deref font-selector-open*)
on-close-font-selector
(mf/use-fn
(fn []
(reset! font-selector-open* false)))
on-click-dropdown-button
(mf/use-fn
(mf/deps font-selector-open?)
(fn [e]
(dom/prevent-default e)
(reset! font-selector-open* (not font-selector-open?))))
on-select-font
(mf/use-fn
(mf/deps on-external-update-value set-font)
(fn [{:keys [family] :as font}]
(when font
(set-font font)
(on-external-update-value family))))
on-update-value'
(mf/use-fn
(mf/deps on-update-value set-font)
(fn [value]
(set-font nil)
(on-update-value value)))
font-selector-button
(mf/html
[:> icon-button*
{:on-click on-click-dropdown-button
:aria-label (tr "workspace.tokens.token-font-family-select")
:icon "arrow-down"
:variant "action"
:type "button"}])]
[:*
[:> input-tokens-value*
{:placeholder (tr "workspace.tokens.token-font-family-value-enter")
:label (tr "workspace.tokens.token-font-family-value")
:default-value default-value
:ref input-ref
:error error
:on-blur on-blur
:on-change on-update-value'
:icon "text-font-family"
:slot-end font-selector-button}]
(when font-selector-open?
[:> font-selector-wrapper* {:font font
:input-ref input-ref
:on-select-font on-select-font
:on-close-font-selector on-close-font-selector}])]))
(mf/defc font-family-form*
[{:keys [token] :rest props}]
(let [on-value-resolve
(mf/use-fn
(fn [value]
(when value
(ctt/join-font-family value))))]
[:> form*
(mf/spread-props props {:token (when token (update token :value ctt/join-font-family))
:custom-input-token-value font-picker*
:on-value-resolve on-value-resolve})]))
(mf/defc form-wrapper*
[{:keys [token token-type] :as props}]
(let [token-type' (or (:type token) token-type)]
(case token-type'
:color [:> color-form* props]
:font-family [:> font-family-form* props]
[:> form* props])))

View file

@ -8,6 +8,7 @@
.form-wrapper {
width: $s-384;
position: relative;
}
.button-row {
@ -64,3 +65,11 @@
.form-modal-title {
color: var(--color-foreground-primary);
}
.font-select-wrapper {
position: absolute;
inset: 0;
// This padding from the modal should be shared as a variable
// Need to set this or the font-select will cause scroll
bottom: $s-32;
}

View file

@ -7,11 +7,10 @@
(ns app.main.ui.workspace.tokens.management.create.input-tokens-value
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
[app.main.ui.ds.controls.utilities.label :refer [label*]]
[app.main.ui.workspace.tokens.management.create.input-token-color-bullet :refer [input-token-color-bullet*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon-list]]
[rumext.v2 :as mf]))
(def ^:private schema::input-tokens-value
@ -20,26 +19,18 @@
[:placeholder {:optional true} :string]
[:value {:optional true} [:maybe :string]]
[:class {:optional true} :string]
[:is-color-token {:optional true} :boolean]
[:color {:optional true} [:maybe :string]]
[:display-colorpicker {:optional true} fn?]
[:error {:optional true} :boolean]])
[:error {:optional true} :boolean]
[:slot-start {:optional true} [:maybe some?]]
[:icon {:optional true}
[:maybe [:and :string [:fn #(contains? icon-list %)]]]]])
(mf/defc input-tokens-value*
{::mf/props :obj
::mf/forward-ref true
::mf/schema schema::input-tokens-value}
[{:keys [class label is-color-token placeholder error value color display-colorpicker] :rest props} ref]
[{:keys [class label placeholder error value icon slot-start] :rest props} ref]
(let [id (mf/use-id)
input-ref (mf/use-ref)
is-color-token (d/nilv is-color-token false)
swatch
(mf/html [:> input-token-color-bullet*
{:color color
:class (stl/css :slot-start)
:on-click display-colorpicker}])
props (mf/spread-props props {:id id
:type "text"
:class (stl/css :input)
@ -47,7 +38,8 @@
:value value
:variant "comfortable"
:hint-type (when error "error")
:slot-start (when is-color-token swatch)
:slot-start slot-start
:icon icon
:ref (or ref input-ref)})]
[:div {:class (dm/str class " " (stl/css-case :wrapper true
:input-error error))}

View file

@ -12,7 +12,7 @@
[app.main.refs :as refs]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.workspace.tokens.management.create.form :refer [form]]
[app.main.ui.workspace.tokens.management.create.form :refer [form-wrapper*]]
[app.util.i18n :refer [tr]]
[okulary.core :as l]
[rumext.v2 :as mf]))
@ -88,11 +88,11 @@
:icon i/close
:variant "action"
:aria-label (tr "labels.close")}]
[:& form {:token token
:action action
:selected-token-set-name selected-token-set-name
:token-type token-type
:on-display-colorpicker update-modal-size}]]))
[:> form-wrapper* {:token token
:action action
:selected-token-set-name selected-token-set-name
:token-type token-type
:on-display-colorpicker update-modal-size}]]))
;; Modals ----------------------------------------------------------------------
@ -191,3 +191,9 @@
::mf/register-as :tokens/letter-spacing}
[properties]
[:& token-update-create-modal properties])
(mf/defc font-familiy-modal
{::mf/register modal/components
::mf/register-as :tokens/font-family}
[properties]
[:& token-update-create-modal properties])

View file

@ -27,6 +27,7 @@
:border-radius "corner-radius"
:color "drop"
:boolean "boolean-difference"
:font-family "text-font-family"
:font-size "text-font-size"
:letter-spacing "text-letterspacing"
:opacity "percentage"

View file

@ -106,7 +106,9 @@
(defn- generate-tooltip
"Generates a tooltip for a given token"
[is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set]
(let [{:keys [name value type resolved-value]} token
(let [{:keys [name type resolved-value]} token
value (cond->> (:value token)
(= :font-family type) ctt/join-font-family)
resolved-value-theme (:resolved-value theme-token)
resolved-value (or resolved-value-theme resolved-value)
{:keys [title] :as token-props} (dwta/get-token-properties theme-token)

View file

@ -558,6 +558,40 @@
(t/is (= (:letter-spacing (:applied-tokens text-1')) (:name token-target')))
(t/is (= (:letter-spacing style-text-blocks) "2")))))))))
(t/deftest test-apply-font-family
(t/testing "applies font-family token and updates the text font-family"
(t/async
done
(let [font-family-token {:name "primary-font"
:value "Arial"
:type :font-family}
file (-> (setup-file-with-tokens)
(update-in [:data :tokens-lib]
#(ctob/add-token-in-set % "Set A" (ctob/make-token font-family-token))))
store (ths/setup-store file)
text-1 (cths/get-shape file :text-1)
events [(dwta/apply-token {:shape-ids [(:id text-1)]
:attributes #{:font-family}
:token (toht/get-token file "primary-font")
:on-update-shape dwta/update-font-family})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-state new-state)
token-target' (toht/get-token file' "primary-font")
text-1' (cths/get-shape file' :text-1)
style-text-blocks (->> (:content text-1')
(txt/content->text+styles)
(remove (fn [[_ text]] (str/empty? (str/trim text))))
(mapv (fn [[style text]]
{:styles (merge txt/default-text-attrs style)
:text-content text}))
(first)
(:styles))]
(t/is (some? (:applied-tokens text-1')))
(t/is (= (:font-family (:applied-tokens text-1')) (:name token-target')))
(t/is (= (:font-family style-text-blocks) (:font-id txt/default-text-attrs))))))))))
(t/deftest test-toggle-token-none
(t/testing "should apply token to all selected items, where no item has the token applied"
(t/async

View file

@ -7528,10 +7528,22 @@ msgstr "Could not resolve reference token with the name: %s"
msgid "workspace.tokens.token-value"
msgstr "Value"
#: src/app/main/ui/workspace/tokens/form.cljs:560
msgid "workspace.tokens.token-font-family-value"
msgstr "Font family"
#: src/app/main/ui/workspace/tokens/form.cljs:559
msgid "workspace.tokens.token-value-enter"
msgstr "Enter a value or alias with {alias}"
#: src/app/main/ui/workspace/tokens/form.cljs:689
msgid "workspace.tokens.token-font-family-value-enter"
msgstr "Font family or list of fonts separated by comma (,)"
#: src/app/main/ui/workspace/tokens/form.cljs:688
msgid "workspace.tokens.token-font-family-select"
msgstr "Select font family"
#: src/app/main/ui/workspace/tokens/sidebar.cljs:336
msgid "workspace.tokens.tokens-section-title"
msgstr "TOKENS - %s"
@ -7900,4 +7912,4 @@ msgid "labels.sources"
msgstr "Sources"
msgid "labels.pinned-projects"
msgstr "Pinned Projects"
msgstr "Pinned Projects"