@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.
Files changed (173) hide show
  1. package/dist/index.cjs.js +1 -138
  2. package/dist/index.d.ts +1083 -715
  3. package/dist/index.es.js +7077 -56175
  4. package/dist/theme.css +1 -1
  5. package/package.json +34 -26
  6. package/src/__doc__/Examples.tsx +1 -1
  7. package/src/__doc__/Intro.mdx +3 -3
  8. package/src/__doc__/Tabs.mdx +112 -0
  9. package/src/__doc__/V2.mdx +1246 -0
  10. package/src/components/accordion/accordion.stories.tsx +143 -0
  11. package/src/components/accordion/accordion.tsx +135 -0
  12. package/src/components/accordion/index.ts +1 -0
  13. package/src/components/alert/alert.stories.tsx +24 -4
  14. package/src/components/alert/alert.tsx +17 -9
  15. package/src/components/alert-dialog/alert-dialog.stories.tsx +24 -0
  16. package/src/components/alert-dialog/alert-dialog.test.tsx +1 -1
  17. package/src/components/alert-dialog/alert-dialog.tsx +58 -10
  18. package/src/components/auto-complete/auto-complete.stories.tsx +616 -200
  19. package/src/components/auto-complete/auto-complete.tsx +420 -68
  20. package/src/components/auto-complete/index.ts +0 -1
  21. package/src/components/avatar/avatar.stories.tsx +162 -21
  22. package/src/components/avatar/avatar.tsx +79 -20
  23. package/src/components/button/button.stories.tsx +219 -294
  24. package/src/components/button/button.test.tsx +10 -17
  25. package/src/components/button/button.tsx +78 -19
  26. package/src/components/button/components/base-button.tsx +30 -53
  27. package/src/components/button/index.ts +0 -1
  28. package/src/components/calendar/calendar.stories.tsx +1 -1
  29. package/src/components/calendar/calendar.tsx +4 -4
  30. package/src/components/card/card.stories.tsx +141 -69
  31. package/src/components/card/card.tsx +155 -54
  32. package/src/components/center/center.stories.tsx +22 -39
  33. package/src/components/checkbox/checkbox.stories.tsx +25 -5
  34. package/src/components/checkbox/checkbox.tsx +76 -15
  35. package/src/components/checkbox-group/checkbox-group.stories.tsx +116 -28
  36. package/src/components/checkbox-group/checkbox-group.tsx +84 -3
  37. package/src/components/combobox/combobox.stories.tsx +33 -23
  38. package/src/components/combobox/combobox.tsx +119 -103
  39. package/src/components/date-picker/date-input.stories.tsx +14 -6
  40. package/src/components/date-picker/date-input.tsx +2 -2
  41. package/src/components/date-picker/date-picker.model.ts +13 -4
  42. package/src/components/date-picker/date-picker.stories.tsx +38 -12
  43. package/src/components/date-picker/date-picker.tsx +28 -14
  44. package/src/components/dialog/dialog.stories.tsx +18 -0
  45. package/src/components/dialog/dialog.test.tsx +1 -1
  46. package/src/components/dialog/dialog.tsx +51 -20
  47. package/src/components/divider/divider.stories.tsx +6 -0
  48. package/src/components/dropzone/dropzone.stories.tsx +71 -90
  49. package/src/components/dropzone/dropzone.tsx +383 -105
  50. package/src/components/dropzone/index.ts +0 -1
  51. package/src/components/empty/empty.stories.tsx +165 -0
  52. package/src/components/empty/empty.tsx +156 -0
  53. package/src/components/empty/index.ts +1 -0
  54. package/src/components/field/field.stories.tsx +226 -3
  55. package/src/components/field/field.tsx +77 -42
  56. package/src/components/form/form.stories.tsx +320 -197
  57. package/src/components/form/form.tsx +3 -23
  58. package/src/components/index.ts +2 -6
  59. package/src/components/input/input.stories.tsx +5 -5
  60. package/src/components/input/input.tsx +4 -4
  61. package/src/components/kbd/kbd.stories.tsx +1 -0
  62. package/src/components/label/label.stories.tsx +16 -0
  63. package/src/components/label/label.tsx +13 -2
  64. package/src/components/loader/loader.stories.tsx +7 -5
  65. package/src/components/loader/loader.tsx +8 -3
  66. package/src/components/menu/menu-primitives.tsx +207 -196
  67. package/src/components/menu/menu.stories.tsx +276 -146
  68. package/src/components/menu/menu.tsx +146 -54
  69. package/src/components/number-input/number-input.stories.tsx +27 -4
  70. package/src/components/number-input/number-input.test.tsx +2 -2
  71. package/src/components/number-input/number-input.tsx +25 -29
  72. package/src/components/otp/index.ts +1 -0
  73. package/src/components/otp/otp.stories.tsx +209 -0
  74. package/src/components/otp/otp.tsx +100 -0
  75. package/src/components/pagination/index.ts +1 -0
  76. package/src/components/pagination/pagination.model.ts +2 -0
  77. package/src/components/pagination/pagination.stories.tsx +154 -59
  78. package/src/components/pagination/pagination.test.tsx +122 -57
  79. package/src/components/pagination/pagination.tsx +575 -77
  80. package/src/components/password/password.stories.tsx +18 -3
  81. package/src/components/password/password.tsx +26 -10
  82. package/src/components/popover/popover.stories.tsx +26 -5
  83. package/src/components/popover/popover.tsx +15 -23
  84. package/src/components/progress/progress.stories.tsx +1 -0
  85. package/src/components/radio-group/index.ts +1 -0
  86. package/src/components/radio-group/radio-group.stories.tsx +251 -0
  87. package/src/components/radio-group/radio-group.tsx +212 -0
  88. package/src/components/scroll-area/scroll-area.stories.tsx +1 -0
  89. package/src/components/select/select.stories.tsx +118 -19
  90. package/src/components/select/select.tsx +67 -62
  91. package/src/components/skeleton/skeleton.stories.tsx +1 -0
  92. package/src/components/stack/stack.stories.tsx +179 -89
  93. package/src/components/stack/stack.tsx +2 -2
  94. package/src/components/stepper/index.ts +1 -1
  95. package/src/components/stepper/stepper.stories.tsx +767 -83
  96. package/src/components/stepper/stepper.test.tsx +18 -18
  97. package/src/components/stepper/stepper.tsx +554 -0
  98. package/src/components/switch/switch.stories.tsx +15 -1
  99. package/src/components/switch/switch.tsx +17 -4
  100. package/src/components/table/index.ts +0 -2
  101. package/src/components/table/table.stories.tsx +131 -18
  102. package/src/components/table/table.test.tsx +1 -1
  103. package/src/components/table/table.tsx +183 -77
  104. package/src/components/tabs/tabs.stories.tsx +373 -155
  105. package/src/components/tabs/tabs.test.tsx +12 -12
  106. package/src/components/tabs/tabs.tsx +72 -149
  107. package/src/components/tag/index.ts +0 -1
  108. package/src/components/tag/tag.stories.tsx +155 -120
  109. package/src/components/tag/tag.tsx +47 -95
  110. package/src/components/textarea/textarea.stories.tsx +8 -22
  111. package/src/components/textarea/textarea.tsx +17 -79
  112. package/src/components/timeline/timeline.stories.tsx +323 -42
  113. package/src/components/timeline/timeline.tsx +359 -132
  114. package/src/components/toast/toast.stories.tsx +1 -0
  115. package/src/components/tooltip/tooltip.tsx +11 -9
  116. package/src/components/tree/index.ts +0 -1
  117. package/src/components/tree/tree.stories.tsx +365 -408
  118. package/src/components/tree/tree.test.tsx +163 -0
  119. package/src/components/tree/tree.tsx +212 -36
  120. package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +5 -5
  121. package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +1 -3
  122. package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +6 -6
  123. package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +1 -1
  124. package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +1 -1
  125. package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +1 -1
  126. package/src/hooks/usePagination/usePagination.tsx +36 -24
  127. package/src/styles/theme.css +1 -1
  128. package/src/utils/form.tsx +67 -37
  129. package/src/utils/index.ts +1 -1
  130. package/src/__doc__/Migration.mdx +0 -451
  131. package/src/components/auto-complete/auto-complete-primitives.tsx +0 -155
  132. package/src/components/background-image/background-image.stories.tsx +0 -21
  133. package/src/components/background-image/background-image.test.tsx +0 -29
  134. package/src/components/background-image/background-image.tsx +0 -23
  135. package/src/components/background-image/index.ts +0 -1
  136. package/src/components/button/button.variants.ts +0 -44
  137. package/src/components/button/components/loader-overlay.tsx +0 -21
  138. package/src/components/button/components/loading-icon.tsx +0 -47
  139. package/src/components/dropzone/upload-primitives.tsx +0 -310
  140. package/src/components/dropzone/use-dropzone.ts +0 -122
  141. package/src/components/empty-state/empty-state.stories.tsx +0 -56
  142. package/src/components/empty-state/empty-state.tsx +0 -39
  143. package/src/components/empty-state/index.ts +0 -1
  144. package/src/components/heading/heading.stories.tsx +0 -74
  145. package/src/components/heading/heading.tsx +0 -28
  146. package/src/components/heading/heading.variants.ts +0 -27
  147. package/src/components/heading/index.ts +0 -1
  148. package/src/components/kbd/kbd.variants.ts +0 -26
  149. package/src/components/menu/util/render-menu-item.tsx +0 -54
  150. package/src/components/multi-select/hooks/use-multi-select.ts +0 -66
  151. package/src/components/multi-select/index.ts +0 -1
  152. package/src/components/multi-select/multi-select.stories.tsx +0 -294
  153. package/src/components/multi-select/multi-select.tsx +0 -300
  154. package/src/components/multi-select/multi-select.variants.ts +0 -22
  155. package/src/components/pagination/components/pagination-option.tsx +0 -27
  156. package/src/components/show/index.ts +0 -1
  157. package/src/components/show/show.stories.tsx +0 -197
  158. package/src/components/show/show.test.tsx +0 -41
  159. package/src/components/show/show.tsx +0 -16
  160. package/src/components/stepper/Stepper.tsx +0 -190
  161. package/src/components/stepper/context/stepper-context.tsx +0 -11
  162. package/src/components/table/table-primitives.tsx +0 -122
  163. package/src/components/table/table.model.ts +0 -20
  164. package/src/components/table-pagination/index.ts +0 -2
  165. package/src/components/table-pagination/table-pagination.model.ts +0 -2
  166. package/src/components/table-pagination/table-pagination.stories.tsx +0 -23
  167. package/src/components/table-pagination/table-pagination.test.tsx +0 -32
  168. package/src/components/table-pagination/table-pagination.tsx +0 -108
  169. package/src/components/tabs/context/tabs-context.tsx +0 -14
  170. package/src/components/tag/tag.variants.ts +0 -31
  171. package/src/components/timeline/timeline-status.ts +0 -5
  172. package/src/components/tree/hooks/use-controllable-tree-state.ts +0 -80
  173. package/src/components/tree/tree-primitives.tsx +0 -126
