@boxcustodia/library 2.0.0-alpha.13 → 2.0.0-alpha.14
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/index.cjs.js +1 -138
- package/dist/index.d.ts +1083 -715
- package/dist/index.es.js +7077 -56175
- package/dist/theme.css +1 -1
- package/package.json +34 -26
- package/src/__doc__/Examples.tsx +1 -1
- package/src/__doc__/Intro.mdx +3 -3
- package/src/__doc__/Tabs.mdx +112 -0
- package/src/__doc__/V2.mdx +1246 -0
- package/src/components/accordion/accordion.stories.tsx +143 -0
- package/src/components/accordion/accordion.tsx +135 -0
- package/src/components/accordion/index.ts +1 -0
- package/src/components/alert/alert.stories.tsx +24 -4
- package/src/components/alert/alert.tsx +17 -9
- package/src/components/alert-dialog/alert-dialog.stories.tsx +24 -0
- package/src/components/alert-dialog/alert-dialog.test.tsx +1 -1
- package/src/components/alert-dialog/alert-dialog.tsx +58 -10
- package/src/components/auto-complete/auto-complete.stories.tsx +616 -200
- package/src/components/auto-complete/auto-complete.tsx +420 -68
- package/src/components/auto-complete/index.ts +0 -1
- package/src/components/avatar/avatar.stories.tsx +162 -21
- package/src/components/avatar/avatar.tsx +79 -20
- package/src/components/button/button.stories.tsx +219 -294
- package/src/components/button/button.test.tsx +10 -17
- package/src/components/button/button.tsx +78 -19
- package/src/components/button/components/base-button.tsx +30 -53
- package/src/components/button/index.ts +0 -1
- package/src/components/calendar/calendar.stories.tsx +1 -1
- package/src/components/calendar/calendar.tsx +4 -4
- package/src/components/card/card.stories.tsx +141 -69
- package/src/components/card/card.tsx +155 -54
- package/src/components/center/center.stories.tsx +22 -39
- package/src/components/checkbox/checkbox.stories.tsx +25 -5
- package/src/components/checkbox/checkbox.tsx +76 -15
- package/src/components/checkbox-group/checkbox-group.stories.tsx +116 -28
- package/src/components/checkbox-group/checkbox-group.tsx +84 -3
- package/src/components/combobox/combobox.stories.tsx +33 -23
- package/src/components/combobox/combobox.tsx +119 -103
- package/src/components/date-picker/date-input.stories.tsx +14 -6
- package/src/components/date-picker/date-input.tsx +2 -2
- package/src/components/date-picker/date-picker.model.ts +13 -4
- package/src/components/date-picker/date-picker.stories.tsx +38 -12
- package/src/components/date-picker/date-picker.tsx +28 -14
- package/src/components/dialog/dialog.stories.tsx +18 -0
- package/src/components/dialog/dialog.test.tsx +1 -1
- package/src/components/dialog/dialog.tsx +51 -20
- package/src/components/divider/divider.stories.tsx +6 -0
- package/src/components/dropzone/dropzone.stories.tsx +71 -90
- package/src/components/dropzone/dropzone.tsx +383 -105
- package/src/components/dropzone/index.ts +0 -1
- package/src/components/empty/empty.stories.tsx +165 -0
- package/src/components/empty/empty.tsx +156 -0
- package/src/components/empty/index.ts +1 -0
- package/src/components/field/field.stories.tsx +226 -3
- package/src/components/field/field.tsx +77 -42
- package/src/components/form/form.stories.tsx +320 -197
- package/src/components/form/form.tsx +3 -23
- package/src/components/index.ts +2 -6
- package/src/components/input/input.stories.tsx +5 -5
- package/src/components/input/input.tsx +4 -4
- package/src/components/kbd/kbd.stories.tsx +1 -0
- package/src/components/label/label.stories.tsx +16 -0
- package/src/components/label/label.tsx +13 -2
- package/src/components/loader/loader.stories.tsx +7 -5
- package/src/components/loader/loader.tsx +8 -3
- package/src/components/menu/menu-primitives.tsx +207 -196
- package/src/components/menu/menu.stories.tsx +276 -146
- package/src/components/menu/menu.tsx +146 -54
- package/src/components/number-input/number-input.stories.tsx +27 -4
- package/src/components/number-input/number-input.test.tsx +2 -2
- package/src/components/number-input/number-input.tsx +25 -29
- package/src/components/otp/index.ts +1 -0
- package/src/components/otp/otp.stories.tsx +209 -0
- package/src/components/otp/otp.tsx +100 -0
- package/src/components/pagination/index.ts +1 -0
- package/src/components/pagination/pagination.model.ts +2 -0
- package/src/components/pagination/pagination.stories.tsx +154 -59
- package/src/components/pagination/pagination.test.tsx +122 -57
- package/src/components/pagination/pagination.tsx +575 -77
- package/src/components/password/password.stories.tsx +18 -3
- package/src/components/password/password.tsx +26 -10
- package/src/components/popover/popover.stories.tsx +26 -5
- package/src/components/popover/popover.tsx +15 -23
- package/src/components/progress/progress.stories.tsx +1 -0
- package/src/components/radio-group/index.ts +1 -0
- package/src/components/radio-group/radio-group.stories.tsx +251 -0
- package/src/components/radio-group/radio-group.tsx +212 -0
- package/src/components/scroll-area/scroll-area.stories.tsx +1 -0
- package/src/components/select/select.stories.tsx +118 -19
- package/src/components/select/select.tsx +67 -62
- package/src/components/skeleton/skeleton.stories.tsx +1 -0
- package/src/components/stack/stack.stories.tsx +179 -89
- package/src/components/stack/stack.tsx +2 -2
- package/src/components/stepper/index.ts +1 -1
- package/src/components/stepper/stepper.stories.tsx +767 -83
- package/src/components/stepper/stepper.test.tsx +18 -18
- package/src/components/stepper/stepper.tsx +554 -0
- package/src/components/switch/switch.stories.tsx +15 -1
- package/src/components/switch/switch.tsx +17 -4
- package/src/components/table/index.ts +0 -2
- package/src/components/table/table.stories.tsx +131 -18
- package/src/components/table/table.test.tsx +1 -1
- package/src/components/table/table.tsx +183 -77
- package/src/components/tabs/tabs.stories.tsx +373 -155
- package/src/components/tabs/tabs.test.tsx +12 -12
- package/src/components/tabs/tabs.tsx +72 -149
- package/src/components/tag/index.ts +0 -1
- package/src/components/tag/tag.stories.tsx +155 -120
- package/src/components/tag/tag.tsx +47 -95
- package/src/components/textarea/textarea.stories.tsx +8 -22
- package/src/components/textarea/textarea.tsx +17 -79
- package/src/components/timeline/timeline.stories.tsx +323 -42
- package/src/components/timeline/timeline.tsx +359 -132
- package/src/components/toast/toast.stories.tsx +1 -0
- package/src/components/tooltip/tooltip.tsx +11 -9
- package/src/components/tree/index.ts +0 -1
- package/src/components/tree/tree.stories.tsx +365 -408
- package/src/components/tree/tree.test.tsx +163 -0
- package/src/components/tree/tree.tsx +212 -36
- package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +5 -5
- package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +1 -3
- package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +6 -6
- package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +1 -1
- package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +1 -1
- package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +1 -1
- package/src/hooks/usePagination/usePagination.tsx +36 -24
- package/src/styles/theme.css +1 -1
- package/src/utils/form.tsx +67 -37
- package/src/utils/index.ts +1 -1
- package/src/__doc__/Migration.mdx +0 -451
- package/src/components/auto-complete/auto-complete-primitives.tsx +0 -155
- package/src/components/background-image/background-image.stories.tsx +0 -21
- package/src/components/background-image/background-image.test.tsx +0 -29
- package/src/components/background-image/background-image.tsx +0 -23
- package/src/components/background-image/index.ts +0 -1
- package/src/components/button/button.variants.ts +0 -44
- package/src/components/button/components/loader-overlay.tsx +0 -21
- package/src/components/button/components/loading-icon.tsx +0 -47
- package/src/components/dropzone/upload-primitives.tsx +0 -310
- package/src/components/dropzone/use-dropzone.ts +0 -122
- package/src/components/empty-state/empty-state.stories.tsx +0 -56
- package/src/components/empty-state/empty-state.tsx +0 -39
- package/src/components/empty-state/index.ts +0 -1
- package/src/components/heading/heading.stories.tsx +0 -74
- package/src/components/heading/heading.tsx +0 -28
- package/src/components/heading/heading.variants.ts +0 -27
- package/src/components/heading/index.ts +0 -1
- package/src/components/kbd/kbd.variants.ts +0 -26
- package/src/components/menu/util/render-menu-item.tsx +0 -54
- package/src/components/multi-select/hooks/use-multi-select.ts +0 -66
- package/src/components/multi-select/index.ts +0 -1
- package/src/components/multi-select/multi-select.stories.tsx +0 -294
- package/src/components/multi-select/multi-select.tsx +0 -300
- package/src/components/multi-select/multi-select.variants.ts +0 -22
- package/src/components/pagination/components/pagination-option.tsx +0 -27
- package/src/components/show/index.ts +0 -1
- package/src/components/show/show.stories.tsx +0 -197
- package/src/components/show/show.test.tsx +0 -41
- package/src/components/show/show.tsx +0 -16
- package/src/components/stepper/Stepper.tsx +0 -190
- package/src/components/stepper/context/stepper-context.tsx +0 -11
- package/src/components/table/table-primitives.tsx +0 -122
- package/src/components/table/table.model.ts +0 -20
- package/src/components/table-pagination/index.ts +0 -2
- package/src/components/table-pagination/table-pagination.model.ts +0 -2
- package/src/components/table-pagination/table-pagination.stories.tsx +0 -23
- package/src/components/table-pagination/table-pagination.test.tsx +0 -32
- package/src/components/table-pagination/table-pagination.tsx +0 -108
- package/src/components/tabs/context/tabs-context.tsx +0 -14
- package/src/components/tag/tag.variants.ts +0 -31
- package/src/components/timeline/timeline-status.ts +0 -5
- package/src/components/tree/hooks/use-controllable-tree-state.ts +0 -80
- package/src/components/tree/tree-primitives.tsx +0 -126
|
@@ -1,100 +1,192 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../../lib";
|
|
2
4
|
import {
|
|
3
5
|
MenuCheckboxItem,
|
|
4
|
-
|
|
6
|
+
MenuGroup,
|
|
5
7
|
MenuItem,
|
|
6
8
|
MenuLabel,
|
|
9
|
+
MenuPopup,
|
|
10
|
+
MenuRadioGroup,
|
|
7
11
|
MenuRadioItem,
|
|
8
12
|
MenuRoot,
|
|
9
13
|
MenuSeparator,
|
|
10
14
|
MenuShortcut,
|
|
15
|
+
MenuSub,
|
|
16
|
+
MenuSubContent,
|
|
11
17
|
MenuSubTrigger,
|
|
12
18
|
MenuTrigger,
|
|
13
19
|
} from "./menu-primitives";
|
|
14
|
-
import { renderMenuItem } from "./util/render-menu-item";
|
|
15
20
|
|
|
16
|
-
type
|
|
17
|
-
|
|
18
|
-
type Shortcut = Partial<{
|
|
19
|
-
shortcut: MenuShortcutProps["keys"];
|
|
20
|
-
onShortcut: MenuShortcutProps["handler"];
|
|
21
|
-
shortcutOptions: MenuShortcutProps["options"];
|
|
22
|
-
}>;
|
|
23
|
-
|
|
24
|
-
type MenuItemBase<T extends ElementType> = ComponentPropsWithoutRef<T> & {
|
|
25
|
-
label?: ReactNode;
|
|
26
|
-
type: string;
|
|
27
|
-
} & Shortcut;
|
|
28
|
-
|
|
29
|
-
type MenuItemItem = MenuItemBase<typeof MenuItem> & {
|
|
21
|
+
type MenuItemItem = {
|
|
30
22
|
type: "item";
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
label: React.ReactNode;
|
|
24
|
+
shortcut?: string;
|
|
25
|
+
} & Omit<React.ComponentProps<typeof MenuItem>, "children">;
|
|
26
|
+
|
|
27
|
+
type MenuItemCheckbox = {
|
|
33
28
|
type: "checkbox";
|
|
34
|
-
|
|
29
|
+
label: React.ReactNode;
|
|
30
|
+
shortcut?: string;
|
|
31
|
+
} & Omit<React.ComponentProps<typeof MenuCheckboxItem>, "children">;
|
|
35
32
|
|
|
36
|
-
type MenuItemRadio =
|
|
33
|
+
type MenuItemRadio = {
|
|
37
34
|
type: "radio";
|
|
35
|
+
label: React.ReactNode;
|
|
36
|
+
shortcut?: string;
|
|
37
|
+
} & Omit<React.ComponentProps<typeof MenuRadioItem>, "children">;
|
|
38
|
+
|
|
39
|
+
type MenuItemRadioGroup = {
|
|
40
|
+
type: "radio-group";
|
|
41
|
+
value?: string;
|
|
42
|
+
onValueChange?: (value: string) => void;
|
|
43
|
+
items: MenuItemRadio[];
|
|
38
44
|
};
|
|
39
|
-
|
|
45
|
+
|
|
46
|
+
type MenuItemLabel = {
|
|
40
47
|
type: "label";
|
|
41
|
-
|
|
48
|
+
label: React.ReactNode;
|
|
49
|
+
} & Omit<React.ComponentProps<typeof MenuLabel>, "children">;
|
|
42
50
|
|
|
43
|
-
type MenuItemSeparator =
|
|
51
|
+
type MenuItemSeparator = {
|
|
44
52
|
type: "separator";
|
|
53
|
+
} & React.ComponentProps<typeof MenuSeparator>;
|
|
54
|
+
|
|
55
|
+
type MenuItemGroup = {
|
|
56
|
+
type: "group";
|
|
57
|
+
label?: React.ReactNode;
|
|
58
|
+
items: MenuItemType[];
|
|
45
59
|
};
|
|
46
60
|
|
|
47
|
-
type MenuItemSubmenu =
|
|
61
|
+
type MenuItemSubmenu = {
|
|
48
62
|
type: "submenu";
|
|
63
|
+
label: React.ReactNode;
|
|
49
64
|
items: MenuItemType[];
|
|
50
|
-
}
|
|
65
|
+
} & Omit<React.ComponentProps<typeof MenuSubTrigger>, "children">;
|
|
51
66
|
|
|
52
67
|
export type MenuItemType =
|
|
53
68
|
| MenuItemItem
|
|
54
69
|
| MenuItemCheckbox
|
|
55
70
|
| MenuItemRadio
|
|
71
|
+
| MenuItemRadioGroup
|
|
56
72
|
| MenuItemLabel
|
|
57
73
|
| MenuItemSeparator
|
|
74
|
+
| MenuItemGroup
|
|
58
75
|
| MenuItemSubmenu;
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
function renderItem(item: MenuItemType, index: number): React.ReactNode {
|
|
78
|
+
switch (item.type) {
|
|
79
|
+
case "item": {
|
|
80
|
+
const { type: _, label, shortcut, ...props } = item;
|
|
81
|
+
return (
|
|
82
|
+
<MenuItem key={index} {...props}>
|
|
83
|
+
{label}
|
|
84
|
+
{shortcut && <MenuShortcut>{shortcut}</MenuShortcut>}
|
|
85
|
+
</MenuItem>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
case "checkbox": {
|
|
89
|
+
const { type: _, label, shortcut, ...props } = item;
|
|
90
|
+
return (
|
|
91
|
+
<MenuCheckboxItem key={index} {...props}>
|
|
92
|
+
{label}
|
|
93
|
+
{shortcut && <MenuShortcut>{shortcut}</MenuShortcut>}
|
|
94
|
+
</MenuCheckboxItem>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
case "radio": {
|
|
98
|
+
const { type: _, label, shortcut, ...props } = item;
|
|
99
|
+
return (
|
|
100
|
+
<MenuRadioItem key={index} {...props}>
|
|
101
|
+
{label}
|
|
102
|
+
{shortcut && <MenuShortcut>{shortcut}</MenuShortcut>}
|
|
103
|
+
</MenuRadioItem>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
case "radio-group": {
|
|
107
|
+
const { type: _, value, onValueChange, items } = item;
|
|
108
|
+
return (
|
|
109
|
+
<MenuRadioGroup
|
|
110
|
+
key={index}
|
|
111
|
+
value={value}
|
|
112
|
+
onValueChange={onValueChange}
|
|
113
|
+
>
|
|
114
|
+
{items.map((subItem, i) => renderItem(subItem, i))}
|
|
115
|
+
</MenuRadioGroup>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
case "label": {
|
|
119
|
+
const { type: _, label, ...props } = item;
|
|
120
|
+
return (
|
|
121
|
+
<MenuGroup key={index}>
|
|
122
|
+
<MenuLabel {...props}>{label}</MenuLabel>
|
|
123
|
+
</MenuGroup>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
case "separator": {
|
|
127
|
+
const { type: _, ...props } = item;
|
|
128
|
+
return <MenuSeparator key={index} {...props} />;
|
|
129
|
+
}
|
|
130
|
+
case "group": {
|
|
131
|
+
const { type: _, label, items } = item;
|
|
132
|
+
return (
|
|
133
|
+
<MenuGroup key={index}>
|
|
134
|
+
{label && <MenuLabel>{label}</MenuLabel>}
|
|
135
|
+
{items.map((subItem, i) => renderItem(subItem, i))}
|
|
136
|
+
</MenuGroup>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
case "submenu": {
|
|
140
|
+
const { type: _, label, items, ...props } = item;
|
|
141
|
+
return (
|
|
142
|
+
<MenuSub key={index}>
|
|
143
|
+
<MenuSubTrigger {...props}>{label}</MenuSubTrigger>
|
|
144
|
+
<MenuSubContent>
|
|
145
|
+
{items.map((subItem, i) => renderItem(subItem, i))}
|
|
146
|
+
</MenuSubContent>
|
|
147
|
+
</MenuSub>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface MenuProps {
|
|
154
|
+
trigger: React.ReactElement;
|
|
62
155
|
items: MenuItemType[];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
156
|
+
className?: string;
|
|
157
|
+
side?: React.ComponentProps<typeof MenuPopup>["side"];
|
|
158
|
+
align?: React.ComponentProps<typeof MenuPopup>["align"];
|
|
159
|
+
offset?: React.ComponentProps<typeof MenuPopup>["sideOffset"];
|
|
160
|
+
alignOffset?: React.ComponentProps<typeof MenuPopup>["alignOffset"];
|
|
161
|
+
/** Styles applied to each internal slot. */
|
|
162
|
+
classNames?: {
|
|
163
|
+
/** Popup panel that opens below the trigger. */
|
|
164
|
+
popup?: string;
|
|
165
|
+
};
|
|
66
166
|
}
|
|
67
167
|
|
|
68
168
|
export const Menu = ({
|
|
69
169
|
trigger,
|
|
70
170
|
items,
|
|
71
|
-
|
|
171
|
+
className,
|
|
72
172
|
side = "bottom",
|
|
173
|
+
align = "start",
|
|
73
174
|
offset,
|
|
74
|
-
|
|
175
|
+
alignOffset,
|
|
176
|
+
classNames,
|
|
177
|
+
}: MenuProps) => {
|
|
75
178
|
return (
|
|
76
179
|
<MenuRoot>
|
|
77
|
-
<MenuTrigger
|
|
78
|
-
<
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
handler={(event) => onShortcut?.(event)}
|
|
88
|
-
options={shortcutOptions}
|
|
89
|
-
>
|
|
90
|
-
{shortcut}
|
|
91
|
-
</MenuShortcut>
|
|
92
|
-
),
|
|
93
|
-
)}
|
|
94
|
-
</React.Fragment>
|
|
95
|
-
),
|
|
96
|
-
)}
|
|
97
|
-
</MenuContent>
|
|
180
|
+
<MenuTrigger render={trigger} />
|
|
181
|
+
<MenuPopup
|
|
182
|
+
className={cn(classNames?.popup, className)}
|
|
183
|
+
side={side}
|
|
184
|
+
align={align}
|
|
185
|
+
sideOffset={offset}
|
|
186
|
+
alignOffset={alignOffset}
|
|
187
|
+
>
|
|
188
|
+
{items.map((item, index) => renderItem(item, index))}
|
|
189
|
+
</MenuPopup>
|
|
98
190
|
</MenuRoot>
|
|
99
191
|
);
|
|
100
192
|
};
|
|
@@ -29,6 +29,9 @@ const meta: Meta<typeof NumberInput> = {
|
|
|
29
29
|
name: "quantity",
|
|
30
30
|
defaultValue: 0,
|
|
31
31
|
},
|
|
32
|
+
argTypes: {
|
|
33
|
+
classNames: { control: false },
|
|
34
|
+
},
|
|
32
35
|
};
|
|
33
36
|
|
|
34
37
|
export default meta;
|
|
@@ -37,6 +40,22 @@ type Story = StoryObj<typeof NumberInput>;
|
|
|
37
40
|
|
|
38
41
|
export const Default: Story = {};
|
|
39
42
|
|
|
43
|
+
/**
|
|
44
|
+
* `className` estila el grupo (el contenedor con borde — root visual del
|
|
45
|
+
* componente). `classNames` expone los slots `input`, `controls`,
|
|
46
|
+
* `increment` y `decrement` para personalizar las partes internas.
|
|
47
|
+
*/
|
|
48
|
+
export const WithClassNames: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
className: "outline-primary",
|
|
51
|
+
classNames: {
|
|
52
|
+
input: "font-mono text-primary",
|
|
53
|
+
increment: "text-primary",
|
|
54
|
+
decrement: "text-primary",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
40
59
|
export const Disabled: Story = {
|
|
41
60
|
args: {
|
|
42
61
|
disabled: true,
|
|
@@ -89,15 +108,19 @@ export const Scrub: Story = {
|
|
|
89
108
|
};
|
|
90
109
|
|
|
91
110
|
/**
|
|
92
|
-
* Usá `value` + `
|
|
93
|
-
* `
|
|
111
|
+
* Usá `value` + `onValueChange` para controlar el valor externamente.
|
|
112
|
+
* `onValueChange` recibe `number | null` — `null` cuando el campo está vacío o contiene un valor inválido.
|
|
94
113
|
*/
|
|
95
114
|
export const Controlled: Story = {
|
|
96
115
|
render: () => {
|
|
97
116
|
const [value, setValue] = useState<number | null>(0);
|
|
98
117
|
return (
|
|
99
118
|
<div className="flex w-48 flex-col gap-2">
|
|
100
|
-
<NumberInput
|
|
119
|
+
<NumberInput
|
|
120
|
+
name="quantity"
|
|
121
|
+
value={value}
|
|
122
|
+
onValueChange={(v) => setValue(v)}
|
|
123
|
+
/>
|
|
101
124
|
<p className="text-muted-foreground text-xs">
|
|
102
125
|
Valor: {value ?? "vacío"}
|
|
103
126
|
</p>
|
|
@@ -118,7 +141,7 @@ export const Range: Story = {
|
|
|
118
141
|
<NumberInput
|
|
119
142
|
name="progress"
|
|
120
143
|
value={value}
|
|
121
|
-
|
|
144
|
+
onValueChange={(v) => v !== null && setValue(v)}
|
|
122
145
|
min={0}
|
|
123
146
|
max={100}
|
|
124
147
|
step={10}
|
|
@@ -33,9 +33,9 @@ describe("NumberInput component", () => {
|
|
|
33
33
|
expect(ref.current).toBeInstanceOf(HTMLInputElement);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it("llama a
|
|
36
|
+
it("llama a onValueChange cuando el valor cambia", () => {
|
|
37
37
|
const handleChange = vi.fn();
|
|
38
|
-
render(<NumberInput name="cantidad"
|
|
38
|
+
render(<NumberInput name="cantidad" onValueChange={handleChange} />);
|
|
39
39
|
const input = screen.getByRole("textbox");
|
|
40
40
|
|
|
41
41
|
fireEvent.input(input, { target: { value: "5" } });
|
|
@@ -154,71 +154,67 @@ export function NumberInputCursorIcon(props: ComponentProps<"svg">) {
|
|
|
154
154
|
export interface NumberInputProps extends NumberFieldPrimitive.Root.Props {
|
|
155
155
|
/** Hides the increment/decrement buttons. */
|
|
156
156
|
hideControls?: boolean;
|
|
157
|
-
/** Shorthand for `onValueChange` — receives only the value, without event details. */
|
|
158
|
-
onChange?: (value: number | null) => void;
|
|
159
157
|
/** Marks the field as invalid, adding the error border to the group. */
|
|
160
158
|
invalid?: boolean;
|
|
161
159
|
/** Forwarded to the underlying input element. */
|
|
162
160
|
placeholder?: string;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
161
|
+
/** Styles applied to each internal slot. */
|
|
162
|
+
classNames?: {
|
|
163
|
+
/** Native `<input>` element. */
|
|
164
|
+
input?: string;
|
|
165
|
+
/** Column wrapper around the increment/decrement buttons. */
|
|
166
|
+
controls?: string;
|
|
167
|
+
/** Increment button (Chevron up). */
|
|
168
|
+
increment?: string;
|
|
169
|
+
/** Decrement button (Chevron down). */
|
|
170
|
+
decrement?: string;
|
|
171
|
+
};
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
export function NumberInput({
|
|
170
175
|
className,
|
|
171
176
|
hideControls = false,
|
|
172
|
-
onChange,
|
|
173
177
|
onValueChange,
|
|
174
178
|
invalid,
|
|
175
179
|
placeholder,
|
|
176
|
-
|
|
177
|
-
inputProps,
|
|
178
|
-
decrementProps,
|
|
179
|
-
incrementProps,
|
|
180
|
+
classNames,
|
|
180
181
|
...props
|
|
181
182
|
}: NumberInputProps) {
|
|
182
183
|
return (
|
|
183
|
-
<NumberInputRoot
|
|
184
|
-
onValueChange={(v, d) => {
|
|
185
|
-
onChange?.(v);
|
|
186
|
-
onValueChange?.(v, d);
|
|
187
|
-
}}
|
|
188
|
-
{...props}
|
|
189
|
-
>
|
|
184
|
+
<NumberInputRoot onValueChange={onValueChange} {...props}>
|
|
190
185
|
<NumberInputGroup
|
|
191
186
|
aria-invalid={invalid || undefined}
|
|
192
|
-
{
|
|
193
|
-
className={cn(className, groupProps?.className)}
|
|
187
|
+
className={className}
|
|
194
188
|
>
|
|
195
189
|
<NumberInputInput
|
|
196
190
|
{...(invalid ? { "aria-invalid": true } : {})}
|
|
197
191
|
placeholder={placeholder}
|
|
198
|
-
{
|
|
199
|
-
className={cn("self-stretch", inputProps?.className)}
|
|
192
|
+
className={cn("self-stretch", classNames?.input)}
|
|
200
193
|
/>
|
|
201
194
|
{!hideControls && (
|
|
202
|
-
<div
|
|
195
|
+
<div
|
|
196
|
+
className={cn(
|
|
197
|
+
"flex flex-col self-stretch divide-y divide-input border-l border-input",
|
|
198
|
+
classNames?.controls,
|
|
199
|
+
)}
|
|
200
|
+
>
|
|
203
201
|
<NumberInputIncrement
|
|
204
202
|
data-testid="increment-trigger"
|
|
205
|
-
{...incrementProps}
|
|
206
203
|
className={cn(
|
|
207
204
|
"flex-1 border-l-0 py-0 [&_svg]:size-3",
|
|
208
|
-
|
|
205
|
+
classNames?.increment,
|
|
209
206
|
)}
|
|
210
207
|
>
|
|
211
|
-
|
|
208
|
+
<ChevronUpIcon />
|
|
212
209
|
</NumberInputIncrement>
|
|
213
210
|
<NumberInputDecrement
|
|
214
211
|
data-testid="decrement-trigger"
|
|
215
|
-
{...decrementProps}
|
|
216
212
|
className={cn(
|
|
217
213
|
"flex-1 border-r-0 py-0 [&_svg]:size-3",
|
|
218
|
-
|
|
214
|
+
classNames?.decrement,
|
|
219
215
|
)}
|
|
220
216
|
>
|
|
221
|
-
|
|
217
|
+
<ChevronDownIcon />
|
|
222
218
|
</NumberInputDecrement>
|
|
223
219
|
</div>
|
|
224
220
|
)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./otp";
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
import { Button } from "../button/button";
|
|
5
|
+
import { Field } from "../field/field";
|
|
6
|
+
import { Form } from "../form/form";
|
|
7
|
+
import { OTP, OTPInput, OTPRoot, OTPSeparator } from "./otp";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Segmented input for one-time passwords and verification codes.
|
|
11
|
+
* The composite `OTP` generates slots from `length` and places
|
|
12
|
+
* `OTPSeparator` automatically via `split`. Use `inputProps` to forward
|
|
13
|
+
* props to every slot. Compose primitives (`OTPRoot`, `OTPInput`,
|
|
14
|
+
* `OTPSeparator`) directly for full control over slot layout.
|
|
15
|
+
*/
|
|
16
|
+
const meta: Meta<typeof OTP> = {
|
|
17
|
+
title: "Components/OTP",
|
|
18
|
+
component: OTP,
|
|
19
|
+
parameters: { layout: "centered" },
|
|
20
|
+
args: {
|
|
21
|
+
length: 6,
|
|
22
|
+
"aria-label": "Verification code",
|
|
23
|
+
},
|
|
24
|
+
tags: ["new"],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
type Story = StoryObj<typeof OTP>;
|
|
29
|
+
|
|
30
|
+
export const Default: Story = {};
|
|
31
|
+
|
|
32
|
+
export const Sizes: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<div className="flex flex-col gap-4">
|
|
35
|
+
<OTP aria-label="Default size" length={6} />
|
|
36
|
+
<OTP aria-label="Large size" length={6} size="lg" />
|
|
37
|
+
</div>
|
|
38
|
+
),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `split={n}` inserts `OTPSeparator` after the nth slot (1-indexed).
|
|
43
|
+
*/
|
|
44
|
+
export const WithSeparator: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
split: 3,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Associate a visible label using a `<label>` pointing to the root's `id`.
|
|
52
|
+
* When inside a `Field` with `FieldLabel`, the association is automatic.
|
|
53
|
+
*/
|
|
54
|
+
export const WithLabel: Story = {
|
|
55
|
+
render: () => (
|
|
56
|
+
<div className="flex flex-col gap-1.5">
|
|
57
|
+
<label className="text-sm font-medium" htmlFor="otp-with-label">
|
|
58
|
+
Verification code
|
|
59
|
+
</label>
|
|
60
|
+
<OTP
|
|
61
|
+
id="otp-with-label"
|
|
62
|
+
aria-label="Verification code"
|
|
63
|
+
length={6}
|
|
64
|
+
split={3}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const Disabled: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
disabled: true,
|
|
73
|
+
split: 3,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* `validationType="alphanumeric"` for recovery, backup, or invite codes
|
|
79
|
+
* that mix letters and numbers. Forward `inputMode="text"` via `inputProps`
|
|
80
|
+
* so mobile keyboards show letters alongside digits.
|
|
81
|
+
*/
|
|
82
|
+
export const Alphanumeric: Story = {
|
|
83
|
+
args: {
|
|
84
|
+
validationType: "alphanumeric",
|
|
85
|
+
split: 3,
|
|
86
|
+
inputProps: { inputMode: "text" },
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* `mask` obscures typed characters in each slot while preserving keyboard
|
|
92
|
+
* navigation and paste behavior — useful on shared screens.
|
|
93
|
+
*/
|
|
94
|
+
export const Masked: Story = {
|
|
95
|
+
args: {
|
|
96
|
+
mask: true,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `placeholder` on each slot shows a hint character. Forward via `inputProps`
|
|
102
|
+
* alongside `focus-visible:placeholder:text-transparent` to hide the hint on
|
|
103
|
+
* the active slot so it does not compete with typed input.
|
|
104
|
+
*/
|
|
105
|
+
export const WithPlaceholder: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
inputProps: {
|
|
108
|
+
placeholder: "1",
|
|
109
|
+
className: "focus-visible:placeholder:text-transparent",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Controlled via `value` + `onValueChange`. Fires with the full string on
|
|
116
|
+
* every keystroke. Check `value.length === length` to detect completion.
|
|
117
|
+
*/
|
|
118
|
+
export const Controlled: Story = {
|
|
119
|
+
render: (args) => {
|
|
120
|
+
const [value, setValue] = React.useState("");
|
|
121
|
+
const done = value.length === 6;
|
|
122
|
+
return (
|
|
123
|
+
<div className="flex flex-col items-start gap-3">
|
|
124
|
+
<OTP {...args} value={value} onValueChange={setValue} />
|
|
125
|
+
<p className="text-sm text-muted-foreground">
|
|
126
|
+
{done ? (
|
|
127
|
+
<span className="text-foreground font-medium">Code: {value}</span>
|
|
128
|
+
) : (
|
|
129
|
+
<>{value.length} / 6 characters</>
|
|
130
|
+
)}
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Uncontrolled via `name` — value is read from `FormData` on submit.
|
|
139
|
+
* Declare constraint errors on `Field` so Base UI intercepts native browser
|
|
140
|
+
* validation and shows messages inline instead of the browser tooltip.
|
|
141
|
+
* `patternMismatch` fires when the OTP is partially filled; `valueMissing`
|
|
142
|
+
* when empty with `required`. `onFormSubmit` only runs when all constraints pass.
|
|
143
|
+
*/
|
|
144
|
+
export const WithForm: Story = {
|
|
145
|
+
render: () => {
|
|
146
|
+
const [result, setResult] = React.useState<string | null>(null);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<Form
|
|
150
|
+
className="flex flex-col items-start gap-3"
|
|
151
|
+
onFormSubmit={(data) => setResult(data.code as string)}
|
|
152
|
+
>
|
|
153
|
+
<Field
|
|
154
|
+
name="code"
|
|
155
|
+
label="Verification code"
|
|
156
|
+
error={[
|
|
157
|
+
{
|
|
158
|
+
message: "Verification code is required.",
|
|
159
|
+
match: "valueMissing",
|
|
160
|
+
},
|
|
161
|
+
{ message: "Enter all 6 digits.", match: "patternMismatch" },
|
|
162
|
+
]}
|
|
163
|
+
>
|
|
164
|
+
<OTP length={6} split={3} required />
|
|
165
|
+
</Field>
|
|
166
|
+
<Button type="submit">Verify</Button>
|
|
167
|
+
{result && (
|
|
168
|
+
<p className="text-sm text-muted-foreground">
|
|
169
|
+
Submitted:{" "}
|
|
170
|
+
<span className="font-medium text-foreground">{result}</span>
|
|
171
|
+
</p>
|
|
172
|
+
)}
|
|
173
|
+
</Form>
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Use primitives directly when you need custom slot layout, conditional
|
|
180
|
+
* rendering, or per-slot props that `inputProps` cannot express uniformly.
|
|
181
|
+
*/
|
|
182
|
+
export const Primitive: Story = {
|
|
183
|
+
render: () => (
|
|
184
|
+
<OTPRoot aria-label="Verification code" length={6}>
|
|
185
|
+
<OTPInput aria-label="Character 1 of 6" />
|
|
186
|
+
<OTPInput aria-label="Character 2 of 6" />
|
|
187
|
+
<OTPInput aria-label="Character 3 of 6" />
|
|
188
|
+
<OTPSeparator />
|
|
189
|
+
<OTPInput aria-label="Character 4 of 6" />
|
|
190
|
+
<OTPInput aria-label="Character 5 of 6" />
|
|
191
|
+
<OTPInput aria-label="Character 6 of 6" />
|
|
192
|
+
</OTPRoot>
|
|
193
|
+
),
|
|
194
|
+
parameters: {
|
|
195
|
+
docs: {
|
|
196
|
+
source: {
|
|
197
|
+
code: `<OTPRoot aria-label="Verification code" length={6}>
|
|
198
|
+
<OTPInput aria-label="Character 1 of 6" />
|
|
199
|
+
<OTPInput aria-label="Character 2 of 6" />
|
|
200
|
+
<OTPInput aria-label="Character 3 of 6" />
|
|
201
|
+
<OTPSeparator />
|
|
202
|
+
<OTPInput aria-label="Character 4 of 6" />
|
|
203
|
+
<OTPInput aria-label="Character 5 of 6" />
|
|
204
|
+
<OTPInput aria-label="Character 6 of 6" />
|
|
205
|
+
</OTPRoot>`,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
};
|