@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,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
|
+
};
|