@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,131 @@
|
|
|
1
|
+
// useClipboard.tsx (Versión Unificada)
|
|
2
|
+
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
|
3
|
+
import { Snackbar, Alert } from '@mui/material';
|
|
4
|
+
import { AlertProps } from '@mui/material/Alert';
|
|
5
|
+
|
|
6
|
+
// Tipo para las opciones del Snackbar
|
|
7
|
+
interface SnackbarOptions {
|
|
8
|
+
message?: string;
|
|
9
|
+
duration?: number;
|
|
10
|
+
severity?: AlertProps['severity'];
|
|
11
|
+
position?: { vertical: 'top' | 'bottom'; horizontal: 'left' | 'center' | 'right' };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hook 'useClipboard'.
|
|
16
|
+
* Proporciona funcionalidad de copiado de texto y feedback visual (Snackbar).
|
|
17
|
+
* Permite dos modos de uso:
|
|
18
|
+
* 1. Uso por Referencia (ref): Se adjunta a un elemento, copia su innerText al hacer clic.
|
|
19
|
+
* 2. Uso por Función (copy): Recibe el texto a copiar directamente como argumento.
|
|
20
|
+
*
|
|
21
|
+
* @param options Opciones de configuración para el Snackbar de feedback.
|
|
22
|
+
* @returns Un objeto que contiene:
|
|
23
|
+
* - 'ref': Una React Ref para adjuntar a un elemento (Modo Ref).
|
|
24
|
+
* - 'copy': Una función (text: string) => void para copiar texto (Modo Función).
|
|
25
|
+
* - 'CopyMessage': El componente Snackbar para renderizar el feedback.
|
|
26
|
+
*/
|
|
27
|
+
export function useClipboard(options?: SnackbarOptions) {
|
|
28
|
+
// --- Elementos de Copiado por Referencia ---
|
|
29
|
+
const elementRef = useRef<HTMLElement>(null);
|
|
30
|
+
|
|
31
|
+
// --- Estados y Opciones del Snackbar ---
|
|
32
|
+
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
|
33
|
+
const [snackbarMessage, setSnackbarMessage] = useState(options?.message || '¡Copiado al portapapeles!');
|
|
34
|
+
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertProps['severity']>(options?.severity || 'success');
|
|
35
|
+
const [snackbarPosition, setSnackbarPosition] = useState<import('@mui/material').SnackbarOrigin>(options?.position || { vertical: 'bottom', horizontal: 'center' });
|
|
36
|
+
const [snackbarDuration, setSnackbarDuration] = useState(options?.duration ?? 3000);
|
|
37
|
+
|
|
38
|
+
// Función para cerrar el Snackbar
|
|
39
|
+
const handleSnackbarClose = useCallback((event?: React.SyntheticEvent | Event, reason?: string) => {
|
|
40
|
+
if (reason === 'clickaway') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
setSnackbarOpen(false);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* FUNCIÓN PRINCIPAL DE COPIADO.
|
|
48
|
+
* Se usa internamente para 'ref' y se expone para 'copy(text)'.
|
|
49
|
+
* @param text El texto exacto a copiar.
|
|
50
|
+
*/
|
|
51
|
+
const executeCopy = useCallback((text: string) => {
|
|
52
|
+
try {
|
|
53
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
54
|
+
// 1. API moderna
|
|
55
|
+
navigator.clipboard.writeText(text);
|
|
56
|
+
} else {
|
|
57
|
+
// 2. Fallback usando textarea
|
|
58
|
+
const textarea = document.createElement('textarea');
|
|
59
|
+
textarea.value = text;
|
|
60
|
+
document.body.appendChild(textarea);
|
|
61
|
+
textarea.select();
|
|
62
|
+
document.execCommand('copy');
|
|
63
|
+
document.body.removeChild(textarea);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Mostrar éxito
|
|
67
|
+
setSnackbarMessage(options?.message || '¡Copiado al portapapeles!');
|
|
68
|
+
setSnackbarSeverity(options?.severity || 'success');
|
|
69
|
+
setSnackbarPosition(options?.position || { vertical: 'bottom', horizontal: 'center' });
|
|
70
|
+
setSnackbarDuration(options?.duration ?? 3000);
|
|
71
|
+
setSnackbarOpen(true);
|
|
72
|
+
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Mostrar error
|
|
75
|
+
console.error('Error al copiar al portapapeles:', err);
|
|
76
|
+
setSnackbarMessage('Error al copiar.');
|
|
77
|
+
setSnackbarSeverity('error');
|
|
78
|
+
setSnackbarPosition(options?.position || { vertical: 'bottom', horizontal: 'center' });
|
|
79
|
+
setSnackbarDuration(options?.duration ?? 3000);
|
|
80
|
+
setSnackbarOpen(true);
|
|
81
|
+
}
|
|
82
|
+
}, [options]);
|
|
83
|
+
|
|
84
|
+
// --- Lógica del Modo Ref ---
|
|
85
|
+
const copyFromRef = useCallback(() => {
|
|
86
|
+
if (elementRef.current) {
|
|
87
|
+
const textToCopy = elementRef.current.innerText || '';
|
|
88
|
+
executeCopy(textToCopy);
|
|
89
|
+
}
|
|
90
|
+
}, [executeCopy]);
|
|
91
|
+
|
|
92
|
+
// Adjunta un event listener de clic al elemento referenciado (Modo Ref)
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const currentElement = elementRef.current;
|
|
95
|
+
if (currentElement) {
|
|
96
|
+
currentElement.style.cursor = 'pointer';
|
|
97
|
+
// Adjuntamos la función que copia el texto del ref
|
|
98
|
+
currentElement.addEventListener('click', copyFromRef);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
if (currentElement) {
|
|
103
|
+
currentElement.removeEventListener('click', copyFromRef);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}, [copyFromRef]);
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
// Componente Snackbar encapsulado. Solo se renderiza si duration es > 0.
|
|
110
|
+
const CopyMessage = () => {
|
|
111
|
+
if (snackbarDuration <= 0) return null; // No renderizar si la duración es 0
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Snackbar
|
|
115
|
+
open={snackbarOpen}
|
|
116
|
+
autoHideDuration={snackbarDuration}
|
|
117
|
+
onClose={handleSnackbarClose}
|
|
118
|
+
anchorOrigin={snackbarPosition}
|
|
119
|
+
>
|
|
120
|
+
<Alert onClose={handleSnackbarClose} severity={snackbarSeverity} sx={{ width: '100%' }}>
|
|
121
|
+
{snackbarMessage}
|
|
122
|
+
</Alert>
|
|
123
|
+
</Snackbar>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Retorna ambas funcionalidades para que el usuario elija
|
|
128
|
+
return { ref: elementRef, copy: executeCopy, CopyMessage };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default useClipboard;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// ClipboardUnifiedDemo.tsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Button, Table, TableBody, TableCell, TableRow, Paper, Stack, Typography, Box, TableContainer, TableHead } from '@mui/material';
|
|
4
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
5
|
+
import { useClipboard } from './ClipBoard';
|
|
6
|
+
|
|
7
|
+
// Datos de ejemplo para la tabla (Modo Función)
|
|
8
|
+
const mockData = [
|
|
9
|
+
{ id: 1, content: 'dato-A-456' },
|
|
10
|
+
{ id: 2, content: 'dato-B-789' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Componente Wrapper para demostrar ambos modos de uso del hook useClipboard unificado.
|
|
15
|
+
* @param {object} args - Propiedades del Snackbar que se pasan al hook.
|
|
16
|
+
*/
|
|
17
|
+
interface ClipboardDemoProps {
|
|
18
|
+
message: string;
|
|
19
|
+
severity: 'success' | 'info' | 'warning' | 'error';
|
|
20
|
+
duration: number;
|
|
21
|
+
vertical: 'top' | 'bottom';
|
|
22
|
+
horizontal: 'left' | 'center' | 'right';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ClipboardUnifiedDemo: React.FC<ClipboardDemoProps> = ({ message, severity, duration, vertical, horizontal }) => {
|
|
26
|
+
const options = {
|
|
27
|
+
message,
|
|
28
|
+
severity,
|
|
29
|
+
duration,
|
|
30
|
+
position: { vertical, horizontal } as const // 'as const' para tipado correcto
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// 1. Llamada al hook para el MODO FUNCIÓN (se usará 'copy' y 'CopyMessage')
|
|
34
|
+
const { copy, CopyMessage } = useClipboard(options);
|
|
35
|
+
|
|
36
|
+
// 2. Llamada al hook para el MODO REF (se usará 'ref' y 'CopyMessageRef')
|
|
37
|
+
// Nota: Creamos una segunda instancia con opciones distintas para diferenciar el feedback
|
|
38
|
+
const { ref: refModeRef, CopyMessage: CopyMessageRef } = useClipboard({
|
|
39
|
+
...options,
|
|
40
|
+
message: options.message + ' (MODO REF)',
|
|
41
|
+
severity: 'warning' // Diferenciamos el color
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Stack spacing={4} sx={{ width: '100%', minWidth: 500, p: 3 }}>
|
|
46
|
+
<Typography variant="h5">Hook Unificado: Ref vs. Función</Typography>
|
|
47
|
+
|
|
48
|
+
{/* ========================================= */}
|
|
49
|
+
{/* A. MODO REF (Copia el innerText por clic) */}
|
|
50
|
+
{/* ========================================= */}
|
|
51
|
+
<Box>
|
|
52
|
+
<Typography variant="h6">1. MODO REF (Copia un solo elemento por clic)</Typography>
|
|
53
|
+
<Paper
|
|
54
|
+
sx={{
|
|
55
|
+
p: 2, mt: 1,
|
|
56
|
+
border: '1px solid #ccc',
|
|
57
|
+
borderRadius: '8px',
|
|
58
|
+
backgroundColor: '#eef',
|
|
59
|
+
cursor: 'pointer',
|
|
60
|
+
'&:hover': { backgroundColor: '#dde' },
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{/* Adjuntamos la ref. El hook maneja el evento de clic */}
|
|
64
|
+
<Typography ref={refModeRef} variant="body1" sx={{ userSelect: 'none' }}>
|
|
65
|
+
Texto para copiar con **REF**: Esta línea contiene el texto completo.
|
|
66
|
+
</Typography>
|
|
67
|
+
<ContentCopyIcon sx={{ fontSize: 16, ml: 1, verticalAlign: 'middle', color: 'text.secondary' }} />
|
|
68
|
+
</Paper>
|
|
69
|
+
<CopyMessageRef /> {/* Su propio Snackbar */}
|
|
70
|
+
</Box>
|
|
71
|
+
|
|
72
|
+
{/* ========================================= */}
|
|
73
|
+
{/* B. MODO FUNCIÓN (Copia en Iteración) */}
|
|
74
|
+
{/* ========================================= */}
|
|
75
|
+
<Box>
|
|
76
|
+
<Typography variant="h6">2. MODO FUNCIÓN (Copia múltiples elementos en tabla)</Typography>
|
|
77
|
+
<TableContainer component={Paper}>
|
|
78
|
+
<Table size="small">
|
|
79
|
+
<TableHead>
|
|
80
|
+
<TableRow><TableCell>ID</TableCell><TableCell>Contenido</TableCell><TableCell>Acción</TableCell></TableRow>
|
|
81
|
+
</TableHead>
|
|
82
|
+
<TableBody>
|
|
83
|
+
{/* Iteración: Se usa la función 'copy' */}
|
|
84
|
+
{mockData.map((item) => (
|
|
85
|
+
<TableRow key={item.id}>
|
|
86
|
+
<TableCell>{item.id}</TableCell>
|
|
87
|
+
<TableCell>{item.content}</TableCell>
|
|
88
|
+
<TableCell>
|
|
89
|
+
<Button
|
|
90
|
+
variant="contained"
|
|
91
|
+
size="small"
|
|
92
|
+
// Llamada a copy() con el texto específico de la fila
|
|
93
|
+
onClick={() => copy(item.content)}
|
|
94
|
+
>
|
|
95
|
+
Copiar {item.id}
|
|
96
|
+
</Button>
|
|
97
|
+
</TableCell>
|
|
98
|
+
</TableRow>
|
|
99
|
+
))}
|
|
100
|
+
</TableBody>
|
|
101
|
+
</Table>
|
|
102
|
+
</TableContainer>
|
|
103
|
+
</Box>
|
|
104
|
+
|
|
105
|
+
{/* El Snackbar del modo FUNCIÓN se renderiza una sola vez */}
|
|
106
|
+
<CopyMessage />
|
|
107
|
+
</Stack>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export default ClipboardUnifiedDemo;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as useClipboard } from './ClipBoard';
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { Box, Button, Paper, Stack, Typography, Alert } from '@mui/material';
|
|
4
|
+
|
|
5
|
+
import { WizardProvider } from './WizardContext';
|
|
6
|
+
import { useWizard } from './useWizard';
|
|
7
|
+
import Stepper from '../../components/Stepper/Stepper';
|
|
8
|
+
import Step from '../../components/Stepper/Step';
|
|
9
|
+
|
|
10
|
+
const meta: Meta = {
|
|
11
|
+
title: 'Hooks/useWizard',
|
|
12
|
+
tags: ['autodocs'],
|
|
13
|
+
parameters: {
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component:
|
|
17
|
+
'Hook + provider para administrar la navegación de un wizard multi-paso. Reemplaza el `StepperComponent` imperativo de Metronic y es retro-compatible con el `WizardContext` de `@soyfri/fri-web-components` (mismos nombres: `nextStep`, `prevStep`, `goToStep`, `submitStep`).',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj;
|
|
25
|
+
|
|
26
|
+
// ── Sub-componente: controles del wizard ─────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const WizardControls = () => {
|
|
29
|
+
const { currentStep, totalSteps, isFirst, isLast, nextStep, prevStep, submitStep, reset, completed } =
|
|
30
|
+
useWizard();
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Stack direction="row" spacing={1} justifyContent="flex-end" sx={{ mt: 3 }}>
|
|
34
|
+
<Button variant="text" onClick={reset} disabled={completed}>
|
|
35
|
+
Reiniciar
|
|
36
|
+
</Button>
|
|
37
|
+
<Button
|
|
38
|
+
variant="outlined"
|
|
39
|
+
onClick={prevStep}
|
|
40
|
+
disabled={isFirst || completed}
|
|
41
|
+
>
|
|
42
|
+
Atrás
|
|
43
|
+
</Button>
|
|
44
|
+
{isLast ? (
|
|
45
|
+
<Button
|
|
46
|
+
variant="contained"
|
|
47
|
+
color="primary"
|
|
48
|
+
onClick={submitStep}
|
|
49
|
+
disabled={completed}
|
|
50
|
+
>
|
|
51
|
+
{completed ? 'Enviado' : 'Enviar'}
|
|
52
|
+
</Button>
|
|
53
|
+
) : (
|
|
54
|
+
<Button variant="contained" onClick={nextStep}>
|
|
55
|
+
Siguiente
|
|
56
|
+
</Button>
|
|
57
|
+
)}
|
|
58
|
+
<Typography
|
|
59
|
+
variant="caption"
|
|
60
|
+
color="text.secondary"
|
|
61
|
+
sx={{ alignSelf: 'center', ml: 2 }}
|
|
62
|
+
>
|
|
63
|
+
Paso {currentStep + 1} de {totalSteps}
|
|
64
|
+
</Typography>
|
|
65
|
+
</Stack>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ── Story 1: wizard básico con Stepper ──────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const BasicWizardBody = () => {
|
|
72
|
+
const { currentStep, completed } = useWizard();
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Paper sx={{ p: 3, maxWidth: 720, mx: 'auto' }}>
|
|
76
|
+
{completed && (
|
|
77
|
+
<Alert severity="success" sx={{ mb: 2 }}>
|
|
78
|
+
¡Wizard enviado! Usá "Reiniciar" para volver al paso 1.
|
|
79
|
+
</Alert>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
<Stepper>
|
|
83
|
+
<Step label="Datos personales">
|
|
84
|
+
<Box sx={{ p: 2 }}>
|
|
85
|
+
<Typography variant="h6">Paso 1 — Datos personales</Typography>
|
|
86
|
+
<Typography color="text.secondary">
|
|
87
|
+
Acá iría el formulario de nombre, email, teléfono, etc.
|
|
88
|
+
</Typography>
|
|
89
|
+
</Box>
|
|
90
|
+
</Step>
|
|
91
|
+
<Step label="Dirección">
|
|
92
|
+
<Box sx={{ p: 2 }}>
|
|
93
|
+
<Typography variant="h6">Paso 2 — Dirección</Typography>
|
|
94
|
+
<Typography color="text.secondary">
|
|
95
|
+
Acá iría el formulario de dirección, ciudad, país.
|
|
96
|
+
</Typography>
|
|
97
|
+
</Box>
|
|
98
|
+
</Step>
|
|
99
|
+
<Step label="Confirmación">
|
|
100
|
+
<Box sx={{ p: 2 }}>
|
|
101
|
+
<Typography variant="h6">Paso 3 — Confirmación</Typography>
|
|
102
|
+
<Typography color="text.secondary">
|
|
103
|
+
Revisá los datos antes de enviar.
|
|
104
|
+
</Typography>
|
|
105
|
+
</Box>
|
|
106
|
+
</Step>
|
|
107
|
+
</Stepper>
|
|
108
|
+
|
|
109
|
+
<WizardControls />
|
|
110
|
+
<Typography
|
|
111
|
+
variant="caption"
|
|
112
|
+
color="text.secondary"
|
|
113
|
+
sx={{ display: 'block', mt: 1, textAlign: 'right' }}
|
|
114
|
+
>
|
|
115
|
+
Paso actual (0-based): {currentStep}
|
|
116
|
+
</Typography>
|
|
117
|
+
</Paper>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const BasicWizard: Story = {
|
|
122
|
+
render: () => (
|
|
123
|
+
<WizardProvider
|
|
124
|
+
totalSteps={3}
|
|
125
|
+
onSubmit={(step) => console.log('Submit desde paso', step)}
|
|
126
|
+
onStepChange={(newStep, prevStep) =>
|
|
127
|
+
console.log(`Cambio: ${prevStep} → ${newStep}`)
|
|
128
|
+
}
|
|
129
|
+
>
|
|
130
|
+
<BasicWizardBody />
|
|
131
|
+
</WizardProvider>
|
|
132
|
+
),
|
|
133
|
+
parameters: {
|
|
134
|
+
docs: {
|
|
135
|
+
description: {
|
|
136
|
+
story:
|
|
137
|
+
'Wizard de 3 pasos. El `Stepper` lee `currentStep` del `WizardContext` automáticamente (sin necesidad de prop explícita). Los controles usan `useWizard()` para llamar `nextStep`, `prevStep`, `submitStep` y `reset`. Verifica la consola para ver los callbacks `onStepChange` y `onSubmit`.',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// ── Story 2: integración con validación por paso ────────────────────────
|
|
144
|
+
|
|
145
|
+
const ValidatedWizardBody = () => {
|
|
146
|
+
const { currentStep, isLast, nextStep, prevStep, submitStep, completed } =
|
|
147
|
+
useWizard();
|
|
148
|
+
const [canAdvance, setCanAdvance] = React.useState(false);
|
|
149
|
+
|
|
150
|
+
// Cada vez que cambia el paso, reseteamos el "canAdvance" como si hubiera
|
|
151
|
+
// que validar de nuevo.
|
|
152
|
+
React.useEffect(() => {
|
|
153
|
+
setCanAdvance(false);
|
|
154
|
+
}, [currentStep]);
|
|
155
|
+
|
|
156
|
+
const handleValidate = () => setCanAdvance(true);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Paper sx={{ p: 3, maxWidth: 720, mx: 'auto' }}>
|
|
160
|
+
{completed && (
|
|
161
|
+
<Alert severity="success" sx={{ mb: 2 }}>
|
|
162
|
+
Wizard completado.
|
|
163
|
+
</Alert>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
<Stepper>
|
|
167
|
+
<Step label="Paso A">
|
|
168
|
+
<Box sx={{ p: 2 }}>
|
|
169
|
+
<Typography>Validá este paso para continuar.</Typography>
|
|
170
|
+
</Box>
|
|
171
|
+
</Step>
|
|
172
|
+
<Step label="Paso B">
|
|
173
|
+
<Box sx={{ p: 2 }}>
|
|
174
|
+
<Typography>Validá este paso para continuar.</Typography>
|
|
175
|
+
</Box>
|
|
176
|
+
</Step>
|
|
177
|
+
<Step label="Paso C">
|
|
178
|
+
<Box sx={{ p: 2 }}>
|
|
179
|
+
<Typography>Último paso — envía el wizard.</Typography>
|
|
180
|
+
</Box>
|
|
181
|
+
</Step>
|
|
182
|
+
</Stepper>
|
|
183
|
+
|
|
184
|
+
<Stack direction="row" spacing={1} justifyContent="flex-end" sx={{ mt: 3 }}>
|
|
185
|
+
<Button
|
|
186
|
+
variant="text"
|
|
187
|
+
onClick={handleValidate}
|
|
188
|
+
disabled={canAdvance}
|
|
189
|
+
>
|
|
190
|
+
{canAdvance ? 'Paso válido ✓' : 'Validar paso'}
|
|
191
|
+
</Button>
|
|
192
|
+
<Button variant="outlined" onClick={prevStep} disabled={currentStep === 0}>
|
|
193
|
+
Atrás
|
|
194
|
+
</Button>
|
|
195
|
+
{isLast ? (
|
|
196
|
+
<Button
|
|
197
|
+
variant="contained"
|
|
198
|
+
onClick={submitStep}
|
|
199
|
+
disabled={!canAdvance || completed}
|
|
200
|
+
>
|
|
201
|
+
Enviar
|
|
202
|
+
</Button>
|
|
203
|
+
) : (
|
|
204
|
+
<Button variant="contained" onClick={nextStep} disabled={!canAdvance}>
|
|
205
|
+
Siguiente
|
|
206
|
+
</Button>
|
|
207
|
+
)}
|
|
208
|
+
</Stack>
|
|
209
|
+
</Paper>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const WithPerStepValidation: Story = {
|
|
214
|
+
render: () => (
|
|
215
|
+
<WizardProvider
|
|
216
|
+
totalSteps={3}
|
|
217
|
+
onSubmit={() => console.log('Enviado!')}
|
|
218
|
+
>
|
|
219
|
+
<ValidatedWizardBody />
|
|
220
|
+
</WizardProvider>
|
|
221
|
+
),
|
|
222
|
+
parameters: {
|
|
223
|
+
docs: {
|
|
224
|
+
description: {
|
|
225
|
+
story:
|
|
226
|
+
'Patrón común: el botón "Siguiente" permanece deshabilitado hasta que el consumer valide el paso actual (normalmente con react-hook-form `trigger()`). El hook solo maneja la navegación — la validación es responsabilidad del consumer.',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// ── Story 3: navegación directa por click en steps ──────────────────────
|
|
233
|
+
|
|
234
|
+
const JumpableWizardBody = () => {
|
|
235
|
+
const { currentStep, totalSteps, goToStep, nextStep, prevStep } = useWizard();
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<Paper sx={{ p: 3, maxWidth: 720, mx: 'auto' }}>
|
|
239
|
+
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
|
240
|
+
Saltar a un paso específico
|
|
241
|
+
</Typography>
|
|
242
|
+
|
|
243
|
+
<Stack direction="row" spacing={1} sx={{ mb: 3 }}>
|
|
244
|
+
{Array.from({ length: totalSteps }).map((_, i) => (
|
|
245
|
+
<Button
|
|
246
|
+
key={i}
|
|
247
|
+
size="small"
|
|
248
|
+
variant={i === currentStep ? 'contained' : 'outlined'}
|
|
249
|
+
onClick={() => goToStep(i)}
|
|
250
|
+
>
|
|
251
|
+
Paso {i + 1}
|
|
252
|
+
</Button>
|
|
253
|
+
))}
|
|
254
|
+
</Stack>
|
|
255
|
+
|
|
256
|
+
<Stepper>
|
|
257
|
+
<Step label="Uno">
|
|
258
|
+
<Box sx={{ p: 2 }}>Paso 1</Box>
|
|
259
|
+
</Step>
|
|
260
|
+
<Step label="Dos">
|
|
261
|
+
<Box sx={{ p: 2 }}>Paso 2</Box>
|
|
262
|
+
</Step>
|
|
263
|
+
<Step label="Tres">
|
|
264
|
+
<Box sx={{ p: 2 }}>Paso 3</Box>
|
|
265
|
+
</Step>
|
|
266
|
+
<Step label="Cuatro">
|
|
267
|
+
<Box sx={{ p: 2 }}>Paso 4</Box>
|
|
268
|
+
</Step>
|
|
269
|
+
</Stepper>
|
|
270
|
+
|
|
271
|
+
<Stack direction="row" spacing={1} justifyContent="flex-end" sx={{ mt: 3 }}>
|
|
272
|
+
<Button variant="outlined" onClick={prevStep} disabled={currentStep === 0}>
|
|
273
|
+
Atrás
|
|
274
|
+
</Button>
|
|
275
|
+
<Button
|
|
276
|
+
variant="contained"
|
|
277
|
+
onClick={nextStep}
|
|
278
|
+
disabled={currentStep === totalSteps - 1}
|
|
279
|
+
>
|
|
280
|
+
Siguiente
|
|
281
|
+
</Button>
|
|
282
|
+
</Stack>
|
|
283
|
+
</Paper>
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export const JumpToStep: Story = {
|
|
288
|
+
render: () => (
|
|
289
|
+
<WizardProvider totalSteps={4}>
|
|
290
|
+
<JumpableWizardBody />
|
|
291
|
+
</WizardProvider>
|
|
292
|
+
),
|
|
293
|
+
parameters: {
|
|
294
|
+
docs: {
|
|
295
|
+
description: {
|
|
296
|
+
story:
|
|
297
|
+
'Uso de `goToStep(n)` para saltar a un paso específico. Útil cuando hay un sidebar o tabs que permiten navegación no-lineal (siempre y cuando el consumer valide antes de permitir el salto).',
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
};
|