Compare commits

...

6 Commits

Author SHA1 Message Date
Andrey Antukh
d7408c25e2 WIP 2025-11-06 13:22:50 +01:00
Andrey Antukh
3e884e1726 WIP 2025-11-06 13:22:06 +01:00
Andrey Antukh
99a618367d WIP 2025-11-06 13:21:53 +01:00
Eva Marco
669b311769 wip 2025-11-06 11:30:15 +01:00
Eva Marco
6d82a01a37 🚧 Wip 2025-11-06 09:53:25 +01:00
Eva Marco
d5e4b6f57e ♻️ Refactor formcomponent structure 2025-11-06 09:53:25 +01:00
16 changed files with 865 additions and 671 deletions

View File

@@ -1318,7 +1318,7 @@ test.describe("Tokens: Apply token", () => {
const firstColorValue = await colorInput.inputValue();
// User adds a second shadow
const addButton = firstShadowFields.getByTestId("shadow-add-button-0");
const addButton = firstShadowFields.getByTestId("shadow-add-button");
await addButton.click();
const secondShadowFields = tokensUpdateCreateModal.getByTestId(
@@ -1327,7 +1327,7 @@ test.describe("Tokens: Apply token", () => {
await expect(secondShadowFields).toBeVisible();
// User adds a third shadow
const addButton2 = secondShadowFields.getByTestId("shadow-add-button-1");
const addButton2 = secondShadowFields.getByTestId("shadow-add-button");
await addButton2.click();
const thirdShadowFields = tokensUpdateCreateModal.getByTestId(
@@ -1353,7 +1353,7 @@ test.describe("Tokens: Apply token", () => {
await removeButton2.click();
// Verify second shadow is removed
await expect(secondShadowFields.getByTestId("shadow-add-button-3")).not.toBeVisible();
await expect(secondShadowFields.getByTestId("shadow-add-button")).not.toBeVisible();
// Verify that the first shadow kept its values
const firstOffsetXValue = await firstShadowFields.getByLabel("X").inputValue();

View File

@@ -112,13 +112,14 @@
(reset! focused-id* nil)
(reset! is-open* false)
(when (fn? on-change)
(on-change id)))))
(on-change id event)))))
on-click
(mf/use-fn
(mf/deps disabled)
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(when-not disabled
(swap! is-open* not))))
@@ -195,7 +196,6 @@
(mf/set-ref-val! options-ref options))
[:div {:class (stl/css :select-wrapper)
:on-click on-click
:ref select-ref
:on-blur on-blur}

View File

@@ -69,7 +69,7 @@
handle-change-tab
(mf/use-fn
(mf/deps from on-change-section)
(fn [new-section]
(fn [new-section _e ]
(reset! section (keyword new-section))
(when on-change-section
(on-change-section (keyword new-section))
@@ -89,7 +89,7 @@
handle-change-color-space
(mf/use-fn
(fn [color-space]
(fn [color-space _e]
(reset! color-space* color-space)))
color-spaces

View File

@@ -490,7 +490,7 @@
switch-component
(mf/use-fn
(mf/deps shapes)
(fn [pos val]
(fn [pos val _e]
(if (= val mixed-label)
(reset! key* (uuid/next))
(let [error-msg (if (> (count shapes) 1)

View File

@@ -11,7 +11,6 @@
[app.common.data.macros :as dm]
[app.common.files.tokens :as cft]
[app.common.schema :as sm]
[app.common.types.color :as c]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
@@ -22,22 +21,19 @@
[app.main.data.workspace.tokens.errors :as wte]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.tokens.propagation :as dwtp]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[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.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.icons :as deprecated-icon]
[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-token* token-value-hint*]]
[app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token*]]
[app.main.ui.workspace.tokens.management.create.shadow :refer [shadow-value-inputs*]]
[app.main.ui.workspace.tokens.management.create.shared.color-picker :refer [color-picker*]]
[app.main.ui.workspace.tokens.management.create.shared.composite-tabs :refer [composite-tabs*]]
[app.main.ui.workspace.tokens.management.create.shared.font-combobox :refer [font-picker-combobox*]]
[app.main.ui.workspace.tokens.management.create.typography-composite :refer [typography-value-inputs*]]
[app.util.dom :as dom]
[app.util.functions :as uf]
[app.util.i18n :refer [tr]]
@@ -554,8 +550,8 @@
handle-key-down-save
(mf/use-fn
(mf/deps on-submit)
(fn [e]
(mf/deps on-submit)
(when (k/enter? e)
(on-submit e))))]
@@ -659,114 +655,16 @@
:disabled disabled?}
(tr "labels.save")]]]]))
;; Tabs Component --------------------------------------------------------------
(mf/defc composite-reference-input*
[{:keys [default-value on-blur on-update-value token-resolve-result reference-label reference-icon is-reference-fn]}]
[:> input-token*
{:aria-label (tr "labels.reference")
:placeholder reference-label
:icon reference-icon
:default-value (when (is-reference-fn default-value) default-value)
:on-blur on-blur
:on-change on-update-value
:token-resolve-result (when (or
(:errors token-resolve-result)
(string? (:value token-resolve-result)))
token-resolve-result)}])
(mf/defc composite-tabs*
[{:keys [default-value
on-update-value
on-external-update-value
on-value-resolve
clear-resolve-value
custom-input-token-value-props]
:rest props}]
(let [;; Active Tab State
{:keys [active-tab
composite-tab
is-reference-fn
reference-icon
reference-label
set-active-tab
title
update-composite-backup-value]} custom-input-token-value-props
reference-tab-active? (= :reference active-tab)
;; Backup value ref
;; Used to restore the previously entered value when switching tabs
;; Uses ref to not trigger state updates during update
backup-state-ref (mf/use-var
(if reference-tab-active?
{:reference default-value}
{:composite default-value}))
default-value (get @backup-state-ref active-tab)
on-toggle-tab
(mf/use-fn
(mf/deps active-tab on-external-update-value on-value-resolve clear-resolve-value)
(fn []
(let [next-tab (if (= active-tab :composite) :reference :composite)]
;; Clear the resolved value so it wont show up before the next-tab value has resolved
(clear-resolve-value)
;; Restore the internal value from backup
(on-external-update-value (get @backup-state-ref next-tab))
(set-active-tab next-tab))))
update-composite-value
(mf/use-fn
(fn [f]
(clear-resolve-value)
(swap! backup-state-ref f)
(on-external-update-value (get @backup-state-ref :composite))))
;; Store updated value in backup-state-ref
on-update-value'
(mf/use-fn
(mf/deps on-update-value reference-tab-active? update-composite-backup-value)
(fn [e]
(if reference-tab-active?
(swap! backup-state-ref assoc :reference (dom/get-target-val e))
(swap! backup-state-ref update :composite #(update-composite-backup-value % e)))
(on-update-value e)))]
[:div {:class (stl/css :typography-inputs-row)}
[:div {:class (stl/css :title-bar)}
[:div {:class (stl/css :title)} title]
[:& radio-buttons {:class (stl/css :listing-options)
:selected (if reference-tab-active? "reference" "composite")
:on-change on-toggle-tab
:name "reference-composite-tab"}
[:& radio-button {:icon deprecated-icon/layers
:value "composite"
:title (tr "workspace.tokens.individual-tokens")
:id "composite-opt"}]
[:& radio-button {:icon deprecated-icon/tokens
:value "reference"
:title (tr "workspace.tokens.use-reference")
:id "reference-opt"}]]]
[:div {:class (stl/css :typography-inputs)}
(if reference-tab-active?
[:> composite-reference-input*
(mf/spread-props props {:default-value default-value
:on-update-value on-update-value'
:reference-icon reference-icon
:reference-label reference-label
:is-reference-fn is-reference-fn})]
[:> composite-tab
(mf/spread-props props {:default-value default-value
:on-update-value on-update-value'
:update-composite-value update-composite-value})])]]))
(mf/defc composite-form*
"Wrapper around form* that manages composite/reference tab state.
Takes the same props as form* plus a function to determine if a token value is a reference."
[{:keys [token is-reference-fn composite-tab reference-icon title update-composite-backup-value] :rest props}]
[{:keys [token is-reference-fn composite-tab reference-icon title update-composite-backup-value type on-add-shadow] :rest props}]
(let [active-tab* (mf/use-state (if (is-reference-fn (:value token)) :reference :composite))
active-tab (deref active-tab*)
custom-input-token-value-props
(mf/use-memo
(mf/deps active-tab composite-tab reference-icon title update-composite-backup-value is-reference-fn)
(mf/deps active-tab composite-tab reference-icon title update-composite-backup-value is-reference-fn type)
(fn []
{:active-tab active-tab
:set-active-tab #(reset! active-tab* %)
@@ -774,6 +672,8 @@
:reference-icon reference-icon
:reference-label (tr "workspace.tokens.reference-composite")
:title title
:type type
:on-add-shadow on-add-shadow
:update-composite-backup-value update-composite-backup-value
:is-reference-fn is-reference-fn}))
@@ -793,124 +693,6 @@
;; Token Type Forms ------------------------------------------------------------
;; 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 on-blur on-update-value on-external-update-value custom-input-token-value-props token-resolve-result]}]
(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?)
(when on-display-colorpicker
(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))]
(dom/set-value! (mf/ref-val input-ref) color-value)
(on-external-update-value color-value))))]
[:*
[:> input-token*
{:placeholder placeholder
:label label
:default-value default-value
:ref input-ref
: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'}])
[:> token-value-hint* {:result token-resolve-result}]]))
(mf/defc color-form*
[{:keys [token on-display-colorpicker] :rest props}]
(let [color* (mf/use-state (:value token))
@@ -945,242 +727,10 @@
:custom-input-token-value color-picker*
:custom-input-token-value-props custom-input-token-value-props})]))
(mf/defc shadow-color-picker-wrapper*
"Wrapper for color-picker* that passes shadow color state from parent.
Similar to color-form* but receives color state from shadow-value-inputs*."
[{:keys [placeholder label default-value input-ref on-update-value on-external-update-value token-resolve-result shadow-color]}]
(let [;; Use the color state passed from parent (shadow-value-inputs*)
resolved-color (get token-resolve-result :resolved-value)
color (or shadow-color resolved-color default-value "")
custom-input-token-value-props
(mf/use-memo
(mf/deps color)
(fn []
{:color color}))]
[:> color-picker*
{:placeholder placeholder
:label label
:default-value default-value
:input-ref input-ref
:on-update-value on-update-value
:on-external-update-value on-external-update-value
:custom-input-token-value-props custom-input-token-value-props
:token-resolve-result token-resolve-result}]))
(def ^:private shadow-inputs
#(d/ordered-map
:offsetX
{:label (tr "workspace.tokens.shadow-x")
:placeholder (tr "workspace.tokens.shadow-x")}
:offsetY
{:label (tr "workspace.tokens.shadow-y")
:placeholder (tr "workspace.tokens.shadow-y")}
:blur
{:label (tr "workspace.tokens.shadow-blur")
:placeholder (tr "workspace.tokens.shadow-blur")}
:spread
{:label (tr "workspace.tokens.shadow-spread")
:placeholder (tr "workspace.tokens.shadow-spread")}
:color
{:label (tr "workspace.tokens.shadow-color")
:placeholder (tr "workspace.tokens.shadow-color")}
:inset
{:label (tr "workspace.tokens.shadow-inset")
:placeholder (tr "workspace.tokens.shadow-inset")}))
(mf/defc inset-type-select*
[{:keys [default-value shadow-idx label on-change]}]
(let [selected* (mf/use-state (or (str default-value) "false"))
selected (deref selected*)
on-change
(mf/use-fn
(mf/deps on-change selected shadow-idx)
(fn [value e]
(obj/set! e "tokenValue" (if (= "true" value) true false))
(on-change e)
(reset! selected* (str value))))]
[:div {:class (stl/css :input-row)}
[:div {:class (stl/css :inset-label)} label]
[:& radio-buttons {:selected selected
:on-change on-change
:name (str "inset-select-" shadow-idx)}
[:& radio-button {:value "false"
:title "false"
:icon "❌"
:id (str "inset-default-" shadow-idx)}]
[:& radio-button {:value "true"
:title "true"
:icon "✅"
:id (str "inset-false-" shadow-idx)}]]]))
(mf/defc shadow-input*
[{:keys [default-value label placeholder shadow-idx input-type on-update-value on-external-update-value token-resolve-result errors-by-key shadow-color]}]
(let [color-input-ref (mf/use-ref)
on-change
(mf/use-fn
(mf/deps shadow-idx input-type on-update-value)
(fn [e]
(-> (obj/set! e "tokenTypeAtIndex" [shadow-idx input-type])
(on-update-value))))
on-external-update-value'
(mf/use-fn
(mf/deps shadow-idx input-type on-external-update-value)
(fn [v]
(on-external-update-value [shadow-idx input-type] v)))
resolved (get-in token-resolve-result [:resolved-value shadow-idx input-type])
errors (get errors-by-key input-type)
should-show? (or (some? resolved) (seq errors))
token-prop (when should-show?
(d/without-nils
{:resolved-value resolved
:errors errors}))]
(case input-type
:inset
[:> inset-type-select*
{:default-value default-value
:shadow-idx shadow-idx
:label label
:on-change on-change}]
:color
[:> shadow-color-picker-wrapper*
{:placeholder placeholder
:label label
:default-value default-value
:input-ref color-input-ref
:on-update-value on-change
:on-external-update-value on-external-update-value'
:token-resolve-result token-prop
:shadow-color shadow-color
:data-testid (str "shadow-color-input-" shadow-idx)}]
[:div {:class (stl/css :input-row)
:data-testid (str "shadow-" (name input-type) "-input-" shadow-idx)}
[:> input-token*
{:label label
:placeholder placeholder
:default-value default-value
:on-change on-change
:token-resolve-result token-prop}]])))
(mf/defc shadow-input-fields*
[{:keys [shadow shadow-idx on-remove-shadow on-add-shadow is-remove-disabled on-update-value token-resolve-result errors-by-key on-external-update-value shadow-color] :as props}]
(let [on-remove-shadow
(mf/use-fn
(mf/deps shadow-idx on-remove-shadow)
#(on-remove-shadow shadow-idx))]
[:div {:data-testid (str "shadow-input-fields-" shadow-idx)}
[:> icon-button* {:icon i/add
:type "button"
:on-click on-add-shadow
:data-testid (str "shadow-add-button-" shadow-idx)
:aria-label (tr "workspace.tokens.shadow-add-shadow")}]
[:> icon-button* {:variant "ghost"
:type "button"
:icon i/remove
:on-click on-remove-shadow
:disabled is-remove-disabled
:data-testid (str "shadow-remove-button-" shadow-idx)
:aria-label (tr "workspace.tokens.shadow-remove-shadow")}]
(for [[input-type {:keys [label placeholder]}] (shadow-inputs)]
[:> shadow-input*
{:key (str input-type shadow-idx)
:input-type input-type
:label label
:placeholder placeholder
:shadow-idx shadow-idx
:default-value (get shadow input-type)
:on-update-value on-update-value
:token-resolve-result token-resolve-result
:errors-by-key errors-by-key
:on-external-update-value on-external-update-value
:shadow-color shadow-color}])]))
(mf/defc shadow-value-inputs*
[{:keys [default-value on-update-value token-resolve-result update-composite-value] :as props}]
(let [shadows* (mf/use-state (or default-value [{}]))
shadows (deref shadows*)
shadows-count (count shadows)
composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result)))
;; Maintain a map of color states for each shadow to prevent reset on add/remove
shadow-colors* (mf/use-state {})
shadow-colors (deref shadow-colors*)
;; Initialize color states for each shadow index
_ (mf/use-effect
(mf/deps shadows)
(fn []
(doseq [[idx shadow] (d/enumerate shadows)]
(when-not (contains? shadow-colors idx)
(let [resolved-color (get-in token-resolve-result [:resolved-value idx :color])
initial-color (or resolved-color (get shadow :color) "")]
(swap! shadow-colors* assoc idx initial-color))))))
;; Define on-external-update-value here where we have access to on-update-value
on-external-update-value
(mf/use-callback
(mf/deps on-update-value shadow-colors*)
(fn [token-type-at-index value]
(let [[idx token-type] token-type-at-index
e (js-obj)]
;; Update shadow color state if this is a color update
(when (= token-type :color)
(swap! shadow-colors* assoc idx value))
(obj/set! e "tokenTypeAtIndex" token-type-at-index)
(obj/set! e "target" #js {:value value})
(on-update-value e))))
on-add-shadow
(mf/use-fn
(mf/deps shadows update-composite-value)
(fn []
(update-composite-value
(fn [state]
(let [new-state (update state :composite (fnil conj []) {})]
(reset! shadows* (:composite new-state))
new-state)))))
on-remove-shadow
(mf/use-fn
(mf/deps shadows update-composite-value)
(fn [idx]
(update-composite-value
(fn [state]
(let [new-state (update state :composite d/remove-at-index idx)]
(reset! shadows* (:composite new-state))
new-state)))))]
[:div {:class (stl/css :nested-input-row)}
(for [[shadow-idx shadow] (d/enumerate shadows)
:let [is-remove-disabled (= shadows-count 1)
key (str shadows-count shadow-idx)
errors-by-key (when composite-token?
(sd/collect-shadow-errors token-resolve-result shadow-idx))]]
[:div {:key key
:class (stl/css :nested-input-row)}
[:> shadow-input-fields*
{:is-remove-disabled is-remove-disabled
:shadow-idx shadow-idx
:on-add-shadow on-add-shadow
:on-remove-shadow on-remove-shadow
:shadow shadow
:on-update-value on-update-value
:token-resolve-result token-resolve-result
:errors-by-key errors-by-key
:on-external-update-value on-external-update-value
:shadow-color (get shadow-colors shadow-idx "")}]])]))
(mf/defc shadow-form*
[{:keys [token] :rest props}]
(let [on-get-token-value
(mf/use-callback
(mf/use-fn
(fn [e prev-composite-value]
(let [prev-composite-value (or prev-composite-value [])
[idx token-type :as token-type-at-index] (obj/get e "tokenTypeAtIndex")
@@ -1194,7 +744,7 @@
:else (assoc-in prev-composite-value token-type-at-index input-value)))))
update-composite-backup-value
(mf/use-callback
(mf/use-fn
(fn [prev-composite-value e]
(let [[idx token-type :as token-type-at-index] (obj/get e "tokenTypeAtIndex")
token-value (case token-type
@@ -1206,7 +756,19 @@
(if valid?
(assoc-in (or prev-composite-value []) token-type-at-index token-value)
;; Remove empty values so they don't retrigger validation when switching tabs
(update prev-composite-value idx dissoc token-type)))))]
(update prev-composite-value idx dissoc token-type)))))
on-add-shadow
#(prn "Add shadow clicked")
;; (mf/use-fn
;; (mf/deps shadows update-composite-value)
;; (fn []
;; (update-composite-backup-value
;; (fn [state]
;; (let [new-state (update state :composite (fnil conj []) {})]
;; (reset! shadows* (:composite new-state))
;; new-state)))))
]
[:> composite-form*
(mf/spread-props props {:token token
:composite-tab shadow-value-inputs*
@@ -1214,88 +776,11 @@
:is-reference-fn cto/typography-composite-token-reference?
:title (tr "workspace.tokens.shadow-title")
:validate-token validate-shadow-token
:type :shadow
:on-add-shadow on-add-shadow
:on-get-token-value on-get-token-value
:update-composite-backup-value update-composite-backup-value})]))
(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)
(cto/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-combobox*
[{:keys [default-value label aria-label input-ref on-blur on-update-value on-external-update-value token-resolve-result placeholder]}]
(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 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 i/arrow-down
:variant "action"
:type "button"}])]
[:*
[:> input-token*
{:placeholder (or placeholder (tr "workspace.tokens.token-font-family-value-enter"))
:label label
:aria-label aria-label
:default-value (or (:name font) default-value)
:ref input-ref
:on-blur on-blur
:on-change on-update-value'
:icon i/text-font-family
:slot-end font-selector-button
:token-resolve-result token-resolve-result}]
(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
@@ -1327,98 +812,6 @@
(mf/spread-props props {:token token
:input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")})])
(def ^:private typography-inputs
#(d/ordered-map
:font-family
{:label (tr "workspace.tokens.token-font-family-value")
:icon i/text-font-family
:placeholder (tr "workspace.tokens.token-font-family-value-enter")}
:font-size
{:label "Font Size"
:icon i/text-font-size
:placeholder (tr "workspace.tokens.font-size-value-enter")}
:font-weight
{:label "Font Weight"
:icon i/text-font-weight
:placeholder (tr "workspace.tokens.font-weight-value-enter")}
:line-height
{:label "Line Height"
:icon i/text-lineheight
:placeholder (tr "workspace.tokens.line-height-value-enter")}
:letter-spacing
{:label "Letter Spacing"
:icon i/text-letterspacing
:placeholder (tr "workspace.tokens.letter-spacing-value-enter-composite")}
:text-case
{:label "Text Case"
:icon i/text-mixed
:placeholder (tr "workspace.tokens.text-case-value-enter")}
:text-decoration
{:label "Text Decoration"
:icon i/text-underlined
:placeholder (tr "workspace.tokens.text-decoration-value-enter")}))
(mf/defc typography-value-inputs*
[{:keys [default-value on-blur on-update-value token-resolve-result]}]
(let [composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result)))
typography-inputs (mf/use-memo typography-inputs)
errors-by-key (sd/collect-typography-errors token-resolve-result)]
[:div {:class (stl/css :nested-input-row)}
(for [[token-type {:keys [label placeholder icon]}] typography-inputs]
(let [value (get default-value token-type)
resolved (get-in token-resolve-result [:resolved-value token-type])
errors (get errors-by-key token-type)
should-show? (or (and (some? resolved)
(not= value (str resolved)))
(seq errors))
token-prop (when (and composite-token? should-show?)
(d/without-nils
{:resolved-value (when-not (str/empty? resolved) resolved)
:errors errors}))
input-ref (mf/use-ref)
on-external-update-value
(mf/use-fn
(mf/deps on-update-value)
(fn [next-value]
(let [element (mf/ref-val input-ref)]
(dom/set-value! element next-value)
(on-update-value #js {:target element
:tokenType :font-family}))))
on-change
(mf/use-fn
(mf/deps token-type)
;; Passing token-type via event to prevent deep function adapting & passing of type
(fn [event]
(-> (obj/set! event "tokenType" token-type)
(on-update-value))))]
[:div {:key (str token-type)
:class (stl/css :input-row)}
(case token-type
:font-family
[:> font-picker-combobox*
{:aria-label label
:placeholder placeholder
:input-ref input-ref
:default-value (when value (cto/join-font-family value))
:on-blur on-blur
:on-update-value on-change
:on-external-update-value on-external-update-value
:token-resolve-result token-prop}]
[:> input-token*
{:aria-label label
:placeholder placeholder
:default-value value
:on-blur on-blur
:icon icon
:on-change on-change
:token-resolve-result token-prop}])]))]))
(mf/defc typography-form*
[{:keys [token] :rest props}]
(let [on-get-token-value

View File

@@ -41,34 +41,6 @@
gap: var(--sp-xs);
}
.nested-input-row {
display: flex;
flex-direction: column;
gap: var(--sp-m);
}
.typography-inputs-row {
display: flex;
flex-direction: column;
gap: var(--sp-m);
}
.typography-inputs {
border-inline-start: $b-1 solid var(--color-accent-primary-muted);
padding-inline-start: var(--sp-m);
}
.title-bar {
display: grid;
grid-template-columns: 1fr auto;
}
.title {
@include t.use-typography("body-small");
color: var(--color-foreground-primary);
display: flex;
align-items: center;
}
.warning-name-change-notification-wrapper {
margin-block-start: var(--sp-l);
}

View File

@@ -0,0 +1,301 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.workspace.tokens.management.create.shadow
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.types.token :as cto]
[app.main.data.style-dictionary :as sd]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token*]]
[app.main.ui.workspace.tokens.management.create.shared.color-picker :refer [color-picker*]]
[app.util.i18n :refer [tr]]
[app.util.object :as obj]
[rumext.v2 :as mf]))
(mf/defc inset-type-select*
{::mf/private true}
[{:keys [default-value on-change]}]
(let [selected* (mf/use-state (or (str default-value) "false"))
selected (deref selected*)
on-change
(mf/use-fn
(mf/deps on-change selected)
(fn [value evento]
(.log js/console (clj->js value))
(.log js/console (clj->js evento))
(obj/set! evento "tokenValue" (if (= "true" value) true false))
(on-change evento)
(reset! selected* (str value))))
options
(mf/with-memo []
[{:id "false" :label "drop-shadow" :icon i/drop-shadow}
{:id "true" :label "inner-shadow" :icon i/inner-shadow}])]
[:div {:class (stl/css :input-row)}
[:> select* {:default-selected selected
:variant "ghost"
:options options
:on-change on-change}]]))
#_(mf/defc inset-type-select*
[{:keys [default-value shadow-idx label on-change]}]
(let [selected* (mf/use-state (or (str default-value) "false"))
selected (deref selected*)
on-change
(mf/use-fn
(mf/deps on-change selected shadow-idx)
(fn [value e]
(.log js/console (clj->js value))
(.log js/console (clj->js e))
(obj/set! e "tokenValue" (if (= "true" value) true false))
(on-change e)
(reset! selected* (str value))))]
[:div {:class (stl/css :input-row)}
[:div {:class (stl/css :inset-label)} label]
[:& radio-buttons {:selected selected
:on-change on-change
:name (str "inset-select-" shadow-idx)}
[:& radio-button {:value "false"
:title "false"
:icon "❌"
:id (str "inset-default-" shadow-idx)}]
[:& radio-button {:value "true"
:title "true"
:icon "✅"
:id (str "inset-false-" shadow-idx)}]]]))
(def ^:private shadow-inputs
#(d/ordered-map
:inset
{:label (tr "workspace.tokens.shadow-inset")
:placeholder (tr "workspace.tokens.shadow-inset")}
:color
{:label (tr "workspace.tokens.shadow-color")
:placeholder (tr "workspace.tokens.shadow-color")}
:offsetX
{:label (tr "workspace.tokens.shadow-x")
:placeholder (tr "workspace.tokens.shadow-x")}
:offsetY
{:label (tr "workspace.tokens.shadow-y")
:placeholder (tr "workspace.tokens.shadow-y")}
:blur
{:label (tr "workspace.tokens.shadow-blur")
:placeholder (tr "workspace.tokens.shadow-blur")}
:spread
{:label (tr "workspace.tokens.shadow-spread")
:placeholder (tr "workspace.tokens.shadow-spread")}))
(def ^:private input-icon
{:offsetX i/character-x
:offsetY i/character-y})
(mf/defc shadow-color-picker-wrapper*
"Wrapper for color-picker* that passes shadow color state from parent.
Similar to color-form* but receives color state from shadow-value-inputs*."
{::mf/private true}
[{:keys [placeholder label default-value input-ref on-update-value on-external-update-value token-resolve-result shadow-color]}]
(let [;; Use the color state passed from parent (shadow-value-inputs*)
resolved-color (get token-resolve-result :resolved-value)
color (or shadow-color resolved-color default-value "")
custom-input-token-value-props
(mf/use-memo
(mf/deps color)
(fn []
{:color color}))]
[:> color-picker*
{:placeholder placeholder
:label label
:default-value default-value
:input-ref input-ref
:on-update-value on-update-value
:on-external-update-value on-external-update-value
:custom-input-token-value-props custom-input-token-value-props
:token-resolve-result token-resolve-result}]))
(mf/defc shadow-input*
{::mf/private true}
[{:keys [default-value label placeholder shadow-idx input-type on-update-value on-external-update-value token-resolve-result errors-by-key shadow-color]}]
(let [color-input-ref (mf/use-ref)
on-change
(mf/use-fn
(mf/deps shadow-idx input-type on-update-value)
(fn [evento]
(-> (obj/set! evento "tokenTypeAtIndex" [shadow-idx input-type])
(on-update-value))))
on-external-update-value'
(mf/use-fn
(mf/deps shadow-idx input-type on-external-update-value)
(fn [v]
(on-external-update-value [shadow-idx input-type] v)))
resolved (get-in token-resolve-result [:resolved-value shadow-idx input-type])
errors (get errors-by-key input-type)
should-show? (or (some? resolved) (seq errors))
token-prop (when should-show?
(d/without-nils
{:resolved-value resolved
:errors errors}))]
(case input-type
:inset
[:> inset-type-select*
{:default-value default-value
:shadow-idx shadow-idx
:label label
:on-change on-change}]
:color
[:> shadow-color-picker-wrapper*
{:placeholder placeholder
:aria-label label
:default-value default-value
:input-ref color-input-ref
:on-update-value on-change
:on-external-update-value on-external-update-value'
:token-resolve-result token-prop
:shadow-color (or shadow-color nil)
:data-testid (str "shadow-color-input-" shadow-idx)}]
[:div {:class (stl/css :input-row)
:data-testid (str "shadow-" (name input-type) "-input-" shadow-idx)}
[:> input-token*
{:aria-label label
:icon (get input-icon input-type)
:placeholder placeholder
:default-value default-value
:on-change on-change
:slot-start (cond (= input-type :blur)
(mf/html [:span {:class (stl/css :shadow-prop-label)} "Blur"])
(= input-type :spread)
(mf/html [:span {:class (stl/css :shadow-prop-label)} "Spread"]))
:token-resolve-result token-prop}]])))
(mf/defc shadow-input-fields*
{::mf/private true}
[{:keys [shadow shadow-idx on-remove-shadow on-add-shadow is-remove-disabled on-update-value token-resolve-result errors-by-key on-external-update-value shadow-color] :as props}]
(let [on-remove-shadow
(mf/use-fn
(mf/deps shadow-idx on-remove-shadow)
#(on-remove-shadow shadow-idx))]
[:div {:data-testid (str "shadow-input-fields-" shadow-idx)
:class (stl/css :shadow-input-fields)}
[:> icon-button* {:icon i/add
:type "button"
:on-click on-add-shadow
:data-testid (str "shadow-add-button-" shadow-idx)
:aria-label (tr "workspace.tokens.shadow-add-shadow")}]
[:> icon-button* {:variant "ghost"
:type "button"
:icon i/remove
:on-click on-remove-shadow
:disabled is-remove-disabled
:data-testid (str "shadow-remove-button-" shadow-idx)
:aria-label (tr "workspace.tokens.shadow-remove-shadow")}]
(for [[input-type {:keys [label placeholder]}] (shadow-inputs)]
[:> shadow-input*
{:key (str input-type shadow-idx)
:input-type input-type
:label label
:placeholder placeholder
:shadow-idx shadow-idx
:default-value (get shadow input-type)
:on-update-value on-update-value
:token-resolve-result token-resolve-result
:errors-by-key errors-by-key
:on-external-update-value on-external-update-value
:shadow-color shadow-color}])]))
(mf/defc shadow-value-inputs*
[{:keys [default-value on-update-value token-resolve-result update-composite-value] :as props}]
(let [shadows* (mf/use-state (or default-value [{}]))
shadows (deref shadows*)
shadows-count (count shadows)
composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result)))
;; Maintain a map of color states for each shadow to prevent reset on add/remove
shadow-colors* (mf/use-state {})
shadow-colors (deref shadow-colors*)
;; Define on-external-update-value here where we have access to on-update-value
on-external-update-value
(mf/use-fn
(mf/deps on-update-value shadow-colors*)
(fn [token-type-at-index value]
(let [[idx token-type] token-type-at-index
e (js-obj)]
;; Update shadow color state if this is a color update
(when (= token-type :color)
(swap! shadow-colors* assoc idx value))
(obj/set! e "tokenTypeAtIndex" token-type-at-index)
(obj/set! e "target" #js {:value value})
(on-update-value e))))
on-add-shadow
(mf/use-fn
(mf/deps shadows update-composite-value)
(fn []
(update-composite-value
(fn [state]
(let [new-state (update state :composite (fnil conj []) {})]
(reset! shadows* (:composite new-state))
new-state)))))
on-remove-shadow
(mf/use-fn
(mf/deps shadows update-composite-value)
(fn [idx]
(update-composite-value
(fn [state]
(let [new-state (update state :composite d/remove-at-index idx)]
(reset! shadows* (:composite new-state))
new-state)))))]
(mf/use-effect
(mf/deps shadows)
(fn []
(doseq [[idx shadow] (d/enumerate shadows)]
(when-not (contains? shadow-colors idx)
(let [resolved-color (get-in token-resolve-result [:resolved-value idx :color])
initial-color (or resolved-color (get shadow :color) "")]
(swap! shadow-colors* assoc idx initial-color))))))
[:div {:class (stl/css :nested-input-row)}
(for [[shadow-idx shadow] (d/enumerate shadows)
:let [is-remove-disabled (= shadows-count 1)
key (str shadows-count shadow-idx)
errors-by-key (when composite-token?
(sd/collect-shadow-errors token-resolve-result shadow-idx))]]
[:div {:key key
:class (stl/css :nested-input-row)}
[:> shadow-input-fields*
{:is-remove-disabled is-remove-disabled
:shadow-idx shadow-idx
:on-add-shadow on-add-shadow
:on-remove-shadow on-remove-shadow
:shadow shadow
:on-update-value on-update-value
:token-resolve-result token-resolve-result
:errors-by-key errors-by-key
:on-external-update-value on-external-update-value
:shadow-color (get shadow-colors shadow-idx "")}]])]))

