♻️ Design review for numeric inputs (#8630)

* ♻️ Update tooltip position on icon buttons

* ♻️ Sort token groups by priority not alphabetically

* ♻️ Add proper padding on text-icon-inputs

* ♻️ Hide detach button when dropdown is open

* 🐛 Fix detach stroke width

* 🐛 Fix strokes applied on all rows

* 🐛 Fix nillable inputs

* 🐛 Fix comments on PR
This commit is contained in:
Eva Marco
2026-03-19 16:46:18 +01:00
committed by GitHub
parent b876417d5b
commit 8e7e6ffc2f
15 changed files with 131 additions and 200 deletions

View File

@@ -522,31 +522,31 @@
(def tokens-by-input
"A map from input name to applicable token for that input."
{:width #{:sizing :dimensions}
:height #{:sizing :dimensions}
:max-width #{:sizing :dimensions}
:max-height #{:sizing :dimensions}
:min-width #{:sizing :dimensions}
:min-height #{:sizing :dimensions}
:x #{:dimensions}
:y #{:dimensions}
:rotation #{:number :rotation}
:border-radius #{:border-radius :dimensions}
:row-gap #{:spacing :dimensions}
:column-gap #{:spacing :dimensions}
:horizontal-padding #{:spacing :dimensions}
:vertical-padding #{:spacing :dimensions}
:sided-paddings #{:spacing :dimensions}
:horizontal-margin #{:spacing :dimensions}
:vertical-margin #{:spacing :dimensions}
:sided-margins #{:spacing :dimensions}
:line-height #{:line-height :number}
:opacity #{:opacity}
:stroke-width #{:stroke-width :dimensions}
:font-size #{:font-size}
:letter-spacing #{:letter-spacing}
:fill #{:color}
:stroke-color #{:color}})
{:width [:sizing :dimensions]
:height [:sizing :dimensions]
:max-width [:sizing :dimensions]
:max-height [:sizing :dimensions]
:min-width [:sizing :dimensions]
:min-height [:sizing :dimensions]
:x [:dimensions]
:y [:dimensions]
:rotation [:rotation :number]
:border-radius [:border-radius :dimensions]
:row-gap [:spacing :dimensions]
:column-gap [:spacing :dimensions]
:horizontal-padding [:spacing :dimensions]
:vertical-padding [:spacing :dimensions]
:sided-paddings [:spacing :dimensions]
:horizontal-margin [:spacing :dimensions]
:vertical-margin [:spacing :dimensions]
:sided-margins [:spacing :dimensions]
:line-height [:line-height :number]
:opacity [:opacity]
:stroke-width [:stroke-width :dimensions]
:font-size [:font-size]
:letter-spacing [:letter-spacing]
:fill [:color]
:stroke-color [:color]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for tokens application

View File

@@ -19,6 +19,7 @@
[app.main.ui.ds.controls.utilities.token-field :refer [token-field*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i]
[app.main.ui.formats :as fmt]
[app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
@@ -83,48 +84,6 @@
(str/replace #"^\{" "")
(str/replace #"\}$" "")))
(defn- token->dropdown-option
[token]
{:id (str (get token :id))
:type :token
:resolved-value (get token :resolved-value)
:name (get token :name)})
(defn- generate-dropdown-options
[tokens no-sets]
(if (empty? tokens)
[{:type :empty
:label (if no-sets
(tr "ds.inputs.numeric-input.no-applicable-tokens")
(tr "ds.inputs.numeric-input.no-matches"))}]
(->> tokens
(map (fn [[type items]]
(cons {:group true
:type :group
:id (dm/str "group-" (name type))
:name (name type)}
(map token->dropdown-option items))))
(interpose [{:separator true
:id "separator"
:type :separator}])
(apply concat)
(vec)
(not-empty))))
(defn- extract-partial-brace-text
[s]
(when-let [start (str/last-index-of s "{")]
(subs s (inc start))))
(defn- filter-token-groups-by-name
[tokens filter-text]
(let [lc-filter (str/lower filter-text)]
(into {}
(keep (fn [[group tokens]]
(let [filtered (filter #(str/includes? (str/lower (:name %)) lc-filter) tokens)]
(when (seq filtered)
[group filtered]))))
tokens)))
(defn- focusable-option?
[option]
@@ -149,31 +108,6 @@
j)))
indices)))
(defn- sort-groups-and-tokens
"Sorts both the groups and the tokens inside them alphabetically.
Input:
A map where:
- keys are groups (keywords or strings, e.g. :dimensions, :colors)
- values are vectors of token maps, each containing at least a :name key
Example input:
{:dimensions [{:name \"tres\"} {:name \"quini\"}]
:colors [{:name \"azul\"} {:name \"rojo\"}]}
Output:
A sorted map where:
- groups are ordered alphabetically by key
- tokens inside each group are sorted alphabetically by :name
Example output:
{:colors [{:name \"azul\"} {:name \"rojo\"}]
:dimensions [{:name \"quini\"} {:name \"tres\"}]}"
[groups->tokens]
(into (sorted-map) ;; ensure groups are ordered alphabetically by their key
(for [[group tokens] groups->tokens]
[group (sort-by :name tokens)])))
(def ^:private schema:icon
[:and :string [:fn #(contains? icon-list %)]])
@@ -288,16 +222,7 @@
dropdown-options
(mf/with-memo [tokens filter-id]
(delay
(let [tokens (if (delay? tokens) @tokens tokens)
sorted-tokens (sort-groups-and-tokens tokens)
partial (extract-partial-brace-text filter-id)
options (if (seq partial)
(filter-token-groups-by-name sorted-tokens partial)
sorted-tokens)
no-sets? (nil? sorted-tokens)]
(generate-dropdown-options options no-sets?))))
(csu/get-token-dropdown-options tokens filter-id))
selected-id*
(mf/use-state (fn []
@@ -649,6 +574,7 @@
:icon i/tokens
:tooltip-class (stl/css :button-tooltip)
:class (stl/css :invisible-button)
:tooltip-placement "top-left"
:aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
:ref open-dropdown-ref
:on-click open-dropdown}])))
@@ -676,6 +602,7 @@
:on-blur on-blur
:class inner-class
:property property
:is-open is-open
:slot-start (when (or icon text-icon)
(mf/html
(cond
@@ -714,6 +641,11 @@
(when-let [node (mf/ref-val ref)]
(dom/set-value! node value'))))
(mf/with-effect [applied-token]
(when (nil? applied-token)
(reset! token-applied* nil)
(reset! selected-id* nil)))
(mf/with-layout-effect [on-mouse-wheel]
(when-let [node (mf/ref-val ref)]
(let [key (events/listen node "wheel" on-mouse-wheel #js {:passive false})]

View File

@@ -35,9 +35,10 @@
.text-icon {
color: var(--color-foreground-secondary);
@include t.use-typography("code-font");
@include t.use-typography("body-small");
inline-size: fit-content;
min-inline-size: px2rem(40);
min-inline-size: px2rem(46);
padding-inline-start: var(--sp-xs);
}
.invisible-button {

View File

@@ -25,6 +25,7 @@
[:property {:optional true} [:maybe :string]]
[:value :any]
[:disabled {:optional true} :boolean]
[:is-open {:optional true} :boolean]
[:slot-start {:optional true} [:maybe some?]]
[:on-click {:optional true} fn?]
[:on-token-key-down fn?]
@@ -36,7 +37,7 @@
{::mf/schema schema:token-field}
[{:keys [id label value slot-start disabled class
on-click on-token-key-down on-blur detach-token
token-wrapper-ref token-detach-btn-ref on-focus property]}]
token-wrapper-ref token-detach-btn-ref on-focus property is-open]}]
(let [set-active? (some? id)
content (if set-active?
label
@@ -88,9 +89,11 @@
(when-not ^boolean disabled
[:> icon-button* {:variant "ghost"
:class (stl/css :invisible-button)
:class (stl/css-case :invisible-button true
:invisible-btn-dropdown-open is-open)
:tooltip-class (stl/css :button-tooltip)
:icon i/broken-link
:ref token-detach-btn-ref
:tooltip-placement "top-left"
:aria-label (tr "ds.inputs.token-field.detach-token")
:on-click detach-token}])]]))

View File

@@ -136,6 +136,9 @@
--opacity-button: 1;
}
}
.invisible-btn-dropdown-open {
--opacity-button: 0;
}
.content-wrapper {
inline-size: 100%;

View File

@@ -172,7 +172,7 @@
(deref placement*)
delay
(d/nilv delay 300)
(d/nilv delay 700)
schedule-ref
(mf/use-ref nil)
@@ -188,7 +188,7 @@
(when-not (.-hidden js/document)
(let [trigger-el (mf/ref-val trigger-ref)]
(clear-schedule schedule-ref)
(add-schedule schedule-ref (d/nilv delay 300)
(add-schedule schedule-ref delay
(fn []
(when-let [active @active-tooltip]
(when (not= (:id active) tooltip-id)

View File

@@ -7,6 +7,8 @@
(ns app.main.ui.workspace.sidebar.options.common
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.workspace.tokens.application :as dwta]
[app.main.store :as st]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
@@ -24,3 +26,17 @@
:ref ref}
children])))
(defn emit-value-or-token [value emit-value-fn ids attrs]
(cond
(nil? value)
(emit-value-fn nil)
(or (string? value) (number? value))
(emit-value-fn value)
:else
(st/emit!
(dwta/toggle-token {:token (first value)
:attrs attrs
:shape-ids ids}))))

View File

@@ -11,6 +11,7 @@
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as hooks]
[app.main.ui.workspace.sidebar.options.common :as soc]
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
[app.util.i18n :as i18n :refer [tr]]
[beicon.v2.core :as rx]
@@ -130,26 +131,21 @@
(mf/use-fn
(mf/deps change-radius ids)
(fn [value]
(if (or (string? value) (number? value))
(st/emit!
(change-radius (fn [shape]
(ctsr/set-radius-to-all-corners shape value))))
(st/emit!
(dwta/toggle-token {:token (first value)
:attrs #{:r1 :r2 :r3 :r4}
:shape-ids ids})))))
(soc/emit-value-or-token
value
#(st/emit! (change-radius (fn [shape] (ctsr/set-radius-to-all-corners shape %))))
ids
#{:r1 :r2 :r3 :r4})))
on-single-radius-change
(mf/use-fn
(mf/deps change-one-radius ids)
(fn [value attr]
(if (or (string? value) (number? value))
(st/emit! (change-one-radius #(ctsr/set-radius-to-single-corner % attr value) attr))
(st/emit! (st/emit!
(dwta/toggle-token {:token (first value)
:attrs #{attr}
:shape-ids ids}))))))
(soc/emit-value-or-token
value
#(st/emit! (change-one-radius (fn [shape] (ctsr/set-radius-to-single-corner shape attr %)) attr))
ids
#{attr})))
on-radius-r1-change #(on-single-radius-change % :r1)
on-radius-r2-change #(on-single-radius-change % :r2)

View File

@@ -1,9 +1,9 @@
(ns app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens
(:require-macros [app.main.style :as stl])
(:require
[app.common.types.token :as tk]
[app.main.ui.context :as muc]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
[app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
@@ -11,11 +11,8 @@
[{:keys [value attr applied-token align on-detach placeholder input-type class] :rest props}]
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
tokens (mf/with-memo [tokens input-type]
(delay
(-> (deref tokens)
(select-keys (get tk/tokens-by-input (or input-type attr)))
(not-empty))))
tokens (mf/with-memo [tokens input-type attr]
(csu/filter-tokens-for-input tokens (or input-type attr)))
on-detach-attr
(mf/use-fn

View File

@@ -30,6 +30,7 @@
[app.main.ui.formats :as fmt]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.sidebar.options.common :as soc]
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -335,15 +336,8 @@
(mf/use-fn
(mf/deps on-change ids)
(fn [value attr event]
(if (or (string? value) (number? value))
(on-change :simple attr value event)
(do
(st/emit!
(dwta/toggle-token {:token (first value)
:attrs (if (= :p1 attr)
#{:p1 :p3}
#{:p2 :p4})
:shape-ids ids}))))))
(let [on-change-fn #(on-change :simple attr % event)]
(soc/emit-value-or-token value on-change-fn ids #{attr}))))
on-detach-token
(mf/use-fn
@@ -719,15 +713,8 @@
(mf/use-fn
(mf/deps on-change wrap-type ids)
(fn [value event attr]
(if (or (string? value) (number? value))
(on-change (= "nowrap" wrap-type) attr value event)
(do
(st/emit!
(dwta/toggle-token {:token (first value)
:attrs (if (= "nowrap" wrap-type)
#{:row-gap :colum-gap}
#{attr})
:shape-ids ids}))))))
(let [on-change-fn #((on-change (= "nowrap" wrap-type) attr % event))]
(soc/emit-value-or-token value on-change-fn ids #{attr}))))
on-detach-token
(mf/use-fn

View File

@@ -21,6 +21,7 @@
[app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.sidebar.options.common :as soc]
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
[app.main.ui.workspace.sidebar.options.menus.layout-container :refer [get-layout-flex-icon]]
[app.util.dom :as dom]
@@ -117,15 +118,11 @@
(mf/use-fn
(mf/deps on-change ids)
(fn [value attr]
(if (or (string? value) (number? value))
(on-change :simple attr value)
(do
(st/emit!
(dwta/toggle-token {:token (first value)
:attrs (if (= :m1 attr)
#{:m1 :m3}
#{:m2 :m4})
:shape-ids ids}))))))
(soc/emit-value-or-token
value
#(on-change :simple attr %)
ids
(if (= :m1 attr) #{:m1 :m3} #{:m2 :m4}))))
on-focus-m1
(mf/use-fn (mf/deps on-focus) #(on-focus :m1))
@@ -247,14 +244,11 @@
(mf/use-fn
(mf/deps on-change ids)
(fn [value attr]
(if (or (string? value) (number? value))
(on-change :multiple attr value)
(do
(st/emit!
(dwta/toggle-token {:token (first value)
:attrs #{attr}
:shape-ids ids}))))))
(soc/emit-value-or-token
value
#(on-change :multiple attr %)
ids
#{attr})))
on-m1-change
(mf/use-fn (mf/deps on-change') #(on-change' % :m1))
@@ -577,13 +571,11 @@
(mf/use-fn
(mf/deps ids)
(fn [value attr]
(if (or (string? value) (number? value))
(st/emit! (dwsl/update-layout-child ids {attr value}))
(do
(st/emit!
(dwta/toggle-token {:token (first value)
:attrs #{attr}
:shape-ids ids}))))))
(soc/emit-value-or-token
value
#(st/emit! (dwsl/update-layout-child ids {attr %}))
ids
#{attr})))
on-layout-item-min-w-change
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-min-w))

View File

@@ -168,6 +168,7 @@
on-blur (fn [_]
(reset! disable-drag false))
on-detach-token
(mf/use-fn
(mf/deps ids)
@@ -205,7 +206,7 @@
(seq strokes)
[:> h/sortable-container* {}
(for [[index value] (d/enumerate (:strokes values []))]
[:> stroke-row* {:key (dm/str "stroke-" index)
[:> stroke-row* {:key (dm/str "stroke-" index "-" (hash applied-tokens))
:stroke value
:title (tr "workspace.options.stroke-color")
:index index
@@ -222,7 +223,7 @@
:on-stroke-cap-start-change on-stroke-cap-start-change
:on-stroke-cap-end-change on-stroke-cap-end-change
:on-stroke-cap-switch on-stroke-cap-switch
:applied-tokens applied-tokens
:applied-tokens (when (= 0 index) applied-tokens)
:on-detach-token on-detach-token
:on-remove on-remove
:on-reorder handle-reorder

View File

@@ -11,7 +11,6 @@
[app.common.data.macros :as dm]
[app.common.types.color :as clr]
[app.common.types.shape.attrs :refer [default-color]]
[app.common.types.token :as tk]
[app.config :as cfg]
[app.main.data.modal :as modal]
[app.main.data.workspace.colors :as dwc]
@@ -27,6 +26,7 @@
[app.main.ui.ds.utilities.swatch :refer [swatch*]]
[app.main.ui.formats :as fmt]
[app.main.ui.hooks :as h]
[app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
[app.util.color :as uc]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -176,12 +176,9 @@
active-tokens* (mf/use-ctx ctx/active-tokens-by-type)
tokens (mf/with-memo [active-tokens* origin]
(let [origin (if (= :color-selection origin) :fill origin)]
(delay
(-> (deref active-tokens*)
(select-keys (get tk/tokens-by-input origin))
(not-empty)))))
tokens (mf/with-memo [active-tokens* origin]
(csu/filter-tokens-for-input active-tokens* origin))
on-focus'
(mf/use-fn
(mf/deps on-focus)

View File

@@ -18,6 +18,7 @@
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as h]
[app.main.ui.workspace.sidebar.options.common :as soc]
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]]
[app.util.i18n :as i18n :refer [tr]]
@@ -92,14 +93,13 @@
on-width-change
(mf/use-fn
(mf/deps index on-stroke-width-change)
(mf/deps index on-stroke-width-change ids)
(fn [value]
(if (or (string? value) (number? value))
(on-stroke-width-change index value)
(st/emit! (dwta/toggle-token {:token (first value)
:attrs #{:stroke-width}
:shape-ids ids})))))
(soc/emit-value-or-token
value
#(on-stroke-width-change index %)
ids
#{:stroke-width})))
stroke-alignment (or (:stroke-alignment stroke) :center)
@@ -164,7 +164,7 @@
(mf/use-fn
(mf/deps on-detach-token)
(fn [token]
(on-detach-token (first token) #{:stroke-width})))
(on-detach-token token #{:stroke-width})))
stroke-caps-options
[{:value nil :label (tr "workspace.options.stroke-cap.none")}

View File

@@ -9,7 +9,7 @@
[token]
{:id (str (get token :id))
:type :token
:resolved-value (get token :value)
:resolved-value (get token :resolved-value)
:name (get token :name)})
(defn- generate-dropdown-options
@@ -53,7 +53,7 @@
tokens)))
(defn- sort-groups-and-tokens
"Sorts both the groups and the tokens inside them alphabetically.
"Sorts the tokens inside the groups alphabetically.
Input:
A map where:
@@ -65,18 +65,18 @@
:colors [{:name \"azul\"} {:name \"rojo\"}]}
Output:
A sorted map where:
- groups are ordered alphabetically by key
A map which:
- tokens inside each group are sorted alphabetically by :name
Example output:
{:colors [{:name \"azul\"} {:name \"rojo\"}]
:dimensions [{:name \"quini\"} {:name \"tres\"}]}"
{:dimensions [{:name \"quini\"} {:name \"tres\"}]
:colors [{:name \"azul\"} {:name \"rojo\"}]}"
[groups->tokens]
(into (sorted-map) ;; ensure groups are ordered alphabetically by their key
(for [[group tokens] groups->tokens]
[group (sort-by :name tokens)])))
(reduce (fn [acc [group tokens]]
(assoc acc group (sort-by :name tokens)))
{}
groups->tokens))
(defn get-token-dropdown-options
[tokens filter-term]
@@ -94,9 +94,15 @@
(defn filter-tokens-for-input
[raw-tokens input-type]
(delay
(-> (deref raw-tokens)
(select-keys (get cto/tokens-by-input input-type))
(not-empty))))
(let [raw-tokens (deref raw-tokens)
key-order (get cto/tokens-by-input input-type)]
(-> (reduce (fn [acc k]
(if (contains? raw-tokens k)
(assoc acc k (get raw-tokens k))
acc))
(array-map)
key-order)
(not-empty)))))
(defn focusable-options [options]
(filter #(= (:type %) :token) options))