@nexus-cross/design-system 1.0.4 → 1.0.5

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.
@@ -0,0 +1,368 @@
1
+ import { cn } from './chunk-MCKOWMLS.mjs';
2
+ import * as React from 'react';
3
+ import { cva } from 'class-variance-authority';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+
6
+ var numberInputVariants = cva("nexus-number-input", {
7
+ variants: {
8
+ variant: {
9
+ basic: "nexus-number-input--basic",
10
+ bind: "nexus-number-input--bind"
11
+ },
12
+ size: {
13
+ lg: "nexus-number-input--lg",
14
+ xl: "nexus-number-input--xl"
15
+ }
16
+ },
17
+ defaultVariants: { variant: "basic", size: "lg" }
18
+ });
19
+ var CHEVRON_PATH = "M3.606.179C3.82-.06 4.18-.06 4.394.179L7.846 4.01C8.18 4.382 7.934 5 7.452 5H.548C.066 5-.18 4.382.154 4.01L3.606.179Z";
20
+ var ChevronUpIcon = () => /* @__PURE__ */ jsx(
21
+ "svg",
22
+ {
23
+ className: "nexus-number-input__chevron-icon",
24
+ viewBox: "0 0 8 5",
25
+ fill: "currentColor",
26
+ children: /* @__PURE__ */ jsx("path", { d: CHEVRON_PATH })
27
+ }
28
+ );
29
+ var ChevronDownIcon = () => /* @__PURE__ */ jsx(
30
+ "svg",
31
+ {
32
+ className: "nexus-number-input__chevron-icon nexus-number-input__chevron-icon--down",
33
+ viewBox: "0 0 8 5",
34
+ fill: "currentColor",
35
+ children: /* @__PURE__ */ jsx("path", { d: CHEVRON_PATH })
36
+ }
37
+ );
38
+ var MinusIcon = () => /* @__PURE__ */ jsx(
39
+ "svg",
40
+ {
41
+ className: "nexus-number-input__bind-icon",
42
+ viewBox: "0 0 16 16",
43
+ fill: "none",
44
+ stroke: "currentColor",
45
+ strokeWidth: "1.33",
46
+ strokeLinecap: "round",
47
+ strokeLinejoin: "round",
48
+ children: /* @__PURE__ */ jsx("path", { d: "M3.333 8h9.334" })
49
+ }
50
+ );
51
+ var PlusIcon = () => /* @__PURE__ */ jsx(
52
+ "svg",
53
+ {
54
+ className: "nexus-number-input__bind-icon",
55
+ viewBox: "0 0 16 16",
56
+ fill: "none",
57
+ stroke: "currentColor",
58
+ strokeWidth: "1.33",
59
+ strokeLinecap: "round",
60
+ strokeLinejoin: "round",
61
+ children: /* @__PURE__ */ jsx("path", { d: "M3.333 8h9.334M8 3.333v9.334" })
62
+ }
63
+ );
64
+ function clampValue(val, min, max) {
65
+ let result = val;
66
+ if (min !== void 0) result = Math.max(min, result);
67
+ if (max !== void 0) result = Math.min(max, result);
68
+ return result;
69
+ }
70
+ function roundToDigit(val, digit) {
71
+ const factor = Math.pow(10, digit);
72
+ return Math.round(val * factor) / factor;
73
+ }
74
+ function numberInputBind(ref, direction) {
75
+ return {
76
+ onPointerDown: (e) => {
77
+ if (direction === "increment") ref.current?.startIncrement(e);
78
+ else ref.current?.startDecrement(e);
79
+ },
80
+ onPointerUp: () => ref.current?.stop(),
81
+ onPointerLeave: () => ref.current?.stop()
82
+ };
83
+ }
84
+ var NumberInput = React.forwardRef(
85
+ ({
86
+ className,
87
+ variant = "basic",
88
+ size,
89
+ error,
90
+ value,
91
+ min,
92
+ max,
93
+ step = 1,
94
+ digit = 0,
95
+ label,
96
+ description,
97
+ showMax,
98
+ hideButtons = false,
99
+ disabled,
100
+ readOnly,
101
+ onValueChange,
102
+ placeholder,
103
+ ...props
104
+ }, ref) => {
105
+ const inputRef = React.useRef(null);
106
+ const [internalValue, setInternalValue] = React.useState(
107
+ value?.toString() ?? ""
108
+ );
109
+ const internalValueRef = React.useRef(internalValue);
110
+ const timeoutRef = React.useRef(null);
111
+ const pressCountRef = React.useRef(0);
112
+ React.useEffect(() => {
113
+ internalValueRef.current = internalValue;
114
+ }, [internalValue]);
115
+ React.useEffect(() => {
116
+ setInternalValue(value?.toString() ?? "");
117
+ }, [value]);
118
+ const commitValue = React.useCallback(
119
+ (raw) => {
120
+ if (raw === "" || raw === "-") {
121
+ setInternalValue(raw);
122
+ onValueChange?.(void 0);
123
+ return;
124
+ }
125
+ const parsed = parseFloat(raw);
126
+ if (isNaN(parsed)) return;
127
+ const clamped = clampValue(roundToDigit(parsed, digit), min, max);
128
+ setInternalValue(clamped.toString());
129
+ onValueChange?.(clamped);
130
+ },
131
+ [digit, min, max, onValueChange]
132
+ );
133
+ const handleInputChange = React.useCallback(
134
+ (e) => {
135
+ const raw = e.target.value;
136
+ const regex = digit > 0 ? /^-?\d*\.?\d*$/ : /^-?\d*$/;
137
+ if (!regex.test(raw)) return;
138
+ setInternalValue(raw);
139
+ },
140
+ [digit]
141
+ );
142
+ const handleBlur = React.useCallback(() => {
143
+ commitValue(internalValue);
144
+ }, [internalValue, commitValue]);
145
+ const adjust = React.useCallback(
146
+ (delta) => {
147
+ const current = parseFloat(internalValueRef.current) || 0;
148
+ const next = clampValue(roundToDigit(current + delta, digit), min, max);
149
+ const nextStr = next.toString();
150
+ internalValueRef.current = nextStr;
151
+ setInternalValue(nextStr);
152
+ onValueChange?.(next);
153
+ },
154
+ [digit, min, max, onValueChange]
155
+ );
156
+ const handleKeyDown = React.useCallback(
157
+ (e) => {
158
+ if (e.key === "Enter") commitValue(internalValue);
159
+ if (e.key === "ArrowUp") {
160
+ e.preventDefault();
161
+ adjust(step);
162
+ }
163
+ if (e.key === "ArrowDown") {
164
+ e.preventDefault();
165
+ adjust(-step);
166
+ }
167
+ },
168
+ [internalValue, commitValue, step, adjust]
169
+ );
170
+ const stopRepeat = React.useCallback(() => {
171
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
172
+ timeoutRef.current = null;
173
+ pressCountRef.current = 0;
174
+ }, []);
175
+ const startRepeat = React.useCallback(
176
+ (delta, e) => {
177
+ e.preventDefault();
178
+ adjust(delta);
179
+ pressCountRef.current = 0;
180
+ timeoutRef.current = setTimeout(() => {
181
+ const accelerate = () => {
182
+ adjust(delta);
183
+ pressCountRef.current += 1;
184
+ let delay = 100;
185
+ if (pressCountRef.current > 20) delay = 30;
186
+ else if (pressCountRef.current > 10) delay = 50;
187
+ else if (pressCountRef.current > 5) delay = 75;
188
+ timeoutRef.current = setTimeout(accelerate, delay);
189
+ };
190
+ accelerate();
191
+ }, 400);
192
+ },
193
+ [adjust]
194
+ );
195
+ React.useEffect(() => () => stopRepeat(), [stopRepeat]);
196
+ const startRepeatRef = React.useRef(startRepeat);
197
+ startRepeatRef.current = startRepeat;
198
+ const stopRepeatRef = React.useRef(stopRepeat);
199
+ stopRepeatRef.current = stopRepeat;
200
+ const adjustRef = React.useRef(adjust);
201
+ adjustRef.current = adjust;
202
+ React.useImperativeHandle(
203
+ ref,
204
+ () => ({
205
+ get input() {
206
+ return inputRef.current;
207
+ },
208
+ increment: () => adjustRef.current(step),
209
+ decrement: () => adjustRef.current(-step),
210
+ startIncrement: (e) => startRepeatRef.current(step, e),
211
+ startDecrement: (e) => startRepeatRef.current(-step, e),
212
+ stop: () => stopRepeatRef.current()
213
+ }),
214
+ [step]
215
+ );
216
+ const handleMaxClick = React.useCallback(() => {
217
+ if (disabled || readOnly || max === void 0) return;
218
+ const maxStr = max.toString();
219
+ internalValueRef.current = maxStr;
220
+ setInternalValue(maxStr);
221
+ onValueChange?.(max);
222
+ inputRef.current?.focus();
223
+ }, [disabled, readOnly, max, onValueChange]);
224
+ const numValue = parseFloat(internalValue) || 0;
225
+ const isMinDisabled = min !== void 0 && numValue <= min;
226
+ const isMaxDisabled = max !== void 0 && numValue >= max;
227
+ const isMaxExceeded = max !== void 0 && numValue > max;
228
+ const displayMax = showMax ?? max !== void 0;
229
+ const showHeader = !!label || displayMax;
230
+ const isError = error || isMaxExceeded;
231
+ const showBtns = !hideButtons && !readOnly;
232
+ const isBasic = variant !== "bind";
233
+ return /* @__PURE__ */ jsxs(
234
+ "div",
235
+ {
236
+ className: cn(
237
+ numberInputVariants({ variant, size }),
238
+ isError && "nexus-number-input--error",
239
+ disabled && "nexus-number-input--disabled",
240
+ className
241
+ ),
242
+ children: [
243
+ showHeader && /* @__PURE__ */ jsxs("div", { className: "nexus-number-input__header", children: [
244
+ label && /* @__PURE__ */ jsx("span", { className: "nexus-number-input__label", children: label }),
245
+ displayMax && max !== void 0 && /* @__PURE__ */ jsxs(
246
+ "button",
247
+ {
248
+ type: "button",
249
+ className: "nexus-number-input__max",
250
+ onClick: handleMaxClick,
251
+ disabled: disabled || readOnly,
252
+ tabIndex: -1,
253
+ children: [
254
+ /* @__PURE__ */ jsx("span", { className: "nexus-number-input__max-text", children: "Max" }),
255
+ /* @__PURE__ */ jsx(
256
+ "span",
257
+ {
258
+ className: cn(
259
+ "nexus-number-input__max-value",
260
+ isMaxExceeded && "nexus-number-input__max-value--exceeded"
261
+ ),
262
+ children: max
263
+ }
264
+ )
265
+ ]
266
+ }
267
+ )
268
+ ] }),
269
+ /* @__PURE__ */ jsxs("div", { className: "nexus-number-input__container", children: [
270
+ !isBasic && showBtns && /* @__PURE__ */ jsx(
271
+ "button",
272
+ {
273
+ type: "button",
274
+ tabIndex: -1,
275
+ disabled: disabled || isMinDisabled,
276
+ className: "nexus-number-input__bind-btn",
277
+ onPointerDown: (e) => startRepeat(-step, e),
278
+ onPointerUp: stopRepeat,
279
+ onPointerLeave: stopRepeat,
280
+ "aria-label": "Decrease",
281
+ children: /* @__PURE__ */ jsx(MinusIcon, {})
282
+ }
283
+ ),
284
+ /* @__PURE__ */ jsx(
285
+ "input",
286
+ {
287
+ ref: inputRef,
288
+ type: "text",
289
+ inputMode: "decimal",
290
+ role: "spinbutton",
291
+ className: "nexus-number-input__field",
292
+ value: internalValue,
293
+ disabled,
294
+ readOnly,
295
+ placeholder: placeholder ?? "0",
296
+ "aria-invalid": isError || void 0,
297
+ "aria-valuemin": min,
298
+ "aria-valuemax": max,
299
+ "aria-valuenow": internalValue ? parseFloat(internalValue) || void 0 : void 0,
300
+ onChange: handleInputChange,
301
+ onBlur: handleBlur,
302
+ onKeyDown: handleKeyDown,
303
+ ...props
304
+ }
305
+ ),
306
+ !isBasic && showBtns && /* @__PURE__ */ jsx(
307
+ "button",
308
+ {
309
+ type: "button",
310
+ tabIndex: -1,
311
+ disabled: disabled || isMaxDisabled,
312
+ className: "nexus-number-input__bind-btn",
313
+ onPointerDown: (e) => startRepeat(step, e),
314
+ onPointerUp: stopRepeat,
315
+ onPointerLeave: stopRepeat,
316
+ "aria-label": "Increase",
317
+ children: /* @__PURE__ */ jsx(PlusIcon, {})
318
+ }
319
+ ),
320
+ isBasic && showBtns && /* @__PURE__ */ jsxs("div", { className: "nexus-number-input__buttons", children: [
321
+ /* @__PURE__ */ jsx(
322
+ "button",
323
+ {
324
+ type: "button",
325
+ tabIndex: -1,
326
+ disabled: disabled || isMaxDisabled,
327
+ className: "nexus-number-input__step nexus-number-input__step--up",
328
+ onPointerDown: (e) => startRepeat(step, e),
329
+ onPointerUp: stopRepeat,
330
+ onPointerLeave: stopRepeat,
331
+ "aria-label": "Increase",
332
+ children: /* @__PURE__ */ jsx(ChevronUpIcon, {})
333
+ }
334
+ ),
335
+ /* @__PURE__ */ jsx(
336
+ "button",
337
+ {
338
+ type: "button",
339
+ tabIndex: -1,
340
+ disabled: disabled || isMinDisabled,
341
+ className: "nexus-number-input__step nexus-number-input__step--down",
342
+ onPointerDown: (e) => startRepeat(-step, e),
343
+ onPointerUp: stopRepeat,
344
+ onPointerLeave: stopRepeat,
345
+ "aria-label": "Decrease",
346
+ children: /* @__PURE__ */ jsx(ChevronDownIcon, {})
347
+ }
348
+ )
349
+ ] })
350
+ ] }),
351
+ description && /* @__PURE__ */ jsx(
352
+ "p",
353
+ {
354
+ className: cn(
355
+ "nexus-number-input__description",
356
+ isError && "nexus-number-input__description--error"
357
+ ),
358
+ children: description
359
+ }
360
+ )
361
+ ]
362
+ }
363
+ );
364
+ }
365
+ );
366
+ NumberInput.displayName = "NumberInput";
367
+
368
+ export { NumberInput, numberInputBind, numberInputVariants };