@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
|
@@ -28,7 +28,8 @@ const meta: Meta<typeof Password> = {
|
|
|
28
28
|
hideIcon: { control: false },
|
|
29
29
|
onShow: { control: false },
|
|
30
30
|
onHide: { control: false },
|
|
31
|
-
|
|
31
|
+
onValueChange: { control: false },
|
|
32
|
+
classNames: { control: false },
|
|
32
33
|
},
|
|
33
34
|
decorators: [
|
|
34
35
|
(Story) => (
|
|
@@ -71,7 +72,7 @@ export const NonToggleable: Story = {
|
|
|
71
72
|
};
|
|
72
73
|
|
|
73
74
|
/**
|
|
74
|
-
* `
|
|
75
|
+
* `onValueChange` receives the string value as first argument, so no
|
|
75
76
|
* `event.target.value` needed.
|
|
76
77
|
*/
|
|
77
78
|
export const Controlled: Story = {
|
|
@@ -81,12 +82,26 @@ export const Controlled: Story = {
|
|
|
81
82
|
<Password
|
|
82
83
|
placeholder="Enter your password"
|
|
83
84
|
value={value}
|
|
84
|
-
|
|
85
|
+
onValueChange={(val) => setValue(val)}
|
|
85
86
|
/>
|
|
86
87
|
);
|
|
87
88
|
},
|
|
88
89
|
};
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* `className` estila el wrapper (el field visible). `classNames` expone los
|
|
93
|
+
* slots `input` (texto interno) y `toggle` (botón del ojo).
|
|
94
|
+
*/
|
|
95
|
+
export const WithClassNames: Story = {
|
|
96
|
+
args: {
|
|
97
|
+
className: "border-primary focus-within:border-primary",
|
|
98
|
+
classNames: {
|
|
99
|
+
input: "font-mono",
|
|
100
|
+
toggle: "text-primary hover:text-primary/80",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
90
105
|
export const CustomIcons: Story = {
|
|
91
106
|
args: {
|
|
92
107
|
showIcon: <Lock className="h-4 w-4" />,
|
|
@@ -10,6 +10,13 @@ export type PasswordProps = Omit<InputProps, "unstyled" | "nativeInput"> & {
|
|
|
10
10
|
onShow?: () => void;
|
|
11
11
|
onHide?: () => void;
|
|
12
12
|
toggleable?: boolean;
|
|
13
|
+
/** Styles applied to each internal slot. */
|
|
14
|
+
classNames?: {
|
|
15
|
+
/** Native `<input>` element (transparent inside the wrapper). */
|
|
16
|
+
input?: string;
|
|
17
|
+
/** Show/hide toggle button on the right. */
|
|
18
|
+
toggle?: string;
|
|
19
|
+
};
|
|
13
20
|
};
|
|
14
21
|
|
|
15
22
|
export function Password({
|
|
@@ -19,7 +26,8 @@ export function Password({
|
|
|
19
26
|
onShow,
|
|
20
27
|
onHide,
|
|
21
28
|
toggleable = true,
|
|
22
|
-
|
|
29
|
+
onValueChange,
|
|
30
|
+
classNames,
|
|
23
31
|
...rest
|
|
24
32
|
}: PasswordProps) {
|
|
25
33
|
const [showPassword, toggleShowPassword] = useToggle(false);
|
|
@@ -29,16 +37,21 @@ export function Password({
|
|
|
29
37
|
showPassword ? onHide?.() : onShow?.();
|
|
30
38
|
};
|
|
31
39
|
|
|
32
|
-
const handleChange =
|
|
40
|
+
const handleChange = onValueChange
|
|
33
41
|
? (event: ChangeEvent<HTMLInputElement>) =>
|
|
34
|
-
|
|
42
|
+
onValueChange(event.target.value, event)
|
|
35
43
|
: undefined;
|
|
36
44
|
|
|
37
45
|
return (
|
|
38
46
|
<div
|
|
39
47
|
className={cn(
|
|
40
|
-
"relative inline-flex w-full",
|
|
48
|
+
"relative inline-flex h-10 w-full items-center text-sm",
|
|
49
|
+
"rounded-md border border-input bg-background transition-shadow",
|
|
50
|
+
"focus-within:border-ring",
|
|
51
|
+
"aria-invalid:border-error focus-within:aria-invalid:ring-error/20",
|
|
52
|
+
"has-aria-invalid:border-error focus-within:has-aria-invalid:ring-error/20",
|
|
41
53
|
"has-disabled:cursor-not-allowed has-disabled:opacity-50",
|
|
54
|
+
"[&::-ms-clear]:hidden [&::-ms-reveal]:hidden",
|
|
42
55
|
className,
|
|
43
56
|
)}
|
|
44
57
|
data-slot="password"
|
|
@@ -48,12 +61,10 @@ export function Password({
|
|
|
48
61
|
onChange={handleChange}
|
|
49
62
|
type={showPassword ? "text" : "password"}
|
|
50
63
|
className={cn(
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"focus-visible:border-ring",
|
|
54
|
-
"aria-invalid:border-error focus-visible:aria-invalid:ring-error/20",
|
|
64
|
+
"min-w-0 flex-1 bg-transparent pl-3 outline-none",
|
|
65
|
+
"placeholder:text-muted-foreground",
|
|
55
66
|
"disabled:cursor-not-allowed",
|
|
56
|
-
|
|
67
|
+
classNames?.input,
|
|
57
68
|
)}
|
|
58
69
|
/>
|
|
59
70
|
<button
|
|
@@ -61,7 +72,12 @@ export function Password({
|
|
|
61
72
|
type="button"
|
|
62
73
|
onClick={handleToggle}
|
|
63
74
|
disabled={!toggleable}
|
|
64
|
-
className=
|
|
75
|
+
className={cn(
|
|
76
|
+
"flex shrink-0 items-center px-2 text-muted-foreground transition-colors",
|
|
77
|
+
"hover:text-foreground",
|
|
78
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
79
|
+
classNames?.toggle,
|
|
80
|
+
)}
|
|
65
81
|
>
|
|
66
82
|
{showPassword ? hideIcon : showIcon}
|
|
67
83
|
</button>
|
|
@@ -19,15 +19,19 @@ import {
|
|
|
19
19
|
* Provides two APIs: `Popover` (composite, single-component) and primitives
|
|
20
20
|
* (`PopoverRoot` + `PopoverTrigger` + `PopoverPopup`) for full layout control.
|
|
21
21
|
*
|
|
22
|
-
* `trigger` accepts a `ReactElement` passed as the `render` prop of
|
|
23
|
-
* wrapper is added.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
22
|
+
* `trigger` accepts a `ReactElement` passed as the `render` prop of
|
|
23
|
+
* `PopoverTrigger` — no wrapper is added. Style the popup via `className`
|
|
24
|
+
* (the visual root) or use `classNames.viewport` for the inner viewport.
|
|
25
|
+
* `title` and `description` render `PopoverTitle` / `PopoverDescription`
|
|
26
|
+
* with automatic ARIA wiring. Supports controlled state via `open` /
|
|
27
|
+
* `onOpenChange` and imperative control via `PopoverCreateHandle`.
|
|
27
28
|
*/
|
|
28
29
|
const meta: Meta<typeof Popover> = {
|
|
29
30
|
title: "Components/Popover",
|
|
30
31
|
component: Popover,
|
|
32
|
+
argTypes: {
|
|
33
|
+
classNames: { control: false },
|
|
34
|
+
},
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
export default meta;
|
|
@@ -43,6 +47,23 @@ export const Default: Story = {
|
|
|
43
47
|
),
|
|
44
48
|
};
|
|
45
49
|
|
|
50
|
+
/**
|
|
51
|
+
* `className` estila el popup (root visual). `classNames` expone el slot
|
|
52
|
+
* `viewport` para el contenedor interno con padding y overflow.
|
|
53
|
+
*/
|
|
54
|
+
export const WithClassNames: Story = {
|
|
55
|
+
render: (args) => (
|
|
56
|
+
<Popover
|
|
57
|
+
{...args}
|
|
58
|
+
className="border-primary"
|
|
59
|
+
classNames={{ viewport: "py-6" }}
|
|
60
|
+
trigger={<Button variant="outline">Open</Button>}
|
|
61
|
+
>
|
|
62
|
+
Custom popup + viewport styling.
|
|
63
|
+
</Popover>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
|
|
46
67
|
/**
|
|
47
68
|
* `tooltipStyle` renders the popup compact — smaller padding, `rounded-md`, and `w-fit`.
|
|
48
69
|
* Intended for brief contextual hints that don't need a full panel.
|
|
@@ -39,9 +39,11 @@ export function PopoverPopup({
|
|
|
39
39
|
matchTriggerWidth = false,
|
|
40
40
|
anchor,
|
|
41
41
|
portalProps,
|
|
42
|
+
viewportClassName,
|
|
42
43
|
...props
|
|
43
44
|
}: PopoverPrimitive.Popup.Props & {
|
|
44
45
|
portalProps?: PopoverPrimitive.Portal.Props;
|
|
46
|
+
viewportClassName?: string;
|
|
45
47
|
side?: PopoverPrimitive.Positioner.Props["side"];
|
|
46
48
|
align?: PopoverPrimitive.Positioner.Props["align"];
|
|
47
49
|
sideOffset?: PopoverPrimitive.Positioner.Props["sideOffset"];
|
|
@@ -79,6 +81,7 @@ export function PopoverPopup({
|
|
|
79
81
|
tooltipStyle
|
|
80
82
|
? "py-1 [--viewport-inline-padding:--spacing(2)]"
|
|
81
83
|
: "not-data-transitioning:overflow-y-auto",
|
|
84
|
+
viewportClassName,
|
|
82
85
|
)}
|
|
83
86
|
data-slot="popover-viewport"
|
|
84
87
|
>
|
|
@@ -126,18 +129,6 @@ export { PopoverPrimitive, PopoverPopup as PopoverContent };
|
|
|
126
129
|
|
|
127
130
|
// --- Composite ---
|
|
128
131
|
|
|
129
|
-
type PopoverPopupOverrideProps = Omit<
|
|
130
|
-
React.ComponentPropsWithoutRef<typeof PopoverPopup>,
|
|
131
|
-
| "children"
|
|
132
|
-
| "side"
|
|
133
|
-
| "align"
|
|
134
|
-
| "sideOffset"
|
|
135
|
-
| "alignOffset"
|
|
136
|
-
| "tooltipStyle"
|
|
137
|
-
| "matchTriggerWidth"
|
|
138
|
-
| "anchor"
|
|
139
|
-
>;
|
|
140
|
-
|
|
141
132
|
interface PopoverProps extends Omit<PopoverPrimitive.Root.Props, "children"> {
|
|
142
133
|
trigger?: React.ReactElement;
|
|
143
134
|
children: ReactNode;
|
|
@@ -150,9 +141,13 @@ interface PopoverProps extends Omit<PopoverPrimitive.Root.Props, "children"> {
|
|
|
150
141
|
tooltipStyle?: boolean;
|
|
151
142
|
matchTriggerWidth?: boolean;
|
|
152
143
|
anchor?: PopoverPrimitive.Positioner.Props["anchor"];
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
144
|
+
/** Styles the popup (visual root). */
|
|
145
|
+
className?: string;
|
|
146
|
+
/** Styles applied to each internal slot. */
|
|
147
|
+
classNames?: {
|
|
148
|
+
/** Inner viewport that holds the popup content (padding, overflow). */
|
|
149
|
+
viewport?: string;
|
|
150
|
+
};
|
|
156
151
|
}
|
|
157
152
|
|
|
158
153
|
export function Popover({
|
|
@@ -167,18 +162,14 @@ export function Popover({
|
|
|
167
162
|
tooltipStyle,
|
|
168
163
|
matchTriggerWidth,
|
|
169
164
|
anchor,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
popupProps,
|
|
165
|
+
className,
|
|
166
|
+
classNames,
|
|
173
167
|
...rootProps
|
|
174
168
|
}: PopoverProps): React.ReactElement {
|
|
175
169
|
return (
|
|
176
170
|
<PopoverRoot {...rootProps}>
|
|
177
|
-
{trigger !== undefined &&
|
|
178
|
-
<PopoverTrigger render={trigger} {...triggerProps} />
|
|
179
|
-
)}
|
|
171
|
+
{trigger !== undefined && <PopoverTrigger render={trigger} />}
|
|
180
172
|
<PopoverPopup
|
|
181
|
-
{...popupProps}
|
|
182
173
|
side={side}
|
|
183
174
|
align={align}
|
|
184
175
|
sideOffset={sideOffset}
|
|
@@ -186,7 +177,8 @@ export function Popover({
|
|
|
186
177
|
tooltipStyle={tooltipStyle}
|
|
187
178
|
matchTriggerWidth={matchTriggerWidth}
|
|
188
179
|
anchor={anchor}
|
|
189
|
-
className={
|
|
180
|
+
className={className}
|
|
181
|
+
viewportClassName={classNames?.viewport}
|
|
190
182
|
>
|
|
191
183
|
{(title ?? description) && (
|
|
192
184
|
<div className="mb-2">
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./radio-group";
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { action } from "storybook/actions";
|
|
4
|
+
import { Button } from "../../components";
|
|
5
|
+
import { Field } from "../field/field";
|
|
6
|
+
import {
|
|
7
|
+
RadioGroup,
|
|
8
|
+
RadioGroupRoot,
|
|
9
|
+
RadioIndicator,
|
|
10
|
+
RadioRoot,
|
|
11
|
+
} from "./radio-group";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* RadioGroup manages single-selection across a set of radio buttons.
|
|
15
|
+
* Exactly one item can be selected at a time. Built on
|
|
16
|
+
* `@base-ui/react/radio-group` and `@base-ui/react/radio`.
|
|
17
|
+
*
|
|
18
|
+
* Designed to be used inside `Field` — `Field` provides the label, description,
|
|
19
|
+
* and error handling. Pass `name` to both `Field` (for server-error routing)
|
|
20
|
+
* and `RadioGroup` (for native `FormData`).
|
|
21
|
+
*
|
|
22
|
+
* **90% case** — pass `items` and let the composite render all radio buttons:
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <RadioGroup
|
|
25
|
+
* name="plan"
|
|
26
|
+
* items={[{ value: "free", label: "Free" }, { value: "pro", label: "Pro" }]}
|
|
27
|
+
* onValueChange={(value) => console.log(value)}
|
|
28
|
+
* />
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* **Compound API** for custom layouts:
|
|
32
|
+
* - `RadioGroup` — group wrapper with value management and a11y fieldset
|
|
33
|
+
* - `RadioGroup.Item` — labeled radio button for use inside `RadioGroup`
|
|
34
|
+
* - `RadioGroup.Legend` — optional fieldset legend when extra semantic grouping is needed
|
|
35
|
+
*
|
|
36
|
+
* Use `onValueChange` to react to selection changes.
|
|
37
|
+
*
|
|
38
|
+
* `controlFirst` (default `true`) places the radio before the label on all
|
|
39
|
+
* `RadioGroup.Item` children via context.
|
|
40
|
+
*
|
|
41
|
+
* Reference: [Radio – Base UI](https://base-ui.com/react/components/radio)
|
|
42
|
+
*/
|
|
43
|
+
const meta: Meta<typeof RadioGroup> = {
|
|
44
|
+
title: "Components/RadioGroup",
|
|
45
|
+
component: RadioGroup,
|
|
46
|
+
parameters: { layout: "centered" },
|
|
47
|
+
args: {
|
|
48
|
+
legend: "Preferred contact",
|
|
49
|
+
},
|
|
50
|
+
argTypes: {
|
|
51
|
+
children: { control: false },
|
|
52
|
+
items: { control: false },
|
|
53
|
+
onValueChange: { control: false },
|
|
54
|
+
},
|
|
55
|
+
tags: ["new"],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default meta;
|
|
59
|
+
type Story = StoryObj<typeof RadioGroup>;
|
|
60
|
+
|
|
61
|
+
const CONTACT_ITEMS = [
|
|
62
|
+
{ value: "email", label: "Email" },
|
|
63
|
+
{ value: "sms", label: "SMS" },
|
|
64
|
+
{ value: "push", label: "Push notifications" },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export const Default: Story = {
|
|
68
|
+
render: (args) => (
|
|
69
|
+
<RadioGroup {...args} items={CONTACT_ITEMS} defaultValue="email" />
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Wrap `RadioGroup` with `Field` to add a label, description, and error handling.
|
|
75
|
+
* Pass `name` to both — `Field` uses it for server-error routing, `RadioGroup`
|
|
76
|
+
* uses it for native `FormData`.
|
|
77
|
+
*/
|
|
78
|
+
export const WithDescription: Story = {
|
|
79
|
+
render: ({ legend, ...args }) => (
|
|
80
|
+
<Field
|
|
81
|
+
label={legend}
|
|
82
|
+
description="We'll only contact you using this method."
|
|
83
|
+
>
|
|
84
|
+
<RadioGroup {...args} items={CONTACT_ITEMS} defaultValue="email" />
|
|
85
|
+
</Field>
|
|
86
|
+
),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Pass `error` to `Field` — the error message renders below the group and
|
|
91
|
+
* all `RadioGroup.Item` children receive `aria-invalid` (red borders).
|
|
92
|
+
* `description` is hidden while the field is invalid.
|
|
93
|
+
*/
|
|
94
|
+
export const WithError: Story = {
|
|
95
|
+
render: ({ legend, ...args }) => (
|
|
96
|
+
<Field
|
|
97
|
+
label={legend}
|
|
98
|
+
invalid
|
|
99
|
+
description="This text is hidden while error is present."
|
|
100
|
+
error="Please select a contact method."
|
|
101
|
+
>
|
|
102
|
+
<RadioGroup {...args} items={CONTACT_ITEMS} />
|
|
103
|
+
</Field>
|
|
104
|
+
),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* `controlFirst={false}` places the label before the radio button.
|
|
109
|
+
* Default is `true` (radio → label). Applied to all items via context.
|
|
110
|
+
*/
|
|
111
|
+
export const LabelFirst: Story = {
|
|
112
|
+
render: (args) => (
|
|
113
|
+
<RadioGroup
|
|
114
|
+
{...args}
|
|
115
|
+
items={CONTACT_ITEMS}
|
|
116
|
+
controlFirst={false}
|
|
117
|
+
defaultValue="email"
|
|
118
|
+
/>
|
|
119
|
+
),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const Disabled: Story = {
|
|
123
|
+
render: (args) => (
|
|
124
|
+
<RadioGroup {...args} items={CONTACT_ITEMS} disabled defaultValue="email" />
|
|
125
|
+
),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* `readOnly` prevents selection changes while keeping the group focusable
|
|
130
|
+
* for keyboard navigation. Unlike `disabled`, it does not apply opacity.
|
|
131
|
+
*/
|
|
132
|
+
export const ReadOnly: Story = {
|
|
133
|
+
render: (args) => (
|
|
134
|
+
<RadioGroup {...args} items={CONTACT_ITEMS} readOnly defaultValue="email" />
|
|
135
|
+
),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Use `value` + `onValueChange` for controlled mode.
|
|
140
|
+
*/
|
|
141
|
+
export const Controlled: Story = {
|
|
142
|
+
render: (args) => {
|
|
143
|
+
const [value, setValue] = useState("email");
|
|
144
|
+
return (
|
|
145
|
+
<RadioGroup
|
|
146
|
+
{...args}
|
|
147
|
+
items={CONTACT_ITEMS}
|
|
148
|
+
value={value}
|
|
149
|
+
onValueChange={setValue}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Base UI injects a hidden `<input type="radio">` per item so `name` and
|
|
157
|
+
* `required` work with native `FormData` — no extra wiring needed.
|
|
158
|
+
*/
|
|
159
|
+
export const Required: Story = {
|
|
160
|
+
render: (args) => (
|
|
161
|
+
<form
|
|
162
|
+
className="space-y-3"
|
|
163
|
+
onSubmit={(event) => {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
const data = new FormData(event.currentTarget);
|
|
166
|
+
action("submit")(Object.fromEntries(data));
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<RadioGroup {...args} items={CONTACT_ITEMS} name="contact" required />
|
|
170
|
+
<Button type="submit">Submit</Button>
|
|
171
|
+
</form>
|
|
172
|
+
),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Pass `items` to let the composite render all radio buttons — the 90% case.
|
|
177
|
+
* Each item supports `value`, `label`, and an optional `disabled` flag for
|
|
178
|
+
* per-item control independent of the group `disabled` prop.
|
|
179
|
+
*/
|
|
180
|
+
export const WithItems: Story = {
|
|
181
|
+
render: (args) => (
|
|
182
|
+
<RadioGroup
|
|
183
|
+
{...args}
|
|
184
|
+
defaultValue="email"
|
|
185
|
+
items={[
|
|
186
|
+
{ value: "email", label: "Email" },
|
|
187
|
+
{ value: "sms", label: "SMS" },
|
|
188
|
+
{ value: "push", label: "Push notifications", disabled: true },
|
|
189
|
+
]}
|
|
190
|
+
/>
|
|
191
|
+
),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* `RadioGroup.Legend` can be placed as a direct child instead of the `legend`
|
|
196
|
+
* prop when you need custom styling or want to visually hide it with `sr-only`.
|
|
197
|
+
*
|
|
198
|
+
* ```tsx
|
|
199
|
+
* <RadioGroup>
|
|
200
|
+
* <RadioGroup.Legend className="sr-only">Contact</RadioGroup.Legend>
|
|
201
|
+
* <RadioGroup.Item value="email" label="Email" />
|
|
202
|
+
* </RadioGroup>
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export const ComposableLegend: Story = {
|
|
206
|
+
render: () => (
|
|
207
|
+
<RadioGroup defaultValue="email">
|
|
208
|
+
<RadioGroup.Legend className="text-base font-semibold">
|
|
209
|
+
Preferred contact method
|
|
210
|
+
</RadioGroup.Legend>
|
|
211
|
+
<RadioGroup.Item value="email" label="Email" />
|
|
212
|
+
<RadioGroup.Item value="sms" label="SMS" />
|
|
213
|
+
<RadioGroup.Item value="push" label="Push notifications" />
|
|
214
|
+
</RadioGroup>
|
|
215
|
+
),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Direct composition with primitives for full structural control.
|
|
220
|
+
* Use when the composite layout is not enough — custom ordering,
|
|
221
|
+
* extra elements between items, or non-standard layouts.
|
|
222
|
+
*
|
|
223
|
+
* ```tsx
|
|
224
|
+
* <RadioGroupRoot name="plan">
|
|
225
|
+
* <RadioRoot value="pro">
|
|
226
|
+
* <RadioIndicator />
|
|
227
|
+
* </RadioRoot>
|
|
228
|
+
* </RadioGroupRoot>
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
export const Primitive: Story = {
|
|
232
|
+
render: () => (
|
|
233
|
+
<RadioGroupRoot name="plan" defaultValue="pro">
|
|
234
|
+
<div className="flex flex-col gap-2">
|
|
235
|
+
{(["free", "pro", "enterprise"] as const).map((plan) => (
|
|
236
|
+
<div key={plan} className="flex items-center gap-2">
|
|
237
|
+
<RadioRoot id={`primitive-${plan}`} value={plan}>
|
|
238
|
+
<RadioIndicator />
|
|
239
|
+
</RadioRoot>
|
|
240
|
+
<label
|
|
241
|
+
htmlFor={`primitive-${plan}`}
|
|
242
|
+
className="select-none text-sm capitalize"
|
|
243
|
+
>
|
|
244
|
+
{plan}
|
|
245
|
+
</label>
|
|
246
|
+
</div>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
</RadioGroupRoot>
|
|
250
|
+
),
|
|
251
|
+
};
|