@hilum/ui 3.3.4 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms.txt CHANGED
@@ -5,7 +5,7 @@
5
5
  ## Key Facts
6
6
 
7
7
  - Package name: `@hilum/ui`
8
- - Package version: 3.3.0
8
+ - Package version: 3.3.4
9
9
  - Component modules: 85
10
10
  - Atom components: 63
11
11
  - Molecule components: 22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hilum/ui",
3
- "version": "3.3.4",
3
+ "version": "3.4.0",
4
4
  "private": false,
5
5
  "description": "Hilum UI — design system primitives (Button, Input, Dialog, Combobox, …) + tokens + fonts + icons",
6
6
  "license": "MIT",
@@ -7,17 +7,21 @@ interface InputNumberProps extends Omit<
7
7
  React.ComponentProps<"input">,
8
8
  "type" | "value" | "onChange"
9
9
  > {
10
- value: number;
10
+ value: number | null;
11
11
  onChange: (next: number) => void;
12
- min?: number;
13
- max?: number;
14
- step?: number;
12
+ min?: number | undefined;
13
+ max?: number | undefined;
14
+ step?: number | undefined;
15
15
  /** Suffix shown after the number (e.g. "px", "mm", "°", "%"). */
16
- unit?: string;
16
+ unit?: string | undefined;
17
17
  /** Number of decimals to display. Default: 0. */
18
- precision?: number;
18
+ precision?: number | undefined;
19
+ /** Text shown when value is null. Useful for mixed multi-selection values. */
20
+ mixedLabel?: string | undefined;
21
+ /** Calls onChange while typing instead of waiting for blur/Enter. */
22
+ commitOnChange?: boolean | undefined;
19
23
  /** Hides the up/down stepper buttons. */
20
- hideSteppers?: boolean;
24
+ hideSteppers?: boolean | undefined;
21
25
  }
22
26
 
23
27
  /**
@@ -34,39 +38,59 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
34
38
  step = 1,
35
39
  unit,
36
40
  precision = 0,
41
+ mixedLabel = "Mixed",
42
+ commitOnChange = false,
37
43
  hideSteppers = false,
38
44
  className,
39
45
  disabled,
46
+ onBlur,
47
+ onFocus,
48
+ onKeyDown,
40
49
  ...rest
41
50
  },
42
51
  ref,
43
52
  ) => {
44
- const [text, setText] = React.useState<string>(value.toFixed(precision));
53
+ const formatValue = React.useCallback(
54
+ (next: number | null) => (next === null ? mixedLabel : next.toFixed(precision)),
55
+ [mixedLabel, precision],
56
+ );
57
+
58
+ const [text, setText] = React.useState<string>(formatValue(value));
45
59
 
46
60
  React.useEffect(() => {
47
- setText(value.toFixed(precision));
48
- }, [value, precision]);
61
+ setText(formatValue(value));
62
+ }, [formatValue, value]);
49
63
 
50
64
  const clamp = React.useCallback((n: number) => Math.min(max, Math.max(min, n)), [min, max]);
51
65
 
52
- const commit = (raw: string) => {
66
+ const parseAndClamp = React.useCallback((raw: string) => {
53
67
  const n = parseFloat(raw);
54
68
  if (Number.isFinite(n)) {
55
- const next = clamp(n);
56
- onChange(next);
57
- setText(next.toFixed(precision));
58
- } else {
59
- setText(value.toFixed(precision));
69
+ return clamp(n);
60
70
  }
71
+ return null;
72
+ }, [clamp]);
73
+
74
+ const commit = (raw: string) => {
75
+ const next = parseAndClamp(raw);
76
+ if (next === null) {
77
+ setText(formatValue(value));
78
+ return;
79
+ }
80
+ onChange(next);
81
+ setText(next.toFixed(precision));
61
82
  };
62
83
 
63
84
  const bump = (delta: number) => {
64
- const next = clamp(value + delta);
85
+ const next = clamp((value ?? 0) + delta);
65
86
  onChange(next);
66
87
  setText(next.toFixed(precision));
67
88
  };
68
89
 
69
- const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
90
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
91
+ onKeyDown?.(e);
92
+ if (e.defaultPrevented) return;
93
+
70
94
  if (e.key === "ArrowUp") {
71
95
  e.preventDefault();
72
96
  bump(step * (e.shiftKey ? 10 : 1));
@@ -93,9 +117,26 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
93
117
  inputMode="decimal"
94
118
  spellCheck={false}
95
119
  value={text}
96
- onChange={(e) => setText(e.target.value)}
97
- onBlur={(e) => commit(e.target.value)}
98
- onKeyDown={onKeyDown}
120
+ onChange={(e) => {
121
+ setText(e.target.value);
122
+ if (commitOnChange) {
123
+ const next = parseAndClamp(e.target.value);
124
+ if (next !== null) {
125
+ onChange(next);
126
+ }
127
+ }
128
+ }}
129
+ onFocus={(e) => {
130
+ if (value === null) {
131
+ setText("");
132
+ }
133
+ onFocus?.(e);
134
+ }}
135
+ onBlur={(e) => {
136
+ commit(e.target.value);
137
+ onBlur?.(e);
138
+ }}
139
+ onKeyDown={handleKeyDown}
99
140
  disabled={disabled}
100
141
  className="w-full caption text-foreground px-2 bg-transparent focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none text-right"
101
142
  {...rest}