@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.
- package/.dockerignore +8 -0
- package/.github/workflows/publish.yml +107 -0
- package/.prettierrc +3 -0
- package/.storybook/main.ts +19 -0
- package/.storybook/preview.ts +14 -0
- package/.storybook/vitest.setup.ts +9 -0
- package/Dockerfile +37 -0
- package/build.js +102 -0
- package/chromatic.config.json +5 -0
- package/cleanDirectories.js +40 -0
- package/dist/README.md +243 -0
- package/dist/components/Icon/Icon.js +1 -1
- package/dist/components/Table/Table.js +1 -1
- package/dist/index.cjs +24 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -1
- package/dist/mui.d.ts +1 -0
- package/dist/package.json +197 -0
- package/package.json +4 -32
- package/rollup.config.cjs +87 -0
- package/src/components/ActionMenu/ActionMenu.stories.tsx +230 -0
- package/src/components/ActionMenu/ActionMenu.tsx +174 -0
- package/src/components/ActionMenu/index.ts +2 -0
- package/src/components/AppBar/AppBar.stories.tsx +272 -0
- package/src/components/AppBar/AppBar.sx.ts +32 -0
- package/src/components/AppBar/AppBar.tsx +123 -0
- package/src/components/AppBar/AppBarBrand.tsx +120 -0
- package/src/components/AppBar/AppBarContext.ts +25 -0
- package/src/components/AppBar/AppBarMenuToggle.tsx +90 -0
- package/src/components/AppBar/AppBarUserMenu.tsx +217 -0
- package/src/components/AppBar/index.ts +25 -0
- package/src/components/Autocomplete/Autocomplete.definitions.ts +477 -0
- package/src/components/Autocomplete/Autocomplete.helpers.ts +60 -0
- package/src/components/Autocomplete/Autocomplete.stories.tsx +748 -0
- package/src/components/Autocomplete/Autocomplete.sx.ts +30 -0
- package/src/components/Autocomplete/Autocomplete.tsx +361 -0
- package/src/components/Autocomplete/Autocomplete.types.ts +13 -0
- package/src/components/Autocomplete/_parts/AutocompleteChips.tsx +55 -0
- package/src/components/Autocomplete/_parts/AutocompleteLoader.tsx +17 -0
- package/src/components/Autocomplete/_parts/AutocompleteOption.tsx +31 -0
- package/src/components/Autocomplete/index.ts +12 -0
- package/src/components/Avatar/Avatar.definitions.ts +162 -0
- package/src/components/Avatar/Avatar.stories.tsx +258 -0
- package/src/components/Avatar/Avatar.tsx +206 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Button/Button.definition.ts +97 -0
- package/src/components/Button/Button.stories.tsx +285 -0
- package/src/components/Button/Button.tsx +67 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Card/Card.definition.ts +5 -0
- package/src/components/Card/Card.stories.tsx +221 -0
- package/src/components/Card/Card.sx.ts +104 -0
- package/src/components/Card/Card.tsx +200 -0
- package/src/components/Card/index.ts +9 -0
- package/src/components/Chip/Chip.definitions.ts +167 -0
- package/src/components/Chip/Chip.stories.tsx +265 -0
- package/src/components/Chip/Chip.tsx +61 -0
- package/src/components/Chip/index.ts +1 -0
- package/src/components/Column/Column.tsx +29 -0
- package/src/components/Column/index.ts +1 -0
- package/src/components/DatePicker/DatePicker.definitions.ts +228 -0
- package/src/components/DatePicker/DatePicker.helpers.ts +24 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +309 -0
- package/src/components/DatePicker/DatePicker.sx.ts +33 -0
- package/src/components/DatePicker/DatePicker.tsx +189 -0
- package/src/components/DatePicker/DatePicker.types.ts +10 -0
- package/src/components/DatePicker/index.ts +9 -0
- package/src/components/DateRangePicker/DateRangePicker.definitions.ts +191 -0
- package/src/components/DateRangePicker/DateRangePicker.stories.tsx +252 -0
- package/src/components/DateRangePicker/DateRangePicker.tsx +56 -0
- package/src/components/DateRangePicker/index.ts +1 -0
- package/src/components/DateTimePicker/DateTimePicker.definitions.ts +256 -0
- package/src/components/DateTimePicker/DateTimePicker.helpers.ts +38 -0
- package/src/components/DateTimePicker/DateTimePicker.stories.tsx +418 -0
- package/src/components/DateTimePicker/DateTimePicker.sx.ts +30 -0
- package/src/components/DateTimePicker/DateTimePicker.tsx +225 -0
- package/src/components/DateTimePicker/DateTimePicker.types.ts +10 -0
- package/src/components/DateTimePicker/index.ts +9 -0
- package/src/components/Drawer/Drawer.stories.tsx +270 -0
- package/src/components/Drawer/Drawer.sx.ts +106 -0
- package/src/components/Drawer/Drawer.tsx +214 -0
- package/src/components/Drawer/DrawerContext.ts +26 -0
- package/src/components/Drawer/DrawerItem.tsx +110 -0
- package/src/components/Drawer/index.ts +10 -0
- package/src/components/Flyout/Flyout.stories.tsx +282 -0
- package/src/components/Flyout/Flyout.tsx +122 -0
- package/src/components/Flyout/index.ts +1 -0
- package/src/components/Gallery/Gallery.definition.tsx +37 -0
- package/src/components/Gallery/Gallery.stories.tsx +82 -0
- package/src/components/Gallery/Gallery.tsx +118 -0
- package/src/components/Gallery/GalleryLightbox.tsx +170 -0
- package/src/components/Gallery/GalleryMain.tsx +84 -0
- package/src/components/Gallery/GalleryThumbnails.tsx +106 -0
- package/src/components/Gallery/index.ts +1 -0
- package/src/components/Icon/Icon.stories.tsx +121 -0
- package/src/components/Icon/Icon.tsx +175 -0
- package/src/components/Icon/index.ts +2 -0
- package/src/components/Input/Input.definitions.ts +324 -0
- package/src/components/Input/Input.helpers.ts +49 -0
- package/src/components/Input/Input.stories.tsx +499 -0
- package/src/components/Input/Input.sx.ts +42 -0
- package/src/components/Input/Input.tsx +141 -0
- package/src/components/Input/Input.types.ts +10 -0
- package/src/components/Input/index.ts +9 -0
- package/src/components/InputGroup/InputGroup.definitions.ts +158 -0
- package/src/components/InputGroup/InputGroup.stories.tsx +267 -0
- package/src/components/InputGroup/InputGroup.tsx +179 -0
- package/src/components/InputGroup/index.ts +1 -0
- package/src/components/MenuButton/MenuButton.stories.tsx +197 -0
- package/src/components/MenuButton/MenuButton.tsx +100 -0
- package/src/components/MenuButton/index.ts +1 -0
- package/src/components/Modal/Modal.stories.tsx +721 -0
- package/src/components/Modal/Modal.tsx +355 -0
- package/src/components/Modal/ModalBody.tsx +16 -0
- package/src/components/Modal/ModalFooter.tsx +71 -0
- package/src/components/Modal/ModalHeader.tsx +18 -0
- package/src/components/Modal/index.ts +6 -0
- package/src/components/PageLoader/PageLoader.stories.tsx +217 -0
- package/src/components/PageLoader/PageLoader.tsx +96 -0
- package/src/components/PageLoader/index.ts +2 -0
- package/src/components/ScrollTopButton/ScrollTopButton.stories.tsx +158 -0
- package/src/components/ScrollTopButton/ScrollTopButton.tsx +135 -0
- package/src/components/ScrollTopButton/index.ts +8 -0
- package/src/components/ScrollTopButton/scrollToTop.ts +37 -0
- package/src/components/Select/Select.definitions.ts +602 -0
- package/src/components/Select/Select.helpers.ts +71 -0
- package/src/components/Select/Select.stories.tsx +687 -0
- package/src/components/Select/Select.sx.ts +14 -0
- package/src/components/Select/Select.tsx +429 -0
- package/src/components/Select/Select.types.ts +15 -0
- package/src/components/Select/_parts/SelectMenuItem.tsx +40 -0
- package/src/components/Select/_parts/SelectSearchHeader.tsx +51 -0
- package/src/components/Select/_parts/SelectValue.tsx +96 -0
- package/src/components/Select/index.ts +14 -0
- package/src/components/Stat/Stat.stories.tsx +85 -0
- package/src/components/Stat/Stat.tsx +117 -0
- package/src/components/Stat/index.ts +2 -0
- package/src/components/StatusMessage/StatusMessage.stories.tsx +130 -0
- package/src/components/StatusMessage/StatusMessage.tsx +162 -0
- package/src/components/StatusMessage/index.ts +2 -0
- package/src/components/Stepper/Step.tsx +21 -0
- package/src/components/Stepper/Stepper.definition.ts +75 -0
- package/src/components/Stepper/Stepper.stories.tsx +122 -0
- package/src/components/Stepper/Stepper.tsx +75 -0
- package/src/components/Stepper/index.ts +2 -0
- package/src/components/Table/EmptyTable.png +0 -0
- package/src/components/Table/Table.definition.ts +580 -0
- package/src/components/Table/Table.stories.tsx +853 -0
- package/src/components/Table/Table.tsx +495 -0
- package/src/components/Table/data.ts +134 -0
- package/src/components/Table/exportsUtils.ts +195 -0
- package/src/components/Table/index.ts +3 -0
- package/src/components/Table/types.ts +34 -0
- package/src/components/Tabs/Tab.definition.ts +53 -0
- package/src/components/Tabs/Tab.tsx +19 -0
- package/src/components/Tabs/Tabs.stories.tsx +118 -0
- package/src/components/Tabs/Tabs.tsx +99 -0
- package/src/components/Tabs/_tabUtils.tsx +4 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Timeline/Timeline.definition.ts +43 -0
- package/src/components/Timeline/Timeline.stories.tsx +108 -0
- package/src/components/Timeline/Timeline.tsx +49 -0
- package/src/components/Timeline/TimelineItem.tsx +31 -0
- package/src/components/Timeline/index.ts +2 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +129 -0
- package/src/components/Tooltip/Tooltip.tsx +58 -0
- package/src/components/Tooltip/index.ts +1 -0
- package/src/components/_shared/formField.sx.ts +118 -0
- package/src/components/_shared/resolvePreset.ts +35 -0
- package/src/hooks/ClipBoard/ClipBoard.stories.tsx +168 -0
- package/src/hooks/ClipBoard/ClipBoard.tsx +131 -0
- package/src/hooks/ClipBoard/ClipboardUnifiedDemo.tsx +111 -0
- package/src/hooks/ClipBoard/index.ts +1 -0
- package/src/hooks/Wizard/Wizard.stories.tsx +301 -0
- package/src/hooks/Wizard/WizardContext.tsx +166 -0
- package/src/hooks/Wizard/index.ts +6 -0
- package/src/hooks/Wizard/useWizard.ts +13 -0
- package/src/index.ts +17 -0
- package/src/mui.ts +54 -0
- package/src/styles.css +3 -0
- package/src/theme/componentStyles.ts +47 -0
- package/src/theme/tokens.ts +43 -0
- package/tailwind.config.js +10 -0
- package/tsconfig.json +48 -0
- package/tsup.config.js +41 -0
- package/vite.config.js +132 -0
- 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
|
+
};
|