View File

@@ -0,0 +1,23 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
.input-row {
display: flex;
flex-direction: column;
gap: var(--sp-xs);
}
.nested-input-row {
display: flex;
flex-direction: column;
gap: var(--sp-m);
}
.shadow-input-fields {
display: flex;
flex-direction: column;
gap: var(--sp-m);
}

View File

@@ -0,0 +1,129 @@
(ns app.main.ui.workspace.tokens.management.create.shared.color-picker
(:require
[app.common.types.color :as c]
[app.main.data.tinycolor :as tinycolor]
[app.main.ui.workspace.colorpicker :as colorpicker]
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-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-token* token-value-hint*]]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
;; 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 on-blur on-update-value on-external-update-value custom-input-token-value-props token-resolve-result]}]
(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*)
_ (.log js/console (clj->js token-resolve-result))
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?)
(when on-display-colorpicker
(on-display-colorpicker open?)))))
swatch
(mf/html
[:> input-token-color-bullet*
{:color color
: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))]
(dom/set-value! (mf/ref-val input-ref) color-value)
(on-external-update-value color-value))))]
[:*
[:> input-token*
{:placeholder placeholder
:label label
:default-value default-value
:ref input-ref
: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'}])
(when token-resolve-result
[:> token-value-hint* {:result token-resolve-result}])]))

