@stackcraftmx/calcita 0.51.0 → 0.52.0

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/README.md CHANGED
@@ -56,7 +56,10 @@ entrar y las del módulo de ejemplo, y `make:module` arma la de cada módulo nue
56
56
  Y dos revisiones para antes de subir: `pnpm lint` (ESLint: una variable que no existe, «t is not defined», se caza al
57
57
  escribir) y `pnpm check:texts` (`calcita check:texts`: que cada `t('llave')` escrita tal cual exista en todos los idiomas y
58
58
  que los idiomas tengan las mismas llaves). Sirve también en un proyecto que no salió del CLI: lee `src/**/*Texts.js` y
59
- `src/**/texts.js`.
59
+ `src/**/texts.js`; con el código en otra carpeta, `calcita check:texts --source resources/js` (un Laravel). Un archivo de
60
+ textos puede reexportar otro con ruta relativa o con `@/` (aquí apunta a la carpeta del código, como en Vite; pide Node 22.15
61
+ o más). Nunca da un verde falso: sin la carpeta, sin ningún archivo de textos o con uno
62
+ que no se pudo leer, sale con problema y no compara llaves.
60
63
 
61
64
  ## Arrancar en un proyecto que ya existe
62
65
 
@@ -597,7 +600,8 @@ el servidor del proyecto); `HelpButton` es el «?» que la vuelve a abrir.
597
600
 
598
601
  Para listas donde una tabla se queda corta, `RecordCard`: arriba quién es (avatar con iniciales, folio, nombre) con su
599
602
  estatus, insignias, botón para abrirlo y menú «⋯»; en el cuerpo sus datos en grupos (`groups`, o `content` libre) y el dato
600
- principal grande; abajo lo que va a todo lo ancho y «Ver detalle», que despliega más. El orden es el que se da y nada se
603
+ principal grande; abajo lo que va a todo lo ancho y «Ver detalle», que despliega más (`footerAlign` los junta a la
604
+ izquierda o a la derecha; por omisión «Ver detalle» va en la otra orilla). El orden es el que se da y nada se
601
605
  reacomoda solo: para un orden obligatorio, `trailing` pone el estatus al final y `columns` fija el ancho de cada columna.
602
606
  `RecordCardSkeleton` es su esqueleto. El menú «⋯» (`ActionsMenu`) acepta secciones, separadores, ligas, renglones propios y
603
607
  submenús (`children`), que se abren dentro del mismo menú con «Regresar». Un renglón con `to` es una pantalla de la app: va
@@ -986,6 +990,20 @@ formik y yup los instala el proyecto (`pnpm add formik yup`); `Formik`, `Form` y
986
990
  `@stackcraftmx/calcita/forms`, que es la misma copia. `FormWizard` parte un formulario en pasos, con pasos opcionales (hay
987
991
  uno para formik y otro para react-hook-form); los ejemplos traen los dos.
988
992
 
993
+ Para editar un registro de varios pasos (se entra a corregir un dato suelto, no a recorrer todo), el mismo `FormWizard`:
994
+
995
+ ```jsx
996
+ <FormWizard steps={steps} submitLabel="Guardar cambios"
997
+ initialStepKey={review.stepKey} // abre en el paso que dice el servidor
998
+ orientation={WIZARD_ORIENTATION.vertical} // un paso debajo del otro, con sus campos adentro
999
+ stepSelection={WIZARD_STEP_SELECTION.free} // los títulos se pulsan: salta directo (o .checked: solo sin errores)
1000
+ onStepChange={(stepKey) => setSearchParams({step: stepKey})} />
1001
+ ```
1002
+
1003
+ Con los títulos que se pulsan, la palomita de un paso la pone la pantalla (`steps[].completed`): «antes» ya no quiere decir
1004
+ «hecho». Al enviar, el formulario regresa al primer paso con error, como siempre. Sin estas opciones se porta igual que
1005
+ antes. `WizardLayout` suelto acepta lo mismo (`orientation`, `onStepSelect`); `skippedKeys` y `onSkip` ya no son obligatorios.
1006
+
989
1007
  Para que el formulario se vea como los de Apple (Ajustes, la hoja «Nuevo evento» de Calendario), los campos van dentro de
990
1008
  una `FormSection`: cada uno se vuelve un renglón, con la etiqueta a la izquierda y el valor a la derecha, sin la caja de
991
1009
  MUI. Son los mismos campos, sin cambiarles nada; las listas llevan su ⌃⌄ y la fecha va en una cápsula. `FormRow` es el
package/cli/calcita.js CHANGED
@@ -36,6 +36,7 @@ const HELP = `Calcita: proyectos y esqueletos con la forma de los ejemplos.
36
36
  --module <módulo> … o solo de un módulo
37
37
  calcita check:texts revisa que cada t('llave') exista en todos los idiomas, y que los idiomas tengan
38
38
  las mismas llaves (lo que si no se descubre en la pantalla)
39
+ --source <carpeta> … con el código en otra carpeta (por omisión src; en Laravel, resources/js)
39
40
 
40
41
  Opciones:
41
42
  -y, --yes sin preguntas: lo recomendado, más lo que diga cada opción
@@ -54,6 +55,7 @@ const {values: options, positionals} = parseArgs({
54
55
  'yes': {type: 'boolean', short: 'y'},
55
56
  'help': {type: 'boolean', short: 'h'},
56
57
  'module': {type: 'string'},
58
+ 'source': {type: 'string'},
57
59
  'label-es': {type: 'string'},
58
60
  'label-en': {type: 'string'},
59
61
  ...ANSWER_OPTIONS,
@@ -129,8 +131,8 @@ async function runMakeComponent(componentName) {
129
131
  }
130
132
 
131
133
  async function runCheckTexts() {
132
- const {problems, checkedKeys, textFiles, languages} = await checkTexts({projectRoot: cwd()});
133
- stdout.write(`Revisé ${checkedKeys} llaves escritas en el código contra ${textFiles.length} archivos de textos (${languages.join(', ') || 'sin idiomas'}).\n`);
134
+ const {problems, checkedKeys, textFiles, languages, sourceFolder} = await checkTexts({projectRoot: cwd(), sourceFolder: options.source});
135
+ stdout.write(`Revisé ${checkedKeys} llaves escritas en el código de ${sourceFolder}/ contra ${textFiles.length} archivos de textos (${languages.join(', ') || 'sin idiomas'}).\n`);
134
136
  if (problems.length === 0) {
135
137
  stdout.write('Todo en su lugar.\n');
136
138
  return;
package/cli/checkTexts.js CHANGED
@@ -8,12 +8,17 @@
8
8
  // Para esas está el aviso de Calcita en la consola (onMissingText de ToolkitProvider).
9
9
  //
10
10
  // Lee los archivos de textos del proyecto (src/**/*Texts.js y src/**/texts.js: cada uno exporta {es: {...}, en: {...}}) con
11
- // node, así que no deben importar otros archivos; el que los junta (con import.meta.glob) se salta. Los namespaces de Calcita
12
- // (common, table, validation…) salen de su dist/.
11
+ // node; el que los junta (con import.meta.glob) se salta. Si uno importa otro archivo, puede ser con ruta relativa o con el
12
+ // alias @/, que aquí apunta a la carpeta del código, como en vite.config.js (trampa pagada: un texts.js que reexportaba con
13
+ // @/ no se leía y sus 400 llaves salían como faltantes). Los namespaces de Calcita (common, table, validation…) salen de su dist/.
14
+ //
15
+ // Un verde falso es peor que un rojo: sin la carpeta, sin ningún archivo de textos o con uno que no se pudo leer, la revisión
16
+ // sale con problema y no compara llaves (con un archivo sin leer, todas las suyas saldrían como faltantes).
13
17
  //
14
18
  // Solo node, sin dependencias (el CLI corre antes de que el proyecto tenga nada instalado): function (regla de la casa).
15
19
  // Pruebas en checkTexts.test.js.
16
- import {readdir, readFile} from 'node:fs/promises';
20
+ import {readdir, readFile, stat} from 'node:fs/promises';
21
+ import * as nodeModule from 'node:module';
17
22
  import {join, relative} from 'node:path';
18
23
  import {pathToFileURL} from 'node:url';
19
24
 
@@ -27,6 +32,30 @@ const NAMESPACE_CALL = /useToolkitTranslation\(\s*\[?\s*(['"])([\w-]+)\1/g;
27
32
  const KEY_CALL = /\bt\(\s*(['"])([^'"`$\n]+?)\1([^)]{0,200})/g;
28
33
  const PLURAL_SUFFIXES = ['', '_zero', '_one', '_two', '_few', '_many', '_other'];
29
34
 
35
+ // la carpeta del código de la revisión en curso, para el alias @/ de los archivos de textos
36
+ let aliasFolderUrl = null;
37
+ let isAliasRegistered = false;
38
+
39
+ /**
40
+ * Que node entienda @/ al leer los archivos de textos, como Vite. Se registra una sola vez por proceso y lee aliasFolderUrl,
41
+ * la carpeta de la revisión en curso. registerHooks llegó en Node 22.15: con uno anterior, un archivo de textos que importe
42
+ * con @/ no se puede leer y se dice.
43
+ */
44
+ function understandAlias() {
45
+ if (isAliasRegistered || typeof nodeModule.registerHooks !== 'function') {
46
+ return;
47
+ }
48
+ nodeModule.registerHooks({
49
+ resolve(specifier, context, nextResolve) {
50
+ if (aliasFolderUrl && specifier.startsWith('@/')) {
51
+ return nextResolve(new URL(specifier.slice(2), aliasFolderUrl).href, context);
52
+ }
53
+ return nextResolve(specifier, context);
54
+ },
55
+ });
56
+ isAliasRegistered = true;
57
+ }
58
+
30
59
  /**
31
60
  * Los archivos de una carpeta y sus subcarpetas, sin node_modules ni dist.
32
61
  *
@@ -126,20 +155,31 @@ async function packageTextsOf() {
126
155
  /**
127
156
  * @param {object} options
128
157
  * @param {string} options.projectRoot la carpeta del proyecto
129
- * @param {string} [options.sourceFolder] dónde está el código (por omisión src)
158
+ * @param {string} [options.sourceFolder] dónde está el código (por omisión src; en Laravel, resources/js): `--source` del CLI
130
159
  * @param {Record<string, object>} [options.packageTexts] los textos de Calcita (por omisión, los de su dist/)
131
- * @returns {Promise<{problems: string[], checkedKeys: number, textFiles: string[], languages: string[]}>}
160
+ * @returns {Promise<{problems: string[], checkedKeys: number, textFiles: string[], languages: string[], sourceFolder: string}>}
132
161
  */
