From a4f20564afd7aee66ccad6996b4131ed8a8d8db4 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 3 Oct 2025 10:14:25 +0200 Subject: [PATCH] :bug: Fix numeric input errors detected on review (#7427) * :bug: Fix review errors * :books: Add docs for numeric input component * :bug: Sort tokens alphabetically --- .../data/workspace/tokens/application.cljs | 6 +- ....stories.jsx => numeric-input.stories.jsx} | 11 ++- .../main/ui/ds/controls/numeric_input.cljs | 36 ++++++++- .../app/main/ui/ds/controls/numeric_input.mdx | 75 ++++++++++++++++++- .../main/ui/ds/controls/numeric_input.scss | 3 + .../ui/ds/controls/utilities/input_field.cljs | 18 ++--- .../ui/ds/controls/utilities/input_field.scss | 2 +- .../ui/ds/controls/utilities/token_field.scss | 2 +- 8 files changed, 133 insertions(+), 20 deletions(-) rename frontend/src/app/main/ui/ds/controls/{numeric_input.stories.jsx => numeric-input.stories.jsx} (95%) diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index bc47fb8c21..47caaaf64c 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -566,13 +566,13 @@ (watch [_ _ _] (let [{:keys [attributes all-attributes on-update-shape]} (get token-properties (:type token)) - unapply-tokens? - (cft/shapes-token-applied? token shapes (or all-attributes attributes)) + unapply-tokens? + (cft/shapes-token-applied? token shapes (or attrs all-attributes attributes)) shape-ids (map :id shapes)] (if unapply-tokens? (rx/of - (unapply-token {:attributes (or all-attributes attributes) + (unapply-token {:attributes (or attrs all-attributes attributes) :token token :shape-ids shape-ids})) (rx/of diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.stories.jsx b/frontend/src/app/main/ui/ds/controls/numeric-input.stories.jsx similarity index 95% rename from frontend/src/app/main/ui/ds/controls/numeric_input.stories.jsx rename to frontend/src/app/main/ui/ds/controls/numeric-input.stories.jsx index f3d2d20c8c..43d2971c42 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.stories.jsx +++ b/frontend/src/app/main/ui/ds/controls/numeric-input.stories.jsx @@ -44,8 +44,9 @@ export default { property: "search", }, parameters: { - controls: { exclude: ["tokens"] }, + controls: { exclude: ["tokens"] } }, + render: ({ ...args }) => , }; @@ -116,6 +117,14 @@ export const WithTokens = { }, ], }, + }, + parameters: { + controls: { exclude: ["tokens"] }, + docs: { + story: { + height: "320px", + }, + }, }, render: ({ ...args }) => , }; diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs index 1267c8caf6..ed11c7b278 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs @@ -149,6 +149,32 @@ 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 %)]]) @@ -260,11 +286,13 @@ (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 tokens partial) - tokens) - no-sets? (nil? tokens)] + (filter-token-groups-by-name sorted-tokens partial) + sorted-tokens) + no-sets? (nil? sorted-tokens)] (generate-dropdown-options options no-sets?)))) selected-id* @@ -601,6 +629,7 @@ {:content property :id property} [:> icon* {:icon-id icon + :size "s" :aria-labelledby property :class (stl/css :icon)}]])) :slot-end (when-not disabled @@ -634,6 +663,7 @@ {:content property :id property} [:> icon* {:icon-id icon + :size "s" :aria-labelledby property :class (stl/css :icon)}]])) :token-wrapper-ref token-wrapper-ref diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.mdx b/frontend/src/app/main/ui/ds/controls/numeric_input.mdx index 9eaac5dd46..def6d701d4 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.mdx +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.mdx @@ -4,8 +4,79 @@ Copyright (c) KALEIDOS INC */ } import { Canvas, Meta } from '@storybook/blocks'; -import * as InputStories from "./numeric_input.stories"; +import * as InputStories from "./numeric-input.stories"; -# Numeric Input \ No newline at end of file +# Numeric Input + +The `numeric-input*` component allows users to enter numerical values, apply tokens, or perform simple mathematical expressions. +It combines a numeric input field with token integration, making it flexible for design systems where values can come from either fixed numbers or token references. + +## Variants + +### Default +The standard numeric input that accepts direct numeric input. + + +### With Tokens +When tokens are available, the user can open a dropdown and apply a token instead of entering a fixed number. +The token will be displayed inside the input field and can be detached if needed. + + +## Technical notes + +### Tokens + +- Numeric input supports applying tokens via a dropdown that opens when typing `{` or clicking the token button. +- Tokens are grouped and searchable. +- Once a token is applied, the input field is replaced with a **token field**, showing the token name and its resolved value. + +### Validation & Math Expressions + +- The input accepts simple math expressions (e.g. `2+2`, `100/4`) and resolves them on blur. +- Values are automatically clamped to the provided `min` and `max` range. +- If the input is left empty and `nillable` is enabled, the value can be `nil`. + +### Icons + +`numeric-input*` supports optional icons at the start of the field for additional context. +Icons come from the `app.main.ui.ds.foundations.assets.icon` namespace. + +```clj +(ns app.main.ui.foo + (:require + [app.main.ui.ds.foundations.assets.icon :as i])) + +[:> numeric-input* + {:icon i/hash + :placeholder "Enter number" + :min 0 + :max 100 + :step 1}] +``` + +## Usage guidelines (design) +### Where to Use + +Use numeric input in forms and properties panels where users need to adjust numerical values, such as sizes, spacings, or opacities. + +### When to Use + +When users must provide a numeric value within a defined range. + +When design tokens can be applied instead of raw numbers. + +When supporting advanced workflows where math expressions improve speed. + +### Interaction / Behavior + +Typing Numbers: The user can type numbers directly in the field. + +Using Tokens: Typing `{` opens a token dropdown; selecting one replaces the raw value with the token. + +Increment / Decrement: Users can adjust values with arrow keys ↑/↓ or mouse wheel. + +Validation: Invalid or out-of-range inputs fall back to the last valid value or the default. + +Detaching Tokens: Applied tokens can be removed via the detach button or with Backspace/Delete. diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.scss b/frontend/src/app/main/ui/ds/controls/numeric_input.scss index a30b94d82b..ec9b2aec69 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.scss +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.scss @@ -7,9 +7,12 @@ @use "ds/_borders.scss" as *; @use "ds/spacing.scss" as *; @use "ds/_sizes.scss" as *; +@use "ds/typography.scss" as t; .input-wrapper { + --input-padding-size: var(--sp-xs); --opacity-button: 0; + @include t.use-typography("code-font"); display: flex; flex-direction: column; gap: var(--sp-xs); diff --git a/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs b/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs index 23ca7eab25..4a8bcb1554 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs +++ b/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs @@ -6,7 +6,6 @@ (ns app.main.ui.ds.controls.utilities.input-field (:require-macros - [app.common.data.macros :as dm] [app.main.style :as stl]) (:require [app.common.data :as d] @@ -58,6 +57,14 @@ :type (d/nilv type "text") :id id :max-length (d/nilv max-length max-input-length)}) + inside-class (stl/css-case :input-wrapper true + :has-hint has-hint + :hint-type-hint (= hint-type "hint") + :hint-type-warning (= hint-type "warning") + :hint-type-error (= hint-type "error") + :variant-seamless (= variant "seamless") + :variant-dense (= variant "dense") + :variant-comfortable (= variant "comfortable")) on-icon-click (mf/use-fn (mf/deps ref) @@ -66,14 +73,7 @@ (dom/select-node input-node) (dom/focus! input-node))))] - [:div {:class (dm/str class " " (stl/css-case :input-wrapper true - :has-hint has-hint - :hint-type-hint (= hint-type "hint") - :hint-type-warning (= hint-type "warning") - :hint-type-error (= hint-type "error") - :variant-seamless (= variant "seamless") - :variant-dense (= variant "dense") - :variant-comfortable (= variant "comfortable")))} + [:div {:class [inside-class class]} (when (some? slot-start) slot-start) (when (some? icon) diff --git a/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss index 08d00e7807..3e95b194d9 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss +++ b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss @@ -25,7 +25,7 @@ background: var(--input-bg-color); border-radius: $br-8; - padding: 0 var(--sp-s); + padding: 0 var(--input-padding-size, var(--sp-s)); outline: $b-1 solid var(--input-outline-color); &:hover { diff --git a/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss index 07a60ccb05..37498bdf7e 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss +++ b/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss @@ -25,7 +25,7 @@ inline-size: 100%; background: var(--token-field-bg-color); border-radius: $br-8; - padding: var(--sp-xs) var(--sp-s); + padding: var(--sp-xs); outline: $b-1 solid var(--token-field-outline-color); &:hover {