View File

@@ -0,0 +1,115 @@
(ns app.main.ui.workspace.tokens.management.create.shared.composite-tabs
(:require-macros [app.main.style :as stl])
(:require
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc composite-reference-input*
{::mf/private true}
[{:keys [default-value on-blur on-update-value token-resolve-result reference-label reference-icon is-reference-fn]}]
[:> input-token*
{:aria-label (tr "labels.reference")
:placeholder reference-label
:icon reference-icon
:default-value (when (is-reference-fn default-value) default-value)
:on-blur on-blur
:on-change on-update-value
:token-resolve-result (when (or
(:errors token-resolve-result)
(string? (:value token-resolve-result)))
token-resolve-result)}])
(mf/defc composite-tabs*
[{:keys [default-value
on-update-value
on-external-update-value
on-value-resolve
clear-resolve-value
custom-input-token-value-props]
:rest props}]
(let [;; Active Tab State
{:keys [active-tab
composite-tab
is-reference-fn
reference-icon
reference-label
set-active-tab
title
type
on-add-shadow
update-composite-backup-value]} custom-input-token-value-props
reference-tab-active? (= :reference active-tab)
;; Backup value ref
;; Used to restore the previously entered value when switching tabs
;; Uses ref to not trigger state updates during update
backup-state-ref (mf/use-var
(if reference-tab-active?
{:reference default-value}
{:composite default-value}))
default-value (get @backup-state-ref active-tab)
on-toggle-tab
(mf/use-fn
(mf/deps active-tab on-external-update-value on-value-resolve clear-resolve-value)
(fn []
(let [next-tab (if (= active-tab :composite) :reference :composite)]
;; Clear the resolved value so it wont show up before the next-tab value has resolved
(clear-resolve-value)
;; Restore the internal value from backup
(on-external-update-value (get @backup-state-ref next-tab))
(set-active-tab next-tab))))
update-composite-value
(mf/use-fn
(fn [f]
(clear-resolve-value)
(swap! backup-state-ref f)
(on-external-update-value (get @backup-state-ref :composite))))
;; Store updated value in backup-state-ref
on-update-value'
(mf/use-fn
(mf/deps on-update-value reference-tab-active? update-composite-backup-value)
(fn [e]
(if reference-tab-active?
(swap! backup-state-ref assoc :reference (dom/get-target-val e))
(swap! backup-state-ref update :composite #(update-composite-backup-value % e)))
(on-update-value e)))]
[:div {:class (stl/css :typography-inputs-row)}
[:div {:class (stl/css :title-bar)}
[:div {:class (stl/css :title)} title]
[:& radio-buttons {:selected (if reference-tab-active? "reference" "composite")
:on-change on-toggle-tab
:name "reference-composite-tab"}
[:& radio-button {:icon deprecated-icon/layers
:value "composite"
:title (tr "workspace.tokens.individual-tokens")
:id "composite-opt"}]
[:& radio-button {:icon deprecated-icon/tokens
:value "reference"
:title (tr "workspace.tokens.use-reference")
:id "reference-opt"}]]
(when (= type :shadow)
[:> icon-button* {:icon i/add
:type "button"
:on-click on-add-shadow
:data-testid "shadow-add-button"
:aria-label (tr "workspace.tokens.shadow-add-shadow")}])]
[:div {:class (stl/css :typography-inputs)}
(if reference-tab-active?
[:> composite-reference-input*
(mf/spread-props props {:default-value default-value
:on-update-value on-update-value'
:reference-icon reference-icon
:reference-label reference-label
:is-reference-fn is-reference-fn})]
[:> composite-tab
(mf/spread-props props {:default-value default-value
:on-update-value on-update-value'
:update-composite-value update-composite-value})])]]))

View File

@@ -0,0 +1,31 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *;
.typography-inputs-row {
display: flex;
flex-direction: column;
gap: var(--sp-m);
}
.title-bar {
display: grid;
grid-template-columns: 1fr auto;
}
.title {
@include t.use-typography("body-small");
color: var(--color-foreground-primary);
display: flex;
align-items: center;
}
.typography-inputs {
border-inline-start: $b-1 solid var(--color-accent-primary-muted);
padding-inline-start: var(--sp-m);
}

View File

@@ -0,0 +1,92 @@
(ns app.main.ui.workspace.tokens.management.create.shared.font-combobox
(:require-macros [app.main.style :as stl])
(:require
[app.common.types.token :as cto]
[app.main.fonts :as fonts]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]]
[app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc font-selector-wrapper*
{::mf/private true}
[{: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)
(cto/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-combobox*
[{:keys [default-value label aria-label input-ref on-blur on-update-value on-external-update-value token-resolve-result placeholder]}]
(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 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 i/arrow-down
:variant "action"
:type "button"}])]
[:*
[:> input-token*
{:placeholder (or placeholder (tr "workspace.tokens.token-font-family-value-enter"))
:label label
:aria-label aria-label
:default-value (or (:name font) default-value)
:ref input-ref
:on-blur on-blur
:on-change on-update-value'
:icon i/text-font-family
:slot-end font-selector-button
:token-resolve-result token-resolve-result}]
(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}])]))

