@boxcustodia/library 2.0.0-alpha.13 → 2.0.0-alpha.15

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