@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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { Field as FieldBase } from "@base-ui/react/field";
|
|
2
|
+
import { Radio as RadioPrimitive } from "@base-ui/react/radio";
|
|
3
|
+
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
|
|
4
|
+
import { createContext, type ReactNode, useContext, useId } from "react";
|
|
5
|
+
import { cn } from "../../lib";
|
|
6
|
+
import {
|
|
7
|
+
FieldLegend,
|
|
8
|
+
FieldSet,
|
|
9
|
+
FieldValidity,
|
|
10
|
+
useFieldName,
|
|
11
|
+
useIsInsideFieldRoot,
|
|
12
|
+
} from "../field/field";
|
|
13
|
+
import { Label } from "../label";
|
|
14
|
+
|
|
15
|
+
// ── Primitives ─────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export function RadioGroupRoot({
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: RadioGroupPrimitive.Props) {
|
|
21
|
+
return (
|
|
22
|
+
<RadioGroupPrimitive
|
|
23
|
+
className={cn("grid w-full gap-2", className)}
|
|
24
|
+
data-slot="radio-group"
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function RadioRoot({ className, ...props }: RadioPrimitive.Root.Props) {
|
|
31
|
+
return (
|
|
32
|
+
<RadioPrimitive.Root
|
|
33
|
+
className={cn(
|
|
34
|
+
"peer relative inline-flex size-4.5 shrink-0 items-center justify-center rounded-full border border-input bg-background not-dark:bg-clip-padding shadow-xs/5 outline-none ring-ring transition-shadow",
|
|
35
|
+
"before:pointer-events-none before:absolute before:inset-0 before:rounded-full",
|
|
36
|
+
"not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)]",
|
|
37
|
+
"focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
38
|
+
"aria-invalid:border-error focus-visible:aria-invalid:border-error focus-visible:aria-invalid:ring-error",
|
|
39
|
+
"data-disabled:cursor-not-allowed data-disabled:opacity-64",
|
|
40
|
+
"sm:size-4",
|
|
41
|
+
"dark:not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",
|
|
42
|
+
"[[data-disabled],[data-checked],[aria-invalid]]:shadow-none",
|
|
43
|
+
"data-checked:border-primary data-checked:bg-primary",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
data-slot="radio"
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function RadioIndicator({
|
|
53
|
+
className,
|
|
54
|
+
children,
|
|
55
|
+
...props
|
|
56
|
+
}: RadioPrimitive.Indicator.Props) {
|
|
57
|
+
return (
|
|
58
|
+
<RadioPrimitive.Indicator
|
|
59
|
+
className={cn("flex size-full items-center justify-center", className)}
|
|
60
|
+
data-slot="radio-indicator"
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
{children ?? (
|
|
64
|
+
<span className="size-2 rounded-full bg-primary-foreground" />
|
|
65
|
+
)}
|
|
66
|
+
</RadioPrimitive.Indicator>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Group context ──────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const RadioGroupContext = createContext<{
|
|
73
|
+
controlFirst: boolean;
|
|
74
|
+
invalid: boolean;
|
|
75
|
+
}>({
|
|
76
|
+
controlFirst: true,
|
|
77
|
+
invalid: false,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ── Composite: RadioGroup.Item ─────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export interface RadioItemProps
|
|
83
|
+
extends Omit<RadioPrimitive.Root.Props, "children"> {
|
|
84
|
+
label: ReactNode;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function RadioGroupItem({ className, label, id, ...props }: RadioItemProps) {
|
|
88
|
+
const { controlFirst, invalid } = useContext(RadioGroupContext);
|
|
89
|
+
const generatedId = useId();
|
|
90
|
+
const idToUse = id ?? generatedId;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<FieldBase.Root invalid={invalid}>
|
|
94
|
+
<div
|
|
95
|
+
className={cn(
|
|
96
|
+
"flex select-none items-center gap-x-2",
|
|
97
|
+
!controlFirst && "flex-row-reverse justify-end",
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
<RadioRoot id={idToUse} className={className} {...props}>
|
|
101
|
+
<RadioIndicator />
|
|
102
|
+
</RadioRoot>
|
|
103
|
+
<Label htmlFor={idToUse}>{label}</Label>
|
|
104
|
+
</div>
|
|
105
|
+
</FieldBase.Root>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Composite: RadioGroup.Legend ───────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export interface RadioGroupLegendProps {
|
|
112
|
+
children: ReactNode;
|
|
113
|
+
className?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function RadioGroupLegend({ children, className }: RadioGroupLegendProps) {
|
|
117
|
+
return <FieldLegend className={className}>{children}</FieldLegend>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Composite: RadioGroup ──────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export interface RadioOption {
|
|
123
|
+
value: string;
|
|
124
|
+
label: ReactNode;
|
|
125
|
+
disabled?: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface RadioGroupProps {
|
|
129
|
+
legend?: ReactNode;
|
|
130
|
+
children?: ReactNode;
|
|
131
|
+
items?: RadioOption[];
|
|
132
|
+
defaultValue?: string;
|
|
133
|
+
value?: string;
|
|
134
|
+
onValueChange?: (value: string) => void;
|
|
135
|
+
disabled?: boolean;
|
|
136
|
+
required?: boolean;
|
|
137
|
+
readOnly?: boolean;
|
|
138
|
+
controlFirst?: boolean;
|
|
139
|
+
className?: string;
|
|
140
|
+
name?: string;
|
|
141
|
+
form?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function RadioGroupComposite({
|
|
145
|
+
legend,
|
|
146
|
+
children,
|
|
147
|
+
items,
|
|
148
|
+
defaultValue,
|
|
149
|
+
value,
|
|
150
|
+
onValueChange,
|
|
151
|
+
disabled,
|
|
152
|
+
required,
|
|
153
|
+
readOnly,
|
|
154
|
+
controlFirst = true,
|
|
155
|
+
className,
|
|
156
|
+
name: nameProp,
|
|
157
|
+
form,
|
|
158
|
+
}: RadioGroupProps) {
|
|
159
|
+
const fieldName = useFieldName();
|
|
160
|
+
const isInsideField = useIsInsideFieldRoot();
|
|
161
|
+
const name = nameProp ?? fieldName;
|
|
162
|
+
|
|
163
|
+
const renderGroup = (invalid: boolean) => (
|
|
164
|
+
<RadioGroupContext.Provider value={{ controlFirst, invalid }}>
|
|
165
|
+
<RadioGroupPrimitive
|
|
166
|
+
defaultValue={defaultValue}
|
|
167
|
+
value={value}
|
|
168
|
+
onValueChange={onValueChange}
|
|
169
|
+
disabled={disabled}
|
|
170
|
+
required={required}
|
|
171
|
+
readOnly={readOnly}
|
|
172
|
+
name={name}
|
|
173
|
+
form={form}
|
|
174
|
+
>
|
|
175
|
+
<FieldSet className={className}>
|
|
176
|
+
{legend && <RadioGroupLegend>{legend}</RadioGroupLegend>}
|
|
177
|
+
<div className="flex flex-col gap-2">
|
|
178
|
+
{items
|
|
179
|
+
? items.map((item) => (
|
|
180
|
+
<RadioGroupItem
|
|
181
|
+
key={item.value}
|
|
182
|
+
value={item.value}
|
|
183
|
+
label={item.label}
|
|
184
|
+
disabled={item.disabled}
|
|
185
|
+
/>
|
|
186
|
+
))
|
|
187
|
+
: children}
|
|
188
|
+
</div>
|
|
189
|
+
</FieldSet>
|
|
190
|
+
</RadioGroupPrimitive>
|
|
191
|
+
</RadioGroupContext.Provider>
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (!isInsideField) return renderGroup(false);
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<FieldValidity>
|
|
198
|
+
{({ validity }) => renderGroup(validity.valid === false)}
|
|
199
|
+
</FieldValidity>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Compound export ────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
export const RadioGroup = Object.assign(RadioGroupComposite, {
|
|
206
|
+
Item: RadioGroupItem,
|
|
207
|
+
Legend: RadioGroupLegend,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ── Primitive escape hatch ─────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
export { RadioGroupPrimitive, RadioPrimitive };
|
|
@@ -32,14 +32,14 @@ const animals: Animal[] = [
|
|
|
32
32
|
];
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Single-selection dropdown built on Base UI. Items shaped as `{
|
|
36
|
-
* or plain strings/numbers work with no extra props. Provide
|
|
37
|
-
* for custom object shapes, or `renderItem` to control the
|
|
38
|
-
* option — the checkmark indicator is always appended on the right.
|
|
35
|
+
* Single-selection dropdown built on Base UI. Items shaped as `{ id, name }`,
|
|
36
|
+
* `{ label, value }`, or plain strings/numbers work with no extra props. Provide
|
|
37
|
+
* `getLabel` / `getId` for custom object shapes, or `renderItem` to control the
|
|
38
|
+
* content inside each option — the checkmark indicator is always appended on the right.
|
|
39
39
|
*
|
|
40
|
-
* Exposes `
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* Exposes `onValueChange` that receives the full item object (or `null`).
|
|
41
|
+
* For controlled usage, pass `value` + `onValueChange` together.
|
|
42
|
+
* Use `SelectRoot` + primitives for full structural control.
|
|
43
43
|
*/
|
|
44
44
|
const meta: Meta<typeof Select> = {
|
|
45
45
|
title: "Components/Select",
|
|
@@ -53,13 +53,12 @@ const meta: Meta<typeof Select> = {
|
|
|
53
53
|
),
|
|
54
54
|
],
|
|
55
55
|
argTypes: {
|
|
56
|
-
|
|
56
|
+
onValueChange: { control: false },
|
|
57
57
|
getLabel: { control: false },
|
|
58
|
-
|
|
58
|
+
getId: { control: false },
|
|
59
|
+
getDisabled: { control: false },
|
|
59
60
|
renderItem: { control: false },
|
|
60
|
-
|
|
61
|
-
popupProps: { control: false },
|
|
62
|
-
itemProps: { control: false },
|
|
61
|
+
classNames: { control: false },
|
|
63
62
|
},
|
|
64
63
|
args: {
|
|
65
64
|
items: animals,
|
|
@@ -72,13 +71,113 @@ type Story = StoryObj<typeof Select>;
|
|
|
72
71
|
|
|
73
72
|
export const Default: Story = {};
|
|
74
73
|
|
|
74
|
+
/**
|
|
75
|
+
* `className` styles the trigger. `classNames` exposes the `popup` and
|
|
76
|
+
* `item` slots.
|
|
77
|
+
*/
|
|
78
|
+
export const WithClassNames: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
classNames: {
|
|
81
|
+
popup: "min-w-56",
|
|
82
|
+
item: "font-mono",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
75
87
|
export const Disabled: Story = {
|
|
76
88
|
args: { disabled: true },
|
|
77
89
|
};
|
|
78
90
|
|
|
79
91
|
/**
|
|
80
|
-
* `
|
|
81
|
-
*
|
|
92
|
+
* `alignItemWithTrigger` controla cómo posiciona Base UI el popup respecto al trigger.
|
|
93
|
+
*
|
|
94
|
+
* - **Default (`false`)**: el popup aparece debajo del trigger como un combo común.
|
|
95
|
+
* Es el comportamiento esperado cuando hay muchos items o cuando el popup no debería
|
|
96
|
+
* crecer hasta llenar el viewport intentando alinear el item seleccionado con el trigger.
|
|
97
|
+
* - **`true`**: Base UI intenta alinear el item seleccionado con el trigger y expande
|
|
98
|
+
* el popup hacia arriba y abajo. Útil para listas cortas con un valor preseleccionado,
|
|
99
|
+
* estilo `<select>` nativo de macOS.
|
|
100
|
+
*
|
|
101
|
+
* Es un cambio estructural — usá `SelectRoot` + `SelectPopup` directamente
|
|
102
|
+
* para activarlo:
|
|
103
|
+
*
|
|
104
|
+
* ```tsx
|
|
105
|
+
* <SelectRoot>
|
|
106
|
+
* <SelectTrigger><SelectValue /></SelectTrigger>
|
|
107
|
+
* <SelectPopup alignItemWithTrigger>…</SelectPopup>
|
|
108
|
+
* </SelectRoot>
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export const AlignItemWithTrigger: Story = {
|
|
112
|
+
render: () => {
|
|
113
|
+
const years = Array.from({ length: 40 }, (_, i) => {
|
|
114
|
+
const year = 1990 + i;
|
|
115
|
+
return { label: String(year), value: String(year) };
|
|
116
|
+
});
|
|
117
|
+
const preselected = years[20];
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="flex flex-col gap-6">
|
|
121
|
+
<div className="flex flex-col gap-1">
|
|
122
|
+
<span className="text-muted-foreground text-xs">
|
|
123
|
+
Default (alignItemWithTrigger=false)
|
|
124
|
+
</span>
|
|
125
|
+
<Select items={years} defaultValue={preselected} />
|
|
126
|
+
</div>
|
|
127
|
+
<div className="flex flex-col gap-1">
|
|
128
|
+
<span className="text-muted-foreground text-xs">
|
|
129
|
+
alignItemWithTrigger=true
|
|
130
|
+
</span>
|
|
131
|
+
<SelectRoot defaultValue={preselected.value}>
|
|
132
|
+
<SelectTrigger>
|
|
133
|
+
<SelectValue />
|
|
134
|
+
</SelectTrigger>
|
|
135
|
+
<SelectPopup alignItemWithTrigger>
|
|
136
|
+
{years.map((y) => (
|
|
137
|
+
<SelectItem key={y.value} value={y.value}>
|
|
138
|
+
{y.label}
|
|
139
|
+
</SelectItem>
|
|
140
|
+
))}
|
|
141
|
+
</SelectPopup>
|
|
142
|
+
</SelectRoot>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* `getDisabled` receives a predicate `(item) => boolean` to disable individual
|
|
151
|
+
* options. Items with a `disabled: true` property are disabled automatically
|
|
152
|
+
* without passing this prop.
|
|
153
|
+
*
|
|
154
|
+
* A disabled item is skipped during keyboard navigation and cannot be selected,
|
|
155
|
+
* but it remains visible in the list at reduced opacity.
|
|
156
|
+
*/
|
|
157
|
+
export const DisabledItem: Story = {
|
|
158
|
+
render: () => {
|
|
159
|
+
const items = [
|
|
160
|
+
{ label: "Cat", value: "cat" },
|
|
161
|
+
{ label: "Dog", value: "dog" },
|
|
162
|
+
{ label: "Bird", value: "bird" },
|
|
163
|
+
{ label: "Rabbit", value: "rabbit" },
|
|
164
|
+
{ label: "Fish", value: "fish" },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<Select
|
|
169
|
+
items={items}
|
|
170
|
+
getDisabled={(item) => item.value === "bird" || item.value === "fish"}
|
|
171
|
+
placeholder="Select an animal"
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* `getLabel` and `getId` accept functions so they work with any object shape.
|
|
179
|
+
* For common shapes (`{ id, name }`, `{ label, value }`, primitives) the
|
|
180
|
+
* defaults resolve everything without passing either function.
|
|
82
181
|
*/
|
|
83
182
|
export const CustomObject: Story = {
|
|
84
183
|
render: () => {
|
|
@@ -92,7 +191,7 @@ export const CustomObject: Story = {
|
|
|
92
191
|
<Select
|
|
93
192
|
items={items}
|
|
94
193
|
getLabel={(a) => a.name}
|
|
95
|
-
|
|
194
|
+
getId={(a) => a.key}
|
|
96
195
|
placeholder="Select an animal"
|
|
97
196
|
/>
|
|
98
197
|
);
|
|
@@ -102,7 +201,7 @@ export const CustomObject: Story = {
|
|
|
102
201
|
/**
|
|
103
202
|
* `multiple` enables multi-selection. The trigger shows selected labels as a
|
|
104
203
|
* comma-separated list with automatic truncation. Use `value: TItem[]` and
|
|
105
|
-
* `
|
|
204
|
+
* `onValueChange: (value: TItem[]) => void` for controlled usage — the discriminated
|
|
106
205
|
* union ensures type safety between single and multiple modes.
|
|
107
206
|
*/
|
|
108
207
|
export const Multiple: Story = {
|
|
@@ -127,7 +226,7 @@ export const CustomRender: Story = {
|
|
|
127
226
|
<Select
|
|
128
227
|
items={items}
|
|
129
228
|
getLabel={(a) => a.name}
|
|
130
|
-
|
|
229
|
+
getId={(a) => a.key}
|
|
131
230
|
renderItem={(a) => (
|
|
132
231
|
<span className="flex items-center gap-2">
|
|
133
232
|
<span>{a.emoji}</span>
|
|
@@ -141,7 +240,7 @@ export const CustomRender: Story = {
|
|
|
141
240
|
};
|
|
142
241
|
|
|
143
242
|
/**
|
|
144
|
-
* `
|
|
243
|
+
* `onValueChange` receives the full item object (or `null`).
|
|
145
244
|
* `value` accepts the item object directly — no string conversion needed.
|
|
146
245
|
*/
|
|
147
246
|
export const Controlled: Story = {
|
|
@@ -152,7 +251,7 @@ export const Controlled: Story = {
|
|
|
152
251
|
<Select
|
|
153
252
|
{...args}
|
|
154
253
|
value={value ?? null}
|
|
155
|
-
|
|
254
|
+
onValueChange={(item) => updateArgs({ value: item })}
|
|
156
255
|
/>
|
|
157
256
|
<p className="text-sm text-muted-foreground">
|
|
158
257
|
Selected: {value ? (value as Animal).label : "none"}
|
|
@@ -30,12 +30,9 @@ export function SelectRoot<
|
|
|
30
30
|
Value = string,
|
|
31
31
|
Multiple extends boolean | undefined = false,
|
|
32
32
|
>({
|
|
33
|
-
onChange,
|
|
34
33
|
...props
|
|
35
|
-
}:
|
|
36
|
-
|
|
37
|
-
}): React.ReactElement {
|
|
38
|
-
return <SelectPrimitive.Root onValueChange={onChange} {...props} />;
|
|
34
|
+
}: SelectPrimitive.Root.Props<Value, Multiple>): React.ReactElement {
|
|
35
|
+
return <SelectPrimitive.Root {...props} />;
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
export function SelectTrigger({
|
|
@@ -117,7 +114,7 @@ export function SelectPopup({
|
|
|
117
114
|
sideOffset = 4,
|
|
118
115
|
align = "start",
|
|
119
116
|
alignOffset = 0,
|
|
120
|
-
alignItemWithTrigger =
|
|
117
|
+
alignItemWithTrigger = false,
|
|
121
118
|
anchor,
|
|
122
119
|
portalProps,
|
|
123
120
|
...props
|
|
@@ -156,7 +153,7 @@ export function SelectPopup({
|
|
|
156
153
|
<div className="relative h-full min-w-(--anchor-width) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]">
|
|
157
154
|
<SelectPrimitive.List
|
|
158
155
|
className={cn(
|
|
159
|
-
"max-h-(--available-height) overflow-y-auto p-1",
|
|
156
|
+
"max-h-[min(24rem,var(--available-height))] overflow-y-auto p-1",
|
|
160
157
|
className,
|
|
161
158
|
)}
|
|
162
159
|
data-slot="select-list"
|
|
@@ -265,41 +262,53 @@ export function SelectLabel({
|
|
|
265
262
|
function defaultGetLabel(item: unknown): string {
|
|
266
263
|
if (item == null) return "";
|
|
267
264
|
if (typeof item === "string" || typeof item === "number") return String(item);
|
|
268
|
-
if (typeof item === "object"
|
|
269
|
-
|
|
265
|
+
if (typeof item === "object") {
|
|
266
|
+
const record = item as Record<string, unknown>;
|
|
267
|
+
if ("label" in record) return String(record.label);
|
|
268
|
+
if ("name" in record) return String(record.name);
|
|
269
|
+
if ("title" in record) return String(record.title);
|
|
270
|
+
}
|
|
270
271
|
return String(item);
|
|
271
272
|
}
|
|
272
273
|
|
|
273
|
-
function
|
|
274
|
+
function defaultGetId(item: unknown): string {
|
|
274
275
|
if (item == null) return "";
|
|
275
276
|
if (typeof item === "string" || typeof item === "number") return String(item);
|
|
276
|
-
if (typeof item === "object"
|
|
277
|
-
|
|
277
|
+
if (typeof item === "object") {
|
|
278
|
+
const record = item as Record<string, unknown>;
|
|
279
|
+
if ("id" in record) return String(record.id);
|
|
280
|
+
if ("value" in record) return String(record.value);
|
|
281
|
+
}
|
|
278
282
|
return String(item);
|
|
279
283
|
}
|
|
280
284
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
};
|
|
285
|
+
function defaultGetDisabled(item: unknown): boolean {
|
|
286
|
+
if (item != null && typeof item === "object" && "disabled" in item)
|
|
287
|
+
return Boolean((item as Record<string, unknown>).disabled);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
287
290
|
|
|
288
291
|
type SelectBaseProps<TItem = unknown> = Omit<
|
|
289
|
-
|
|
290
|
-
"children" | "
|
|
292
|
+
SelectPrimitive.Root.Props<string, false>,
|
|
293
|
+
"children" | "onValueChange" | "value" | "defaultValue" | "items" | "multiple"
|
|
291
294
|
> & {
|
|
292
295
|
items: readonly TItem[];
|
|
296
|
+
/** Returns the display text for an item. Used for the trigger value and ARIA. Defaults to `item.label`, `item.name`, `item.title`, or the stringified primitive. */
|
|
293
297
|
getLabel?: (item: TItem) => string;
|
|
294
|
-
|
|
298
|
+
/** Returns a stable string identifier for an item. Used as the React key and the underlying option value. Defaults to `item.id`, `item.value`, or the stringified primitive. */
|
|
299
|
+
getId?: (item: TItem) => string;
|
|
300
|
+
/** Returns whether an item should be disabled. Defaults to `item.disabled === true`. */
|
|
301
|
+
getDisabled?: (item: TItem) => boolean;
|
|
295
302
|
renderItem?: (item: TItem) => React.ReactNode;
|
|
296
303
|
placeholder?: string;
|
|
297
|
-
triggerProps?: React.ComponentProps<typeof SelectTrigger>;
|
|
298
|
-
popupProps?: React.ComponentProps<typeof SelectPopup>;
|
|
299
|
-
popupClassName?: string;
|
|
300
|
-
itemProps?: SelectPrimitive.Item.Props;
|
|
301
|
-
itemClassName?: string;
|
|
302
304
|
className?: string;
|
|
305
|
+
/** Styles applied to each internal slot. */
|
|
306
|
+
classNames?: {
|
|
307
|
+
/** Popup panel containing the item list. */
|
|
308
|
+
popup?: string;
|
|
309
|
+
/** Individual item row. */
|
|
310
|
+
item?: string;
|
|
311
|
+
};
|
|
303
312
|
};
|
|
304
313
|
|
|
305
314
|
export type SelectProps<TItem = unknown> =
|
|
@@ -307,25 +316,26 @@ export type SelectProps<TItem = unknown> =
|
|
|
307
316
|
multiple?: false;
|
|
308
317
|
value?: TItem | null;
|
|
309
318
|
defaultValue?: TItem | null;
|
|
310
|
-
|
|
319
|
+
onValueChange?: (value: TItem | null) => void;
|
|
311
320
|
})
|
|
312
321
|
| (SelectBaseProps<TItem> & {
|
|
313
322
|
multiple: true;
|
|
314
323
|
value?: TItem[];
|
|
315
324
|
defaultValue?: TItem[];
|
|
316
|
-
|
|
325
|
+
onValueChange?: (value: TItem[]) => void;
|
|
317
326
|
});
|
|
318
327
|
|
|
319
328
|
/**
|
|
320
|
-
* Composite select for single and multiple selection.
|
|
321
|
-
*
|
|
322
|
-
*
|
|
329
|
+
* Composite select for single and multiple selection.
|
|
330
|
+
*
|
|
331
|
+
* Items shaped as `{ id, name }`, `{ label, value }`, or plain strings/numbers
|
|
332
|
+
* work with no extra props. For other shapes, provide `getLabel` and/or `getId`.
|
|
323
333
|
*
|
|
324
334
|
* `renderItem` customizes the content **inside** each `SelectItem` (the
|
|
325
335
|
* checkmark indicator is always rendered by the item wrapper).
|
|
326
336
|
*
|
|
327
337
|
* Use `SelectRoot` + primitives directly when you need full structural
|
|
328
|
-
* control beyond what
|
|
338
|
+
* control beyond what `className` + `classNames` slots offer.
|
|
329
339
|
*/
|
|
330
340
|
export function Select<TItem = unknown>(
|
|
331
341
|
allProps: SelectProps<TItem>,
|
|
@@ -333,88 +343,83 @@ export function Select<TItem = unknown>(
|
|
|
333
343
|
const {
|
|
334
344
|
items,
|
|
335
345
|
getLabel: getLabelProp,
|
|
336
|
-
|
|
346
|
+
getId: getIdProp,
|
|
347
|
+
getDisabled: getDisabledProp,
|
|
337
348
|
renderItem,
|
|
338
349
|
placeholder,
|
|
339
|
-
|
|
350
|
+
onValueChange,
|
|
340
351
|
value,
|
|
341
352
|
defaultValue,
|
|
342
353
|
multiple,
|
|
343
|
-
triggerProps,
|
|
344
|
-
popupProps,
|
|
345
|
-
itemProps,
|
|
346
|
-
itemClassName,
|
|
347
354
|
className,
|
|
348
|
-
|
|
355
|
+
classNames,
|
|
349
356
|
...rest
|
|
350
357
|
} = allProps as SelectBaseProps<TItem> & {
|
|
351
358
|
multiple?: boolean;
|
|
352
|
-
|
|
359
|
+
onValueChange?: (value: any) => void;
|
|
353
360
|
value?: any;
|
|
354
361
|
defaultValue?: any;
|
|
355
362
|
};
|
|
356
363
|
|
|
357
364
|
const getLabel: (item: TItem) => string = getLabelProp ?? defaultGetLabel;
|
|
358
|
-
const
|
|
365
|
+
const getId: (item: TItem) => string = getIdProp ?? defaultGetId;
|
|
366
|
+
const getDisabled: (item: TItem) => boolean =
|
|
367
|
+
getDisabledProp ?? defaultGetDisabled;
|
|
359
368
|
|
|
360
369
|
const stringValue = multiple
|
|
361
|
-
? (value as TItem[] | undefined)?.map(
|
|
370
|
+
? (value as TItem[] | undefined)?.map(getId)
|
|
362
371
|
: value != null
|
|
363
|
-
?
|
|
372
|
+
? getId(value as TItem)
|
|
364
373
|
: undefined;
|
|
365
374
|
|
|
366
375
|
const stringDefaultValue = multiple
|
|
367
|
-
? (defaultValue as TItem[] | undefined)?.map(
|
|
376
|
+
? (defaultValue as TItem[] | undefined)?.map(getId)
|
|
368
377
|
: defaultValue != null
|
|
369
|
-
?
|
|
378
|
+
? getId(defaultValue as TItem)
|
|
370
379
|
: undefined;
|
|
371
380
|
|
|
372
381
|
const handleChange = (stringVal: string | string[] | null) => {
|
|
373
382
|
if (multiple) {
|
|
374
383
|
const vals = (stringVal as string[]) ?? [];
|
|
375
384
|
const found = vals
|
|
376
|
-
.map((sv) => items.find((i) =>
|
|
385
|
+
.map((sv) => items.find((i) => getId(i) === sv) ?? null)
|
|
377
386
|
.filter(Boolean) as TItem[];
|
|
378
|
-
|
|
387
|
+
onValueChange?.(found);
|
|
379
388
|
} else {
|
|
380
389
|
if (stringVal == null) {
|
|
381
|
-
|
|
390
|
+
onValueChange?.(null);
|
|
382
391
|
return;
|
|
383
392
|
}
|
|
384
393
|
const item =
|
|
385
|
-
items.find((i) =>
|
|
386
|
-
|
|
394
|
+
items.find((i) => getId(i) === (stringVal as string)) ?? null;
|
|
395
|
+
onValueChange?.(item);
|
|
387
396
|
}
|
|
388
397
|
};
|
|
389
398
|
|
|
390
399
|
const itemToStringLabel = (stringVal: string) => {
|
|
391
|
-
const item = items.find((i) =>
|
|
400
|
+
const item = items.find((i) => getId(i) === stringVal);
|
|
392
401
|
return item ? getLabel(item) : stringVal;
|
|
393
402
|
};
|
|
394
403
|
|
|
395
404
|
return (
|
|
396
405
|
<SelectRoot
|
|
397
406
|
itemToStringLabel={itemToStringLabel}
|
|
398
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
399
407
|
multiple={multiple as any}
|
|
400
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
401
408
|
value={stringValue as any}
|
|
402
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
403
409
|
defaultValue={stringDefaultValue as any}
|
|
404
|
-
|
|
405
|
-
onChange={handleChange as any}
|
|
410
|
+
onValueChange={handleChange as any}
|
|
406
411
|
{...(rest as any)}
|
|
407
412
|
>
|
|
408
|
-
<SelectTrigger className={className}
|
|
413
|
+
<SelectTrigger className={className}>
|
|
409
414
|
<SelectValue placeholder={placeholder} />
|
|
410
415
|
</SelectTrigger>
|
|
411
|
-
<SelectPopup className={
|
|
416
|
+
<SelectPopup className={classNames?.popup}>
|
|
412
417
|
{items.map((item) => (
|
|
413
418
|
<SelectItem
|
|
414
|
-
key={
|
|
415
|
-
value={
|
|
416
|
-
|
|
417
|
-
{
|
|
419
|
+
key={getId(item)}
|
|
420
|
+
value={getId(item)}
|
|
421
|
+
disabled={getDisabled(item)}
|
|
422
|
+
className={classNames?.item}
|
|
418
423
|
>
|
|
419
424
|
{renderItem ? renderItem(item) : getLabel(item)}
|
|
420
425
|
</SelectItem>
|