🎉 Add numeric-input component to DS

A new numeric-input impl compatible with tokens
This commit is contained in:
Eva Marco 2025-08-25 12:09:14 +02:00 committed by Andrey Antukh
parent 79786dde16
commit 33fc578f96
26 changed files with 1510 additions and 149 deletions

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
<path d="M12.994 5.422v5.157a.334.334 0 0 1-.172.29l-4.66 2.59a.333.333 0 0 1-.323 0l-4.661-2.59a.334.334 0 0 1-.172-.29V5.421c0-.121.066-.233.172-.292l4.66-2.588a.333.333 0 0 1 .323 0l4.661 2.588a.335.335 0 0 1 .172.293ZM8 8.5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1Z" />
</svg>

After

Width:  |  Height:  |  Size: 380 B

View File

@ -214,7 +214,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [data (dsh/lookup-file-data state)
tlib (get-tokens-lib state)
tlib (get-tokens-lib state)
changes (-> (pcb/empty-changes)
(pcb/with-library-data data)
(clt/generate-toggle-token-set tlib name))]
@ -351,7 +351,6 @@
(:id token)
token))]
(js/console.log "Creating token" (clj->js changes))
(rx/of (dch/commit-changes changes)
(ptk/data-event ::ev/event {::ev/name "create-token" :type token-type})))

View File