@@ -1,241 +1,657 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { BuildingIcon, SearchIcon, UserIcon } from "lucide-react";
3
+ import { useRef, useState, useTransition } from "react";
2
4
  import {
3
- Archive,
4
- Calendar,
5
- FlameKindling,
6
- Github,
7
- Mail,
8
- Settings,
9
- Slack,
10
- TicketPercent,
11
- User,
12
- UserPlus,
13
- } from "lucide-react";
14
- import React, { useState } from "react";
15
- import {
16
- AutoComplete,
17
- AutoCompleteDialog,
18
- AutoCompleteEmpty,
19
- AutoCompleteGroup,
20
- AutoCompleteInput,
21
- AutoCompleteItem,
22
- AutoCompleteList,
23
- AutoCompleteRoot,
24
- AutoCompleteSeparator,
25
- AutoCompleteShortcut,
26
- Button,
27
- MenuContent,
28
- MenuGroup,
29
- MenuRoot,
30
- MenuSub,
31
- MenuSubContent,
32
- MenuSubTrigger,
33
- MenuTrigger,
34
- Popover,
35
- } from "../../components";
36
- import { useHotkey } from "../../hooks";
37
-
38
- const meta: Meta<typeof AutoComplete> = {
39
- title: "Data entry/AutoComplete",
40
- component: AutoComplete,
5
+ Autocomplete,
6
+ AutocompleteCollection,
7
+ AutocompleteEmpty,
8
+ AutocompleteGroup,
9
+ AutocompleteGroupLabel,
10
+ AutocompleteInput,
11
+ AutocompleteItem,
12
+ AutocompleteList,
13
+ type AutocompleteOption,
14
+ AutocompletePopup,
15
+ AutocompleteRoot,
16
+ AutocompleteRow,
17
+ AutocompleteSeparator,
18
+ AutocompleteStatus,
19
+ AutocompleteValue,
20
+ useAutocompleteFilter,
21
+ } from "./auto-complete";
22
+
23
+ /**
24
+ * Autocomplete / combobox built on [Base UI Autocomplete](https://base-ui.com/react/components/autocomplete).
25
+ * Provides a composite `<Autocomplete>` for standard use cases and individual primitives
26
+ * for full structural control (groups, custom layouts, async search).
27
+ *
28
+ * Key behaviors:
29
+ * - `autoHighlight` is `true` by default in `AutocompleteRoot` — the first filtered item is
30
+ * highlighted as the user types without requiring an explicit selection.
31
+ * - `adornment` on `AutocompleteInput` is mutually exclusive: `"trigger"` renders the chevron
32
+ * button, `"clear"` renders the X button. Setting both is not possible by design.
33
+ * - In the composite `Autocomplete`, `onValueChange` fires with the full `TOption` object (or `null`
34
+ * on clear), not just the string value. `value` and `defaultValue` also accept `TOption | null`.
35
+ * - `itemToStringValue` on `AutocompleteRoot` controls what is shown in the input after selection.
36
+ * The composite sets it to `(opt) => opt.label` automatically.
37
+ * - `filter={null}` disables client-side filtering entirely — use it for server-side search.
38
+ * - `AutocompleteStatus` collapses to zero height when its children are empty (`empty:m-0 empty:p-0`).
39
+ * Pass `undefined` (not an empty string) to hide it.
40
+ * - `AutocompleteEmpty` is shown by Base UI only when the list has no visible items.
41
+ *
42
+ * Reference: [Base UI – Autocomplete](https://base-ui.com/react/components/autocomplete)
43
+ */
44
+ const meta: Meta<typeof Autocomplete> = {
45
+ title: "Components/Autocomplete",
46
+ component: Autocomplete,
47
+ parameters: { layout: "centered" },
48
+ decorators: [
49
+ (Story) => (
50
+ <div className="w-72">
51
+ <Story />
52
+ </div>
53
+ ),
54
+ ],
55
+ args: {
56
+ items: [
57
+ { value: "apple", label: "Apple" },
58
+ { value: "banana", label: "Banana" },
59
+ { value: "cherry", label: "Cherry" },
60
+ { value: "grape", label: "Grape" },
61
+ { value: "kiwi", label: "Kiwi" },
62
+ { value: "mango", label: "Mango" },
63
+ { value: "orange", label: "Orange" },
64
+ { value: "peach", label: "Peach" },
65
+ { value: "pear", label: "Pear" },
66
+ { value: "strawberry", label: "Strawberry" },
67
+ ],
68
+ placeholder: "Search...",
69
+ emptyMessage: "No results",
70
+ },
71
+ argTypes: {
72
+ onValueChange: { control: false },
73
+ renderOption: { control: false },
74
+ startAddon: { control: false },
75
+ classNames: { control: false },
76
+ },
77
+ tags: ["beta"],
41
78
  };
