mirror of https://github.com/penpot/penpot.git
🐛 Fix numeric input errors detected on review (#7427)
* 🐛 Fix review errors * 📚 Add docs for numeric input component * 🐛 Sort tokens alphabetically
This commit is contained in:
parent
93d4b19477
commit
a4f20564af
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -44,8 +44,9 @@ export default {
|
|||
property: "search",
|
||||
},
|
||||
parameters: {
|
||||
controls: { exclude: ["tokens"] },
|
||||
controls: { exclude: ["tokens"] }
|
||||
},
|
||||
|
||||
render: ({ ...args }) => <NumericInput {...args} />,
|
||||
};
|
||||
|
||||
|
|
@ -116,6 +117,14 @@ export const WithTokens = {
|
|||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
controls: { exclude: ["tokens"] },
|
||||
docs: {
|
||||
story: {
|
||||
height: "320px",
|
||||
},
|
||||
},
|
||||
},
|
||||
render: ({ ...args }) => <NumericInput {...args} />,
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
<Meta title="Controls/Numeric Input" />
|
||||
|
||||
# Numeric Input
|
||||
# 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.
|
||||
<Canvas of={InputStories.Default} />
|
||||
|
||||
### 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.
|
||||
<Canvas of={InputStories.WithTokens} />
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue