@rovula/ui 0.0.76 → 0.0.77
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/dist/cjs/bundle.css +12 -0
- package/dist/cjs/bundle.js +3 -3
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +7 -0
- package/dist/cjs/types/components/InputFilter/InputFilter.stories.d.ts +7 -0
- package/dist/cjs/types/components/NumberInput/NumberInput.d.ts +39 -0
- package/dist/cjs/types/components/NumberInput/NumberInput.stories.d.ts +18 -0
- package/dist/cjs/types/components/NumberInput/index.d.ts +2 -0
- package/dist/cjs/types/components/RadioGroup/RadioGroup.stories.d.ts +1 -1
- package/dist/cjs/types/components/Search/Search.stories.d.ts +7 -0
- package/dist/cjs/types/components/Slider/Slider.stories.d.ts +1 -1
- package/dist/cjs/types/components/TextInput/TextInput.d.ts +14 -0
- package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +14 -0
- package/dist/cjs/types/index.d.ts +2 -0
- package/dist/components/NumberInput/NumberInput.js +254 -0
- package/dist/components/NumberInput/NumberInput.stories.js +212 -0
- package/dist/components/NumberInput/index.js +1 -0
- package/dist/components/TextInput/TextInput.js +13 -11
- package/dist/esm/bundle.css +12 -0
- package/dist/esm/bundle.js +3 -3
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +7 -0
- package/dist/esm/types/components/InputFilter/InputFilter.stories.d.ts +7 -0
- package/dist/esm/types/components/NumberInput/NumberInput.d.ts +39 -0
- package/dist/esm/types/components/NumberInput/NumberInput.stories.d.ts +18 -0
- package/dist/esm/types/components/NumberInput/index.d.ts +2 -0
- package/dist/esm/types/components/RadioGroup/RadioGroup.stories.d.ts +1 -1
- package/dist/esm/types/components/Search/Search.stories.d.ts +7 -0
- package/dist/esm/types/components/Slider/Slider.stories.d.ts +1 -1
- package/dist/esm/types/components/TextInput/TextInput.d.ts +14 -0
- package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +14 -0
- package/dist/esm/types/index.d.ts +2 -0
- package/dist/index.d.ts +52 -1
- package/dist/index.js +1 -0
- package/dist/src/theme/global.css +16 -0
- package/package.json +1 -1
- package/src/components/NumberInput/NumberInput.stories.tsx +350 -0
- package/src/components/NumberInput/NumberInput.tsx +428 -0
- package/src/components/NumberInput/index.ts +2 -0
- package/src/components/TextInput/TextInput.tsx +54 -12
- package/src/index.ts +2 -0
|
@@ -362,6 +362,13 @@ declare const meta: {
|
|
|
362
362
|
iconMode?: "flat" | "solid" | undefined;
|
|
363
363
|
keepCloseIconOnValue?: boolean | undefined;
|
|
364
364
|
labelClassName?: string | undefined;
|
|
365
|
+
classes?: {
|
|
366
|
+
iconWrapper?: string;
|
|
367
|
+
iconSearchWrapper?: string;
|
|
368
|
+
icon?: string;
|
|
369
|
+
startIconWrapper?: string;
|
|
370
|
+
endIconWrapper?: string;
|
|
371
|
+
} | undefined;
|
|
365
372
|
onClickStartIcon?: (() => void) | undefined;
|
|
366
373
|
onClickEndIcon?: (() => void) | undefined;
|
|
367
374
|
renderStartIcon?: (() => React.ReactNode) | undefined;
|
|
@@ -352,6 +352,13 @@ declare const meta: {
|
|
|
352
352
|
iconMode?: "flat" | "solid" | undefined;
|
|
353
353
|
keepCloseIconOnValue?: boolean | undefined;
|
|
354
354
|
labelClassName?: string | undefined;
|
|
355
|
+
classes?: {
|
|
356
|
+
iconWrapper?: string;
|
|
357
|
+
iconSearchWrapper?: string;
|
|
358
|
+
icon?: string;
|
|
359
|
+
startIconWrapper?: string;
|
|
360
|
+
endIconWrapper?: string;
|
|
361
|
+
} | undefined;
|
|
355
362
|
onClickStartIcon?: (() => void) | undefined;
|
|
356
363
|
onClickEndIcon?: (() => void) | undefined;
|
|
357
364
|
renderStartIcon?: (() => React.ReactNode) | undefined;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { InputProps as TextInputProps } from "../TextInput/TextInput";
|
|
3
|
+
export type NumberInputProps = Omit<TextInputProps, "type" | "value" | "defaultValue" | "onChange"> & {
|
|
4
|
+
value?: number | string;
|
|
5
|
+
defaultValue?: number | string;
|
|
6
|
+
min?: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
step?: number;
|
|
9
|
+
precision?: number;
|
|
10
|
+
hideControls?: boolean;
|
|
11
|
+
allowDecimal?: boolean;
|
|
12
|
+
allowNegative?: boolean;
|
|
13
|
+
formatDisplay?: boolean;
|
|
14
|
+
thousandSeparator?: string;
|
|
15
|
+
decimalSeparator?: string;
|
|
16
|
+
prefix?: string;
|
|
17
|
+
suffix?: string;
|
|
18
|
+
onChange?: (value: number | undefined) => void;
|
|
19
|
+
onValueChange?: (value: string) => void;
|
|
20
|
+
};
|
|
21
|
+
export declare const NumberInput: React.ForwardRefExoticComponent<Omit<TextInputProps, "type" | "onChange" | "value" | "defaultValue"> & {
|
|
22
|
+
value?: number | string;
|
|
23
|
+
defaultValue?: number | string;
|
|
24
|
+
min?: number;
|
|
25
|
+
max?: number;
|
|
26
|
+
step?: number;
|
|
27
|
+
precision?: number;
|
|
28
|
+
hideControls?: boolean;
|
|
29
|
+
allowDecimal?: boolean;
|
|
30
|
+
allowNegative?: boolean;
|
|
31
|
+
formatDisplay?: boolean;
|
|
32
|
+
thousandSeparator?: string;
|
|
33
|
+
decimalSeparator?: string;
|
|
34
|
+
prefix?: string;
|
|
35
|
+
suffix?: string;
|
|
36
|
+
onChange?: (value: number | undefined) => void;
|
|
37
|
+
onValueChange?: (value: string) => void;
|
|
38
|
+
} & React.RefAttributes<HTMLInputElement>>;
|
|
39
|
+
export default NumberInput;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { NumberInput } from "@/index";
|
|
3
|
+
declare const meta: Meta<typeof NumberInput>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof NumberInput>;
|
|
6
|
+
export declare const Default: Story;
|
|
7
|
+
export declare const WithMinMax: Story;
|
|
8
|
+
export declare const WithStep: Story;
|
|
9
|
+
export declare const IntegerOnly: Story;
|
|
10
|
+
export declare const ControllWithOutLine: Story;
|
|
11
|
+
export declare const WithoutControls: Story;
|
|
12
|
+
export declare const ErrorState: Story;
|
|
13
|
+
export declare const HelperText: Story;
|
|
14
|
+
export declare const Disabled: Story;
|
|
15
|
+
export declare const FormattedNumber: Story;
|
|
16
|
+
export declare const CurrencyFormat: Story;
|
|
17
|
+
export declare const CurrencyThailand: Story;
|
|
18
|
+
export declare const CustomSeparators: Story;
|
|
@@ -279,9 +279,9 @@ declare const meta: {
|
|
|
279
279
|
inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined | undefined;
|
|
280
280
|
is?: string | undefined | undefined;
|
|
281
281
|
required?: boolean | undefined;
|
|
282
|
+
onValueChange?: ((value: string) => void) | undefined;
|
|
282
283
|
loop?: boolean | undefined;
|
|
283
284
|
asChild?: boolean | undefined;
|
|
284
|
-
onValueChange?: ((value: string) => void) | undefined;
|
|
285
285
|
ref?: React.LegacyRef<HTMLDivElement> | undefined;
|
|
286
286
|
}>) => import("react/jsx-runtime").JSX.Element)[];
|
|
287
287
|
};
|
|
@@ -314,6 +314,13 @@ declare const meta: {
|
|
|
314
314
|
helperText?: string | undefined;
|
|
315
315
|
errorMessage?: string | undefined;
|
|
316
316
|
labelClassName?: string | undefined;
|
|
317
|
+
classes?: {
|
|
318
|
+
iconWrapper?: string;
|
|
319
|
+
iconSearchWrapper?: string;
|
|
320
|
+
icon?: string;
|
|
321
|
+
startIconWrapper?: string;
|
|
322
|
+
endIconWrapper?: string;
|
|
323
|
+
} | undefined;
|
|
317
324
|
onClickStartIcon?: (() => void) | undefined;
|
|
318
325
|
onClickEndIcon?: (() => void) | undefined;
|
|
319
326
|
renderStartIcon?: (() => React.ReactNode) | undefined;
|
|
@@ -289,10 +289,10 @@ declare const meta: {
|
|
|
289
289
|
unselectable?: "on" | "off" | undefined | undefined;
|
|
290
290
|
inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined | undefined;
|
|
291
291
|
is?: string | undefined | undefined;
|
|
292
|
+
onValueChange?: ((value: number[]) => void) | undefined;
|
|
292
293
|
asChild?: boolean | undefined;
|
|
293
294
|
inverted?: boolean | undefined;
|
|
294
295
|
minStepsBetweenThumbs?: number | undefined;
|
|
295
|
-
onValueChange?: ((value: number[]) => void) | undefined;
|
|
296
296
|
onValueCommit?: ((value: number[]) => void) | undefined;
|
|
297
297
|
ref?: React.LegacyRef<HTMLSpanElement> | undefined;
|
|
298
298
|
}>) => import("react/jsx-runtime").JSX.Element)[];
|
|
@@ -21,6 +21,13 @@ export type InputProps = {
|
|
|
21
21
|
endIcon?: ReactNode;
|
|
22
22
|
className?: string;
|
|
23
23
|
labelClassName?: string;
|
|
24
|
+
classes?: {
|
|
25
|
+
iconWrapper?: string;
|
|
26
|
+
iconSearchWrapper?: string;
|
|
27
|
+
icon?: string;
|
|
28
|
+
startIconWrapper?: string;
|
|
29
|
+
endIconWrapper?: string;
|
|
30
|
+
};
|
|
24
31
|
onClickStartIcon?: () => void;
|
|
25
32
|
onClickEndIcon?: () => void;
|
|
26
33
|
renderStartIcon?: () => ReactNode;
|
|
@@ -48,6 +55,13 @@ export declare const TextInput: React.ForwardRefExoticComponent<{
|
|
|
48
55
|
endIcon?: ReactNode;
|
|
49
56
|
className?: string;
|
|
50
57
|
labelClassName?: string;
|
|
58
|
+
classes?: {
|
|
59
|
+
iconWrapper?: string;
|
|
60
|
+
iconSearchWrapper?: string;
|
|
61
|
+
icon?: string;
|
|
62
|
+
startIconWrapper?: string;
|
|
63
|
+
endIconWrapper?: string;
|
|
64
|
+
};
|
|
51
65
|
onClickStartIcon?: () => void;
|
|
52
66
|
onClickEndIcon?: () => void;
|
|
53
67
|
renderStartIcon?: () => ReactNode;
|
|
@@ -23,6 +23,13 @@ declare const meta: {
|
|
|
23
23
|
endIcon?: React.ReactNode;
|
|
24
24
|
className?: string;
|
|
25
25
|
labelClassName?: string;
|
|
26
|
+
classes?: {
|
|
27
|
+
iconWrapper?: string;
|
|
28
|
+
iconSearchWrapper?: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
startIconWrapper?: string;
|
|
31
|
+
endIconWrapper?: string;
|
|
32
|
+
};
|
|
26
33
|
onClickStartIcon?: () => void;
|
|
27
34
|
onClickEndIcon?: () => void;
|
|
28
35
|
renderStartIcon?: () => React.ReactNode;
|
|
@@ -54,6 +61,13 @@ declare const meta: {
|
|
|
54
61
|
endIcon?: React.ReactNode;
|
|
55
62
|
className?: string | undefined;
|
|
56
63
|
labelClassName?: string | undefined;
|
|
64
|
+
classes?: {
|
|
65
|
+
iconWrapper?: string;
|
|
66
|
+
iconSearchWrapper?: string;
|
|
67
|
+
icon?: string;
|
|
68
|
+
startIconWrapper?: string;
|
|
69
|
+
endIconWrapper?: string;
|
|
70
|
+
} | undefined;
|
|
57
71
|
onClickStartIcon?: (() => void) | undefined;
|
|
58
72
|
onClickEndIcon?: (() => void) | undefined;
|
|
59
73
|
renderStartIcon?: (() => React.ReactNode) | undefined;
|
|
@@ -2,6 +2,7 @@ import "./theme/global.css";
|
|
|
2
2
|
import "./icons/iconConfig";
|
|
3
3
|
export { default as Button } from "./components/Button/Button";
|
|
4
4
|
export { default as TextInput } from "./components/TextInput/TextInput";
|
|
5
|
+
export { NumberInput } from "./components/NumberInput/NumberInput";
|
|
5
6
|
export { default as TextArea } from "./components/TextArea/TextArea";
|
|
6
7
|
export { default as Text } from "./components/Text/Text";
|
|
7
8
|
export { default as Tabs } from "./components/Tabs/Tabs";
|
|
@@ -38,6 +39,7 @@ export * from "./components/FocusedScrollView/FocusedScrollView";
|
|
|
38
39
|
export * from "./components/RadioGroup/RadioGroup";
|
|
39
40
|
export type { ButtonProps } from "./components/Button/Button";
|
|
40
41
|
export type { InputProps } from "./components/TextInput/TextInput";
|
|
42
|
+
export type { NumberInputProps } from "./components/NumberInput/NumberInput";
|
|
41
43
|
export type { TextAreaProps } from "./components/TextArea/TextArea";
|
|
42
44
|
export type { DropdownProps, Options } from "./components/Dropdown/Dropdown";
|
|
43
45
|
export type { NavbarProps } from "./components/Navbar/Navbar";
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState, } from "react";
|
|
14
|
+
import { TextInput, } from "../TextInput/TextInput";
|
|
15
|
+
import { ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
|
|
16
|
+
import { cn } from "@/utils/cn";
|
|
17
|
+
export const NumberInput = forwardRef((_a, ref) => {
|
|
18
|
+
var _b;
|
|
19
|
+
var { value, defaultValue, min, max, step = 1, precision, hideControls = false, allowDecimal = true, allowNegative = true, formatDisplay = false, thousandSeparator = ",", decimalSeparator = ".", prefix = "", suffix = "", disabled = false, onChange, onValueChange, className, size = "md" } = _a, props = __rest(_a, ["value", "defaultValue", "min", "max", "step", "precision", "hideControls", "allowDecimal", "allowNegative", "formatDisplay", "thousandSeparator", "decimalSeparator", "prefix", "suffix", "disabled", "onChange", "onValueChange", "className", "size"]);
|
|
20
|
+
const inputRef = useRef(null);
|
|
21
|
+
const [internalValue, setInternalValue] = useState(((_b = value !== null && value !== void 0 ? value : defaultValue) !== null && _b !== void 0 ? _b : "").toString());
|
|
22
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
23
|
+
useImperativeHandle(ref, () => inputRef === null || inputRef === void 0 ? void 0 : inputRef.current);
|
|
24
|
+
// Helper function to remove formatting
|
|
25
|
+
const unformatNumber = useCallback((formattedValue) => {
|
|
26
|
+
let cleaned = formattedValue;
|
|
27
|
+
// Remove prefix and suffix
|
|
28
|
+
if (prefix)
|
|
29
|
+
cleaned = cleaned.replace(prefix, "");
|
|
30
|
+
if (suffix)
|
|
31
|
+
cleaned = cleaned.replace(suffix, "");
|
|
32
|
+
// Remove thousand separators
|
|
33
|
+
if (thousandSeparator) {
|
|
34
|
+
cleaned = cleaned.split(thousandSeparator).join("");
|
|
35
|
+
}
|
|
36
|
+
// Replace decimal separator with standard dot
|
|
37
|
+
if (decimalSeparator !== ".") {
|
|
38
|
+
cleaned = cleaned.replace(decimalSeparator, ".");
|
|
39
|
+
}
|
|
40
|
+
return cleaned.trim();
|
|
41
|
+
}, [prefix, suffix, thousandSeparator, decimalSeparator]);
|
|
42
|
+
// Helper function to format number for display
|
|
43
|
+
const formatNumber = useCallback((numValue) => {
|
|
44
|
+
if (!formatDisplay || numValue === "" || numValue === "-")
|
|
45
|
+
return numValue;
|
|
46
|
+
const num = parseFloat(numValue);
|
|
47
|
+
if (isNaN(num))
|
|
48
|
+
return numValue;
|
|
49
|
+
let formatted = num.toString();
|
|
50
|
+
// Apply precision formatting
|
|
51
|
+
if (precision !== undefined) {
|
|
52
|
+
formatted = num.toFixed(precision);
|
|
53
|
+
}
|
|
54
|
+
// Split into integer and decimal parts
|
|
55
|
+
const parts = formatted.split(".");
|
|
56
|
+
let integerPart = parts[0];
|
|
57
|
+
const decimalPart = parts[1];
|
|
58
|
+
// Add thousand separators
|
|
59
|
+
if (thousandSeparator) {
|
|
60
|
+
integerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator);
|
|
61
|
+
}
|
|
62
|
+
// Combine with decimal separator
|
|
63
|
+
formatted = decimalPart
|
|
64
|
+
? `${integerPart}${decimalSeparator}${decimalPart}`
|
|
65
|
+
: integerPart;
|
|
66
|
+
// Add prefix and suffix
|
|
67
|
+
if (prefix)
|
|
68
|
+
formatted = `${prefix}${formatted}`;
|
|
69
|
+
if (suffix)
|
|
70
|
+
formatted = `${formatted}${suffix}`;
|
|
71
|
+
return formatted;
|
|
72
|
+
}, [
|
|
73
|
+
formatDisplay,
|
|
74
|
+
precision,
|
|
75
|
+
thousandSeparator,
|
|
76
|
+
decimalSeparator,
|
|
77
|
+
prefix,
|
|
78
|
+
suffix,
|
|
79
|
+
]);
|
|
80
|
+
const validateAndFormat = useCallback((val) => {
|
|
81
|
+
if (val === "" || val === "-")
|
|
82
|
+
return val;
|
|
83
|
+
let numValue = parseFloat(val);
|
|
84
|
+
if (isNaN(numValue))
|
|
85
|
+
return internalValue;
|
|
86
|
+
// Apply min/max constraints
|
|
87
|
+
if (min !== undefined && numValue < min)
|
|
88
|
+
numValue = min;
|
|
89
|
+
if (max !== undefined && numValue > max)
|
|
90
|
+
numValue = max;
|
|
91
|
+
// Apply precision
|
|
92
|
+
if (precision !== undefined) {
|
|
93
|
+
numValue = parseFloat(numValue.toFixed(precision));
|
|
94
|
+
}
|
|
95
|
+
return numValue.toString();
|
|
96
|
+
}, [min, max, precision, internalValue]);
|
|
97
|
+
const handleInputChange = useCallback((e) => {
|
|
98
|
+
let inputValue = e.target.value;
|
|
99
|
+
// Unformat the input if formatting is enabled
|
|
100
|
+
if (formatDisplay) {
|
|
101
|
+
inputValue = unformatNumber(inputValue);
|
|
102
|
+
}
|
|
103
|
+
// Allow empty input
|
|
104
|
+
if (inputValue === "") {
|
|
105
|
+
setInternalValue("");
|
|
106
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(undefined);
|
|
107
|
+
onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange("");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Allow negative sign at the start
|
|
111
|
+
if (allowNegative && inputValue === "-") {
|
|
112
|
+
setInternalValue("-");
|
|
113
|
+
onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange("-");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Filter non-numeric characters based on settings
|
|
117
|
+
const regex = allowDecimal
|
|
118
|
+
? allowNegative
|
|
119
|
+
? /^-?\d*\.?\d*$/
|
|
120
|
+
: /^\d*\.?\d*$/
|
|
121
|
+
: allowNegative
|
|
122
|
+
? /^-?\d*$/
|
|
123
|
+
: /^\d*$/;
|
|
124
|
+
if (!regex.test(inputValue)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
setInternalValue(inputValue);
|
|
128
|
+
onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(inputValue);
|
|
129
|
+
// Only call onChange with a valid number
|
|
130
|
+
const numValue = parseFloat(inputValue);
|
|
131
|
+
if (!isNaN(numValue)) {
|
|
132
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(numValue);
|
|
133
|
+
}
|
|
134
|
+
}, [
|
|
135
|
+
allowDecimal,
|
|
136
|
+
allowNegative,
|
|
137
|
+
formatDisplay,
|
|
138
|
+
unformatNumber,
|
|
139
|
+
onChange,
|
|
140
|
+
onValueChange,
|
|
141
|
+
]);
|
|
142
|
+
const handleFocus = useCallback((e) => {
|
|
143
|
+
var _a;
|
|
144
|
+
setIsFocused(true);
|
|
145
|
+
(_a = props.onFocus) === null || _a === void 0 ? void 0 : _a.call(props, e);
|
|
146
|
+
}, [props]);
|
|
147
|
+
const handleBlur = useCallback((e) => {
|
|
148
|
+
var _a;
|
|
149
|
+
setIsFocused(false);
|
|
150
|
+
const formatted = validateAndFormat(internalValue);
|
|
151
|
+
setInternalValue(formatted);
|
|
152
|
+
const numValue = parseFloat(formatted);
|
|
153
|
+
if (!isNaN(numValue)) {
|
|
154
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(numValue);
|
|
155
|
+
}
|
|
156
|
+
(_a = props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, e);
|
|
157
|
+
}, [internalValue, validateAndFormat, onChange, props]);
|
|
158
|
+
const increment = useCallback((e) => {
|
|
159
|
+
if (disabled)
|
|
160
|
+
return;
|
|
161
|
+
e.stopPropagation();
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
const currentValue = parseFloat(internalValue) || 0;
|
|
164
|
+
let newValue = currentValue + step;
|
|
165
|
+
if (max !== undefined && newValue > max) {
|
|
166
|
+
newValue = max;
|
|
167
|
+
}
|
|
168
|
+
const formatted = validateAndFormat(newValue.toString());
|
|
169
|
+
setInternalValue(formatted);
|
|
170
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(parseFloat(formatted));
|
|
171
|
+
onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(formatted);
|
|
172
|
+
}, [
|
|
173
|
+
internalValue,
|
|
174
|
+
step,
|
|
175
|
+
max,
|
|
176
|
+
disabled,
|
|
177
|
+
validateAndFormat,
|
|
178
|
+
onChange,
|
|
179
|
+
onValueChange,
|
|
180
|
+
]);
|
|
181
|
+
const decrement = useCallback((e) => {
|
|
182
|
+
if (disabled)
|
|
183
|
+
return;
|
|
184
|
+
e.stopPropagation();
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
const currentValue = parseFloat(internalValue) || 0;
|
|
187
|
+
let newValue = currentValue - step;
|
|
188
|
+
if (min !== undefined && newValue < min) {
|
|
189
|
+
newValue = min;
|
|
190
|
+
}
|
|
191
|
+
const formatted = validateAndFormat(newValue.toString());
|
|
192
|
+
setInternalValue(formatted);
|
|
193
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(parseFloat(formatted));
|
|
194
|
+
onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(formatted);
|
|
195
|
+
}, [
|
|
196
|
+
internalValue,
|
|
197
|
+
step,
|
|
198
|
+
min,
|
|
199
|
+
disabled,
|
|
200
|
+
validateAndFormat,
|
|
201
|
+
onChange,
|
|
202
|
+
onValueChange,
|
|
203
|
+
]);
|
|
204
|
+
const handleKeyDown = useCallback((e) => {
|
|
205
|
+
var _a;
|
|
206
|
+
switch (e.key) {
|
|
207
|
+
case "ArrowUp":
|
|
208
|
+
increment(e);
|
|
209
|
+
break;
|
|
210
|
+
case "ArrowDown":
|
|
211
|
+
decrement(e);
|
|
212
|
+
break;
|
|
213
|
+
case "Enter":
|
|
214
|
+
e.currentTarget.blur();
|
|
215
|
+
break;
|
|
216
|
+
default:
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
(_a = props.onKeyDown) === null || _a === void 0 ? void 0 : _a.call(props, e);
|
|
220
|
+
}, [increment, decrement, props]);
|
|
221
|
+
// Sync internal value with external value prop
|
|
222
|
+
React.useEffect(() => {
|
|
223
|
+
if (value !== undefined) {
|
|
224
|
+
setInternalValue(value.toString());
|
|
225
|
+
}
|
|
226
|
+
}, [value]);
|
|
227
|
+
// Get the display value (formatted when not focused)
|
|
228
|
+
const displayValue = React.useMemo(() => {
|
|
229
|
+
if (isFocused || !formatDisplay) {
|
|
230
|
+
return internalValue;
|
|
231
|
+
}
|
|
232
|
+
return formatNumber(internalValue);
|
|
233
|
+
}, [isFocused, formatDisplay, internalValue, formatNumber]);
|
|
234
|
+
const controlsSize = {
|
|
235
|
+
sm: "w-3 h-3",
|
|
236
|
+
md: "w-4 h-4",
|
|
237
|
+
lg: "w-6 h-6",
|
|
238
|
+
}[size];
|
|
239
|
+
const paddingSize = {
|
|
240
|
+
sm: "absolute top-0.5",
|
|
241
|
+
md: "absolute top-1",
|
|
242
|
+
lg: "absolute top-1",
|
|
243
|
+
}[size];
|
|
244
|
+
const renderControls = () => {
|
|
245
|
+
if (hideControls)
|
|
246
|
+
return null;
|
|
247
|
+
return (_jsxs("div", { className: cn("flex flex-col", props.iconMode === "flat" && paddingSize), children: [_jsx("button", { type: "button", onClick: increment, disabled: disabled ||
|
|
248
|
+
(max !== undefined && parseFloat(internalValue) >= max), className: cn(" hover:bg-input-active-stroke/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors rounded-full text-input-filled-text"), tabIndex: -1, children: _jsx(ChevronUpIcon, { className: controlsSize }) }), _jsx("button", { type: "button", onClick: decrement, disabled: disabled ||
|
|
249
|
+
(min !== undefined && parseFloat(internalValue) <= min), className: cn(" hover:bg-input-active-stroke/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors rounded-full text-input-filled-text"), tabIndex: -1, children: _jsx(ChevronDownIcon, { className: controlsSize }) })] }));
|
|
250
|
+
};
|
|
251
|
+
return (_jsx(TextInput, Object.assign({}, props, { ref: inputRef, type: "text", inputMode: "decimal", value: displayValue, onChange: handleInputChange, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, disabled: disabled, size: size, hasClearIcon: false, endIcon: renderControls(), className: cn(className) })));
|
|
252
|
+
});
|
|
253
|
+
NumberInput.displayName = "NumberInput";
|
|
254
|
+
export default NumberInput;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { NumberInput } from "@/index";
|
|
3
|
+
const meta = {
|
|
4
|
+
title: "Components/NumberInput",
|
|
5
|
+
component: NumberInput,
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: "fullscreen",
|
|
8
|
+
},
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
decorators: [
|
|
11
|
+
(Story) => (_jsx("div", { className: "p-5 flex h-screen w-full bg-base-bg2", children: _jsx(Story, {}) })),
|
|
12
|
+
],
|
|
13
|
+
argTypes: {
|
|
14
|
+
size: {
|
|
15
|
+
control: { type: "radio" },
|
|
16
|
+
options: ["sm", "md", "lg"],
|
|
17
|
+
},
|
|
18
|
+
rounded: {
|
|
19
|
+
control: { type: "radio" },
|
|
20
|
+
options: ["none", "normal", "full"],
|
|
21
|
+
},
|
|
22
|
+
variant: {
|
|
23
|
+
control: { type: "radio" },
|
|
24
|
+
options: ["flat", "outline", "underline"],
|
|
25
|
+
},
|
|
26
|
+
isFloatingLabel: { control: "boolean" },
|
|
27
|
+
fullwidth: { control: "boolean" },
|
|
28
|
+
disabled: { control: "boolean" },
|
|
29
|
+
error: { control: "boolean" },
|
|
30
|
+
required: { control: "boolean" },
|
|
31
|
+
hideControls: { control: "boolean" },
|
|
32
|
+
allowDecimal: { control: "boolean" },
|
|
33
|
+
allowNegative: { control: "boolean" },
|
|
34
|
+
},
|
|
35
|
+
args: {
|
|
36
|
+
label: "Number",
|
|
37
|
+
placeholder: "Enter a number",
|
|
38
|
+
size: "md",
|
|
39
|
+
rounded: "normal",
|
|
40
|
+
variant: "outline",
|
|
41
|
+
isFloatingLabel: true,
|
|
42
|
+
fullwidth: true,
|
|
43
|
+
disabled: false,
|
|
44
|
+
error: false,
|
|
45
|
+
required: true,
|
|
46
|
+
hideControls: false,
|
|
47
|
+
allowDecimal: true,
|
|
48
|
+
allowNegative: true,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
export default meta;
|
|
52
|
+
export const Default = {
|
|
53
|
+
render: (args) => {
|
|
54
|
+
console.log("args ", args);
|
|
55
|
+
const props = Object.assign({}, args);
|
|
56
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
export const WithMinMax = {
|
|
60
|
+
args: {
|
|
61
|
+
label: "Age",
|
|
62
|
+
min: 0,
|
|
63
|
+
max: 120,
|
|
64
|
+
helperText: "Age must be between 0 and 120",
|
|
65
|
+
},
|
|
66
|
+
render: (args) => {
|
|
67
|
+
const props = Object.assign({}, args);
|
|
68
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
export const WithStep = {
|
|
72
|
+
args: {
|
|
73
|
+
label: "Rating",
|
|
74
|
+
min: 0,
|
|
75
|
+
max: 5,
|
|
76
|
+
step: 0.5,
|
|
77
|
+
defaultValue: 2.5,
|
|
78
|
+
helperText: "Use 0.5 increments",
|
|
79
|
+
},
|
|
80
|
+
render: (args) => {
|
|
81
|
+
const props = Object.assign({}, args);
|
|
82
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
export const IntegerOnly = {
|
|
86
|
+
args: {
|
|
87
|
+
label: "Quantity",
|
|
88
|
+
allowDecimal: false,
|
|
89
|
+
min: 1,
|
|
90
|
+
defaultValue: 1,
|
|
91
|
+
helperText: "Integer values only",
|
|
92
|
+
},
|
|
93
|
+
render: (args) => {
|
|
94
|
+
const props = Object.assign({}, args);
|
|
95
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
export const ControllWithOutLine = {
|
|
99
|
+
args: {
|
|
100
|
+
label: "Quantity",
|
|
101
|
+
allowDecimal: false,
|
|
102
|
+
iconMode: "flat",
|
|
103
|
+
min: 1,
|
|
104
|
+
defaultValue: 1,
|
|
105
|
+
helperText: "Integer values only",
|
|
106
|
+
},
|
|
107
|
+
render: (args) => {
|
|
108
|
+
const props = Object.assign({}, args);
|
|
109
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
export const WithoutControls = {
|
|
113
|
+
args: {
|
|
114
|
+
label: "Custom Number",
|
|
115
|
+
hideControls: true,
|
|
116
|
+
helperText: "No increment/decrement buttons",
|
|
117
|
+
},
|
|
118
|
+
render: (args) => {
|
|
119
|
+
const props = Object.assign({}, args);
|
|
120
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
export const ErrorState = {
|
|
124
|
+
args: {
|
|
125
|
+
error: true,
|
|
126
|
+
errorMessage: "Value exceeds maximum allowed",
|
|
127
|
+
defaultValue: 150,
|
|
128
|
+
},
|
|
129
|
+
render: (args) => {
|
|
130
|
+
const props = Object.assign({}, args);
|
|
131
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
export const HelperText = {
|
|
135
|
+
args: {
|
|
136
|
+
helperText: "Enter a number between 0 and 100",
|
|
137
|
+
min: 0,
|
|
138
|
+
max: 100,
|
|
139
|
+
},
|
|
140
|
+
render: (args) => {
|
|
141
|
+
const props = Object.assign({}, args);
|
|
142
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
export const Disabled = {
|
|
146
|
+
args: {
|
|
147
|
+
disabled: true,
|
|
148
|
+
defaultValue: 50,
|
|
149
|
+
},
|
|
150
|
+
render: (args) => {
|
|
151
|
+
const props = Object.assign({}, args);
|
|
152
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
export const FormattedNumber = {
|
|
156
|
+
args: {
|
|
157
|
+
label: "Amount",
|
|
158
|
+
formatDisplay: true,
|
|
159
|
+
precision: 2,
|
|
160
|
+
defaultValue: 1234567.89,
|
|
161
|
+
helperText: "Number with thousand separator",
|
|
162
|
+
},
|
|
163
|
+
render: (args) => {
|
|
164
|
+
const props = Object.assign({}, args);
|
|
165
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
export const CurrencyFormat = {
|
|
169
|
+
args: {
|
|
170
|
+
label: "Price",
|
|
171
|
+
formatDisplay: true,
|
|
172
|
+
precision: 2,
|
|
173
|
+
prefix: "$",
|
|
174
|
+
defaultValue: 1230.0,
|
|
175
|
+
min: 0,
|
|
176
|
+
helperText: "Currency format with $ prefix",
|
|
177
|
+
},
|
|
178
|
+
render: (args) => {
|
|
179
|
+
const props = Object.assign({}, args);
|
|
180
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
export const CurrencyThailand = {
|
|
184
|
+
args: {
|
|
185
|
+
label: "ราคา",
|
|
186
|
+
formatDisplay: true,
|
|
187
|
+
precision: 2,
|
|
188
|
+
suffix: " ฿",
|
|
189
|
+
defaultValue: 15000.5,
|
|
190
|
+
min: 0,
|
|
191
|
+
helperText: "Thai Baht format",
|
|
192
|
+
},
|
|
193
|
+
render: (args) => {
|
|
194
|
+
const props = Object.assign({}, args);
|
|
195
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
export const CustomSeparators = {
|
|
199
|
+
args: {
|
|
200
|
+
label: "European Format",
|
|
201
|
+
formatDisplay: true,
|
|
202
|
+
precision: 2,
|
|
203
|
+
thousandSeparator: ".",
|
|
204
|
+
decimalSeparator: ",",
|
|
205
|
+
defaultValue: 9999.99,
|
|
206
|
+
helperText: "European number format (9.999,99)",
|
|
207
|
+
},
|
|
208
|
+
render: (args) => {
|
|
209
|
+
const props = Object.assign({}, args);
|
|
210
|
+
return (_jsxs("div", { className: "flex flex-row gap-4 w-full", children: [_jsx(NumberInput, Object.assign({ id: "1" }, props, { size: "lg" })), _jsx(NumberInput, Object.assign({ id: "2" }, props, { size: "md" })), _jsx(NumberInput, Object.assign({ id: "3" }, props, { size: "sm" }))] }));
|
|
211
|
+
},
|
|
212
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NumberInput, default } from "./NumberInput";
|