42
79
 
43
80
  export default meta;
44
- type Story = StoryObj<typeof AutoComplete>;
81
+ type Story = StoryObj<typeof Autocomplete>;
82
+
83
+ export const Default: Story = {};
84
+
85
+ /**
86
+ * `className` styles the input field. `classNames` exposes the popup, list,
87
+ * item, empty, and status slots for fine-grained tweaks without dropping to
88
+ * the primitive composition.
89
+ *
90
+ * For deeper customization (custom layouts, extra slots, async patterns) use
91
+ * the primitives directly — they are the real extension point.
92
+ */
93
+ export const WithClassNames: Story = {
94
+ args: {
95
+ className: "w-80",
96
+ classNames: {
97
+ popup: "max-h-60",
98
+ item: "py-2",
99
+ empty: "italic",
100
+ },
101
+ },
102
+ };
103
+
104
+ /**
105
+ * The trigger button opens and closes the popup on click — useful when
106
+ * the user wants to browse all options without typing.
107
+ */
108
+ export const WithTrigger: Story = {
109
+ args: {
110
+ adornment: "trigger",
111
+ },
112
+ };
45
113
 
46
- const items = [
47
- { value: "apple", label: "Apple" },
48
- { value: "banana", label: "Banana" },
49
- { value: "cherry", label: "Cherry" },
50
- ];
114
+ /**
115
+ * The clear button appears whenever the input has a value and resets it on click.
116
+ * `adornment` is mutually exclusive — `"clear"` and `"trigger"` cannot both be active.
117
+ */
118
+ export const WithClear: Story = {
119
+ args: {
120
+ adornment: "clear",
121
+ },
122
+ };
51
123
 
