@soyfri/shared-library 2.0.0-beta.2 → 2.0.0-beta.4

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 (187) hide show
  1. package/.dockerignore +8 -0
  2. package/.github/workflows/publish.yml +107 -0
  3. package/.prettierrc +3 -0
  4. package/.storybook/main.ts +19 -0
  5. package/.storybook/preview.ts +14 -0
  6. package/.storybook/vitest.setup.ts +9 -0
  7. package/Dockerfile +37 -0
  8. package/build.js +102 -0
  9. package/chromatic.config.json +5 -0
  10. package/cleanDirectories.js +40 -0
  11. package/dist/README.md +243 -0
  12. package/dist/components/Icon/Icon.js +1 -1
  13. package/dist/components/Table/Table.js +1 -1
  14. package/dist/index.cjs +24 -0
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.js +7 -1
  17. package/dist/mui.d.ts +1 -0
  18. package/dist/package.json +197 -0
  19. package/package.json +4 -32
  20. package/rollup.config.cjs +87 -0
  21. package/src/components/ActionMenu/ActionMenu.stories.tsx +230 -0
  22. package/src/components/ActionMenu/ActionMenu.tsx +174 -0
  23. package/src/components/ActionMenu/index.ts +2 -0
  24. package/src/components/AppBar/AppBar.stories.tsx +272 -0
  25. package/src/components/AppBar/AppBar.sx.ts +32 -0
  26. package/src/components/AppBar/AppBar.tsx +123 -0
  27. package/src/components/AppBar/AppBarBrand.tsx +120 -0
  28. package/src/components/AppBar/AppBarContext.ts +25 -0
  29. package/src/components/AppBar/AppBarMenuToggle.tsx +90 -0
  30. package/src/components/AppBar/AppBarUserMenu.tsx +217 -0
  31. package/src/components/AppBar/index.ts +25 -0
  32. package/src/components/Autocomplete/Autocomplete.definitions.ts +477 -0
  33. package/src/components/Autocomplete/Autocomplete.helpers.ts +60 -0
  34. package/src/components/Autocomplete/Autocomplete.stories.tsx +748 -0
  35. package/src/components/Autocomplete/Autocomplete.sx.ts +30 -0
  36. package/src/components/Autocomplete/Autocomplete.tsx +361 -0
  37. package/src/components/Autocomplete/Autocomplete.types.ts +13 -0
  38. package/src/components/Autocomplete/_parts/AutocompleteChips.tsx +55 -0
  39. package/src/components/Autocomplete/_parts/AutocompleteLoader.tsx +17 -0
  40. package/src/components/Autocomplete/_parts/AutocompleteOption.tsx +31 -0
  41. package/src/components/Autocomplete/index.ts +12 -0
  42. package/src/components/Avatar/Avatar.definitions.ts +162 -0
  43. package/src/components/Avatar/Avatar.stories.tsx +258 -0
  44. package/src/components/Avatar/Avatar.tsx +206 -0
  45. package/src/components/Avatar/index.ts +1 -0
  46. package/src/components/Button/Button.definition.ts +97 -0
  47. package/src/components/Button/Button.stories.tsx +285 -0
  48. package/src/components/Button/Button.tsx +67 -0
  49. package/src/components/Button/index.ts +1 -0
  50. package/src/components/Card/Card.definition.ts +5 -0
  51. package/src/components/Card/Card.stories.tsx +221 -0
  52. package/src/components/Card/Card.sx.ts +104 -0
  53. package/src/components/Card/Card.tsx +200 -0
  54. package/src/components/Card/index.ts +9 -0
  55. package/src/components/Chip/Chip.definitions.ts +167 -0
  56. package/src/components/Chip/Chip.stories.tsx +265 -0
  57. package/src/components/Chip/Chip.tsx +61 -0
  58. package/src/components/Chip/index.ts +1 -0
  59. package/src/components/Column/Column.tsx +29 -0
  60. package/src/components/Column/index.ts +1 -0
  61. package/src/components/DatePicker/DatePicker.definitions.ts +228 -0
  62. package/src/components/DatePicker/DatePicker.helpers.ts +24 -0
  63. package/src/components/DatePicker/DatePicker.stories.tsx +309 -0
  64. package/src/components/DatePicker/DatePicker.sx.ts +33 -0
  65. package/src/components/DatePicker/DatePicker.tsx +189 -0
  66. package/src/components/DatePicker/DatePicker.types.ts +10 -0
  67. package/src/components/DatePicker/index.ts +9 -0
  68. package/src/components/DateRangePicker/DateRangePicker.definitions.ts +191 -0
  69. package/src/components/DateRangePicker/DateRangePicker.stories.tsx +252 -0
  70. package/src/components/DateRangePicker/DateRangePicker.tsx +56 -0
  71. package/src/components/DateRangePicker/index.ts +1 -0
  72. package/src/components/DateTimePicker/DateTimePicker.definitions.ts +256 -0
  73. package/src/components/DateTimePicker/DateTimePicker.helpers.ts +38 -0
  74. package/src/components/DateTimePicker/DateTimePicker.stories.tsx +418 -0
  75. package/src/components/DateTimePicker/DateTimePicker.sx.ts +30 -0
  76. package/src/components/DateTimePicker/DateTimePicker.tsx +225 -0
  77. package/src/components/DateTimePicker/DateTimePicker.types.ts +10 -0
  78. package/src/components/DateTimePicker/index.ts +9 -0
  79. package/src/components/Drawer/Drawer.stories.tsx +270 -0
  80. package/src/components/Drawer/Drawer.sx.ts +106 -0
  81. package/src/components/Drawer/Drawer.tsx +214 -0
  82. package/src/components/Drawer/DrawerContext.ts +26 -0
  83. package/src/components/Drawer/DrawerItem.tsx +110 -0
  84. package/src/components/Drawer/index.ts +10 -0
  85. package/src/components/Flyout/Flyout.stories.tsx +282 -0
  86. package/src/components/Flyout/Flyout.tsx +122 -0
  87. package/src/components/Flyout/index.ts +1 -0
  88. package/src/components/Gallery/Gallery.definition.tsx +37 -0
  89. package/src/components/Gallery/Gallery.stories.tsx +82 -0
  90. package/src/components/Gallery/Gallery.tsx +118 -0
  91. package/src/components/Gallery/GalleryLightbox.tsx +170 -0
  92. package/src/components/Gallery/GalleryMain.tsx +84 -0
  93. package/src/components/Gallery/GalleryThumbnails.tsx +106 -0
  94. package/src/components/Gallery/index.ts +1 -0
  95. package/src/components/Icon/Icon.stories.tsx +121 -0
  96. package/src/components/Icon/Icon.tsx +175 -0
  97. package/src/components/Icon/index.ts +2 -0
  98. package/src/components/Input/Input.definitions.ts +324 -0
  99. package/src/components/Input/Input.helpers.ts +49 -0
  100. package/src/components/Input/Input.stories.tsx +499 -0
  101. package/src/components/Input/Input.sx.ts +42 -0
  102. package/src/components/Input/Input.tsx +141 -0
  103. package/src/components/Input/Input.types.ts +10 -0
  104. package/src/components/Input/index.ts +9 -0
  105. package/src/components/InputGroup/InputGroup.definitions.ts +158 -0
  106. package/src/components/InputGroup/InputGroup.stories.tsx +267 -0
  107. package/src/components/InputGroup/InputGroup.tsx +179 -0
  108. package/src/components/InputGroup/index.ts +1 -0
  109. package/src/components/MenuButton/MenuButton.stories.tsx +197 -0
  110. package/src/components/MenuButton/MenuButton.tsx +100 -0
  111. package/src/components/MenuButton/index.ts +1 -0
  112. package/src/components/Modal/Modal.stories.tsx +721 -0
  113. package/src/components/Modal/Modal.tsx +355 -0
  114. package/src/components/Modal/ModalBody.tsx +16 -0
  115. package/src/components/Modal/ModalFooter.tsx +71 -0
  116. package/src/components/Modal/ModalHeader.tsx +18 -0
  117. package/src/components/Modal/index.ts +6 -0
  118. package/src/components/PageLoader/PageLoader.stories.tsx +217 -0
  119. package/src/components/PageLoader/PageLoader.tsx +96 -0
  120. package/src/components/PageLoader/index.ts +2 -0
  121. package/src/components/ScrollTopButton/ScrollTopButton.stories.tsx +158 -0
  122. package/src/components/ScrollTopButton/ScrollTopButton.tsx +135 -0
  123. package/src/components/ScrollTopButton/index.ts +8 -0
  124. package/src/components/ScrollTopButton/scrollToTop.ts +37 -0
  125. package/src/components/Select/Select.definitions.ts +602 -0
  126. package/src/components/Select/Select.helpers.ts +71 -0
  127. package/src/components/Select/Select.stories.tsx +687 -0
  128. package/src/components/Select/Select.sx.ts +14 -0
  129. package/src/components/Select/Select.tsx +429 -0
  130. package/src/components/Select/Select.types.ts +15 -0
  131. package/src/components/Select/_parts/SelectMenuItem.tsx +40 -0
  132. package/src/components/Select/_parts/SelectSearchHeader.tsx +51 -0
  133. package/src/components/Select/_parts/SelectValue.tsx +96 -0
  134. package/src/components/Select/index.ts +14 -0
  135. package/src/components/Stat/Stat.stories.tsx +85 -0
  136. package/src/components/Stat/Stat.tsx +117 -0
  137. package/src/components/Stat/index.ts +2 -0
  138. package/src/components/StatusMessage/StatusMessage.stories.tsx +130 -0
  139. package/src/components/StatusMessage/StatusMessage.tsx +162 -0
  140. package/src/components/StatusMessage/index.ts +2 -0
  141. package/src/components/Stepper/Step.tsx +21 -0
  142. package/src/components/Stepper/Stepper.definition.ts +75 -0
  143. package/src/components/Stepper/Stepper.stories.tsx +122 -0
  144. package/src/components/Stepper/Stepper.tsx +75 -0
  145. package/src/components/Stepper/index.ts +2 -0
  146. package/src/components/Table/EmptyTable.png +0 -0
  147. package/src/components/Table/Table.definition.ts +580 -0
  148. package/src/components/Table/Table.stories.tsx +853 -0
  149. package/src/components/Table/Table.tsx +495 -0
  150. package/src/components/Table/data.ts +134 -0
  151. package/src/components/Table/exportsUtils.ts +195 -0
  152. package/src/components/Table/index.ts +3 -0
  153. package/src/components/Table/types.ts +34 -0
  154. package/src/components/Tabs/Tab.definition.ts +53 -0
  155. package/src/components/Tabs/Tab.tsx +19 -0
  156. package/src/components/Tabs/Tabs.stories.tsx +118 -0
  157. package/src/components/Tabs/Tabs.tsx +99 -0
  158. package/src/components/Tabs/_tabUtils.tsx +4 -0
  159. package/src/components/Tabs/index.ts +2 -0
  160. package/src/components/Timeline/Timeline.definition.ts +43 -0
  161. package/src/components/Timeline/Timeline.stories.tsx +108 -0
  162. package/src/components/Timeline/Timeline.tsx +49 -0
  163. package/src/components/Timeline/TimelineItem.tsx +31 -0
  164. package/src/components/Timeline/index.ts +2 -0
  165. package/src/components/Tooltip/Tooltip.stories.tsx +129 -0
  166. package/src/components/Tooltip/Tooltip.tsx +58 -0
  167. package/src/components/Tooltip/index.ts +1 -0
  168. package/src/components/_shared/formField.sx.ts +118 -0
  169. package/src/components/_shared/resolvePreset.ts +35 -0
  170. package/src/hooks/ClipBoard/ClipBoard.stories.tsx +168 -0
  171. package/src/hooks/ClipBoard/ClipBoard.tsx +131 -0
  172. package/src/hooks/ClipBoard/ClipboardUnifiedDemo.tsx +111 -0
  173. package/src/hooks/ClipBoard/index.ts +1 -0
  174. package/src/hooks/Wizard/Wizard.stories.tsx +301 -0
  175. package/src/hooks/Wizard/WizardContext.tsx +166 -0
  176. package/src/hooks/Wizard/index.ts +6 -0
  177. package/src/hooks/Wizard/useWizard.ts +13 -0
  178. package/src/index.ts +17 -0
  179. package/src/mui.ts +54 -0
  180. package/src/styles.css +3 -0
  181. package/src/theme/componentStyles.ts +47 -0
  182. package/src/theme/tokens.ts +43 -0
  183. package/tailwind.config.js +10 -0
  184. package/tsconfig.json +48 -0
  185. package/tsup.config.js +41 -0
  186. package/vite.config.js +132 -0
  187. package/vitest.config.ts +35 -0
