@stackcraftmx/calcita 0.53.0 → 0.55.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.
Files changed (47) hide show
  1. package/README.md +87 -2
  2. package/dist/api/createApiClient.js +11 -3
  3. package/dist/api/createApiClient.js.map +1 -1
  4. package/dist/filepond/FileDropzone.js +60 -19
  5. package/dist/filepond/FileDropzone.js.map +1 -1
  6. package/dist/filepond/pondServer.js +86 -27
  7. package/dist/filepond/pondServer.js.map +1 -1
  8. package/dist/forms/FormWizard.js +22 -17
  9. package/dist/forms/FormWizard.js.map +1 -1
  10. package/dist/hookForm/FormWizard.js +22 -17
  11. package/dist/hookForm/FormWizard.js.map +1 -1
  12. package/dist/index.js +4 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/inputs/FormSection.js +40 -13
  15. package/dist/inputs/FormSection.js.map +1 -1
  16. package/dist/inputs/WizardLayout.js +25 -4
  17. package/dist/inputs/WizardLayout.js.map +1 -1
  18. package/dist/inputs/formLayoutContext.js +1 -1
  19. package/dist/inputs/formLayoutContext.js.map +1 -1
  20. package/dist/inputs/useWizardSteps.js +3 -0
  21. package/dist/inputs/useWizardSteps.js.map +1 -1
  22. package/dist/locales/en.js +1 -0
  23. package/dist/locales/en.js.map +1 -1
  24. package/dist/locales/es.js +1 -0
  25. package/dist/locales/es.js.map +1 -1
  26. package/dist/types/api/createApiClient.d.ts +10 -0
  27. package/dist/types/filepond/FileDropzone.d.ts +5 -0
  28. package/dist/types/filepond/chunkedUpload.d.ts +122 -0
  29. package/dist/types/filepond/pondServer.d.ts +25 -3
  30. package/dist/types/forms/FormWizard.d.ts +4 -0
  31. package/dist/types/hookForm/FormWizard.d.ts +4 -0
  32. package/dist/types/index.d.ts +2 -1
  33. package/dist/types/inputs/FormSection.d.ts +14 -0
  34. package/dist/types/inputs/WizardLayout.d.ts +1 -0
  35. package/dist/types/inputs/formLayoutContext.d.ts +8 -7
  36. package/dist/types/inputs/useWizardSteps.d.ts +2 -1
  37. package/dist/types/locales/en.d.ts +1 -0
  38. package/dist/types/locales/es.d.ts +1 -0
  39. package/dist/types/upload/chunkedUpload.d.ts +189 -0
  40. package/dist/types/upload/uploadWithProgress.d.ts +4 -2
  41. package/dist/upload/FileUploadInput.js +1 -1
  42. package/dist/upload/FileUploadInput.js.map +1 -1
  43. package/dist/upload/chunkedUpload.js +261 -0
  44. package/dist/upload/chunkedUpload.js.map +1 -0
  45. package/dist/upload/uploadWithProgress.js +8 -3
  46. package/dist/upload/uploadWithProgress.js.map +1 -1
  47. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"FileDropzone.js","sources":["../../src/filepond/FileDropzone.jsx"],"sourcesContent":["// La zona para soltar archivos con FilePond por dentro (react-filepond): el MISMO uso que la de MUI (FileDropzone de la\n// puerta principal) más varios archivos, animaciones, el error dentro de cada archivo y, si se quiere, la subida de cada\n// archivo con su propia barra de avance (server). Cambiar de una a otra es cambiar el import.\n//\n// Solo ELIGE archivos (controlado: value / onChange). Enviarlos es de la pantalla: arma su FormData y los sube\n// (uploadWithProgress + uploadMachine). Huecos: actions (botones abajo) y children (lo que va debajo).\n//\n// Configuración, además de la de la zona de MUI (accept, maxSize, hint, buttonLabel, disabled, busy, busyModal, onReject):\n// multiple varios archivos: value / onChange hablan en ARREGLOS de File\n// maxFiles tope de archivos (solo con multiple)\n// maxTotalSize tope de lo que pesan todos juntos (solo con multiple), con unidades como maxSize\n// validate revisión del CONTENIDO al entrar: async (file) => true | 'mensaje de rechazo' (un .xml bien formado\n// puede no ser una factura); lo que no pasa no entra a la lista\n// server subir cada archivo por su cuenta con su barra (createPondServer, o un process propio)\n// instantUpload con server, sube al soltar (por omisión NO: regla de la casa, sube cuando la persona oprime el botón,\n// que llama apiRef.current.processFiles())\n// apiRef la API de FilePond para ese botón (processFiles, getFiles, removeFiles…). OJO: processFiles() regresa\n// una promesa que FALLA si algún archivo no subió; el porqué ya se ve en su tarjeta, pero hay que\n// atraparla (.catch) o queda un error suelto en la consola\n// onProcessProgress({done, total}) el «subidos 2 de 5» del modo server\n// autoReject (por omisión sí) lo que no es de un tipo permitido enseña su error un momento y se quita solo\n//\n// OJO, trampa pagada (react-filepond#198): react-filepond no siempre refresca sus funciones después de montar, y puede\n// quedarse para siempre con la del primer render: quitar el archivo no apagaba el botón porque se comparaba contra un\n// value viejo. Blindaje: las funciones que recibe FilePond no cambian nunca (useCallback sin dependencias) y todo lo que\n// necesitan de afuera lo leen de UN ref que cada render vuelve a apuntar (latest).\n//\n// OJO: FilePond va NO controlado. Pasarle los archivos como prop revive archivos recién quitados; la verdad es FilePond y\n// value es su espejo. Lo único que se manda de afuera hacia adentro es LIMPIAR (value vacío vacía la zona).\n//\n// Los colores salen del tema (claro y oscuro) con selectores que le ganan a la hoja de FilePond sin importar el orden.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaPondDropzone con defaultProps y\n// styleOverrides de sus partes: root, summary, limits, actions, extra.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useCallback, useEffect, useId, useMemo, useRef} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Box from '@mui/material/Box';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport {FileStatus} from 'filepond';\nimport FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size';\nimport FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';\nimport {FilePond, registerPlugin} from 'react-filepond';\n// la hoja de FilePond la carga el empaquetador del proyecto junto con la zona (en los tipos se quita: ver\n// scripts/stripStyleImportsFromTypes.js)\nimport 'filepond/dist/filepond.min.css';\n\nimport ModalWaiting from '@/feedback/ModalWaiting.jsx';\nimport {toastError} from '@/feedback/toast.jsx';\nimport {accentTextStylesOf} from '@/theme/glassSurface.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {FILE_REJECTION, parseFileSize, resolveAccept} from '@/upload/fileRules.js';\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.CalcitaPondDropzone)\nconst THEME_NAME = 'CalcitaPondDropzone';\n// la raíz: el estilo de FilePond de Calcita abajo, el tema del proyecto encima y el sx de la pantalla al final\nconst PondRoot = piecePart(THEME_NAME, 'Root', Box);\nconst PondSummary = piecePart(THEME_NAME, 'Summary', Typography);\nconst PondLimits = piecePart(THEME_NAME, 'Limits', Typography);\nconst PondActions = piecePart(THEME_NAME, 'Actions', Stack);\nconst PondExtra = piecePart(THEME_NAME, 'Extra', Box);\n\nregisterPlugin(FilePondPluginFileValidateType, FilePondPluginFileValidateSize);\n\n// cuánto se ve el error de un archivo de tipo no permitido antes de quitarse solo: lo suficiente para leer por qué\nconst AUTO_REJECT_DELAY_MILLISECONDS = 1800;\n// el aviso de FilePond cuando se sueltan más archivos de los permitidos\nconst MAX_FILES_WARNING = 'max files';\n\n/**\n * El tipo de un archivo para el complemento de tipos: el paquete habla de EXTENSIONES (.xlsx) y FilePond, por omisión,\n * del tipo MIME. Imagen, audio y video van por su tipo (para que 'image/*' funcione); lo demás, por su extensión.\n *\n * @param {File} file\n * @returns {Promise<string>}\n */\nfunction detectFileType(file) {\n const mimeType = (file.type ?? '').toLowerCase();\n if (/^(image|audio|video)\\//.test(mimeType)) {\n return Promise.resolve(mimeType);\n }\n const dotPosition = file.name.lastIndexOf('.');\n return Promise.resolve(dotPosition >= 0 ? file.name.slice(dotPosition).toLowerCase() : mimeType);\n}\n\n/**\n * ¿Los mismos archivos en el mismo orden?\n *\n * @param {File[]} firstFiles\n * @param {File[]} secondFiles\n * @returns {boolean}\n */\nfunction sameFiles(firstFiles, secondFiles) {\n return firstFiles.length === secondFiles.length && firstFiles.every((file, position) => file === secondFiles[position]);\n}\n\n/**\n * value (File | File[] | null) como arreglo.\n *\n * @param {File|File[]|null} value\n * @param {boolean} multiple\n * @returns {File[]}\n */\nfunction filesOf(value, multiple) {\n if (multiple) {\n return Array.isArray(value) ? value : (value ? [value] : []);\n }\n return value ? [value] : [];\n}\n\n/**\n * Los textos que FilePond pinta dentro de cada tarjeta (vienen en inglés), desde los textos del paquete.\n *\n * El de «no se pudo subir» enseña el mensaje del servidor cuando lo hay (createPondServer manda el título del problema\n * RFC 9457): «No se pudo subir» a secas no le dice a la persona qué corregir.\n *\n * @param {(key: string) => string} t traductor de common\n * @returns {object} props de FilePond\n */\nfunction pondLabelsOf(t) {\n return {\n labelFileLoading: t('upload.pond.loading'),\n labelFileLoadError: t('upload.pond.loadError'),\n labelFileWaitingForSize: t('upload.pond.waitingForSize'),\n labelFileSizeNotAvailable: t('upload.pond.sizeNotAvailable'),\n labelInvalidField: t('upload.pond.invalidField'),\n labelFileProcessing: t('upload.pond.processing'),\n labelFileProcessingComplete: t('upload.pond.processingComplete'),\n labelFileProcessingAborted: t('upload.pond.processingAborted'),\n labelFileProcessingError: (error) => error?.body || t('upload.pond.processingError'),\n labelFileProcessingRevertError: t('upload.pond.revertError'),\n labelFileRemoveError: t('upload.pond.removeError'),\n labelTapToCancel: t('upload.pond.tapToCancel'),\n labelTapToRetry: t('upload.pond.tapToRetry'),\n labelTapToUndo: t('upload.pond.tapToUndo'),\n labelButtonRemoveItem: t('upload.pond.remove'),\n labelButtonAbortItemLoad: t('upload.pond.abortLoad'),\n labelButtonRetryItemLoad: t('upload.pond.retryLoad'),\n labelButtonAbortItemProcessing: t('upload.pond.abortProcessing'),\n labelButtonUndoItemProcessing: t('upload.pond.undoProcessing'),\n labelButtonRetryItemProcessing: t('upload.pond.retryProcessing'),\n labelButtonProcessItem: t('upload.pond.processItem'),\n };\n}\n\n/**\n * Los colores de FilePond desde el tema. El «& » los anida dentro de la caja: pesan más que los de la hoja de FilePond\n * aunque esa hoja cargue después.\n *\n * @param {object} theme tema de MUI (con o sin variables CSS)\n * @returns {object}\n */\nfunction pondThemeSxOf(theme) {\n const {palette} = theme.vars ?? theme;\n return {\n '& .filepond--root': {fontFamily: theme.typography.fontFamily, mb: 0},\n '& .filepond--panel-root': {bgcolor: palette.background.paper, border: `2px dashed ${palette.divider}`, borderRadius: 2},\n '& .filepond--drop-label': {color: palette.text.primary},\n // la liga «Elegir archivos»: del color del tema pero legible (accentTextStylesOf); el naranja tal cual se perdía en oscuro\n '& .filepond--label-action': {...accentTextStylesOf(theme), textDecorationColor: 'currentColor'},\n '& .filepond--item-panel': {bgcolor: palette.primary.main},\n '& .filepond--file, & .filepond--file-action-button': {color: palette.primary.contrastText},\n };\n}\n\n/**\n * @param {object} props\n * @param {File|File[]|null} [props.value]\n * @param {(value: File|File[]|null) => void} props.onChange\n * @param {string|string[]} [props.accept]\n * @param {number|string} [props.maxSize] por archivo\n * @param {boolean} [props.multiple]\n * @param {number} [props.maxFiles]\n * @param {number|string} [props.maxTotalSize]\n * @param {string} [props.name] nombre del campo; por omisión uno propio por zona (dos zonas no se pisan)\n * @param {string} [props.hint]\n * @param {string} [props.buttonLabel]\n * @param {boolean} [props.disabled]\n * @param {boolean} [props.busy]\n * @param {boolean|{title?: string, text?: string}} [props.busyModal]\n * @param {boolean} [props.autoReject]\n * @param {((file: File) => Promise<true|string>)|null} [props.validate]\n * @param {object|null} [props.server] createPondServer(...) o {process} propio\n * @param {boolean} [props.instantUpload]\n * @param {{current: object|null}|null} [props.apiRef]\n * @param {(progress: {done: number, total: number}) => void} [props.onProcessProgress]\n * @param {(file: File|null, reason: string, message?: string) => void} [props.onReject] reason de FILE_REJECTION\n * @param {import('react').ReactNode} [props.actions]\n * @param {import('react').ReactNode} [props.children]\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 FileDropzoneContent(props) {\n // lo que el proyecto puso en su tema (components.CalcitaPondDropzone.defaultProps) y encima lo que llega aquí\n const {\n value = null, onChange, accept, maxSize, multiple = false, maxFiles, maxTotalSize, name, hint, buttonLabel, disabled = false,\n busy = false, busyModal = false, autoReject = true, validate = null, server = null, instantUpload = false, apiRef = null,\n onProcessProgress, onReject, actions, children, sx, texts,\n ...unknownProps\n} = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const {formatFileSize} = useFormatters();\n const generatedId = useId();\n const pondName = name ?? `file-dropzone-${generatedId}`;\n // el candado: apagado o subiendo. Va también en cada allow* de FilePond, no solo en lo visual\n const isLocked = disabled || busy;\n\n const resolvedAccept = useMemo(() => resolveAccept(accept), [accept]);\n const acceptedTypes = useMemo(() => (resolvedAccept ? resolvedAccept.split(',') : null), [resolvedAccept]);\n const maxBytes = parseFileSize(maxSize);\n const maxTotalBytes = multiple ? parseFileSize(maxTotalSize) : null;\n const typeErrorLabel = t('upload.invalidTitle');\n const totalErrorLabel = t('upload.totalTooLargeShort');\n const files = useMemo(() => filesOf(value, multiple), [value, multiple]);\n\n const pondRef = useRef(null);\n // lo que las funciones fijas necesitan de afuera, siempre al día (ver la trampa del encabezado)\n const latest = useRef({});\n latest.current = {files, multiple, onChange, onReject, autoReject, validate, onProcessProgress, maxFiles, typeErrorLabel, totalErrorLabel, t};\n\n useEffect(() => {\n if (apiRef) {\n apiRef.current = pondRef.current;\n }\n });\n\n // value desde FilePond. Los archivos con error se quedan en pantalla (en rojo) pero no cuentan: con un archivo malo\n // el botón de enviar no se prende. Y un removeFiles() hecho desde aquí no debe avisar un cambio fantasma\n const syncFromPond = useCallback(() => {\n const {files: currentFiles, multiple: isMultiple, onChange: emitChange} = latest.current;\n const pondItems = pondRef.current?.getFiles?.() ?? [];\n const nextFiles = pondItems.filter((item) => item.status !== FileStatus.LOAD_ERROR).map((item) => item.file);\n if (isMultiple) {\n if (!sameFiles(nextFiles, currentFiles)) {\n emitChange(nextFiles);\n }\n return;\n }\n const nextFile = nextFiles[0] ?? null;\n if (nextFile !== (currentFiles[0] ?? null)) {\n emitChange(nextFile);\n }\n }, []);\n\n const handleError = useCallback((error, item) => {\n // el archivo acaba de quedar con error: se saca de value\n syncFromPond();\n const {onReject: reject, autoReject: shouldAutoReject, typeErrorLabel: typeLabel, totalErrorLabel: totalLabel} = latest.current;\n // el motivo se reconoce por el texto, que es nuestro: comparación estable\n let reason = FILE_REJECTION.size;\n if (error?.main === typeLabel) {\n reason = FILE_REJECTION.type;\n } else if (error?.main === totalLabel) {\n reason = FILE_REJECTION.total;\n }\n // el de tipo no permitido se va solo después de que se lea su error; el de peso se queda para saber cuál pesa de más\n if (reason === FILE_REJECTION.type && shouldAutoReject && item?.id) {\n setTimeout(() => {\n if (pondRef.current?.getFile?.(item.id)) {\n pondRef.current.removeFile(item.id);\n }\n }, AUTO_REJECT_DELAY_MILLISECONDS);\n }\n if (reject && item?.file) {\n reject(item.file, reason);\n }\n }, [syncFromPond]);\n\n // la revisión del contenido, ANTES de que el archivo entre a la lista\n const handleBeforeAdd = useCallback(async (item) => {\n const {validate: judge, onReject: reject, typeErrorLabel: fallbackMessage} = latest.current;\n if (!judge) {\n return true;\n }\n let verdict;\n try {\n verdict = await judge(item.file);\n } catch {\n verdict = false;\n }\n if (verdict === true) {\n return true;\n }\n const message = typeof verdict === 'string' ? verdict : fallbackMessage;\n if (reject) {\n reject(item.file, FILE_REJECTION.content, message);\n } else {\n toastError(`${item.file?.name}: ${message}`);\n }\n return false;\n }, []);\n\n // el «subidos X de N» del modo server: cada archivo que termina su subida avisa la cuenta\n const handleProcessed = useCallback(() => {\n const report = latest.current.onProcessProgress;\n if (!report) {\n return;\n }\n const pondItems = pondRef.current?.getFiles?.() ?? [];\n report({done: pondItems.filter((item) => item.status === FileStatus.PROCESSING_COMPLETE).length, total: pondItems.length});\n }, []);\n\n // lo que pasa de maxFiles ni siquiera entra (FilePond corta lo soltado y solo avisa): el aviso lo damos nosotros\n const handleWarning = useCallback((warning) => {\n const {maxFiles: maxFileCount, onReject: reject, t: translate} = latest.current;\n if (maxFileCount && String(warning?.body ?? '').toLowerCase().includes(MAX_FILES_WARNING)) {\n if (reject) {\n reject(null, FILE_REJECTION.count);\n } else {\n toastError(translate('upload.tooManyFiles', {count: maxFileCount}));\n }\n }\n }, []);\n\n // limpiar desde afuera: value vacío (p. ej. después de subir bien) vacía la zona\n useEffect(() => {\n const pondItems = pondRef.current?.getFiles?.() ?? [];\n if (files.length === 0 && pondItems.length > 0) {\n pondRef.current.removeFiles();\n }\n }, [files]);\n\n const limitHints = [\n maxBytes && t('upload.maxSizeHint', {max: formatFileSize(maxBytes)}),\n maxTotalBytes && t('upload.maxTotalSizeHint', {max: formatFileSize(maxTotalBytes)}),\n multiple && maxFiles && t('upload.maxFilesHint', {count: maxFiles}),\n ].filter(Boolean);\n // con varios archivos se dice cuántos van y cuánto pesan juntos: nadie suma tarjeta por tarjeta\n const totalBytes = multiple ? files.reduce((sum, file) => sum + (file?.size ?? 0), 0) : 0;\n const hintText = hint ?? (multiple ? t('upload.hintMultiple') : t('upload.hint'));\n const chooseText = buttonLabel ?? (multiple ? t('upload.chooseMultiple') : t('upload.choose'));\n\n return (\n <PondRoot ownSx={(theme) => pondThemeSxOf(theme)} sx={sx}>\n <FilePond\n ref={pondRef}\n name={pondName}\n server={server}\n instantUpload={instantUpload}\n beforeAddFile={handleBeforeAdd}\n onprocessfile={handleProcessed}\n onupdatefiles={syncFromPond}\n allowMultiple={multiple}\n maxFiles={multiple ? (maxFiles ?? null) : 1}\n disabled={isLocked}\n allowDrop={!isLocked}\n allowBrowse={!isLocked}\n allowPaste={!isLocked}\n allowRemove={!isLocked}\n allowReplace={!isLocked}\n credits={false}\n acceptedFileTypes={acceptedTypes}\n fileValidateTypeDetectType={detectFileType}\n fileValidateTypeLabelExpectedTypes={resolvedAccept ?? ''}\n maxFileSize={maxBytes ? String(maxBytes) : null}\n maxTotalFileSize={maxTotalBytes ? String(maxTotalBytes) : null}\n labelIdle={`${hintText} — <span class=\"filepond--label-action\">${chooseText}</span>`}\n labelFileTypeNotAllowed={typeErrorLabel}\n labelMaxFileSizeExceeded={t('upload.tooLargeShort')}\n labelMaxFileSize={maxBytes ? t('upload.maxSizeHint', {max: formatFileSize(maxBytes)}) : ''}\n labelMaxTotalFileSizeExceeded={totalErrorLabel}\n labelMaxTotalFileSize={maxTotalBytes ? t('upload.maxTotalSizeHint', {max: formatFileSize(maxTotalBytes)}) : ''}\n onwarning={handleWarning}\n onerror={handleError}\n {...pondLabelsOf(t)}\n />\n {multiple && files.length > 0 && (\n <PondSummary variant=\"body2\" ownSx={{textAlign: 'center', mt: 1, fontWeight: 500}}>\n {maxTotalBytes\n ? t('upload.summaryOf', {count: files.length, total: formatFileSize(totalBytes), max: formatFileSize(maxTotalBytes)})\n : t('upload.summary', {count: files.length, total: formatFileSize(totalBytes)})}\n </PondSummary>\n )}\n {limitHints.length > 0 && (\n <PondLimits variant=\"caption\" color=\"text.secondary\" ownSx={{display: 'block', textAlign: 'center', mt: 0.5}}>\n {limitHints.join(' · ')}\n </PondLimits>\n )}\n {actions && (\n <PondActions direction={{xs: 'column', sm: 'row'}} spacing={2} ownSx={{justifyContent: 'center', alignItems: {xs: 'stretch', sm: 'center'}, mt: 2}}>\n {actions}\n </PondActions>\n )}\n {children && <PondExtra ownSx={{mt: 3}}>{children}</PondExtra>}\n {busyModal && <ModalWaiting open={busy} title={busyModal.title} text={busyModal.text} />}\n </PondRoot>\n );\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst FileDropzone = withPieceSlots(FileDropzoneContent, THEME_NAME);\nexport default FileDropzone;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FileDropzone.js","sources":["../../src/filepond/FileDropzone.jsx"],"sourcesContent":["// La zona para soltar archivos con FilePond por dentro (react-filepond): el MISMO uso que la de MUI (FileDropzone de la\n// puerta principal) más varios archivos, animaciones, el error dentro de cada archivo y, si se quiere, la subida de cada\n// archivo con su propia barra de avance (server). Cambiar de una a otra es cambiar el import.\n//\n// Solo ELIGE archivos (controlado: value / onChange). Enviarlos es de la pantalla: arma su FormData y los sube\n// (uploadWithProgress + uploadMachine). Huecos: actions (botones abajo) y children (lo que va debajo).\n//\n// Configuración, además de la de la zona de MUI (accept, maxSize, hint, buttonLabel, disabled, busy, busyModal, onReject):\n// multiple varios archivos: value / onChange hablan en ARREGLOS de File\n// maxFiles tope de archivos (solo con multiple)\n// maxTotalSize tope de lo que pesan todos juntos (solo con multiple), con unidades como maxSize\n// validate revisión del CONTENIDO al entrar: async (file) => true | 'mensaje de rechazo' (un .xml bien formado\n// puede no ser una factura); lo que no pasa no entra a la lista\n// server subir cada archivo por su cuenta con su barra (createPondServer, o un process propio)\n// instantUpload con server, sube al soltar (por omisión NO: regla de la casa, sube cuando la persona oprime el botón,\n// que llama apiRef.current.processFiles())\n// apiRef la API de FilePond para ese botón (processFiles, getFiles, removeFiles…). OJO: processFiles() regresa\n// una promesa que FALLA si algún archivo no subió; el porqué ya se ve en su tarjeta, pero hay que\n// atraparla (.catch) o queda un error suelto en la consola\n// onProcessProgress({done, total}) el «subidos 2 de 5» del modo server\n// autoReject (por omisión sí) lo que no es de un tipo permitido enseña su error un momento y se quita solo\n//\n// Por trozos, con los nombres de FilePond (así se copia tal cual lo de un sistema que ya subía por trozos):\n// chunkUploads sube por trozos lo que pase de chunkSize (con createPondServer, o con las direcciones de FilePond)\n// chunkForce por trozos TODO, también lo chico (hay servidores que solo reciben por trozos)\n// chunkSize el tamaño de cada trozo, en bytes o con unidades como maxSize ('1 MB'); por omisión el de FilePond, 5 MB\n// chunkRetryDelays las esperas entre reintentos de un trozo, en milisegundos; null o [] = sin reintentos\n// Con createPondServer la barra es honesta: cuenta lo que el servidor ya confirmó y no dice 100 % antes de tiempo\n// (ver chunkedUpload.js).\n//\n// pondProps: cualquier otra opción de FilePond, tal cual (maxParallelUploads, onaddfile, onprocessfiles…). Lo que la zona\n// ya maneja gana: name, server, files, los tipos y pesos, los textos y el candado (disabled y busy apagan soltar, elegir,\n// pegar y quitar aunque pondProps los prenda). Los eventos que la zona ya usa (onupdatefiles, onprocessfile, onwarning,\n// onerror, beforeAddFile) corren después de los suyos; beforeAddFile del proyecto revisa lo que ya pasó validate.\n//\n// OJO, trampa pagada (react-filepond#198): react-filepond no siempre refresca sus funciones después de montar, y puede\n// quedarse para siempre con la del primer render: quitar el archivo no apagaba el botón porque se comparaba contra un\n// value viejo. Blindaje: las funciones que recibe FilePond no cambian nunca (useCallback sin dependencias) y todo lo que\n// necesitan de afuera lo leen de UN ref que cada render vuelve a apuntar (latest).\n//\n// OJO: FilePond va NO controlado. Pasarle los archivos como prop revive archivos recién quitados; la verdad es FilePond y\n// value es su espejo. Lo único que se manda de afuera hacia adentro es LIMPIAR (value vacío vacía la zona).\n//\n// Los colores salen del tema (claro y oscuro) con selectores que le ganan a la hoja de FilePond sin importar el orden.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaPondDropzone con defaultProps y\n// styleOverrides de sus partes: root, summary, limits, actions, extra.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useCallback, useEffect, useId, useMemo, useRef} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Box from '@mui/material/Box';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport {FileStatus} from 'filepond';\nimport FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size';\nimport FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';\nimport {FilePond, registerPlugin} from 'react-filepond';\n// la hoja de FilePond la carga el empaquetador del proyecto junto con la zona (en los tipos se quita: ver\n// scripts/stripStyleImportsFromTypes.js)\nimport 'filepond/dist/filepond.min.css';\n\nimport ModalWaiting from '@/feedback/ModalWaiting.jsx';\nimport {toastError} from '@/feedback/toast.jsx';\nimport {accentTextStylesOf} from '@/theme/glassSurface.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {FILE_REJECTION, parseFileSize, resolveAccept} from '@/upload/fileRules.js';\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.CalcitaPondDropzone)\nconst THEME_NAME = 'CalcitaPondDropzone';\n// la raíz: el estilo de FilePond de Calcita abajo, el tema del proyecto encima y el sx de la pantalla al final\nconst PondRoot = piecePart(THEME_NAME, 'Root', Box);\nconst PondSummary = piecePart(THEME_NAME, 'Summary', Typography);\nconst PondLimits = piecePart(THEME_NAME, 'Limits', Typography);\nconst PondActions = piecePart(THEME_NAME, 'Actions', Stack);\nconst PondExtra = piecePart(THEME_NAME, 'Extra', Box);\n\nregisterPlugin(FilePondPluginFileValidateType, FilePondPluginFileValidateSize);\n\n// cuánto se ve el error de un archivo de tipo no permitido antes de quitarse solo: lo suficiente para leer por qué\nconst AUTO_REJECT_DELAY_MILLISECONDS = 1800;\n// el aviso de FilePond cuando se sueltan más archivos de los permitidos\nconst MAX_FILES_WARNING = 'max files';\n\n/**\n * El tipo de un archivo para el complemento de tipos: el paquete habla de EXTENSIONES (.xlsx) y FilePond, por omisión,\n * del tipo MIME. Imagen, audio y video van por su tipo (para que 'image/*' funcione); lo demás, por su extensión.\n *\n * @param {File} file\n * @returns {Promise<string>}\n */\nfunction detectFileType(file) {\n const mimeType = (file.type ?? '').toLowerCase();\n if (/^(image|audio|video)\\//.test(mimeType)) {\n return Promise.resolve(mimeType);\n }\n const dotPosition = file.name.lastIndexOf('.');\n return Promise.resolve(dotPosition >= 0 ? file.name.slice(dotPosition).toLowerCase() : mimeType);\n}\n\n/**\n * pondProps con sus funciones blindadas contra la trampa del encabezado: cada función que llega se cambia por una fija que\n * llama a la de ESTE render (la lee del ref latest). files se quita: la verdad de la lista es FilePond (ver el OJO).\n *\n * @param {object|null} pondProps\n * @param {{current: Map<string, Function>}} stableFunctions las funciones fijas ya hechas, por nombre\n * @param {{current: {pondProps?: object|null}}} latest\n * @returns {object}\n */\nfunction stablePondPropsOf(pondProps, stableFunctions, latest) {\n const {files: ignoredFiles, ...pondOptions} = pondProps ?? {};\n if (ignoredFiles !== undefined) {\n warnUnknownProps(THEME_NAME, {'pondProps.files': ignoredFiles});\n }\n return Object.fromEntries(Object.entries(pondOptions).map(([option, value]) => {\n if (typeof value !== 'function') {\n return [option, value];\n }\n if (!stableFunctions.current.has(option)) {\n stableFunctions.current.set(option, (...args) => latest.current.pondProps?.[option]?.(...args));\n }\n return [option, stableFunctions.current.get(option)];\n }));\n}\n\n/**\n * Las opciones de trozos que la pantalla puso, para FilePond: solo las que vienen (sin ellas, FilePond sigue con las suyas).\n *\n * @param {{chunkUploads?: boolean, chunkForce?: boolean, chunkSize?: number|string, chunkRetryDelays?: number[]|null}} chunks\n * @returns {object}\n */\nfunction chunkOptionsOf({chunkUploads, chunkForce, chunkSize, chunkRetryDelays}) {\n return {\n ...(chunkUploads !== undefined && {chunkUploads}),\n ...(chunkForce !== undefined && {chunkForce}),\n ...(chunkSize !== undefined && {chunkSize: parseFileSize(chunkSize)}),\n // FilePond truena con null: sin reintentos es una lista vacía\n ...(chunkRetryDelays !== undefined && {chunkRetryDelays: chunkRetryDelays ?? []}),\n };\n}\n\n/**\n * ¿Los mismos archivos en el mismo orden?\n *\n * @param {File[]} firstFiles\n * @param {File[]} secondFiles\n * @returns {boolean}\n */\nfunction sameFiles(firstFiles, secondFiles) {\n return firstFiles.length === secondFiles.length && firstFiles.every((file, position) => file === secondFiles[position]);\n}\n\n/**\n * value (File | File[] | null) como arreglo.\n *\n * @param {File|File[]|null} value\n * @param {boolean} multiple\n * @returns {File[]}\n */\nfunction filesOf(value, multiple) {\n if (multiple) {\n return Array.isArray(value) ? value : (value ? [value] : []);\n }\n return value ? [value] : [];\n}\n\n/**\n * Los textos que FilePond pinta dentro de cada tarjeta (vienen en inglés), desde los textos del paquete.\n *\n * El de «no se pudo subir» enseña el mensaje del servidor cuando lo hay (createPondServer manda el título del problema\n * RFC 9457): «No se pudo subir» a secas no le dice a la persona qué corregir.\n *\n * @param {(key: string) => string} t traductor de common\n * @returns {object} props de FilePond\n */\nfunction pondLabelsOf(t) {\n return {\n labelFileLoading: t('upload.pond.loading'),\n labelFileLoadError: t('upload.pond.loadError'),\n labelFileWaitingForSize: t('upload.pond.waitingForSize'),\n labelFileSizeNotAvailable: t('upload.pond.sizeNotAvailable'),\n labelInvalidField: t('upload.pond.invalidField'),\n labelFileProcessing: t('upload.pond.processing'),\n labelFileProcessingComplete: t('upload.pond.processingComplete'),\n labelFileProcessingAborted: t('upload.pond.processingAborted'),\n labelFileProcessingError: (error) => error?.body || t('upload.pond.processingError'),\n labelFileProcessingRevertError: t('upload.pond.revertError'),\n labelFileRemoveError: t('upload.pond.removeError'),\n labelTapToCancel: t('upload.pond.tapToCancel'),\n labelTapToRetry: t('upload.pond.tapToRetry'),\n labelTapToUndo: t('upload.pond.tapToUndo'),\n labelButtonRemoveItem: t('upload.pond.remove'),\n labelButtonAbortItemLoad: t('upload.pond.abortLoad'),\n labelButtonRetryItemLoad: t('upload.pond.retryLoad'),\n labelButtonAbortItemProcessing: t('upload.pond.abortProcessing'),\n labelButtonUndoItemProcessing: t('upload.pond.undoProcessing'),\n labelButtonRetryItemProcessing: t('upload.pond.retryProcessing'),\n labelButtonProcessItem: t('upload.pond.processItem'),\n };\n}\n\n/**\n * Los colores de FilePond desde el tema. El «& » los anida dentro de la caja: pesan más que los de la hoja de FilePond\n * aunque esa hoja cargue después.\n *\n * @param {object} theme tema de MUI (con o sin variables CSS)\n * @returns {object}\n */\nfunction pondThemeSxOf(theme) {\n const {palette} = theme.vars ?? theme;\n return {\n '& .filepond--root': {fontFamily: theme.typography.fontFamily, mb: 0},\n '& .filepond--panel-root': {bgcolor: palette.background.paper, border: `2px dashed ${palette.divider}`, borderRadius: 2},\n '& .filepond--drop-label': {color: palette.text.primary},\n // la liga «Elegir archivos»: del color del tema pero legible (accentTextStylesOf); el naranja tal cual se perdía en oscuro\n '& .filepond--label-action': {...accentTextStylesOf(theme), textDecorationColor: 'currentColor'},\n '& .filepond--item-panel': {bgcolor: palette.primary.main},\n '& .filepond--file, & .filepond--file-action-button': {color: palette.primary.contrastText},\n };\n}\n\n/**\n * @param {object} props\n * @param {File|File[]|null} [props.value]\n * @param {(value: File|File[]|null) => void} props.onChange\n * @param {string|string[]} [props.accept]\n * @param {number|string} [props.maxSize] por archivo\n * @param {boolean} [props.multiple]\n * @param {number} [props.maxFiles]\n * @param {number|string} [props.maxTotalSize]\n * @param {string} [props.name] nombre del campo; por omisión uno propio por zona (dos zonas no se pisan)\n * @param {string} [props.hint]\n * @param {string} [props.buttonLabel]\n * @param {boolean} [props.disabled]\n * @param {boolean} [props.busy]\n * @param {boolean|{title?: string, text?: string}} [props.busyModal]\n * @param {boolean} [props.autoReject]\n * @param {((file: File) => Promise<true|string>)|null} [props.validate]\n * @param {object|null} [props.server] createPondServer(...) o {process} propio\n * @param {boolean} [props.instantUpload]\n * @param {{current: object|null}|null} [props.apiRef]\n * @param {(progress: {done: number, total: number}) => void} [props.onProcessProgress]\n * @param {(file: File|null, reason: string, message?: string) => void} [props.onReject] reason de FILE_REJECTION\n * @param {boolean} [props.chunkUploads] sube por trozos lo que pase de chunkSize\n * @param {boolean} [props.chunkForce] por trozos todo, también lo chico\n * @param {number|string} [props.chunkSize] en bytes o con unidades ('1 MB'); por omisión 5 MB\n * @param {number[]|null} [props.chunkRetryDelays] esperas entre reintentos de un trozo (ms); null o [] = sin reintentos\n * @param {Partial<import('filepond').FilePondOptions>|null} [props.pondProps] cualquier otra opción de FilePond, tal cual\n * @param {import('react').ReactNode} [props.actions]\n * @param {import('react').ReactNode} [props.children]\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 FileDropzoneContent(props) {\n // lo que el proyecto puso en su tema (components.CalcitaPondDropzone.defaultProps) y encima lo que llega aquí\n const {\n value = null, onChange, accept, maxSize, multiple = false, maxFiles, maxTotalSize, name, hint, buttonLabel, disabled = false,\n busy = false, busyModal = false, autoReject = true, validate = null, server = null, instantUpload = false, apiRef = null,\n onProcessProgress, onReject, chunkUploads, chunkForce, chunkSize, chunkRetryDelays, pondProps = null, actions, children, sx, texts,\n ...unknownProps\n} = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const {formatFileSize} = useFormatters();\n const generatedId = useId();\n const pondName = name ?? `file-dropzone-${generatedId}`;\n // el candado: apagado o subiendo. Va también en cada allow* de FilePond, no solo en lo visual\n const isLocked = disabled || busy;\n\n const resolvedAccept = useMemo(() => resolveAccept(accept), [accept]);\n const acceptedTypes = useMemo(() => (resolvedAccept ? resolvedAccept.split(',') : null), [resolvedAccept]);\n const maxBytes = parseFileSize(maxSize);\n const maxTotalBytes = multiple ? parseFileSize(maxTotalSize) : null;\n const typeErrorLabel = t('upload.invalidTitle');\n const totalErrorLabel = t('upload.totalTooLargeShort');\n const files = useMemo(() => filesOf(value, multiple), [value, multiple]);\n\n const pondRef = useRef(null);\n // lo que las funciones fijas necesitan de afuera, siempre al día (ver la trampa del encabezado)\n const latest = useRef({});\n latest.current = {files, multiple, onChange, onReject, autoReject, validate, onProcessProgress, maxFiles, typeErrorLabel, totalErrorLabel, t, pondProps};\n const stableFunctions = useRef(new Map());\n const pondOptions = stablePondPropsOf(pondProps, stableFunctions, latest);\n // el candado gana: apagado o subiendo no se suelta, elige, pega ni quita aunque pondProps lo prenda\n const pondAllows = (option) => !isLocked && (pondProps?.[option] ?? true);\n\n useEffect(() => {\n if (apiRef) {\n apiRef.current = pondRef.current;\n }\n });\n\n // value desde FilePond. Los archivos con error se quedan en pantalla (en rojo) pero no cuentan: con un archivo malo\n // el botón de enviar no se prende. Y un removeFiles() hecho desde aquí no debe avisar un cambio fantasma\n const syncFromPond = useCallback(() => {\n const {files: currentFiles, multiple: isMultiple, onChange: emitChange} = latest.current;\n const pondItems = pondRef.current?.getFiles?.() ?? [];\n const nextFiles = pondItems.filter((item) => item.status !== FileStatus.LOAD_ERROR).map((item) => item.file);\n if (isMultiple) {\n if (!sameFiles(nextFiles, currentFiles)) {\n emitChange(nextFiles);\n }\n return;\n }\n const nextFile = nextFiles[0] ?? null;\n if (nextFile !== (currentFiles[0] ?? null)) {\n emitChange(nextFile);\n }\n }, []);\n\n const handleError = useCallback((error, item, status) => {\n // el archivo acaba de quedar con error: se saca de value\n syncFromPond();\n const {onReject: reject, autoReject: shouldAutoReject, typeErrorLabel: typeLabel, totalErrorLabel: totalLabel} = latest.current;\n // el motivo se reconoce por el texto, que es nuestro: comparación estable\n let reason = FILE_REJECTION.size;\n if (error?.main === typeLabel) {\n reason = FILE_REJECTION.type;\n } else if (error?.main === totalLabel) {\n reason = FILE_REJECTION.total;\n }\n // el de tipo no permitido se va solo después de que se lea su error; el de peso se queda para saber cuál pesa de más\n if (reason === FILE_REJECTION.type && shouldAutoReject && item?.id) {\n setTimeout(() => {\n if (pondRef.current?.getFile?.(item.id)) {\n pondRef.current.removeFile(item.id);\n }\n }, AUTO_REJECT_DELAY_MILLISECONDS);\n }\n if (reject && item?.file) {\n reject(item.file, reason);\n }\n latest.current.pondProps?.onerror?.(error, item, status);\n }, [syncFromPond]);\n\n // la revisión del contenido, ANTES de que el archivo entre a la lista\n const handleBeforeAdd = useCallback(async (item) => {\n const {validate: judge, onReject: reject, typeErrorLabel: fallbackMessage, pondProps: projectOptions} = latest.current;\n // la revisión del proyecto en pondProps va después de validate: lo que validate rechazó ya no le llega\n const projectAccepts = async () => (projectOptions?.beforeAddFile ? (await projectOptions.beforeAddFile(item)) !== false : true);\n if (!judge) {\n return projectAccepts();\n }\n let verdict;\n try {\n verdict = await judge(item.file);\n } catch {\n verdict = false;\n }\n if (verdict === true) {\n return projectAccepts();\n }\n const message = typeof verdict === 'string' ? verdict : fallbackMessage;\n if (reject) {\n reject(item.file, FILE_REJECTION.content, message);\n } else {\n toastError(`${item.file?.name}: ${message}`);\n }\n return false;\n }, []);\n\n // el «subidos X de N» del modo server: cada archivo que termina su subida avisa la cuenta\n const handleProcessed = useCallback((error, processedItem) => {\n const {onProcessProgress: report, pondProps: projectOptions} = latest.current;\n if (report) {\n const pondItems = pondRef.current?.getFiles?.() ?? [];\n report({done: pondItems.filter((item) => item.status === FileStatus.PROCESSING_COMPLETE).length, total: pondItems.length});\n }\n projectOptions?.onprocessfile?.(error, processedItem);\n }, []);\n\n // lo que pasa de maxFiles ni siquiera entra (FilePond corta lo soltado y solo avisa): el aviso lo damos nosotros\n const handleWarning = useCallback((warning, file, status) => {\n const {maxFiles: maxFileCount, onReject: reject, t: translate, pondProps: projectOptions} = latest.current;\n if (maxFileCount && String(warning?.body ?? '').toLowerCase().includes(MAX_FILES_WARNING)) {\n if (reject) {\n reject(null, FILE_REJECTION.count);\n } else {\n toastError(translate('upload.tooManyFiles', {count: maxFileCount}));\n }\n }\n projectOptions?.onwarning?.(warning, file, status);\n }, []);\n\n // el cambio de la lista: el espejo de value y, después, el evento del proyecto\n const handleUpdateFiles = useCallback((items) => {\n syncFromPond();\n latest.current.pondProps?.onupdatefiles?.(items);\n }, [syncFromPond]);\n\n // limpiar desde afuera: value vacío (p. ej. después de subir bien) vacía la zona\n useEffect(() => {\n const pondItems = pondRef.current?.getFiles?.() ?? [];\n if (files.length === 0 && pondItems.length > 0) {\n pondRef.current.removeFiles();\n }\n }, [files]);\n\n const limitHints = [\n maxBytes && t('upload.maxSizeHint', {max: formatFileSize(maxBytes)}),\n maxTotalBytes && t('upload.maxTotalSizeHint', {max: formatFileSize(maxTotalBytes)}),\n multiple && maxFiles && t('upload.maxFilesHint', {count: maxFiles}),\n ].filter(Boolean);\n // con varios archivos se dice cuántos van y cuánto pesan juntos: nadie suma tarjeta por tarjeta\n const totalBytes = multiple ? files.reduce((sum, file) => sum + (file?.size ?? 0), 0) : 0;\n const hintText = hint ?? (multiple ? t('upload.hintMultiple') : t('upload.hint'));\n const chooseText = buttonLabel ?? (multiple ? t('upload.chooseMultiple') : t('upload.choose'));\n\n return (\n <PondRoot ownSx={(theme) => pondThemeSxOf(theme)} sx={sx}>\n <FilePond\n {...pondOptions}\n {...chunkOptionsOf({chunkUploads, chunkForce, chunkSize, chunkRetryDelays})}\n ref={pondRef}\n name={pondName}\n server={server}\n instantUpload={instantUpload}\n beforeAddFile={handleBeforeAdd}\n onprocessfile={handleProcessed}\n onupdatefiles={handleUpdateFiles}\n allowMultiple={multiple}\n maxFiles={multiple ? (maxFiles ?? null) : 1}\n disabled={isLocked}\n allowDrop={pondAllows('allowDrop')}\n allowBrowse={pondAllows('allowBrowse')}\n allowPaste={pondAllows('allowPaste')}\n allowRemove={pondAllows('allowRemove')}\n allowReplace={pondAllows('allowReplace')}\n credits={false}\n acceptedFileTypes={acceptedTypes}\n fileValidateTypeDetectType={detectFileType}\n fileValidateTypeLabelExpectedTypes={resolvedAccept ?? ''}\n maxFileSize={maxBytes ? String(maxBytes) : null}\n maxTotalFileSize={maxTotalBytes ? String(maxTotalBytes) : null}\n labelIdle={`${hintText} — <span class=\"filepond--label-action\">${chooseText}</span>`}\n labelFileTypeNotAllowed={typeErrorLabel}\n labelMaxFileSizeExceeded={t('upload.tooLargeShort')}\n labelMaxFileSize={maxBytes ? t('upload.maxSizeHint', {max: formatFileSize(maxBytes)}) : ''}\n labelMaxTotalFileSizeExceeded={totalErrorLabel}\n labelMaxTotalFileSize={maxTotalBytes ? t('upload.maxTotalSizeHint', {max: formatFileSize(maxTotalBytes)}) : ''}\n onwarning={handleWarning}\n onerror={handleError}\n {...pondLabelsOf(t)}\n />\n {multiple && files.length > 0 && (\n <PondSummary variant=\"body2\" ownSx={{textAlign: 'center', mt: 1, fontWeight: 500}}>\n {maxTotalBytes\n ? t('upload.summaryOf', {count: files.length, total: formatFileSize(totalBytes), max: formatFileSize(maxTotalBytes)})\n : t('upload.summary', {count: files.length, total: formatFileSize(totalBytes)})}\n </PondSummary>\n )}\n {limitHints.length > 0 && (\n <PondLimits variant=\"caption\" color=\"text.secondary\" ownSx={{display: 'block', textAlign: 'center', mt: 0.5}}>\n {limitHints.join(' · ')}\n </PondLimits>\n )}\n {actions && (\n <PondActions direction={{xs: 'column', sm: 'row'}} spacing={2} ownSx={{justifyContent: 'center', alignItems: {xs: 'stretch', sm: 'center'}, mt: 2}}>\n {actions}\n </PondActions>\n )}\n {children && <PondExtra ownSx={{mt: 3}}>{children}</PondExtra>}\n {busyModal && <ModalWaiting open={busy} title={busyModal.title} text={busyModal.text} />}\n </PondRoot>\n );\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst FileDropzone = withPieceSlots(FileDropzoneContent, THEME_NAME);\nexport default FileDropzone;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AAuEA,MAAM,aAAa;AAEnB,MAAM,WAAW,UAAU,YAAY,QAAQ,GAAG;AAClD,MAAM,cAAc,UAAU,YAAY,WAAW,UAAU;AAC/D,MAAM,aAAa,UAAU,YAAY,UAAU,UAAU;AAC7D,MAAM,cAAc,UAAU,YAAY,WAAW,KAAK;AAC1D,MAAM,YAAY,UAAU,YAAY,SAAS,GAAG;AAEpD,eAAe,gCAAgC,8BAA8B;AAG7E,MAAM,iCAAiC;AAEvC,MAAM,oBAAoB;AAS1B,SAAS,eAAe,MAAM;AAC1B,QAAM,YAAY,KAAK,QAAQ,IAAI,YAAA;AACnC,MAAI,yBAAyB,KAAK,QAAQ,GAAG;AACzC,WAAO,QAAQ,QAAQ,QAAQ;AAAA,EACnC;AACA,QAAM,cAAc,KAAK,KAAK,YAAY,GAAG;AAC7C,SAAO,QAAQ,QAAQ,eAAe,IAAI,KAAK,KAAK,MAAM,WAAW,EAAE,YAAA,IAAgB,QAAQ;AACnG;AAWA,SAAS,kBAAkB,WAAW,iBAAiB,QAAQ;AAC3D,QAAM,EAAC,OAAO,cAAc,GAAG,YAAA,IAAe,aAAa,CAAA;AAC3D,MAAI,iBAAiB,QAAW;AAC5B,qBAAiB,YAAY,EAAC,mBAAmB,aAAA,CAAa;AAAA,EAClE;AACA,SAAO,OAAO,YAAY,OAAO,QAAQ,WAAW,EAAE,IAAI,CAAC,CAAC,QAAQ,KAAK,MAAM;AAC3E,QAAI,OAAO,UAAU,YAAY;AAC7B,aAAO,CAAC,QAAQ,KAAK;AAAA,IACzB;AACA,QAAI,CAAC,gBAAgB,QAAQ,IAAI,MAAM,GAAG;AACtC,sBAAgB,QAAQ,IAAI,QAAQ,IAAI,SAAS,OAAO,QAAQ,YAAY,MAAM,IAAI,GAAG,IAAI,CAAC;AAAA,IAClG;AACA,WAAO,CAAC,QAAQ,gBAAgB,QAAQ,IAAI,MAAM,CAAC;AAAA,EACvD,CAAC,CAAC;AACN;AAQA,SAAS,eAAe,EAAC,cAAc,YAAY,WAAW,oBAAmB;AAC7E,SAAO;AAAA,IACH,GAAI,iBAAiB,UAAa,EAAC,aAAA;AAAA,IACnC,GAAI,eAAe,UAAa,EAAC,WAAA;AAAA,IACjC,GAAI,cAAc,UAAa,EAAC,WAAW,cAAc,SAAS,EAAA;AAAA;AAAA,IAElE,GAAI,qBAAqB,UAAa,EAAC,kBAAkB,oBAAoB,CAAA,EAAC;AAAA,EAAC;AAEvF;AASA,SAAS,UAAU,YAAY,aAAa;AACxC,SAAO,WAAW,WAAW,YAAY,UAAU,WAAW,MAAM,CAAC,MAAM,aAAa,SAAS,YAAY,QAAQ,CAAC;AAC1H;AASA,SAAS,QAAQ,OAAO,UAAU;AAC9B,MAAI,UAAU;AACV,WAAO,MAAM,QAAQ,KAAK,IAAI,QAAS,QAAQ,CAAC,KAAK,IAAI,CAAA;AAAA,EAC7D;AACA,SAAO,QAAQ,CAAC,KAAK,IAAI,CAAA;AAC7B;AAWA,SAAS,aAAa,GAAG;AACrB,SAAO;AAAA,IACH,kBAAkB,EAAE,qBAAqB;AAAA,IACzC,oBAAoB,EAAE,uBAAuB;AAAA,IAC7C,yBAAyB,EAAE,4BAA4B;AAAA,IACvD,2BAA2B,EAAE,8BAA8B;AAAA,IAC3D,mBAAmB,EAAE,0BAA0B;AAAA,IAC/C,qBAAqB,EAAE,wBAAwB;AAAA,IAC/C,6BAA6B,EAAE,gCAAgC;AAAA,IAC/D,4BAA4B,EAAE,+BAA+B;AAAA,IAC7D,0BAA0B,CAAC,UAAU,OAAO,QAAQ,EAAE,6BAA6B;AAAA,IACnF,gCAAgC,EAAE,yBAAyB;AAAA,IAC3D,sBAAsB,EAAE,yBAAyB;AAAA,IACjD,kBAAkB,EAAE,yBAAyB;AAAA,IAC7C,iBAAiB,EAAE,wBAAwB;AAAA,IAC3C,gBAAgB,EAAE,uBAAuB;AAAA,IACzC,uBAAuB,EAAE,oBAAoB;AAAA,IAC7C,0BAA0B,EAAE,uBAAuB;AAAA,IACnD,0BAA0B,EAAE,uBAAuB;AAAA,IACnD,gCAAgC,EAAE,6BAA6B;AAAA,IAC/D,+BAA+B,EAAE,4BAA4B;AAAA,IAC7D,gCAAgC,EAAE,6BAA6B;AAAA,IAC/D,wBAAwB,EAAE,yBAAyB;AAAA,EAAA;AAE3D;AASA,SAAS,cAAc,OAAO;AAC1B,QAAM,EAAC,QAAA,IAAW,MAAM,QAAQ;AAChC,SAAO;AAAA,IACH,qBAAqB,EAAC,YAAY,MAAM,WAAW,YAAY,IAAI,EAAA;AAAA,IACnE,2BAA2B,EAAC,SAAS,QAAQ,WAAW,OAAO,QAAQ,cAAc,QAAQ,OAAO,IAAI,cAAc,EAAA;AAAA,IACtH,2BAA2B,EAAC,OAAO,QAAQ,KAAK,QAAA;AAAA;AAAA,IAEhD,6BAA6B,EAAC,GAAG,mBAAmB,KAAK,GAAG,qBAAqB,eAAA;AAAA,IACjF,2BAA2B,EAAC,SAAS,QAAQ,QAAQ,KAAA;AAAA,IACrD,sDAAsD,EAAC,OAAO,QAAQ,QAAQ,aAAA;AAAA,EAAY;AAElG;AAkCA,SAAS,oBAAoB,OAAO;AAEhC,QAAM;AAAA,IACN,QAAQ;AAAA,IAAM;AAAA,IAAU;AAAA,IAAQ;AAAA,IAAS,WAAW;AAAA,IAAO;AAAA,IAAU;AAAA,IAAc;AAAA,IAAM;AAAA,IAAM;AAAA,IAAa,WAAW;AAAA,IACvH,OAAO;AAAA,IAAO,YAAY;AAAA,IAAO,aAAa;AAAA,IAAM,WAAW;AAAA,IAAM,SAAS;AAAA,IAAM,gBAAgB;AAAA,IAAO,SAAS;AAAA,IACpH;AAAA,IAAmB;AAAA,IAAU;AAAA,IAAc;AAAA,IAAY;AAAA,IAAW;AAAA,IAAkB,YAAY;AAAA,IAAM;AAAA,IAAS;AAAA,IAAU;AAAA,IAAI;AAAA,IAC7H,GAAG;AAAA,EAAA,IACH,cAAc,OAAO,UAAU;AAC/B,mBAAiB,YAAY,YAAY;AACzC,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AACjD,QAAM,EAAC,eAAA,IAAkB,cAAA;AACzB,QAAM,cAAc,MAAA;AACpB,QAAM,WAAW,QAAQ,iBAAiB,WAAW;AAErD,QAAM,WAAW,YAAY;AAE7B,QAAM,iBAAiB,QAAQ,MAAM,cAAc,MAAM,GAAG,CAAC,MAAM,CAAC;AACpE,QAAM,gBAAgB,QAAQ,MAAO,iBAAiB,eAAe,MAAM,GAAG,IAAI,MAAO,CAAC,cAAc,CAAC;AACzG,QAAM,WAAW,cAAc,OAAO;AACtC,QAAM,gBAAgB,WAAW,cAAc,YAAY,IAAI;AAC/D,QAAM,iBAAiB,EAAE,qBAAqB;AAC9C,QAAM,kBAAkB,EAAE,2BAA2B;AACrD,QAAM,QAAQ,QAAQ,MAAM,QAAQ,OAAO,QAAQ,GAAG,CAAC,OAAO,QAAQ,CAAC;AAEvE,QAAM,UAAU,OAAO,IAAI;AAE3B,QAAM,SAAS,OAAO,EAAE;AACxB,SAAO,UAAU,EAAC,OAAO,UAAU,UAAU,UAAU,YAAY,UAAU,mBAAmB,UAAU,gBAAgB,iBAAiB,GAAG,UAAA;AAC9I,QAAM,kBAAkB,OAAO,oBAAI,KAAK;AACxC,QAAM,cAAc,kBAAkB,WAAW,iBAAiB,MAAM;AAExE,QAAM,aAAa,CAAC,WAAW,CAAC,aAAa,YAAY,MAAM,KAAK;AAEpE,YAAU,MAAM;AACZ,QAAI,QAAQ;AACR,aAAO,UAAU,QAAQ;AAAA,IAC7B;AAAA,EACJ,CAAC;AAID,QAAM,eAAe,YAAY,MAAM;AACnC,UAAM,EAAC,OAAO,cAAc,UAAU,YAAY,UAAU,eAAc,OAAO;AACjF,UAAM,YAAY,QAAQ,SAAS,WAAA,KAAgB,CAAA;AACnD,UAAM,YAAY,UAAU,OAAO,CAAC,SAAS,KAAK,WAAW,WAAW,UAAU,EAAE,IAAI,CAAC,SAAS,KAAK,IAAI;AAC3G,QAAI,YAAY;AACZ,UAAI,CAAC,UAAU,WAAW,YAAY,GAAG;AACrC,mBAAW,SAAS;AAAA,MACxB;AACA;AAAA,IACJ;AACA,UAAM,WAAW,UAAU,CAAC,KAAK;AACjC,QAAI,cAAc,aAAa,CAAC,KAAK,OAAO;AACxC,iBAAW,QAAQ;AAAA,IACvB;AAAA,EACJ,GAAG,CAAA,CAAE;AAEL,QAAM,cAAc,YAAY,CAAC,OAAO,MAAM,WAAW;AAErD,iBAAA;AACA,UAAM,EAAC,UAAU,QAAQ,YAAY,kBAAkB,gBAAgB,WAAW,iBAAiB,WAAA,IAAc,OAAO;AAExH,QAAI,SAAS,eAAe;AAC5B,QAAI,OAAO,SAAS,WAAW;AAC3B,eAAS,eAAe;AAAA,IAC5B,WAAW,OAAO,SAAS,YAAY;AACnC,eAAS,eAAe;AAAA,IAC5B;AAEA,QAAI,WAAW,eAAe,QAAQ,oBAAoB,MAAM,IAAI;AAChE,iBAAW,MAAM;AACb,YAAI,QAAQ,SAAS,UAAU,KAAK,EAAE,GAAG;AACrC,kBAAQ,QAAQ,WAAW,KAAK,EAAE;AAAA,QACtC;AAAA,MACJ,GAAG,8BAA8B;AAAA,IACrC;AACA,QAAI,UAAU,MAAM,MAAM;AACtB,aAAO,KAAK,MAAM,MAAM;AAAA,IAC5B;AACA,WAAO,QAAQ,WAAW,UAAU,OAAO,MAAM,MAAM;AAAA,EAC3D,GAAG,CAAC,YAAY,CAAC;AAGjB,QAAM,kBAAkB,YAAY,OAAO,SAAS;AAChD,UAAM,EAAC,UAAU,OAAO,UAAU,QAAQ,gBAAgB,iBAAiB,WAAW,eAAA,IAAkB,OAAO;AAE/G,UAAM,iBAAiB,YAAa,gBAAgB,gBAAiB,MAAM,eAAe,cAAc,IAAI,MAAO,QAAQ;AAC3H,QAAI,CAAC,OAAO;AACR,aAAO,eAAA;AAAA,IACX;AACA,QAAI;AACJ,QAAI;AACA,gBAAU,MAAM,MAAM,KAAK,IAAI;AAAA,IACnC,QAAQ;AACJ,gBAAU;AAAA,IACd;AACA,QAAI,YAAY,MAAM;AAClB,aAAO,eAAA;AAAA,IACX;AACA,UAAM,UAAU,OAAO,YAAY,WAAW,UAAU;AACxD,QAAI,QAAQ;AACR,aAAO,KAAK,MAAM,eAAe,SAAS,OAAO;AAAA,IACrD,OAAO;AACH,iBAAW,GAAG,KAAK,MAAM,IAAI,KAAK,OAAO,EAAE;AAAA,IAC/C;AACA,WAAO;AAAA,EACX,GAAG,CAAA,CAAE;AAGL,QAAM,kBAAkB,YAAY,CAAC,OAAO,kBAAkB;AAC1D,UAAM,EAAC,mBAAmB,QAAQ,WAAW,eAAA,IAAkB,OAAO;AACtE,QAAI,QAAQ;AACR,YAAM,YAAY,QAAQ,SAAS,WAAA,KAAgB,CAAA;AACnD,aAAO,EAAC,MAAM,UAAU,OAAO,CAAC,SAAS,KAAK,WAAW,WAAW,mBAAmB,EAAE,QAAQ,OAAO,UAAU,QAAO;AAAA,IAC7H;AACA,oBAAgB,gBAAgB,OAAO,aAAa;AAAA,EACxD,GAAG,CAAA,CAAE;AAGL,QAAM,gBAAgB,YAAY,CAAC,SAAS,MAAM,WAAW;AACzD,UAAM,EAAC,UAAU,cAAc,UAAU,QAAQ,GAAG,WAAW,WAAW,eAAA,IAAkB,OAAO;AACnG,QAAI,gBAAgB,OAAO,SAAS,QAAQ,EAAE,EAAE,YAAA,EAAc,SAAS,iBAAiB,GAAG;AACvF,UAAI,QAAQ;AACR,eAAO,MAAM,eAAe,KAAK;AAAA,MACrC,OAAO;AACH,mBAAW,UAAU,uBAAuB,EAAC,OAAO,aAAA,CAAa,CAAC;AAAA,MACtE;AAAA,IACJ;AACA,oBAAgB,YAAY,SAAS,MAAM,MAAM;AAAA,EACrD,GAAG,CAAA,CAAE;AAGL,QAAM,oBAAoB,YAAY,CAAC,UAAU;AAC7C,iBAAA;AACA,WAAO,QAAQ,WAAW,gBAAgB,KAAK;AAAA,EACnD,GAAG,CAAC,YAAY,CAAC;AAGjB,YAAU,MAAM;AACZ,UAAM,YAAY,QAAQ,SAAS,WAAA,KAAgB,CAAA;AACnD,QAAI,MAAM,WAAW,KAAK,UAAU,SAAS,GAAG;AAC5C,cAAQ,QAAQ,YAAA;AAAA,IACpB;AAAA,EACJ,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,aAAa;AAAA,IACf,YAAY,EAAE,sBAAsB,EAAC,KAAK,eAAe,QAAQ,GAAE;AAAA,IACnE,iBAAiB,EAAE,2BAA2B,EAAC,KAAK,eAAe,aAAa,GAAE;AAAA,IAClF,YAAY,YAAY,EAAE,uBAAuB,EAAC,OAAO,UAAS;AAAA,EAAA,EACpE,OAAO,OAAO;AAEhB,QAAM,aAAa,WAAW,MAAM,OAAO,CAAC,KAAK,SAAS,OAAO,MAAM,QAAQ,IAAI,CAAC,IAAI;AACxF,QAAM,WAAW,SAAS,WAAW,EAAE,qBAAqB,IAAI,EAAE,aAAa;AAC/E,QAAM,aAAa,gBAAgB,WAAW,EAAE,uBAAuB,IAAI,EAAE,eAAe;AAE5F,SACI,qBAAC,YAAS,OAAO,CAAC,UAAU,cAAc,KAAK,GAAG,IAC9C,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACI,GAAG;AAAA,QACH,GAAG,eAAe,EAAC,cAAc,YAAY,WAAW,kBAAiB;AAAA,QAC1E,KAAK;AAAA,QACL,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,eAAe;AAAA,QACf,eAAe;AAAA,QACf,eAAe;AAAA,QACf,eAAe;AAAA,QACf,UAAU,WAAY,YAAY,OAAQ;AAAA,QAC1C,UAAU;AAAA,QACV,WAAW,WAAW,WAAW;AAAA,QACjC,aAAa,WAAW,aAAa;AAAA,QACrC,YAAY,WAAW,YAAY;AAAA,QACnC,aAAa,WAAW,aAAa;AAAA,QACrC,cAAc,WAAW,cAAc;AAAA,QACvC,SAAS;AAAA,QACT,mBAAmB;AAAA,QACnB,4BAA4B;AAAA,QAC5B,oCAAoC,kBAAkB;AAAA,QACtD,aAAa,WAAW,OAAO,QAAQ,IAAI;AAAA,QAC3C,kBAAkB,gBAAgB,OAAO,aAAa,IAAI;AAAA,QAC1D,WAAW,GAAG,QAAQ,2CAA2C,UAAU;AAAA,QAC3E,yBAAyB;AAAA,QACzB,0BAA0B,EAAE,sBAAsB;AAAA,QAClD,kBAAkB,WAAW,EAAE,sBAAsB,EAAC,KAAK,eAAe,QAAQ,EAAA,CAAE,IAAI;AAAA,QACxF,+BAA+B;AAAA,QAC/B,uBAAuB,gBAAgB,EAAE,2BAA2B,EAAC,KAAK,eAAe,aAAa,EAAA,CAAE,IAAI;AAAA,QAC5G,WAAW;AAAA,QACX,SAAS;AAAA,QACR,GAAG,aAAa,CAAC;AAAA,MAAA;AAAA,IAAA;AAAA,IAErB,YAAY,MAAM,SAAS,yBACvB,aAAA,EAAY,SAAQ,SAAQ,OAAO,EAAC,WAAW,UAAU,IAAI,GAAG,YAAY,IAAA,GACxE,UAAA,gBACK,EAAE,oBAAoB,EAAC,OAAO,MAAM,QAAQ,OAAO,eAAe,UAAU,GAAG,KAAK,eAAe,aAAa,GAAE,IAClH,EAAE,kBAAkB,EAAC,OAAO,MAAM,QAAQ,OAAO,eAAe,UAAU,EAAA,CAAE,GACtF;AAAA,IAEH,WAAW,SAAS,KACjB,oBAAC,cAAW,SAAQ,WAAU,OAAM,kBAAiB,OAAO,EAAC,SAAS,SAAS,WAAW,UAAU,IAAI,OACnG,UAAA,WAAW,KAAK,KAAK,GAC1B;AAAA,IAEH,WACG,oBAAC,aAAA,EAAY,WAAW,EAAC,IAAI,UAAU,IAAI,MAAA,GAAQ,SAAS,GAAG,OAAO,EAAC,gBAAgB,UAAU,YAAY,EAAC,IAAI,WAAW,IAAI,SAAA,GAAW,IAAI,EAAA,GAC3I,UAAA,QAAA,CACL;AAAA,IAEH,gCAAa,WAAA,EAAU,OAAO,EAAC,IAAI,KAAK,UAAS;AAAA,IACjD,aAAa,oBAAC,cAAA,EAAa,MAAM,MAAM,OAAO,UAAU,OAAO,MAAM,UAAU,KAAA,CAAM;AAAA,EAAA,GAC1F;AAER;AAGA,MAAM,eAAe,eAAe,qBAAqB,UAAU;"}
@@ -1,38 +1,96 @@
1
+ import { createLogger } from "../observability/logger.js";
1
2
  import { problemTitle } from "../problems.js";
2
- import { toolkitSettings } from "../toolkit/toolkitSettings.js";
3
+ import { toolkitSettings, translateToolkit } from "../toolkit/toolkitSettings.js";
4
+ import { defaultPatchUrlOf, startChunkedUpload, UploadFailure, UPLOAD_FAILURE, problemOfResponse } from "../upload/chunkedUpload.js";
5
+ import { uploadWithProgress } from "../upload/uploadWithProgress.js";
6
+ const log = createLogger("pond");
3
7
  const OFFLINE_STATUS = 0;
4
- function problemTitleOf(request) {
5
- let problem = null;
6
- try {
7
- problem = JSON.parse(request.responseText);
8
- } catch {
8
+ function usesChunks(file, options) {
9
+ return options?.chunkUploads === true && (file.size > options.chunkSize || options.chunkForce === true);
10
+ }
11
+ async function failureMessageOf(failure, toProblem) {
12
+ if (failure?.reason === UPLOAD_FAILURE.noTransferId) {
13
+ return translateToolkit("common", "upload.pond.noTransferId");
14
+ }
15
+ if (failure?.reason === UPLOAD_FAILURE.noResponse) {
16
+ return problemTitle(null, OFFLINE_STATUS);
17
+ }
18
+ if (failure?.reason === UPLOAD_FAILURE.rejected && failure.response) {
19
+ return problemTitle(await problemOfResponse(failure.response, toProblem), failure.response.status);
9
20
  }
10
- return problemTitle(problem, request.status);
21
+ log.error("La subida falló sin respuesta del servidor", failure);
22
+ return "";
11
23
  }
12
- function createPondServer({ url, fieldName, headers = {}, withCredentials = false }) {
13
- return {
14
- process: (pondFieldName, file, metadata, load, error, progress, abort) => {
15
- const request = new XMLHttpRequest();
16
- request.open("POST", url);
17
- request.withCredentials = withCredentials;
18
- for (const [headerName, headerValue] of Object.entries({ "Accept-Language": toolkitSettings().language, ...headers })) {
19
- request.setRequestHeader(headerName, headerValue);
24
+ function startSingleUpload({ url, fieldName, file, headers, withCredentials, sendRequest, onProgress }) {
25
+ const controller = new AbortController();
26
+ const formData = new FormData();
27
+ formData.append(fieldName, file, file.name);
28
+ const done = sendRequest(url, formData, {
29
+ method: "POST",
30
+ headers,
31
+ withCredentials,
32
+ signal: controller.signal,
33
+ onProgress: (percent, transfer) => {
34
+ if (!controller.signal.aborted) {
35
+ onProgress(transfer.loaded, transfer.total);
36
+ }
37
+ }
38
+ }).then(
39
+ async (response) => {
40
+ if (!response.ok) {
41
+ throw new UploadFailure(UPLOAD_FAILURE.rejected, response);
20
42
  }
21
- request.upload.onprogress = (event) => progress(event.lengthComputable, event.loaded, event.total);
22
- request.onload = () => {
23
- if (request.status >= 200 && request.status < 300) {
24
- load(request.responseText || file.name);
43
+ return await response.text() || file.name;
44
+ },
45
+ (failure) => {
46
+ if (failure?.name === "AbortError") {
47
+ throw failure;
48
+ }
49
+ throw new UploadFailure(UPLOAD_FAILURE.noResponse);
50
+ }
51
+ );
52
+ return { done, abort: () => controller.abort() };
53
+ }
54
+ function createPondServer({ url, fieldName, headers = {}, withCredentials = false, patchUrl, toProblem, sendRequest = uploadWithProgress }) {
55
+ const chunkUrl = patchUrl ?? defaultPatchUrlOf(url);
56
+ return {
57
+ // FilePond llama process con su progress, su folio (transfer) y, al final, sus opciones de trozos
58
+ process: (pondFieldName, file, metadata, load, error, progress, abort, transfer, options = {}) => {
59
+ const requestHeaders = { "Accept-Language": toolkitSettings().language, ...headers };
60
+ const reportProgress = (loaded, total) => progress(true, loaded, total);
61
+ const upload = usesChunks(file, options) ? startChunkedUpload({
62
+ file,
63
+ fileName: file.name,
64
+ fieldName: fieldName ?? pondFieldName,
65
+ metadata,
66
+ url,
67
+ patchUrl: chunkUrl,
68
+ headers: requestHeaders,
69
+ withCredentials,
70
+ chunkSize: options.chunkSize,
71
+ retryDelays: options.chunkRetryDelays,
72
+ transferId: options.chunkTransferId ?? null,
73
+ onTransferId: transfer,
74
+ onProgress: reportProgress,
75
+ sendRequest
76
+ }) : startSingleUpload({
77
+ url,
78
+ fieldName: fieldName ?? pondFieldName,
79
+ file,
80
+ headers: requestHeaders,
81
+ withCredentials,
82
+ sendRequest,
83
+ onProgress: reportProgress
84
+ });
85
+ upload.done.then(load, async (failure) => {
86
+ if (failure?.name === "AbortError") {
25
87
  return;
26
88
  }
27
- error(problemTitleOf(request));
28
- };
29
- request.onerror = () => error(problemTitle(null, OFFLINE_STATUS));
30
- const formData = new FormData();
31
- formData.append(fieldName ?? pondFieldName, file, file.name);
32
- request.send(formData);
89
+ error(await failureMessageOf(failure, toProblem));
90
+ });
33
91
  return {
34
92
  abort: () => {
35
- request.abort();
93
+ upload.abort();
36
94
  abort();
37
95
  }
38
96
  };
@@ -43,6 +101,7 @@ function createPondServer({ url, fieldName, headers = {}, withCredentials = fals
43
101
  };
44
102
  }
45
103
  export {
46
- createPondServer
104
+ createPondServer,
105
+ usesChunks
47
106
  };
48
107
  //# sourceMappingURL=pondServer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pondServer.js","sources":["../../src/filepond/pondServer.js"],"sourcesContent":["// El «server» de FilePond con lo de la casa: cada archivo se sube en su propio POST con su barra de avance dentro de su\n// tarjeta, el idioma activo en Accept-Language, el error RFC 9457 del backend escrito en la tarjeta y la ✕ que cancela la\n// petición de verdad (no solo la pantalla).\n//\n// Cuándo SÍ: archivos que valen cada uno por su cuenta (adjuntos sueltos, una galería).\n// Cuándo NO: el envío de siempre, «todos los archivos y los campos en UN solo envío»: ahí la zona solo elige y la\n// pantalla arma su FormData. OJO: aquí el backend ve UN archivo por petición; las reglas del lote completo (cuántos,\n// cuánto pesan juntos) ya no las puede revisar por petición.\n//\n// <FileDropzone multiple server={createPondServer({url: '/api/v1/adjuntos', headers: {Authorization: `Bearer ${token}`}})} />\n//\n// Funciones del navegador, sin React: function (regla de la casa).\nimport {problemTitle} from '@/problems.js';\nimport {toolkitSettings} from '@/toolkit/toolkitSettings.js';\n\n// status de «no hubo respuesta» para los textos del paquete (problems: offline)\nconst OFFLINE_STATUS = 0;\n\n/**\n * El título del problema RFC 9457 de una respuesta (o el genérico del paquete, ya traducido).\n *\n * @param {XMLHttpRequest} request\n * @returns {string}\n */\nfunction problemTitleOf(request) {\n let problem = null;\n try {\n problem = JSON.parse(request.responseText);\n } catch {\n // la respuesta no era JSON (un 502 del proxy en HTML): va el título genérico de su status\n }\n return problemTitle(problem, request.status);\n}\n\n/**\n * @param {object} options\n * @param {string} options.url a dónde va cada archivo\n * @param {string} [options.fieldName] el campo del archivo en el FormData; por omisión el name de la zona\n * @param {Record<string, string>} [options.headers] p. ej. Authorization\n * @param {boolean} [options.withCredentials] manda las cookies a otro dominio\n * @returns {{process: (...args: any[]) => any, revert: (...args: any[]) => any}} lo que recibe la prop server de FileDropzone\n */\nexport function createPondServer({url, fieldName, headers = {}, withCredentials = false}) {\n return {\n process: (pondFieldName, file, metadata, load, error, progress, abort) => {\n const request = new XMLHttpRequest();\n request.open('POST', url);\n request.withCredentials = withCredentials;\n for (const [headerName, headerValue] of Object.entries({'Accept-Language': toolkitSettings().language, ...headers})) {\n request.setRequestHeader(headerName, headerValue);\n }\n\n request.upload.onprogress = (event) => progress(event.lengthComputable, event.loaded, event.total);\n request.onload = () => {\n if (request.status >= 200 && request.status < 300) {\n // lo que responda el servidor es el id del archivo (FilePond lo guarda para deshacer)\n load(request.responseText || file.name);\n return;\n }\n error(problemTitleOf(request));\n };\n request.onerror = () => error(problemTitle(null, OFFLINE_STATUS));\n\n const formData = new FormData();\n formData.append(fieldName ?? pondFieldName, file, file.name);\n request.send(formData);\n\n // la ✕ de la tarjeta cancela la petición de verdad\n return {\n abort: () => {\n request.abort();\n abort();\n },\n };\n },\n // sin nada que deshacer en el servidor: el «deshacer» de la tarjeta es local. Si el backend guarda el archivo,\n // aquí iría su DELETE con el id que regresó\n revert: (serverId, load) => load(),\n };\n}\n"],"names":[],"mappings":";;AAgBA,MAAM,iBAAiB;AAQvB,SAAS,eAAe,SAAS;AAC7B,MAAI,UAAU;AACd,MAAI;AACA,cAAU,KAAK,MAAM,QAAQ,YAAY;AAAA,EAC7C,QAAQ;AAAA,EAER;AACA,SAAO,aAAa,SAAS,QAAQ,MAAM;AAC/C;AAUO,SAAS,iBAAiB,EAAC,KAAK,WAAW,UAAU,IAAI,kBAAkB,MAAK,GAAG;AACtF,SAAO;AAAA,IACH,SAAS,CAAC,eAAe,MAAM,UAAU,MAAM,OAAO,UAAU,UAAU;AACtE,YAAM,UAAU,IAAI,eAAc;AAClC,cAAQ,KAAK,QAAQ,GAAG;AACxB,cAAQ,kBAAkB;AAC1B,iBAAW,CAAC,YAAY,WAAW,KAAK,OAAO,QAAQ,EAAC,mBAAmB,gBAAe,EAAG,UAAU,GAAG,QAAO,CAAC,GAAG;AACjH,gBAAQ,iBAAiB,YAAY,WAAW;AAAA,MACpD;AAEA,cAAQ,OAAO,aAAa,CAAC,UAAU,SAAS,MAAM,kBAAkB,MAAM,QAAQ,MAAM,KAAK;AACjG,cAAQ,SAAS,MAAM;AACnB,YAAI,QAAQ,UAAU,OAAO,QAAQ,SAAS,KAAK;AAE/C,eAAK,QAAQ,gBAAgB,KAAK,IAAI;AACtC;AAAA,QACJ;AACA,cAAM,eAAe,OAAO,CAAC;AAAA,MACjC;AACA,cAAQ,UAAU,MAAM,MAAM,aAAa,MAAM,cAAc,CAAC;AAEhE,YAAM,WAAW,IAAI,SAAQ;AAC7B,eAAS,OAAO,aAAa,eAAe,MAAM,KAAK,IAAI;AAC3D,cAAQ,KAAK,QAAQ;AAGrB,aAAO;AAAA,QACH,OAAO,MAAM;AACT,kBAAQ,MAAK;AACb,gBAAK;AAAA,QACT;AAAA,MAChB;AAAA,IACQ;AAAA;AAAA;AAAA,IAGA,QAAQ,CAAC,UAAU,SAAS,KAAI;AAAA,EACxC;AACA;"}
1
+ {"version":3,"file":"pondServer.js","sources":["../../src/filepond/pondServer.js"],"sourcesContent":["// El «server» de FilePond con lo de la casa: cada archivo se sube en su propio POST con su barra de avance dentro de su\n// tarjeta, el idioma activo en Accept-Language, el error del backend escrito en la tarjeta y la ✕ que cancela la petición\n// de verdad (no solo la pantalla).\n//\n// Cuándo SÍ: archivos que valen cada uno por su cuenta (adjuntos sueltos, una galería).\n// Cuándo NO: el envío de siempre, «todos los archivos y los campos en UN solo envío»: ahí la zona solo elige y la\n// pantalla arma su FormData. OJO: aquí el backend ve UN archivo por petición; las reglas del lote completo (cuántos,\n// cuánto pesan juntos) ya no las puede revisar por petición.\n//\n// <FileDropzone multiple server={createPondServer({url: '/api/v1/adjuntos', headers: {Authorization: `Bearer ${token}`}})} />\n//\n// POR TROZOS: cuando la zona pide trozos (chunkUploads, y el archivo pasa de chunkSize o va chunkForce), cada archivo sube\n// con el protocolo de trozos de FilePond y la barra honesta de chunkedUpload.js: el folio con un POST a url, cada trozo con\n// un PATCH a patchUrl + folio, y retomar con un HEAD (lo mismo que usa el cargador mini con createChunkedUpload). Las reglas son las de FilePond (mismos nombres de opciones), así un\n// backend que ya recibía trozos de FilePond no cambia.\n//\n// <FileDropzone chunkUploads chunkForce chunkSize=\"1 MB\"\n// server={createPondServer({url: '/upload', patchUrl: '/upload?patch=', headers: {'X-Upload-Context': JSON.stringify(data)}})} />\n//\n// OJO con los headers: por trozos viajan en UTF-8 byte por byte (como los manda FilePond): se pasan como texto normal, sin\n// convertirlos a mano antes (convertidos dos veces, los acentos llegan rotos). El envío de un solo POST los manda tal cual.\n//\n// Un backend que no contesta RFC 9457 (Laravel manda {message, errors}): toProblem convierte su error, el mismo adaptador\n// de createApiClient. Cada petición sale por sendRequest (por omisión uploadWithProgress de la casa): se puede pasar por el\n// cliente del proyecto o probar sin servidor.\n//\n// Funciones del navegador, sin React: function (regla de la casa).\nimport {createLogger} from '@/observability/logger.js';\nimport {problemTitle} from '@/problems.js';\nimport {toolkitSettings, translateToolkit} from '@/toolkit/toolkitSettings.js';\nimport {UPLOAD_FAILURE, UploadFailure, defaultPatchUrlOf, problemOfResponse, startChunkedUpload} from '@/upload/chunkedUpload.js';\nimport {uploadWithProgress} from '@/upload/uploadWithProgress.js';\n\nconst log = createLogger('pond');\n\n// status de «no hubo respuesta» para los textos del paquete (problems: offline)\nconst OFFLINE_STATUS = 0;\n\n/**\n * ¿Este archivo va por trozos? La regla de FilePond: la zona los pidió y el archivo pasa del tamaño del trozo, o la zona los\n * fuerza para todos (hay servidores que solo reciben por trozos).\n *\n * @param {Blob} file\n * @param {{chunkUploads?: boolean, chunkForce?: boolean, chunkSize?: number}} options lo que FilePond le pasa al server\n * @returns {boolean}\n */\nexport function usesChunks(file, options) {\n return options?.chunkUploads === true && (file.size > options.chunkSize || options.chunkForce === true);\n}\n\n/**\n * Lo que se escribe en la tarjeta cuando un archivo no subió: el título del problema del servidor, o el genérico de su\n * status, ya traducido. Vacío deja el «No se pudo subir» de la zona.\n *\n * @param {unknown} failure\n * @param {((body: any, response: Response) => any)|undefined} toProblem\n * @returns {Promise<string>}\n */\nasync function failureMessageOf(failure, toProblem) {\n if (failure?.reason === UPLOAD_FAILURE.noTransferId) {\n return translateToolkit('common', 'upload.pond.noTransferId');\n }\n if (failure?.reason === UPLOAD_FAILURE.noResponse) {\n return problemTitle(null, OFFLINE_STATUS);\n }\n if (failure?.reason === UPLOAD_FAILURE.rejected && failure.response) {\n return problemTitle(await problemOfResponse(failure.response, toProblem), failure.response.status);\n }\n // no es de la red ni del servidor: un error de programación, que se anota para encontrarlo\n log.error('La subida falló sin respuesta del servidor', failure);\n return '';\n}\n\n/**\n * La subida de siempre: el archivo en un solo POST. done termina con lo que contestó el servidor (el id del archivo) o,\n * si contestó vacío, con el nombre del archivo. Los errores tienen la forma de la subida por trozos (UploadFailure): los\n * dos envíos se leen igual.\n *\n * @param {object} options\n * @returns {{done: Promise<string>, abort: () => void}}\n */\nfunction startSingleUpload({url, fieldName, file, headers, withCredentials, sendRequest, onProgress}) {\n const controller = new AbortController();\n const formData = new FormData();\n formData.append(fieldName, file, file.name);\n const done = sendRequest(url, formData, {\n method: 'POST', headers, withCredentials, signal: controller.signal,\n onProgress: (percent, transfer) => {\n // cancelada, ya no hay avance que contar: la tarjeta se queda en «cancelada»\n if (!controller.signal.aborted) {\n onProgress(transfer.loaded, transfer.total);\n }\n },\n }).then(\n async (response) => {\n if (!response.ok) {\n throw new UploadFailure(UPLOAD_FAILURE.rejected, response);\n }\n return (await response.text()) || file.name;\n },\n (failure) => {\n if (failure?.name === 'AbortError') {\n throw failure;\n }\n throw new UploadFailure(UPLOAD_FAILURE.noResponse);\n },\n );\n return {done, abort: () => controller.abort()};\n}\n\n/**\n * @param {object} options\n * @param {string} options.url a dónde va cada archivo (por trozos: el POST que pide el folio)\n * @param {string} [options.fieldName] el campo del archivo en el FormData; por omisión el name de la zona\n * @param {Record<string, string>} [options.headers] p. ej. Authorization; por trozos van en las tres peticiones\n * @param {boolean} [options.withCredentials] manda las cookies a otro dominio\n * @param {string} [options.patchUrl] por trozos: a dónde va cada trozo, con el folio pegado al final. Por omisión, el de\n * FilePond: url + '?patch='\n * @param {(body: any, response: Response) => any} [options.toProblem] el error del backend a la forma RFC 9457 (Laravel:\n * body => ({title: body.message}))\n * @param {import('@/upload/chunkedUpload.js').UploadSendRequest} [options.sendRequest] cómo viaja cada petición; por omisión\n * uploadWithProgress de la casa\n * @returns {{process: (...args: any[]) => any, revert: (...args: any[]) => any}} lo que recibe la prop server de FileDropzone\n */\nexport function createPondServer({url, fieldName, headers = {}, withCredentials = false, patchUrl, toProblem, sendRequest = uploadWithProgress}) {\n const chunkUrl = patchUrl ?? defaultPatchUrlOf(url);\n return {\n // FilePond llama process con su progress, su folio (transfer) y, al final, sus opciones de trozos\n process: (pondFieldName, file, metadata, load, error, progress, abort, transfer, options = {}) => {\n const requestHeaders = {'Accept-Language': toolkitSettings().language, ...headers};\n const reportProgress = (loaded, total) => progress(true, loaded, total);\n const upload = usesChunks(file, options)\n ? startChunkedUpload({\n file, fileName: file.name, fieldName: fieldName ?? pondFieldName, metadata, url, patchUrl: chunkUrl,\n headers: requestHeaders, withCredentials, chunkSize: options.chunkSize, retryDelays: options.chunkRetryDelays,\n transferId: options.chunkTransferId ?? null, onTransferId: transfer, onProgress: reportProgress, sendRequest,\n })\n : startSingleUpload({\n url, fieldName: fieldName ?? pondFieldName, file, headers: requestHeaders, withCredentials, sendRequest,\n onProgress: reportProgress,\n });\n\n // lo que responda el servidor es el id del archivo (por trozos, el folio): FilePond lo guarda para deshacer\n upload.done.then(load, async (failure) => {\n // la ✕ ya le avisó a FilePond (abort de abajo)\n if (failure?.name === 'AbortError') {\n return;\n }\n error(await failureMessageOf(failure, toProblem));\n });\n\n // la ✕ de la tarjeta cancela la petición de verdad\n return {\n abort: () => {\n upload.abort();\n abort();\n },\n };\n },\n // sin nada que deshacer en el servidor: el «deshacer» de la tarjeta es local. Si el backend guarda el archivo,\n // aquí iría su DELETE con el id que regresó\n revert: (serverId, load) => load(),\n };\n}\n"],"names":[],"mappings":"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"}
@@ -57,23 +57,28 @@ function FormWizard({
57
57
  setSkippedKeys((currentKeys) => /* @__PURE__ */ new Set([...currentKeys, activeStep.key]));
58
58
  setActiveIndex((currentIndex) => currentIndex + 1);
59
59
  };
60
- return /* @__PURE__ */ jsx(
61
- WizardLayout,
62
- {
63
- steps,
64
- activeIndex,
65
- skippedKeys,
66
- isSubmitting,
67
- orientation,
68
- hideBackOnFirstStep,
69
- onBack: () => setActiveIndex((currentIndex) => currentIndex - 1),
70
- onNext: goNext,
71
- onSkip: skipStep,
72
- onStepSelect: selectStep,
73
- submitButton: /* @__PURE__ */ jsx(SubmitButton, { children: submitLabel }),
74
- problemAlert: /* @__PURE__ */ jsx(FormProblemAlert, {}),
75
- texts
76
- }
60
+ return (
61
+ // un paso con su motivo para no avanzar (steps[].nextDisabledReason): «Siguiente» apagado y, en el último, también el de
62
+ // enviar; este sí con disabled de verdad, así un Enter en un campo tampoco envía
63
+ /* @__PURE__ */ jsx(
64
+ WizardLayout,
65
+ {
66
+ steps,
67
+ activeIndex,
68
+ skippedKeys,
69
+ isSubmitting,
70
+ orientation,
71
+ hideBackOnFirstStep,
72
+ nextDisabledReason: activeStep.nextDisabledReason,
73
+ onBack: () => setActiveIndex((currentIndex) => currentIndex - 1),
74
+ onNext: goNext,
75
+ onSkip: skipStep,
76
+ onStepSelect: selectStep,
77
+ submitButton: /* @__PURE__ */ jsx(SubmitButton, { disabled: Boolean(activeStep.nextDisabledReason), children: submitLabel }),
78
+ problemAlert: /* @__PURE__ */ jsx(FormProblemAlert, {}),
79
+ texts
80
+ }
81
+ )
77
82
  );
78
83
  }
79
84
  export {
@@ -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// - 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 {boolean} [props.hideBackOnFirstStep] en el primer paso no se pinta «Anterior»; por omisión sale apagado, como antes\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, hideBackOnFirstStep, 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} hideBackOnFirstStep={hideBackOnFirstStep}\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;AAoBA,SAAwB,WAAW;AAAA,EAC/B;AAAA,EAAO;AAAA,EAAa;AAAA,EAAa;AAAA,EAAqB;AAAA,EAAgB;AAAA,EAAe;AAAA,EAAc;AAAA,EAAO,GAAG;AACjH,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,MAA0B;AAAA,MACxH,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;"}
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[].nextDisabledReason] ya traducido: el paso todavía no se puede dejar («Faltan 2\n * documentos»). «Siguiente» se ve apagado con el motivo en un aviso (escrito, en el teléfono); en el último paso se apaga\n * el de enviar. Con stepSelection checked tampoco se salta a un paso de adelante. Sin él, como siempre\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 {boolean} [props.hideBackOnFirstStep] en el primer paso no se pinta «Anterior»; por omisión sale apagado, como antes\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, hideBackOnFirstStep, 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 // un paso con su motivo para no avanzar (steps[].nextDisabledReason): «Siguiente» apagado y, en el último, también el de\n // enviar; este sí con disabled de verdad, así un Enter en un campo tampoco envía\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting} orientation={orientation} hideBackOnFirstStep={hideBackOnFirstStep}\n nextDisabledReason={activeStep.nextDisabledReason} onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)}\n onNext={goNext} onSkip={skipStep} onStepSelect={selectStep}\n submitButton={<SubmitButton disabled={Boolean(activeStep.nextDisabledReason)}>{submitLabel}</SubmitButton>}\n 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;AAuBA,SAAwB,WAAW;AAAA,EAC/B;AAAA,EAAO;AAAA,EAAa;AAAA,EAAa;AAAA,EAAqB;AAAA,EAAgB;AAAA,EAAe;AAAA,EAAc;AAAA,EAAO,GAAG;AACjH,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;AAAA;AAAA;AAAA,IAGI;AAAA,MAAC;AAAA,MAAA;AAAA,QAAa;AAAA,QAAc;AAAA,QAA0B;AAAA,QAA0B;AAAA,QAA4B;AAAA,QAA0B;AAAA,QACxH,oBAAoB,WAAW;AAAA,QAAoB,QAAQ,MAAM,eAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,QAClH,QAAQ;AAAA,QAAQ,QAAQ;AAAA,QAAU,cAAc;AAAA,QAChD,kCAAe,cAAA,EAAa,UAAU,QAAQ,WAAW,kBAAkB,GAAI,UAAA,aAAY;AAAA,QAC3F,kCAAe,kBAAA,EAAiB;AAAA,QAAI;AAAA,MAAA;AAAA,IAAA;AAAA;AAE1D;"}
@@ -52,23 +52,28 @@ function FormWizard({
52
52
  setSkippedKeys((currentKeys) => /* @__PURE__ */ new Set([...currentKeys, activeStep.key]));
53
53
  setActiveIndex((currentIndex) => currentIndex + 1);
54
54
  };
55
- return /* @__PURE__ */ jsx(
56
- WizardLayout,
57
- {
58
- steps,
59
- activeIndex,
60
- skippedKeys,
61
- isSubmitting,
62
- orientation,
63
- hideBackOnFirstStep,
64
- onBack: () => setActiveIndex((currentIndex) => currentIndex - 1),
65
- onNext: goNext,
66
- onSkip: skipStep,
67
- onStepSelect: selectStep,
68
- submitButton: /* @__PURE__ */ jsx(SubmitButton, { children: submitLabel }),
69
- problemAlert: /* @__PURE__ */ jsx(FormProblemAlert, {}),
70
- texts
71
- }
55
+ return (
56
+ // un paso con su motivo para no avanzar (steps[].nextDisabledReason): «Siguiente» apagado y, en el último, también el de
57
+ // enviar; este sí con disabled de verdad, así un Enter en un campo tampoco envía
58
+ /* @__PURE__ */ jsx(
59
+ WizardLayout,
60
+ {
61
+ steps,
62
+ activeIndex,
63
+ skippedKeys,
64
+ isSubmitting,
65
+ orientation,
66
+ hideBackOnFirstStep,
67
+ nextDisabledReason: activeStep.nextDisabledReason,
68
+ onBack: () => setActiveIndex((currentIndex) => currentIndex - 1),
69
+ onNext: goNext,
70
+ onSkip: skipStep,
71
+ onStepSelect: selectStep,
72
+ submitButton: /* @__PURE__ */ jsx(SubmitButton, { disabled: Boolean(activeStep.nextDisabledReason), children: submitLabel }),
73
+ problemAlert: /* @__PURE__ */ jsx(FormProblemAlert, {}),
74
+ texts
75
+ }
76
+ )
72
77
  );
73
78
  }
74
79
  export {
@@ -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// - 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 {boolean} [props.hideBackOnFirstStep] en el primer paso no se pinta «Anterior»; por omisión sale apagado, como antes\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, hideBackOnFirstStep, 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} hideBackOnFirstStep={hideBackOnFirstStep}\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":";;;;;;;;;AAqDA,SAAwB,WAAW;AAAA,EAC/B;AAAA,EAAO;AAAA,EAAa;AAAA,EAAa;AAAA,EAAqB;AAAA,EAAgB;AAAA,EAAe;AAAA,EAAc;AAAA,EAAO,GAAG;AACjH,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,MAA0B;AAAA,MACxH,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;"}
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[].nextDisabledReason] ya traducido: el paso todavía no se puede dejar («Faltan 2\n * documentos»). «Siguiente» se ve apagado con el motivo en un aviso (escrito, en el teléfono); en el último paso se apaga\n * el de enviar. Con stepSelection checked tampoco se salta a un paso de adelante. Sin él, como siempre\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 {boolean} [props.hideBackOnFirstStep] en el primer paso no se pinta «Anterior»; por omisión sale apagado, como antes\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, hideBackOnFirstStep, 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 // un paso con su motivo para no avanzar (steps[].nextDisabledReason): «Siguiente» apagado y, en el último, también el de\n // enviar; este sí con disabled de verdad, así un Enter en un campo tampoco envía\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting} orientation={orientation} hideBackOnFirstStep={hideBackOnFirstStep}\n nextDisabledReason={activeStep.nextDisabledReason} onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)}\n onNext={goNext} onSkip={skipStep} onStepSelect={selectStep}\n submitButton={<SubmitButton disabled={Boolean(activeStep.nextDisabledReason)}>{submitLabel}</SubmitButton>}\n problemAlert={<FormProblemAlert />} texts={texts} />\n );\n}\n"],"names":[],"mappings":";;;;;;;;;AAwDA,SAAwB,WAAW;AAAA,EAC/B;AAAA,EAAO;AAAA,EAAa;AAAA,EAAa;AAAA,EAAqB;AAAA,EAAgB;AAAA,EAAe;AAAA,EAAc;AAAA,EAAO,GAAG;AACjH,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;AAAA;AAAA;AAAA,IAGI;AAAA,MAAC;AAAA,MAAA;AAAA,QAAa;AAAA,QAAc;AAAA,QAA0B;AAAA,QAA0B;AAAA,QAA4B;AAAA,QAA0B;AAAA,QACxH,oBAAoB,WAAW;AAAA,QAAoB,QAAQ,MAAM,eAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,QAClH,QAAQ;AAAA,QAAQ,QAAQ;AAAA,QAAU,cAAc;AAAA,QAChD,kCAAe,cAAA,EAAa,UAAU,QAAQ,WAAW,kBAAkB,GAAI,UAAA,aAAY;AAAA,QAC3F,kCAAe,kBAAA,EAAiB;AAAA,QAAI;AAAA,MAAA;AAAA,IAAA;AAAA;AAE1D;"}
package/dist/index.js CHANGED
@@ -72,6 +72,7 @@ import { transferProgressOf } from "./upload/transferProgress.js";
72
72
  import { useFilePicker } from "./upload/useFilePicker.js";
73
73
  import { buildUploadActors, uploadMachine } from "./upload/uploadMachine.js";
74
74
  import { uploadWithProgress } from "./upload/uploadWithProgress.js";
75
+ import { createChunkedUpload } from "./upload/chunkedUpload.js";
75
76
  import { APP_BAR_HEIGHT_CSS_VARIABLE, SCROLL_MARGIN_BELOW_APP_BAR, useAppBarHeightPublisher } from "./ui/layout/appBarHeight.js";
76
77
  import { default as default30 } from "./ui/layout/CollapsibleSection.js";
77
78
  import { default as default31 } from "./ui/layout/ContentDialog.js";
@@ -120,7 +121,7 @@ import { default as default69 } from "./inputs/ColorInput.js";
120
121
  import { default as default70 } from "./inputs/FieldFrame.js";
121
122
  import { default as default71 } from "./inputs/MaskedInput.js";
122
123
  import { INPUT_MASK, isMaskComplete, maskPatternOf, maskedTextOf, rawValueOf } from "./inputs/inputMasks.js";
123
- import { FORM_LABEL_PLACEMENT, FORM_ROW_CLASS, FormRow, default as default72 } from "./inputs/FormSection.js";
124
+ import { FORM_LABEL_PLACEMENT, FORM_LABEL_WIDTH, FORM_ROW_CLASS, FormRow, default as default72 } from "./inputs/FormSection.js";
124
125
  import { default as default73 } from "./inputs/CheckboxGroupInput.js";
125
126
  import { default as default74 } from "./inputs/NumberInput.js";
126
127
  import { default as default75 } from "./inputs/TransferListInput.js";
@@ -237,6 +238,7 @@ export {
237
238
  FILTER_FIELD_KIND,
238
239
  FONT_SIZE_LEVELS,
239
240
  FORM_LABEL_PLACEMENT,
241
+ FORM_LABEL_WIDTH,
240
242
  FORM_ROW_CLASS,
241
243
  default70 as FieldFrame,
242
244
  default25 as FileDropzone,
@@ -367,6 +369,7 @@ export {
367
369
  createAccessChecker,
368
370
  createApiClient,
369
371
  createBrowserNavigation,
372
+ createChunkedUpload,
370
373
  createHashNavigation,
371
374
  createLogger,
372
375
  createPathIndex,
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}