52
- export const Default: Story = {
124
+ export const WithStartAddon: Story = {
53
125
  args: {
54
- items: items,
55
- onSelect: console.log,
56
- placeholder: "Search fruits",
126
+ startAddon: <SearchIcon />,
127
+ },
128
+ };
129
+
130
+ export const Disabled: Story = {
131
+ args: { disabled: true },
132
+ };
133
+
134
+ export const ReadOnly: Story = {
135
+ args: { readOnly: true, defaultValue: { value: "mango", label: "Mango" } },
136
+ };
137
+
138
+ /**
139
+ * `renderOption` receives the full option object typed as `TOption extends AutocompleteOption`
140
+ * and returns a `ReactNode`. Use it to render icons, metadata, descriptions, or any layout.
141
+ * When `renderOption` is provided, `option.label` is NOT rendered automatically.
142
+ *
143
+ * Extend `AutocompleteOption` to add extra fields:
144
+ * ```ts
145
+ * interface UserOption extends AutocompleteOption {
146
+ * role: "admin" | "user" | "company";
147
+ * }
148
+ * ```
149
+ */
150
+ export const WithCustomOption: Story = {
151
+ render: () => {
152
+ interface UserOption {
153
+ value: string;
154
+ label: string;
155
+ role: string;
156
+ }
157
+
158
+ const users: UserOption[] = [
159
+ { value: "ana", label: "Ana García", role: "admin" },
160
+ { value: "carlos", label: "Carlos López", role: "user" },
161
+ { value: "maria", label: "María Fernández", role: "admin" },
162
+ { value: "acme", label: "Acme Corp.", role: "company" },
163
+ ];
164
+
165
+ const RoleIcon = ({ role }: { role: string }) =>
166
+ role === "company" ? (
167
+ <BuildingIcon className="size-4 shrink-0 text-muted-foreground" />
168
+ ) : (
169
+ <UserIcon className="size-4 shrink-0 text-muted-foreground" />
170
+ );
171
+
172
+ return (
173
+ <Autocomplete
174
+ items={users}
175
+ placeholder="Search user..."
176
+ emptyMessage="No users found"
177
+ adornment="clear"
178
+ startAddon={<SearchIcon />}
179
+ renderOption={(u) => (
180
+ <span className="flex items-center gap-2">
181
+ <RoleIcon role={u.role} />
182
+ <span className="flex-1">{u.label}</span>
183
+ <span className="text-xs capitalize text-muted-foreground">
184
+ {u.role}
185
+ </span>
186
+ </span>
187
+ )}
188
+ />
189
+ );
57
190
  },
58
191
  };
59
192
 
60
193
  /**
61
- * Componente primitive AutoComplete que permite construir un campo de búsqueda con sugerencias.
62
- * Filtra y ordena los elementos de una lista de sugerencias.
63
- * ## Estructura
194
+ * `onValueChange` fires with the full `TOption` object (or `null` when cleared).
195
+ * `value` and `defaultValue` also accept `TOption | null`, matching the
196
+ * same pattern as `Select` and `Combobox` in this library.
197
+ *
198
+ * To clear programmatically, set `value={null}`.
199
+ */
200
+ export const Controlled: Story = {
201
+ render: () => {
202
+ const items = [
203
+ { value: "apple", label: "Apple" },
204
+ { value: "banana", label: "Banana" },
205
+ { value: "cherry", label: "Cherry" },
206
+ { value: "grape", label: "Grape" },
207
+ { value: "mango", label: "Mango" },
208
+ { value: "orange", label: "Orange" },
209
+ ];
210
+ const [selected, setSelected] = useState<AutocompleteOption | null>(null);
211
+
212
+ return (
213
+ <div className="flex flex-col gap-4">
214
+ <Autocomplete
215
+ items={items}
216
+ value={selected}
217
+ onValueChange={setSelected}
218
+ adornment="clear"
219
+ placeholder="Search fruit..."
220
+ emptyMessage="No results"
221
+ />
222
+ <p className="text-sm text-muted-foreground">
223
+ Selected:{" "}
224
+ <strong>
225
+ {selected ? `${selected.label} (${selected.value})` : "—"}
226
+ </strong>
227
+ </p>
228
+ </div>
229
+ );
230
+ },
231
+ };
232
+
233
+ /**
234
+ * For server-side search, set `filter={null}` on `AutocompleteRoot` to disable client-side
235
+ * filtering entirely — the caller owns the `items` prop and updates it on each keystroke.
236
+ *
237
+ * Pattern:
238
+ * - `useTransition` exposes a `isPending` flag without blocking the UI.
239
+ * - `AbortController` cancels the previous request when the user types again.
240
+ * - `useAutocompleteFilter` provides the same `contains` logic used internally,
241
+ * so you can apply it on the server response if needed.
242
+ * - Hide `AutocompleteEmpty` while `isPending` to avoid a flash of "no results".
243
+ *
244
+ * ```tsx
245
+ * <AutocompleteRoot
246
+ * items={results}
247
+ * filter={null}
248
+ * itemToStringValue={(item) => item.label}
249
+ * onValueChange={search}
250
+ * >
251
+ * <AutocompleteInput placeholder="Search..." adornment="clear"/>
252
+ * <AutocompletePopup>
253
+ * <AutocompleteStatus>{isPending ? "Searching..." : undefined}</AutocompleteStatus>
254
+ * <AutocompleteList>
255
+ * {(item) => (
256
+ * <AutocompleteItem key={item.value} value={item}>
257
+ * {item.label}
258
+ * </AutocompleteItem>
259
+ * )}
260
+ * </AutocompleteList>
261
+ * {!isPending && <AutocompleteEmpty>No results</AutocompleteEmpty>}
262
+ * </AutocompletePopup>
263
+ * </AutocompleteRoot>
264
+ * ```
265
+ */
266
+ export const AsyncSearch: Story = {
267
+ render: () => {
268
+ const allFruits = [
269
+ { value: "apple", label: "Apple" },
270
+ { value: "banana", label: "Banana" },
271
+ { value: "cherry", label: "Cherry" },
272
+ { value: "grape", label: "Grape" },
273
+ { value: "kiwi", label: "Kiwi" },
274
+ { value: "mango", label: "Mango" },
275
+ { value: "orange", label: "Orange" },
276
+ ];
277
+
278
+ const [results, setResults] = useState<typeof allFruits>([]);
279
+ const [isPending, startTransition] = useTransition();
280
+ const abortRef = useRef<AbortController | null>(null);
281
+ const { contains } = useAutocompleteFilter();
282
+
283
+ const search = (query: string) => {
284
+ abortRef.current?.abort();
285
+ const controller = new AbortController();
286
+ abortRef.current = controller;
287
+
288
+ if (!query) {
289
+ setResults([]);
290
+ return;
291
+ }
292
+
293
+ startTransition(async () => {
294
+ await new Promise((r) => setTimeout(r, 400));
295
+ if (controller.signal.aborted) return;
296
+ startTransition(() => {
297
+ setResults(allFruits.filter((f) => contains(f.label, query)));
298
+ });
299
+ });
300
+ };
301
+
302
+ return (
303
+ <AutocompleteRoot items={results} filter={null} onValueChange={search}>
304
+ <AutocompleteInput placeholder="Search fruit..." adornment="clear" />
305
+ <AutocompletePopup>
306
+ <AutocompleteStatus>
307
+ {isPending ? "Searching..." : undefined}
308
+ </AutocompleteStatus>
309
+ <AutocompleteList>
310
+ {(fruit) => (
311
+ <AutocompleteItem key={fruit.value} value={fruit}>
312
+ {fruit.label}
313
+ </AutocompleteItem>
314
+ )}
315
+ </AutocompleteList>
316
+ {!isPending && <AutocompleteEmpty>No results</AutocompleteEmpty>}
317
+ </AutocompletePopup>
318
+ </AutocompleteRoot>
319
+ );
320
+ },
321
+ };
322
+
323
+ /**
324
+ * Grouped items require the primitive composition pattern — the composite `Autocomplete`
325
+ * does not support groups. Pass group objects to `AutocompleteRoot.items`, where each group
326
+ * has a nested `items` array. Use `AutocompleteCollection` inside each `AutocompleteGroup`
327
+ * so Base UI can filter and highlight items within groups independently.
328
+ *
329
+ * `AutocompleteGroupLabel` is sticky and not selectable. `[[role=group]+&]:mt-1.5` on
330
+ * `AutocompleteGroup` adds spacing between consecutive groups automatically.
331
+ *
64
332
  * ```tsx
65
- * <AutocompleteRoot>
66
- * <AutocompleteInput placeholder="Buscar..." />
333
+ * <AutocompleteRoot items={groups}>
67
334
  * <AutocompleteList>
68
- * <AutocompleteEmpty>No Se Encontraron Resultados</AutocompleteEmpty>
69
- * <AutocompleteGroup>
70
- * <AutocompleteItem>Calendario</AutocompleteItem>
71
- * <AutocompleteItem>Eventos</AutocompleteItem>
72
- * <AutocompleteItem>Archivados</AutocompleteItem>
73
- * </AutocompleteGroup>
335
+ * {(group) => (
336
+ * <AutocompleteGroup key={group.value} items={group.items}>
337
+ * <AutocompleteGroupLabel>{group.label}</AutocompleteGroupLabel>
338
+ * <AutocompleteCollection>
339
+ * {(item) => (
340
+ * <AutocompleteItem key={item.value} value={item}>
341
+ * {item.label}
342
+ * </AutocompleteItem>
343
+ * )}
344
+ * </AutocompleteCollection>
345
+ * </AutocompleteGroup>
346
+ * )}
74
347
  * </AutocompleteList>
75
348
  * </AutocompleteRoot>
76
- * ```
77
- * */
78
- export const Primitive: Story = {
349
+ * ```
350
+ */
351
+ export const WithGroups: Story = {
79
352
  render: () => {
353
+ interface FoodItem {
354
+ value: string;
355
+ label: string;
356
+ }
357
+ interface FoodGroup {
358
+ value: string;
359
+ items: FoodItem[];
360
+ }
361
+
362
+ const groups: FoodGroup[] = [
363
+ {
364
+ value: "fruits",
365
+ items: [
366
+ { value: "apple", label: "Apple" },
367
+ { value: "banana", label: "Banana" },
368
+ { value: "orange", label: "Orange" },
369
+ ],
370
+ },
371
+ {
372
+ value: "vegetables",
373
+ items: [
374
+ { value: "carrot", label: "Carrot" },
375
+ { value: "spinach", label: "Spinach" },
376
+ { value: "broccoli", label: "Broccoli" },
377
+ ],
378
+ },
379
+ ];
380
+
80
381
  return (
81
- <AutoCompleteRoot className="border border-input">
82
- <AutoCompleteInput placeholder="Buscar..." />
83
- <AutoCompleteList>
84
- <AutoCompleteEmpty>No se encontraron resultados</AutoCompleteEmpty>
85
- <AutoCompleteGroup>
86
- <AutoCompleteItem>Calendario</AutoCompleteItem>
87
- <AutoCompleteItem>Eventos</AutoCompleteItem>
88
- <AutoCompleteItem>Archivados</AutoCompleteItem>
89
- </AutoCompleteGroup>
90
- <AutoCompleteSeparator />
91
- </AutoCompleteList>
92
- </AutoCompleteRoot>
382
+ <AutocompleteRoot items={groups}>
383
+ <AutocompleteInput placeholder="Search food..." adornment="trigger" />
384
+ <AutocompletePopup>
385
+ <AutocompleteList>
386
+ {(group: FoodGroup) => (
387
+ <AutocompleteGroup key={group.value} items={group.items}>
388
+ <AutocompleteGroupLabel className="capitalize">
389
+ {group.value}
390
+ </AutocompleteGroupLabel>
391
+ <AutocompleteCollection>
392
+ {(item: FoodItem) => (
393
+ <AutocompleteItem key={item.value} value={item}>
394
+ {item.label}
395
+ </AutocompleteItem>
396
+ )}
397
+ </AutocompleteCollection>
398
+ </AutocompleteGroup>
399
+ )}
400
+ </AutocompleteList>
401
+ <AutocompleteEmpty>No results</AutocompleteEmpty>
402
+ </AutocompletePopup>
403
+ </AutocompleteRoot>
93
404
  );
94
405
  },
95
406
  };
96
407
 
97
- export const Dialog: Story = {
408
+ /**
409
+ * Base UI's `render` prop replaces the underlying DOM element while preserving all
410
+ * autocomplete behavior (keyboard navigation, ARIA attributes, filtering, selection).
411
+ *
412
+ * Pass `render={<textarea />}` to `AutocompleteInput` to enable multiline text entry.
413
+ * The override works because `{...props}` is spread after the default
414
+ * `render={<input autoComplete="off" />}` inside `AutocompleteInput`.
415
+ *
416
+ * Override `h-10` from `inputBaseClasses` via `className="h-auto"` since textarea
417
+ * height is determined by `rows` rather than a fixed value.
418
+ */
419
+ export const customRender: Story = {
420
+ render: () => (
421
+ <AutocompleteRoot
422
+ items={[
423
+ { value: "apple", label: "Apple" },
424
+ { value: "banana", label: "Banana" },
425
+ { value: "cherry", label: "Cherry" },
426
+ { value: "grape", label: "Grape" },
427
+ { value: "kiwi", label: "Kiwi" },
428
+ ]}
429
+ >
430
+ <AutocompleteInput
431
+ placeholder="Search..."
432
+ render={<textarea rows={3} />}
433
+ className="h-auto resize-none"
434
+ />
435
+ <AutocompletePopup>
436
+ <AutocompleteList>
437
+ {(item) => (
438
+ <AutocompleteItem key={item.value} value={item}>
439
+ {item.label}
440
+ </AutocompleteItem>
441
+ )}
442
+ </AutocompleteList>
443
+ <AutocompleteEmpty>No results</AutocompleteEmpty>
444
+ </AutocompletePopup>
445
+ </AutocompleteRoot>
446
+ ),
447
+ };
448
+
449
+ /**
450
+ * `triggerProps.render` replaces the trigger's `<button>` element.
451
+ * Base UI transfers all accessibility attributes (aria-*, data-*, event handlers)
452
+ * to the provided element, so the open/close behavior is fully preserved.
453
+ *
454
+ * `triggerProps.children` replaces the default `ChevronsUpDown` icon.
455
+ * `triggerProps.className` is merged with (not replaced by) the default trigger classes,
456
+ * so positioning and sizing stay intact.
457
+ */
458
+ export const WithCustomTrigger: Story = {
459
+ render: () => (
460
+ <AutocompleteRoot
461
+ items={[
462
+ { value: "apple", label: "Apple" },
463
+ { value: "banana", label: "Banana" },
464
+ { value: "cherry", label: "Cherry" },
465
+ { value: "grape", label: "Grape" },
466
+ { value: "kiwi", label: "Kiwi" },
467
+ ]}
468
+ >
469
+ <AutocompleteInput
470
+ adornment="trigger"
471
+ placeholder="Search fruit..."
472
+ triggerProps={{
473
+ render: <button type="button" />,
474
+ className:
475
+ "px-2 text-xs font-medium text-muted-foreground hover:text-foreground",
476
+ children: "▾",
477
+ }}
478
+ />
479
+ <AutocompletePopup>
480
+ <AutocompleteList>
481
+ {(item) => (
482
+ <AutocompleteItem key={item.value} value={item}>
483
+ {item.label}
484
+ </AutocompleteItem>
485
+ )}
486
+ </AutocompleteList>
487
+ <AutocompleteEmpty>No results</AutocompleteEmpty>
488
+ </AutocompletePopup>
489
+ </AutocompleteRoot>
490
+ ),
491
+ };
492
+
493
+ /**
494
+ * `AutocompleteRow` groups items into a horizontal row for multi-column list layouts.
495
+ * Useful for compact grids like color pickers, flag selectors, or emoji pickers.
496
+ * Each item inside the row is individually selectable and keyboard-navigable.
497
+ *
498
+ * Pass rows as the `items` array — each entry is a row, and the row's items are
499
+ * rendered with `AutocompleteItem` inside.
500
+ */
501
+ export const WithRow: Story = {
98
502
  render: () => {
99
- const [open, setOpen] = React.useState(false);
100
- useHotkey("shift+/", () => setOpen((open) => !open), {
101
- ignoreInputFields: false,
102
- });
503
+ interface ColorOption {
504
+ value: string;
505
+ label: string;
506
+ hex: string;
507
+ }
508
+ type ColorRow = ColorOption[];
509
+
510
+ const colorRows: ColorRow[] = [
511
+ [
512
+ { value: "red", label: "Red", hex: "#ef4444" },
513
+ { value: "orange", label: "Orange", hex: "#f97316" },
514
+ { value: "yellow", label: "Yellow", hex: "#eab308" },
515
+ ],
516
+ [
517
+ { value: "green", label: "Green", hex: "#22c55e" },
518
+ { value: "blue", label: "Blue", hex: "#3b82f6" },
519
+ { value: "purple", label: "Purple", hex: "#a855f7" },
520
+ ],
521
+ ];
103
522
 
104
523
  return (
105
- <>
106
- <p className="text-sm flex items-center gap-x-2">
107
- Presiona
108
- <kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
109
- /
110
- </kbd>
111
- para abrir el diálogo de comandos.
112
- </p>
113
- <AutoCompleteDialog open={open} onOpenChange={setOpen}>
114
- <AutoCompleteInput placeholder="Busca una vista o escribe un comando..." />
115
- <AutoCompleteList>
116
- <AutoCompleteEmpty>No se encontraron resultados</AutoCompleteEmpty>
117
- <AutoCompleteGroup heading="Sugerencias">
118
- <AutoCompleteItem onSelect={(value) => console.log(value)}>
119
- <Calendar />
120
- <span>Calendario</span>
121
- </AutoCompleteItem>
122
- <AutoCompleteItem>
123
- <TicketPercent />
124
- <span>Eventos</span>
125
- </AutoCompleteItem>
126
- <AutoCompleteItem>
127
- <Archive />
128
- <span>Archivados</span>
129
- </AutoCompleteItem>
130
- </AutoCompleteGroup>
131
- <AutoCompleteSeparator />
132
- <AutoCompleteGroup heading="Configuraciones">
133
- <AutoCompleteItem>
134
- <User />
135
- <span>Mi perfil</span>
136
- <AutoCompleteShortcut>P</AutoCompleteShortcut>
137
- </AutoCompleteItem>
138
- <AutoCompleteItem>
139
- <Settings />
140
- <span>Configuración</span>
141
- <AutoCompleteShortcut>C</AutoCompleteShortcut>
142
- </AutoCompleteItem>
143
- </AutoCompleteGroup>
144
- </AutoCompleteList>
145
- </AutoCompleteDialog>
146
- </>
524
+ <AutocompleteRoot
525
+ items={colorRows}
526
+ itemToStringValue={(row) => (row as ColorRow)[0]?.label ?? ""}
527
+ >
528
+ <AutocompleteInput placeholder="Search color..." />
529
+ <AutocompletePopup>
530
+ <AutocompleteList>
531
+ {(row: ColorRow) => (
532
+ <AutocompleteRow key={row[0].value}>
533
+ {row.map((color) => (
534
+ <AutocompleteItem
535
+ key={color.value}
536
+ value={color}
537
+ className="flex-col gap-1 rounded-md p-2 text-center text-xs"
538
+ >
539
+ <span
540
+ className="block size-6 rounded-full border"
541
+ style={{ background: color.hex }}
542
+ />
543
+ {color.label}
544
+ </AutocompleteItem>
545
+ ))}
546
+ </AutocompleteRow>
547
+ )}
548
+ </AutocompleteList>
549
+ <AutocompleteEmpty>No results</AutocompleteEmpty>
550
+ </AutocompletePopup>
551
+ </AutocompleteRoot>
147
552
  );
148
553
  },
149
554
  };
150
555
 
151
- export const SearchableMenu = {
152
- name: "Menu",
556
+ /**
557
+ * `AutocompleteValue` renders the selected item's label outside the input.
558
+ * Use it when you need to display the current selection independently —
559
+ * e.g., inside a custom trigger, a badge, or a read-only display area.
560
+ *
561
+ * It renders nothing when no item is selected, so no conditional rendering is needed.
562
+ * The displayed text is determined by `itemToStringValue` on `AutocompleteRoot`.
563
+ */
564
+ export const WithValueDisplay: Story = {
153
565
  render: () => {
154
- const [open, setOpen] = useState(false);
566
+ const countries = [
567
+ { value: "ar", label: "Argentina" },
568
+ { value: "br", label: "Brazil" },
569
+ { value: "cl", label: "Chile" },
570
+ { value: "co", label: "Colombia" },
571
+ { value: "mx", label: "Mexico" },
572
+ { value: "uy", label: "Uruguay" },
573
+ ];
155
574
 
156
575
  return (
157
- <MenuRoot open={open} onOpenChange={setOpen}>
158
- <MenuTrigger asChild>
159
- <Button variant="outline">Open</Button>
160
- </MenuTrigger>
161
- <MenuContent className="w-56">
162
- <MenuSub>
163
- <MenuSubTrigger>
164
- <UserPlus className="mr-2 h-4 w-4" />
165
- Añadir usuario
166
- </MenuSubTrigger>
167
- <MenuGroup>
168
- <MenuSubContent>
169
- <AutoCompleteRoot>
170
- <AutoCompleteInput placeholder="Buscar..." autoFocus={true} />
171
- <AutoCompleteList>
172
- <AutoCompleteEmpty>No hay resultados</AutoCompleteEmpty>
173
- <AutoCompleteGroup heading="Medio de notificación">
174
- <AutoCompleteItem>
175
- <Mail />
176
- Correo electrónico
177
- </AutoCompleteItem>
178
- <AutoCompleteItem>
179
- <Slack />
180
- Slack
181
- </AutoCompleteItem>
182
- <AutoCompleteItem>
183
- <Github />
184
- Github
185
- </AutoCompleteItem>
186
- <AutoCompleteItem>
187
- <FlameKindling />
188
- Señal de humo
189
- </AutoCompleteItem>
190
- </AutoCompleteGroup>
191
- </AutoCompleteList>
192
- </AutoCompleteRoot>
193
- </MenuSubContent>
194
- </MenuGroup>
195
- </MenuSub>
196
- </MenuContent>
197
- </MenuRoot>
576
+ <AutocompleteRoot
577
+ items={countries}
578
+ itemToStringValue={(c) => (c as (typeof countries)[0]).label}
579
+ >
580
+ <AutocompleteInput placeholder="Search country..." adornment="clear" />
581
+ <div className="mt-2 flex items-center gap-2 text-sm text-muted-foreground">
582
+ <span>Selected:</span>
583
+ <AutocompleteValue />
584
+ </div>
585
+ <AutocompletePopup>
586
+ <AutocompleteList>
587
+ {(country) => (
588
+ <AutocompleteItem key={country.value} value={country}>
589
+ {country.label}
590
+ </AutocompleteItem>
591
+ )}
592
+ </AutocompleteList>
593
+ <AutocompleteEmpty>No results</AutocompleteEmpty>
594
+ </AutocompletePopup>
595
+ </AutocompleteRoot>
198
596
  );
199
597
  },
200
598
  };
201
599
 
202
- export const SearchablePopover = {
203
- name: "Popover",
600
+ /**
601
+ * Direct composition with all primitives for full structural control.
602
+ * Use when the composite `Autocomplete` is not enough — custom element ordering,
603
+ * extra slots between parts, non-standard layouts, or `AutocompleteSeparator` usage.
604
+ *
605
+ * `AutocompleteSeparator` uses `last:hidden` to suppress a trailing divider automatically.
606
+ *
607
+ * ```tsx
608
+ * <AutocompleteRoot items={items}>
609
+ * <AutocompleteInput placeholder="Search..." adornment="clear"/>
610
+ * <AutocompletePopup>
611
+ * <AutocompleteStatus>{status}</AutocompleteStatus>
612
+ * <AutocompleteList>
613
+ * {(item) => (
614
+ * <>
615
+ * <AutocompleteItem key={item.value} value={item}>
616
+ * {item.label}
617
+ * </AutocompleteItem>
618
+ * <AutocompleteSeparator />
619
+ * </>
620
+ * )}
621
+ * </AutocompleteList>
622
+ * <AutocompleteEmpty>No results</AutocompleteEmpty>
623
+ * </AutocompletePopup>
624
+ * </AutocompleteRoot>
625
+ * ```
626
+ */
627
+ export const Primitive: Story = {
204
628
  render: () => {
629
+ const countries = [
630
+ { value: "ar", label: "Argentina" },
631
+ { value: "br", label: "Brazil" },
632
+ { value: "cl", label: "Chile" },
633
+ { value: "co", label: "Colombia" },
634
+ { value: "mx", label: "Mexico" },
635
+ { value: "uy", label: "Uruguay" },
636
+ ];
637
+
205
638
  return (
206
- <Popover
207
- trigger={
208
- <Button>
209
- <UserPlus className="mr-2 h-4 w-4" />
210
- Añadir usuario
211
- </Button>
212
- }
213
- >
214
- <AutoCompleteRoot>
215
- <AutoCompleteInput placeholder="Buscar..." autoFocus={true} />
216
- <AutoCompleteList>
217
- <AutoCompleteEmpty>No hay resultados</AutoCompleteEmpty>
218
- <AutoCompleteGroup heading="Medio de notificación">
219
- <AutoCompleteItem>
220
- <Mail />
221
- Correo electrónico
222
- </AutoCompleteItem>
223
- <AutoCompleteItem>
224
- <Slack />
225
- Slack
226
- </AutoCompleteItem>
227
- <AutoCompleteItem>
228
- <Github />
229
- Github
230
- </AutoCompleteItem>
231
- <AutoCompleteItem>
232
- <FlameKindling />
233
- Señal de humo
234
- </AutoCompleteItem>
235
- </AutoCompleteGroup>
236
- </AutoCompleteList>
237
- </AutoCompleteRoot>
238
- </Popover>
639
+ <AutocompleteRoot items={countries}>
640
+ <AutocompleteInput placeholder="Search country..." adornment="clear" />
641
+ <AutocompletePopup>
642
+ <AutocompleteList>
643
+ {(country) => (
644
+ <>
645
+ <AutocompleteItem key={country.value} value={country}>
646
+ {country.label}
647
+ </AutocompleteItem>
648
+ <AutocompleteSeparator />
649
+ </>
650
+ )}
651
+ </AutocompleteList>
652
+ <AutocompleteEmpty>No results</AutocompleteEmpty>
653
+ </AutocompletePopup>
654
+ </AutocompleteRoot>
239
655
  );
240
656
  },
241
657
  };