View File

@@ -0,0 +1,13 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
.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: var(--sp-xxxl);
}

View File

@@ -0,0 +1,107 @@
(ns app.main.ui.workspace.tokens.management.create.typography-composite
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.types.token :as cto]
[app.main.data.style-dictionary :as sd]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token*]]
[app.main.ui.workspace.tokens.management.create.shared.font-combobox :refer [font-picker-combobox*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.object :as obj]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(def ^:private typography-inputs
#(d/ordered-map
:font-family
{:label (tr "workspace.tokens.token-font-family-value")
:icon i/text-font-family
:placeholder (tr "workspace.tokens.token-font-family-value-enter")}
:font-size
{:label "Font Size"
:icon i/text-font-size
:placeholder (tr "workspace.tokens.font-size-value-enter")}
:font-weight
{:label "Font Weight"
:icon i/text-font-weight
:placeholder (tr "workspace.tokens.font-weight-value-enter")}
:line-height
{:label "Line Height"
:icon i/text-lineheight
:placeholder (tr "workspace.tokens.line-height-value-enter")}
:letter-spacing
{:label "Letter Spacing"
:icon i/text-letterspacing
:placeholder (tr "workspace.tokens.letter-spacing-value-enter-composite")}
:text-case
{:label "Text Case"
:icon i/text-mixed
:placeholder (tr "workspace.tokens.text-case-value-enter")}
:text-decoration
{:label "Text Decoration"
:icon i/text-underlined
:placeholder (tr "workspace.tokens.text-decoration-value-enter")}))
(mf/defc typography-value-inputs*
[{:keys [default-value on-blur on-update-value token-resolve-result]}]
(let [composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result)))
typography-inputs (mf/use-memo typography-inputs)
errors-by-key (sd/collect-typography-errors token-resolve-result)]
[:div {:class (stl/css :nested-input-row)}
(for [[token-type {:keys [label placeholder icon]}] typography-inputs]
(let [value (get default-value token-type)
resolved (get-in token-resolve-result [:resolved-value token-type])
errors (get errors-by-key token-type)
should-show? (or (and (some? resolved)
(not= value (str resolved)))
(seq errors))
token-prop (when (and composite-token? should-show?)
(d/without-nils
{:resolved-value (when-not (str/empty? resolved) resolved)
:errors errors}))
input-ref (mf/use-ref)
on-external-update-value
(mf/use-fn
(mf/deps on-update-value)
(fn [next-value]
(let [element (mf/ref-val input-ref)]
(dom/set-value! element next-value)
(on-update-value #js {:target element
:tokenType :font-family}))))
on-change
(mf/use-fn
(mf/deps token-type)
;; Passing token-type via event to prevent deep function adapting & passing of type
(fn [event]
(-> (obj/set! event "tokenType" token-type)
(on-update-value))))]
[:div {:key (str token-type)
:class (stl/css :input-row)}
(case token-type
:font-family
[:> font-picker-combobox*
{:aria-label label
:placeholder placeholder
:input-ref input-ref
:default-value (when value (cto/join-font-family value))
:on-blur on-blur
:on-update-value on-change
:on-external-update-value on-external-update-value
:token-resolve-result token-prop}]
[:> input-token*
{:aria-label label
:placeholder placeholder
:default-value value
:on-blur on-blur
:icon icon
:on-change on-change
:token-resolve-result token-prop}])]))]))

View File

@@ -0,0 +1,17 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
.input-row {
display: flex;
flex-direction: column;
gap: var(--sp-xs);
}
.nested-input-row {
display: flex;
flex-direction: column;
gap: var(--sp-m);
}

View File

@@ -38,6 +38,7 @@
:opacity "percentage"
:number "number"
:rotation "rotation"
:shadow "drop-shadow"
:spacing "padding-extended"
:string "text-mixed"
:stroke-width "stroke-size"