@boxcustodia/library 2.0.0-alpha.12 → 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 +1087 -720
- package/dist/index.es.js +7011 -56097
- 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 +99 -77
- 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 +126 -51
- package/src/components/divider/divider.tsx +16 -16
- 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 +227 -4
- 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 +31 -33
- 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 +29 -9
- 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 -475
- 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" } });
|
|
@@ -32,11 +32,12 @@ export function NumberInputGroup({
|
|
|
32
32
|
return (
|
|
33
33
|
<NumberFieldPrimitive.Group
|
|
34
34
|
className={cn(
|
|
35
|
-
"flex w-full items-center overflow-hidden rounded-md
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
35
|
+
"flex h-10 w-full items-center overflow-hidden rounded-md bg-background text-sm transition-shadow",
|
|
36
|
+
"outline outline-1 outline-input [outline-offset:-1px]",
|
|
37
|
+
"focus-within:outline-ring",
|
|
38
|
+
"aria-invalid:outline-error focus-within:aria-invalid:ring-error/20",
|
|
39
|
+
"has-aria-invalid:outline-error focus-within:has-aria-invalid:ring-error/20",
|
|
40
|
+
"data-[invalid]:outline-error focus-within:data-[invalid]:ring-error/20",
|
|
40
41
|
"data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:pointer-events-none",
|
|
41
42
|
"[&_svg]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
42
43
|
className,
|
|
@@ -153,70 +154,67 @@ export function NumberInputCursorIcon(props: ComponentProps<"svg">) {
|
|
|
153
154
|
export interface NumberInputProps extends NumberFieldPrimitive.Root.Props {
|
|
154
155
|
/** Hides the increment/decrement buttons. */
|
|
155
156
|
hideControls?: boolean;
|
|
156
|
-
/** Shorthand for `onValueChange` — receives only the value, without event details. */
|
|
157
|
-
onChange?: (value: number | null) => void;
|
|
158
157
|
/** Marks the field as invalid, adding the error border to the group. */
|
|
159
158
|
invalid?: boolean;
|
|
160
159
|
/** Forwarded to the underlying input element. */
|
|
161
160
|
placeholder?: string;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
};
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
export function NumberInput({
|
|
169
175
|
className,
|
|
170
176
|
hideControls = false,
|
|
171
|
-
onChange,
|
|
172
177
|
onValueChange,
|
|
173
178
|
invalid,
|
|
174
179
|
placeholder,
|
|
175
|
-
|
|
176
|
-
inputProps,
|
|
177
|
-
decrementProps,
|
|
178
|
-
incrementProps,
|
|
180
|
+
classNames,
|
|
179
181
|
...props
|
|
180
182
|
}: NumberInputProps) {
|
|
181
183
|
return (
|
|
182
|
-
<NumberInputRoot
|
|
183
|
-
onValueChange={(v, d) => {
|
|
184
|
-
onChange?.(v);
|
|
185
|
-
onValueChange?.(v, d);
|
|
186
|
-
}}
|
|
187
|
-
{...props}
|
|
188
|
-
>
|
|
184
|
+
<NumberInputRoot onValueChange={onValueChange} {...props}>
|
|
189
185
|
<NumberInputGroup
|
|
190
186
|
aria-invalid={invalid || undefined}
|
|
191
|
-
{
|
|
192
|
-
className={cn(className, groupProps?.className)}
|
|
187
|
+
className={className}
|
|
193
188
|
>
|
|
194
189
|
<NumberInputInput
|
|
195
190
|
{...(invalid ? { "aria-invalid": true } : {})}
|
|
196
191
|
placeholder={placeholder}
|
|
197
|
-
{
|
|
192
|
+
className={cn("self-stretch", classNames?.input)}
|
|
198
193
|
/>
|
|
199
194
|
{!hideControls && (
|
|
200
|
-
<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
|
+
>
|
|
201
201
|
<NumberInputIncrement
|
|
202
202
|
data-testid="increment-trigger"
|
|
203
|
-
{...incrementProps}
|
|
204
203
|
className={cn(
|
|
205
204
|
"flex-1 border-l-0 py-0 [&_svg]:size-3",
|
|
206
|
-
|
|
205
|
+
classNames?.increment,
|
|
207
206
|
)}
|
|
208
207
|
>
|
|
209
|
-
|
|
208
|
+
<ChevronUpIcon />
|
|
210
209
|
</NumberInputIncrement>
|
|
211
210
|
<NumberInputDecrement
|
|
212
211
|
data-testid="decrement-trigger"
|
|
213
|
-
{...decrementProps}
|
|
214
212
|
className={cn(
|
|
215
213
|
"flex-1 border-r-0 py-0 [&_svg]:size-3",
|
|
216
|
-
|
|
214
|
+
classNames?.decrement,
|
|
217
215
|
)}
|
|
218
216
|
>
|
|
219
|
-
|
|
217
|
+
<ChevronDownIcon />
|
|
220
218
|
</NumberInputDecrement>
|
|
221
219
|
</div>
|
|
222
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
|
+
};
|