@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,499 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
Typography,
|
|
6
|
+
} from '@mui/material';
|
|
7
|
+
|
|
8
|
+
// Importa el componente Input (asumiendo que está en el mismo directorio o ruta definida)
|
|
9
|
+
import { Input } from './Input';
|
|
10
|
+
|
|
11
|
+
// Importa las definiciones de las historias
|
|
12
|
+
import {
|
|
13
|
+
BasicTextInputDefinition,
|
|
14
|
+
NumberInputDefinition,
|
|
15
|
+
EmailInputDefinition,
|
|
16
|
+
PasswordInputDefinition,
|
|
17
|
+
InputWithPlaceholderDefinition,
|
|
18
|
+
InputWithErrorDefinition,
|
|
19
|
+
DisabledInputDefinition,
|
|
20
|
+
ReadOnlyInputDefinition,
|
|
21
|
+
InputVariantsDefinition,
|
|
22
|
+
InputSizesDefinition,
|
|
23
|
+
FullWidthInputDefinition,
|
|
24
|
+
LabelPositionFloatingDefinition,
|
|
25
|
+
CustomBorderRadiusDefinition,
|
|
26
|
+
CustomStylingDefinition,
|
|
27
|
+
} from './Input.definitions';
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Definición de las meta-propiedades para Storybook
|
|
31
|
+
// =============================================================================
|
|
32
|
+
const meta: Meta<typeof Input> = {
|
|
33
|
+
title: 'Components/Input',
|
|
34
|
+
component: Input,
|
|
35
|
+
tags: ['autodocs'],
|
|
36
|
+
parameters: {
|
|
37
|
+
layout: 'centered',
|
|
38
|
+
docs: {
|
|
39
|
+
description: {
|
|
40
|
+
component: 'Un componente `Input` personalizado basado en `TextField` de Material UI, que soporta varios tipos de entrada HTML y propiedades de `TextField`.',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
argTypes: {
|
|
45
|
+
label: { control: 'text', description: 'Etiqueta flotante para el campo de entrada.' },
|
|
46
|
+
value: { control: 'text', description: 'El valor actual del campo de entrada.', type: { name: 'string', required: true } },
|
|
47
|
+
onChange: { action: 'valueChanged', description: 'Callback que se dispara cuando el valor del campo de entrada cambia.' },
|
|
48
|
+
type: {
|
|
49
|
+
control: 'select',
|
|
50
|
+
options: ['text', 'number', 'email', 'password', 'tel', 'url', 'search', 'date', 'datetime-local', 'month', 'week', 'time', 'color'],
|
|
51
|
+
description: 'El tipo HTML del campo de entrada (controla el teclado en móviles, etc.).',
|
|
52
|
+
defaultValue: 'text',
|
|
53
|
+
},
|
|
54
|
+
placeholder: { control: 'text', description: 'Texto de marcador de posición cuando el campo está vacío.' },
|
|
55
|
+
disabled: { control: 'boolean', description: 'Si es verdadero, el campo de entrada estará deshabilitado.' },
|
|
56
|
+
// readOnly: { control: 'boolean', description: 'Si es verdadero, el campo de entrada estará en modo de solo lectura.' },
|
|
57
|
+
error: { control: 'boolean', description: 'Si es verdadero, el campo de entrada estará en estado de error.' },
|
|
58
|
+
helperText: { control: 'text', description: 'Texto auxiliar que se muestra debajo del campo de entrada.' },
|
|
59
|
+
variant: {
|
|
60
|
+
control: 'radio',
|
|
61
|
+
options: ['outlined', 'filled', 'standard'],
|
|
62
|
+
description: 'La variante de diseño del campo de entrada.',
|
|
63
|
+
defaultValue: 'outlined',
|
|
64
|
+
},
|
|
65
|
+
size: {
|
|
66
|
+
control: 'radio',
|
|
67
|
+
options: ['small', 'medium'],
|
|
68
|
+
description: 'El tamaño del campo de entrada.',
|
|
69
|
+
defaultValue: 'small',
|
|
70
|
+
},
|
|
71
|
+
labelPosition: {
|
|
72
|
+
control: 'radio',
|
|
73
|
+
options: ['outside', 'floating'],
|
|
74
|
+
description: '"outside" (default): label arriba del input. "floating": label clásico de MUI dentro del notched outline.',
|
|
75
|
+
defaultValue: 'outside',
|
|
76
|
+
},
|
|
77
|
+
borderRadius: {
|
|
78
|
+
control: { type: 'number' },
|
|
79
|
+
description: 'Border radius del input en px (o string CSS). Default: 10.',
|
|
80
|
+
defaultValue: 10,
|
|
81
|
+
},
|
|
82
|
+
fullWidth: { control: 'boolean', description: 'Si es verdadero, el campo de entrada ocupará todo el ancho disponible.', defaultValue: false },
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default meta;
|
|
87
|
+
type Story = StoryObj<typeof Input>;
|
|
88
|
+
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// Historias
|
|
91
|
+
// =============================================================================
|
|
92
|
+
|
|
93
|
+
export const BasicTextInput: Story = {
|
|
94
|
+
render: () => {
|
|
95
|
+
const [value, setValue] = useState('Texto de ejemplo');
|
|
96
|
+
return (
|
|
97
|
+
<Box sx={{ width: 300 }}>
|
|
98
|
+
<Input
|
|
99
|
+
label="Nombre"
|
|
100
|
+
value={value}
|
|
101
|
+
onChange={(value) => setValue(value.toString())}
|
|
102
|
+
/>
|
|
103
|
+
<Typography sx={{ mt: 2 }}>Valor actual: {value}</Typography>
|
|
104
|
+
</Box>
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
parameters: {
|
|
108
|
+
docs: {
|
|
109
|
+
description: {
|
|
110
|
+
story: "Un campo de entrada de texto básico con una etiqueta."
|
|
111
|
+
},
|
|
112
|
+
source: { code: BasicTextInputDefinition.trim() }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const NumberInput: Story = {
|
|
118
|
+
render: () => {
|
|
119
|
+
const [value, setValue] = useState(123);
|
|
120
|
+
return (
|
|
121
|
+
<Box sx={{ width: 300 }}>
|
|
122
|
+
<Input
|
|
123
|
+
label="Cantidad"
|
|
124
|
+
type="number"
|
|
125
|
+
value={value.toString()}
|
|
126
|
+
onChange={(value) => setValue(Number(value))}
|
|
127
|
+
/>
|
|
128
|
+
<Typography sx={{ mt: 2 }}>Valor actual: {value}</Typography>
|
|
129
|
+
</Box>
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
parameters: {
|
|
133
|
+
docs: {
|
|
134
|
+
description: {
|
|
135
|
+
story: "Un campo de entrada numérico. Permite solo números y controla el teclado numérico en dispositivos móviles."
|
|
136
|
+
},
|
|
137
|
+
source: { code: NumberInputDefinition.trim() }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const EmailInput: Story = {
|
|
143
|
+
render: () => {
|
|
144
|
+
const [value, setValue] = useState('ejemplo@dominio.com');
|
|
145
|
+
return (
|
|
146
|
+
<Box sx={{ width: 300 }}>
|
|
147
|
+
<Input
|
|
148
|
+
label="Correo Electrónico"
|
|
149
|
+
type="email"
|
|
150
|
+
value={value.toString()}
|
|
151
|
+
onChange={(value) => setValue(value.toString())}
|
|
152
|
+
/>
|
|
153
|
+
<Typography sx={{ mt: 2 }}>Valor actual: {value}</Typography>
|
|
154
|
+
</Box>
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
parameters: {
|
|
158
|
+
docs: {
|
|
159
|
+
description: {
|
|
160
|
+
story: "Un campo de entrada para correo electrónico. Proporciona validación básica del navegador y sugiere correos electrónicos."
|
|
161
|
+
},
|
|
162
|
+
source: { code: EmailInputDefinition.trim() }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const PasswordInput: Story = {
|
|
168
|
+
render: () => {
|
|
169
|
+
const [value, setValue] = useState('micontraseña');
|
|
170
|
+
return (
|
|
171
|
+
<Box sx={{ width: 300 }}>
|
|
172
|
+
<Input
|
|
173
|
+
label="Contraseña"
|
|
174
|
+
type="password"
|
|
175
|
+
value={value.toString()}
|
|
176
|
+
onChange={(value) => setValue(value.toString())}
|
|
177
|
+
/>
|
|
178
|
+
<Typography sx={{ mt: 2 }}>Valor actual: {value}</Typography>
|
|
179
|
+
</Box>
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
parameters: {
|
|
183
|
+
docs: {
|
|
184
|
+
description: {
|
|
185
|
+
story: "Un campo de entrada de contraseña. Oculta los caracteres a medida que se escriben."
|
|
186
|
+
},
|
|
187
|
+
source: { code: PasswordInputDefinition.trim() }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const InputWithPlaceholder: Story = {
|
|
193
|
+
render: () => {
|
|
194
|
+
const [value, setValue] = useState('');
|
|
195
|
+
return (
|
|
196
|
+
<Box sx={{ width: 300 }}>
|
|
197
|
+
<Input
|
|
198
|
+
label="Búsqueda"
|
|
199
|
+
placeholder="Escribe tu término de búsqueda..."
|
|
200
|
+
value={value.toString()}
|
|
201
|
+
onChange={(value) => setValue(value.toString())}
|
|
202
|
+
/>
|
|
203
|
+
<Typography sx={{ mt: 2 }}>Valor actual: {value || 'Vacío'}</Typography>
|
|
204
|
+
</Box>
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
parameters: {
|
|
208
|
+
docs: {
|
|
209
|
+
description: {
|
|
210
|
+
story: "Un campo de entrada con un texto de marcador de posición visible cuando el campo está vacío."
|
|
211
|
+
},
|
|
212
|
+
source: { code: InputWithPlaceholderDefinition.trim() }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const InputWithError: Story = {
|
|
218
|
+
render: () => {
|
|
219
|
+
const [value, setValue] = useState('invalido');
|
|
220
|
+
// Lógica simple de error para la demostración
|
|
221
|
+
const hasError = value.length < 5 && value.length > 0;
|
|
222
|
+
return (
|
|
223
|
+
<Box sx={{ width: 300 }}>
|
|
224
|
+
<Input
|
|
225
|
+
label="Nombre de usuario"
|
|
226
|
+
value={value.toString()}
|
|
227
|
+
onChange={(value) => setValue(value.toString())}
|
|
228
|
+
error={hasError}
|
|
229
|
+
helperText={hasError ? 'Mínimo 5 caracteres' : ''}
|
|
230
|
+
/>
|
|
231
|
+
<Typography sx={{ mt: 2 }}>Valor actual: {value}</Typography>
|
|
232
|
+
</Box>
|
|
233
|
+
);
|
|
234
|
+
},
|
|
235
|
+
parameters: {
|
|
236
|
+
docs: {
|
|
237
|
+
description: {
|
|
238
|
+
story: "Un campo de entrada que muestra un estado de error y un texto de ayuda cuando la validación falla."
|
|
239
|
+
},
|
|
240
|
+
source: { code: InputWithErrorDefinition.trim() }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export const DisabledInput: Story = {
|
|
246
|
+
render: () => {
|
|
247
|
+
const [value, setValue] = useState('Campo deshabilitado');
|
|
248
|
+
return (
|
|
249
|
+
<Box sx={{ width: 300 }}>
|
|
250
|
+
<Input
|
|
251
|
+
label="Estado"
|
|
252
|
+
value={value.toString()}
|
|
253
|
+
onChange={(value) => setValue(value.toString())}
|
|
254
|
+
disabled
|
|
255
|
+
/>
|
|
256
|
+
<Typography sx={{ mt: 2 }}>Valor actual: {value}</Typography>
|
|
257
|
+
</Box>
|
|
258
|
+
);
|
|
259
|
+
},
|
|
260
|
+
parameters: {
|
|
261
|
+
docs: {
|
|
262
|
+
story: "Un campo de entrada deshabilitado, lo que impide la interacción del usuario."
|
|
263
|
+
},
|
|
264
|
+
source: { code: DisabledInputDefinition.trim() }
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export const ReadOnlyInput: Story = {
|
|
269
|
+
render: () => {
|
|
270
|
+
const [value, setValue] = useState('Valor de solo lectura');
|
|
271
|
+
return (
|
|
272
|
+
<Box sx={{ width: 300 }}>
|
|
273
|
+
<Input
|
|
274
|
+
label="Información"
|
|
275
|
+
value={value.toString()}
|
|
276
|
+
onChange={(value) => setValue(value.toString())}
|
|
277
|
+
readOnly
|
|
278
|
+
/>
|
|
279
|
+
<Typography sx={{ mt: 2 }}>Valor actual: {value}</Typography>
|
|
280
|
+
</Box>
|
|
281
|
+
);
|
|
282
|
+
},
|
|
283
|
+
parameters: {
|
|
284
|
+
docs: {
|
|
285
|
+
story: "Un campo de entrada de solo lectura, el usuario puede ver el valor pero no modificarlo."
|
|
286
|
+
},
|
|
287
|
+
source: { code: ReadOnlyInputDefinition.trim() }
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export const InputVariants: Story = {
|
|
292
|
+
render: () => {
|
|
293
|
+
const [valueOutlined, setValueOutlined] = useState('Outlined');
|
|
294
|
+
const [valueFilled, setValueFilled] = useState('Filled');
|
|
295
|
+
const [valueStandard, setValueStandard] = useState('Standard');
|
|
296
|
+
return (
|
|
297
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, width: 300 }}>
|
|
298
|
+
<Input
|
|
299
|
+
label="Esquema Outlined"
|
|
300
|
+
value={valueOutlined.toString()}
|
|
301
|
+
onChange={(value) => setValueOutlined(value.toString())}
|
|
302
|
+
variant="outlined"
|
|
303
|
+
/>
|
|
304
|
+
<Input
|
|
305
|
+
label="Esquema Filled"
|
|
306
|
+
value={valueFilled.toString()}
|
|
307
|
+
onChange={(value) => setValueFilled(value.toString())}
|
|
308
|
+
variant="filled"
|
|
309
|
+
/>
|
|
310
|
+
<Input
|
|
311
|
+
label="Esquema Standard"
|
|
312
|
+
value={valueStandard.toString()}
|
|
313
|
+
onChange={(value) => setValueStandard(value.toString())}
|
|
314
|
+
variant="standard"
|
|
315
|
+
/>
|
|
316
|
+
</Box>
|
|
317
|
+
);
|
|
318
|
+
},
|
|
319
|
+
parameters: {
|
|
320
|
+
docs: {
|
|
321
|
+
story: "Demuestra las diferentes variantes de diseño: `outlined`, `filled` y `standard`."
|
|
322
|
+
},
|
|
323
|
+
source: { code: InputVariantsDefinition.trim() }
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
export const InputSizes: Story = {
|
|
328
|
+
render: () => {
|
|
329
|
+
const [valueMedium, setValueMedium] = useState('Tamaño Mediano');
|
|
330
|
+
const [valueSmall, setValueSmall] = useState('Tamaño Pequeño');
|
|
331
|
+
return (
|
|
332
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, width: 300 }}>
|
|
333
|
+
<Input
|
|
334
|
+
label="Input Mediano"
|
|
335
|
+
value={valueMedium.toString()}
|
|
336
|
+
onChange={(value) => setValueMedium(value.toString())}
|
|
337
|
+
size="medium"
|
|
338
|
+
/>
|
|
339
|
+
<Input
|
|
340
|
+
label="Input Pequeño"
|
|
341
|
+
value={valueSmall.toString()}
|
|
342
|
+
onChange={(value) => setValueSmall(value.toString())}
|
|
343
|
+
size="small"
|
|
344
|
+
/>
|
|
345
|
+
</Box>
|
|
346
|
+
);
|
|
347
|
+
},
|
|
348
|
+
parameters: {
|
|
349
|
+
docs: {
|
|
350
|
+
story: "Demuestra los diferentes tamaños del campo de entrada: `medium` (por defecto) y `small`."
|
|
351
|
+
},
|
|
352
|
+
source: { code: InputSizesDefinition.trim() }
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export const FullWidthInput: Story = {
|
|
357
|
+
render: () => {
|
|
358
|
+
const [value, setValue] = useState('Ocupa todo el ancho');
|
|
359
|
+
return (
|
|
360
|
+
<Box sx={{ width: '100%' }}>
|
|
361
|
+
<Input
|
|
362
|
+
label="Input de Ancho Completo"
|
|
363
|
+
value={value.toString()}
|
|
364
|
+
onChange={(value) => setValue(value.toString())}
|
|
365
|
+
fullWidth
|
|
366
|
+
/>
|
|
367
|
+
<Typography sx={{ mt: 2 }}>Valor actual: {value}</Typography>
|
|
368
|
+
</Box>
|
|
369
|
+
);
|
|
370
|
+
},
|
|
371
|
+
parameters: {
|
|
372
|
+
docs: {
|
|
373
|
+
story: "Un campo de entrada que ocupa todo el ancho disponible de su contenedor."
|
|
374
|
+
},
|
|
375
|
+
source: { code: FullWidthInputDefinition.trim() }
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
export const StandardVariant: Story = {
|
|
380
|
+
render: () => {
|
|
381
|
+
const [value, setValue] = useState('Ocupa todo el ancho');
|
|
382
|
+
return (
|
|
383
|
+
<Box sx={{ width: '100%' }}>
|
|
384
|
+
<Input
|
|
385
|
+
|
|
386
|
+
value={value.toString()}
|
|
387
|
+
variant='standard'
|
|
388
|
+
onChange={(value) => setValue(value.toString())}
|
|
389
|
+
fullWidth
|
|
390
|
+
/>
|
|
391
|
+
<Typography sx={{ mt: 2 }}>Valor actual: {value}</Typography>
|
|
392
|
+
</Box>
|
|
393
|
+
);
|
|
394
|
+
},
|
|
395
|
+
parameters: {
|
|
396
|
+
docs: {
|
|
397
|
+
story: "Un campo de entrada que ocupa todo el ancho disponible de su contenedor."
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
export const LabelPositionFloating: Story = {
|
|
404
|
+
render: () => {
|
|
405
|
+
const [valueOutside, setValueOutside] = useState('Outside (default)');
|
|
406
|
+
const [valueFloating, setValueFloating] = useState('Floating');
|
|
407
|
+
return (
|
|
408
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, width: 300 }}>
|
|
409
|
+
<Input
|
|
410
|
+
label="Label outside"
|
|
411
|
+
value={valueOutside.toString()}
|
|
412
|
+
onChange={(value) => setValueOutside(value.toString())}
|
|
413
|
+
labelPosition="outside"
|
|
414
|
+
/>
|
|
415
|
+
<Input
|
|
416
|
+
label="Label floating (MUI clásico)"
|
|
417
|
+
value={valueFloating.toString()}
|
|
418
|
+
onChange={(value) => setValueFloating(value.toString())}
|
|
419
|
+
labelPosition="floating"
|
|
420
|
+
/>
|
|
421
|
+
</Box>
|
|
422
|
+
);
|
|
423
|
+
},
|
|
424
|
+
parameters: {
|
|
425
|
+
docs: {
|
|
426
|
+
description: {
|
|
427
|
+
story: "Compara `labelPosition='outside'` (default, label arriba del input) vs `labelPosition='floating'` (label flotante clásico de MUI dentro del notched outline)."
|
|
428
|
+
},
|
|
429
|
+
source: { code: LabelPositionFloatingDefinition.trim() }
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
export const CustomBorderRadius: Story = {
|
|
435
|
+
render: () => {
|
|
436
|
+
const [v0, setV0] = useState('Sin radius');
|
|
437
|
+
const [v4, setV4] = useState('Radius 4px');
|
|
438
|
+
const [v10, setV10] = useState('Radius 10px (default)');
|
|
439
|
+
const [v24, setV24] = useState('Radius 24px (pill)');
|
|
440
|
+
return (
|
|
441
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, width: 300 }}>
|
|
442
|
+
<Input label="borderRadius={0}" value={v0} onChange={(v) => setV0(v.toString())} borderRadius={0} />
|
|
443
|
+
<Input label="borderRadius={4}" value={v4} onChange={(v) => setV4(v.toString())} borderRadius={4} />
|
|
444
|
+
<Input label="borderRadius={10} (default)" value={v10} onChange={(v) => setV10(v.toString())} />
|
|
445
|
+
<Input label="borderRadius={24}" value={v24} onChange={(v) => setV24(v.toString())} borderRadius={24} />
|
|
446
|
+
</Box>
|
|
447
|
+
);
|
|
448
|
+
},
|
|
449
|
+
parameters: {
|
|
450
|
+
docs: {
|
|
451
|
+
description: {
|
|
452
|
+
story: "El prop `borderRadius` acepta un número (px) o un string CSS. Override directo del default `10`."
|
|
453
|
+
},
|
|
454
|
+
source: { code: CustomBorderRadiusDefinition.trim() }
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
export const CustomStyling: Story = {
|
|
460
|
+
render: () => {
|
|
461
|
+
const [bg, setBg] = useState('Fondo gris claro');
|
|
462
|
+
const [color, setColor] = useState('Borde rojo');
|
|
463
|
+
const [combined, setCombined] = useState('Custom completo');
|
|
464
|
+
return (
|
|
465
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, width: 300 }}>
|
|
466
|
+
<Input
|
|
467
|
+
label="Override de background"
|
|
468
|
+
value={bg}
|
|
469
|
+
onChange={(v) => setBg(v.toString())}
|
|
470
|
+
sx={{ '& .MuiOutlinedInput-root': { bgcolor: '#f5f5f5' } }}
|
|
471
|
+
/>
|
|
472
|
+
<Input
|
|
473
|
+
label="Override de borderColor"
|
|
474
|
+
value={color}
|
|
475
|
+
onChange={(v) => setColor(v.toString())}
|
|
476
|
+
sx={{ '& .MuiOutlinedInput-notchedOutline': { borderColor: 'red' } }}
|
|
477
|
+
/>
|
|
478
|
+
<Input
|
|
479
|
+
label="Combinado: bg + borderColor + label color"
|
|
480
|
+
value={combined}
|
|
481
|
+
onChange={(v) => setCombined(v.toString())}
|
|
482
|
+
sx={{
|
|
483
|
+
'& .MuiInputLabel-root': { color: 'primary.main' },
|
|
484
|
+
'& .MuiOutlinedInput-root': { bgcolor: '#fff7e6' },
|
|
485
|
+
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'warning.main', borderWidth: 2 },
|
|
486
|
+
}}
|
|
487
|
+
/>
|
|
488
|
+
</Box>
|
|
489
|
+
);
|
|
490
|
+
},
|
|
491
|
+
parameters: {
|
|
492
|
+
docs: {
|
|
493
|
+
description: {
|
|
494
|
+
story: "Demuestra que el prop `sx` se mergea al final y siempre gana sobre los defaults internos. Antes esto no funcionaba por culpa del `styled()` interno."
|
|
495
|
+
},
|
|
496
|
+
source: { code: CustomStylingDefinition.trim() }
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { TextFieldProps } from '@mui/material';
|
|
2
|
+
|
|
3
|
+
import { buildFormFieldSx } from '../_shared/formField.sx';
|
|
4
|
+
import { FIELD_INPUT_PADDING_Y } from '../../theme/tokens';
|
|
5
|
+
import type { LabelPosition } from './Input';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builder de sx para el Input. Usa el builder compartido
|
|
9
|
+
* `buildFormFieldSx` y añade overrides específicos de TextField
|
|
10
|
+
* (padding del input base, filled, standard).
|
|
11
|
+
*/
|
|
12
|
+
export const buildInputSx = (
|
|
13
|
+
borderRadius: number | string,
|
|
14
|
+
labelPosition: LabelPosition,
|
|
15
|
+
): TextFieldProps['sx'] => {
|
|
16
|
+
const radius = typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;
|
|
17
|
+
|
|
18
|
+
return buildFormFieldSx({
|
|
19
|
+
borderRadius,
|
|
20
|
+
labelPosition,
|
|
21
|
+
extraOutsideSx: {
|
|
22
|
+
'& .MuiInputBase-input': {
|
|
23
|
+
paddingTop: FIELD_INPUT_PADDING_Y,
|
|
24
|
+
paddingBottom: FIELD_INPUT_PADDING_Y,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// Filled: quitar el padding-top reservado para el label flotante dentro.
|
|
28
|
+
'& .MuiFilledInput-root': {
|
|
29
|
+
paddingTop: 0,
|
|
30
|
+
borderRadius: `${radius} ${radius} 0 0`,
|
|
31
|
+
},
|
|
32
|
+
'& .MuiFilledInput-input': {
|
|
33
|
+
paddingTop: FIELD_INPUT_PADDING_Y,
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// Standard: quitar el margin-top reservado para el label flotante.
|
|
37
|
+
'& .MuiInput-root': {
|
|
38
|
+
marginTop: 0,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { TextField, type TextFieldProps } from '@mui/material';
|
|
2
|
+
import { useTheme } from '@mui/material/styles';
|
|
3
|
+
import { Controller, type Control, type RegisterOptions } from 'react-hook-form';
|
|
4
|
+
|
|
5
|
+
import { parseValue, buildSlotProps } from './Input.helpers';
|
|
6
|
+
import { buildInputSx } from './Input.sx';
|
|
7
|
+
import { resolvePreset } from '../_shared/resolvePreset';
|
|
8
|
+
|
|
9
|
+
// ── Tipos de dominio ─────────────────────────────────────────────────────
|
|
10
|
+
export type InputType =
|
|
11
|
+
| 'text' | 'number' | 'email' | 'password' | 'tel'
|
|
12
|
+
| 'url' | 'search' | 'date' | 'datetime-local'
|
|
13
|
+
| 'month' | 'week' | 'time' | 'color';
|
|
14
|
+
|
|
15
|
+
export type LabelPosition = 'outside' | 'floating';
|
|
16
|
+
|
|
17
|
+
// ── Props base (todo lo común entre RHF y controlado) ────────────────────
|
|
18
|
+
export interface BaseInputProps
|
|
19
|
+
extends Omit<TextFieldProps, 'value' | 'onChange' | 'type'> {
|
|
20
|
+
type?: InputType;
|
|
21
|
+
min?: number;
|
|
22
|
+
max?: number;
|
|
23
|
+
/** Border radius del input. Default: 10 */
|
|
24
|
+
borderRadius?: number | string;
|
|
25
|
+
/** "outside" = label arriba del campo (default). "floating" = label clásico MUI dentro del borde */
|
|
26
|
+
labelPosition?: LabelPosition;
|
|
27
|
+
readOnly?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Nombre del preset de estilo registrado en `theme.styles.Input`.
|
|
30
|
+
* - `"default"` (o ausente) = estilo built-in del paquete.
|
|
31
|
+
* - Cualquier otro string = mergea el preset encima del estilo built-in.
|
|
32
|
+
*/
|
|
33
|
+
preset?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Variantes discriminadas (RHF vs controlado) ──────────────────────────
|
|
37
|
+
export interface RHFInputProps extends BaseInputProps {
|
|
38
|
+
name: string;
|
|
39
|
+
control: Control<any>;
|
|
40
|
+
validation?: RegisterOptions;
|
|
41
|
+
value?: never;
|
|
42
|
+
onChange?: never;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ControlledInputProps extends BaseInputProps {
|
|
46
|
+
name?: string;
|
|
47
|
+
control?: never;
|
|
48
|
+
validation?: never;
|
|
49
|
+
value: string | number;
|
|
50
|
+
onChange: (value: string | number) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── API pública final ────────────────────────────────────────────────────
|
|
54
|
+
export type InputProps = RHFInputProps | ControlledInputProps;
|
|
55
|
+
|
|
56
|
+
export const Input: React.FC<InputProps> = (props) => {
|
|
57
|
+
const {
|
|
58
|
+
type = 'text',
|
|
59
|
+
variant = 'outlined',
|
|
60
|
+
size = 'small',
|
|
61
|
+
borderRadius = 10,
|
|
62
|
+
labelPosition = 'outside',
|
|
63
|
+
preset,
|
|
64
|
+
min,
|
|
65
|
+
max,
|
|
66
|
+
readOnly,
|
|
67
|
+
inputProps,
|
|
68
|
+
slotProps,
|
|
69
|
+
sx,
|
|
70
|
+
...rest
|
|
71
|
+
} = props as ControlledInputProps & {
|
|
72
|
+
control?: Control<any>;
|
|
73
|
+
validation?: RegisterOptions;
|
|
74
|
+
readOnly?: boolean;
|
|
75
|
+
preset?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const theme = useTheme();
|
|
79
|
+
const presetSx = resolvePreset('Input', preset, theme);
|
|
80
|
+
|
|
81
|
+
const finalSlotProps = buildSlotProps(
|
|
82
|
+
type,
|
|
83
|
+
min,
|
|
84
|
+
max,
|
|
85
|
+
slotProps,
|
|
86
|
+
inputProps,
|
|
87
|
+
labelPosition,
|
|
88
|
+
readOnly,
|
|
89
|
+
);
|
|
90
|
+
const mergedSx = [
|
|
91
|
+
buildInputSx(borderRadius, labelPosition),
|
|
92
|
+
...(presetSx ? [presetSx] : []),
|
|
93
|
+
...(Array.isArray(sx) ? sx : [sx]),
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
if ('control' in props && props.control) {
|
|
97
|
+
const { name, control, validation } = props as RHFInputProps;
|
|
98
|
+
return (
|
|
99
|
+
<Controller
|
|
100
|
+
name={name}
|
|
101
|
+
control={control}
|
|
102
|
+
rules={validation}
|
|
103
|
+
render={({ field, fieldState: { error } }) => (
|
|
104
|
+
<TextField
|
|
105
|
+
fullWidth
|
|
106
|
+
{...rest}
|
|
107
|
+
name={field.name}
|
|
108
|
+
value={field.value ?? ''}
|
|
109
|
+
onChange={(e) => field.onChange(parseValue(e.target.value, type))}
|
|
110
|
+
onBlur={field.onBlur}
|
|
111
|
+
inputRef={field.ref}
|
|
112
|
+
type={type}
|
|
113
|
+
variant={variant}
|
|
114
|
+
size={size}
|
|
115
|
+
slotProps={finalSlotProps}
|
|
116
|
+
sx={mergedSx}
|
|
117
|
+
error={!!error || rest.error}
|
|
118
|
+
helperText={error?.message ?? rest.helperText}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { value, onChange } = props as ControlledInputProps;
|
|
126
|
+
return (
|
|
127
|
+
<TextField
|
|
128
|
+
fullWidth
|
|
129
|
+
{...rest}
|
|
130
|
+
value={value ?? ''}
|
|
131
|
+
onChange={(e) => onChange(parseValue(e.target.value, type))}
|
|
132
|
+
type={type}
|
|
133
|
+
variant={variant}
|
|
134
|
+
size={size}
|
|
135
|
+
slotProps={finalSlotProps}
|
|
136
|
+
sx={mergedSx}
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export default Input;
|