@ -11,6 +11,7 @@
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.combobox :refer [combobox*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
@ -66,6 +67,7 @@
:Tooltip tooltip*
:ContextNotification context-notification*
:NotificationPill notification-pill*
:NumericInput numeric-input*
:Actionable actionable*
:TokenStatusIcon token-status-icon*
:Swatch swatch*

View File

@ -9,6 +9,7 @@
// TODO: create actual tokens once we have them from design
$br-8: px2rem(8);
$br-4: px2rem(4);
$br-6: px2rem(6);
$br-circle: 50%;
$b-1: px2rem(1);

View File

@ -7,6 +7,7 @@
@use "./utils.scss" as *;
// TODO: create actual tokens once we have them from design
$sz-6: px2rem(6);
$sz-16: px2rem(16);
$sz-24: px2rem(24);
$sz-32: px2rem(32);

View File

@ -189,25 +189,28 @@ export const TestInteractions = {
await userEvent.clear(input);
});
await step("Filter with 'es' (Tuesday, Wednesday) and select Wednesday", async () => {
await userEvent.clear(input);
await userEvent.keyboard("{Escape}");
await step(
"Filter with 'es' (Tuesday, Wednesday) and select Wednesday",
async () => {
await userEvent.clear(input);
await userEvent.keyboard("{Escape}");
await userEvent.click(input);
await userEvent.click(input);
await userEvent.type(input, "es");
await userEvent.type(input, "es");
const options = await canvas.findAllByTestId("dropdown-option");
expect(options).toHaveLength(2);
const options = await canvas.findAllByTestId("dropdown-option");
expect(options).toHaveLength(2);
await userEvent.keyboard("[ArrowDown]");
await userEvent.keyboard("[ArrowDown]");
await userEvent.keyboard("[ArrowDown]");
await userEvent.keyboard("[ArrowDown]");
await userEvent.keyboard("{Enter}");
await userEvent.keyboard("{Enter}");
expect(input).toHaveValue("Wednesday");
expect(lastValue).toBe("Wednesday");
});
expect(input).toHaveValue("Wednesday");
expect(lastValue).toBe("Wednesday");
},
);
await step("Close dropdown when focusing out", async () => {
await userEvent.clear(input);

View File

@ -13,19 +13,15 @@
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
[app.main.ui.ds.controls.utilities.label :refer [label*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon-list]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(def ^:private schema:input
[:map
[:id {:optional true} :string]
[:label {:optional true} :string]
[:class {:optional true} :string]
[:label {:optional true} :string]
[:is-optional {:optional true} :boolean]
[:placeholder {:optional true} :string]
[:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]]
[:type {:optional true} :string]
[:max-length {:optional true} :int]
[:variant {:optional true} [:maybe [:enum "seamless" "dense" "comfortable"]]]

View File

@ -0,0 +1,677 @@
;; 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.ds.controls.numeric-input
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.math :as mth]
[app.common.schema :as sm]
[app.main.constants :refer [max-input-length]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.select :refer [get-option handle-focus-change]]
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]]
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
[app.main.ui.ds.controls.utilities.token-field :refer [token-field*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
[app.main.ui.ds.tooltip :refer [tooltip*]]
[app.main.ui.formats :as fmt]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.simple-math :as smt]
[cuerdas.core :as str]
[goog.events :as events]
[rumext.v2 :as mf]
[rumext.v2.util :as mfu]))
(defn- increment
"Increments `val` by `step`, clamped to [`min-val`, `max-val`]."
[val step min-val max-val]
(mth/clamp (+ val step) min-val max-val))
(defn- decrement
"Decrements `val` by `step`, clamped to [`min-val`, `max-val`]."
[val step min-val max-val]
(mth/clamp (- val step) min-val max-val))
(defn- parse-value
"Parses and clamps `raw-value` as a number within bounds;
returns nil if invalid or empty."
[raw-value last-value min-value max-value nillable]
(let [new-value (-> raw-value
(str)
(str/strip-suffix ".")
(smt/expr-eval (d/parse-double last-value)))]
(cond
(and nillable (nil? raw-value))
nil
(d/num? new-value)
(-> new-value
(mth/max (/ sm/min-safe-int 2))
(mth/min (/ sm/max-safe-int 2))
(cond-> (d/num? min-value)
(mth/max min-value))
(cond-> (d/num? max-value)
(mth/min max-value)))
:else nil)))
(defn- get-option-by-name
[options name]
(d/seek #(= name (get % :name)) options))
(defn- get-token-op
[tokens name]
(->> tokens
vals
(apply concat)
(some #(when (= (:name %) name) %))))
(defn- clean-token-name
[s]
(some-> s
(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-by-type 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-by-type)))
(defn- focusable-option?
[option]
(and (:id option)
(not= :group (:type option))
(not= :separator (:type option))))
(defn- first-focusable-id
[options]
(some #(when (focusable-option? %) (:id %)) options))
(defn- next-focus-index
[options focused-id direction]
(let [len (count options)
start-index (or (d/index-of-pred options #(= focused-id (:id %))) -1)
indices (case direction
:down (range (inc start-index) (+ len start-index))
:up (range (dec start-index) (- start-index len) -1))]
(some (fn [i]
(let [j (mod i len)]
(when (focusable-option? (nth options j))
j)))
indices)))
(def ^:private schema:icon
[:and :string [:fn #(contains? icon-list %)]])
;; TODO: Review schema props
(def ^:private schema:numeric-input
[:map
[:id {:optional true} :string]
[:class {:optional true} :string]
;; [:value {:optional true} [:maybe [:or
;; :int
;; :string]]]
[:default {:optional true} [:maybe :string]]
[:placeholder {:optional true} :string]
[:icon {:optional true} [:maybe schema:icon]]
[:disabled {:optional true} [:maybe :boolean]]
[:min {:optional true} [:maybe :int]]
[:max {:optional true} [:maybe :int]]
[:max-length {:optional true} :int]
[:step {:optional true} [:maybe :int]]
[:is-selected-on-focus {:optional true} :boolean]
[:nillable {:optional true} :boolean]
[:applied-token {:optional true} [:maybe [:or :string [:= :multiple]]]]
[:empty-to-end {:optional true} :boolean]
[:on-change {:optional true} fn?]
[:on-blur {:optional true} fn?]
[:on-focus {:optional true} fn?]
[:on-detach {:optional true} fn?]
[:property {:optional true} :string]
[:align {:optional true} [:enum :left :right]]])
(mf/defc numeric-input*
{::mf/schema schema:numeric-input}
[{:keys [id class value default placeholder icon disabled
min max max-length step
is-selected-on-focus nillable
tokens applied-token empty-to-end
on-change on-blur on-focus on-detach
property align ref] :rest props}]
(let [;; NOTE: we use mfu/bean here for transparently handle
;; options provide as clojure data structures or javascript
;; plain objects and lists.
tokens (if (object? tokens)
(mfu/bean tokens)
tokens)
value (if (= :multiple applied-token)
:multiple
value)
is-multiple? (= :multiple value)
value (cond
is-multiple? nil
(and nillable (nil? value)) nil
:else (d/parse-double value default))
;; Default props
nillable (d/nilv nillable false)
disabled (d/nilv disabled false)
select-on-focus (d/nilv is-selected-on-focus true)
default (mf/with-memo [default nillable]
(d/parse-double default (when-not nillable 0)))
step (mf/with-memo [step]
(d/parse-double step 1))
min (mf/with-memo [min]
(d/parse-double min sm/min-safe-int))
max (mf/with-memo [max]
(d/parse-double max sm/max-safe-int))
max-length (d/nilv max-length max-input-length)
empty-to-end (d/nilv empty-to-end false)
internal-id (mf/use-id)
id (d/nilv id internal-id)
listbox-id (mf/use-id)
align (d/nilv align :left)
;; State and values
is-open* (mf/use-state false)
is-open (deref is-open*)
token-applied* (mf/use-state applied-token)
token-applied (deref token-applied*)
focused-id* (mf/use-state nil)
focused-id (deref focused-id*)
filter-id* (mf/use-state "")
filter-id (deref filter-id*)
raw-value* (mf/use-ref nil)
last-value* (mf/use-ref nil)
;; Refs
wrapper-ref (mf/use-ref nil)
nodes-ref (mf/use-ref nil)
options-ref (mf/use-ref nil)
token-wrapper-ref (mf/use-ref nil)
internal-ref (mf/use-ref nil)
ref (or ref internal-ref)
dirty-ref (mf/use-ref false)
open-dropdown-ref (mf/use-ref nil)
token-detach-btn-ref (mf/use-ref nil)
dropdown-options
(mf/with-memo [tokens filter-id]
(let [partial (extract-partial-brace-text filter-id)
options (if (seq partial)
(filter-token-groups-by-name tokens partial)
tokens)
no-sets? (nil? tokens)]
(generate-dropdown-options options no-sets?)))
selected-id*
(mf/use-state (fn []
(if applied-token
(:id (get-option-by-name dropdown-options applied-token))
nil)))
selected-id
(deref selected-id*)
set-option-ref
(mf/use-fn
(fn [node]
(let [state (mf/ref-val nodes-ref)
state (d/nilv state #js {})
id (dom/get-data node "id")
state (obj/set! state id node)]
(mf/set-ref-val! nodes-ref state)
(fn []
(let [state (mf/ref-val nodes-ref)
state (d/nilv state #js {})
id (dom/get-data node "id")
state (obj/unset! state id)]
(mf/set-ref-val! nodes-ref state))))))
;; Callbacks
update-input
(mf/use-fn
(fn [new-value]
(when-let [node (mf/ref-val ref)]
(dom/set-value! node new-value))))
apply-value
(mf/use-fn
(mf/deps on-change update-input value nillable min max)
(fn [raw-value]
(if-let [parsed (parse-value raw-value (mf/ref-val last-value*) min max nillable)]
(when-not (= parsed (mf/ref-val last-value*))
(mf/set-ref-val! last-value* parsed)
(reset! token-applied* nil)
(when (fn? on-change)
(on-change parsed))
;; Comprar si es valor es necesario, sino borrar
(mf/set-ref-val! raw-value* (fmt/format-number parsed))
(update-input (fmt/format-number parsed)))
(if (and nillable (empty? raw-value))
(do
(mf/set-ref-val! last-value* nil)
(mf/set-ref-val! raw-value* "")
(reset! token-applied* nil)
(update-input "")
(when (fn? on-change)
(on-change nil)))
(let [fallback-value (or (mf/ref-val last-value*) default)]
(mf/set-ref-val! raw-value* fallback-value)
(mf/set-ref-val! last-value* fallback-value)
(reset! token-applied* nil)
(update-input (fmt/format-number fallback-value))
(when (and (fn? on-change) (not= fallback-value (str value)))
(on-change fallback-value)))))))
apply-token
(mf/use-fn
(mf/deps min max nillable on-change tokens)
(fn [value name]
(let [parsed (parse-value value (mf/ref-val last-value*) min max nillable)]
(when-not (= parsed (mf/ref-val last-value*))
(mf/set-ref-val! last-value* parsed)
(when (fn? on-change)
(on-change (get-token-op tokens name)))))))
store-raw-value
(mf/use-fn
(fn [event]
(let [text (dom/get-target-val event)]
(mf/set-ref-val! raw-value* text)
(reset! filter-id* text))))
on-token-apply
(mf/use-fn
(mf/deps apply-token)
(fn [id value name]
(reset! selected-id* id)
(reset! focused-id* nil)
(reset! is-open* false)
(reset! token-applied* name)
(apply-token value name)))
on-option-click
(mf/use-fn
(mf/deps dropdown-options on-token-apply)
(fn [event]
(let [node (dom/get-current-target event)
id (dom/get-data node "id")
option (get-option dropdown-options id)
value (get option :resolved-value)
name (get option :name)]
(on-token-apply id value name)
(reset! filter-id* ""))))
on-option-enter
(mf/use-fn
(mf/deps dropdown-options focused-id on-token-apply)
(fn [_]
(let [option (get-option dropdown-options focused-id)
value (get option :resolved-value)
name (get option :name)]
(on-token-apply focused-id value name)
(reset! filter-id* ""))))
on-blur
(mf/use-fn
(mf/deps apply-value on-blur)
(fn [event]
(let [target (dom/get-related-target event)
self-node (mf/ref-val wrapper-ref)]
(when-not (dom/is-child? self-node target)
(reset! filter-id* "")
(reset! focused-id* nil)
(reset! is-open* false)))
(when (mf/ref-val dirty-ref)
(apply-value (mf/ref-val raw-value*))
(when (fn? on-blur)
(on-blur event)))))
handle-key-down
(mf/use-fn
(mf/deps dropdown-options is-open apply-value update-input is-open focused-id handle-focus-change)
(fn [event]
(mf/set-ref-val! dirty-ref true)
(let [up? (kbd/up-arrow? event)
down? (kbd/down-arrow? event)
enter? (kbd/enter? event)
esc? (kbd/esc? event)
node (mf/ref-val ref)
open-tokens (kbd/is-key? event "{")
close-tokens (kbd/is-key? event "}")
options (mf/ref-val options-ref)]
(cond
(and (some? options) open-tokens)
(reset! is-open* true)
close-tokens
(do
(let [name (clean-token-name (mf/ref-val raw-value*))
token (get-option-by-name options name)]
(if token
(apply-token (:resolved-value token) name)
(apply-value (mf/ref-val last-value*)))))
enter?
(if is-open
(do
(dom/prevent-default event)
(if focused-id
(on-option-enter event)
(let [option-id (first-focusable-id dropdown-options)
option (get-option dropdown-options option-id)
value (get option :resolved-value)
name (get option :name)]
(on-token-apply option-id value name)
(reset! filter-id* ""))))
(on-blur event))
esc?
(do
(update-input (fmt/format-number (mf/ref-val last-value*)))
(reset! is-open* false)
(dom/blur! node))
(kbd/home? event)
(handle-focus-change options focused-id* 0 (mf/ref-val nodes-ref))
up?
(if is-open
(let [new-index (next-focus-index options focused-id :up)]
(dom/prevent-default event)
(handle-focus-change options focused-id* new-index (mf/ref-val nodes-ref)))
(let [parsed (parse-value (mf/ref-val raw-value*) (mf/ref-val last-value*) min max nillable)
current-value (or parsed default)
new-val (increment current-value step min max)]
(dom/prevent-default event)
(update-input (fmt/format-number new-val))
(apply-value (dm/str new-val))))
down?
(if is-open
(let [new-index (next-focus-index options focused-id :down)]
(dom/prevent-default event)
(handle-focus-change options focused-id* new-index (mf/ref-val nodes-ref)))
(let [parsed (parse-value (mf/ref-val raw-value*) (mf/ref-val last-value*) min max nillable)
current-value (or parsed default)
new-val (decrement current-value step min max)]
(dom/prevent-default event)
(update-input (fmt/format-number new-val))
(apply-value (dm/str new-val))))))))
handle-focus
(mf/use-fn
(mf/deps on-focus select-on-focus)
(fn [event]
(when (fn? on-focus)
(on-focus event))
(let [target (dom/get-target event)]
(when select-on-focus
(dom/select-text! target)
;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
(.addEventListener target "mouseup" dom/prevent-default #js {:once true})))))
handle-mouse-wheel
(mf/use-fn
(mf/deps apply-value parse-value min max nillable ref default step min max)
(fn [event]
(when-let [node (mf/ref-val ref)]
(when (dom/active? node)
(let [inc? (->> (dom/get-delta-position event)
:y
(neg?))
parsed (parse-value (mf/ref-val raw-value*) (mf/ref-val last-value*) min max nillable)
current-value (or parsed default)
new-val (if inc?
(increment current-value step min max)
(decrement current-value step min max))]
(dom/prevent-default event)
(dom/stop-propagation event)
(apply-value (dm/str new-val)))))))
open-dropdown
(mf/use-fn
(mf/deps disabled ref)
(fn [event]
(when-not disabled
(dom/prevent-default event)
(swap! is-open* not)
(dom/focus! (mf/ref-val ref)))))
open-dropdown-token
(mf/use-fn
(mf/deps disabled token-wrapper-ref)
(fn [event]
(when-not disabled
(dom/prevent-default event)
(swap! is-open* not)
(dom/focus! (mf/ref-val token-wrapper-ref)))))
detach-token
(mf/use-fn
(mf/deps on-detach tokens disabled token-applied)
(fn [event]
(let [token (get-token-op tokens token-applied)]
(when-not disabled
(dom/prevent-default event)
(dom/stop-propagation event)
(reset! token-applied* nil)
(reset! selected-id* nil)
(reset! focused-id* nil)
(dom/focus! (mf/ref-val ref))
(when on-detach
(on-detach token))))))
on-token-key-down
(mf/use-fn
(mf/deps detach-token is-open)
(fn [event]
(let [esc? (kbd/esc? event)
delete? (kbd/delete? event)
backspace? (kbd/backspace? event)
enter? (kbd/enter? event)
up? (kbd/up-arrow? event)
down? (kbd/down-arrow? event)
options (mf/ref-val options-ref)
detach-btn (mf/ref-val token-detach-btn-ref)
target (dom/get-target event)]
(when-not disabled
(cond
(or delete? backspace?)
(do
(dom/prevent-default event)
(detach-token event)
(dom/focus! (mf/ref-val ref)))
enter?
(if is-open
(do
(dom/prevent-default event)
(dom/stop-propagation event)
(on-option-enter event))
(when (not= target detach-btn)
(dom/prevent-default event)
(reset! is-open* true)))
esc?
(dom/blur! (mf/ref-val token-wrapper-ref))
up?
(when is-open
(let [new-index (next-focus-index options focused-id :up)]
(dom/prevent-default event)
(handle-focus-change options focused-id* new-index nodes-ref)))
down?
(when is-open
(let [new-index (next-focus-index options focused-id :down)]
(dom/prevent-default event)
(handle-focus-change options focused-id* new-index nodes-ref))))))))
input-props
(mf/spread-props props {:ref ref
:type "text"
:id id
:placeholder (if is-multiple?
(tr "labels.mixed-values")
placeholder)
:default-value (or (mf/ref-val last-value*) (fmt/format-number value))
:on-blur on-blur
:on-key-down handle-key-down
:on-focus handle-focus
:on-change store-raw-value
:disabled disabled
:slot-start (when icon
(mf/html [:> tooltip*
{:content property
:id property}
[:> icon* {:icon-id icon
:aria-labelledby property
:class (stl/css :icon)}]]))
:slot-end (when-not disabled
(when (some? tokens)
(mf/html [:> icon-button* {:variant "action"
:icon "tokens"
:class (stl/css :invisible-button)
:aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
:ref open-dropdown-ref
:on-click open-dropdown}])))
:max-length max-length})
token-props
(when (and token-applied (not= :multiple token-applied))
(let [token (get-option-by-name dropdown-options token-applied)
id (get token :id)
label (get token :name)
token-value (or (get token :resolved-value)
(or (mf/ref-val last-value*) (fmt/format-number value)))]
(mf/spread-props props
{:id id
:label label
:value token-value
:on-click open-dropdown-token
:on-token-key-down on-token-key-down
:disabled disabled
:on-blur on-blur
:slot-start (when icon
(mf/html [:> tooltip*
{:content property
:id property}
[:> icon* {:icon-id icon
:aria-labelledby property
:class (stl/css :icon)}]]))
:token-wrapper-ref token-wrapper-ref
:token-detach-btn-ref token-detach-btn-ref
:detach-token detach-token})))]
(mf/with-effect [value default applied-token]
(let [value' (cond
is-multiple?
""
(and nillable (nil? value))
""
:else
(fmt/format-number (d/parse-double value default)))]
(mf/set-ref-val! raw-value* value')
(mf/set-ref-val! last-value* value')
(reset! token-applied* applied-token)
(if applied-token
(let [token-id (:id (get-option-by-name dropdown-options applied-token))]
(reset! selected-id* token-id))
(reset! selected-id* nil))
(when-let [node (mf/ref-val ref)]
(dom/set-value! node value'))))
(mf/with-layout-effect [handle-mouse-wheel]
(when-let [node (mf/ref-val ref)]
(let [key (events/listen node "wheel" handle-mouse-wheel #js {:passive false})]
#(events/unlistenByKey key))))
(mf/with-effect [dropdown-options]
(mf/set-ref-val! options-ref dropdown-options))
[:div {:class (dm/str class " " (stl/css :input-wrapper))
:ref wrapper-ref}
(if (and (some? token-applied)
(not= :multiple token-applied))
[:> token-field* token-props]
[:> input-field* input-props])
(when ^boolean is-open
[:> options-dropdown* {:on-click on-option-click
:id listbox-id
:options dropdown-options
:selected selected-id
:focused focused-id
:align align
:empty-to-end empty-to-end
:ref set-option-ref}])]))

View File

@ -0,0 +1,11 @@
{ /* 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 */ }
import { Canvas, Meta } from '@storybook/blocks';
import * as InputStories from "./numeric_input.stories";
<Meta title="Controls/Numeric Input" />
# Numeric Input

View File

@ -0,0 +1,42 @@
// 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 "../spacing.scss" as *;
@use "../borders.scss" as *;
@use "../sizes.scss" as *;
@use "../typography.scss" as t;
.input-wrapper {
--opacity-button: 0;
display: flex;
flex-direction: column;
gap: var(--sp-xs);
inline-size: 100%;
position: relative;
&:hover {
--opacity-button: 1;
}
&:focus-within {
--opacity-button: 1;
}
}
.icon {
color: var(--color-foreground-secondary);
min-width: var(--sp-l);
}
.invisible-button {
opacity: var(--opacity-button);
&:hover {
--opacity-button: 1;
}
&:focus {
--opacity-button: 1;
}
}

View File

@ -0,0 +1,121 @@
// 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
import * as React from "react";
import Components from "@target/components";
const { NumericInput } = Components;
const { icons } = Components.meta;
export default {
title: "Controls/Numeric Input",
component: Components.NumericInput,
argTypes: {
placeholder: {
control: { type: "text" },
},
disabled: {
control: { type: "boolean" },
},
nillable: {
control: { type: "boolean" },
},
min: {
control: { type: "number" },
},
max: {
control: { type: "number" },
},
step: {
control: { type: "number" },
},
icon: {
options: icons,
control: { type: "select" },
},
},
args: {
placeholder: "--",
disabled: false,
nillable: false,
icon: "search",
property: "search",
},
parameters: {
controls: { exclude: [ "tokens" ] },
},
render: ({ ...args }) => <NumericInput {...args} />,
};
export const Default = {};
export const WithTokens = {
args: {
placeholder: "--",
disabled: false,
nillable: false,
icon: "search",
min: 0,
max: 100,
step: 1,
tokens: {
dimensions: [
{
id: "ef79ae43-3f3f-8008-8006-988189c5e56e",
name: "dimension-1",
type: "dimensions",
value: "30",
description: "",
"modified-at": "2025-08-04T12:02:00.087-00:00",
resolvedValue: 30,
unit: null,
},
{
id: "ef79ae43-3f3f-8008-8006-98a5d53d85d0",
name: "dimension-2",
type: "dimensions",
value: "20",
description: "",
"modified-at": "2025-08-04T14:40:34.550-00:00",
resolvedValue: 20,
unit: null,
},
{
id: "ef79ae43-3f3f-8008-8006-98a5dd078629",
name: "dimension-3",
type: "dimensions",
value: "100",
description: "",
"modified-at": "2025-08-04T14:40:42.526-00:00",
resolvedValue: 100,
unit: null,
},
{
id: "ef79ae43-3f3f-8008-8006-98a5e75c0299",
name: "dimension-4",
type: "dimensions",
value: "500",
description: "",
"modified-at": "2025-08-04T14:40:53.104-00:00",
resolvedValue: 500,
unit: null,
},
],
spacing: [
{
id: "ef79ae43-3f3f-8008-8006-98a5f1c64d5a",
name: "spacing-1",
type: "spacing",
value: "32",
description: "",
"modified-at": "2025-08-04T14:41:03.769-00:00",
resolvedValue: 32,
unit: "px",
},
],
},
},
render: ({ ...args }) => <NumericInput {...args} />,
};

View File

@ -28,7 +28,6 @@
(let [option (get-option options default)]
(get option :id)))
;; Also used in combobox
(defn handle-focus-change
[options focused* new-index nodes]

View File

@ -0,0 +1,69 @@
;; 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.ds.controls.shared.option
(:require-macros
[app.main.style :as stl])
(:require
[app.main.ui.ds.foundations.assets.icon :as i]
[rumext.v2 :as mf]))
(def ^:private schema:option
"A schema for option* component props"
[:and
[:map {:title "option"}
[:id :string]
[:icon {:optional true} [:maybe :string]]
[:selected {:optional true} :boolean]
[:focused {:optional true} :boolean]
[:dimmed {:optional true} :boolean]
[:label {:optional true} :string]
[:aria-label {:optional true} [:maybe :string]]
[:on-click {:optional true} fn?]]
[:fn {:error/message "invalid data: missing required props"}
(fn [props]
(or (and (contains? props :icon)
(or (contains? props :label)
(contains? props :aria-label)))
(contains? props :label)))]])
(mf/defc option*
{::mf/schema schema:option}
[{:keys [id ref label icon aria-label on-click selected focused dimmed] :rest props}]
(let [class (stl/css-case :option true
:option-with-icon (some? icon)
:option-selected selected
:option-current focused)]
[:li {:value id
:class class
:aria-selected selected
:ref ref
:role "option"
:id id
:on-click on-click
:data-id id
:data-testid "dropdown-option"}
(when (some? icon)
[:> i/icon*
{:icon-id icon
:size "s"
:class (stl/css :option-icon)
:aria-hidden (when label true)
:aria-label (when (not label) aria-label)}])
[:span {:class (stl/css-case :option-text true
:option-text-dimmed dimmed)}
label]
(when ^boolean selected
[:> i/icon*
{:icon-id i/tick
:size "s"
:class (stl/css :option-check)
:aria-hidden (when ^boolean label true)}])]))

View File

@ -0,0 +1,65 @@
// 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 "../../_borders.scss" as *;
@use "../../_sizes.scss" as *;
@use "../../typography.scss" as *;
.option {
--options-fg-color: var(--color-foreground-primary);
--options-bg-color: unset;
--options-empty: var(--color-canvas);
display: grid;
align-items: center;
justify-items: start;
grid-template-columns: 1fr auto;
gap: var(--sp-xs);
width: 100%;
height: $sz-32;
padding: var(--sp-s);
border-radius: $br-8;
outline: $b-1 solid var(--options-outline-color);
outline-offset: calc(-1 * $b-1);
background-color: var(--options-bg-color);
color: var(--options-fg-color);
&:hover,
&[aria-selected="true"] {
--options-bg-color: var(--color-background-quaternary);
}
}
.option-with-icon {
grid-template-columns: auto 1fr auto;
}
.option-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
min-width: 0;
padding-inline-start: var(--sp-xxs);
}
.option-text-dimmed {
color: var(--options-empty);
}
.option-icon {
color: var(--options-icon-fg-color);
}
.option-current {
--options-outline-color: var(--color-accent-primary);
outline: $b-1 solid var(--options-outline-color);
}
.option-selected {
--options-fg-color: var(--color-accent-primary);
--options-icon-fg-color: var(--color-accent-primary);
}

View File

@ -8,6 +8,10 @@
(:require-macros
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.weak :refer [weak-key]]
[app.main.ui.ds.controls.shared.option :refer [option*]]
[app.main.ui.ds.controls.shared.token-option :refer [token-option*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@ -17,27 +21,26 @@
[:fn {:error/message "invalid data: invalid icon"} #(contains? i/icon-list %)]])
(def schema:option
[:and
[:map {:title "option"}
[:id :string]
[:icon {:optional true} schema:icon-list]
[:label {:optional true} :string]
[:aria-label {:optional true} :string]]
[:fn {:error/message "invalid data: missing required props"}
(fn [option]
(or (and (contains? option :icon)
(or (contains? option :label)
(contains? option :aria-label)))
(contains? option :label)))]])
"A schema for the option data structure expected to receive on props
for the `options-dropdown*` component."
[:map
[:id {:optional true} :string]
[:resolved-value {:optional true}
[:or :int :string]]
[:name {:optional true} :string]
[:icon {:optional true} schema:icon-list]
[:label {:optional true} :string]
[:aria-label {:optional true} :string]])
(def ^:private schema:options-dropdown
[:map
[:ref {:optional true} fn?]
[:on-click fn?]
[:options [:vector schema:option]]
[:selected :any]
[:selected {:optional true} :any]
[:focused {:optional true} :any]
[:empty-to-end {:optional true} :boolean]])
[:empty-to-end {:optional true} [:maybe :boolean]]
[:align {:optional true} [:maybe [:enum :left :right]]]])
(def ^:private
xf:filter-blank-id
@ -47,46 +50,66 @@
xf:filter-non-blank-id
(remove #(str/blank? (get % :id))))
(mf/defc option*
{::mf/private true}
[{:keys [id ref label icon aria-label on-click selected focused] :rest props}]
(let [class (stl/css-case :option true
:option-with-icon (some? icon)
:option-selected selected
:option-current focused)]
[:li {:value id
:class class
:aria-selected selected
:ref ref
:role "option"
:id id
:on-click on-click
:data-id id
:data-testid "dropdown-option"}
(defn- render-option
[option ref on-click selected focused]
(let [id (get option :id)
name (get option :name)
type (get option :type)]
(when (some? icon)
[:> i/icon*
{:icon-id icon
:size "s"
:class (stl/css :option-icon)
:aria-hidden (when label true)
:aria-label (when (not label) aria-label)}])
(mf/html
(case type
:group
[:li {:class (stl/css :group-option)
:key (weak-key option)}
[:> i/icon*
{:icon-id i/arrow-down
:size "m"
:class (stl/css :option-check)
:aria-hidden (when name true)}]
(d/name name)]
[:span {:class (stl/css :option-text)} label]
:separator
[:hr {:key (weak-key option) :class (stl/css :option-separator)}]
:empty
[:li {:key (weak-key option) :class (stl/css :option-empty)}
(get option :label)]
;; Token option
:token
[:> token-option* {:selected (= id selected)
:key (weak-key option)
:id id
:name name
:resolved (get option :resolved-value)
:ref ref
:focused (= id focused)
:on-click on-click}]
;; Normal option
[:> option* {:selected (= id selected)
:key (weak-key option)
:id id
:label (get option :label)
:aria-label (get option :aria-label)
:icon (get option :icon)
:ref ref
:focused (= id focused)
:dimmed false
:on-click on-click}]))))
(when selected
[:> i/icon*
{:icon-id i/tick
:size "s"
:class (stl/css :option-check)
:aria-hidden (when label true)}])]))
(mf/defc options-dropdown*
{::mf/schema schema:options-dropdown}
[{:keys [ref on-click options selected focused empty-to-end] :rest props}]
(let [props
[{:keys [ref on-click options selected focused empty-to-end align] :rest props}]
(let [align
(d/nilv align :left)
props
(mf/spread-props props
{:class (stl/css :option-list)
{:class (stl/css-case :option-list true
:left-align (= align :left)
:right-align (= align :right))
:tab-index "-1"
:role "listbox"})
@ -103,19 +126,7 @@
[:> :ul props
(for [option options]
(let [id (get option :id)
label (get option :label)
aria-label (get option :aria-label)
icon (get option :icon)]
[:> option* {:selected (= id selected)
:key id
:id id
:label label
:icon icon
:aria-label aria-label
:ref ref
:focused (= id focused)
:on-click on-click}]))
(render-option option ref on-click selected focused))
(when (seq options-blank)
[:*
@ -123,16 +134,4 @@
[:hr {:class (stl/css :option-separator)}])
(for [option options-blank]
(let [id (get option :id)
label (get option :label)
aria-label (get option :aria-label)
icon (get option :icon)]
[:> option* {:selected (= id selected)
:key id
:id id
:label label
:icon icon
:aria-label aria-label
:ref ref
:focused (= id focused)
:on-click on-click}]))])]))
(render-option option ref on-click selected focused))])]))

View File

@ -13,12 +13,10 @@
--options-dropdown-bg-color: var(--color-background-tertiary);
--options-dropdown-outline-color: none;
--options-dropdown-border-color: var(--color-background-quaternary);
--options-dropdown-empty: var(--color-canvas);
position: absolute;
right: 0;
top: $sz-36;
width: 100%;
width: var(--dropdown-width, 100%);
background-color: var(--options-dropdown-bg-color);
border-radius: $br-8;
border: $b-1 solid var(--options-dropdown-border-color);
@ -30,55 +28,12 @@
z-index: var(--z-index-dropdown);
}
.option {
--options-dropdown-fg-color: var(--color-foreground-primary);
--options-dropdown-bg-color: unset;
display: grid;
align-items: center;
justify-items: start;
grid-template-columns: 1fr auto;
gap: var(--sp-xs);
width: 100%;
height: $sz-32;
padding: var(--sp-s);
border-radius: $br-8;
outline: $b-1 solid var(--options-dropdown-outline-color);
outline-offset: -1px;
background-color: var(--options-dropdown-bg-color);
color: var(--options-dropdown-fg-color);
&:hover,
&[aria-selected="true"] {
--options-dropdown-bg-color: var(--color-background-quaternary);
}
.left-align {
left: 0;
}
.option-with-icon {
grid-template-columns: auto 1fr auto;
}
.option-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
min-width: 0;
padding-inline-start: var(--sp-xxs);
}
.option-icon {
color: var(--options-dropdown-icon-fg-color);
}
.option-current {
--options-dropdown-outline-color: var(--color-accent-primary);
outline: $b-1 solid var(--options-dropdown-outline-color);
}
.option-selected {
--options-dropdown-fg-color: var(--color-accent-primary);
--options-dropdown-icon-fg-color: var(--color-accent-primary);
.right-align {
right: 0;
}
.option-separator {
@ -86,3 +41,20 @@
margin-top: var(--sp-xs);
margin-bottom: var(--sp-xs);
}
.group-option,
.option-empty {
@include use-typography("body-small");
display: flex;
align-items: center;
gap: var(--sp-xs);
color: var(--color-foreground-secondary);
padding-inline: var(--sp-s);
height: var(--sp-xxxl);
}
.option-empty {
justify-content: center;
text-align: center;
padding: 0 40px;
}

View File

@ -0,0 +1,61 @@
;; 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.ds.controls.shared.token-option
(:require-macros
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
[rumext.v2 :as mf]))
(def ^:private schema:token-option
[:map
[:id {:optiona true} :string]
[:ref some?]
[:resolved {:optional true} [:or :number :string]]
[:name {:optional true} :string]
[:on-click {:optional true} fn?]
[:selected {:optional true} :boolean]
[:focused {:optional true} :boolean]
[:focused {:optional true} :boolean]])
(mf/defc token-option*
{::mf/schema schema:token-option}
[{:keys [id name on-click selected ref focused resolved] :rest props}]
(let [internal-id (mf/use-id)
id (d/nilv id internal-id)]
[:li {:value id
:class (stl/css-case :token-option true
:option-with-pill true
:option-selected-token selected
:option-current focused)
:aria-selected selected
:ref ref
:role "option"
:id id
:on-click on-click
:data-id id
:data-testid "dropdown-option"}
(if selected
[:> icon*
{:icon-id i/tick
:size "s"
:class (stl/css :option-check)
:aria-hidden (when name true)}]
[:span {:class (stl/css :icon-placeholder)}])
[:> tooltip* {:content name
:id (dm/str id "-name")
:class (stl/css :option-text)}
;; Add ellipsis
[:span {:aria-labelledby (dm/str id "-name")}
name]]
(when resolved
[:> :span {:class (stl/css :option-pill)}
resolved])]))

View File

@ -0,0 +1,77 @@
// 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 "../../_borders.scss" as *;
@use "../../_sizes.scss" as *;
@use "../../typography.scss" as *;
.token-option {
--token-options-fg-color: var(--color-foreground-primary);
--token-options-bg-color: unset;
--token-options-empty: var(--color-canvas);
@include use-typography("body-small");
display: grid;
align-items: center;
justify-items: start;
grid-template-columns: 1fr auto;
gap: $sz-6;
width: 100%;
height: $sz-32;
padding: var(--sp-s);
border-radius: $br-8;
outline: $b-1 solid var(--token-options-outline-color);
outline-offset: calc(-1 * $b-1);
background-color: var(--token-options-bg-color);
color: var(--token-options-fg-color);
overflow: hidden;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover,
&[aria-selected="true"] {
--token-options-bg-color: var(--color-background-quaternary);
}
}
.option-with-pill {
grid-template-columns: auto 1fr auto;
}
.option-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
width: 100%;
padding-inline-start: var(--sp-xxs);
}
.option-current {
--token-options-outline-color: var(--color-accent-primary);
outline: $b-1 solid var(--token-options-outline-color);
}
.option-pill {
@include use-typography("code-font");
color: var(--color-foreground-secondary);
background-color: var(--color-background-primary);
border-radius: $br-6;
padding: var(--sp-xxs) var(--sp-xs);
}
.option-selected-token {
--token-options-fg-color: var(--color-foreground-primary);
--token-options-icon-fg-color: var(--color-accent-primary);
}
.icon-placeholder {
inline-size: var(--sp-l);
}
.option-check {
color: var(--token-options-icon-fg-color);
min-width: var(--sp-l);
}

View File

@ -32,7 +32,10 @@
(mf/defc input-field*
{::mf/forward-ref true
::mf/schema schema:input-field}
[{:keys [id icon has-hint hint-type class type max-length variant slot-start slot-end] :rest props} ref]
[{:keys [id icon class type
has-hint hint-type
max-length variant
slot-start slot-end] :rest props} ref]
(let [input-ref (mf/use-ref)
type (d/nilv type "text")
variant (d/nilv variant "dense")

View File

@ -25,7 +25,7 @@
background: var(--input-bg-color);
border-radius: $br-8;
padding: 0 var(--sp-s);
padding: 0 var(--sp-xs);
outline: $b-1 solid var(--input-outline-color);
&:hover {

View File

@ -0,0 +1,85 @@
;; 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.ds.controls.utilities.token-field
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.tooltip :refer [tooltip*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
(def ^:private schema:token-field
[:map
[:id {:optional true} [:maybe :string]]
[:label {:optional true} [:maybe :string]]
[:value :any]
[:disabled {:optional true} :boolean]
[:slot-start {:optional true} [:maybe some?]]
[:on-click {:optional true} fn?]
[:on-token-key-down fn?]
[:on-blur {:optional true} fn?]
[:detach-token fn?]])
(mf/defc token-field*
{::mf/schema schema:token-field}
[{:keys [id label value slot-start disabled
on-click on-token-key-down on-blur detach-token
token-wrapper-ref token-detach-btn-ref]}]
(let [set-active? (some? id)
content (if set-active?
label
(tr "ds.inputs.token-field.no-active-token-option"))
default-id (mf/use-id)
id (d/nilv id default-id)
focus-wrapper
(mf/use-fn
(mf/deps disabled)
(fn [event]
(when-not ^boolean disabled
(dom/prevent-default event)
(dom/focus! (mf/ref-val token-wrapper-ref)))))
class
(stl/css-case :token-field true
:with-icon (some? slot-start)
:token-field-disabled disabled)]
[:div {:class class
:on-click focus-wrapper
:disabled disabled
:on-key-down on-token-key-down
:ref token-wrapper-ref
:on-blur on-blur
:tab-index (if disabled -1 0)}
(when (some? slot-start) slot-start)
[:> tooltip* {:content content
:id (dm/str id "-pill")}
[:button {:on-click on-click
:class (stl/css-case :pill true
:no-set-pill (not set-active?)
:pill-disabled disabled)
:disabled disabled
:aria-labelledby (dm/str id "-pill")
:on-key-down on-token-key-down}
value
(when-not set-active?
[:div {:class (stl/css :pill-dot)}])]]
(when-not ^boolean disabled
[:> icon-button* {:variant "action"
:class (stl/css :invisible-button)
:icon "broken-link"
:ref token-detach-btn-ref
:aria-label (tr "ds.inputs.token-field.detach-token")
:on-click detach-token}])]))

View File

@ -0,0 +1,126 @@
// 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 "../../_borders.scss" as *;
@use "../../_sizes.scss" as *;
@use "../../typography.scss" as t;
@use "../../colors.scss" as *;
.token-field {
--token-field-bg-color: var(--color-background-tertiary);
--token-field-fg-color: var(--color-foreground-primary);
--token-field-icon-color: var(--color-foreground-secondary);
--token-field-outline-color: none;
--token-field-height: var(--sp-xxxl);
--token-field-margin: unset;
display: grid;
grid-template-columns: 1fr auto;
column-gap: var(--sp-xs);
align-items: center;
position: relative;
inline-size: 100%;
background: var(--token-field-bg-color);
border-radius: $br-8;
padding: var(--sp-xs);
outline: $b-1 solid var(--token-field-outline-color);
&:hover {
--token-field-bg-color: var(--color-background-quaternary);
}
&:focus {
--token-field-bg-color: var(--color-background-primary);
--token-field-outline-color: var(--color-accent-primary);
}
}
.with-icon {
grid-template-columns: auto 1fr auto;
}
.token-field-disabled {
user-select: none;
--token-field-bg-color: var(--color-background-primary);
--token-field-outline-color: var(--color-background-quaternary);
&:hover {
--token-field-bg-color: var(--color-background-primary);
--token-field-outline-color: var(--color-background-quaternary);
}
}
.pill {
--pill-border-color: var(--color-token-border);
--pill-bg-color: var(--color-background-tertiary);
--pill-fg-color: var(--color-token-foreground);
@include t.use-typography("code-font");
height: var(--sp-xxl);
width: fit-content;
background: var(--pill-bg-color);
cursor: pointer;
border: $b-1 solid var(--pill-border-color);
color: var(--pill-fg-color);
border-radius: $br-6;
padding-inline: $sz-6;
&:hover {
--pill-bg-color: var(--color-token-background);
--pill-fg-color: var(--color-foreground-primary);
--pill-border-color: var(--color-token-foreground);
}
&:focus-visible {
--pill-bg-color: var(--color-token-background);
--pill-fg-color: var(--color-foreground-primary);
--pill-border-color: var(--color-accent-primary);
outline: none;
}
}
.pill-disabled {
user-select: none;
--pill-bg-color: none;
--pill-fg-color: var(--color-foreground-secondary);
--pill-border-color: var(--color-token-border);
&:hover {
--pill-bg-color: none;
--pill-fg-color: var(--color-foreground-secondary);
--pill-border-color: var(--color-token-border);
}
}
.no-set-pill {
--pill-bg-color: none;
--pill-fg-color: var(--color-foreground-secondary);
--pill-border-color: var(--color-token-border);
position: relative;
&:hover {
--pill-bg-color: none;
--pill-fg-color: var(--color-foreground-secondary);
--pill-border-color: var(--color-token-border);
}
}
.pill-dot {
width: $sz-6;
height: $sz-6;
outline: var(--sp-xxs) solid var(--color-background-primary);
border-radius: 50%;
background-color: var(--color-foreground-error);
margin-left: var(--sp-xs);
position: absolute;
right: 0;
top: 0;
}
.invisible-button {
opacity: var(--opacity-button);
&:hover {
--opacity-button: 1;
}
&:focus {
--opacity-button: 1;
}
}

View File

@ -280,6 +280,7 @@
(def ^:icon-id text-uppercase "text-uppercase")
(def ^:icon-id thumbnail "thumbnail")
(def ^:icon-id tick "tick")
(def ^:icon-id tokens "tokens")
(def ^:icon-id to-corner "to-corner")
(def ^:icon-id to-curve "to-curve")
(def ^:icon-id tree "tree")

View File

@ -38,9 +38,11 @@
(true? (.-editing event)))
(defn is-key?
[^string key]
(fn [^KeyboardEvent e]
(= (.-key e) key)))
([^string key]
(fn [^KeyboardEvent event]
(= (.-key event) key)))
([^KeyboardEvent event ^string key]
(= (.-key event) key)))
(defn is-key-ignore-case?
[^string key]

View File

@ -3610,6 +3610,9 @@ msgstr "Detach"
msgid "settings.multiple"
msgstr "Mixed"
msgid "labels.mixed-values"
msgstr "Mixed"
#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:274
msgid "settings.remove-color"
msgstr "Remove color"
@ -7641,6 +7644,26 @@ msgstr "Invalid value: % is not allowed."
msgid "workspace.tokens.warning-name-change"
msgstr "Renaming this token will break any reference to its old name."
#: src/app/main/ui/ds/controls/numeric_input.cljs
msgid "ds.inputs.token-field.no-active-token-option"
msgstr "This token is not available in any active set or theme."
#: src/app/main/ui/ds/controls/numeric_input.cljs
msgid "ds.inputs.token-field.detach-token"
msgstr "Detach token"
#: src/app/main/ui/ds/controls/numeric_input.cljs
msgid "ds.inputs.numeric-input.no-applicable-tokens"
msgstr "No applicable tokens in active sets or themes."
#: src/app/main/ui/ds/controls/numeric_input.cljs
msgid "ds.inputs.numeric-input.no-matches"
msgstr "No matches found."
#: src/app/main/ui/ds/controls/numeric_input.cljs
msgid "ds.inputs.numeric-input.open-token-list-dropdown"
msgstr "Open token list"
#: src/app/main/ui/workspace/sidebar.cljs:137, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Assets"

View File

@ -3613,6 +3613,9 @@ msgstr "Desvincular"
msgid "settings.multiple"
msgstr "Varios"
msgid "labels.mixed-values"
msgstr "Varios"
#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:274
msgid "settings.remove-color"
msgstr "Eliminar color"
@ -7530,6 +7533,26 @@ msgstr "Valor no válido: No se permiten unidades."
msgid "workspace.tokens.warning-name-change"
msgstr "Al renombrar este token se romperán las referencias al nombre anterior"
#: src/app/main/ui/ds/controls/numeric_input.cljs
msgid "ds.inputs.token-field.no-active-token-option"
msgstr "Este token no está disponible en ningún set ni tema activo."
#: src/app/main/ui/ds/controls/numeric_input.cljs
msgid "ds.inputs.token-field.detach-token"
msgstr "Desvincular token"
#: src/app/main/ui/ds/controls/numeric_input.cljs
msgid "ds.inputs.numeric-input.no-applicable-tokens"
msgstr "No hay tokens aplicables en sets o temas activos."
#: src/app/main/ui/ds/controls/numeric_input.cljs
msgid "ds.inputs.numeric-input.no-matches"
msgstr "No hay coincidencias"
#: src/app/main/ui/ds/controls/numeric_input.cljs
msgid "ds.inputs.numeric-input.open-token-list-dropdown"
msgstr "Abrir lista de tokens"
#: src/app/main/ui/workspace/sidebar.cljs:137, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Recursos"