@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,355 @@
1
+ import React, {
2
+ useState,
3
+ useImperativeHandle,
4
+ forwardRef,
5
+ type ReactNode,
6
+ } from 'react';
7
+ import {
8
+ Modal as MuiModal,
9
+ Paper,
10
+ useTheme,
11
+ useMediaQuery,
12
+ Box,
13
+ Stack,
14
+ Typography,
15
+ } from '@mui/material';
16
+ import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
17
+ import WarningAmberIcon from '@mui/icons-material/WarningAmber';
18
+ import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
19
+ import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
20
+
21
+ import type { DialogProps } from '@mui/material/Dialog';
22
+ import { ModalAction, ModalFooter, ModalFooterProps } from './ModalFooter';
23
+ import { ModalHeader } from './ModalHeader';
24
+ import { ModalBody } from './ModalBody';
25
+
26
+ // Define la interfaz para los métodos que el padre puede llamar a través de la ref
27
+ export interface ModalRef {
28
+ open: () => void;
29
+ close: () => void;
30
+ }
31
+
32
+ export type ModalMode = 'default' | 'confirm';
33
+ export type ModalSeverity = 'info' | 'warning' | 'error' | 'success';
34
+
35
+ interface ModalProps {
36
+ /**
37
+ * Modo del modal.
38
+ * - `default` (default): modal genérico con slots `Header`/`Body`/`Footer` y
39
+ * `actions` custom.
40
+ * - `confirm`: reemplaza al `ConfirmModal` legacy. Renderiza un layout
41
+ * focalizado con icono por severidad, mensaje y botones "Cancelar" /
42
+ * "Confirmar".
43
+ */
44
+ mode?: ModalMode;
45
+
46
+ // ── Props comunes ────────────────────────────────────────────────────
47
+ /** Controlado externamente. Omitir si se usa vía ref. */
48
+ open?: boolean;
49
+ /** Callback al cerrar. También se dispara al confirmar/cancelar. */
50
+ onClose?: () => void;
51
+ title?: string;
52
+ children?: ReactNode;
53
+ showCloseButton?: boolean;
54
+ closeButtonText?: string;
55
+ closeButtonDisabled?: boolean;
56
+ actions?: ModalAction[];
57
+ maxWidth?: DialogProps['maxWidth'];
58
+ hiddenFooter?: boolean;
59
+
60
+ // ── Props del modo confirm ───────────────────────────────────────────
61
+ /**
62
+ * Callback de confirmación. Soporta promesa — si devuelve `Promise`, el
63
+ * botón muestra estado `disabled` mientras resuelve.
64
+ */
65
+ onConfirm?: () => void | Promise<void>;
66
+ /** Texto del botón primario en modo `confirm`. Default: "Confirmar". */
67
+ confirmText?: string;
68
+ /** Deshabilita el botón de confirmar. */
69
+ confirmDisabled?: boolean;
70
+ /**
71
+ * Severidad visual en modo `confirm`. Controla el icono y el color del
72
+ * botón de confirmación.
73
+ * - `info` (default): primary
74
+ * - `warning`: warning (amarillo)
75
+ * - `error`: error (rojo) — típico para "Eliminar"
76
+ * - `success`: success (verde) — típico para "Aprobar"
77
+ */
78
+ severity?: ModalSeverity;
79
+ /**
80
+ * Mensaje del confirm. Puede ser string o cualquier ReactNode. Si se omite,
81
+ * se usa `children`.
82
+ */
83
+ confirmMessage?: ReactNode;
84
+ }
85
+
86
+ const modalStyle = {
87
+ position: 'absolute' as const,
88
+ top: '50%',
89
+ left: '50%',
90
+ transform: 'translate(-50%, -50%)',
91
+ width: '90%',
92
+ maxHeight: '90vh',
93
+ display: 'flex',
94
+ flexDirection: 'column' as const,
95
+ outline: 'none',
96
+ };
97
+
98
+ const severityConfig: Record<
99
+ ModalSeverity,
100
+ { color: ModalAction['color']; icon: ReactNode; bg: string }
101
+ > = {
102
+ info: {
103
+ color: 'primary',
104
+ icon: <InfoOutlinedIcon sx={{ fontSize: 48 }} />,
105
+ bg: 'primary.light',
106
+ },
107
+ warning: {
108
+ color: 'warning',
109
+ icon: <WarningAmberIcon sx={{ fontSize: 48 }} />,
110
+ bg: 'warning.light',
111
+ },
112
+ error: {
113
+ color: 'error',
114
+ icon: <ErrorOutlineIcon sx={{ fontSize: 48 }} />,
115
+ bg: 'error.light',
116
+ },
117
+ success: {
118
+ color: 'success',
119
+ icon: <CheckCircleOutlineIcon sx={{ fontSize: 48 }} />,
120
+ bg: 'success.light',
121
+ },
122
+ };
123
+
124
+ export const Modal = forwardRef<ModalRef, ModalProps>(
125
+ (
126
+ {
127
+ mode = 'default',
128
+ open: controlledOpen,
129
+ onClose: controlledOnClose,
130
+ title,
131
+ children,
132
+ showCloseButton = true,
133
+ closeButtonText = 'Cerrar',
134
+ closeButtonDisabled = false,
135
+ actions = [],
136
+ maxWidth = 'sm',
137
+ hiddenFooter = false,
138
+
139
+ // Props del modo confirm
140
+ onConfirm,
141
+ confirmText = 'Confirmar',
142
+ confirmDisabled = false,
143
+ severity = 'info',
144
+ confirmMessage,
145
+ },
146
+ ref,
147
+ ) => {
148
+ const [internalOpen, setInternalOpen] = useState(false);
149
+ const [confirmLoading, setConfirmLoading] = useState(false);
150
+
151
+ const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
152
+
153
+ useImperativeHandle(ref, () => ({
154
+ open: () => setInternalOpen(true),
155
+ close: () => {
156
+ setInternalOpen(false);
157
+ controlledOnClose?.();
158
+ },
159
+ }));
160
+
161
+ const handleCloseInternal = () => {
162
+ setInternalOpen(false);
163
+ controlledOnClose?.();
164
+ };
165
+
166
+ const handleConfirm = async () => {
167
+ if (!onConfirm) {
168
+ handleCloseInternal();
169
+ return;
170
+ }
171
+ try {
172
+ const result = onConfirm();
173
+ if (result instanceof Promise) {
174
+ setConfirmLoading(true);
175
+ await result;
176
+ }
177
+ } finally {
178
+ setConfirmLoading(false);
179
+ handleCloseInternal();
180
+ }
181
+ };
182
+
183
+ const theme = useTheme();
184
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
185
+
186
+ const getWidth = () => {
187
+ if (isMobile) return '95%';
188
+ switch (maxWidth) {
189
+ case 'xs':
190
+ return 300;
191
+ case 'sm':
192
+ return 500;
193
+ case 'md':
194
+ return 700;
195
+ case 'lg':
196
+ return 900;
197
+ case 'xl':
198
+ return 1100;
199
+ case false:
200
+ return 'auto';
201
+ default:
202
+ return 500;
203
+ }
204
+ };
205
+
206
+ // ── Render modo CONFIRM ─────────────────────────────────────────────
207
+ if (mode === 'confirm') {
208
+ const config = severityConfig[severity];
209
+ const message = confirmMessage ?? children;
210
+
211
+ return (
212
+ <MuiModal
213
+ open={isOpen}
214
+ onClose={handleCloseInternal}
215
+ aria-labelledby="modal-title"
216
+ closeAfterTransition
217
+ >
218
+ <Paper sx={{ ...modalStyle, width: getWidth() }}>
219
+ <Stack spacing={2.5} sx={{ p: 3, alignItems: 'center', textAlign: 'center' }}>
220
+ <Box
221
+ sx={{
222
+ width: 72,
223
+ height: 72,
224
+ borderRadius: '50%',
225
+ backgroundColor: (t) =>
226
+ t.palette[severity === 'info' ? 'primary' : severity].light,
227
+ color: (t) =>
228
+ t.palette[severity === 'info' ? 'primary' : severity].dark,
229
+ display: 'flex',
230
+ alignItems: 'center',
231
+ justifyContent: 'center',
232
+ opacity: 0.9,
233
+ }}
234
+ >
235
+ {config.icon}
236
+ </Box>
237
+ {title && (
238
+ <Typography variant="h6" component="h2" sx={{ fontWeight: 700 }}>
239
+ {title}
240
+ </Typography>
241
+ )}
242
+ {message && (
243
+ <Typography variant="body2" color="text.secondary">
244
+ {message}
245
+ </Typography>
246
+ )}
247
+ </Stack>
248
+ <ModalFooter
249
+ showCloseButton={showCloseButton}
250
+ closeButtonText={closeButtonText}
251
+ closeButtonDisabled={closeButtonDisabled || confirmLoading}
252
+ onClose={handleCloseInternal}
253
+ actions={[
254
+ {
255
+ text: confirmText,
256
+ onClick: handleConfirm,
257
+ disabled: confirmDisabled || confirmLoading,
258
+ variant: 'contained',
259
+ color: config.color,
260
+ },
261
+ ]}
262
+ />
263
+ </Paper>
264
+ </MuiModal>
265
+ );
266
+ }
267
+
268
+ // ── Render modo DEFAULT (legacy) ────────────────────────────────────
269
+ const renderChildren = () => {
270
+ let header: ReactNode | null = null;
271
+ let body: ReactNode | null = null;
272
+ let footer: ReactNode | null = null;
273
+
274
+ React.Children.forEach(children, (child) => {
275
+ if (React.isValidElement(child)) {
276
+ if (child.type === ModalHeader) {
277
+ header = child;
278
+ } else if (child.type === ModalBody) {
279
+ body = child;
280
+ } else if (child.type === ModalFooter) {
281
+ const footerChild = child as React.ReactElement<ModalFooterProps>;
282
+ const {
283
+ showCloseButton: childShowCloseButton,
284
+ closeButtonText: childCloseButtonText,
285
+ closeButtonDisabled: childCloseButtonDisabled,
286
+ onClose: childOnClose,
287
+ actions: childActions,
288
+ ...restOfFooterProps
289
+ } = footerChild.props;
290
+
291
+ footer = React.cloneElement(footerChild, {
292
+ showCloseButton,
293
+ closeButtonText,
294
+ closeButtonDisabled,
295
+ onClose: handleCloseInternal,
296
+ actions,
297
+ ...restOfFooterProps,
298
+ });
299
+ }
300
+ }
301
+ });
302
+
303
+ if (!footer && !hiddenFooter) {
304
+ footer = (
305
+ <ModalFooter
306
+ showCloseButton={showCloseButton}
307
+ closeButtonText={closeButtonText}
308
+ closeButtonDisabled={closeButtonDisabled}
309
+ onClose={handleCloseInternal}
310
+ actions={actions}
311
+ />
312
+ );
313
+ }
314
+
315
+ return (
316
+ <>
317
+ {header || (title && <ModalHeader>{title}</ModalHeader>)}
318
+ {body}
319
+ {footer}
320
+ </>
321
+ );
322
+ };
323
+
324
+ return (
325
+ <MuiModal
326
+ open={isOpen}
327
+ onClose={handleCloseInternal}
328
+ aria-labelledby="modal-title"
329
+ aria-describedby="modal-description"
330
+ closeAfterTransition
331
+ >
332
+ <Paper sx={{ ...modalStyle, width: getWidth() }}>
333
+ {renderChildren()}
334
+ </Paper>
335
+ </MuiModal>
336
+ );
337
+ },
338
+ );
339
+
340
+ // Define los sub-componentes como propiedades estáticas con tipos explícitos
341
+ type ModalComponent = React.ForwardRefExoticComponent<
342
+ ModalProps & React.RefAttributes<ModalRef>
343
+ > & {
344
+ Header: typeof ModalHeader;
345
+ Body: typeof ModalBody;
346
+ Footer: typeof ModalFooter;
347
+ };
348
+
349
+ const ModalWithStatics = Modal as ModalComponent;
350
+
351
+ ModalWithStatics.Header = ModalHeader;
352
+ ModalWithStatics.Body = ModalBody;
353
+ ModalWithStatics.Footer = ModalFooter;
354
+
355
+ export default ModalWithStatics;
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { Box } from '@mui/material';
3
+
4
+ interface ModalBodyProps { // Renombrado
5
+ children: React.ReactNode;
6
+ }
7
+
8
+ export const ModalBody: React.FC<ModalBodyProps> = ({ children }) => { // Renombrado
9
+ return (
10
+ <Box sx={{ padding: 2, overflowY: 'auto', flexGrow: 1 }}>
11
+ {children}
12
+ </Box>
13
+ );
14
+ };
15
+
16
+ export default ModalBody;
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+ import { Box, Button, Stack } from '@mui/material';
3
+ import { ButtonProps } from '@mui/material/Button';
4
+
5
+ // Interfaz para acciones personalizadas (se mantiene aquí)
6
+ export interface ModalAction {
7
+ text: string;
8
+ onClick?: () => void;
9
+ disabled?: boolean;
10
+ variant?: ButtonProps['variant'];
11
+ color?: ButtonProps['color'];
12
+ /**
13
+ * Props adicionales que se forwardean al `<Button>` interno. Útil para casos
14
+ * avanzados como conectar el botón con un `<form id="...">` externo usando
15
+ * `buttonProps={{ type: 'submit', form: 'my-form-id' }}`.
16
+ */
17
+ buttonProps?: Partial<ButtonProps> & { form?: string };
18
+ }
19
+
20
+ export interface ModalFooterProps { // Renombrado
21
+ children?: React.ReactNode;
22
+ showCloseButton?: boolean;
23
+ closeButtonText?: string;
24
+ closeButtonDisabled?: boolean;
25
+ onClose: () => void;
26
+ actions?: ModalAction[];
27
+ }
28
+
29
+ export const ModalFooter: React.FC<ModalFooterProps> = ({ // Renombrado
30
+ children,
31
+ showCloseButton = true,
32
+ closeButtonText = "Cerrar",
33
+ closeButtonDisabled = false,
34
+ onClose,
35
+ actions = [],
36
+ }) => {
37
+ return (
38
+ <Box sx={{ padding: 2, borderTop: '1px solid #e0e0e0', display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
39
+ {children}
40
+ <Stack direction="row" spacing={1}>
41
+ {showCloseButton && (
42
+ <Button
43
+ onClick={onClose}
44
+ disabled={closeButtonDisabled}
45
+ variant="outlined"
46
+ color="secondary"
47
+ >
48
+ {closeButtonText}
49
+ </Button>
50
+ )}
51
+ {actions.map((action, index) => {
52
+ const { buttonProps } = action;
53
+ return (
54
+ <Button
55
+ key={index}
56
+ onClick={action.onClick}
57
+ disabled={action.disabled}
58
+ variant={action.variant || 'contained'}
59
+ color={action.color || 'primary'}
60
+ {...(buttonProps as any)}
61
+ >
62
+ {action.text}
63
+ </Button>
64
+ );
65
+ })}
66
+ </Stack>
67
+ </Box>
68
+ );
69
+ };
70
+
71
+ export default ModalFooter;
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { Box, Typography } from '@mui/material';
3
+
4
+ interface ModalHeaderProps { // Renombrado
5
+ children: React.ReactNode;
6
+ }
7
+
8
+ export const ModalHeader: React.FC<ModalHeaderProps> = ({ children }) => { // Renombrado
9
+ return (
10
+ <Box sx={{ padding: 2, borderBottom: '1px solid #e0e0e0', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
11
+ <Typography variant="h6" component="h2">
12
+ {children}
13
+ </Typography>
14
+ </Box>
15
+ );
16
+ };
17
+
18
+ export default ModalHeader;
@@ -0,0 +1,6 @@
1
+ export { default as Modal } from './Modal';
2
+ export type { ModalRef, ModalMode, ModalSeverity } from './Modal';
3
+ export { ModalHeader } from './ModalHeader';
4
+ export { ModalBody } from './ModalBody';
5
+ export { ModalFooter } from './ModalFooter';
6
+ export type { ModalAction, ModalFooterProps } from './ModalFooter';
@@ -0,0 +1,217 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { Box, Button, Paper, Typography } from '@mui/material';
4
+
5
+ import { PageLoader } from './PageLoader';
6
+
7
+ const meta: Meta<typeof PageLoader> = {
8
+ title: 'Components/PageLoader',
9
+ component: PageLoader,
10
+ tags: ['autodocs'],
11
+ parameters: {
12
+ layout: 'fullscreen',
13
+ docs: {
14
+ description: {
15
+ component:
16
+ 'Loader visual para estados de carga, tanto a nivel de página completa (splash screen) como inline dentro de un contenedor. Reemplaza el `LayoutSplashScreen` de Metronic.',
17
+ },
18
+ },
19
+ },
20
+ argTypes: {
21
+ fullscreen: {
22
+ control: 'boolean',
23
+ description: 'Ocupa toda la ventana con backdrop. Default: true.',
24
+ },
25
+ size: {
26
+ control: { type: 'number', min: 16, max: 120, step: 4 },
27
+ description: 'Tamaño del spinner en px.',
28
+ },
29
+ open: {
30
+ control: 'boolean',
31
+ description: 'Si es false, el loader se desmonta (con fade en fullscreen).',
32
+ },
33
+ },
34
+ };
35
+
36
+ export default meta;
37
+ type Story = StoryObj<typeof PageLoader>;
38
+
39
+ // ── Stories ──────────────────────────────────────────────────────────────
40
+
41
+ export const FullscreenDefault: Story = {
42
+ args: {
43
+ fullscreen: true,
44
+ open: true,
45
+ },
46
+ parameters: {
47
+ docs: {
48
+ description: {
49
+ story: 'Loader full-page sin texto ni logo. Caso más minimal.',
50
+ },
51
+ },
52
+ },
53
+ };
54
+
55
+ export const FullscreenWithMessage: Story = {
56
+ args: {
57
+ fullscreen: true,
58
+ open: true,
59
+ message: 'Cargando aplicación...',
60
+ },
61
+ parameters: {
62
+ docs: {
63
+ description: {
64
+ story: 'Splash screen típico con mensaje bajo el spinner.',
65
+ },
66
+ },
67
+ },
68
+ };
69
+
70
+ export const FullscreenWithLogo: Story = {
71
+ args: {
72
+ fullscreen: true,
73
+ open: true,
74
+ message: 'Inicializando...',
75
+ logo: (
76
+ <Box
77
+ sx={{
78
+ width: 120,
79
+ height: 120,
80
+ borderRadius: '50%',
81
+ bgcolor: 'primary.main',
82
+ color: '#fff',
83
+ display: 'flex',
84
+ alignItems: 'center',
85
+ justifyContent: 'center',
86
+ fontSize: 36,
87
+ fontWeight: 700,
88
+ }}
89
+ >
90
+ fri
91
+ </Box>
92
+ ),
93
+ size: 40,
94
+ },
95
+ parameters: {
96
+ docs: {
97
+ description: {
98
+ story:
99
+ 'Splash de arranque de app con logo/branding arriba del spinner. Equivalente visual al `LayoutSplashScreen` de Metronic.',
100
+ },
101
+ },
102
+ },
103
+ };
104
+
105
+ export const Inline: Story = {
106
+ args: {
107
+ fullscreen: false,
108
+ open: true,
109
+ message: 'Cargando datos',
110
+ size: 32,
111
+ },
112
+ render: (args) => (
113
+ <Paper sx={{ maxWidth: 480, mx: 'auto', mt: 4 }}>
114
+ <Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
115
+ <Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
116
+ Panel de resumen
117
+ </Typography>
118
+ </Box>
119
+ <PageLoader {...args} />
120
+ </Paper>
121
+ ),
122
+ parameters: {
123
+ docs: {
124
+ description: {
125
+ story:
126
+ 'Loader inline (no fullscreen). Ocupa el ancho del contenedor padre y tiene una altura mínima. Útil dentro de cards mientras se cargan datos.',
127
+ },
128
+ },
129
+ },
130
+ };
131
+
132
+ export const ToggledFullscreen: Story = {
133
+ render: () => {
134
+ const [loading, setLoading] = useState(false);
135
+
136
+ const simulate = () => {
137
+ setLoading(true);
138
+ setTimeout(() => setLoading(false), 1800);
139
+ };
140
+
141
+ return (
142
+ <Box sx={{ p: 4, height: '100vh' }}>
143
+ <Typography variant="h5" gutterBottom>
144
+ Simulación de carga
145
+ </Typography>
146
+ <Typography color="text.secondary" sx={{ mb: 2 }}>
147
+ Al hacer click, el loader aparece por 1.8s y luego desaparece con fade.
148
+ </Typography>
149
+ <Button variant="contained" onClick={simulate}>
150
+ Simular carga
151
+ </Button>
152
+
153
+ <PageLoader
154
+ fullscreen
155
+ open={loading}
156
+ message="Procesando..."
157
+ logo={
158
+ <Box
159
+ sx={{
160
+ width: 72,
161
+ height: 72,
162
+ borderRadius: 2,
163
+ bgcolor: 'primary.main',
164
+ color: '#fff',
165
+ display: 'flex',
166
+ alignItems: 'center',
167
+ justifyContent: 'center',
168
+ fontSize: 24,
169
+ fontWeight: 700,
170
+ }}
171
+ >
172
+ fri
173
+ </Box>
174
+ }
175
+ />
176
+ </Box>
177
+ );
178
+ },
179
+ parameters: {
180
+ docs: {
181
+ description: {
182
+ story:
183
+ 'Ejemplo controlado: el loader se monta/desmonta según un estado externo, con fade de salida. Patrón típico para bloquear la app durante una operación crítica (auth, inicialización, navegación global).',
184
+ },
185
+ },
186
+ },
187
+ };
188
+
189
+ export const AutoDismiss: Story = {
190
+ render: () => {
191
+ const [open, setOpen] = useState(true);
192
+
193
+ useEffect(() => {
194
+ const t = setTimeout(() => setOpen(false), 2000);
195
+ return () => clearTimeout(t);
196
+ }, []);
197
+
198
+ return (
199
+ <Box sx={{ p: 4, height: '100vh' }}>
200
+ {!open && (
201
+ <Typography variant="h5">
202
+ App lista. (El loader se cerró tras 2s.)
203
+ </Typography>
204
+ )}
205
+ <PageLoader open={open} message="Cargando..." />
206
+ </Box>
207
+ );
208
+ },
209
+ parameters: {
210
+ docs: {
211
+ description: {
212
+ story:
213
+ 'Arranque típico: el loader aparece al montar, y el consumer lo cierra cuando termina la inicialización. Aquí simulamos 2 segundos fijos.',
214
+ },
215
+ },
216
+ },
217
+ };