133
- export async function checkTexts({projectRoot, sourceFolder = 'src', packageTexts}) {
162
+ export async function checkTexts({projectRoot, sourceFolder: askedFolder = 'src', packageTexts}) {
163
+ // 'resources/js/' y 'resources/js' son la misma carpeta (el mensaje la dice sin la diagonal de más)
164
+ const sourceFolder = askedFolder.replace(/[\\/]+$/, '');
134
165
  const sourceRoot = join(projectRoot, sourceFolder);
166
+ const nothingChecked = {checkedKeys: 0, textFiles: [], languages: [], sourceFolder};
167
+ const isFolder = await stat(sourceRoot).then((found) => found.isDirectory(), () => false);
168
+ if (!isFolder) {
169
+ return {...nothingChecked, problems: [`No existe la carpeta ${sourceFolder}/ en el proyecto: no hay código que revisar (si está en otra, --source resources/js)`]};
170
+ }
135
171
  const files = await filesOf(sourceRoot);
136
172
  const problems = [];
173
+ aliasFolderUrl = `${pathToFileURL(sourceRoot).href}/`;
174
+ understandAlias();
137
175
 
138
176
  // 1. los textos del proyecto
139
177
  const projectTexts = {};
140
178
  const duplicates = [];
141
179
  const textFiles = files.filter((file) => TEXT_FILE.test(file.replaceAll('\\', '/')));
142
180
  const readTextFiles = [];
181
+ const unreadTextFiles = [];
182
+ const addedTexts = new Set();
143
183
  for (const file of textFiles) {
144
184
  const fileName = relative(projectRoot, file);
145
185
  // el que JUNTA los textos (appTexts.js, con el glob de Vite) no es un archivo de textos: se salta
@@ -153,11 +193,28 @@ export async function checkTexts({projectRoot, sourceFolder = 'src', packageText
153
193
  if (exported.length === 0) {
154
194
  problems.push(`${fileName}: no exporta textos con la forma {es: {...}, en: {...}}`);
155
195
  }
156
- exported.forEach((fileTexts) => addTexts(projectTexts, fileTexts, duplicates, fileName));
196
+ // el mismo objeto que otro archivo ya reexportó (texts.js que reexporta documentFileTexts.js) se suma una vez:
197
+ // node da el mismo objeto por archivo, y sumarlo dos veces lo haría pasar por repetido
198
+ exported.filter((fileTexts) => !addedTexts.has(fileTexts)).forEach((fileTexts) => {
199
+ addedTexts.add(fileTexts);
200
+ addTexts(projectTexts, fileTexts, duplicates, fileName);
201
+ });
157
202
  } catch (error) {
158
- problems.push(`${fileName}: no se pudo leer (${error.message.split('\n')[0]}); un archivo de textos no debe importar otros archivos`);
203
+ unreadTextFiles.push(fileName);
204
+ const reason = error.message.split('\n')[0];
205
+ const hint = isAliasRegistered
206
+ ? 'revise lo que importa: otro archivo va con ruta relativa o con @/'
207
+ : 'si importa con @/, esta revisión necesita Node 22.15 o más';
208
+ problems.push(`${fileName}: no se pudo leer (${reason}); ${hint}`);
159
209
  }
160
210
  }
211
+ if (unreadTextFiles.length > 0) {
212
+ problems.push(`No revisé las llaves: sin ${unreadTextFiles.length === 1 ? 'ese archivo' : 'esos archivos'} de textos, sus llaves saldrían todas como faltantes`);
213
+ return {...nothingChecked, problems, textFiles: readTextFiles};
214
+ }
215
+ if (readTextFiles.length === 0) {
216
+ return {...nothingChecked, problems: [`No encontré archivos de textos (*Texts.js o texts.js) en ${sourceFolder}/: no hay con qué comparar (si el código está en otra carpeta, --source resources/js)`]};
217
+ }
161
218
  problems.push(...duplicates.map((duplicate) => `texto repetido: ${duplicate}`));
162
219
 
163
220
  const languages = Object.keys(projectTexts);
@@ -205,5 +262,5 @@ export async function checkTexts({projectRoot, sourceFolder = 'src', packageText
205
262
  }
206
263
  }
207
264
 
208
- return {problems, checkedKeys, textFiles: readTextFiles, languages};
265
+ return {problems, checkedKeys, textFiles: readTextFiles, languages, sourceFolder};
209
266
  }
@@ -1 +1 @@
1
- {"version":3,"file":"BarActionButton.js","sources":["../../src/appShell/BarActionButton.jsx"],"sourcesContent":["// Un botón de acción de la pantalla que se acomoda solo a donde quedó (lo pidió el autor, y es lo que pide la guía de Apple\n// para las barras: «Prefer simple, recognizable symbols for items instead of text»):\n// - en una cápsula (arriba en la barra, flotando en el teléfono o en la franja): solo el ícono, con su nombre en el tooltip\n// y para el lector de pantalla. En el teléfono el tooltip sale al dejar el dedo encima, como en iOS;\n// - en el menú «⋯» del teléfono (las que ya no caben en el renglón flotante): un renglón con ícono y texto;\n// - suelto (fuera de BarActions): ícono con texto, botón sin relleno.\n// Lo demás va al botón (disabled, color, component y to para una liga…). Sin ícono, en la cápsula va su texto (un botón sin\n// relleno) y en el «⋯» el renglón sin ícono: mejor un texto que un botón vacío.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaBarActionButton con defaultProps, styleOverrides de\n// sus partes (button, iconButton, menuItem) y slots/slotProps para cambiarlas.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Button from '@mui/material/Button';\nimport IconButton from '@mui/material/IconButton';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport MenuItem from '@mui/material/MenuItem';\nimport Tooltip from '@mui/material/Tooltip';\n\nimport {BAR_ACTIONS_PLACEMENT, useBarActionsPlacement, useCloseBarActionsMenu} from '@/appShell/barActionsSlot.js';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaBarActionButton)\nconst THEME_NAME = 'CalcitaBarActionButton';\nconst BarActionButtonButton = piecePart(THEME_NAME, 'Button', Button);\nconst BarActionButtonIconButton = piecePart(THEME_NAME, 'IconButton', IconButton);\nconst BarActionButtonMenuItem = piecePart(THEME_NAME, 'MenuItem', MenuItem);\n\n/**\n * @param {object} props\n * @param {string} props.label el nombre de la acción, ya traducido: «Agregar sucursal»\n * @param {import('react').ReactNode} [props.icon] el ícono (<AddRoundedIcon />); sin él, se ve su texto\n * @param {(event: object) => void} [props.onClick]\n * @param {boolean} [props.disabled]\n * @param {'inherit'|'primary'|'secondary'|'success'|'error'|'info'|'warning'} [props.color] por omisión primary\n */\nfunction BarActionButtonContent(props) {\n const {label, icon, onClick, disabled, color = 'primary', ...buttonProps} = usePieceProps(props, THEME_NAME);\n const placement = useBarActionsPlacement();\n const closeMenu = useCloseBarActionsMenu();\n\n if (placement === BAR_ACTIONS_PLACEMENT.menu) {\n // primero se cierra el menú: la acción puede abrir una ventana y el menú quedaba encima\n const pickFromMenu = (event) => {\n closeMenu();\n onClick?.(event);\n };\n return (\n <BarActionButtonMenuItem onClick={pickFromMenu} disabled={disabled} {...buttonProps}>\n {icon && <ListItemIcon>{icon}</ListItemIcon>}\n <ListItemText inset={!icon}>{label}</ListItemText>\n </BarActionButtonMenuItem>\n );\n }\n if (placement === BAR_ACTIONS_PLACEMENT.capsule && icon) {\n // OJO: el tooltip envuelve un span para que también salga con el botón deshabilitado (MUI no lo pinta sobre un\n // elemento deshabilitado)\n return (\n <Tooltip title={label}>\n <span>\n <BarActionButtonIconButton aria-label={label} onClick={onClick} disabled={disabled} color={color} {...buttonProps}>{icon}</BarActionButtonIconButton>\n </span>\n </Tooltip>\n );\n }\n return <BarActionButtonButton startIcon={icon} onClick={onClick} disabled={disabled} color={color} {...buttonProps}>{label}</BarActionButtonButton>;\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst BarActionButton = withPieceSlots(BarActionButtonContent, THEME_NAME);\nexport default BarActionButton;\n"],"names":[],"mappings":";;;;;;;;;;AAwBA,MAAM,aAAa;AACnB,MAAM,wBAAwB,UAAU,YAAY,UAAU,MAAM;AACpE,MAAM,4BAA4B,UAAU,YAAY,cAAc,UAAU;AAChF,MAAM,0BAA0B,UAAU,YAAY,YAAY,QAAQ;AAU1E,SAAS,uBAAuB,OAAO;AACnC,QAAM,EAAC,OAAO,MAAM,SAAS,UAAU,QAAQ,WAAW,GAAG,YAAA,IAAe,cAAc,OAAO,UAAU;AAC3G,QAAM,YAAY,uBAAA;AAClB,QAAM,YAAY,uBAAA;AAElB,MAAI,cAAc,sBAAsB,MAAM;AAE1C,UAAM,eAAe,CAAC,UAAU;AAC5B,gBAAA;AACA,gBAAU,KAAK;AAAA,IACnB;AACA,gCACK,yBAAA,EAAwB,SAAS,cAAc,UAAqB,GAAG,aACnE,UAAA;AAAA,MAAA,QAAQ,oBAAC,gBAAc,UAAA,KAAA,CAAK;AAAA,MAC7B,oBAAC,cAAA,EAAa,OAAO,CAAC,MAAO,UAAA,MAAA,CAAM;AAAA,IAAA,GACvC;AAAA,EAER;AACA,MAAI,cAAc,sBAAsB,WAAW,MAAM;AAGrD,+BACK,SAAA,EAAQ,OAAO,OACZ,UAAA,oBAAC,UACG,UAAA,oBAAC,2BAAA,EAA0B,cAAY,OAAO,SAAkB,UAAoB,OAAe,GAAG,aAAc,UAAA,MAAK,GAC7H,EAAA,CACJ;AAAA,EAER;AACA,SAAO,oBAAC,yBAAsB,WAAW,MAAM,SAAkB,UAAoB,OAAe,GAAG,aAAc,UAAA,MAAA,CAAM;AAC/H;AAGA,MAAM,kBAAkB,eAAe,wBAAwB,UAAU;"}
1
+ {"version":3,"file":"BarActionButton.js","sources":["../../src/appShell/BarActionButton.jsx"],"sourcesContent":["// Un botón de acción de la pantalla que se acomoda solo a donde quedó (lo pidió el autor, y es lo que pide la guía de Apple\n// para las barras: «Prefer simple, recognizable symbols for items instead of text»):\n// - en una cápsula (arriba en la barra, flotando en el teléfono o en la franja): solo el ícono, con su nombre en el tooltip\n// y para el lector de pantalla. En el teléfono el tooltip sale al dejar el dedo encima, como en iOS;\n// - en el menú «⋯» del teléfono (las que ya no caben en el renglón flotante): un renglón con ícono y texto;\n// - suelto (fuera de BarActions): ícono con texto, botón sin relleno.\n// Lo demás va al botón (disabled, color, component y to para una liga…). Sin ícono, en la cápsula va su texto (un botón sin\n// relleno) y en el «⋯» el renglón sin ícono: mejor un texto que un botón vacío.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaBarActionButton con defaultProps, styleOverrides de\n// sus partes (button, iconButton, menuItem) y slots/slotProps para cambiarlas.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Button from '@mui/material/Button';\nimport IconButton from '@mui/material/IconButton';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport MenuItem from '@mui/material/MenuItem';\nimport Tooltip from '@mui/material/Tooltip';\n\nimport {BAR_ACTIONS_PLACEMENT, useBarActionsPlacement, useCloseBarActionsMenu} from '@/appShell/barActionsSlot.js';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaBarActionButton)\nconst THEME_NAME = 'CalcitaBarActionButton';\nconst BarActionButtonButton = piecePart(THEME_NAME, 'Button', Button);\nconst BarActionButtonIconButton = piecePart(THEME_NAME, 'IconButton', IconButton);\nconst BarActionButtonMenuItem = piecePart(THEME_NAME, 'MenuItem', MenuItem);\n\n/**\n * @param {object} props\n * @param {string} props.label el nombre de la acción, ya traducido: «Agregar sucursal»\n * @param {import('react').ReactNode} [props.icon] el ícono (<AddRoundedIcon />); sin él, se ve su texto\n * @param {(event: object) => void} [props.onClick]\n * @param {boolean} [props.disabled]\n * @param {'inherit'|'primary'|'secondary'|'success'|'error'|'info'|'warning'} [props.color] por omisión primary\n * @param {object} [props.sx] ajustes del botón; lo demás que se le pase también llega al botón (o al renglón del «⋯»)\n */\nfunction BarActionButtonContent(props) {\n const {label, icon, onClick, disabled, color = 'primary', ...buttonProps} = usePieceProps(props, THEME_NAME);\n const placement = useBarActionsPlacement();\n const closeMenu = useCloseBarActionsMenu();\n\n if (placement === BAR_ACTIONS_PLACEMENT.menu) {\n // primero se cierra el menú: la acción puede abrir una ventana y el menú quedaba encima\n const pickFromMenu = (event) => {\n closeMenu();\n onClick?.(event);\n };\n return (\n <BarActionButtonMenuItem onClick={pickFromMenu} disabled={disabled} {...buttonProps}>\n {icon && <ListItemIcon>{icon}</ListItemIcon>}\n <ListItemText inset={!icon}>{label}</ListItemText>\n </BarActionButtonMenuItem>\n );\n }\n if (placement === BAR_ACTIONS_PLACEMENT.capsule && icon) {\n // OJO: el tooltip envuelve un span para que también salga con el botón deshabilitado (MUI no lo pinta sobre un\n // elemento deshabilitado)\n return (\n <Tooltip title={label}>\n <span>\n <BarActionButtonIconButton aria-label={label} onClick={onClick} disabled={disabled} color={color} {...buttonProps}>{icon}</BarActionButtonIconButton>\n </span>\n </Tooltip>\n );\n }\n return <BarActionButtonButton startIcon={icon} onClick={onClick} disabled={disabled} color={color} {...buttonProps}>{label}</BarActionButtonButton>;\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst BarActionButton = withPieceSlots(BarActionButtonContent, THEME_NAME);\nexport default BarActionButton;\n"],"names":[],"mappings":";;;;;;;;;;AAwBA,MAAM,aAAa;AACnB,MAAM,wBAAwB,UAAU,YAAY,UAAU,MAAM;AACpE,MAAM,4BAA4B,UAAU,YAAY,cAAc,UAAU;AAChF,MAAM,0BAA0B,UAAU,YAAY,YAAY,QAAQ;AAW1E,SAAS,uBAAuB,OAAO;AACnC,QAAM,EAAC,OAAO,MAAM,SAAS,UAAU,QAAQ,WAAW,GAAG,YAAA,IAAe,cAAc,OAAO,UAAU;AAC3G,QAAM,YAAY,uBAAA;AAClB,QAAM,YAAY,uBAAA;AAElB,MAAI,cAAc,sBAAsB,MAAM;AAE1C,UAAM,eAAe,CAAC,UAAU;AAC5B,gBAAA;AACA,gBAAU,KAAK;AAAA,IACnB;AACA,gCACK,yBAAA,EAAwB,SAAS,cAAc,UAAqB,GAAG,aACnE,UAAA;AAAA,MAAA,QAAQ,oBAAC,gBAAc,UAAA,KAAA,CAAK;AAAA,MAC7B,oBAAC,cAAA,EAAa,OAAO,CAAC,MAAO,UAAA,MAAA,CAAM;AAAA,IAAA,GACvC;AAAA,EAER;AACA,MAAI,cAAc,sBAAsB,WAAW,MAAM;AAGrD,+BACK,SAAA,EAAQ,OAAO,OACZ,UAAA,oBAAC,UACG,UAAA,oBAAC,2BAAA,EAA0B,cAAY,OAAO,SAAkB,UAAoB,OAAe,GAAG,aAAc,UAAA,MAAK,GAC7H,EAAA,CACJ;AAAA,EAER;AACA,SAAO,oBAAC,yBAAsB,WAAW,MAAM,SAAkB,UAAoB,OAAe,GAAG,aAAc,UAAA,MAAA,CAAM;AAC/H;AAGA,MAAM,kBAAkB,eAAe,wBAAwB,UAAU;"}
@@ -4,17 +4,33 @@ import { useState, useRef, useEffect } from "react";
4
4
  import { useFormikContext, getIn } from "formik";
5
5
  import FormProblemAlert from "./FormProblemAlert.js";
6
6
  import SubmitButton from "./SubmitButton.js";
7
+ import { useWizardSteps } from "../inputs/useWizardSteps.js";
7
8
  import WizardLayout from "../inputs/WizardLayout.js";
8
9
  import { warnUnknownProps } from "../toolkit/unknownProps.js";
9
10
  function firstStepWithErrorOf(steps, errors) {
10
11
  return steps.findIndex((step) => (step.fields ?? []).some((field) => getIn(errors, field)));
11
12
  }
12
- function FormWizard({ steps, submitLabel, texts, ...unknownProps }) {
13
+ function FormWizard({
14
+ steps,
15
+ submitLabel,
16
+ orientation,
17
+ initialStepKey,
18
+ stepSelection,
19
+ onStepChange,
20
+ texts,
21
+ ...unknownProps
22
+ }) {
13
23
  warnUnknownProps("FormWizard", unknownProps);
14
24
  const { validateForm, setFieldTouched, setFieldValue, initialValues, errors, isSubmitting } = useFormikContext();
15
- const [activeIndex, setActiveIndex] = useState(0);
16
25
  const [skippedKeys, setSkippedKeys] = useState(() => /* @__PURE__ */ new Set());
17
26
  const wasSubmitting = useRef(false);
27
+ const isStepValid = async (index) => {
28
+ const stepFields = steps[index].fields ?? [];
29
+ const formErrors = await validateForm();
30
+ stepFields.forEach((field) => setFieldTouched(field, true, false));
31
+ return !stepFields.some((field) => getIn(formErrors, field));
32
+ };
33
+ const { activeIndex, setActiveIndex, selectStep } = useWizardSteps({ steps, initialStepKey, stepSelection, onStepChange, isStepValid });
18
34
  const activeStep = steps[activeIndex];
19
35
  useEffect(() => {
20
36
  if (wasSubmitting.current && !isSubmitting) {
@@ -26,10 +42,7 @@ function FormWizard({ steps, submitLabel, texts, ...unknownProps }) {
26
42
  wasSubmitting.current = isSubmitting;
27
43
  }, [isSubmitting, errors, steps]);
28
44
  const goNext = async () => {
29
- const stepFields = activeStep.fields ?? [];
30
- const formErrors = await validateForm();
31
- stepFields.forEach((field) => setFieldTouched(field, true, false));
32
- if (stepFields.some((field) => getIn(formErrors, field))) {
45
+ if (!await isStepValid(activeIndex)) {
33
46
  return;
34
47
  }
35
48
  setSkippedKeys((currentKeys) => new Set([...currentKeys].filter((key) => key !== activeStep.key)));
@@ -50,9 +63,11 @@ function FormWizard({ steps, submitLabel, texts, ...unknownProps }) {
50
63
  activeIndex,
51
64
  skippedKeys,
52
65
  isSubmitting,
66
+ orientation,
53
67
  onBack: () => setActiveIndex((currentIndex) => currentIndex - 1),
54
68
  onNext: goNext,
55
69
  onSkip: skipStep,
70
+ onStepSelect: selectStep,
56
71
  submitButton: /* @__PURE__ */ jsx(SubmitButton, { children: submitLabel }),
57
72
  problemAlert: /* @__PURE__ */ jsx(FormProblemAlert, {}),
58
73
  texts
@@ -1 +1 @@
1
- {"version":3,"file":"FormWizard.js","sources":["../../src/forms/FormWizard.jsx"],"sourcesContent":["// Formulario por pasos (wizard) con formik: un solo formulario partido en pasos, con pasos opcionales que se pueden omitir.\n//\n// Cómo se porta, para que la pantalla solo declare sus pasos:\n// - «Siguiente» revisa SOLO los campos del paso (fields) y no avanza si alguno tiene error; los marca como tocados para\n// que se vea por qué;\n// - «Omitir este paso» (solo en los opcionales) regresa sus campos a como empezaron: omitir es no dar ese dato, no dejar\n// lo que se escribió a medias;\n// - el último paso envía TODO el formulario (el esquema de yup completo vale para todos los pasos);\n// - si el envío falla por un campo de un paso anterior (yup o el 422 del servidor con applyServerProblem), el wizard\n// regresa solo a ese paso: un error que no se ve no se puede corregir.\n//\n// Va DENTRO de <Formik> y <Form>:\n// <Formik initialValues={…} validationSchema={…} onSubmit={…}>\n// <Form noValidate>\n// <FormWizard submitLabel=\"Enviar solicitud\" steps={[\n// {key: 'datos', label: 'Sus datos', fields: ['name', 'email'], content: <>…campos…</>},\n// {key: 'aval', label: 'Aval', optional: true, fields: ['guarantor'], content: <>…</>},\n// ]} />\n// </Form>\n// </Formik>\n//\n// Lo que se ve vive en WizardLayout (inputs/), el mismo del FormWizard de react-hook-form. Los textos («Siguiente»,\n// «Anterior»…) se cambian con `texts`: {wizard: {next: 'Continuar'}}.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useRef, useState} from 'react';\nimport {getIn, useFormikContext} from 'formik';\n\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport WizardLayout from '@/inputs/WizardLayout.jsx';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/**\n * El primer paso que tiene un campo con error; -1 si ninguno.\n *\n * @param {Array<{fields?: string[]}>} steps\n * @param {object} errors los de formik\n * @returns {number}\n */\nfunction firstStepWithErrorOf(steps, errors) {\n return steps.findIndex((step) => (step.fields ?? []).some((field) => getIn(errors, field)));\n}\n\n/**\n * @param {object} props\n * @param {Object[]} props.steps en orden\n * @param {string} props.steps[].key\n * @param {string} props.steps[].label ya traducido\n * @param {string[]} [props.steps[].fields] los campos del paso (los que revisa «Siguiente»)\n * @param {boolean} [props.steps[].optional] se puede omitir\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}\n */\nexport default function FormWizard({steps, submitLabel, texts, ...unknownProps}) {\n warnUnknownProps('FormWizard', unknownProps);\n const {validateForm, setFieldTouched, setFieldValue, initialValues, errors, isSubmitting} = useFormikContext();\n const [activeIndex, setActiveIndex] = useState(0);\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n const activeStep = steps[activeIndex];\n\n // al terminar un envío fallido, regresa al paso con el primer error (solo en ese momento: escribir en otro paso no\n // debe jalar a la persona de regreso)\n useEffect(() => {\n if (wasSubmitting.current && !isSubmitting) {\n const errorStepIndex = firstStepWithErrorOf(steps, errors);\n if (errorStepIndex >= 0) {\n setActiveIndex(errorStepIndex);\n }\n }\n wasSubmitting.current = isSubmitting;\n }, [isSubmitting, errors, steps]);\n\n const goNext = async () => {\n const stepFields = activeStep.fields ?? [];\n const formErrors = await validateForm();\n stepFields.forEach((field) => setFieldTouched(field, true, false));\n // guard clause: el paso tiene errores; se queda para que se corrijan\n if (stepFields.some((field) => getIn(formErrors, field))) {\n return;\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys].filter((key) => key !== activeStep.key)));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n const skipStep = () => {\n for (const field of activeStep.fields ?? []) {\n setFieldValue(field, getIn(initialValues, field), false);\n setFieldTouched(field, false, false);\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys, activeStep.key]));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n return (\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting}\n onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)} onNext={goNext} onSkip={skipStep}\n submitButton={<SubmitButton>{submitLabel}</SubmitButton>} problemAlert={<FormProblemAlert />} texts={texts} />\n );\n}\n"],"names":[],"mappings":";;;;;;;;AAwCA,SAAS,qBAAqB,OAAO,QAAQ;AACzC,SAAO,MAAM,UAAU,CAAC,UAAU,KAAK,UAAU,CAAA,GAAI,KAAK,CAAC,UAAU,MAAM,QAAQ,KAAK,CAAC,CAAC;AAC9F;AAaA,SAAwB,WAAW,EAAC,OAAO,aAAa,OAAO,GAAG,gBAAe;AAC7E,mBAAiB,cAAc,YAAY;AAC3C,QAAM,EAAC,cAAc,iBAAiB,eAAe,eAAe,QAAQ,aAAA,IAAgB,iBAAA;AAC5F,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAChD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM,oBAAI,KAAK;AAC9D,QAAM,gBAAgB,OAAO,KAAK;AAClC,QAAM,aAAa,MAAM,WAAW;AAIpC,YAAU,MAAM;AACZ,QAAI,cAAc,WAAW,CAAC,cAAc;AACxC,YAAM,iBAAiB,qBAAqB,OAAO,MAAM;AACzD,UAAI,kBAAkB,GAAG;AACrB,uBAAe,cAAc;AAAA,MACjC;AAAA,IACJ;AACA,kBAAc,UAAU;AAAA,EAC5B,GAAG,CAAC,cAAc,QAAQ,KAAK,CAAC;AAEhC,QAAM,SAAS,YAAY;AACvB,UAAM,aAAa,WAAW,UAAU,CAAA;AACxC,UAAM,aAAa,MAAM,aAAA;AACzB,eAAW,QAAQ,CAAC,UAAU,gBAAgB,OAAO,MAAM,KAAK,CAAC;AAEjE,QAAI,WAAW,KAAK,CAAC,UAAU,MAAM,YAAY,KAAK,CAAC,GAAG;AACtD;AAAA,IACJ;AACA,mBAAe,CAAC,gBAAgB,IAAI,IAAI,CAAC,GAAG,WAAW,EAAE,OAAO,CAAC,QAAQ,QAAQ,WAAW,GAAG,CAAC,CAAC;AACjG,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,QAAM,WAAW,MAAM;AACnB,eAAW,SAAS,WAAW,UAAU,CAAA,GAAI;AACzC,oBAAc,OAAO,MAAM,eAAe,KAAK,GAAG,KAAK;AACvD,sBAAgB,OAAO,OAAO,KAAK;AAAA,IACvC;AACA,mBAAe,CAAC,gBAAgB,oBAAI,IAAI,CAAC,GAAG,aAAa,WAAW,GAAG,CAAC,CAAC;AACzE,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAa;AAAA,MAAc;AAAA,MAA0B;AAAA,MAA0B;AAAA,MAClE,QAAQ,MAAM,eAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAQ,QAAQ;AAAA,MAC1F,cAAc,oBAAC,cAAA,EAAc,UAAA,YAAA,CAAY;AAAA,MAAiB,kCAAe,kBAAA,EAAiB;AAAA,MAAI;AAAA,IAAA;AAAA,EAAA;AAEpH;"}
1
+ {"version":3,"file":"FormWizard.js","sources":["../../src/forms/FormWizard.jsx"],"sourcesContent":["// Formulario por pasos (wizard) con formik: un solo formulario partido en pasos, con pasos opcionales que se pueden omitir.\n//\n// Cómo se porta, para que la pantalla solo declare sus pasos:\n// - «Siguiente» revisa SOLO los campos del paso (fields) y no avanza si alguno tiene error; los marca como tocados para\n// que se vea por qué;\n// - «Omitir este paso» (solo en los opcionales) regresa sus campos a como empezaron: omitir es no dar ese dato, no dejar\n// lo que se escribió a medias;\n// - el último paso envía TODO el formulario (el esquema de yup completo vale para todos los pasos);\n// - si el envío falla por un campo de un paso anterior (yup o el 422 del servidor con applyServerProblem), el wizard\n// regresa solo a ese paso: un error que no se ve no se puede corregir;\n// - para editar un registro (se entra a corregir un dato suelto): initialStepKey abre en el paso que diga el servidor,\n// orientation=\"vertical\" pone los campos dentro de su paso, y stepSelection deja pulsar el título de otro paso para\n// saltar directo (free) o solo si el paso abierto no tiene errores (checked). onStepChange avisa cada cambio de paso.\n//\n// Va DENTRO de <Formik> y <Form>:\n// <Formik initialValues={…} validationSchema={…} onSubmit={…}>\n// <Form noValidate>\n// <FormWizard submitLabel=\"Enviar solicitud\" steps={[\n// {key: 'datos', label: 'Sus datos', fields: ['name', 'email'], content: <>…campos…</>},\n// {key: 'aval', label: 'Aval', optional: true, fields: ['guarantor'], content: <>…</>},\n// ]} />\n// </Form>\n// </Formik>\n//\n// Lo que se ve vive en WizardLayout (inputs/), el mismo del FormWizard de react-hook-form. Los textos («Siguiente»,\n// «Anterior»…) se cambian con `texts`: {wizard: {next: 'Continuar'}}.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useRef, useState} from 'react';\nimport {getIn, useFormikContext} from 'formik';\n\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport {useWizardSteps} from '@/inputs/useWizardSteps.js';\nimport WizardLayout from '@/inputs/WizardLayout.jsx';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/**\n * El primer paso que tiene un campo con error; -1 si ninguno.\n *\n * @param {Array<{fields?: string[]}>} steps\n * @param {object} errors los de formik\n * @returns {number}\n */\nfunction firstStepWithErrorOf(steps, errors) {\n return steps.findIndex((step) => (step.fields ?? []).some((field) => getIn(errors, field)));\n}\n\n/**\n * @param {object} props\n * @param {Object[]} props.steps en orden\n * @param {string} props.steps[].key\n * @param {string} props.steps[].label ya traducido\n * @param {string[]} [props.steps[].fields] los campos del paso (los que revisa «Siguiente»)\n * @param {boolean} [props.steps[].optional] se puede omitir\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {boolean} [props.steps[].completed] lleva la palomita (cuando los títulos se pulsan, solo si la pantalla lo dice)\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {'horizontal'|'vertical'} [props.orientation] uno de WIZARD_ORIENTATION; por omisión horizontal\n * @param {string} [props.initialStepKey] la llave del paso en el que se abre (la que diga el servidor); por omisión el primero\n * @param {'none'|'free'|'checked'} [props.stepSelection] uno de WIZARD_STEP_SELECTION: si el título de otro paso se pulsa y qué\n * revisa antes; por omisión none (se avanza con «Siguiente», como siempre)\n * @param {(stepKey: string, index: number) => void} [props.onStepChange] cada vez que cambia el paso abierto\n * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}\n */\nexport default function FormWizard({\n steps, submitLabel, orientation, initialStepKey, stepSelection, onStepChange, texts, ...unknownProps\n}) {\n warnUnknownProps('FormWizard', unknownProps);\n const {validateForm, setFieldTouched, setFieldValue, initialValues, errors, isSubmitting} = useFormikContext();\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n\n // revisa SOLO los campos de un paso y los marca como tocados para que se vea por qué no avanza\n const isStepValid = async (index) => {\n const stepFields = steps[index].fields ?? [];\n const formErrors = await validateForm();\n stepFields.forEach((field) => setFieldTouched(field, true, false));\n return !stepFields.some((field) => getIn(formErrors, field));\n };\n const {activeIndex, setActiveIndex, selectStep} = useWizardSteps({steps, initialStepKey, stepSelection, onStepChange, isStepValid});\n const activeStep = steps[activeIndex];\n\n // al terminar un envío fallido, regresa al paso con el primer error (solo en ese momento: escribir en otro paso no\n // debe jalar a la persona de regreso)\n useEffect(() => {\n if (wasSubmitting.current && !isSubmitting) {\n const errorStepIndex = firstStepWithErrorOf(steps, errors);\n if (errorStepIndex >= 0) {\n setActiveIndex(errorStepIndex);\n }\n }\n wasSubmitting.current = isSubmitting;\n }, [isSubmitting, errors, steps]);\n\n const goNext = async () => {\n // el paso tiene errores: se queda ahí para que se corrijan\n if (!(await isStepValid(activeIndex))) {\n return;\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys].filter((key) => key !== activeStep.key)));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n const skipStep = () => {\n for (const field of activeStep.fields ?? []) {\n setFieldValue(field, getIn(initialValues, field), false);\n setFieldTouched(field, false, false);\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys, activeStep.key]));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n return (\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting} orientation={orientation}\n onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)} onNext={goNext} onSkip={skipStep} onStepSelect={selectStep}\n submitButton={<SubmitButton>{submitLabel}</SubmitButton>} problemAlert={<FormProblemAlert />} texts={texts} />\n );\n}\n"],"names":[],"mappings":";;;;;;;;;AA4CA,SAAS,qBAAqB,OAAO,QAAQ;AACzC,SAAO,MAAM,UAAU,CAAC,UAAU,KAAK,UAAU,CAAA,GAAI,KAAK,CAAC,UAAU,MAAM,QAAQ,KAAK,CAAC,CAAC;AAC9F;AAmBA,SAAwB,WAAW;AAAA,EAC/B;AAAA,EAAO;AAAA,EAAa;AAAA,EAAa;AAAA,EAAgB;AAAA,EAAe;AAAA,EAAc;AAAA,EAAO,GAAG;AAC5F,GAAG;AACC,mBAAiB,cAAc,YAAY;AAC3C,QAAM,EAAC,cAAc,iBAAiB,eAAe,eAAe,QAAQ,aAAA,IAAgB,iBAAA;AAC5F,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM,oBAAI,KAAK;AAC9D,QAAM,gBAAgB,OAAO,KAAK;AAGlC,QAAM,cAAc,OAAO,UAAU;AACjC,UAAM,aAAa,MAAM,KAAK,EAAE,UAAU,CAAA;AAC1C,UAAM,aAAa,MAAM,aAAA;AACzB,eAAW,QAAQ,CAAC,UAAU,gBAAgB,OAAO,MAAM,KAAK,CAAC;AACjE,WAAO,CAAC,WAAW,KAAK,CAAC,UAAU,MAAM,YAAY,KAAK,CAAC;AAAA,EAC/D;AACA,QAAM,EAAC,aAAa,gBAAgB,WAAA,IAAc,eAAe,EAAC,OAAO,gBAAgB,eAAe,cAAc,YAAA,CAAY;AAClI,QAAM,aAAa,MAAM,WAAW;AAIpC,YAAU,MAAM;AACZ,QAAI,cAAc,WAAW,CAAC,cAAc;AACxC,YAAM,iBAAiB,qBAAqB,OAAO,MAAM;AACzD,UAAI,kBAAkB,GAAG;AACrB,uBAAe,cAAc;AAAA,MACjC;AAAA,IACJ;AACA,kBAAc,UAAU;AAAA,EAC5B,GAAG,CAAC,cAAc,QAAQ,KAAK,CAAC;AAEhC,QAAM,SAAS,YAAY;AAEvB,QAAI,CAAE,MAAM,YAAY,WAAW,GAAI;AACnC;AAAA,IACJ;AACA,mBAAe,CAAC,gBAAgB,IAAI,IAAI,CAAC,GAAG,WAAW,EAAE,OAAO,CAAC,QAAQ,QAAQ,WAAW,GAAG,CAAC,CAAC;AACjG,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,QAAM,WAAW,MAAM;AACnB,eAAW,SAAS,WAAW,UAAU,CAAA,GAAI;AACzC,oBAAc,OAAO,MAAM,eAAe,KAAK,GAAG,KAAK;AACvD,sBAAgB,OAAO,OAAO,KAAK;AAAA,IACvC;AACA,mBAAe,CAAC,gBAAgB,oBAAI,IAAI,CAAC,GAAG,aAAa,WAAW,GAAG,CAAC,CAAC;AACzE,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAa;AAAA,MAAc;AAAA,MAA0B;AAAA,MAA0B;AAAA,MAA4B;AAAA,MAC9F,QAAQ,MAAM,eAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAQ,QAAQ;AAAA,MAAU,cAAc;AAAA,MAClH,cAAc,oBAAC,cAAA,EAAc,UAAA,YAAA,CAAY;AAAA,MAAiB,kCAAe,kBAAA,EAAiB;AAAA,MAAI;AAAA,IAAA;AAAA,EAAA;AAEpH;"}
@@ -4,15 +4,29 @@ import { useState, useRef, useEffect } from "react";
4
4
  import { useFormContext } from "react-hook-form";
5
5
  import FormProblemAlert from "./FormProblemAlert.js";
6
6
  import SubmitButton from "./SubmitButton.js";
7
+ import { useWizardSteps } from "../inputs/useWizardSteps.js";
7
8
  import WizardLayout from "../inputs/WizardLayout.js";
8
9
  import { warnUnknownProps } from "../toolkit/unknownProps.js";
9
- function FormWizard({ steps, submitLabel, texts, ...unknownProps }) {
10
+ function FormWizard({
11
+ steps,
12
+ submitLabel,
13
+ orientation,
14
+ initialStepKey,
15
+ stepSelection,
16
+ onStepChange,
17
+ texts,
18
+ ...unknownProps
19
+ }) {
10
20
  warnUnknownProps("FormWizard", unknownProps);
11
21
  const { trigger, resetField, getFieldState, formState } = useFormContext();
12
22
  const { isSubmitting, submitCount } = formState;
13
- const [activeIndex, setActiveIndex] = useState(0);
14
23
  const [skippedKeys, setSkippedKeys] = useState(() => /* @__PURE__ */ new Set());
15
24
  const wasSubmitting = useRef(false);
25
+ const isStepValid = async (index) => {
26
+ const stepFields = steps[index].fields ?? [];
27
+ return stepFields.length === 0 || trigger(stepFields, { shouldFocus: true });
28
+ };
29
+ const { activeIndex, setActiveIndex, selectStep } = useWizardSteps({ steps, initialStepKey, stepSelection, onStepChange, isStepValid });
16
30
  const activeStep = steps[activeIndex];
17
31
  useEffect(() => {
18
32
  if (wasSubmitting.current && !isSubmitting) {
@@ -24,9 +38,7 @@ function FormWizard({ steps, submitLabel, texts, ...unknownProps }) {
24
38
  wasSubmitting.current = isSubmitting;
25
39
  }, [isSubmitting, submitCount]);
26
40
  const goNext = async () => {
27
- const stepFields = activeStep.fields ?? [];
28
- const isStepValid = stepFields.length === 0 || await trigger(stepFields, { shouldFocus: true });
29
- if (!isStepValid) {
41
+ if (!await isStepValid(activeIndex)) {
30
42
  return;
31
43
  }
32
44
  setSkippedKeys((currentKeys) => new Set([...currentKeys].filter((key) => key !== activeStep.key)));
@@ -46,9 +58,11 @@ function FormWizard({ steps, submitLabel, texts, ...unknownProps }) {
46
58
  activeIndex,
47
59
  skippedKeys,
48
60
  isSubmitting,
61
+ orientation,
49
62
  onBack: () => setActiveIndex((currentIndex) => currentIndex - 1),
50
63
  onNext: goNext,
51
64
  onSkip: skipStep,
65
+ onStepSelect: selectStep,
52
66
  submitButton: /* @__PURE__ */ jsx(SubmitButton, { children: submitLabel }),
53
67
  problemAlert: /* @__PURE__ */ jsx(FormProblemAlert, {}),
54
68
  texts
@@ -1 +1 @@
1
- {"version":3,"file":"FormWizard.js","sources":["../../src/hookForm/FormWizard.jsx"],"sourcesContent":["// Formulario por pasos (wizard) con react-hook-form: el mismo del de formik (@stackcraftmx/calcita/forms), con las mismas\n// props y el mismo comportamiento:\n// - «Siguiente» revisa SOLO los campos del paso (trigger con sus nombres) y no avanza si alguno tiene error;\n// - «Omitir este paso» (solo en los opcionales) regresa sus campos a su valor de inicio (resetField): omitir es no dar ese\n// dato, no dejar lo que se escribió a medias;\n// - el último paso envía TODO el formulario (el esquema de yup completo vale para todos los pasos);\n// - si el envío falla por un campo de un paso anterior (yup o el 422 del servidor con applyServerProblem), regresa solo a\n// ese paso: un error que no se ve no se puede corregir.\n//\n// Va DENTRO de <FormProvider> y del <form> con su handleSubmit (usa useFormContext):\n// const form = useForm({resolver: yupResolver(schema), defaultValues});\n// <FormProvider {...form}>\n// <form noValidate onSubmit={form.handleSubmit(send)}>\n// <FormWizard submitLabel=\"Enviar solicitud\" steps={[\n// {key: 'datos', label: 'Sus datos', fields: ['name', 'email'], content: <>…campos…</>},\n// {key: 'aval', label: 'Aval', optional: true, fields: ['guarantor'], content: <>…</>},\n// ]} />\n// </form>\n// </FormProvider>\n//\n// Lo que se ve vive en WizardLayout (inputs/). Los textos se cambian con `texts`: {wizard: {next: 'Continuar'}}.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useRef, useState} from 'react';\nimport {useFormContext} from 'react-hook-form';\n\nimport FormProblemAlert from '@/hookForm/FormProblemAlert.jsx';\nimport SubmitButton from '@/hookForm/SubmitButton.jsx';\nimport WizardLayout from '@/inputs/WizardLayout.jsx';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/**\n * @param {object} props\n * @param {Object[]} props.steps en orden\n * @param {string} props.steps[].key\n * @param {string} props.steps[].label ya traducido\n * @param {string[]} [props.steps[].fields] los campos del paso (los que revisa «Siguiente»)\n * @param {boolean} [props.steps[].optional] se puede omitir\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}\n */\nexport default function FormWizard({steps, submitLabel, texts, ...unknownProps}) {\n warnUnknownProps('FormWizard', unknownProps);\n const {trigger, resetField, getFieldState, formState} = useFormContext();\n const {isSubmitting, submitCount} = formState;\n const [activeIndex, setActiveIndex] = useState(0);\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n const activeStep = steps[activeIndex];\n\n // al terminar un envío fallido, regresa al paso con el primer error (solo en ese momento: escribir en otro paso no\n // debe jalar a la persona de regreso). OJO: getFieldState necesita formState para leer los errores de ahora\n useEffect(() => {\n if (wasSubmitting.current && !isSubmitting) {\n const errorStepIndex = steps.findIndex((step) => (step.fields ?? []).some((field) => getFieldState(field, formState).invalid));\n if (errorStepIndex >= 0) {\n setActiveIndex(errorStepIndex);\n }\n }\n wasSubmitting.current = isSubmitting;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isSubmitting, submitCount]);\n\n const goNext = async () => {\n const stepFields = activeStep.fields ?? [];\n // guard clause: el paso tiene errores; se queda para que se corrijan (trigger los marca y los pinta)\n const isStepValid = stepFields.length === 0 || await trigger(stepFields, {shouldFocus: true});\n if (!isStepValid) {\n return;\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys].filter((key) => key !== activeStep.key)));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n const skipStep = () => {\n for (const field of activeStep.fields ?? []) {\n resetField(field);\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys, activeStep.key]));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n return (\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting}\n onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)} onNext={goNext} onSkip={skipStep}\n submitButton={<SubmitButton>{submitLabel}</SubmitButton>} problemAlert={<FormProblemAlert />} texts={texts} />\n );\n}\n"],"names":[],"mappings":";;;;;;;;AA0CA,SAAwB,WAAW,EAAC,OAAO,aAAa,OAAO,GAAG,gBAAe;AAC7E,mBAAiB,cAAc,YAAY;AAC3C,QAAM,EAAC,SAAS,YAAY,eAAe,UAAA,IAAa,eAAA;AACxD,QAAM,EAAC,cAAc,YAAA,IAAe;AACpC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAChD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM,oBAAI,KAAK;AAC9D,QAAM,gBAAgB,OAAO,KAAK;AAClC,QAAM,aAAa,MAAM,WAAW;AAIpC,YAAU,MAAM;AACZ,QAAI,cAAc,WAAW,CAAC,cAAc;AACxC,YAAM,iBAAiB,MAAM,UAAU,CAAC,UAAU,KAAK,UAAU,CAAA,GAAI,KAAK,CAAC,UAAU,cAAc,OAAO,SAAS,EAAE,OAAO,CAAC;AAC7H,UAAI,kBAAkB,GAAG;AACrB,uBAAe,cAAc;AAAA,MACjC;AAAA,IACJ;AACA,kBAAc,UAAU;AAAA,EAE5B,GAAG,CAAC,cAAc,WAAW,CAAC;AAE9B,QAAM,SAAS,YAAY;AACvB,UAAM,aAAa,WAAW,UAAU,CAAA;AAExC,UAAM,cAAc,WAAW,WAAW,KAAK,MAAM,QAAQ,YAAY,EAAC,aAAa,MAAK;AAC5F,QAAI,CAAC,aAAa;AACd;AAAA,IACJ;AACA,mBAAe,CAAC,gBAAgB,IAAI,IAAI,CAAC,GAAG,WAAW,EAAE,OAAO,CAAC,QAAQ,QAAQ,WAAW,GAAG,CAAC,CAAC;AACjG,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,QAAM,WAAW,MAAM;AACnB,eAAW,SAAS,WAAW,UAAU,CAAA,GAAI;AACzC,iBAAW,KAAK;AAAA,IACpB;AACA,mBAAe,CAAC,gBAAgB,oBAAI,IAAI,CAAC,GAAG,aAAa,WAAW,GAAG,CAAC,CAAC;AACzE,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAa;AAAA,MAAc;AAAA,MAA0B;AAAA,MAA0B;AAAA,MAClE,QAAQ,MAAM,eAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAQ,QAAQ;AAAA,MAC1F,cAAc,oBAAC,cAAA,EAAc,UAAA,YAAA,CAAY;AAAA,MAAiB,kCAAe,kBAAA,EAAiB;AAAA,MAAI;AAAA,IAAA;AAAA,EAAA;AAEpH;"}
1
+ {"version":3,"file":"FormWizard.js","sources":["../../src/hookForm/FormWizard.jsx"],"sourcesContent":["// Formulario por pasos (wizard) con react-hook-form: el mismo del de formik (@stackcraftmx/calcita/forms), con las mismas\n// props y el mismo comportamiento:\n// - «Siguiente» revisa SOLO los campos del paso (trigger con sus nombres) y no avanza si alguno tiene error;\n// - «Omitir este paso» (solo en los opcionales) regresa sus campos a su valor de inicio (resetField): omitir es no dar ese\n// dato, no dejar lo que se escribió a medias;\n// - el último paso envía TODO el formulario (el esquema de yup completo vale para todos los pasos);\n// - si el envío falla por un campo de un paso anterior (yup o el 422 del servidor con applyServerProblem), regresa solo a\n// ese paso: un error que no se ve no se puede corregir;\n// - para editar un registro (se entra a corregir un dato suelto): initialStepKey abre en el paso que diga el servidor,\n// orientation=\"vertical\" pone los campos dentro de su paso, y stepSelection deja pulsar el título de otro paso para\n// saltar directo (free) o solo si el paso abierto no tiene errores (checked). onStepChange avisa cada cambio de paso.\n//\n// Va DENTRO de <FormProvider> y del <form> con su handleSubmit (usa useFormContext):\n// const form = useForm({resolver: yupResolver(schema), defaultValues});\n// <FormProvider {...form}>\n// <form noValidate onSubmit={form.handleSubmit(send)}>\n// <FormWizard submitLabel=\"Enviar solicitud\" steps={[\n// {key: 'datos', label: 'Sus datos', fields: ['name', 'email'], content: <>…campos…</>},\n// {key: 'aval', label: 'Aval', optional: true, fields: ['guarantor'], content: <>…</>},\n// ]} />\n// </form>\n// </FormProvider>\n//\n// Lo que se ve vive en WizardLayout (inputs/). Los textos se cambian con `texts`: {wizard: {next: 'Continuar'}}.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useRef, useState} from 'react';\nimport {useFormContext} from 'react-hook-form';\n\nimport FormProblemAlert from '@/hookForm/FormProblemAlert.jsx';\nimport SubmitButton from '@/hookForm/SubmitButton.jsx';\nimport {useWizardSteps} from '@/inputs/useWizardSteps.js';\nimport WizardLayout from '@/inputs/WizardLayout.jsx';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/**\n * @param {object} props\n * @param {Object[]} props.steps en orden\n * @param {string} props.steps[].key\n * @param {string} props.steps[].label ya traducido\n * @param {string[]} [props.steps[].fields] los campos del paso (los que revisa «Siguiente»)\n * @param {boolean} [props.steps[].optional] se puede omitir\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {boolean} [props.steps[].completed] lleva la palomita (cuando los títulos se pulsan, solo si la pantalla lo dice)\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {'horizontal'|'vertical'} [props.orientation] uno de WIZARD_ORIENTATION; por omisión horizontal\n * @param {string} [props.initialStepKey] la llave del paso en el que se abre (la que diga el servidor); por omisión el primero\n * @param {'none'|'free'|'checked'} [props.stepSelection] uno de WIZARD_STEP_SELECTION: si el título de otro paso se pulsa y qué\n * revisa antes; por omisión none (se avanza con «Siguiente», como siempre)\n * @param {(stepKey: string, index: number) => void} [props.onStepChange] cada vez que cambia el paso abierto\n * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}\n */\nexport default function FormWizard({\n steps, submitLabel, orientation, initialStepKey, stepSelection, onStepChange, texts, ...unknownProps\n}) {\n warnUnknownProps('FormWizard', unknownProps);\n const {trigger, resetField, getFieldState, formState} = useFormContext();\n const {isSubmitting, submitCount} = formState;\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n\n // revisa SOLO los campos de un paso (trigger los marca, los pinta y pone el foco en el primero con error)\n const isStepValid = async (index) => {\n const stepFields = steps[index].fields ?? [];\n return stepFields.length === 0 || trigger(stepFields, {shouldFocus: true});\n };\n const {activeIndex, setActiveIndex, selectStep} = useWizardSteps({steps, initialStepKey, stepSelection, onStepChange, isStepValid});\n const activeStep = steps[activeIndex];\n\n // al terminar un envío fallido, regresa al paso con el primer error (solo en ese momento: escribir en otro paso no\n // debe jalar a la persona de regreso). OJO: getFieldState necesita formState para leer los errores de ahora\n useEffect(() => {\n if (wasSubmitting.current && !isSubmitting) {\n const errorStepIndex = steps.findIndex((step) => (step.fields ?? []).some((field) => getFieldState(field, formState).invalid));\n if (errorStepIndex >= 0) {\n setActiveIndex(errorStepIndex);\n }\n }\n wasSubmitting.current = isSubmitting;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isSubmitting, submitCount]);\n\n const goNext = async () => {\n // el paso tiene errores: se queda ahí para que se corrijan\n if (!(await isStepValid(activeIndex))) {\n return;\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys].filter((key) => key !== activeStep.key)));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n const skipStep = () => {\n for (const field of activeStep.fields ?? []) {\n resetField(field);\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys, activeStep.key]));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n return (\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting} orientation={orientation}\n onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)} onNext={goNext} onSkip={skipStep} onStepSelect={selectStep}\n submitButton={<SubmitButton>{submitLabel}</SubmitButton>} problemAlert={<FormProblemAlert />} texts={texts} />\n );\n}\n"],"names":[],"mappings":";;;;;;;;;AAoDA,SAAwB,WAAW;AAAA,EAC/B;AAAA,EAAO;AAAA,EAAa;AAAA,EAAa;AAAA,EAAgB;AAAA,EAAe;AAAA,EAAc;AAAA,EAAO,GAAG;AAC5F,GAAG;AACC,mBAAiB,cAAc,YAAY;AAC3C,QAAM,EAAC,SAAS,YAAY,eAAe,UAAA,IAAa,eAAA;AACxD,QAAM,EAAC,cAAc,YAAA,IAAe;AACpC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM,oBAAI,KAAK;AAC9D,QAAM,gBAAgB,OAAO,KAAK;AAGlC,QAAM,cAAc,OAAO,UAAU;AACjC,UAAM,aAAa,MAAM,KAAK,EAAE,UAAU,CAAA;AAC1C,WAAO,WAAW,WAAW,KAAK,QAAQ,YAAY,EAAC,aAAa,MAAK;AAAA,EAC7E;AACA,QAAM,EAAC,aAAa,gBAAgB,WAAA,IAAc,eAAe,EAAC,OAAO,gBAAgB,eAAe,cAAc,YAAA,CAAY;AAClI,QAAM,aAAa,MAAM,WAAW;AAIpC,YAAU,MAAM;AACZ,QAAI,cAAc,WAAW,CAAC,cAAc;AACxC,YAAM,iBAAiB,MAAM,UAAU,CAAC,UAAU,KAAK,UAAU,CAAA,GAAI,KAAK,CAAC,UAAU,cAAc,OAAO,SAAS,EAAE,OAAO,CAAC;AAC7H,UAAI,kBAAkB,GAAG;AACrB,uBAAe,cAAc;AAAA,MACjC;AAAA,IACJ;AACA,kBAAc,UAAU;AAAA,EAE5B,GAAG,CAAC,cAAc,WAAW,CAAC;AAE9B,QAAM,SAAS,YAAY;AAEvB,QAAI,CAAE,MAAM,YAAY,WAAW,GAAI;AACnC;AAAA,IACJ;AACA,mBAAe,CAAC,gBAAgB,IAAI,IAAI,CAAC,GAAG,WAAW,EAAE,OAAO,CAAC,QAAQ,QAAQ,WAAW,GAAG,CAAC,CAAC;AACjG,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,QAAM,WAAW,MAAM;AACnB,eAAW,SAAS,WAAW,UAAU,CAAA,GAAI;AACzC,iBAAW,KAAK;AAAA,IACpB;AACA,mBAAe,CAAC,gBAAgB,oBAAI,IAAI,CAAC,GAAG,aAAa,WAAW,GAAG,CAAC,CAAC;AACzE,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAa;AAAA,MAAc;AAAA,MAA0B;AAAA,MAA0B;AAAA,MAA4B;AAAA,MAC9F,QAAQ,MAAM,eAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAQ,QAAQ;AAAA,MAAU,cAAc;AAAA,MAClH,cAAc,oBAAC,cAAA,EAAc,UAAA,YAAA,CAAY;AAAA,MAAiB,kCAAe,kBAAA,EAAiB;AAAA,MAAI;AAAA,IAAA;AAAA,EAAA;AAEpH;"}
package/dist/index.js CHANGED
@@ -95,7 +95,7 @@ import { default as default47 } from "./ui/navigation/PageBreadcrumbs.js";
95
95
  import { ROUTE_TABS_MODE, default as default48, useActiveRouteTab } from "./ui/navigation/RouteTabs.js";
96
96
  import { default as default49 } from "./ui/help/HelpButton.js";
97
97
  import { HELP_CLOSE_REASON, default as default50 } from "./ui/help/HelpCarouselDialog.js";
98
- import { default as default51 } from "./ui/cards/RecordCard.js";
98
+ import { RECORD_CARD_FOOTER_ALIGN, default as default51 } from "./ui/cards/RecordCard.js";
99
99
  import { default as default52 } from "./ui/cards/RecordCardSkeleton.js";
100
100
  import { useHelpCarousel } from "./ui/help/useHelpCarousel.js";
101
101
  import { default as default53 } from "./ui/filters/DateRangeFilter.js";
@@ -129,6 +129,7 @@ import { default as default76 } from "./inputs/RepeatingFields.js";
129
129
  import { useRowKeys } from "./inputs/useRowKeys.js";
130
130
  import { default as default77 } from "./inputs/SelectInput.js";
131
131
  import { default as default78 } from "./inputs/WizardLayout.js";
132
+ import { WIZARD_ORIENTATION, WIZARD_STEP_SELECTION, isStepCompleted, openingStepOf, stepSelectionActionOf } from "./inputs/wizardSteps.js";
132
133
  import { default as default79 } from "./inputs/TextInput.js";
133
134
  import { HEX_COLOR, MIN_CONTRAST, contrastOnWhite, readableVariantOf } from "./inputs/colorContrast.js";
134
135
  import { cleanNumberText, completeDecimals, groupThousands } from "./inputs/numberText.js";
@@ -294,6 +295,7 @@ export {
294
295
  default21 as PermissionMatrix,
295
296
  default39 as ProfileHeader,
296
297
  default56 as QuickFilters,
298
+ RECORD_CARD_FOOTER_ALIGN,
297
299
  REDIRECT_PARAM,
298
300
  ROUTE_TABS_MODE,
299
301
  default51 as RecordCard,
@@ -329,6 +331,8 @@ export {
329
331
  ToolkitProvider,
330
332
  default75 as TransferListInput,
331
333
  UNKNOWN_FILE_TYPE,
334
+ WIZARD_ORIENTATION,
335
+ WIZARD_STEP_SELECTION,
332
336
  default78 as WizardLayout,
333
337
  XLSX_CONTENT_TYPE,
334
338
  accentTextStylesOf,
@@ -415,6 +419,7 @@ export {
415
419
  isPasswordAccepted,
416
420
  isPlainLeftClick,
417
421
  isStaleChunkError,
422
+ isStepCompleted,
418
423
  labelOf,
419
424
  lookup,
420
425
  maskPatternOf,
@@ -436,6 +441,7 @@ export {
436
441
  mobileTabsOf,
437
442
  newIdentifier,
438
443
  normalizedValidationMessage,
444
+ openingStepOf,
439
445
  pageNameAt,
440
446
  parentMenuItemOf,
441
447
  parseFileSize,
@@ -475,6 +481,7 @@ export {
475
481
  stackcraftDarkPalette,
476
482
  stackcraftLightPalette,
477
483
  startDownload,
484
+ stepSelectionActionOf,
478
485
  sxListOf,
479
486
  tableRowTextOf,
480
487
  toastDefault,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -5,11 +5,15 @@ import Box from "@mui/material/Box";
5
5
  import Button from "@mui/material/Button";
6
6
  import Stack from "@mui/material/Stack";
7
7
  import Step from "@mui/material/Step";
8
+ import StepButton from "@mui/material/StepButton";
9
+ import StepContent from "@mui/material/StepContent";
8
10
  import StepLabel from "@mui/material/StepLabel";
9
11
  import Stepper from "@mui/material/Stepper";
10
12
  import Typography from "@mui/material/Typography";
13
+ import { WIZARD_ORIENTATION, isStepCompleted } from "./wizardSteps.js";
11
14
  import { piecePart, withPieceSlots, usePieceProps } from "../theme/themePieces.js";
12
15
  import { warnUnknownProps } from "../toolkit/unknownProps.js";
16
+ const NO_SKIPPED_KEYS = /* @__PURE__ */ new Set();
13
17
  const THEME_NAME = "CalcitaWizardLayout";
14
18
  const WizardStepper = piecePart(THEME_NAME, "Stepper", Stepper);
15
19
  const WizardPhoneSteps = piecePart(THEME_NAME, "PhoneSteps", Box);
@@ -18,11 +22,13 @@ function WizardLayoutContent(props) {
18
22
  const {
19
23
  steps,
20
24
  activeIndex,
21
- skippedKeys,
25
+ skippedKeys = NO_SKIPPED_KEYS,
22
26
  isSubmitting = false,
27
+ orientation = WIZARD_ORIENTATION.horizontal,
23
28
  onBack,
24
29
  onNext,
25
30
  onSkip,
31
+ onStepSelect,
26
32
  submitButton,
27
33
  problemAlert,
28
34
  texts,
@@ -32,29 +38,51 @@ function WizardLayoutContent(props) {
32
38
  const { t } = useToolkitTranslation("common", texts);
33
39
  const activeStep = steps[activeIndex];
34
40
  const isLastStep = activeIndex === steps.length - 1;
41
+ const isVertical = orientation === WIZARD_ORIENTATION.vertical;
42
+ const isSelectable = typeof onStepSelect === "function";
43
+ const stepTitleOf = (step, index) => {
44
+ let caption;
45
+ if (skippedKeys.has(step.key)) {
46
+ caption = t("wizard.skipped");
47
+ } else if (step.optional) {
48
+ caption = t("wizard.optional");
49
+ }
50
+ const optional = caption && /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", children: caption });
51
+ if (isSelectable) {
52
+ return /* @__PURE__ */ jsx(StepButton, { optional, onClick: () => onStepSelect(index), children: step.label });
53
+ }
54
+ return /* @__PURE__ */ jsx(StepLabel, { optional, children: step.label });
55
+ };
56
+ const actions = /* @__PURE__ */ jsxs(WizardActions, { direction: { xs: "column-reverse", sm: "row" }, spacing: 1.5, ownSx: { justifyContent: "space-between" }, children: [
57
+ /* @__PURE__ */ jsx(Button, { variant: "text", color: "inherit", disabled: activeIndex === 0 || isSubmitting, onClick: onBack, children: t("wizard.back") }),
58
+ /* @__PURE__ */ jsxs(Stack, { direction: { xs: "column-reverse", sm: "row" }, spacing: 1.5, children: [
59
+ activeStep.optional && !isLastStep && onSkip && /* @__PURE__ */ jsx(Button, { variant: "text", onClick: onSkip, children: t("wizard.skip") }),
60
+ isLastStep ? submitButton : /* @__PURE__ */ jsx(Button, { variant: "contained", size: "large", onClick: onNext, children: t("wizard.next") })
61
+ ] })
62
+ ] });
63
+ const completedOf = (step, index) => isStepCompleted(step, { index, activeIndex, skippedKeys, isSelectable });
64
+ if (isVertical) {
65
+ return (
66
+ // nonLinear: con los títulos que se pulsan, MUI no debe apagar los pasos de adelante
67
+ /* @__PURE__ */ jsx(WizardStepper, { activeStep: activeIndex, orientation: "vertical", nonLinear: isSelectable, children: steps.map((step, index) => /* @__PURE__ */ jsxs(Step, { completed: completedOf(step, index), children: [
68
+ stepTitleOf(step, index),
69
+ /* @__PURE__ */ jsx(StepContent, { slotProps: { transition: { unmountOnExit: true } }, children: /* @__PURE__ */ jsxs(Stack, { spacing: 3, sx: { pt: 1 }, children: [
70
+ /* @__PURE__ */ jsx(Box, { children: step.content }),
71
+ index === activeIndex && isLastStep && problemAlert,
72
+ index === activeIndex && actions
73
+ ] }) })
74
+ ] }, step.key)) })
75
+ );
76
+ }
35
77
  return /* @__PURE__ */ jsxs(Stack, { spacing: 3, children: [
36
- /* @__PURE__ */ jsx(WizardStepper, { activeStep: activeIndex, alternativeLabel: true, ownSx: { display: { xs: "none", sm: "flex" } }, children: steps.map((step) => {
37
- let caption;
38
- if (skippedKeys.has(step.key)) {
39
- caption = t("wizard.skipped");
40
- } else if (step.optional) {
41
- caption = t("wizard.optional");
42
- }
43
- return /* @__PURE__ */ jsx(Step, { completed: skippedKeys.has(step.key) ? false : void 0, children: /* @__PURE__ */ jsx(StepLabel, { optional: caption && /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", children: caption }), children: step.label }) }, step.key);
44
- }) }),
78
+ /* @__PURE__ */ jsx(WizardStepper, { activeStep: activeIndex, alternativeLabel: true, nonLinear: isSelectable, ownSx: { display: { xs: "none", sm: "flex" } }, children: steps.map((step, index) => /* @__PURE__ */ jsx(Step, { completed: completedOf(step, index), children: stepTitleOf(step, index) }, step.key)) }),
45
79
  /* @__PURE__ */ jsxs(WizardPhoneSteps, { ownSx: { display: { xs: "block", sm: "none" } }, children: [
46
80
  /* @__PURE__ */ jsx(Typography, { variant: "overline", color: "text.secondary", children: t("wizard.stepOf", { current: activeIndex + 1, total: steps.length }) }),
47
81
  /* @__PURE__ */ jsx(Typography, { variant: "h6", children: activeStep.label })
48
82
  ] }),
49
83
  /* @__PURE__ */ jsx(Box, { children: activeStep.content }),
50
84
  isLastStep && problemAlert,
51
- /* @__PURE__ */ jsxs(WizardActions, { direction: { xs: "column-reverse", sm: "row" }, spacing: 1.5, ownSx: { justifyContent: "space-between" }, children: [
52
- /* @__PURE__ */ jsx(Button, { variant: "text", color: "inherit", disabled: activeIndex === 0 || isSubmitting, onClick: onBack, children: t("wizard.back") }),
53
- /* @__PURE__ */ jsxs(Stack, { direction: { xs: "column-reverse", sm: "row" }, spacing: 1.5, children: [
54
- activeStep.optional && !isLastStep && /* @__PURE__ */ jsx(Button, { variant: "text", onClick: onSkip, children: t("wizard.skip") }),
55
- isLastStep ? submitButton : /* @__PURE__ */ jsx(Button, { variant: "contained", size: "large", onClick: onNext, children: t("wizard.next") })
56
- ] })
57
- ] })
85
+ actions
58
86
  ] });
59
87
  }
60
88
  const WizardLayout = withPieceSlots(WizardLayoutContent, THEME_NAME);
@@ -1 +1 @@
1
- {"version":3,"file":"WizardLayout.js","sources":["../../src/inputs/WizardLayout.jsx"],"sourcesContent":["// Lo que se ve de un formulario por pasos: los pasos arriba (en el teléfono, «Paso 2 de 4» y su nombre), los campos del paso\n// y los botones (Anterior · Omitir este paso · Siguiente o Enviar). No sabe de formik ni de react-hook-form: el FormWizard de\n// cada uno (/forms y /react-hook-form) pone la lógica (qué revisa «Siguiente», qué hace «Omitir») y le pasa su botón de\n// enviar y su aviso de errores. Suelto sirve para armar un asistente propio con otra librería.\n//\n// Los textos se cambian con `texts`, con la forma de `common` en los textos del paquete: {wizard: {next: 'Continuar'}}.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaWizardLayout con defaultProps y\n// styleOverrides de sus partes: stepper, phoneSteps, actions.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Stack from '@mui/material/Stack';\nimport Step from '@mui/material/Step';\nimport StepLabel from '@mui/material/StepLabel';\nimport Stepper from '@mui/material/Stepper';\nimport Typography from '@mui/material/Typography';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaWizardLayout)\nconst THEME_NAME = 'CalcitaWizardLayout';\nconst WizardStepper = piecePart(THEME_NAME, 'Stepper', Stepper);\nconst WizardPhoneSteps = piecePart(THEME_NAME, 'PhoneSteps', Box);\nconst WizardActions = piecePart(THEME_NAME, 'Actions', Stack);\n\n/**\n * @param {object} props\n * @param {Object[]} props.steps en orden\n * @param {string} props.steps[].key\n * @param {string} props.steps[].label ya traducido\n * @param {boolean} [props.steps[].optional] se puede omitir\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {number} props.activeIndex el paso que se ve\n * @param {Set<string>} props.skippedKeys los pasos omitidos\n * @param {boolean} [props.isSubmitting] mientras se envía, «Anterior» se apaga\n * @param {() => void} props.onBack\n * @param {() => void} props.onNext\n * @param {() => void} props.onSkip\n * @param {import('react').ReactNode} props.submitButton el botón de enviar del último paso\n * @param {import('react').ReactNode} [props.problemAlert] el aviso de errores del servidor, arriba de los botones del último paso\n * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}\n */\nfunction WizardLayoutContent(props) {\n // lo que el proyecto puso en su tema (components.CalcitaWizardLayout.defaultProps) y encima lo que llega aquí\n const {\n steps, activeIndex, skippedKeys, isSubmitting = false, onBack, onNext, onSkip, submitButton, problemAlert, texts,\n ...unknownProps\n} = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const activeStep = steps[activeIndex];\n const isLastStep = activeIndex === steps.length - 1;\n\n return (\n <Stack spacing={3}>\n {/* en el teléfono los nombres de los pasos no caben: se dice en qué paso va */}\n <WizardStepper activeStep={activeIndex} alternativeLabel ownSx={{display: {xs: 'none', sm: 'flex'}}}>\n {steps.map((step) => {\n let caption;\n if (skippedKeys.has(step.key)) {\n caption = t('wizard.skipped');\n } else if (step.optional) {\n caption = t('wizard.optional');\n }\n return (\n <Step key={step.key} completed={skippedKeys.has(step.key) ? false : undefined}>\n <StepLabel optional={caption && <Typography variant=\"caption\" color=\"text.secondary\">{caption}</Typography>}>\n {step.label}\n </StepLabel>\n </Step>\n );\n })}\n </WizardStepper>\n <WizardPhoneSteps ownSx={{display: {xs: 'block', sm: 'none'}}}>\n <Typography variant=\"overline\" color=\"text.secondary\">{t('wizard.stepOf', {current: activeIndex + 1, total: steps.length})}</Typography>\n <Typography variant=\"h6\">{activeStep.label}</Typography>\n </WizardPhoneSteps>\n\n <Box>{activeStep.content}</Box>\n\n {isLastStep && problemAlert}\n\n <WizardActions direction={{xs: 'column-reverse', sm: 'row'}} spacing={1.5} ownSx={{justifyContent: 'space-between'}}>\n <Button variant=\"text\" color=\"inherit\" disabled={activeIndex === 0 || isSubmitting} onClick={onBack}>\n {t('wizard.back')}\n </Button>\n <Stack direction={{xs: 'column-reverse', sm: 'row'}} spacing={1.5}>\n {activeStep.optional && !isLastStep && (\n <Button variant=\"text\" onClick={onSkip}>{t('wizard.skip')}</Button>\n )}\n {isLastStep\n ? submitButton\n : <Button variant=\"contained\" size=\"large\" onClick={onNext}>{t('wizard.next')}</Button>}\n </Stack>\n </WizardActions>\n </Stack>\n );\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst WizardLayout = withPieceSlots(WizardLayoutContent, THEME_NAME);\nexport default WizardLayout;\n"],"names":[],"mappings":";;;;;;;;;;;;AAuBA,MAAM,aAAa;AACnB,MAAM,gBAAgB,UAAU,YAAY,WAAW,OAAO;AAC9D,MAAM,mBAAmB,UAAU,YAAY,cAAc,GAAG;AAChE,MAAM,gBAAgB,UAAU,YAAY,WAAW,KAAK;AAmB5D,SAAS,oBAAoB,OAAO;AAEhC,QAAM;AAAA,IACN;AAAA,IAAO;AAAA,IAAa;AAAA,IAAa,eAAe;AAAA,IAAO;AAAA,IAAQ;AAAA,IAAQ;AAAA,IAAQ;AAAA,IAAc;AAAA,IAAc;AAAA,IAC3G,GAAG;AAAA,EAAA,IACH,cAAc,OAAO,UAAU;AAC/B,mBAAiB,YAAY,YAAY;AACzC,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AACjD,QAAM,aAAa,MAAM,WAAW;AACpC,QAAM,aAAa,gBAAgB,MAAM,SAAS;AAElD,SACI,qBAAC,OAAA,EAAM,SAAS,GAEZ,UAAA;AAAA,IAAA,oBAAC,iBAAc,YAAY,aAAa,kBAAgB,MAAC,OAAO,EAAC,SAAS,EAAC,IAAI,QAAQ,IAAI,OAAA,EAAM,GAC5F,UAAA,MAAM,IAAI,CAAC,SAAS;AACjB,UAAI;AACJ,UAAI,YAAY,IAAI,KAAK,GAAG,GAAG;AAC3B,kBAAU,EAAE,gBAAgB;AAAA,MAChC,WAAW,KAAK,UAAU;AACtB,kBAAU,EAAE,iBAAiB;AAAA,MACjC;AACA,aACI,oBAAC,MAAA,EAAoB,WAAW,YAAY,IAAI,KAAK,GAAG,IAAI,QAAQ,QAChE,UAAA,oBAAC,WAAA,EAAU,UAAU,WAAW,oBAAC,YAAA,EAAW,SAAQ,WAAU,OAAM,kBAAkB,UAAA,QAAA,CAAQ,GACzF,UAAA,KAAK,MAAA,CACV,EAAA,GAHO,KAAK,GAIhB;AAAA,IAER,CAAC,EAAA,CACL;AAAA,IACA,qBAAC,kBAAA,EAAiB,OAAO,EAAC,SAAS,EAAC,IAAI,SAAS,IAAI,OAAA,EAAM,GACvD,UAAA;AAAA,MAAA,oBAAC,YAAA,EAAW,SAAQ,YAAW,OAAM,kBAAkB,UAAA,EAAE,iBAAiB,EAAC,SAAS,cAAc,GAAG,OAAO,MAAM,OAAA,CAAO,GAAE;AAAA,MAC3H,oBAAC,YAAA,EAAW,SAAQ,MAAM,qBAAW,MAAA,CAAM;AAAA,IAAA,GAC/C;AAAA,IAEA,oBAAC,KAAA,EAAK,UAAA,WAAW,QAAA,CAAQ;AAAA,IAExB,cAAc;AAAA,IAEf,qBAAC,eAAA,EAAc,WAAW,EAAC,IAAI,kBAAkB,IAAI,MAAA,GAAQ,SAAS,KAAK,OAAO,EAAC,gBAAgB,mBAC/F,UAAA;AAAA,MAAA,oBAAC,QAAA,EAAO,SAAQ,QAAO,OAAM,WAAU,UAAU,gBAAgB,KAAK,cAAc,SAAS,QACxF,UAAA,EAAE,aAAa,GACpB;AAAA,MACA,qBAAC,OAAA,EAAM,WAAW,EAAC,IAAI,kBAAkB,IAAI,MAAA,GAAQ,SAAS,KACzD,UAAA;AAAA,QAAA,WAAW,YAAY,CAAC,cACrB,oBAAC,QAAA,EAAO,SAAQ,QAAO,SAAS,QAAS,UAAA,EAAE,aAAa,EAAA,CAAE;AAAA,QAE7D,aACK,eACA,oBAAC,QAAA,EAAO,SAAQ,aAAY,MAAK,SAAQ,SAAS,QAAS,UAAA,EAAE,aAAa,EAAA,CAAE;AAAA,MAAA,EAAA,CACtF;AAAA,IAAA,EAAA,CACJ;AAAA,EAAA,GACJ;AAER;AAGA,MAAM,eAAe,eAAe,qBAAqB,UAAU;"}
1
+ {"version":3,"file":"WizardLayout.js","sources":["../../src/inputs/WizardLayout.jsx"],"sourcesContent":["// Lo que se ve de un formulario por pasos: los pasos, los campos del paso abierto y los botones (Anterior · Omitir este paso ·\n// Siguiente o Enviar). No sabe de formik ni de react-hook-form: el FormWizard de cada uno (/forms y /react-hook-form) pone la\n// lógica (qué revisa «Siguiente», qué hace «Omitir», si se puede saltar a otro paso) y le pasa su botón de enviar y su aviso de\n// errores. Suelto sirve para armar un asistente propio con otra librería.\n//\n// Dos acomodos (WIZARD_ORIENTATION):\n// horizontal los pasos en fila arriba y los campos abajo; en el teléfono no caben los nombres: «Paso 2 de 4» y su nombre;\n// vertical uno debajo del otro, como un acordeón: los campos y los botones van dentro del paso abierto (también en el\n// teléfono). Es el de editar un expediente: se ve de un vistazo qué pasos hay y se entra al que hace falta.\n//\n// Con onStepSelect los títulos de los pasos se pulsan (con teclado también: son botones) y avisan qué paso se pidió; quien lo\n// usa decide si salta. Sin él, los títulos solo informan (lo de siempre). Omitir es opcional: sin onSkip no sale el botón.\n//\n// Los textos se cambian con `texts`, con la forma de `common` en los textos del paquete: {wizard: {next: 'Continuar'}}.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaWizardLayout con defaultProps y\n// styleOverrides de sus partes: stepper, phoneSteps, actions.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Stack from '@mui/material/Stack';\nimport Step from '@mui/material/Step';\nimport StepButton from '@mui/material/StepButton';\nimport StepContent from '@mui/material/StepContent';\nimport StepLabel from '@mui/material/StepLabel';\nimport Stepper from '@mui/material/Stepper';\nimport Typography from '@mui/material/Typography';\nimport {WIZARD_ORIENTATION, isStepCompleted} from '@/inputs/wizardSteps.js';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// sin pasos omitidos (quien no usa «Omitir» no tiene que mandar nada)\nconst NO_SKIPPED_KEYS = new Set();\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaWizardLayout)\nconst THEME_NAME = 'CalcitaWizardLayout';\nconst WizardStepper = piecePart(THEME_NAME, 'Stepper', Stepper);\nconst WizardPhoneSteps = piecePart(THEME_NAME, 'PhoneSteps', Box);\nconst WizardActions = piecePart(THEME_NAME, 'Actions', Stack);\n\n/**\n * @param {object} props\n * @param {Object[]} props.steps en orden\n * @param {string} props.steps[].key\n * @param {string} props.steps[].label ya traducido\n * @param {boolean} [props.steps[].optional] se puede omitir\n * @param {boolean} [props.steps[].completed] lleva la palomita; por omisión, en fila y sin pulsar, los de antes del abierto\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {number} props.activeIndex el paso que se ve\n * @param {Set<string>} [props.skippedKeys] los pasos omitidos\n * @param {boolean} [props.isSubmitting] mientras se envía, «Anterior» se apaga\n * @param {'horizontal'|'vertical'} [props.orientation] uno de WIZARD_ORIENTATION; por omisión horizontal\n * @param {() => void} props.onBack\n * @param {() => void} props.onNext\n * @param {() => void} [props.onSkip] sin él no sale «Omitir este paso»\n * @param {(index: number) => void} [props.onStepSelect] con él, los títulos de los pasos se pulsan y avisan cuál se pidió\n * @param {import('react').ReactNode} props.submitButton el botón de enviar del último paso\n * @param {import('react').ReactNode} [props.problemAlert] el aviso de errores del servidor, arriba de los botones del último paso\n * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}\n */\nfunction WizardLayoutContent(props) {\n // lo que el proyecto puso en su tema (components.CalcitaWizardLayout.defaultProps) y encima lo que llega aquí\n const {\n steps, activeIndex, skippedKeys = NO_SKIPPED_KEYS, isSubmitting = false, orientation = WIZARD_ORIENTATION.horizontal,\n onBack, onNext, onSkip, onStepSelect, submitButton, problemAlert, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const activeStep = steps[activeIndex];\n const isLastStep = activeIndex === steps.length - 1;\n const isVertical = orientation === WIZARD_ORIENTATION.vertical;\n const isSelectable = typeof onStepSelect === 'function';\n\n // el título de un paso: con onStepSelect es un botón (se pulsa y se llega con Tab); sin él, solo informa\n const stepTitleOf = (step, index) => {\n let caption;\n if (skippedKeys.has(step.key)) {\n caption = t('wizard.skipped');\n } else if (step.optional) {\n caption = t('wizard.optional');\n }\n const optional = caption && <Typography variant=\"caption\" color=\"text.secondary\">{caption}</Typography>;\n if (isSelectable) {\n return <StepButton optional={optional} onClick={() => onStepSelect(index)}>{step.label}</StepButton>;\n }\n return <StepLabel optional={optional}>{step.label}</StepLabel>;\n };\n\n // los botones del paso abierto: en fila van abajo de los campos; en vertical, dentro del paso\n const actions = (\n <WizardActions direction={{xs: 'column-reverse', sm: 'row'}} spacing={1.5} ownSx={{justifyContent: 'space-between'}}>\n <Button variant=\"text\" color=\"inherit\" disabled={activeIndex === 0 || isSubmitting} onClick={onBack}>\n {t('wizard.back')}\n </Button>\n <Stack direction={{xs: 'column-reverse', sm: 'row'}} spacing={1.5}>\n {activeStep.optional && !isLastStep && onSkip && (\n <Button variant=\"text\" onClick={onSkip}>{t('wizard.skip')}</Button>\n )}\n {isLastStep\n ? submitButton\n : <Button variant=\"contained\" size=\"large\" onClick={onNext}>{t('wizard.next')}</Button>}\n </Stack>\n </WizardActions>\n );\n const completedOf = (step, index) => isStepCompleted(step, {index, activeIndex, skippedKeys, isSelectable});\n\n if (isVertical) {\n return (\n // nonLinear: con los títulos que se pulsan, MUI no debe apagar los pasos de adelante\n <WizardStepper activeStep={activeIndex} orientation=\"vertical\" nonLinear={isSelectable}>\n {steps.map((step, index) => (\n <Step key={step.key} completed={completedOf(step, index)}>\n {stepTitleOf(step, index)}\n {/* solo el paso abierto tiene sus campos montados: al cerrarse se desmontan (unmountOnExit). Los botones\n van solo en el abierto: el que se cierra (la animación dura un momento) ya no es el paso de esos\n botones, y un «Guardar» repetido confunde */}\n <StepContent slotProps={{transition: {unmountOnExit: true}}}>\n <Stack spacing={3} sx={{pt: 1}}>\n <Box>{step.content}</Box>\n {index === activeIndex && isLastStep && problemAlert}\n {index === activeIndex && actions}\n </Stack>\n </StepContent>\n </Step>\n ))}\n </WizardStepper>\n );\n }\n\n return (\n <Stack spacing={3}>\n {/* en el teléfono los nombres de los pasos no caben: se dice en qué paso va */}\n <WizardStepper activeStep={activeIndex} alternativeLabel nonLinear={isSelectable} ownSx={{display: {xs: 'none', sm: 'flex'}}}>\n {steps.map((step, index) => (\n <Step key={step.key} completed={completedOf(step, index)}>\n {stepTitleOf(step, index)}\n </Step>\n ))}\n </WizardStepper>\n <WizardPhoneSteps ownSx={{display: {xs: 'block', sm: 'none'}}}>\n <Typography variant=\"overline\" color=\"text.secondary\">{t('wizard.stepOf', {current: activeIndex + 1, total: steps.length})}</Typography>\n <Typography variant=\"h6\">{activeStep.label}</Typography>\n </WizardPhoneSteps>\n\n <Box>{activeStep.content}</Box>\n\n {isLastStep && problemAlert}\n\n {actions}\n </Stack>\n );\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst WizardLayout = withPieceSlots(WizardLayoutContent, THEME_NAME);\nexport default WizardLayout;\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ "use client";
2
+ import { useState, useRef, useEffect } from "react";
3
+ import { openingStepOf, WIZARD_STEP_SELECTION, stepSelectionActionOf } from "./wizardSteps.js";
4
+ import { createLogger } from "../observability/logger.js";
5
+ const logger = createLogger("FormWizard");
6
+ function useWizardSteps({ steps, initialStepKey, stepSelection = WIZARD_STEP_SELECTION.none, onStepChange, isStepValid }) {
7
+ const [opening] = useState(() => openingStepOf(steps, initialStepKey));
8
+ const [activeIndex, setActiveIndex] = useState(opening.index);
9
+ const announcedIndex = useRef(opening.index);
10
+ useEffect(() => {
11
+ if (opening.isUnknownKey) {
12
+ logger.warn(`initialStepKey «${initialStepKey}» no es de ningún paso: se abrió el primero`, { stepKeys: steps.map((step) => step.key) });
13
+ }
14
+ }, []);
15
+ useEffect(() => {
16
+ if (announcedIndex.current !== activeIndex) {
17
+ announcedIndex.current = activeIndex;
18
+ onStepChange?.(steps[activeIndex].key, activeIndex);
19
+ }
20
+ }, [activeIndex, onStepChange, steps]);
21
+ const selectStep = async (index) => {
22
+ const action = stepSelectionActionOf(stepSelection, activeIndex, index);
23
+ if (action === "ignore") {
24
+ return;
25
+ }
26
+ if (action === "checkFirst" && !await isStepValid(activeIndex)) {
27
+ return;
28
+ }
29
+ setActiveIndex(index);
30
+ };
31
+ return { activeIndex, setActiveIndex, selectStep: stepSelection === WIZARD_STEP_SELECTION.none ? void 0 : selectStep };
32
+ }
33
+ export {
34
+ useWizardSteps
35
+ };
36
+ //# sourceMappingURL=useWizardSteps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useWizardSteps.js","sources":["../../src/inputs/useWizardSteps.js"],"sourcesContent":["// Lo que los dos FormWizard (formik y react-hook-form) hacen igual con el paso abierto: en qué paso empieza, avisar hacia\n// afuera cada vez que cambia y qué pasa al pulsar el título de otro paso. Lo de cada librería (cómo se revisa un paso) llega\n// en isStepValid.\n//\n// Hook genérico: function (regla de la casa).\nimport {useEffect, useRef, useState} from 'react';\n\nimport {WIZARD_STEP_SELECTION, openingStepOf, stepSelectionActionOf} from '@/inputs/wizardSteps.js';\nimport {createLogger} from '@/observability/logger.js';\n\nconst logger = createLogger('FormWizard');\n\n/**\n * @param {object} options\n * @param {Array<{key: string}>} options.steps\n * @param {string} [options.initialStepKey] el paso en el que se abre (el que diga el servidor); por omisión el primero\n * @param {string} [options.stepSelection] uno de WIZARD_STEP_SELECTION; por omisión none\n * @param {(stepKey: string, index: number) => void} [options.onStepChange] cada vez que cambia el paso abierto\n * @param {(index: number) => Promise<boolean>} options.isStepValid revisa los campos de ese paso y marca sus errores\n * @returns {{activeIndex: number, setActiveIndex: import('react').Dispatch<import('react').SetStateAction<number>>, selectStep: ((index: number) => Promise<void>)|undefined}}\n * selectStep es undefined con none: así WizardLayout sabe que los títulos no se pulsan\n */\nexport function useWizardSteps({steps, initialStepKey, stepSelection = WIZARD_STEP_SELECTION.none, onStepChange, isStepValid}) {\n const [opening] = useState(() => openingStepOf(steps, initialStepKey));\n const [activeIndex, setActiveIndex] = useState(opening.index);\n const announcedIndex = useRef(opening.index);\n\n // una llave que no es de ningún paso no tumba la pantalla: se abre el primero y se dice\n useEffect(() => {\n if (opening.isUnknownKey) {\n logger.warn(`initialStepKey «${initialStepKey}» no es de ningún paso: se abrió el primero`, {stepKeys: steps.map((step) => step.key)});\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // hacia afuera, solo cuando de veras cambia (no al abrir): la pantalla puede guardar el paso en la dirección\n useEffect(() => {\n if (announcedIndex.current !== activeIndex) {\n announcedIndex.current = activeIndex;\n onStepChange?.(steps[activeIndex].key, activeIndex);\n }\n }, [activeIndex, onStepChange, steps]);\n\n const selectStep = async (index) => {\n const action = stepSelectionActionOf(stepSelection, activeIndex, index);\n if (action === 'ignore') {\n return;\n }\n // con checked, un paso con errores no se deja: se queda ahí para que se corrijan\n if (action === 'checkFirst' && !(await isStepValid(activeIndex))) {\n return;\n }\n setActiveIndex(index);\n };\n\n return {activeIndex, setActiveIndex, selectStep: stepSelection === WIZARD_STEP_SELECTION.none ? undefined : selectStep};\n}\n"],"names":[],"mappings":";;;;AAUA,MAAM,SAAS,aAAa,YAAY;AAYjC,SAAS,eAAe,EAAC,OAAO,gBAAgB,gBAAgB,sBAAsB,MAAM,cAAc,YAAW,GAAG;AAC3H,QAAM,CAAC,OAAO,IAAI,SAAS,MAAM,cAAc,OAAO,cAAc,CAAC;AACrE,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,QAAQ,KAAK;AAC5D,QAAM,iBAAiB,OAAO,QAAQ,KAAK;AAG3C,YAAU,MAAM;AACZ,QAAI,QAAQ,cAAc;AACtB,aAAO,KAAK,mBAAmB,cAAc,+CAA+C,EAAC,UAAU,MAAM,IAAI,CAAC,SAAS,KAAK,GAAG,EAAC,CAAC;AAAA,IACzI;AAAA,EAEJ,GAAG,CAAA,CAAE;AAGL,YAAU,MAAM;AACZ,QAAI,eAAe,YAAY,aAAa;AACxC,qBAAe,UAAU;AACzB,qBAAe,MAAM,WAAW,EAAE,KAAK,WAAW;AAAA,IACtD;AAAA,EACJ,GAAG,CAAC,aAAa,cAAc,KAAK,CAAC;AAErC,QAAM,aAAa,OAAO,UAAU;AAChC,UAAM,SAAS,sBAAsB,eAAe,aAAa,KAAK;AACtE,QAAI,WAAW,UAAU;AACrB;AAAA,IACJ;AAEA,QAAI,WAAW,gBAAgB,CAAE,MAAM,YAAY,WAAW,GAAI;AAC9D;AAAA,IACJ;AACA,mBAAe,KAAK;AAAA,EACxB;AAEA,SAAO,EAAC,aAAa,gBAAgB,YAAY,kBAAkB,sBAAsB,OAAO,SAAY,WAAU;AAC1H;"}
@@ -0,0 +1,32 @@
1
+ const WIZARD_ORIENTATION = Object.freeze({ horizontal: "horizontal", vertical: "vertical" });
2
+ const WIZARD_STEP_SELECTION = Object.freeze({ none: "none", free: "free", checked: "checked" });
3
+ function openingStepOf(steps, stepKey) {
4
+ if (stepKey === void 0 || stepKey === null) {
5
+ return { index: 0, isUnknownKey: false };
6
+ }
7
+ const index = steps.findIndex((step) => step.key === stepKey);
8
+ return index >= 0 ? { index, isUnknownKey: false } : { index: 0, isUnknownKey: true };
9
+ }
10
+ function stepSelectionActionOf(stepSelection, activeIndex, targetIndex) {
11
+ if (stepSelection === WIZARD_STEP_SELECTION.none || targetIndex === activeIndex) {
12
+ return "ignore";
13
+ }
14
+ return stepSelection === WIZARD_STEP_SELECTION.checked ? "checkFirst" : "jump";
15
+ }
16
+ function isStepCompleted(step, { index, activeIndex, skippedKeys, isSelectable }) {
17
+ if (typeof step.completed === "boolean") {
18
+ return step.completed;
19
+ }
20
+ if (isSelectable || skippedKeys.has(step.key)) {
21
+ return false;
22
+ }
23
+ return index < activeIndex;
24
+ }
25
+ export {
26
+ WIZARD_ORIENTATION,
27
+ WIZARD_STEP_SELECTION,
28
+ isStepCompleted,
29
+ openingStepOf,
30
+ stepSelectionActionOf
31
+ };
32
+ //# sourceMappingURL=wizardSteps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wizardSteps.js","sources":["../../src/inputs/wizardSteps.js"],"sourcesContent":["// Las reglas del formulario por pasos que no dependen de formik, de react-hook-form ni de React: cómo se acomodan los pasos,\n// qué pasa al pulsar el título de otro paso y en qué paso se abre. Las usan WizardLayout y los dos FormWizard.\n//\n// Por qué existen: editar un registro de varios pasos (un expediente, un perfil) casi nunca es recorrerlos todos; se entra a\n// corregir un dato suelto y el servidor dice en qué paso está. Con «Siguiente» como único camino, eso es el camino largo.\n//\n// Funciones puras, sin React: function (regla de la casa). Pruebas en wizardSteps.test.js.\n\n/**\n * Cómo se acomodan los pasos:\n * horizontal los pasos en fila arriba y los campos abajo (lo de siempre; en el teléfono, «Paso 2 de 4» y su nombre);\n * vertical uno debajo del otro, como un acordeón: los campos y los botones van dentro del paso que está abierto.\n */\nexport const WIZARD_ORIENTATION = Object.freeze({horizontal: 'horizontal', vertical: 'vertical'});\n\n/**\n * Qué pasa al pulsar el título de otro paso:\n * none los títulos no se pulsan (lo de siempre): se avanza con «Siguiente»;\n * free salta directo, sin revisar el paso en el que está; al enviar, el formulario regresa al primer paso con error;\n * checked salta solo si el paso en el que está no tiene errores (igual que «Siguiente»).\n */\nexport const WIZARD_STEP_SELECTION = Object.freeze({none: 'none', free: 'free', checked: 'checked'});\n\n/**\n * El paso en el que se abre: el de esa llave (la que dice el servidor) o el primero si no llega.\n *\n * @param {Array<{key: string}>} steps\n * @param {string} [stepKey]\n * @returns {{index: number, isUnknownKey: boolean}} isUnknownKey: llegó una llave que no es de ningún paso (se abre el primero)\n */\nexport function openingStepOf(steps, stepKey) {\n if (stepKey === undefined || stepKey === null) {\n return {index: 0, isUnknownKey: false};\n }\n const index = steps.findIndex((step) => step.key === stepKey);\n return index >= 0 ? {index, isUnknownKey: false} : {index: 0, isUnknownKey: true};\n}\n\n/**\n * ¿Al pulsar el título del paso `targetIndex` hay que revisar antes el paso en el que está? Solo con `checked` y si de veras\n * cambia de paso. Pulsar el paso en el que ya está no hace nada.\n *\n * @param {string} stepSelection uno de WIZARD_STEP_SELECTION\n * @param {number} activeIndex\n * @param {number} targetIndex\n * @returns {'ignore'|'jump'|'checkFirst'}\n */\nexport function stepSelectionActionOf(stepSelection, activeIndex, targetIndex) {\n if (stepSelection === WIZARD_STEP_SELECTION.none || targetIndex === activeIndex) {\n return 'ignore';\n }\n return stepSelection === WIZARD_STEP_SELECTION.checked ? 'checkFirst' : 'jump';\n}\n\n/**\n * ¿El paso se pinta como hecho (la palomita)? En fila y sin pulsar, los de antes del que está abierto, salvo los omitidos\n * (lo de siempre). Cuando los títulos se pulsan, «antes» ya no quiere decir «hecho» (se pudo brincar): sin palomita, salvo\n * que la pantalla lo diga en `steps[].completed` (sabe qué datos ya están completos).\n *\n * @param {{key: string, completed?: boolean}} step\n * @param {object} place\n * @param {number} place.index\n * @param {number} place.activeIndex\n * @param {Set<string>} place.skippedKeys\n * @param {boolean} place.isSelectable\n * @returns {boolean}\n */\nexport function isStepCompleted(step, {index, activeIndex, skippedKeys, isSelectable}) {\n if (typeof step.completed === 'boolean') {\n return step.completed;\n }\n if (isSelectable || skippedKeys.has(step.key)) {\n return false;\n }\n return index < activeIndex;\n}\n"],"names":[],"mappings":"AAaY,MAAC,qBAAqB,OAAO,OAAO,EAAC,YAAY,cAAc,UAAU,WAAU,CAAC;AAQpF,MAAC,wBAAwB,OAAO,OAAO,EAAC,MAAM,QAAQ,MAAM,QAAQ,SAAS,UAAS,CAAC;AAS5F,SAAS,cAAc,OAAO,SAAS;AAC1C,MAAI,YAAY,UAAa,YAAY,MAAM;AAC3C,WAAO,EAAC,OAAO,GAAG,cAAc,MAAK;AAAA,EACzC;AACA,QAAM,QAAQ,MAAM,UAAU,CAAC,SAAS,KAAK,QAAQ,OAAO;AAC5D,SAAO,SAAS,IAAI,EAAC,OAAO,cAAc,MAAK,IAAI,EAAC,OAAO,GAAG,cAAc,KAAI;AACpF;AAWO,SAAS,sBAAsB,eAAe,aAAa,aAAa;AAC3E,MAAI,kBAAkB,sBAAsB,QAAQ,gBAAgB,aAAa;AAC7E,WAAO;AAAA,EACX;AACA,SAAO,kBAAkB,sBAAsB,UAAU,eAAe;AAC5E;AAeO,SAAS,gBAAgB,MAAM,EAAC,OAAO,aAAa,aAAa,aAAY,GAAG;AACnF,MAAI,OAAO,KAAK,cAAc,WAAW;AACrC,WAAO,KAAK;AAAA,EAChB;AACA,MAAI,gBAAgB,YAAY,IAAI,KAAK,GAAG,GAAG;AAC3C,WAAO;AAAA,EACX;AACA,SAAO,QAAQ;AACnB;"}
@@ -4,6 +4,7 @@ declare const BarActionButton: (props: {
4
4
  onClick?: (event: object) => void;
5
5
  disabled?: boolean;
6
6
  color?: 'inherit' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning';
7
+ sx?: object;
7
8
  } & {
8
9
  slots?: Record<string, import('react').ElementType>;
9
10
  slotProps?: Record<string, object | ((partProps: object) => object)>;
@@ -6,17 +6,28 @@
6
6
  * @param {string[]} [props.steps[].fields] los campos del paso (los que revisa «Siguiente»)
7
7
  * @param {boolean} [props.steps[].optional] se puede omitir
8
8
  * @param {import('react').ReactNode} props.steps[].content los campos del paso
9
+ * @param {boolean} [props.steps[].completed] lleva la palomita (cuando los títulos se pulsan, solo si la pantalla lo dice)
9
10
  * @param {string} props.submitLabel ya traducido: el botón del último paso
11
+ * @param {'horizontal'|'vertical'} [props.orientation] uno de WIZARD_ORIENTATION; por omisión horizontal
12
+ * @param {string} [props.initialStepKey] la llave del paso en el que se abre (la que diga el servidor); por omisión el primero
13
+ * @param {'none'|'free'|'checked'} [props.stepSelection] uno de WIZARD_STEP_SELECTION: si el título de otro paso se pulsa y qué
14
+ * revisa antes; por omisión none (se avanza con «Siguiente», como siempre)
15
+ * @param {(stepKey: string, index: number) => void} [props.onStepChange] cada vez que cambia el paso abierto
10
16
  * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}
11
17
  */
12
- export default function FormWizard({ steps, submitLabel, texts, ...unknownProps }: {
18
+ export default function FormWizard({ steps, submitLabel, orientation, initialStepKey, stepSelection, onStepChange, texts, ...unknownProps }: {
13
19
  steps: {
14
20
  key: string;
15
21
  label: string;
16
22
  fields?: string[];
17
23
  optional?: boolean;
18
24
  content: import('react').ReactNode;
25
+ completed?: boolean;
19
26
  }[];
20
27
  submitLabel: string;
28
+ orientation?: 'horizontal' | 'vertical';
29
+ initialStepKey?: string;
30
+ stepSelection?: 'none' | 'free' | 'checked';
31
+ onStepChange?: (stepKey: string, index: number) => void;
21
32
  texts?: object;
22
33
  }): import("react").JSX.Element;
@@ -6,17 +6,28 @@
6
6
  * @param {string[]} [props.steps[].fields] los campos del paso (los que revisa «Siguiente»)
7
7
  * @param {boolean} [props.steps[].optional] se puede omitir
8
8
  * @param {import('react').ReactNode} props.steps[].content los campos del paso
9
+ * @param {boolean} [props.steps[].completed] lleva la palomita (cuando los títulos se pulsan, solo si la pantalla lo dice)
9
10
  * @param {string} props.submitLabel ya traducido: el botón del último paso
11
+ * @param {'horizontal'|'vertical'} [props.orientation] uno de WIZARD_ORIENTATION; por omisión horizontal
12
+ * @param {string} [props.initialStepKey] la llave del paso en el que se abre (la que diga el servidor); por omisión el primero
13
+ * @param {'none'|'free'|'checked'} [props.stepSelection] uno de WIZARD_STEP_SELECTION: si el título de otro paso se pulsa y qué
14
+ * revisa antes; por omisión none (se avanza con «Siguiente», como siempre)
15
+ * @param {(stepKey: string, index: number) => void} [props.onStepChange] cada vez que cambia el paso abierto
10
16
  * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}
11
17
  */
12
- export default function FormWizard({ steps, submitLabel, texts, ...unknownProps }: {
18
+ export default function FormWizard({ steps, submitLabel, orientation, initialStepKey, stepSelection, onStepChange, texts, ...unknownProps }: {
13
19
  steps: {
14
20
  key: string;
15
21
  label: string;
16
22
  fields?: string[];
17
23
  optional?: boolean;
18
24
  content: import('react').ReactNode;
25
+ completed?: boolean;
19
26
  }[];
20
27
  submitLabel: string;
28
+ orientation?: 'horizontal' | 'vertical';
29
+ initialStepKey?: string;
30
+ stepSelection?: 'none' | 'free' | 'checked';
31
+ onStepChange?: (stepKey: string, index: number) => void;
21
32
  texts?: object;
22
33
  }): import("react").JSX.Element;
@@ -62,7 +62,7 @@ export { transferProgressOf } from './upload/transferProgress.js';
62
62
  export { useFilePicker } from './upload/useFilePicker.js';
63
63
  export { buildUploadActors, uploadMachine } from './upload/uploadMachine.js';
64
64
  export { uploadWithProgress } from './upload/uploadWithProgress.js';
65
- export { APP_BAR_HEIGHT_CSS_VARIABLE, BackButton, COMPARISON_OPERATOR, FILTER_DRAWER_ANCHOR, FILTER_FIELD_KIND, FilterDrawer, activeDrawerFilterCountOf, comparisonOf, comparisonTextOf, filterDrawerChipsOf, filterDrawerDefaultsOf, filterDrawerKindsOf, filterKeysOf, CHART_VIEW, CalendarHeatmap, CollapsibleSection, ColorSchemeToggle, ContentDialog, CopyButton, DateRangeFilter, DefinitionList, DetailHeader, DictionaryText, DocumentTitle, DownloadMenu, FONT_SIZE_LEVELS, FontSizeToggle, FullPageLoader, GlassLensFilter, GlassPanel, HELP_CLOSE_REASON, HelpButton, HelpCarouselDialog, ICON_BADGE_CLASSES, ICON_BADGE_VARIANT, IconBadge, LANGUAGES, LanguageToggle, MetricCard, PageBreadcrumbs, PageHeader, PageHeading, ProfileHeader, ROUTE_TABS_MODE, RecordCard, RecordCardSkeleton, RouteTabs, TREND_DIRECTION, PeriodRangeSelector, QuickFilters, SCROLL_MARGIN_BELOW_APP_BAR, SEGMENTED_CONTROL_SX, ScrollableRow, SecretField, SectionCard, SegmentedControl, StatusChip, TableToggle, Tag, scrollableRow, tableRowTextOf, useActiveRouteTab, useAppBarHeightPublisher, useHelpCarousel, } from './ui/index.js';
65
+ export { APP_BAR_HEIGHT_CSS_VARIABLE, BackButton, COMPARISON_OPERATOR, FILTER_DRAWER_ANCHOR, FILTER_FIELD_KIND, FilterDrawer, activeDrawerFilterCountOf, comparisonOf, comparisonTextOf, filterDrawerChipsOf, filterDrawerDefaultsOf, filterDrawerKindsOf, filterKeysOf, CHART_VIEW, CalendarHeatmap, CollapsibleSection, ColorSchemeToggle, ContentDialog, CopyButton, DateRangeFilter, DefinitionList, DetailHeader, DictionaryText, DocumentTitle, DownloadMenu, FONT_SIZE_LEVELS, FontSizeToggle, FullPageLoader, GlassLensFilter, GlassPanel, HELP_CLOSE_REASON, HelpButton, HelpCarouselDialog, ICON_BADGE_CLASSES, ICON_BADGE_VARIANT, IconBadge, LANGUAGES, LanguageToggle, MetricCard, PageBreadcrumbs, PageHeader, PageHeading, ProfileHeader, ROUTE_TABS_MODE, RECORD_CARD_FOOTER_ALIGN, RecordCard, RecordCardSkeleton, RouteTabs, TREND_DIRECTION, PeriodRangeSelector, QuickFilters, SCROLL_MARGIN_BELOW_APP_BAR, SEGMENTED_CONTROL_SX, ScrollableRow, SecretField, SectionCard, SegmentedControl, StatusChip, TableToggle, Tag, scrollableRow, tableRowTextOf, useActiveRouteTab, useAppBarHeightPublisher, useHelpCarousel, } from './ui/index.js';
66
66
  export { default as CatalogInput } from './inputs/CatalogInput.js';
67
67
  export { default as ColorInput } from './inputs/ColorInput.js';
68
68
  export { default as FieldFrame } from './inputs/FieldFrame.js';
@@ -77,6 +77,7 @@ export { default as RepeatingFields } from './inputs/RepeatingFields.js';
77
77
  export { useRowKeys } from './inputs/useRowKeys.js';
78
78
  export { default as SelectInput } from './inputs/SelectInput.js';
79
79
  export { default as WizardLayout } from './inputs/WizardLayout.js';
80
+ export { WIZARD_ORIENTATION, WIZARD_STEP_SELECTION, isStepCompleted, openingStepOf, stepSelectionActionOf } from './inputs/wizardSteps.js';
80
81
  export { default as TextInput } from './inputs/TextInput.js';
81
82
  export { HEX_COLOR, MIN_CONTRAST, contrastOnWhite, readableVariantOf } from './inputs/colorContrast.js';
82
83
  export { cleanNumberText, completeDecimals, groupThousands } from './inputs/numberText.js';
@@ -3,14 +3,17 @@ declare const WizardLayout: (props: {
3
3
  key: string;
4
4
  label: string;
5
5
  optional?: boolean;
6
+ completed?: boolean;
6
7
  content: import('react').ReactNode;
7
8
  }[];
8
9
  activeIndex: number;
9
- skippedKeys: Set<string>;
10
+ skippedKeys?: Set<string>;
10
11
  isSubmitting?: boolean;
12
+ orientation?: 'horizontal' | 'vertical';
11
13
  onBack: () => void;
12
14
  onNext: () => void;
13
- onSkip: () => void;
15
+ onSkip?: () => void;
16
+ onStepSelect?: (index: number) => void;
14
17
  submitButton: import('react').ReactNode;
15
18
  problemAlert?: import('react').ReactNode;
16
19
  texts?: object;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @param {object} options
3
+ * @param {Array<{key: string}>} options.steps
4
+ * @param {string} [options.initialStepKey] el paso en el que se abre (el que diga el servidor); por omisión el primero
5
+ * @param {string} [options.stepSelection] uno de WIZARD_STEP_SELECTION; por omisión none
6
+ * @param {(stepKey: string, index: number) => void} [options.onStepChange] cada vez que cambia el paso abierto
7
+ * @param {(index: number) => Promise<boolean>} options.isStepValid revisa los campos de ese paso y marca sus errores
8
+ * @returns {{activeIndex: number, setActiveIndex: import('react').Dispatch<import('react').SetStateAction<number>>, selectStep: ((index: number) => Promise<void>)|undefined}}
9
+ * selectStep es undefined con none: así WizardLayout sabe que los títulos no se pulsan
10
+ */
11
+ export declare function useWizardSteps({ steps, initialStepKey, stepSelection, onStepChange, isStepValid }: {
12
+ steps: Array<{
13
+ key: string;
14
+ }>;
15
+ initialStepKey?: string;
16
+ stepSelection?: string;
17
+ onStepChange?: (stepKey: string, index: number) => void;
18
+ isStepValid: (index: number) => Promise<boolean>;
19
+ }): {
20
+ activeIndex: number;
21
+ setActiveIndex: import('react').Dispatch<import('react').SetStateAction<number>>;
22
+ selectStep: ((index: number) => Promise<void>) | undefined;
23
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Cómo se acomodan los pasos:
3
+ * horizontal los pasos en fila arriba y los campos abajo (lo de siempre; en el teléfono, «Paso 2 de 4» y su nombre);
4
+ * vertical uno debajo del otro, como un acordeón: los campos y los botones van dentro del paso que está abierto.
5
+ */
6
+ export declare const WIZARD_ORIENTATION: Readonly<{
7
+ horizontal: "horizontal";
8
+ vertical: "vertical";
9
+ }>;
10
+ /**
11
+ * Qué pasa al pulsar el título de otro paso:
12
+ * none los títulos no se pulsan (lo de siempre): se avanza con «Siguiente»;
13
+ * free salta directo, sin revisar el paso en el que está; al enviar, el formulario regresa al primer paso con error;
14
+ * checked salta solo si el paso en el que está no tiene errores (igual que «Siguiente»).
15
+ */
16
+ export declare const WIZARD_STEP_SELECTION: Readonly<{
17
+ none: "none";
18
+ free: "free";
19
+ checked: "checked";
20
+ }>;
21
+ /**
22
+ * El paso en el que se abre: el de esa llave (la que dice el servidor) o el primero si no llega.
23
+ *
24
+ * @param {Array<{key: string}>} steps
25
+ * @param {string} [stepKey]
26
+ * @returns {{index: number, isUnknownKey: boolean}} isUnknownKey: llegó una llave que no es de ningún paso (se abre el primero)
27
+ */
28
+ export declare function openingStepOf(steps: Array<{
29
+ key: string;
30
+ }>, stepKey?: string): {
31
+ index: number;
32
+ isUnknownKey: boolean;
33
+ };
34
+ /**
35
+ * ¿Al pulsar el título del paso `targetIndex` hay que revisar antes el paso en el que está? Solo con `checked` y si de veras
36
+ * cambia de paso. Pulsar el paso en el que ya está no hace nada.
37
+ *
38
+ * @param {string} stepSelection uno de WIZARD_STEP_SELECTION
39
+ * @param {number} activeIndex
40
+ * @param {number} targetIndex
41
+ * @returns {'ignore'|'jump'|'checkFirst'}
42
+ */
43
+ export declare function stepSelectionActionOf(stepSelection: string, activeIndex: number, targetIndex: number): 'ignore' | 'jump' | 'checkFirst';
44
+ /**
45
+ * ¿El paso se pinta como hecho (la palomita)? En fila y sin pulsar, los de antes del que está abierto, salvo los omitidos
46
+ * (lo de siempre). Cuando los títulos se pulsan, «antes» ya no quiere decir «hecho» (se pudo brincar): sin palomita, salvo
47
+ * que la pantalla lo diga en `steps[].completed` (sabe qué datos ya están completos).
48
+ *
49
+ * @param {{key: string, completed?: boolean}} step
50
+ * @param {object} place
51
+ * @param {number} place.index
52
+ * @param {number} place.activeIndex
53
+ * @param {Set<string>} place.skippedKeys
54
+ * @param {boolean} place.isSelectable
55
+ * @returns {boolean}
56
+ */
57
+ export declare function isStepCompleted(step: {
58
+ key: string;
59
+ completed?: boolean;
60
+ }, { index, activeIndex, skippedKeys, isSelectable }: {
61
+ index: number;
62
+ activeIndex: number;
63
+ skippedKeys: Set<string>;
64
+ isSelectable: boolean;
65
+ }): boolean;
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Cómo se acomoda el renglón de abajo (footer y «Ver detalle»):
3
+ * spaceBetween el footer a la izquierda y «Ver detalle» en la otra orilla (lo de siempre);
4
+ * start los dos juntos, a la izquierda;
5
+ * end los dos juntos, a la derecha.
6
+ */
7
+ export declare const RECORD_CARD_FOOTER_ALIGN: Readonly<{
8
+ spaceBetween: "spaceBetween";
9
+ start: "start";
10
+ end: "end";
11
+ }>;
1
12
  declare const RecordCard: (props: {
2
13
  title: import('react').ReactNode;
3
14
  eyebrow?: import('react').ReactNode;
@@ -25,6 +36,7 @@ declare const RecordCard: (props: {
25
36
  trailing?: import('react').ReactNode;
26
37
  columns?: string;
27
38
  footer?: import('react').ReactNode;
39
+ footerAlign?: 'spaceBetween' | 'start' | 'end';
28
40
  details?: import('react').ReactNode;
29
41
  detailsLabel?: string;
30
42
  defaultDetailsOpen?: boolean;
@@ -21,7 +21,7 @@ export { default as PageBreadcrumbs } from './navigation/PageBreadcrumbs.js';
21
21
  export { ROUTE_TABS_MODE, default as RouteTabs, useActiveRouteTab } from './navigation/RouteTabs.js';
22
22
  export { default as HelpButton } from './help/HelpButton.js';
23
23
  export { HELP_CLOSE_REASON, default as HelpCarouselDialog } from './help/HelpCarouselDialog.js';
24
- export { default as RecordCard } from './cards/RecordCard.js';
24
+ export { RECORD_CARD_FOOTER_ALIGN, default as RecordCard } from './cards/RecordCard.js';
25
25
  export { default as RecordCardSkeleton } from './cards/RecordCardSkeleton.js';
26
26
  export { useHelpCarousel } from './help/useHelpCarousel.js';
27
27
  export { default as DateRangeFilter } from './filters/DateRangeFilter.js';
@@ -3,6 +3,7 @@ declare const BackButton: (props: {
3
3
  listKey?: string;
4
4
  label?: string;
5
5
  texts?: object;
6
+ sx?: object;
6
7
  } & {
7
8
  slots?: Record<string, import('react').ElementType>;
8
9
  slotProps?: Record<string, object | ((partProps: object) => object)>;
@@ -17,6 +17,8 @@ import { piecePart, withPieceSlots, usePieceProps } from "../../theme/themePiece
17
17
  import { useToolkitTranslation } from "../../toolkit/useToolkitTranslation.js";
18
18
  import { warnUnknownProps } from "../../toolkit/unknownProps.js";
19
19
  import ActionsMenu from "../actions/ActionsMenu.js";
20
+ const RECORD_CARD_FOOTER_ALIGN = Object.freeze({ spaceBetween: "spaceBetween", start: "start", end: "end" });
21
+ const FOOTER_JUSTIFY = Object.freeze({ spaceBetween: "flex-start", start: "flex-start", end: "flex-end" });
20
22
  const THEME_NAME = "CalcitaRecordCard";
21
23
  const RecordCardRoot = piecePart(THEME_NAME, "Root", Paper);
22
24
  const RecordCardHeader = piecePart(THEME_NAME, "Header", Stack);
@@ -42,6 +44,7 @@ function RecordCardContent(props) {
42
44
  trailing,
43
45
  columns,
44
46
  footer,
47
+ footerAlign = RECORD_CARD_FOOTER_ALIGN.spaceBetween,
45
48
  details,
46
49
  detailsLabel,
47
50
  defaultDetailsOpen = false,
@@ -122,8 +125,8 @@ function RecordCardContent(props) {
122
125
  trailing && /* @__PURE__ */ jsx(Box, { sx: { minWidth: 0, alignSelf: "center", borderTop: { xs: 1, md: 0 }, pt: { xs: 1.5, md: 0 }, borderColor: "divider" }, children: trailing })
123
126
  ] }),
124
127
  children,
125
- (footer || details) && /* @__PURE__ */ jsxs(RecordCardFooter, { direction: "row", ownSx: { mt: 2, gap: 1, alignItems: "center", flexWrap: "wrap" }, children: [
126
- /* @__PURE__ */ jsx(Box, { sx: { flex: 1, minWidth: 0 }, children: footer }),
128
+ (footer || details) && /* @__PURE__ */ jsxs(RecordCardFooter, { direction: "row", ownSx: { mt: 2, gap: 1, alignItems: "center", flexWrap: "wrap", justifyContent: FOOTER_JUSTIFY[footerAlign] ?? "flex-start" }, children: [
129
+ /* @__PURE__ */ jsx(Box, { sx: { flex: footerAlign === RECORD_CARD_FOOTER_ALIGN.spaceBetween ? 1 : "0 1 auto", minWidth: 0 }, children: footer }),
127
130
  details && /* @__PURE__ */ jsx(
128
131
  Button,
129
132
  {
@@ -141,6 +144,7 @@ function RecordCardContent(props) {
141
144
  }
142
145
  const RecordCard = withPieceSlots(RecordCardContent, THEME_NAME);
143
146
  export {
147
+ RECORD_CARD_FOOTER_ALIGN,
144
148
  RecordCard as default
145
149
  };
146
150
  //# sourceMappingURL=RecordCard.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"RecordCard.js","sources":["../../../src/ui/cards/RecordCard.jsx"],"sourcesContent":["// La tarjeta de un registro (un pedido, un cliente, una solicitud) para listas donde una tabla se queda corta: cada registro\n// con su identidad arriba, sus datos agrupados en el cuerpo y lo que se puede hacer con él. Para una lista de tarjetas,\n// una debajo de otra; con RecordCardSkeleton mientras carga.\n//\n// encabezado avatar (iniciales o ícono), eyebrow (el folio), title, subtitle; a la derecha status (estatus e\n// insignias) y el menú «⋯» (actions)\n// cuerpo groups: grupos de datos {title?, facts: [{label, value}]} lado a lado en escritorio y uno debajo de otro en\n// el teléfono; highlight: el dato principal, grande (el total)\n// abajo footer (botones, una cuenta regresiva, un formulario corto) y details: lo que se despliega con «Ver\n// detalle» (una tabla, un historial), que avisa con onDetailsToggle\n//\n// El ORDEN es el que se da y nada se reacomoda solo: los grupos de izquierda a derecha (de arriba abajo en el teléfono) y\n// los datos de cada grupo en el orden de facts. Para un orden obligatorio (el de un sistema que ya existe, donde el estatus\n// va después de las fechas), trailing es la última columna del cuerpo y columns fija el ancho de cada una.\n//\n// La tarjeta NO se pica entera (la regla de la casa: una acción, un botón): para abrir el registro, onOpen pone su botón.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaRecordCard con defaultProps y\n// styleOverrides de sus partes: root, header, avatar, titles, status, body, group, fact, highlight, footer, details.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId, useState} from 'react';\nimport Avatar from '@mui/material/Avatar';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Collapse from '@mui/material/Collapse';\nimport IconButton from '@mui/material/IconButton';\nimport Paper from '@mui/material/Paper';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded';\nimport OpenInNewRoundedIcon from '@mui/icons-material/OpenInNewRounded';\n\nimport {initialsOf} from '@/appShell/menuItemText.js';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\nimport ActionsMenu from '@/ui/actions/ActionsMenu.jsx';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaRecordCard)\nconst THEME_NAME = 'CalcitaRecordCard';\nconst RecordCardRoot = piecePart(THEME_NAME, 'Root', Paper);\nconst RecordCardHeader = piecePart(THEME_NAME, 'Header', Stack);\nconst RecordCardAvatar = piecePart(THEME_NAME, 'Avatar', Avatar);\nconst RecordCardTitles = piecePart(THEME_NAME, 'Titles', Box);\nconst RecordCardStatus = piecePart(THEME_NAME, 'Status', Stack);\nconst RecordCardBody = piecePart(THEME_NAME, 'Body', Box);\nconst RecordCardGroup = piecePart(THEME_NAME, 'Group', Box);\nconst RecordCardFact = piecePart(THEME_NAME, 'Fact', Box);\nconst RecordCardHighlight = piecePart(THEME_NAME, 'Highlight', Box);\nconst RecordCardFooter = piecePart(THEME_NAME, 'Footer', Stack);\nconst RecordCardDetails = piecePart(THEME_NAME, 'Details', Box);\n\n/**\n * @param {object} props\n * @param {import('react').ReactNode} props.title el nombre del registro, ya traducido\n * @param {import('react').ReactNode} [props.eyebrow] arriba del título: el folio o el tipo\n * @param {import('react').ReactNode} [props.subtitle] abajo del título\n * @param {string|import('react').ReactNode} [props.avatar] un texto (de él salen las iniciales) o un ícono\n * @param {import('react').ReactNode} [props.status] a la derecha: StatusChip, IconBadge…\n * @param {Parameters<typeof import('@/ui/actions/ActionsMenu.jsx').default>[0]['actions']} [props.actions] el menú «⋯», con sus secciones,\n * separadores, ligas y submenús (los renglones de ActionsMenu)\n * @param {Array<{key?: string, title?: import('react').ReactNode, facts?: Array<{label: import('react').ReactNode, value: import('react').ReactNode, emphasis?: boolean}>, content?: import('react').ReactNode}>} [props.groups]\n * los datos, en grupos; emphasis los pone en negritas; content en lugar de facts para un grupo libre (una barra, unas\n * insignias, un enlace)\n * @param {import('react').ReactNode} [props.header] un encabezado propio en lugar del de la casa (avatar, títulos, estatus)\n * @param {import('react').ReactNode} [props.children] contenido libre entre el cuerpo y lo de abajo\n * @param {{label: import('react').ReactNode, value: import('react').ReactNode, caption?: import('react').ReactNode}} [props.highlight] el dato principal\n * @param {import('react').ReactNode} [props.trailing] la última columna del cuerpo, después de los grupos y del dato principal\n * (el estatus o unos botones, cuando el orden obligatorio los pone al final)\n * @param {string} [props.columns] en escritorio, el ancho de cada columna del cuerpo como en CSS ('1.2fr 1fr 1fr auto'); por\n * omisión, los grupos a partes iguales y el dato principal y trailing a su medida\n * @param {import('react').ReactNode} [props.footer] abajo, a todo lo ancho\n * @param {import('react').ReactNode} [props.details] lo que se despliega con «Ver detalle»\n * @param {string} [props.detailsLabel] ya traducido; por omisión «Ver detalle»\n * @param {boolean} [props.defaultDetailsOpen]\n * @param {(isOpen: boolean) => void} [props.onDetailsToggle]\n * @param {() => void} [props.onOpen] el botón para abrir el registro (su detalle)\n * @param {string} [props.openLabel] ya traducido: el nombre de ese botón («Abrir el pedido P-1203»)\n * @param {object} [props.sx]\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nfunction RecordCardContent(props) {\n const {\n title, eyebrow, subtitle, avatar, status, actions, groups = [], highlight, trailing, columns, footer, details, detailsLabel, defaultDetailsOpen = false,\n onDetailsToggle, onOpen, openLabel, header, children, sx, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const titleId = useId();\n const detailsId = useId();\n const [isDetailsOpen, setIsDetailsOpen] = useState(defaultDetailsOpen);\n const toggleDetails = () => {\n setIsDetailsOpen((current) => !current);\n onDetailsToggle?.(!isDetailsOpen);\n };\n const hasBody = groups.length > 0 || highlight || trailing;\n // las columnas en escritorio: las que se pidan, o los grupos a partes iguales y lo demás a su medida\n const desktopColumns = columns ?? `${groups.length > 0 ? `repeat(${groups.length}, minmax(0, 1fr))` : ''}${highlight ? ' auto' : ''}${trailing ? ' auto' : ''}`.trim();\n\n return (\n <RecordCardRoot variant=\"outlined\" component=\"article\" aria-labelledby={titleId} ownSx={{p: {xs: 2, sm: 2.5}, minWidth: 0}} sx={sx}>\n {/* en el teléfono el estatus baja abajo del título (si no, le quitaba medio ancho) y arriba quedan los botones */}\n {header ?? (\n <RecordCardHeader direction=\"row\" ownSx={{gap: 1.5, alignItems: 'flex-start', flexWrap: {xs: 'wrap', sm: 'nowrap'}}}>\n {avatar && (\n <RecordCardAvatar ownSx={{width: 44, height: 44, bgcolor: 'action.selected', color: 'text.primary', fontSize: 16, fontWeight: 600}}>\n {typeof avatar === 'string' ? initialsOf(avatar) : avatar}\n </RecordCardAvatar>\n )}\n <RecordCardTitles ownSx={{flex: 1, minWidth: 0, order: 1}}>\n {eyebrow && <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\" sx={{fontVariantNumeric: 'tabular-nums'}}>{eyebrow}</Typography>}\n <Typography id={titleId} variant=\"subtitle1\" component=\"h3\" sx={{fontWeight: 600, overflowWrap: 'anywhere'}}>{title}</Typography>\n {subtitle && <Typography variant=\"body2\" color=\"text.secondary\">{subtitle}</Typography>}\n </RecordCardTitles>\n {status && (\n <RecordCardStatus direction=\"row\" ownSx={{\n gap: 0.75, alignItems: 'center', flexWrap: 'wrap', order: {xs: 3, sm: 2},\n flexBasis: {xs: '100%', sm: 'auto'}, justifyContent: {xs: 'flex-start', sm: 'flex-end'}, maxWidth: {sm: '50%'},\n }}>\n {status}\n </RecordCardStatus>\n )}\n <Stack direction=\"row\" sx={{gap: 0.25, alignItems: 'center', order: {xs: 2, sm: 3}, flexShrink: 0}}>\n {onOpen && (\n <Tooltip title={openLabel ?? t('recordCard.open')}>\n <IconButton size=\"small\" aria-label={openLabel ?? t('recordCard.open')} onClick={onOpen}><OpenInNewRoundedIcon fontSize=\"small\" /></IconButton>\n </Tooltip>\n )}\n {actions?.length > 0 && <ActionsMenu label={t('recordCard.actions')} actions={actions} size=\"small\" />}\n </Stack>\n </RecordCardHeader>\n )}\n\n {hasBody && (\n <RecordCardBody ownSx={{\n mt: 2, display: 'grid', gap: {xs: 1.5, md: 3}, alignItems: 'start',\n // en escritorio los grupos lado a lado y el dato principal al final; en el teléfono, uno debajo de otro\n gridTemplateColumns: {xs: '1fr', md: desktopColumns},\n }}>\n {groups.map((group, groupIndex) => (\n <RecordCardGroup key={group.key ?? groupIndex} ownSx={{\n minWidth: 0, pt: {xs: groupIndex > 0 ? 1.5 : 0, md: 0}, borderTop: {xs: groupIndex > 0 ? 1 : 0, md: 0}, borderColor: 'divider',\n }}>\n {group.title && <Typography variant=\"overline\" color=\"text.secondary\" component=\"p\" sx={{lineHeight: 1.8}}>{group.title}</Typography>}\n {group.content ?? (\n <Box component=\"dl\" sx={{m: 0, display: 'grid', gridTemplateColumns: 'auto 1fr', columnGap: 1.5, rowGap: 0.5}}>\n {(group.facts ?? []).map((fact, factIndex) => (\n <RecordCardFact key={factIndex} ownSx={{display: 'contents'}}>\n <Typography component=\"dt\" variant=\"body2\" color=\"text.secondary\">{fact.label}</Typography>\n <Typography component=\"dd\" variant=\"body2\" sx={{m: 0, textAlign: 'right', fontVariantNumeric: 'tabular-nums', fontWeight: fact.emphasis ? 600 : 400, overflowWrap: 'anywhere'}}>\n {fact.value}\n </Typography>\n </RecordCardFact>\n ))}\n </Box>\n )}\n </RecordCardGroup>\n ))}\n {highlight && (\n <RecordCardHighlight ownSx={{\n textAlign: {xs: 'left', md: 'right'}, pl: {md: 3}, borderLeft: {md: 1}, borderTop: {xs: 1, md: 0}, pt: {xs: 1.5, md: 0}, borderColor: 'divider',\n }}>\n <Typography variant=\"overline\" color=\"text.secondary\" component=\"p\" sx={{lineHeight: 1.8}}>{highlight.label}</Typography>\n <Typography variant=\"h5\" component=\"p\" sx={{fontWeight: 700, fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap'}}>{highlight.value}</Typography>\n {highlight.caption && <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\">{highlight.caption}</Typography>}\n </RecordCardHighlight>\n )}\n {trailing && (\n <Box sx={{minWidth: 0, alignSelf: 'center', borderTop: {xs: 1, md: 0}, pt: {xs: 1.5, md: 0}, borderColor: 'divider'}}>{trailing}</Box>\n )}\n </RecordCardBody>\n )}\n\n {children}\n {(footer || details) && (\n <RecordCardFooter direction=\"row\" ownSx={{mt: 2, gap: 1, alignItems: 'center', flexWrap: 'wrap'}}>\n <Box sx={{flex: 1, minWidth: 0}}>{footer}</Box>\n {details && (\n <Button size=\"small\" onClick={toggleDetails} aria-expanded={isDetailsOpen} aria-controls={detailsId}\n endIcon={<ExpandMoreRoundedIcon sx={{transform: isDetailsOpen ? 'rotate(180deg)' : 'none', transition: 'transform 200ms'}} />}>\n {isDetailsOpen ? t('recordCard.hideDetails') : (detailsLabel ?? t('recordCard.showDetails'))}\n </Button>\n )}\n </RecordCardFooter>\n )}\n {details && (\n <Collapse in={isDetailsOpen} unmountOnExit>\n <RecordCardDetails id={detailsId} ownSx={{mt: 2, pt: 2, borderTop: 1, borderColor: 'divider'}}>{details}</RecordCardDetails>\n </Collapse>\n )}\n </RecordCardRoot>\n );\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst RecordCard = withPieceSlots(RecordCardContent, THEME_NAME);\nexport default RecordCard;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RecordCard.js","sources":["../../../src/ui/cards/RecordCard.jsx"],"sourcesContent":["// La tarjeta de un registro (un pedido, un cliente, una solicitud) para listas donde una tabla se queda corta: cada registro\n// con su identidad arriba, sus datos agrupados en el cuerpo y lo que se puede hacer con él. Para una lista de tarjetas,\n// una debajo de otra; con RecordCardSkeleton mientras carga.\n//\n// encabezado avatar (iniciales o ícono), eyebrow (el folio), title, subtitle; a la derecha status (estatus e\n// insignias) y el menú «⋯» (actions)\n// cuerpo groups: grupos de datos {title?, facts: [{label, value}]} lado a lado en escritorio y uno debajo de otro en\n// el teléfono; highlight: el dato principal, grande (el total)\n// abajo footer (botones, una cuenta regresiva, un formulario corto) y details: lo que se despliega con «Ver\n// detalle» (una tabla, un historial), que avisa con onDetailsToggle. footerAlign acomoda ese renglón: el\n// footer a la izquierda y «Ver detalle» en la otra orilla (lo de siempre), o los dos juntos a la izquierda o a\n// la derecha (en una tarjeta ancha, dos controles en orillas opuestas se ven sueltos)\n//\n// El ORDEN es el que se da y nada se reacomoda solo: los grupos de izquierda a derecha (de arriba abajo en el teléfono) y\n// los datos de cada grupo en el orden de facts. Para un orden obligatorio (el de un sistema que ya existe, donde el estatus\n// va después de las fechas), trailing es la última columna del cuerpo y columns fija el ancho de cada una.\n//\n// La tarjeta NO se pica entera (la regla de la casa: una acción, un botón): para abrir el registro, onOpen pone su botón.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaRecordCard con defaultProps y\n// styleOverrides de sus partes: root, header, avatar, titles, status, body, group, fact, highlight, footer, details.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId, useState} from 'react';\nimport Avatar from '@mui/material/Avatar';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Collapse from '@mui/material/Collapse';\nimport IconButton from '@mui/material/IconButton';\nimport Paper from '@mui/material/Paper';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded';\nimport OpenInNewRoundedIcon from '@mui/icons-material/OpenInNewRounded';\n\nimport {initialsOf} from '@/appShell/menuItemText.js';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\nimport ActionsMenu from '@/ui/actions/ActionsMenu.jsx';\n\n/**\n * Cómo se acomoda el renglón de abajo (footer y «Ver detalle»):\n * spaceBetween el footer a la izquierda y «Ver detalle» en la otra orilla (lo de siempre);\n * start los dos juntos, a la izquierda;\n * end los dos juntos, a la derecha.\n */\nexport const RECORD_CARD_FOOTER_ALIGN = Object.freeze({spaceBetween: 'spaceBetween', start: 'start', end: 'end'});\n// cómo se reparte el renglón en cada caso: con spaceBetween el footer se estira y empuja «Ver detalle» a la orilla\nconst FOOTER_JUSTIFY = Object.freeze({spaceBetween: 'flex-start', start: 'flex-start', end: 'flex-end'});\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaRecordCard)\nconst THEME_NAME = 'CalcitaRecordCard';\nconst RecordCardRoot = piecePart(THEME_NAME, 'Root', Paper);\nconst RecordCardHeader = piecePart(THEME_NAME, 'Header', Stack);\nconst RecordCardAvatar = piecePart(THEME_NAME, 'Avatar', Avatar);\nconst RecordCardTitles = piecePart(THEME_NAME, 'Titles', Box);\nconst RecordCardStatus = piecePart(THEME_NAME, 'Status', Stack);\nconst RecordCardBody = piecePart(THEME_NAME, 'Body', Box);\nconst RecordCardGroup = piecePart(THEME_NAME, 'Group', Box);\nconst RecordCardFact = piecePart(THEME_NAME, 'Fact', Box);\nconst RecordCardHighlight = piecePart(THEME_NAME, 'Highlight', Box);\nconst RecordCardFooter = piecePart(THEME_NAME, 'Footer', Stack);\nconst RecordCardDetails = piecePart(THEME_NAME, 'Details', Box);\n\n/**\n * @param {object} props\n * @param {import('react').ReactNode} props.title el nombre del registro, ya traducido\n * @param {import('react').ReactNode} [props.eyebrow] arriba del título: el folio o el tipo\n * @param {import('react').ReactNode} [props.subtitle] abajo del título\n * @param {string|import('react').ReactNode} [props.avatar] un texto (de él salen las iniciales) o un ícono\n * @param {import('react').ReactNode} [props.status] a la derecha: StatusChip, IconBadge…\n * @param {Parameters<typeof import('@/ui/actions/ActionsMenu.jsx').default>[0]['actions']} [props.actions] el menú «⋯», con sus secciones,\n * separadores, ligas y submenús (los renglones de ActionsMenu)\n * @param {Array<{key?: string, title?: import('react').ReactNode, facts?: Array<{label: import('react').ReactNode, value: import('react').ReactNode, emphasis?: boolean}>, content?: import('react').ReactNode}>} [props.groups]\n * los datos, en grupos; emphasis los pone en negritas; content en lugar de facts para un grupo libre (una barra, unas\n * insignias, un enlace)\n * @param {import('react').ReactNode} [props.header] un encabezado propio en lugar del de la casa (avatar, títulos, estatus)\n * @param {import('react').ReactNode} [props.children] contenido libre entre el cuerpo y lo de abajo\n * @param {{label: import('react').ReactNode, value: import('react').ReactNode, caption?: import('react').ReactNode}} [props.highlight] el dato principal\n * @param {import('react').ReactNode} [props.trailing] la última columna del cuerpo, después de los grupos y del dato principal\n * (el estatus o unos botones, cuando el orden obligatorio los pone al final)\n * @param {string} [props.columns] en escritorio, el ancho de cada columna del cuerpo como en CSS ('1.2fr 1fr 1fr auto'); por\n * omisión, los grupos a partes iguales y el dato principal y trailing a su medida\n * @param {import('react').ReactNode} [props.footer] abajo, a todo lo ancho\n * @param {'spaceBetween'|'start'|'end'} [props.footerAlign] uno de RECORD_CARD_FOOTER_ALIGN; por omisión spaceBetween\n * @param {import('react').ReactNode} [props.details] lo que se despliega con «Ver detalle»\n * @param {string} [props.detailsLabel] ya traducido; por omisión «Ver detalle»\n * @param {boolean} [props.defaultDetailsOpen]\n * @param {(isOpen: boolean) => void} [props.onDetailsToggle]\n * @param {() => void} [props.onOpen] el botón para abrir el registro (su detalle)\n * @param {string} [props.openLabel] ya traducido: el nombre de ese botón («Abrir el pedido P-1203»)\n * @param {object} [props.sx]\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nfunction RecordCardContent(props) {\n const {\n title, eyebrow, subtitle, avatar, status, actions, groups = [], highlight, trailing, columns, footer,\n footerAlign = RECORD_CARD_FOOTER_ALIGN.spaceBetween, details, detailsLabel, defaultDetailsOpen = false,\n onDetailsToggle, onOpen, openLabel, header, children, sx, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const titleId = useId();\n const detailsId = useId();\n const [isDetailsOpen, setIsDetailsOpen] = useState(defaultDetailsOpen);\n const toggleDetails = () => {\n setIsDetailsOpen((current) => !current);\n onDetailsToggle?.(!isDetailsOpen);\n };\n const hasBody = groups.length > 0 || highlight || trailing;\n // las columnas en escritorio: las que se pidan, o los grupos a partes iguales y lo demás a su medida\n const desktopColumns = columns ?? `${groups.length > 0 ? `repeat(${groups.length}, minmax(0, 1fr))` : ''}${highlight ? ' auto' : ''}${trailing ? ' auto' : ''}`.trim();\n\n return (\n <RecordCardRoot variant=\"outlined\" component=\"article\" aria-labelledby={titleId} ownSx={{p: {xs: 2, sm: 2.5}, minWidth: 0}} sx={sx}>\n {/* en el teléfono el estatus baja abajo del título (si no, le quitaba medio ancho) y arriba quedan los botones */}\n {header ?? (\n <RecordCardHeader direction=\"row\" ownSx={{gap: 1.5, alignItems: 'flex-start', flexWrap: {xs: 'wrap', sm: 'nowrap'}}}>\n {avatar && (\n <RecordCardAvatar ownSx={{width: 44, height: 44, bgcolor: 'action.selected', color: 'text.primary', fontSize: 16, fontWeight: 600}}>\n {typeof avatar === 'string' ? initialsOf(avatar) : avatar}\n </RecordCardAvatar>\n )}\n <RecordCardTitles ownSx={{flex: 1, minWidth: 0, order: 1}}>\n {eyebrow && <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\" sx={{fontVariantNumeric: 'tabular-nums'}}>{eyebrow}</Typography>}\n <Typography id={titleId} variant=\"subtitle1\" component=\"h3\" sx={{fontWeight: 600, overflowWrap: 'anywhere'}}>{title}</Typography>\n {subtitle && <Typography variant=\"body2\" color=\"text.secondary\">{subtitle}</Typography>}\n </RecordCardTitles>\n {status && (\n <RecordCardStatus direction=\"row\" ownSx={{\n gap: 0.75, alignItems: 'center', flexWrap: 'wrap', order: {xs: 3, sm: 2},\n flexBasis: {xs: '100%', sm: 'auto'}, justifyContent: {xs: 'flex-start', sm: 'flex-end'}, maxWidth: {sm: '50%'},\n }}>\n {status}\n </RecordCardStatus>\n )}\n <Stack direction=\"row\" sx={{gap: 0.25, alignItems: 'center', order: {xs: 2, sm: 3}, flexShrink: 0}}>\n {onOpen && (\n <Tooltip title={openLabel ?? t('recordCard.open')}>\n <IconButton size=\"small\" aria-label={openLabel ?? t('recordCard.open')} onClick={onOpen}><OpenInNewRoundedIcon fontSize=\"small\" /></IconButton>\n </Tooltip>\n )}\n {actions?.length > 0 && <ActionsMenu label={t('recordCard.actions')} actions={actions} size=\"small\" />}\n </Stack>\n </RecordCardHeader>\n )}\n\n {hasBody && (\n <RecordCardBody ownSx={{\n mt: 2, display: 'grid', gap: {xs: 1.5, md: 3}, alignItems: 'start',\n // en escritorio los grupos lado a lado y el dato principal al final; en el teléfono, uno debajo de otro\n gridTemplateColumns: {xs: '1fr', md: desktopColumns},\n }}>\n {groups.map((group, groupIndex) => (\n <RecordCardGroup key={group.key ?? groupIndex} ownSx={{\n minWidth: 0, pt: {xs: groupIndex > 0 ? 1.5 : 0, md: 0}, borderTop: {xs: groupIndex > 0 ? 1 : 0, md: 0}, borderColor: 'divider',\n }}>\n {group.title && <Typography variant=\"overline\" color=\"text.secondary\" component=\"p\" sx={{lineHeight: 1.8}}>{group.title}</Typography>}\n {group.content ?? (\n <Box component=\"dl\" sx={{m: 0, display: 'grid', gridTemplateColumns: 'auto 1fr', columnGap: 1.5, rowGap: 0.5}}>\n {(group.facts ?? []).map((fact, factIndex) => (\n <RecordCardFact key={factIndex} ownSx={{display: 'contents'}}>\n <Typography component=\"dt\" variant=\"body2\" color=\"text.secondary\">{fact.label}</Typography>\n <Typography component=\"dd\" variant=\"body2\" sx={{m: 0, textAlign: 'right', fontVariantNumeric: 'tabular-nums', fontWeight: fact.emphasis ? 600 : 400, overflowWrap: 'anywhere'}}>\n {fact.value}\n </Typography>\n </RecordCardFact>\n ))}\n </Box>\n )}\n </RecordCardGroup>\n ))}\n {highlight && (\n <RecordCardHighlight ownSx={{\n textAlign: {xs: 'left', md: 'right'}, pl: {md: 3}, borderLeft: {md: 1}, borderTop: {xs: 1, md: 0}, pt: {xs: 1.5, md: 0}, borderColor: 'divider',\n }}>\n <Typography variant=\"overline\" color=\"text.secondary\" component=\"p\" sx={{lineHeight: 1.8}}>{highlight.label}</Typography>\n <Typography variant=\"h5\" component=\"p\" sx={{fontWeight: 700, fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap'}}>{highlight.value}</Typography>\n {highlight.caption && <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\">{highlight.caption}</Typography>}\n </RecordCardHighlight>\n )}\n {trailing && (\n <Box sx={{minWidth: 0, alignSelf: 'center', borderTop: {xs: 1, md: 0}, pt: {xs: 1.5, md: 0}, borderColor: 'divider'}}>{trailing}</Box>\n )}\n </RecordCardBody>\n )}\n\n {children}\n {(footer || details) && (\n <RecordCardFooter direction=\"row\" ownSx={{mt: 2, gap: 1, alignItems: 'center', flexWrap: 'wrap', justifyContent: FOOTER_JUSTIFY[footerAlign] ?? 'flex-start'}}>\n <Box sx={{flex: footerAlign === RECORD_CARD_FOOTER_ALIGN.spaceBetween ? 1 : '0 1 auto', minWidth: 0}}>{footer}</Box>\n {details && (\n <Button size=\"small\" onClick={toggleDetails} aria-expanded={isDetailsOpen} aria-controls={detailsId}\n endIcon={<ExpandMoreRoundedIcon sx={{transform: isDetailsOpen ? 'rotate(180deg)' : 'none', transition: 'transform 200ms'}} />}>\n {isDetailsOpen ? t('recordCard.hideDetails') : (detailsLabel ?? t('recordCard.showDetails'))}\n </Button>\n )}\n </RecordCardFooter>\n )}\n {details && (\n <Collapse in={isDetailsOpen} unmountOnExit>\n <RecordCardDetails id={detailsId} ownSx={{mt: 2, pt: 2, borderTop: 1, borderColor: 'divider'}}>{details}</RecordCardDetails>\n </Collapse>\n )}\n </RecordCardRoot>\n );\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst RecordCard = withPieceSlots(RecordCardContent, THEME_NAME);\nexport default RecordCard;\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"BackButton.js","sources":["../../../src/ui/navigation/BackButton.jsx"],"sourcesContent":["// Botón de regreso por el ROUTER, no por un enlace fijo.\n//\n// navigate(-1) regresa a la entrada anterior del historial con sus parámetros intactos: los filtros, el orden, la\n// pestaña y la página que nuqs dejó en la URL de la lista siguen ahí. Un enlace fijo a la lista los perdería.\n//\n// Si alguien llegó directo (pegó el enlace del detalle, abrió en pestaña nueva, recargó después de un login) no hay\n// historial al que volver: se usa la última URL de la lista que se guardó en sessionStorage al salir de ella\n// (useRememberListUrl) y, si tampoco hay, fallbackTo.\n//\n// El TEXTO dice a dónde regresa: si la página anterior anotó su nombre de regreso (usePageName de navigationTrail.js),\n// se pinta ese («Regresar al detalle del pedido 15»); si no, o si no hay historial, el label que le pasen.\n//\n// Navega con la navegación del proveedor (ToolkitProvider), no con un router en particular.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaBackButton con defaultProps, styleOverrides de\n// sus partes (root) y slots/slotProps para cambiarlas.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Button from '@mui/material/Button';\nimport ArrowBackIcon from '@mui/icons-material/ArrowBack';\n\nimport {recallListUrl} from '@/lastListUrl.js';\nimport {currentHistoryIndex, pageNameAt} from '@/navigationTrail.js';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaBackButton)\nconst THEME_NAME = 'CalcitaBackButton';\nconst BackButtonRoot = piecePart(THEME_NAME, 'Root', Button);\n\n/**\n * @param {object} props\n * @param {string} props.fallbackTo ruta de la lista sin filtros (último respaldo)\n * @param {string} [props.listKey] la misma llave que usa useRememberListUrl en la lista\n * @param {string} [props.label] ya traducido; se usa cuando la página anterior no anotó su nombre o no hay historial\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nfunction BackButtonContent(props) {\n const {fallbackTo, listKey, label, texts, ...buttonProps} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('common', texts);\n const {useNavigate} = useToolkitNavigation();\n const navigate = useNavigate();\n\n // react-router guarda en history.state un índice: 0 = primera entrada de esta pestaña en la app. Otro router no lo\n // guarda: entonces se toma como si no hubiera historial y se regresa a la última lista o a fallbackTo\n const historyIndex = currentHistoryIndex();\n const hasHistory = historyIndex > 0;\n const previousPageName = hasHistory ? pageNameAt(historyIndex - 1) : null;\n\n const goBack = () => {\n if (hasHistory) {\n navigate(-1);\n return;\n }\n navigate((listKey && recallListUrl(listKey)) || fallbackTo);\n };\n\n return (\n <BackButtonRoot size=\"small\" color=\"inherit\" startIcon={<ArrowBackIcon />} onClick={goBack} {...buttonProps}>\n {previousPageName ? t(previousPageName.labelKey, previousPageName.params) : (label ?? t('actions.back'))}\n </BackButtonRoot>\n );\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst BackButton = withPieceSlots(BackButtonContent, THEME_NAME);\nexport default BackButton;\n"],"names":[],"mappings":";;;;;;;;;AA4BA,MAAM,aAAa;AACnB,MAAM,iBAAiB,UAAU,YAAY,QAAQ,MAAM;AAS3D,SAAS,kBAAkB,OAAO;AAC9B,QAAM,EAAC,YAAY,SAAS,OAAO,OAAO,GAAG,gBAAe,cAAc,OAAO,UAAU;AAC3F,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AACjD,QAAM,EAAC,YAAA,IAAe,qBAAA;AACtB,QAAM,WAAW,YAAA;AAIjB,QAAM,eAAe,oBAAA;AACrB,QAAM,aAAa,eAAe;AAClC,QAAM,mBAAmB,aAAa,WAAW,eAAe,CAAC,IAAI;AAErE,QAAM,SAAS,MAAM;AACjB,QAAI,YAAY;AACZ,eAAS,EAAE;AACX;AAAA,IACJ;AACA,aAAU,WAAW,cAAc,OAAO,KAAM,UAAU;AAAA,EAC9D;AAEA,SACI,oBAAC,gBAAA,EAAe,MAAK,SAAQ,OAAM,WAAU,WAAW,oBAAC,eAAA,CAAA,CAAc,GAAI,SAAS,QAAS,GAAG,aAC3F,UAAA,mBAAmB,EAAE,iBAAiB,UAAU,iBAAiB,MAAM,IAAK,SAAS,EAAE,cAAc,EAAA,CAC1G;AAER;AAGA,MAAM,aAAa,eAAe,mBAAmB,UAAU;"}
1
+ {"version":3,"file":"BackButton.js","sources":["../../../src/ui/navigation/BackButton.jsx"],"sourcesContent":["// Botón de regreso por el ROUTER, no por un enlace fijo.\n//\n// navigate(-1) regresa a la entrada anterior del historial con sus parámetros intactos: los filtros, el orden, la\n// pestaña y la página que nuqs dejó en la URL de la lista siguen ahí. Un enlace fijo a la lista los perdería.\n//\n// Si alguien llegó directo (pegó el enlace del detalle, abrió en pestaña nueva, recargó después de un login) no hay\n// historial al que volver: se usa la última URL de la lista que se guardó en sessionStorage al salir de ella\n// (useRememberListUrl) y, si tampoco hay, fallbackTo.\n//\n// El TEXTO dice a dónde regresa: si la página anterior anotó su nombre de regreso (usePageName de navigationTrail.js),\n// se pinta ese («Regresar al detalle del pedido 15»); si no, o si no hay historial, el label que le pasen.\n//\n// Navega con la navegación del proveedor (ToolkitProvider), no con un router en particular.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaBackButton con defaultProps, styleOverrides de\n// sus partes (root) y slots/slotProps para cambiarlas.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Button from '@mui/material/Button';\nimport ArrowBackIcon from '@mui/icons-material/ArrowBack';\n\nimport {recallListUrl} from '@/lastListUrl.js';\nimport {currentHistoryIndex, pageNameAt} from '@/navigationTrail.js';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaBackButton)\nconst THEME_NAME = 'CalcitaBackButton';\nconst BackButtonRoot = piecePart(THEME_NAME, 'Root', Button);\n\n/**\n * @param {object} props\n * @param {string} props.fallbackTo ruta de la lista sin filtros (último respaldo)\n * @param {string} [props.listKey] la misma llave que usa useRememberListUrl en la lista\n * @param {string} [props.label] ya traducido; se usa cuando la página anterior no anotó su nombre o no hay historial\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n * @param {object} [props.sx] ajustes del botón (un margen, por ejemplo); lo demás que se le pase también llega al Button de MUI\n */\nfunction BackButtonContent(props) {\n const {fallbackTo, listKey, label, texts, ...buttonProps} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('common', texts);\n const {useNavigate} = useToolkitNavigation();\n const navigate = useNavigate();\n\n // react-router guarda en history.state un índice: 0 = primera entrada de esta pestaña en la app. Otro router no lo\n // guarda: entonces se toma como si no hubiera historial y se regresa a la última lista o a fallbackTo\n const historyIndex = currentHistoryIndex();\n const hasHistory = historyIndex > 0;\n const previousPageName = hasHistory ? pageNameAt(historyIndex - 1) : null;\n\n const goBack = () => {\n if (hasHistory) {\n navigate(-1);\n return;\n }\n navigate((listKey && recallListUrl(listKey)) || fallbackTo);\n };\n\n return (\n <BackButtonRoot size=\"small\" color=\"inherit\" startIcon={<ArrowBackIcon />} onClick={goBack} {...buttonProps}>\n {previousPageName ? t(previousPageName.labelKey, previousPageName.params) : (label ?? t('actions.back'))}\n </BackButtonRoot>\n );\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst BackButton = withPieceSlots(BackButtonContent, THEME_NAME);\nexport default BackButton;\n"],"names":[],"mappings":";;;;;;;;;AA4BA,MAAM,aAAa;AACnB,MAAM,iBAAiB,UAAU,YAAY,QAAQ,MAAM;AAU3D,SAAS,kBAAkB,OAAO;AAC9B,QAAM,EAAC,YAAY,SAAS,OAAO,OAAO,GAAG,gBAAe,cAAc,OAAO,UAAU;AAC3F,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AACjD,QAAM,EAAC,YAAA,IAAe,qBAAA;AACtB,QAAM,WAAW,YAAA;AAIjB,QAAM,eAAe,oBAAA;AACrB,QAAM,aAAa,eAAe;AAClC,QAAM,mBAAmB,aAAa,WAAW,eAAe,CAAC,IAAI;AAErE,QAAM,SAAS,MAAM;AACjB,QAAI,YAAY;AACZ,eAAS,EAAE;AACX;AAAA,IACJ;AACA,aAAU,WAAW,cAAc,OAAO,KAAM,UAAU;AAAA,EAC9D;AAEA,SACI,oBAAC,gBAAA,EAAe,MAAK,SAAQ,OAAM,WAAU,WAAW,oBAAC,eAAA,CAAA,CAAc,GAAI,SAAS,QAAS,GAAG,aAC3F,UAAA,mBAAmB,EAAE,iBAAiB,UAAU,iBAAiB,MAAM,IAAK,SAAS,EAAE,cAAc,EAAA,CAC1G;AAER;AAGA,MAAM,aAAa,eAAe,mBAAmB,UAAU;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackcraftmx/calcita",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "Componentes y utilidades de React con MUI para arrancar proyectos rápido: tabla de servidor, diálogos, avisos, descargas, formularios, formatos y reportes en Excel.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "calcifux",