@@ -0,0 +1,748 @@
1
+ import { useState, useEffect, useMemo } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Box, Typography, Avatar, Stack, Button } from "@mui/material";
4
+ import { useForm } from "react-hook-form";
5
+
6
+ import Autocomplete, { SelectOption } from "./Autocomplete";
7
+ import {
8
+ CustomChipRenderAutocompleteDefinition,
9
+ CustomRenderOptionAutocompleteDefinition,
10
+ EmptyOptionsAutocompleteDefinition,
11
+ LoadingAutocompleteDefinition,
12
+ MultipleAutocompleteDefinition,
13
+ MultipleWithLimitAutocompleteDefinition,
14
+ SimpleAutocompleteDefinition,
15
+ WithPlaceholderAutocompleteDefinition,
16
+ WithErrorAutocompleteDefinition,
17
+ LabelPositionAutocompleteDefinition,
18
+ CustomBorderRadiusAutocompleteDefinition,
19
+ EmptyWithPlaceholderAutocompleteDefinition,
20
+ RHFAutocompleteDefinition,
21
+ AsyncServiceAutocompleteDefinition,
22
+ } from "./Autocomplete.definitions";
23
+
24
+ // =============================================================================
25
+ // Datos de ejemplo
26
+ // =============================================================================
27
+ const basicOptions: SelectOption[] = [
28
+ { value: "10", label: "10" },
29
+ { value: "25", label: "25" },
30
+ { value: "50", label: "50" },
31
+ { value: "100", label: "100" },
32
+ ];
33
+
34
+ const userOptions: SelectOption[] = [
35
+ {
36
+ value: "admin",
37
+ label: "Administrador",
38
+ img: "https://placehold.co/40x40?text=A",
39
+ },
40
+ { value: "user", label: "Usuario", img: "https://placehold.co/40x40?text=U" },
41
+ {
42
+ value: "moderator",
43
+ label: "Moderador",
44
+ img: "https://placehold.co/40x40?text=M",
45
+ },
46
+ ];
47
+
48
+ const manyOptions = Array.from({ length: 50 }, (_, i) => ({
49
+ value: `option-${i}`,
50
+ label: `Opción ${i + 1}`,
51
+ }));
52
+
53
+ // =============================================================================
54
+ // META
55
+ // =============================================================================
56
+ const meta: Meta<typeof Autocomplete> = {
57
+ title: "Components/Autocomplete",
58
+ component: Autocomplete,
59
+ tags: ["autodocs"],
60
+ parameters: {
61
+ layout: "padded",
62
+ docs: {
63
+ description: {
64
+ component:
65
+ "Autocomplete personalizado basado en MUI con soporte para selección múltiple, chips, render custom y async.",
66
+ },
67
+ },
68
+ },
69
+ argTypes: {
70
+ label: {
71
+ control: "text",
72
+ description: "Etiqueta para el campo de selección.",
73
+ },
74
+ options: {
75
+ control: "object",
76
+ description:
77
+ "Array de objetos `SelectOption` para las opciones del menú.",
78
+ },
79
+ value: {
80
+ control: "object",
81
+ description: "Valor(es) seleccionado(s) del select.",
82
+ },
83
+ onChange: {
84
+ action: "changed",
85
+ description:
86
+ "Función de callback que se llama cuando el valor del select cambia.",
87
+ },
88
+ multiple: {
89
+ control: "boolean",
90
+ description:
91
+ "Si es verdadero, permite la selección de múltiples opciones.",
92
+ },
93
+ placeholder: {
94
+ control: "text",
95
+ description: "Texto que se muestra cuando no hay nada seleccionado.",
96
+ },
97
+ maxChipsToShow: {
98
+ control: "number",
99
+ description:
100
+ "Número máximo de chips a mostrar en selección múltiple antes de agrupar.",
101
+ if: { arg: "multiple", eq: true },
102
+ },
103
+ renderChipLabel: {
104
+ control: false,
105
+ description:
106
+ "Función para personalizar el contenido del label de cada chip seleccionado (para múltiple) o del valor mostrado (para individual).",
107
+ },
108
+ },
109
+ };
110
+
111
+ export default meta;
112
+ type Story = StoryObj<typeof Autocomplete>;
113
+
114
+ // =============================================================================
115
+ // STORIES
116
+ // =============================================================================
117
+
118
+ export const Simple: Story = {
119
+ render: () => {
120
+ const [value, setValue] = useState<string>("25");
121
+
122
+ return (
123
+ <Box sx={{ width: 250 }}>
124
+ <Autocomplete
125
+ label="Registros por página"
126
+ options={basicOptions}
127
+ value={value}
128
+ onChange={(val) => {
129
+ setValue(val as string);
130
+ }}
131
+ />
132
+ <Typography sx={{ mt: 2 }}>Valor: {value}</Typography>
133
+ </Box>
134
+ );
135
+ },
136
+ parameters: {
137
+ docs: {
138
+ description: {
139
+ story: "Autocomplete simple con opciones básicas y valor por defecto.",
140
+ },
141
+ source: { code: SimpleAutocompleteDefinition.trim() },
142
+ },
143
+ },
144
+ };
145
+
146
+ export const WithPlaceholder: Story = {
147
+ render: () => {
148
+ const [value, setValue] = useState<string>("");
149
+
150
+ return (
151
+ <Box sx={{ width: 300 }}>
152
+ <Autocomplete
153
+ label="Seleccione una opción"
154
+ options={basicOptions}
155
+ value={value}
156
+ onChange={(val) => {
157
+ setValue(val as string);
158
+ }}
159
+ placeholder="Ninguna opción seleccionada"
160
+ />
161
+ <Typography sx={{ mt: 2 }}>{value || "Ninguno"}</Typography>
162
+ </Box>
163
+ );
164
+ },
165
+ parameters: {
166
+ docs: {
167
+ description: {
168
+ story:
169
+ "Autocomplete sin valor inicial que muestra un placeholder cuando no hay selección.",
170
+ },
171
+ source: { code: WithPlaceholderAutocompleteDefinition.trim() },
172
+ },
173
+ },
174
+ };
175
+
176
+ export const Multiple: Story = {
177
+ render: () => {
178
+ const [value, setValue] = useState<string[]>([]);
179
+
180
+ return (
181
+ <Box sx={{ width: 400 }}>
182
+ <Autocomplete
183
+ label="Seleccionar valores"
184
+ multiple
185
+ options={basicOptions}
186
+ value={value}
187
+ onChange={(val) => {
188
+ setValue(val as string[]);
189
+ }}
190
+ />
191
+ <Typography sx={{ mt: 2 }}>
192
+ Valor seleccionado: {JSON.stringify(value)}
193
+ </Typography>
194
+ </Box>
195
+ );
196
+ },
197
+ parameters: {
198
+ docs: {
199
+ description: {
200
+ story:
201
+ "Autocomplete en modo múltiple que permite seleccionar varias opciones y las muestra como chips.",
202
+ },
203
+ source: { code: MultipleAutocompleteDefinition.trim() },
204
+ },
205
+ },
206
+ };
207
+
208
+ export const MultipleWithLimit: Story = {
209
+ render: () => {
210
+ const [value, setValue] = useState<string[]>(
211
+ manyOptions.slice(0, 5).map((opt) => opt.value),
212
+ );
213
+
214
+ return (
215
+ <Box sx={{ width: 400 }}>
216
+ <Autocomplete
217
+ label="Muchas selecciones"
218
+ multiple
219
+ maxChipsToShow={2}
220
+ options={manyOptions}
221
+ value={value}
222
+ onChange={(val) => {
223
+ setValue(val as string[]);
224
+ }}
225
+ />
226
+ </Box>
227
+ );
228
+ },
229
+ parameters: {
230
+ docs: {
231
+ description: {
232
+ story:
233
+ "Autocomplete múltiple con límite de chips visibles, agrupando el resto en un contador.",
234
+ },
235
+ source: { code: MultipleWithLimitAutocompleteDefinition.trim() },
236
+ },
237
+ },
238
+ };
239
+
240
+ export const CustomRenderOption: Story = {
241
+ render: () => {
242
+ const [value, setValue] = useState<string[]>([]);
243
+
244
+ return (
245
+ <Box sx={{ width: 300 }}>
246
+ <Autocomplete
247
+ label="Usuarios"
248
+ multiple
249
+ options={userOptions}
250
+ value={value}
251
+ onChange={(val) => {
252
+ setValue(val as string[]);
253
+ }}
254
+ renderOptionItem={(item) => (
255
+ <Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
256
+ <Avatar src={item.img} sx={{ width: 24, height: 24 }} />
257
+ {item.label}
258
+ </Box>
259
+ )}
260
+ />
261
+ </Box>
262
+ );
263
+ },
264
+ parameters: {
265
+ docs: {
266
+ description: {
267
+ story:
268
+ "Autocomplete con renderizado personalizado de opciones, incluyendo avatar e información adicional.",
269
+ },
270
+ source: { code: CustomRenderOptionAutocompleteDefinition.trim() },
271
+ },
272
+ },
273
+ };
274
+
275
+ export const CustomChipRender: Story = {
276
+ render: () => {
277
+ const [value, setValue] = useState<string[]>([]);
278
+
279
+ return (
280
+ <Box sx={{ width: 400 }}>
281
+ <Autocomplete
282
+ label="Usuarios seleccionados"
283
+ multiple
284
+ options={userOptions}
285
+ value={value}
286
+ onChange={(val) => {
287
+ setValue(val as string[]);
288
+ }}
289
+ maxChipsToShow={2}
290
+ renderChipLabel={(item) => (
291
+ <Typography variant="caption" fontWeight="bold">
292
+ {item.label.charAt(0)}
293
+ </Typography>
294
+ )}
295
+ />
296
+ </Box>
297
+ );
298
+ },
299
+ parameters: {
300
+ docs: {
301
+ description: {
302
+ story:
303
+ "Autocomplete múltiple con renderizado personalizado de chips, mostrando solo la inicial del label.",
304
+ },
305
+ source: { code: CustomChipRenderAutocompleteDefinition.trim() },
306
+ },
307
+ },
308
+ };
309
+
310
+ export const Loading: Story = {
311
+ render: () => {
312
+ const [value, setValue] = useState<string>("");
313
+
314
+ return (
315
+ <Box sx={{ width: 300 }}>
316
+ <Autocomplete
317
+ label="Cargando..."
318
+ options={[]}
319
+ loading
320
+ value={value}
321
+ onChange={(val) => {
322
+ setValue(val as string);
323
+ }}
324
+ />
325
+ </Box>
326
+ );
327
+ },
328
+ parameters: {
329
+ docs: {
330
+ description: {
331
+ story:
332
+ "Autocomplete en estado de carga, útil para integraciones con APIs o búsquedas asíncronas.",
333
+ },
334
+ source: { code: LoadingAutocompleteDefinition.trim() },
335
+ },
336
+ },
337
+ };
338
+
339
+ export const EmptyOptions: Story = {
340
+ render: () => {
341
+ const [value, setValue] = useState<string>("");
342
+
343
+ return (
344
+ <Box sx={{ width: 300 }}>
345
+ <Autocomplete
346
+ label="Sin opciones"
347
+ options={[]}
348
+ value={value}
349
+ onChange={(val) => {
350
+ setValue(val as string);
351
+ }}
352
+ placeholder="No hay opciones"
353
+ />
354
+ </Box>
355
+ );
356
+ },
357
+ parameters: {
358
+ docs: {
359
+ description: {
360
+ story:
361
+ "Autocomplete sin opciones disponibles, mostrando mensaje vacío al usuario.",
362
+ },
363
+ source: { code: EmptyOptionsAutocompleteDefinition.trim() },
364
+ },
365
+ },
366
+ };
367
+
368
+ export const ManyOptions: Story = {
369
+ render: () => {
370
+ const [value, setValue] = useState<string>("");
371
+
372
+ return (
373
+ <Box sx={{ width: 300 }}>
374
+ <Autocomplete
375
+ label="Muchas opciones"
376
+ options={manyOptions}
377
+ value={value}
378
+ onChange={(val) => {
379
+ setValue(val as string);
380
+ }}
381
+ />
382
+ </Box>
383
+ );
384
+ },
385
+ parameters: {
386
+ docs: {
387
+ description: {
388
+ story:
389
+ "Autocomplete sin opciones disponibles, mostrando mensaje vacío al usuario.",
390
+ },
391
+ source: { code: EmptyOptionsAutocompleteDefinition.trim() },
392
+ },
393
+ },
394
+ };
395
+
396
+ // =============================================================================
397
+ // NUEVAS STORIES — API refactorizada
398
+ // =============================================================================
399
+
400
+ export const WithError: Story = {
401
+ render: () => {
402
+ const [value, setValue] = useState<string | null>(null);
403
+ return (
404
+ <Box sx={{ width: 300 }}>
405
+ <Autocomplete
406
+ label="Cantidad"
407
+ options={basicOptions}
408
+ value={value}
409
+ onChange={(val) => setValue(val as string | null)}
410
+ error={!value}
411
+ helperText={!value ? "Debes seleccionar una cantidad" : " "}
412
+ />
413
+ </Box>
414
+ );
415
+ },
416
+ parameters: {
417
+ docs: {
418
+ description: {
419
+ story:
420
+ "Autocomplete en estado de error con `helperText`. Muestra la validación visual (borde + label + texto en rojo).",
421
+ },
422
+ source: { code: WithErrorAutocompleteDefinition.trim() },
423
+ },
424
+ },
425
+ };
426
+
427
+ export const LabelPosition: Story = {
428
+ render: () => {
429
+ const [a, setA] = useState<string | null>(null);
430
+ const [b, setB] = useState<string | null>(null);
431
+ return (
432
+ <Stack spacing={3} sx={{ width: 320 }}>
433
+ <Autocomplete
434
+ label="Outside (default)"
435
+ labelPosition="outside"
436
+ options={basicOptions}
437
+ value={a}
438
+ onChange={(val) => setA(val as string | null)}
439
+ />
440
+ <Autocomplete
441
+ label="Floating"
442
+ labelPosition="floating"
443
+ options={basicOptions}
444
+ value={b}
445
+ onChange={(val) => setB(val as string | null)}
446
+ />
447
+ </Stack>
448
+ );
449
+ },
450
+ parameters: {
451
+ docs: {
452
+ description: {
453
+ story:
454
+ "Comparación entre `labelPosition='outside'` (label arriba del input, consistente con Input/Select/DatePicker) y `labelPosition='floating'` (comportamiento nativo MUI dentro del notch).",
455
+ },
456
+ source: { code: LabelPositionAutocompleteDefinition.trim() },
457
+ },
458
+ },
459
+ };
460
+
461
+ export const CustomBorderRadius: Story = {
462
+ render: () => {
463
+ const [a, setA] = useState<string | null>(null);
464
+ const [b, setB] = useState<string | null>(null);
465
+ const [c, setC] = useState<string | null>(null);
466
+ return (
467
+ <Stack spacing={2} sx={{ width: 300 }}>
468
+ <Autocomplete
469
+ label="0"
470
+ borderRadius={0}
471
+ options={basicOptions}
472
+ value={a}
473
+ onChange={(v) => setA(v as string | null)}
474
+ />
475
+ <Autocomplete
476
+ label="10 (default)"
477
+ borderRadius={10}
478
+ options={basicOptions}
479
+ value={b}
480
+ onChange={(v) => setB(v as string | null)}
481
+ />
482
+ <Autocomplete
483
+ label="24 (pill)"
484
+ borderRadius={24}
485
+ options={basicOptions}
486
+ value={c}
487
+ onChange={(v) => setC(v as string | null)}
488
+ />
489
+ </Stack>
490
+ );
491
+ },
492
+ parameters: {
493
+ docs: {
494
+ description: {
495
+ story:
496
+ "La prop `borderRadius` permite personalizar el radio del borde sin usar `sx` (acepta number en px o string).",
497
+ },
498
+ source: { code: CustomBorderRadiusAutocompleteDefinition.trim() },
499
+ },
500
+ },
501
+ };
502
+
503
+ export const EmptyWithPlaceholder: Story = {
504
+ render: () => {
505
+ const [value, setValue] = useState<string | null>(null);
506
+ const countries: SelectOption[] = [
507
+ { value: "mx", label: "México" },
508
+ { value: "co", label: "Colombia" },
509
+ { value: "ar", label: "Argentina" },
510
+ ];
511
+ return (
512
+ <Box sx={{ width: 300 }}>
513
+ <Autocomplete
514
+ label="País"
515
+ placeholder="Buscar país..."
516
+ options={countries}
517
+ value={value}
518
+ onChange={(val) => setValue(val as string | null)}
519
+ />
520
+ <Typography sx={{ mt: 2 }} variant="caption" color="text.secondary">
521
+ Al estar vacío y sin foco, solo se ve el label como placeholder. Al
522
+ enfocar, el label sube y aparece el placeholder real.
523
+ </Typography>
524
+ </Box>
525
+ );
526
+ },
527
+ parameters: {
528
+ docs: {
529
+ description: {
530
+ story:
531
+ "Autocomplete vacío con placeholder. El placeholder solo se muestra cuando el campo está enfocado (mismo patrón que Select/Input).",
532
+ },
533
+ source: { code: EmptyWithPlaceholderAutocompleteDefinition.trim() },
534
+ },
535
+ },
536
+ };
537
+
538
+ export const WithReactHookForm: Story = {
539
+ render: () => {
540
+ type FormValues = { country: string | null };
541
+ const { control, handleSubmit, watch } = useForm<FormValues>({
542
+ defaultValues: { country: null },
543
+ });
544
+ const countries: SelectOption[] = [
545
+ { value: "mx", label: "México" },
546
+ { value: "co", label: "Colombia" },
547
+ { value: "ar", label: "Argentina" },
548
+ ];
549
+
550
+ const [submitted, setSubmitted] = useState<FormValues | null>(null);
551
+
552
+ return (
553
+ <Box
554
+ component="form"
555
+ onSubmit={handleSubmit((data) => setSubmitted(data))}
556
+ >
557
+ <Stack spacing={2} sx={{ width: 320 }}>
558
+ <Autocomplete
559
+ name="country"
560
+ control={control}
561
+ validation={{ required: "Campo requerido" }}
562
+ label="País"
563
+ placeholder="Seleccione..."
564
+ options={countries}
565
+ />
566
+ <Button type="submit" variant="contained">
567
+ Guardar
568
+ </Button>
569
+ <Typography variant="caption" color="text.secondary">
570
+ Valor actual (watch): {JSON.stringify(watch("country"))}
571
+ </Typography>
572
+ {submitted && (
573
+ <Typography variant="caption" color="success.main">
574
+ Submit: {JSON.stringify(submitted)}
575
+ </Typography>
576
+ )}
577
+ </Stack>
578
+ </Box>
579
+ );
580
+ },
581
+ parameters: {
582
+ docs: {
583
+ description: {
584
+ story:
585
+ "Integración con React Hook Form usando `name`/`control`/`validation`. El error de validación se renderiza automáticamente como helperText.",
586
+ },
587
+ source: { code: RHFAutocompleteDefinition.trim() },
588
+ },
589
+ },
590
+ };
591
+
592
+ // =============================================================================
593
+ // ASYNC — búsqueda remota contra un servicio
594
+ // =============================================================================
595
+ // Simulador de servicio: hace una "llamada" a una API fake con latencia.
596
+ const mockUsersDb: SelectOption[] = [
597
+ { value: 1, label: "Andrea García" },
598
+ { value: 2, label: "Andrés Pérez" },
599
+ { value: 3, label: "Beatriz López" },
600
+ { value: 4, label: "Carlos Ruiz" },
601
+ { value: 5, label: "Camila Torres" },
602
+ { value: 6, label: "Diego Fernández" },
603
+ { value: 7, label: "Elena Morales" },
604
+ { value: 8, label: "Fabián Núñez" },
605
+ ];
606
+
607
+ const fakeFetchUsers = (query: string): Promise<SelectOption[]> =>
608
+ new Promise((resolve) => {
609
+ setTimeout(() => {
610
+ if (!query) return resolve([]);
611
+ const q = query.toLowerCase();
612
+ resolve(mockUsersDb.filter((u) => u.label.toLowerCase().includes(q)));
613
+ }, 400);
614
+ });
615
+
616
+ // Debounce casero (para no agregar lodash como dep de la story).
617
+ function debounce<T extends (...args: any[]) => void>(fn: T, ms: number) {
618
+ let t: ReturnType<typeof setTimeout>;
619
+ return (...args: Parameters<T>) => {
620
+ clearTimeout(t);
621
+ t = setTimeout(() => fn(...args), ms);
622
+ };
623
+ }
624
+
625
+ export const AsyncService: Story = {
626
+ render: () => {
627
+ const [value, setValue] = useState<number | null>(null);
628
+ const [input, setInput] = useState("");
629
+ const [options, setOptions] = useState<SelectOption[]>([]);
630
+ const [loading, setLoading] = useState(false);
631
+
632
+ const search = useMemo(
633
+ () =>
634
+ debounce(async (q: string) => {
635
+ if (!q) {
636
+ setOptions([]);
637
+ return;
638
+ }
639
+ setLoading(true);
640
+ try {
641
+ const res = await fakeFetchUsers(q);
642
+ setOptions(res);
643
+ } finally {
644
+ setLoading(false);
645
+ }
646
+ }, 300),
647
+ [],
648
+ );
649
+
650
+ useEffect(() => {
651
+ search(input);
652
+ }, [input, search]);
653
+
654
+ return (
655
+ <Box sx={{ width: 320 }}>
656
+ <Autocomplete<number>
657
+ label="Usuario"
658
+ placeholder="Buscar usuario..."
659
+ options={options}
660
+ value={value}
661
+ onChange={(v) => setValue(v as number | null)}
662
+ inputValue={input}
663
+ onInputChange={(_, v) => setInput(v)}
664
+ loading={loading}
665
+ // Desactiva filtro cliente: confiamos en el servicio.
666
+ filterOptions={(x) => x}
667
+ />
668
+ <Typography sx={{ mt: 2 }} variant="caption" color="text.secondary">
669
+ {"Escribe para buscar. El servicio se consulta con debounce (300ms). filterOptions={(x) => x} desactiva el filtro cliente."}
670
+ </Typography>
671
+ </Box>
672
+ );
673
+ },
674
+ parameters: {
675
+ docs: {
676
+ description: {
677
+ story:
678
+ "Búsqueda asíncrona contra un servicio. El consumer maneja el estado de `options`/`loading` y reacciona a `onInputChange` para llamar al servicio. Usar `filterOptions={(x) => x}` para desactivar el filtro cliente.",
679
+ },
680
+ source: { code: AsyncServiceAutocompleteDefinition.trim() },
681
+ },
682
+ },
683
+ };
684
+
685
+ export const AsyncServiceMultiple: Story = {
686
+ render: () => {
687
+ const [value, setValue] = useState<number[]>([]);
688
+ const [input, setInput] = useState("");
689
+ const [options, setOptions] = useState<SelectOption[]>([]);
690
+ const [loading, setLoading] = useState(false);
691
+
692
+ const search = useMemo(
693
+ () =>
694
+ debounce(async (q: string) => {
695
+ if (!q) {
696
+ setOptions([]);
697
+ return;
698
+ }
699
+ setLoading(true);
700
+ try {
701
+ const res = await fakeFetchUsers(q);
702
+ setOptions(res);
703
+ } finally {
704
+ setLoading(false);
705
+ }
706
+ }, 300),
707
+ [],
708
+ );
709
+
710
+ useEffect(() => {
711
+ search(input);
712
+ }, [input, search]);
713
+
714
+ return (
715
+ <Box sx={{ width: 420 }}>
716
+ <Autocomplete<number>
717
+ multiple
718
+ maxChipsToShow={4}
719
+ label="Usuarios"
720
+ placeholder="Buscar y seleccionar varios..."
721
+ options={options}
722
+ value={value}
723
+ onChange={(v) => setValue(v as number[])}
724
+ inputValue={input}
725
+ onInputChange={(_, v) => setInput(v)}
726
+ loading={loading}
727
+ filterOptions={(x) => x}
728
+ />
729
+ <Typography sx={{ mt: 2 }} variant="caption" color="text.secondary">
730
+ Selecciona varios usuarios buscando con diferentes queries. Los chips
731
+ persisten aunque el search cambie las `options` (cache interno).
732
+ </Typography>
733
+ <Typography sx={{ mt: 1 }} variant="caption" display="block">
734
+ Seleccionados: {JSON.stringify(value)}
735
+ </Typography>
736
+ </Box>
737
+ );
738
+ },
739
+ parameters: {
740
+ docs: {
741
+ description: {
742
+ story:
743
+ "Búsqueda asíncrona **con selección múltiple**. El componente mantiene un cache interno de opciones ya vistas, así los chips de los items seleccionados NO desaparecen cuando el usuario busca otros términos. Flujo: buscar 'an' → seleccionar Andrea → buscar 'be' → seleccionar Beatriz → los dos chips siguen visibles.",
744
+ },
745
+ source: { code: AsyncServiceAutocompleteDefinition.trim() },
746
+ },
747
+ },
748
+ };