@stackcraftmx/calcita 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/dist/DataTable/AuditTrailDialog.js +18 -14
- package/dist/DataTable/AuditTrailDialog.js.map +1 -1
- package/dist/DataTable/ColumnFilters/HeaderFilterPopover.js +12 -6
- package/dist/DataTable/ColumnFilters/HeaderFilterPopover.js.map +1 -1
- package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js +8 -3
- package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js.map +1 -1
- package/dist/DataTable/ColumnFilters/SortableFilterHeader.js +26 -18
- package/dist/DataTable/ColumnFilters/SortableFilterHeader.js.map +1 -1
- package/dist/DataTable/EmptyState.js +8 -3
- package/dist/DataTable/EmptyState.js.map +1 -1
- package/dist/DataTable/FilterChips.js +11 -5
- package/dist/DataTable/FilterChips.js.map +1 -1
- package/dist/DataTable/GoToPage.js +9 -4
- package/dist/DataTable/GoToPage.js.map +1 -1
- package/dist/DataTable/ServerTable.js +74 -61
- package/dist/DataTable/ServerTable.js.map +1 -1
- package/dist/DataTable/columns.js +1 -8
- package/dist/DataTable/columns.js.map +1 -1
- package/dist/access/PermissionMatrix.js +18 -9
- package/dist/access/PermissionMatrix.js.map +1 -1
- package/dist/account/AccountSecuritySection.js +8 -3
- package/dist/account/AccountSecuritySection.js.map +1 -1
- package/dist/account/DemoTotpCodeButton.js +8 -3
- package/dist/account/DemoTotpCodeButton.js.map +1 -1
- package/dist/account/ProfileScreen.js +19 -13
- package/dist/account/ProfileScreen.js.map +1 -1
- package/dist/account/SignInScreen.js +6 -2
- package/dist/account/SignInScreen.js.map +1 -1
- package/dist/appShell/AccountMenu.js +13 -5
- package/dist/appShell/AccountMenu.js.map +1 -1
- package/dist/appShell/AppLayout.js +100 -71
- package/dist/appShell/AppLayout.js.map +1 -1
- package/dist/appShell/AppearanceSettings.js +21 -13
- package/dist/appShell/AppearanceSettings.js.map +1 -1
- package/dist/appShell/AuthLayout.js +26 -13
- package/dist/appShell/AuthLayout.js.map +1 -1
- package/dist/appShell/BarActions.js +10 -5
- package/dist/appShell/BarActions.js.map +1 -1
- package/dist/appShell/BarCapsule.js +9 -4
- package/dist/appShell/BarCapsule.js.map +1 -1
- package/dist/appShell/BarSearch.js +7 -3
- package/dist/appShell/BarSearch.js.map +1 -1
- package/dist/appShell/HeaderTools.js +20 -15
- package/dist/appShell/HeaderTools.js.map +1 -1
- package/dist/appShell/MobileTabBar.js +25 -14
- package/dist/appShell/MobileTabBar.js.map +1 -1
- package/dist/appShell/SettingsMenu.js +7 -3
- package/dist/appShell/SettingsMenu.js.map +1 -1
- package/dist/appShell/SideMenu.js +53 -35
- package/dist/appShell/SideMenu.js.map +1 -1
- package/dist/appShell/SideMenuEntry.js +18 -9
- package/dist/appShell/SideMenuEntry.js.map +1 -1
- package/dist/appShell/SubmenuActionsMenu.js +6 -2
- package/dist/appShell/SubmenuActionsMenu.js.map +1 -1
- package/dist/appShell/TabBarEditor.js +16 -7
- package/dist/appShell/TabBarEditor.js.map +1 -1
- package/dist/appShell/TopMenu.js +12 -7
- package/dist/appShell/TopMenu.js.map +1 -1
- package/dist/appShell/Wordmark.js +6 -2
- package/dist/appShell/Wordmark.js.map +1 -1
- package/dist/auth/PasswordChecklist.js +14 -7
- package/dist/auth/PasswordChecklist.js.map +1 -1
- package/dist/calendar/EventCalendar.js +26 -19
- package/dist/calendar/EventCalendar.js.map +1 -1
- package/dist/calendar/EventDetails.js +28 -13
- package/dist/calendar/EventDetails.js.map +1 -1
- package/dist/downloads/DownloadManager.js +34 -18
- package/dist/downloads/DownloadManager.js.map +1 -1
- package/dist/feedback/ConfirmDialog.js +22 -17
- package/dist/feedback/ConfirmDialog.js.map +1 -1
- package/dist/feedback/ModalWaiting.js +12 -5
- package/dist/feedback/ModalWaiting.js.map +1 -1
- package/dist/feedback/NewBuildNotice.js +16 -8
- package/dist/feedback/NewBuildNotice.js.map +1 -1
- package/dist/filepond/FileDropzone.js +39 -31
- package/dist/filepond/FileDropzone.js.map +1 -1
- package/dist/forms/FormProblemAlert.js +4 -1
- package/dist/forms/FormProblemAlert.js.map +1 -1
- package/dist/forms/auth/SignInForm.js +6 -2
- package/dist/forms/auth/SignInForm.js.map +1 -1
- package/dist/forms/security/SecondStepForm.js +6 -2
- package/dist/forms/security/SecondStepForm.js.map +1 -1
- package/dist/forms/security/TwoStepVerificationCard.js +19 -9
- package/dist/forms/security/TwoStepVerificationCard.js.map +1 -1
- package/dist/hookForm/FormProblemAlert.js +4 -1
- package/dist/hookForm/FormProblemAlert.js.map +1 -1
- package/dist/inbox/InboxLayout.js +47 -29
- package/dist/inbox/InboxLayout.js.map +1 -1
- package/dist/inbox/MessageList.js +53 -32
- package/dist/inbox/MessageList.js.map +1 -1
- package/dist/inbox/MessageView.js +31 -15
- package/dist/inbox/MessageView.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/inputs/ColorInput.js +22 -17
- package/dist/inputs/ColorInput.js.map +1 -1
- package/dist/inputs/FormSection.js +64 -64
- package/dist/inputs/FormSection.js.map +1 -1
- package/dist/inputs/SelectInput.js +21 -16
- package/dist/inputs/SelectInput.js.map +1 -1
- package/dist/inputs/TextInput.js +25 -20
- package/dist/inputs/TextInput.js.map +1 -1
- package/dist/inputs/WizardLayout.js +21 -15
- package/dist/inputs/WizardLayout.js.map +1 -1
- package/dist/kanban/KanbanBoard.js +82 -50
- package/dist/kanban/KanbanBoard.js.map +1 -1
- package/dist/maps/MapView.js +19 -14
- package/dist/maps/MapView.js.map +1 -1
- package/dist/pwa/InstallAppSetting.js +4 -1
- package/dist/pwa/InstallAppSetting.js.map +1 -1
- package/dist/pwa/InstallInstructionsDialog.js +6 -2
- package/dist/pwa/InstallInstructionsDialog.js.map +1 -1
- package/dist/pwa/InstallPromptBanner.js +10 -5
- package/dist/pwa/InstallPromptBanner.js.map +1 -1
- package/dist/security/ActivityLog.js +6 -2
- package/dist/security/ActivityLog.js.map +1 -1
- package/dist/security/RecoveryCodesDialog.js +8 -3
- package/dist/security/RecoveryCodesDialog.js.map +1 -1
- package/dist/security/SessionsCard.js +12 -5
- package/dist/security/SessionsCard.js.map +1 -1
- package/dist/theme/components/glassComponents.js +19 -16
- package/dist/theme/components/glassComponents.js.map +1 -1
- package/dist/theme/components/minimalComponents.js +6 -2
- package/dist/theme/components/minimalComponents.js.map +1 -1
- package/dist/theme/createToolkitTheme.js +8 -1
- package/dist/theme/createToolkitTheme.js.map +1 -1
- package/dist/theme/cssTokens.js +23 -0
- package/dist/theme/cssTokens.js.map +1 -0
- package/dist/theme/glassSurface.js +9 -3
- package/dist/theme/glassSurface.js.map +1 -1
- package/dist/theme/glassToasterStyles.js +2 -3
- package/dist/theme/glassToasterStyles.js.map +1 -1
- package/dist/theme/themePieces.js +16 -0
- package/dist/theme/themePieces.js.map +1 -0
- package/dist/types/DataTable/AuditTrailDialog.d.ts +1 -1
- package/dist/types/DataTable/ColumnFilters/HeaderFilterPopover.d.ts +1 -1
- package/dist/types/DataTable/ColumnFilters/MultiSelectColumnFilter.d.ts +1 -1
- package/dist/types/DataTable/ColumnFilters/SortableFilterHeader.d.ts +1 -1
- package/dist/types/DataTable/EmptyState.d.ts +1 -1
- package/dist/types/DataTable/FilterChips.d.ts +1 -1
- package/dist/types/DataTable/GoToPage.d.ts +1 -1
- package/dist/types/DataTable/ServerTable.d.ts +1 -1
- package/dist/types/access/PermissionMatrix.d.ts +1 -1
- package/dist/types/account/AccountSecuritySection.d.ts +1 -1
- package/dist/types/account/DemoTotpCodeButton.d.ts +1 -1
- package/dist/types/account/ProfileScreen.d.ts +1 -1
- package/dist/types/account/SignInScreen.d.ts +1 -1
- package/dist/types/appShell/AccountMenu.d.ts +1 -1
- package/dist/types/appShell/AppLayout.d.ts +1 -1
- package/dist/types/appShell/AppearanceSettings.d.ts +1 -1
- package/dist/types/appShell/AuthLayout.d.ts +1 -1
- package/dist/types/appShell/BarActions.d.ts +1 -1
- package/dist/types/appShell/BarCapsule.d.ts +1 -1
- package/dist/types/appShell/BarSearch.d.ts +1 -1
- package/dist/types/appShell/HeaderTools.d.ts +1 -1
- package/dist/types/appShell/MobileTabBar.d.ts +1 -1
- package/dist/types/appShell/SettingsMenu.d.ts +1 -1
- package/dist/types/appShell/SideMenu.d.ts +1 -1
- package/dist/types/appShell/SideMenuEntry.d.ts +1 -1
- package/dist/types/appShell/SubmenuActionsMenu.d.ts +1 -1
- package/dist/types/appShell/TabBarEditor.d.ts +1 -1
- package/dist/types/appShell/TopMenu.d.ts +1 -1
- package/dist/types/appShell/Wordmark.d.ts +1 -1
- package/dist/types/auth/PasswordChecklist.d.ts +1 -1
- package/dist/types/calendar/EventCalendar.d.ts +1 -1
- package/dist/types/calendar/EventDetails.d.ts +1 -1
- package/dist/types/feedback/ConfirmDialog.d.ts +1 -1
- package/dist/types/feedback/ModalWaiting.d.ts +1 -1
- package/dist/types/feedback/NewBuildNotice.d.ts +1 -1
- package/dist/types/filepond/FileDropzone.d.ts +1 -1
- package/dist/types/forms/auth/SignInForm.d.ts +1 -1
- package/dist/types/forms/security/SecondStepForm.d.ts +1 -1
- package/dist/types/forms/security/TwoStepVerificationCard.d.ts +1 -1
- package/dist/types/inbox/InboxLayout.d.ts +1 -1
- package/dist/types/inbox/MessageList.d.ts +1 -1
- package/dist/types/inbox/MessageView.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/inputs/ColorInput.d.ts +1 -1
- package/dist/types/inputs/FormSection.d.ts +2 -2
- package/dist/types/inputs/SelectInput.d.ts +1 -1
- package/dist/types/inputs/TextInput.d.ts +1 -1
- package/dist/types/inputs/WizardLayout.d.ts +1 -1
- package/dist/types/kanban/KanbanBoard.d.ts +1 -1
- package/dist/types/maps/MapView.d.ts +1 -1
- package/dist/types/pwa/InstallInstructionsDialog.d.ts +1 -1
- package/dist/types/pwa/InstallPromptBanner.d.ts +1 -1
- package/dist/types/security/ActivityLog.d.ts +1 -1
- package/dist/types/security/RecoveryCodesDialog.d.ts +1 -1
- package/dist/types/security/SessionsCard.d.ts +1 -1
- package/dist/types/theme/createToolkitTheme.d.ts +12 -1
- package/dist/types/theme/cssTokens.d.ts +46 -0
- package/dist/types/theme/glassSurface.d.ts +3 -0
- package/dist/types/theme/themePieces.d.ts +20 -0
- package/dist/types/ui/actions/ActionsMenu.d.ts +1 -1
- package/dist/types/ui/actions/DownloadMenu.d.ts +1 -1
- package/dist/types/ui/actions/SelectionActions.d.ts +1 -1
- package/dist/types/ui/filters/DateRangeFilter.d.ts +1 -1
- package/dist/types/ui/filters/PeriodRangeSelector.d.ts +1 -1
- package/dist/types/ui/filters/QuickFilters.d.ts +1 -1
- package/dist/types/ui/filters/SegmentedControl.d.ts +7 -2
- package/dist/types/ui/labels/StatusChip.d.ts +1 -1
- package/dist/types/ui/labels/Tag.d.ts +1 -1
- package/dist/types/ui/layout/CollapsibleSection.d.ts +1 -1
- package/dist/types/ui/layout/ContentDialog.d.ts +1 -1
- package/dist/types/ui/layout/DefinitionList.d.ts +1 -1
- package/dist/types/ui/layout/DetailHeader.d.ts +1 -1
- package/dist/types/ui/layout/ErrorPage.d.ts +1 -1
- package/dist/types/ui/layout/GlassPanel.d.ts +1 -1
- package/dist/types/ui/layout/PageHeader.d.ts +1 -1
- package/dist/types/ui/layout/PageHeading.d.ts +1 -1
- package/dist/types/ui/layout/ProfileHeader.d.ts +1 -1
- package/dist/types/ui/layout/ScrollableRow.d.ts +1 -1
- package/dist/types/ui/layout/SectionCard.d.ts +1 -1
- package/dist/types/ui/metrics/BarList.d.ts +1 -1
- package/dist/types/ui/metrics/CalendarHeatmap.d.ts +1 -1
- package/dist/types/ui/metrics/ChartTooltip.d.ts +1 -1
- package/dist/types/ui/metrics/ColumnChart.d.ts +1 -1
- package/dist/types/ui/metrics/LineChart.d.ts +1 -1
- package/dist/types/ui/metrics/MetricCard.d.ts +1 -1
- package/dist/types/ui/metrics/TableToggle.d.ts +1 -1
- package/dist/types/ui/navigation/ColorSchemeToggle.d.ts +1 -1
- package/dist/types/ui/navigation/FontSizeToggle.d.ts +1 -1
- package/dist/types/ui/navigation/LanguageToggle.d.ts +1 -1
- package/dist/types/ui/navigation/PageBreadcrumbs.d.ts +1 -1
- package/dist/types/ui/timeline/GroupedTimeline.d.ts +1 -1
- package/dist/types/upload/FileDropzone.d.ts +1 -1
- package/dist/ui/actions/ActionsMenu.js +9 -4
- package/dist/ui/actions/ActionsMenu.js.map +1 -1
- package/dist/ui/actions/DownloadMenu.js +21 -17
- package/dist/ui/actions/DownloadMenu.js.map +1 -1
- package/dist/ui/actions/SelectionActions.js +15 -7
- package/dist/ui/actions/SelectionActions.js.map +1 -1
- package/dist/ui/filters/DateRangeFilter.js +11 -5
- package/dist/ui/filters/DateRangeFilter.js.map +1 -1
- package/dist/ui/filters/PeriodRangeSelector.js +11 -5
- package/dist/ui/filters/PeriodRangeSelector.js.map +1 -1
- package/dist/ui/filters/QuickFilters.js +16 -8
- package/dist/ui/filters/QuickFilters.js.map +1 -1
- package/dist/ui/filters/SegmentedControl.js +12 -5
- package/dist/ui/filters/SegmentedControl.js.map +1 -1
- package/dist/ui/labels/StatusChip.js +35 -25
- package/dist/ui/labels/StatusChip.js.map +1 -1
- package/dist/ui/labels/Tag.js +8 -3
- package/dist/ui/labels/Tag.js.map +1 -1
- package/dist/ui/layout/CollapsibleSection.js +18 -8
- package/dist/ui/layout/CollapsibleSection.js.map +1 -1
- package/dist/ui/layout/ContentDialog.js +13 -6
- package/dist/ui/layout/ContentDialog.js.map +1 -1
- package/dist/ui/layout/DefinitionList.js +13 -6
- package/dist/ui/layout/DefinitionList.js.map +1 -1
- package/dist/ui/layout/DetailHeader.js +14 -6
- package/dist/ui/layout/DetailHeader.js.map +1 -1
- package/dist/ui/layout/ErrorPage.js +20 -15
- package/dist/ui/layout/ErrorPage.js.map +1 -1
- package/dist/ui/layout/FullPageLoader.js +4 -1
- package/dist/ui/layout/FullPageLoader.js.map +1 -1
- package/dist/ui/layout/GlassPanel.js +8 -4
- package/dist/ui/layout/GlassPanel.js.map +1 -1
- package/dist/ui/layout/PageHeader.js +10 -4
- package/dist/ui/layout/PageHeader.js.map +1 -1
- package/dist/ui/layout/PageHeading.js +17 -8
- package/dist/ui/layout/PageHeading.js.map +1 -1
- package/dist/ui/layout/ProfileHeader.js +21 -10
- package/dist/ui/layout/ProfileHeader.js.map +1 -1
- package/dist/ui/layout/ScrollableRow.js +19 -15
- package/dist/ui/layout/ScrollableRow.js.map +1 -1
- package/dist/ui/layout/SectionCard.js +10 -4
- package/dist/ui/layout/SectionCard.js.map +1 -1
- package/dist/ui/metrics/BarList.js +9 -4
- package/dist/ui/metrics/BarList.js.map +1 -1
- package/dist/ui/metrics/CalendarHeatmap.js +40 -21
- package/dist/ui/metrics/CalendarHeatmap.js.map +1 -1
- package/dist/ui/metrics/ChartTooltip.js +6 -2
- package/dist/ui/metrics/ChartTooltip.js.map +1 -1
- package/dist/ui/metrics/ColumnChart.js +22 -18
- package/dist/ui/metrics/ColumnChart.js.map +1 -1
- package/dist/ui/metrics/LineChart.js +20 -16
- package/dist/ui/metrics/LineChart.js.map +1 -1
- package/dist/ui/metrics/MetricCard.js +42 -24
- package/dist/ui/metrics/MetricCard.js.map +1 -1
- package/dist/ui/metrics/TableToggle.js +7 -2
- package/dist/ui/metrics/TableToggle.js.map +1 -1
- package/dist/ui/metrics/chartParts.js +14 -5
- package/dist/ui/metrics/chartParts.js.map +1 -1
- package/dist/ui/navigation/ColorSchemeToggle.js +8 -4
- package/dist/ui/navigation/ColorSchemeToggle.js.map +1 -1
- package/dist/ui/navigation/FontSizeToggle.js +6 -2
- package/dist/ui/navigation/FontSizeToggle.js.map +1 -1
- package/dist/ui/navigation/LanguageToggle.js +8 -4
- package/dist/ui/navigation/LanguageToggle.js.map +1 -1
- package/dist/ui/navigation/PageBreadcrumbs.js +12 -6
- package/dist/ui/navigation/PageBreadcrumbs.js.map +1 -1
- package/dist/ui/timeline/GroupedTimeline.js +26 -13
- package/dist/ui/timeline/GroupedTimeline.js.map +1 -1
- package/dist/upload/FileDropzone.js +35 -25
- package/dist/upload/FileDropzone.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ModalWaiting.js","sources":["../../src/feedback/ModalWaiting.jsx"],"sourcesContent":["// Bloqueo general \"espere un momento\" como diálogo modal, para una acción que no se puede interrumpir (p. ej. generar\n// un archivo grande).\n//\n// OJO zIndex: HASTA ARRIBA de todo, encima de modal (1300), snackbar (1400) y tooltip (1500), para que ningún elemento\n// de la interfaz se le monte mientras bloquea. No se cierra con Escape a propósito.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport CircularProgress from '@mui/material/CircularProgress';\nimport Dialog from '@mui/material/Dialog';\nimport DialogContent from '@mui/material/DialogContent';\nimport DialogContentText from '@mui/material/DialogContentText';\nimport DialogTitle from '@mui/material/DialogTitle';\nimport Stack from '@mui/material/Stack';\n\n/**\n * @param {object} props\n * @param {boolean} props.open\n * @param {string} [props.title] ya traducido; por omisión common:waiting.title\n * @param {string} [props.text] ya traducido; por omisión common:waiting.text\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function ModalWaiting({open, title, text, texts})
|
|
1
|
+
{"version":3,"file":"ModalWaiting.js","sources":["../../src/feedback/ModalWaiting.jsx"],"sourcesContent":["// Bloqueo general \"espere un momento\" como diálogo modal, para una acción que no se puede interrumpir (p. ej. generar\n// un archivo grande).\n//\n// OJO zIndex: HASTA ARRIBA de todo, encima de modal (1300), snackbar (1400) y tooltip (1500), para que ningún elemento\n// de la interfaz se le monte mientras bloquea. No se cierra con Escape a propósito.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaModalWaiting con defaultProps y\n// styleOverrides de sus partes: root, title, body, text.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport CircularProgress from '@mui/material/CircularProgress';\nimport Dialog from '@mui/material/Dialog';\nimport DialogContent from '@mui/material/DialogContent';\nimport DialogContentText from '@mui/material/DialogContentText';\nimport DialogTitle from '@mui/material/DialogTitle';\nimport Stack from '@mui/material/Stack';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaModalWaiting)\nconst THEME_NAME = 'CalcitaModalWaiting';\nconst WaitingRoot = piecePart(THEME_NAME, 'Root', Dialog);\nconst WaitingTitle = piecePart(THEME_NAME, 'Title', DialogTitle);\nconst WaitingBody = piecePart(THEME_NAME, 'Body', Stack);\nconst WaitingText = piecePart(THEME_NAME, 'Text', DialogContentText);\n\n/**\n * @param {object} props\n * @param {boolean} props.open\n * @param {string} [props.title] ya traducido; por omisión common:waiting.title\n * @param {string} [props.text] ya traducido; por omisión common:waiting.text\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function ModalWaiting(props) {\n // lo que el proyecto puso en su tema (components.CalcitaModalWaiting.defaultProps) y encima lo que llega aquí\n const {open, title, text, texts} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('common', texts);\n\n return (\n <WaitingRoot open={open} disableEscapeKeyDown aria-busy=\"true\" ownSx={{zIndex: (theme) => theme.zIndex.tooltip + 10}}>\n <WaitingTitle ownSx={{textAlign: 'center'}}>{title ?? t('waiting.title')}</WaitingTitle>\n <DialogContent>\n <WaitingBody spacing={2} ownSx={{alignItems: 'center'}}>\n <CircularProgress size={28} />\n <WaitingText ownSx={{textAlign: 'center'}}>{text ?? t('waiting.text')}</WaitingText>\n </WaitingBody>\n </DialogContent>\n </WaitingRoot>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;AAoBA,MAAM,aAAa;AACnB,MAAM,cAAc,UAAU,YAAY,QAAQ,MAAM;AACxD,MAAM,eAAe,UAAU,YAAY,SAAS,WAAW;AAC/D,MAAM,cAAc,UAAU,YAAY,QAAQ,KAAK;AACvD,MAAM,cAAc,UAAU,YAAY,QAAQ,iBAAiB;AASnE,SAAwB,aAAa,OAAO;AAExC,QAAM,EAAC,MAAM,OAAO,MAAM,UAAS,cAAc,OAAO,UAAU;AAClE,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AAEjD,8BACK,aAAA,EAAY,MAAY,sBAAoB,MAAC,aAAU,QAAO,OAAO,EAAC,QAAQ,CAAC,UAAU,MAAM,OAAO,UAAU,MAC7G,UAAA;AAAA,IAAA,oBAAC,cAAA,EAAa,OAAO,EAAC,WAAW,YAAY,UAAA,SAAS,EAAE,eAAe,EAAA,CAAE;AAAA,IACzE,oBAAC,eAAA,EACG,UAAA,qBAAC,aAAA,EAAY,SAAS,GAAG,OAAO,EAAC,YAAY,SAAA,GACzC,UAAA;AAAA,MAAA,oBAAC,kBAAA,EAAiB,MAAM,GAAA,CAAI;AAAA,MAC5B,oBAAC,aAAA,EAAY,OAAO,EAAC,WAAW,YAAY,UAAA,QAAQ,EAAE,cAAc,EAAA,CAAE;AAAA,IAAA,EAAA,CAC1E,EAAA,CACJ;AAAA,EAAA,GACJ;AAER;"}
|
|
@@ -11,6 +11,13 @@ import { aboveMobileTabBarOf, GLASS_PANEL_GAP_PIXELS } from "../appShell/shellSi
|
|
|
11
11
|
import { minimalFloatingStylesOf } from "../theme/components/minimalComponents.js";
|
|
12
12
|
import { isGlassTheme, glassStylesOf } from "../theme/glassSurface.js";
|
|
13
13
|
import { APP_BAR_HEIGHT } from "../ui/layout/appBarHeight.js";
|
|
14
|
+
import { piecePart, usePieceProps } from "../theme/themePieces.js";
|
|
15
|
+
const THEME_NAME = "CalcitaNewBuildNotice";
|
|
16
|
+
const NoticeFloating = piecePart(THEME_NAME, "Floating", Box);
|
|
17
|
+
const NoticeIcon = piecePart(THEME_NAME, "Icon", RefreshRoundedIcon);
|
|
18
|
+
const NoticeMessage = piecePart(THEME_NAME, "Message", Typography);
|
|
19
|
+
const NoticeReloadButton = piecePart(THEME_NAME, "ReloadButton", Button);
|
|
20
|
+
const NoticeBanner = piecePart(THEME_NAME, "Banner", Alert);
|
|
14
21
|
const NEW_BUILD_NOTICE_VARIANT = Object.freeze({ ribbon: "ribbon", floating: "floating" });
|
|
15
22
|
const FLOATING_GAP_PIXELS = 8;
|
|
16
23
|
const FLOATING_ENTER_MILLISECONDS = 280;
|
|
@@ -45,7 +52,8 @@ function floatingSxOf(theme) {
|
|
|
45
52
|
"@media (prefers-reduced-motion: reduce)": { animation: "none" }
|
|
46
53
|
};
|
|
47
54
|
}
|
|
48
|
-
function NewBuildNotice(
|
|
55
|
+
function NewBuildNotice(props) {
|
|
56
|
+
const { message, reloadLabel, onReload = reloadPage, variant = NEW_BUILD_NOTICE_VARIANT.ribbon, texts } = usePieceProps(props, THEME_NAME);
|
|
49
57
|
const { t } = useToolkitTranslation("common", texts);
|
|
50
58
|
const isNewBuildPublished = useNewBuildPublished();
|
|
51
59
|
if (!isNewBuildPublished) {
|
|
@@ -53,21 +61,21 @@ function NewBuildNotice({ message, reloadLabel, onReload = reloadPage, variant =
|
|
|
53
61
|
}
|
|
54
62
|
const buttonLabel = reloadLabel ?? t("newBuild.reload");
|
|
55
63
|
if (variant === NEW_BUILD_NOTICE_VARIANT.floating) {
|
|
56
|
-
return /* @__PURE__ */ jsxs(
|
|
57
|
-
/* @__PURE__ */ jsx(
|
|
58
|
-
/* @__PURE__ */ jsx(
|
|
59
|
-
/* @__PURE__ */ jsx(
|
|
64
|
+
return /* @__PURE__ */ jsxs(NoticeFloating, { role: "status", "aria-live": "polite", "aria-label": message ?? t("newBuild.message"), ownSx: floatingSxOf, children: [
|
|
65
|
+
/* @__PURE__ */ jsx(NoticeIcon, { fontSize: "small", ownSx: { color: "text.secondary" } }),
|
|
66
|
+
/* @__PURE__ */ jsx(NoticeMessage, { variant: "body2", noWrap: true, ownSx: { fontWeight: 500, minWidth: 0 }, children: message ?? t("newBuild.short") }),
|
|
67
|
+
/* @__PURE__ */ jsx(NoticeReloadButton, { size: "small", variant: "contained", onClick: onReload, ownSx: { flexShrink: 0 }, children: buttonLabel })
|
|
60
68
|
] });
|
|
61
69
|
}
|
|
62
70
|
return /* @__PURE__ */ jsx(
|
|
63
|
-
|
|
71
|
+
NoticeBanner,
|
|
64
72
|
{
|
|
65
73
|
severity: "warning",
|
|
66
74
|
square: true,
|
|
67
75
|
role: "status",
|
|
68
76
|
"aria-live": "polite",
|
|
69
|
-
|
|
70
|
-
action: /* @__PURE__ */ jsx(
|
|
77
|
+
ownSx: { py: 0, px: { xs: 2, md: 3 }, alignItems: "center" },
|
|
78
|
+
action: /* @__PURE__ */ jsx(NoticeReloadButton, { size: "small", variant: "outlined", color: "inherit", onClick: onReload, ownSx: { flexShrink: 0 }, children: buttonLabel }),
|
|
71
79
|
children: message ?? t("newBuild.message")
|
|
72
80
|
}
|
|
73
81
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NewBuildNotice.js","sources":["../../src/feedback/NewBuildNotice.jsx"],"sourcesContent":["// Aviso «Hay una versión nueva» con su botón de recargar.\n//\n// Sale cuando el servidor anuncia una compilación del front distinta a la que corre en la pestaña\n// (observability/publishedBuild.js) y se queda hasta que la persona recarga. No recarga solo: la persona puede estar a\n// media captura y decide cuándo.\n//\n// Dos formas, el mismo aviso:\n// - «ribbon» (por omisión): una franja a todo lo ancho que el proyecto pone donde se vea siempre, normalmente dentro de\n// su AppBar, debajo de la barra. Se arma con el Alert de MUI, que ya trae sus colores para claro y oscuro.\n// - «floating»: una cápsula de vidrio que flota aparte, sin tocar la barra, como el reproductor chico que Apple pone\n// encima de las pestañas en el iPhone. En escritorio, centrada debajo de la barra de arriba; en el teléfono, encima de\n// la barra de pestañas. Es la que usa AppLayout con vidrio: dentro de la cápsula de la barra la franja la hacía crecer al\n// doble, la curva cortaba el texto y el botón se salía por la orilla (lo reportaron).\n// Sin compilación nueva no pinta nada ni ocupa lugar.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Alert from '@mui/material/Alert';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Typography from '@mui/material/Typography';\nimport RefreshRoundedIcon from '@mui/icons-material/RefreshRounded';\n\nimport {useNewBuildPublished} from '@/observability/publishedBuild.js';\nimport {GLASS_PANEL_GAP_PIXELS, aboveMobileTabBarOf} from '@/appShell/shellSizes.js';\nimport {minimalFloatingStylesOf} from '@/theme/components/minimalComponents.js';\nimport {glassStylesOf, isGlassTheme} from '@/theme/glassSurface.js';\nimport {APP_BAR_HEIGHT} from '@/ui/layout/appBarHeight.js';\n\n/** Las formas del aviso: franja (dentro de la barra del proyecto) o cápsula flotante (la del marco con vidrio). */\nexport const NEW_BUILD_NOTICE_VARIANT = Object.freeze({ribbon: 'ribbon', floating: 'floating'});\n\n// lo que se separa la cápsula de la barra (px) y lo que tarda en aparecer (ms)\nconst FLOATING_GAP_PIXELS = 8;\nconst FLOATING_ENTER_MILLISECONDS = 280;\n\n/** Lo que hace el botón si el proyecto no dice otra cosa: recargar la página, que trae la compilación nueva. */\nfunction reloadPage() {\n window.location.reload();\n}\n\n/**\n * La cápsula flotante: fija a la ventana y centrada. En escritorio queda debajo de la barra de arriba (su alto real,\n * APP_BAR_HEIGHT); en el teléfono, encima de la barra de pestañas (su alto, sus separaciones y la zona del gesto de inicio).\n *\n * Va centrada en la VENTANA, debajo del centro de la barra (que va de orilla a orilla): centrada en el contenido quedaba\n * corrida a la derecha respecto a la barra (lo reportaron).\n *\n * @param {object} theme\n * @returns {object}\n */\nfunction floatingSxOf(theme) {\n return {\n // con vidrio, cápsula de vidrio; con el minimalista (barra en cápsulas), sólida del gris de lo que flota\n ...(isGlassTheme(theme) ? glassStylesOf(theme) : minimalFloatingStylesOf(theme)),\n position: 'fixed',\n left: '50%',\n transform: 'translateX(-50%)',\n zIndex: theme.zIndex.appBar,\n display: 'flex',\n alignItems: 'center',\n gap: 1.5,\n width: 'max-content',\n maxWidth: `calc(100% - ${GLASS_PANEL_GAP_PIXELS * 2}px)`,\n borderRadius: '999px',\n py: 0.75,\n pl: 2,\n pr: 0.75,\n top: `calc(${APP_BAR_HEIGHT} + ${FLOATING_GAP_PIXELS}px)`,\n ...aboveMobileTabBarOf(FLOATING_GAP_PIXELS),\n // aparece subiendo un poco y aclarándose; sin movimiento si la persona lo pidió así en su equipo\n '@keyframes calcitaNoticeEnter': {\n from: {opacity: 0, transform: 'translate(-50%, 8px) scale(0.96)'},\n to: {opacity: 1, transform: 'translate(-50%, 0) scale(1)'},\n },\n animation: `calcitaNoticeEnter ${FLOATING_ENTER_MILLISECONDS}ms ${theme.transitions.easing.easeOut}`,\n '@media (prefers-reduced-motion: reduce)': {animation: 'none'},\n };\n}\n\n/**\n * @param {object} [props]\n * @param {string} [props.message] ya traducido; por omisión el del paquete (en la cápsula, el corto: common:newBuild.short)\n * @param {string} [props.reloadLabel] ya traducido; por omisión «Recargar»\n * @param {() => void} [props.onReload] por omisión recarga la página\n * @param {'ribbon'|'floating'} [props.variant] franja o cápsula flotante (NEW_BUILD_NOTICE_VARIANT); por omisión franja\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function NewBuildNotice({message, reloadLabel, onReload = reloadPage, variant = NEW_BUILD_NOTICE_VARIANT.ribbon, texts})
|
|
1
|
+
{"version":3,"file":"NewBuildNotice.js","sources":["../../src/feedback/NewBuildNotice.jsx"],"sourcesContent":["// Aviso «Hay una versión nueva» con su botón de recargar.\n//\n// Sale cuando el servidor anuncia una compilación del front distinta a la que corre en la pestaña\n// (observability/publishedBuild.js) y se queda hasta que la persona recarga. No recarga solo: la persona puede estar a\n// media captura y decide cuándo.\n//\n// Dos formas, el mismo aviso:\n// - «ribbon» (por omisión): una franja a todo lo ancho que el proyecto pone donde se vea siempre, normalmente dentro de\n// su AppBar, debajo de la barra. Se arma con el Alert de MUI, que ya trae sus colores para claro y oscuro.\n// - «floating»: una cápsula de vidrio que flota aparte, sin tocar la barra, como el reproductor chico que Apple pone\n// encima de las pestañas en el iPhone. En escritorio, centrada debajo de la barra de arriba; en el teléfono, encima de\n// la barra de pestañas. Es la que usa AppLayout con vidrio: dentro de la cápsula de la barra la franja la hacía crecer al\n// doble, la curva cortaba el texto y el botón se salía por la orilla (lo reportaron).\n// Sin compilación nueva no pinta nada ni ocupa lugar.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaNewBuildNotice con defaultProps y\n// styleOverrides de sus partes: floating, icon, message, reloadButton, banner.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Alert from '@mui/material/Alert';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Typography from '@mui/material/Typography';\nimport RefreshRoundedIcon from '@mui/icons-material/RefreshRounded';\n\nimport {useNewBuildPublished} from '@/observability/publishedBuild.js';\nimport {GLASS_PANEL_GAP_PIXELS, aboveMobileTabBarOf} from '@/appShell/shellSizes.js';\nimport {minimalFloatingStylesOf} from '@/theme/components/minimalComponents.js';\nimport {glassStylesOf, isGlassTheme} from '@/theme/glassSurface.js';\nimport {APP_BAR_HEIGHT} from '@/ui/layout/appBarHeight.js';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaNewBuildNotice)\nconst THEME_NAME = 'CalcitaNewBuildNotice';\nconst NoticeFloating = piecePart(THEME_NAME, 'Floating', Box);\nconst NoticeIcon = piecePart(THEME_NAME, 'Icon', RefreshRoundedIcon);\nconst NoticeMessage = piecePart(THEME_NAME, 'Message', Typography);\nconst NoticeReloadButton = piecePart(THEME_NAME, 'ReloadButton', Button);\nconst NoticeBanner = piecePart(THEME_NAME, 'Banner', Alert);\n\n/** Las formas del aviso: franja (dentro de la barra del proyecto) o cápsula flotante (la del marco con vidrio). */\nexport const NEW_BUILD_NOTICE_VARIANT = Object.freeze({ribbon: 'ribbon', floating: 'floating'});\n\n// lo que se separa la cápsula de la barra (px) y lo que tarda en aparecer (ms)\nconst FLOATING_GAP_PIXELS = 8;\nconst FLOATING_ENTER_MILLISECONDS = 280;\n\n/** Lo que hace el botón si el proyecto no dice otra cosa: recargar la página, que trae la compilación nueva. */\nfunction reloadPage() {\n window.location.reload();\n}\n\n/**\n * La cápsula flotante: fija a la ventana y centrada. En escritorio queda debajo de la barra de arriba (su alto real,\n * APP_BAR_HEIGHT); en el teléfono, encima de la barra de pestañas (su alto, sus separaciones y la zona del gesto de inicio).\n *\n * Va centrada en la VENTANA, debajo del centro de la barra (que va de orilla a orilla): centrada en el contenido quedaba\n * corrida a la derecha respecto a la barra (lo reportaron).\n *\n * @param {object} theme\n * @returns {object}\n */\nfunction floatingSxOf(theme) {\n return {\n // con vidrio, cápsula de vidrio; con el minimalista (barra en cápsulas), sólida del gris de lo que flota\n ...(isGlassTheme(theme) ? glassStylesOf(theme) : minimalFloatingStylesOf(theme)),\n position: 'fixed',\n left: '50%',\n transform: 'translateX(-50%)',\n zIndex: theme.zIndex.appBar,\n display: 'flex',\n alignItems: 'center',\n gap: 1.5,\n width: 'max-content',\n maxWidth: `calc(100% - ${GLASS_PANEL_GAP_PIXELS * 2}px)`,\n borderRadius: '999px',\n py: 0.75,\n pl: 2,\n pr: 0.75,\n top: `calc(${APP_BAR_HEIGHT} + ${FLOATING_GAP_PIXELS}px)`,\n ...aboveMobileTabBarOf(FLOATING_GAP_PIXELS),\n // aparece subiendo un poco y aclarándose; sin movimiento si la persona lo pidió así en su equipo\n '@keyframes calcitaNoticeEnter': {\n from: {opacity: 0, transform: 'translate(-50%, 8px) scale(0.96)'},\n to: {opacity: 1, transform: 'translate(-50%, 0) scale(1)'},\n },\n animation: `calcitaNoticeEnter ${FLOATING_ENTER_MILLISECONDS}ms ${theme.transitions.easing.easeOut}`,\n '@media (prefers-reduced-motion: reduce)': {animation: 'none'},\n };\n}\n\n/**\n * @param {object} [props]\n * @param {string} [props.message] ya traducido; por omisión el del paquete (en la cápsula, el corto: common:newBuild.short)\n * @param {string} [props.reloadLabel] ya traducido; por omisión «Recargar»\n * @param {() => void} [props.onReload] por omisión recarga la página\n * @param {'ribbon'|'floating'} [props.variant] franja o cápsula flotante (NEW_BUILD_NOTICE_VARIANT); por omisión franja\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function NewBuildNotice(props) {\n // lo que el proyecto puso en su tema (components.CalcitaNewBuildNotice.defaultProps) y encima lo que llega aquí\n const {message, reloadLabel, onReload = reloadPage, variant = NEW_BUILD_NOTICE_VARIANT.ribbon, texts} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('common', texts);\n const isNewBuildPublished = useNewBuildPublished();\n\n // guard clause: la compilación de la pestaña sigue siendo la publicada; el aviso no ocupa lugar\n if (!isNewBuildPublished) {\n return null;\n }\n\n const buttonLabel = reloadLabel ?? t('newBuild.reload');\n\n if (variant === NEW_BUILD_NOTICE_VARIANT.floating) {\n // el texto largo va para el lector de pantalla; a la vista, el corto (una cápsula de un renglón)\n return (\n <NoticeFloating role=\"status\" aria-live=\"polite\" aria-label={message ?? t('newBuild.message')} ownSx={floatingSxOf}>\n <NoticeIcon fontSize=\"small\" ownSx={{color: 'text.secondary'}} />\n <NoticeMessage variant=\"body2\" noWrap ownSx={{fontWeight: 500, minWidth: 0}}>{message ?? t('newBuild.short')}</NoticeMessage>\n <NoticeReloadButton size=\"small\" variant=\"contained\" onClick={onReload} ownSx={{flexShrink: 0}}>{buttonLabel}</NoticeReloadButton>\n </NoticeFloating>\n );\n }\n\n return (\n <NoticeBanner severity=\"warning\" square role=\"status\" aria-live=\"polite\"\n ownSx={{py: 0, px: {xs: 2, md: 3}, alignItems: 'center'}}\n action={(\n <NoticeReloadButton size=\"small\" variant=\"outlined\" color=\"inherit\" onClick={onReload} ownSx={{flexShrink: 0}}>\n {buttonLabel}\n </NoticeReloadButton>\n )}>\n {message ?? t('newBuild.message')}\n </NoticeBanner>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAkCA,MAAM,aAAa;AACnB,MAAM,iBAAiB,UAAU,YAAY,YAAY,GAAG;AAC5D,MAAM,aAAa,UAAU,YAAY,QAAQ,kBAAkB;AACnE,MAAM,gBAAgB,UAAU,YAAY,WAAW,UAAU;AACjE,MAAM,qBAAqB,UAAU,YAAY,gBAAgB,MAAM;AACvE,MAAM,eAAe,UAAU,YAAY,UAAU,KAAK;AAGnD,MAAM,2BAA2B,OAAO,OAAO,EAAC,QAAQ,UAAU,UAAU,YAAW;AAG9F,MAAM,sBAAsB;AAC5B,MAAM,8BAA8B;AAGpC,SAAS,aAAa;AAClB,SAAO,SAAS,OAAA;AACpB;AAYA,SAAS,aAAa,OAAO;AACzB,SAAO;AAAA;AAAA,IAEH,GAAI,aAAa,KAAK,IAAI,cAAc,KAAK,IAAI,wBAAwB,KAAK;AAAA,IAC9E,UAAU;AAAA,IACV,MAAM;AAAA,IACN,WAAW;AAAA,IACX,QAAQ,MAAM,OAAO;AAAA,IACrB,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,KAAK;AAAA,IACL,OAAO;AAAA,IACP,UAAU,eAAe,yBAAyB,CAAC;AAAA,IACnD,cAAc;AAAA,IACd,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,KAAK,QAAQ,cAAc,MAAM,mBAAmB;AAAA,IACpD,GAAG,oBAAoB,mBAAmB;AAAA;AAAA,IAE1C,iCAAiC;AAAA,MAC7B,MAAM,EAAC,SAAS,GAAG,WAAW,mCAAA;AAAA,MAC9B,IAAI,EAAC,SAAS,GAAG,WAAW,8BAAA;AAAA,IAA6B;AAAA,IAE7D,WAAW,sBAAsB,2BAA2B,MAAM,MAAM,YAAY,OAAO,OAAO;AAAA,IAClG,2CAA2C,EAAC,WAAW,OAAA;AAAA,EAAM;AAErE;AAUA,SAAwB,eAAe,OAAO;AAE1C,QAAM,EAAC,SAAS,aAAa,WAAW,YAAY,UAAU,yBAAyB,QAAQ,MAAA,IAAS,cAAc,OAAO,UAAU;AACvI,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AACjD,QAAM,sBAAsB,qBAAA;AAG5B,MAAI,CAAC,qBAAqB;AACtB,WAAO;AAAA,EACX;AAEA,QAAM,cAAc,eAAe,EAAE,iBAAiB;AAEtD,MAAI,YAAY,yBAAyB,UAAU;AAE/C,WACI,qBAAC,gBAAA,EAAe,MAAK,UAAS,aAAU,UAAS,cAAY,WAAW,EAAE,kBAAkB,GAAG,OAAO,cAClG,UAAA;AAAA,MAAA,oBAAC,cAAW,UAAS,SAAQ,OAAO,EAAC,OAAO,oBAAmB;AAAA,0BAC9D,eAAA,EAAc,SAAQ,SAAQ,QAAM,MAAC,OAAO,EAAC,YAAY,KAAK,UAAU,KAAK,UAAA,WAAW,EAAE,gBAAgB,GAAE;AAAA,MAC7G,oBAAC,oBAAA,EAAmB,MAAK,SAAQ,SAAQ,aAAY,SAAS,UAAU,OAAO,EAAC,YAAY,EAAA,GAAK,UAAA,YAAA,CAAY;AAAA,IAAA,GACjH;AAAA,EAER;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAa,UAAS;AAAA,MAAU,QAAM;AAAA,MAAC,MAAK;AAAA,MAAS,aAAU;AAAA,MACzD,OAAO,EAAC,IAAI,GAAG,IAAI,EAAC,IAAI,GAAG,IAAI,KAAI,YAAY,SAAA;AAAA,MAC/C,QACI,oBAAC,oBAAA,EAAmB,MAAK,SAAQ,SAAQ,YAAW,OAAM,WAAU,SAAS,UAAU,OAAO,EAAC,YAAY,KACtG,UAAA,aACL;AAAA,MAEN,UAAA,WAAW,EAAE,kBAAkB;AAAA,IAAA;AAAA,EAAA;AAG5C;"}
|
|
@@ -15,6 +15,13 @@ import { toastError } from "../feedback/toast.js";
|
|
|
15
15
|
import { accentTextStylesOf } from "../theme/glassSurface.js";
|
|
16
16
|
import { useFormatters } from "../toolkit/useFormatters.js";
|
|
17
17
|
import { resolveAccept, parseFileSize, FILE_REJECTION } from "../upload/fileRules.js";
|
|
18
|
+
import { piecePart, usePieceProps } from "../theme/themePieces.js";
|
|
19
|
+
const THEME_NAME = "CalcitaPondDropzone";
|
|
20
|
+
const PondRoot = piecePart(THEME_NAME, "Root", Box);
|
|
21
|
+
const PondSummary = piecePart(THEME_NAME, "Summary", Typography);
|
|
22
|
+
const PondLimits = piecePart(THEME_NAME, "Limits", Typography);
|
|
23
|
+
const PondActions = piecePart(THEME_NAME, "Actions", Stack);
|
|
24
|
+
const PondExtra = piecePart(THEME_NAME, "Extra", Box);
|
|
18
25
|
registerPlugin(FilePondPluginFileValidateType, FilePondPluginFileValidateSize);
|
|
19
26
|
const AUTO_REJECT_DELAY_MILLISECONDS = 1800;
|
|
20
27
|
const MAX_FILES_WARNING = "max files";
|
|
@@ -72,32 +79,33 @@ function pondThemeSxOf(theme) {
|
|
|
72
79
|
"& .filepond--file, & .filepond--file-action-button": { color: palette.primary.contrastText }
|
|
73
80
|
};
|
|
74
81
|
}
|
|
75
|
-
function FileDropzone({
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
82
|
+
function FileDropzone(props) {
|
|
83
|
+
const {
|
|
84
|
+
value = null,
|
|
85
|
+
onChange,
|
|
86
|
+
accept,
|
|
87
|
+
maxSize,
|
|
88
|
+
multiple = false,
|
|
89
|
+
maxFiles,
|
|
90
|
+
maxTotalSize,
|
|
91
|
+
name,
|
|
92
|
+
hint,
|
|
93
|
+
buttonLabel,
|
|
94
|
+
disabled = false,
|
|
95
|
+
busy = false,
|
|
96
|
+
busyModal = false,
|
|
97
|
+
autoReject = true,
|
|
98
|
+
validate = null,
|
|
99
|
+
server = null,
|
|
100
|
+
instantUpload = false,
|
|
101
|
+
apiRef = null,
|
|
102
|
+
onProcessProgress,
|
|
103
|
+
onReject,
|
|
104
|
+
actions,
|
|
105
|
+
children,
|
|
106
|
+
sx,
|
|
107
|
+
texts
|
|
108
|
+
} = usePieceProps(props, THEME_NAME);
|
|
101
109
|
const { t } = useToolkitTranslation("common", texts);
|
|
102
110
|
const { formatFileSize } = useFormatters();
|
|
103
111
|
const generatedId = useId();
|
|
@@ -207,7 +215,7 @@ function FileDropzone({
|
|
|
207
215
|
const totalBytes = multiple ? files.reduce((sum, file) => sum + (file?.size ?? 0), 0) : 0;
|
|
208
216
|
const hintText = hint ?? (multiple ? t("upload.hintMultiple") : t("upload.hint"));
|
|
209
217
|
const chooseText = buttonLabel ?? (multiple ? t("upload.chooseMultiple") : t("upload.choose"));
|
|
210
|
-
return /* @__PURE__ */ jsxs(
|
|
218
|
+
return /* @__PURE__ */ jsxs(PondRoot, { ownSx: (theme) => pondThemeSxOf(theme), sx, children: [
|
|
211
219
|
/* @__PURE__ */ jsx(
|
|
212
220
|
FilePond,
|
|
213
221
|
{
|
|
@@ -243,10 +251,10 @@ function FileDropzone({
|
|
|
243
251
|
...pondLabelsOf(t)
|
|
244
252
|
}
|
|
245
253
|
),
|
|
246
|
-
multiple && files.length > 0 && /* @__PURE__ */ jsx(
|
|
247
|
-
limitHints.length > 0 && /* @__PURE__ */ jsx(
|
|
248
|
-
actions && /* @__PURE__ */ jsx(
|
|
249
|
-
children && /* @__PURE__ */ jsx(
|
|
254
|
+
multiple && files.length > 0 && /* @__PURE__ */ jsx(PondSummary, { variant: "body2", ownSx: { textAlign: "center", mt: 1, fontWeight: 500 }, children: maxTotalBytes ? t("upload.summaryOf", { count: files.length, total: formatFileSize(totalBytes), max: formatFileSize(maxTotalBytes) }) : t("upload.summary", { count: files.length, total: formatFileSize(totalBytes) }) }),
|
|
255
|
+
limitHints.length > 0 && /* @__PURE__ */ jsx(PondLimits, { variant: "caption", color: "text.secondary", ownSx: { display: "block", textAlign: "center", mt: 0.5 }, children: limitHints.join(" · ") }),
|
|
256
|
+
actions && /* @__PURE__ */ jsx(PondActions, { direction: { xs: "column", sm: "row" }, spacing: 2, ownSx: { justifyContent: "center", alignItems: { xs: "stretch", sm: "center" }, mt: 2 }, children: actions }),
|
|
257
|
+
children && /* @__PURE__ */ jsx(PondExtra, { ownSx: { mt: 3 }, children }),
|
|
250
258
|
busyModal && /* @__PURE__ */ jsx(ModalWaiting, { open: busy, title: busyModal.title, text: busyModal.text })
|
|
251
259
|
] });
|
|
252
260
|
}
|
|
@@ -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// 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';\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 */\nexport default function FileDropzone({\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}) {\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 <Box sx={[(theme) => pondThemeSxOf(theme), ...(Array.isArray(sx) ? 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 <Typography variant=\"body2\" sx={{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 </Typography>\n )}\n {limitHints.length > 0 && (\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{display: 'block', textAlign: 'center', mt: 0.5}}>\n {limitHints.join(' · ')}\n </Typography>\n )}\n {actions && (\n <Stack direction={{xs: 'column', sm: 'row'}} spacing={2} sx={{justifyContent: 'center', alignItems: {xs: 'stretch', sm: 'center'}, mt: 2}}>\n {actions}\n </Stack>\n )}\n {children && <Box sx={{mt: 3}}>{children}</Box>}\n {busyModal && <ModalWaiting open={busy} title={busyModal.title} text={busyModal.text} />}\n </Box>\n );\n}\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// 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} from '@/theme/themePieces.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 */\nexport default function FileDropzone(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} = usePieceProps(props, THEME_NAME);\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"],"names":[],"mappings":"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"}
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useFormikContext } from "formik";
|
|
4
4
|
import Alert from "@mui/material/Alert";
|
|
5
|
+
import { piecePart } from "../theme/themePieces.js";
|
|
6
|
+
const THEME_NAME = "CalcitaFormProblemAlert";
|
|
7
|
+
const ProblemAlert = piecePart(THEME_NAME, "Root", Alert);
|
|
5
8
|
function FormProblemAlert({ sx }) {
|
|
6
9
|
const { status } = useFormikContext();
|
|
7
10
|
if (!status) {
|
|
8
11
|
return null;
|
|
9
12
|
}
|
|
10
|
-
return /* @__PURE__ */ jsx(
|
|
13
|
+
return /* @__PURE__ */ jsx(ProblemAlert, { severity: "error", role: "alert", sx, children: status });
|
|
11
14
|
}
|
|
12
15
|
export {
|
|
13
16
|
FormProblemAlert as default
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormProblemAlert.js","sources":["../../src/forms/FormProblemAlert.jsx"],"sourcesContent":["// El aviso de arriba del formulario de formik: lo que el servidor dijo que no es de un campo (sin conexión, un 409, un 500)\n// o el error de un campo que el formulario no tiene. applyServerProblem lo deja en el status de formik; aquí se pinta.\n//\n// Sin mensaje no pinta nada ni ocupa lugar.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useFormikContext} from 'formik';\nimport Alert from '@mui/material/Alert';\n\n/**\n * @param {{sx?: object}} props\n */\nexport default function FormProblemAlert({sx}) {\n const {status} = useFormikContext();\n\n // guard clause: sin aviso del servidor\n if (!status) {\n return null;\n }\n return <
|
|
1
|
+
{"version":3,"file":"FormProblemAlert.js","sources":["../../src/forms/FormProblemAlert.jsx"],"sourcesContent":["// El aviso de arriba del formulario de formik: lo que el servidor dijo que no es de un campo (sin conexión, un 409, un 500)\n// o el error de un campo que el formulario no tiene. applyServerProblem lo deja en el status de formik; aquí se pinta.\n//\n// Sin mensaje no pinta nada ni ocupa lugar.\n//\n// Se cambia desde el tema del proyecto: components.CalcitaFormProblemAlert con styleOverrides.root.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useFormikContext} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport {piecePart} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y su parte (theme.components.CalcitaFormProblemAlert): el estilo del proyecto va\n// después del de MUI y el sx de la pantalla al final\nconst THEME_NAME = 'CalcitaFormProblemAlert';\nconst ProblemAlert = piecePart(THEME_NAME, 'Root', Alert);\n\n/**\n * @param {{sx?: object}} props\n */\nexport default function FormProblemAlert({sx}) {\n const {status} = useFormikContext();\n\n // guard clause: sin aviso del servidor\n if (!status) {\n return null;\n }\n return <ProblemAlert severity=\"error\" role=\"alert\" sx={sx}>{status}</ProblemAlert>;\n}\n"],"names":[],"mappings":";;;;;AAcA,MAAM,aAAa;AACnB,MAAM,eAAe,UAAU,YAAY,QAAQ,KAAK;AAKxD,SAAwB,iBAAiB,EAAC,MAAK;AAC3C,QAAM,EAAC,OAAA,IAAU,iBAAA;AAGjB,MAAI,CAAC,QAAQ;AACT,WAAO;AAAA,EACX;AACA,6BAAQ,cAAA,EAAa,UAAS,SAAQ,MAAK,SAAQ,IAAS,UAAA,QAAO;AACvE;"}
|
|
@@ -13,11 +13,15 @@ import FormTextField from "../FormTextField.js";
|
|
|
13
13
|
import SubmitButton from "../SubmitButton.js";
|
|
14
14
|
import { applyServerProblem } from "../applyServerProblem.js";
|
|
15
15
|
import * as Yup from "yup";
|
|
16
|
+
import { piecePart, usePieceProps } from "../../theme/themePieces.js";
|
|
17
|
+
const THEME_NAME = "CalcitaSignInForm";
|
|
18
|
+
const SignInFormFooter = piecePart(THEME_NAME, "Footer", Stack);
|
|
16
19
|
function RememberMeField({ label }) {
|
|
17
20
|
const [field] = useField({ name: "remember", type: "checkbox" });
|
|
18
21
|
return /* @__PURE__ */ jsx(FormControlLabel, { label, control: /* @__PURE__ */ jsx(Checkbox, { ...field }) });
|
|
19
22
|
}
|
|
20
|
-
function SignInForm(
|
|
23
|
+
function SignInForm(props) {
|
|
24
|
+
const { onSubmit, forgotPasswordPath, showRememberMe = true, initialEmail = "", texts } = usePieceProps(props, THEME_NAME);
|
|
21
25
|
const { t } = useToolkitTranslation("auth", texts);
|
|
22
26
|
const { Link: RouterLink } = useToolkitNavigation();
|
|
23
27
|
const schema = useMemo(() => Yup.object({
|
|
@@ -35,7 +39,7 @@ function SignInForm({ onSubmit, forgotPasswordPath, showRememberMe = true, initi
|
|
|
35
39
|
/* @__PURE__ */ jsx(FormProblemAlert, {}),
|
|
36
40
|
/* @__PURE__ */ jsx(FormTextField, { name: "email", label: t("signIn.email"), type: "email", autoComplete: "username", autoFocus: true }),
|
|
37
41
|
/* @__PURE__ */ jsx(FormTextField, { name: "password", label: t("signIn.password"), type: "password", autoComplete: "current-password" }),
|
|
38
|
-
/* @__PURE__ */ jsxs(
|
|
42
|
+
/* @__PURE__ */ jsxs(SignInFormFooter, { direction: "row", ownSx: { alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 1 }, children: [
|
|
39
43
|
showRememberMe ? /* @__PURE__ */ jsx(RememberMeField, { label: t("signIn.remember") }) : /* @__PURE__ */ jsx("span", {}),
|
|
40
44
|
forgotPasswordPath && /* @__PURE__ */ jsx(Link, { component: RouterLink, to: forgotPasswordPath, variant: "body2", children: t("signIn.forgot") })
|
|
41
45
|
] }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SignInForm.js","sources":["../../../src/forms/auth/SignInForm.jsx"],"sourcesContent":["// El formulario de iniciar sesión (formik): correo, contraseña, «mantener la sesión abierta» y la liga a «¿Olvidó su\n// contraseña?». La pantalla solo dice qué hacer con los datos:\n//\n// <AuthLayout title={t('auth:signIn.title')}>\n// <SignInForm onSubmit={(values) => signIn(values)} forgotPasswordPath=\"/forgot-password\" />\n// </AuthLayout>\n//\n// Si onSubmit truena con el problema del servidor (RFC 9457; un 401 «correo o contraseña incorrectos», un 429 «espere\n// un minuto»), el aviso sale arriba del formulario y lo de cada campo, en su campo (applyServerProblem).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo} from 'react';\nimport {Form, Formik, useField} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Checkbox from '@mui/material/Checkbox';\nimport FormControlLabel from '@mui/material/FormControlLabel';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\n\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport FormTextField from '@/forms/FormTextField.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport {applyServerProblem} from '@/forms/applyServerProblem.js';\nimport {Yup} from '@/forms/validation.js';\n\n/** «Mantener la sesión abierta», conectado a formik. */\nfunction RememberMeField({label}) {\n const [field] = useField({name: 'remember', type: 'checkbox'});\n return <FormControlLabel label={label} control={<Checkbox {...field} />} />;\n}\n\n/**\n * @param {object} props\n * @param {(values: {email: string, password: string, remember: boolean}) => Promise<unknown>} props.onSubmit entra; si\n * el servidor dice que no, truena con su problema\n * @param {string} [props.forgotPasswordPath] sin ella no sale la liga de «¿Olvidó su contraseña?»\n * @param {boolean} [props.showRememberMe] por omisión sí\n * @param {string} [props.initialEmail]\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function SignInForm({onSubmit, forgotPasswordPath, showRememberMe = true, initialEmail = '', texts})
|
|
1
|
+
{"version":3,"file":"SignInForm.js","sources":["../../../src/forms/auth/SignInForm.jsx"],"sourcesContent":["// El formulario de iniciar sesión (formik): correo, contraseña, «mantener la sesión abierta» y la liga a «¿Olvidó su\n// contraseña?». La pantalla solo dice qué hacer con los datos:\n//\n// <AuthLayout title={t('auth:signIn.title')}>\n// <SignInForm onSubmit={(values) => signIn(values)} forgotPasswordPath=\"/forgot-password\" />\n// </AuthLayout>\n//\n// Si onSubmit truena con el problema del servidor (RFC 9457; un 401 «correo o contraseña incorrectos», un 429 «espere\n// un minuto»), el aviso sale arriba del formulario y lo de cada campo, en su campo (applyServerProblem).\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaSignInForm con defaultProps y\n// styleOverrides de sus partes: footer.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo} from 'react';\nimport {Form, Formik, useField} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Checkbox from '@mui/material/Checkbox';\nimport FormControlLabel from '@mui/material/FormControlLabel';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\n\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport FormTextField from '@/forms/FormTextField.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport {applyServerProblem} from '@/forms/applyServerProblem.js';\nimport {Yup} from '@/forms/validation.js';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaSignInForm)\nconst THEME_NAME = 'CalcitaSignInForm';\nconst SignInFormFooter = piecePart(THEME_NAME, 'Footer', Stack);\n\n/** «Mantener la sesión abierta», conectado a formik. */\nfunction RememberMeField({label}) {\n const [field] = useField({name: 'remember', type: 'checkbox'});\n return <FormControlLabel label={label} control={<Checkbox {...field} />} />;\n}\n\n/**\n * @param {object} props\n * @param {(values: {email: string, password: string, remember: boolean}) => Promise<unknown>} props.onSubmit entra; si\n * el servidor dice que no, truena con su problema\n * @param {string} [props.forgotPasswordPath] sin ella no sale la liga de «¿Olvidó su contraseña?»\n * @param {boolean} [props.showRememberMe] por omisión sí\n * @param {string} [props.initialEmail]\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function SignInForm(props) {\n // lo que el proyecto puso en su tema (components.CalcitaSignInForm.defaultProps) y encima lo que llega aquí\n const {onSubmit, forgotPasswordPath, showRememberMe = true, initialEmail = '', texts} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('auth', texts);\n const {Link: RouterLink} = useToolkitNavigation();\n // el esquema se arma al pintar y no al importar: yup toma sus mensajes al armarlo (buildYupLocale ya corrió)\n const schema = useMemo(() => Yup.object({\n email: Yup.string().trim().required().email(),\n password: Yup.string().required(),\n }), []);\n\n const submit = async (values, formikHelpers) => {\n try {\n await onSubmit({...values, email: values.email.trim()});\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <Formik initialValues={{email: initialEmail, password: '', remember: false}} validationSchema={schema} onSubmit={submit}>\n <Form noValidate>\n <Stack spacing={2}>\n <FormProblemAlert />\n <FormTextField name=\"email\" label={t('signIn.email')} type=\"email\" autoComplete=\"username\" autoFocus />\n <FormTextField name=\"password\" label={t('signIn.password')} type=\"password\" autoComplete=\"current-password\" />\n <SignInFormFooter direction=\"row\" ownSx={{alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 1}}>\n {showRememberMe ? <RememberMeField label={t('signIn.remember')} /> : <span />}\n {forgotPasswordPath && (\n <Link component={RouterLink} to={forgotPasswordPath} variant=\"body2\">{t('signIn.forgot')}</Link>\n )}\n </SignInFormFooter>\n <SubmitButton fullWidth>{t('signIn.submit')}</SubmitButton>\n </Stack>\n </Form>\n </Formik>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AA+BA,MAAM,aAAa;AACnB,MAAM,mBAAmB,UAAU,YAAY,UAAU,KAAK;AAG9D,SAAS,gBAAgB,EAAC,SAAQ;AAC9B,QAAM,CAAC,KAAK,IAAI,SAAS,EAAC,MAAM,YAAY,MAAM,YAAW;AAC7D,SAAO,oBAAC,oBAAiB,OAAc,6BAAU,UAAA,EAAU,GAAG,OAAO,GAAI;AAC7E;AAWA,SAAwB,WAAW,OAAO;AAEtC,QAAM,EAAC,UAAU,oBAAoB,iBAAiB,MAAM,eAAe,IAAI,MAAA,IAAS,cAAc,OAAO,UAAU;AACvH,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ,KAAK;AAC/C,QAAM,EAAC,MAAM,WAAA,IAAc,qBAAA;AAE3B,QAAM,SAAS,QAAQ,MAAM,IAAI,OAAO;AAAA,IACpC,OAAO,IAAI,OAAA,EAAS,OAAO,SAAA,EAAW,MAAA;AAAA,IACtC,UAAU,IAAI,OAAA,EAAS,SAAA;AAAA,EAAS,CACnC,GAAG,CAAA,CAAE;AAEN,QAAM,SAAS,OAAO,QAAQ,kBAAkB;AAC5C,QAAI;AACA,YAAM,SAAS,EAAC,GAAG,QAAQ,OAAO,OAAO,MAAM,KAAA,GAAO;AAAA,IAC1D,SAAS,OAAO;AACZ,yBAAmB,OAAO,EAAC,GAAG,eAAe,QAAO;AAAA,IACxD;AAAA,EACJ;AAEA,SACI,oBAAC,UAAO,eAAe,EAAC,OAAO,cAAc,UAAU,IAAI,UAAU,MAAA,GAAQ,kBAAkB,QAAQ,UAAU,QAC7G,UAAA,oBAAC,MAAA,EAAK,YAAU,MACZ,UAAA,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,IAAA,oBAAC,kBAAA,EAAiB;AAAA,IAClB,oBAAC,eAAA,EAAc,MAAK,SAAQ,OAAO,EAAE,cAAc,GAAG,MAAK,SAAQ,cAAa,YAAW,WAAS,KAAA,CAAC;AAAA,IACrG,oBAAC,eAAA,EAAc,MAAK,YAAW,OAAO,EAAE,iBAAiB,GAAG,MAAK,YAAW,cAAa,mBAAA,CAAmB;AAAA,IAC5G,qBAAC,kBAAA,EAAiB,WAAU,OAAM,OAAO,EAAC,YAAY,UAAU,gBAAgB,iBAAiB,UAAU,QAAQ,KAAK,KACnH,UAAA;AAAA,MAAA,iBAAiB,oBAAC,mBAAgB,OAAO,EAAE,iBAAiB,GAAG,wBAAM,QAAA,CAAA,CAAK;AAAA,MAC1E,sBACG,oBAAC,MAAA,EAAK,WAAW,YAAY,IAAI,oBAAoB,SAAQ,SAAS,UAAA,EAAE,eAAe,EAAA,CAAE;AAAA,IAAA,GAEjG;AAAA,wBACC,cAAA,EAAa,WAAS,MAAE,UAAA,EAAE,eAAe,EAAA,CAAE;AAAA,EAAA,EAAA,CAChD,GACJ,GACJ;AAER;"}
|
|
@@ -16,13 +16,17 @@ import { applyServerProblem } from "../applyServerProblem.js";
|
|
|
16
16
|
import * as Yup from "yup";
|
|
17
17
|
import { TOTP_CODE_MIN_LENGTH, TOTP_CODE_MAX_LENGTH, TOTP_CODE_PATTERN, RECOVERY_CODE_MIN_LENGTH, RECOVERY_CODE_MAX_LENGTH, RECOVERY_CODE_PATTERN, normalizeTotpCode, normalizeRecoveryCode } from "./securityRules.js";
|
|
18
18
|
import { useToolkitTranslation } from "../../toolkit/useToolkitTranslation.js";
|
|
19
|
+
import { piecePart, usePieceProps } from "../../theme/themePieces.js";
|
|
20
|
+
const THEME_NAME = "CalcitaSecondStepForm";
|
|
21
|
+
const SecondStepActions = piecePart(THEME_NAME, "Actions", Stack);
|
|
19
22
|
const SECOND_STEP_METHOD = Object.freeze({ passkey: "passkey", totp: "totp", recoveryCode: "recoveryCode" });
|
|
20
23
|
const UNAUTHENTICATED_STATUS = 401;
|
|
21
24
|
const ACCOUNT_LOCKED_CODE = "account_locked";
|
|
22
25
|
function isOutOfAttempts(error) {
|
|
23
26
|
return error?.code === ACCOUNT_LOCKED_CODE || error?.status === UNAUTHENTICATED_STATUS && !Number.isInteger(error?.problem?.remaining_attempts);
|
|
24
27
|
}
|
|
25
|
-
function SecondStepForm(
|
|
28
|
+
function SecondStepForm(props) {
|
|
29
|
+
const { methods, canUsePasskeys = false, onUsePasskey, onSubmitCode, onCancel, footer, texts } = usePieceProps(props, THEME_NAME);
|
|
26
30
|
const { t } = useToolkitTranslation("security", texts);
|
|
27
31
|
const hasApp = methods.includes(SECOND_STEP_METHOD.totp);
|
|
28
32
|
const hasRecovery = methods.includes(SECOND_STEP_METHOD.recoveryCode);
|
|
@@ -100,7 +104,7 @@ function SecondStepForm({ methods, canUsePasskeys = false, onUsePasskey, onSubmi
|
|
|
100
104
|
footer
|
|
101
105
|
] }) }, method),
|
|
102
106
|
!hasApp && !hasRecovery && footer,
|
|
103
|
-
/* @__PURE__ */ jsxs(
|
|
107
|
+
/* @__PURE__ */ jsxs(SecondStepActions, { spacing: 0.5, ownSx: { alignItems: "center" }, children: [
|
|
104
108
|
hasApp && hasRecovery && /* @__PURE__ */ jsx(
|
|
105
109
|
Link,
|
|
106
110
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecondStepForm.js","sources":["../../../src/forms/security/SecondStepForm.jsx"],"sourcesContent":["// El segundo paso al entrar: la contraseña fue correcta y la cuenta tiene verificación en dos pasos.\n//\n// Los métodos van en el orden de su fuerza: primero la llave de acceso (no se la roba una página falsa), luego el código de\n// la app, y el de recuperación detrás de un enlace (es para emergencias y se gasta). Cada código equivocado cuenta para el\n// bloqueo de la cuenta: el servidor dice cuántos intentos quedan (problem.remaining_attempts) y se le dice a la persona.\n// Si se acabaron los intentos o se venció el tiempo (un 401 sin intentos, o code account_locked), se regresa a empezar.\n//\n// La pantalla no sabe del servidor: el proyecto le pasa qué hacer.\n//\n// <SecondStepForm methods={['passkey', 'totp', 'recoveryCode']} canUsePasskeys={passkeysSupported()}\n// onUsePasskey={async () => finishWith(await api.post('/auth/second-step/passkey',\n// await signWithPasskey(await api.get('/auth/second-step/passkey-options'))))}\n// onSubmitCode={async ({method, code}) => finishWith(await api.post(`/auth/second-step/${method}`, {code}))}\n// onCancel={(message) => backToSignIn(message)} />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport Button from '@mui/material/Button';\nimport Divider from '@mui/material/Divider';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport FingerprintOutlinedIcon from '@mui/icons-material/FingerprintOutlined';\n\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport FormTextField from '@/forms/FormTextField.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport {applyServerProblem} from '@/forms/applyServerProblem.js';\nimport {Yup} from '@/forms/validation.js';\nimport {\n RECOVERY_CODE_MAX_LENGTH, RECOVERY_CODE_MIN_LENGTH, RECOVERY_CODE_PATTERN, TOTP_CODE_MAX_LENGTH, TOTP_CODE_MIN_LENGTH, TOTP_CODE_PATTERN,\n normalizeRecoveryCode, normalizeTotpCode,\n} from '@/forms/security/securityRules.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/** Las formas de confirmar el segundo paso, como las manda el servidor. */\nexport const SECOND_STEP_METHOD = Object.freeze({passkey: 'passkey', totp: 'totp', recoveryCode: 'recoveryCode'});\n\nconst UNAUTHENTICATED_STATUS = 401;\nconst ACCOUNT_LOCKED_CODE = 'account_locked';\n\n/**\n * ¿Se acabó la oportunidad (bloqueo o se venció el tiempo)? Entonces hay que empezar de nuevo desde la contraseña.\n *\n * @param {Error & {status?: number, code?: string, problem?: object}} error\n * @returns {boolean}\n */\nfunction isOutOfAttempts(error) {\n return error?.code === ACCOUNT_LOCKED_CODE\n || (error?.status === UNAUTHENTICATED_STATUS && !Number.isInteger(error?.problem?.remaining_attempts));\n}\n\n/**\n * @param {object} props\n * @param {string[]} props.methods los que tiene la cuenta (SECOND_STEP_METHOD), como los mandó el servidor\n * @param {boolean} [props.canUsePasskeys] passkeysSupported() de /passkeys\n * @param {() => Promise<unknown>} [props.onUsePasskey] firma con la llave y termina de entrar; truena si no se pudo\n * @param {(input: {method: string, code: string}) => Promise<unknown>} props.onSubmitCode manda el código ya limpio y\n * termina de entrar; truena con el ApiError si no\n * @param {(message: string|null) => void} props.onCancel regresa a escribir la contraseña (con el motivo, si lo hay)\n * @param {import('react').ReactNode} [props.footer] abajo del botón, dentro del formulario: puede llenar el código con\n * useFormikContext (p. ej. el atajo de los ejemplos)\n * @param {object} [props.texts] los textos que cambian, con la forma de `security` en los textos del paquete\n */\nexport default function SecondStepForm({methods, canUsePasskeys = false, onUsePasskey, onSubmitCode, onCancel, footer, texts}) {\n const {t} = useToolkitTranslation('security', texts);\n const hasApp = methods.includes(SECOND_STEP_METHOD.totp);\n const hasRecovery = methods.includes(SECOND_STEP_METHOD.recoveryCode);\n const hasPasskey = methods.includes(SECOND_STEP_METHOD.passkey) && canUsePasskeys && Boolean(onUsePasskey);\n const [method, setMethod] = useState(hasApp ? SECOND_STEP_METHOD.totp : SECOND_STEP_METHOD.recoveryCode);\n const [passkeyError, setPasskeyError] = useState(null);\n const [isUsingPasskey, setIsUsingPasskey] = useState(false);\n const isApp = method === SECOND_STEP_METHOD.totp;\n\n // el esquema se arma al pintar: yup toma sus mensajes al armarlo\n const schema = isApp\n ? Yup.object({code: Yup.string().trim().required(t('twoStep.codeRequired')).min(TOTP_CODE_MIN_LENGTH).max(TOTP_CODE_MAX_LENGTH)\n .matches(TOTP_CODE_PATTERN, t('twoStep.codeFormat'))})\n : Yup.object({code: Yup.string().trim().required(t('secondStep.recoveryCodeRequired')).min(RECOVERY_CODE_MIN_LENGTH)\n .max(RECOVERY_CODE_MAX_LENGTH).matches(RECOVERY_CODE_PATTERN, t('secondStep.recoveryCodeFormat'))});\n\n const usePasskey = async () => {\n setPasskeyError(null);\n setIsUsingPasskey(true);\n try {\n await onUsePasskey();\n } catch (error) {\n // cancelar el aviso del sistema no es un error que enseñar\n if (!error?.cancelled) {\n setPasskeyError(error?.message);\n }\n } finally {\n setIsUsingPasskey(false);\n }\n };\n\n const submit = async (values, formikHelpers) => {\n formikHelpers.setStatus(null);\n try {\n const code = isApp ? normalizeTotpCode(values.code) : normalizeRecoveryCode(values.code);\n await onSubmitCode({method, code});\n } catch (error) {\n formikHelpers.setFieldValue('code', '', false);\n if (isOutOfAttempts(error)) {\n onCancel(error?.message ?? null);\n return;\n }\n applyServerProblem(error, {...formikHelpers, values});\n // cuántos intentos quedan, junto al mensaje del servidor\n const remainingAttempts = error?.problem?.remaining_attempts;\n if (Number.isInteger(remainingAttempts)) {\n formikHelpers.setStatus(`${error.message} ${t('secondStep.remainingAttempts', {count: remainingAttempts})}`);\n }\n }\n };\n\n return (\n <Stack spacing={2.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">{t('secondStep.intro')}</Typography>\n\n {hasPasskey && (\n <>\n <Button variant=\"contained\" size=\"large\" startIcon={<FingerprintOutlinedIcon />} onClick={usePasskey} disabled={isUsingPasskey}>\n {t('secondStep.usePasskey')}\n </Button>\n {passkeyError && <Alert severity=\"error\" role=\"alert\">{passkeyError}</Alert>}\n {(hasApp || hasRecovery) && <Divider>{t('secondStep.or')}</Divider>}\n </>\n )}\n\n {(hasApp || hasRecovery) && (\n // key: al cambiar de método, el formulario empieza limpio\n <Formik key={method} initialValues={{code: ''}} validationSchema={schema} onSubmit={submit}>\n <Stack component={Form} noValidate spacing={2}>\n <FormProblemAlert />\n {isApp ? (\n <FormTextField name=\"code\" label={t('secondStep.appCodeLabel')} autoFocus autoComplete=\"one-time-code\" clearable={false}\n helperText={t('secondStep.appCodeHelp')}\n slotProps={{htmlInput: {inputMode: 'numeric', maxLength: TOTP_CODE_MAX_LENGTH}}} />\n ) : (\n <FormTextField name=\"code\" label={t('secondStep.recoveryCodeLabel')} autoFocus autoComplete=\"off\" clearable={false}\n helperText={t('secondStep.recoveryCodeHelp')} slotProps={{htmlInput: {maxLength: RECOVERY_CODE_MAX_LENGTH}}} />\n )}\n <SubmitButton fullWidth>{t('secondStep.confirm')}</SubmitButton>\n {footer}\n </Stack>\n </Formik>\n )}\n {!hasApp && !hasRecovery && footer}\n\n <Stack spacing={0.5} sx={{alignItems: 'center'}}>\n {hasApp && hasRecovery && (\n <Link component=\"button\" type=\"button\" variant=\"body2\"\n onClick={() => setMethod(isApp ? SECOND_STEP_METHOD.recoveryCode : SECOND_STEP_METHOD.totp)}>\n {isApp ? t('secondStep.useRecovery') : t('secondStep.useApp')}\n </Link>\n )}\n <Link component=\"button\" type=\"button\" variant=\"body2\" onClick={() => onCancel(null)}>{t('secondStep.otherAccount')}</Link>\n </Stack>\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SecondStepForm.js","sources":["../../../src/forms/security/SecondStepForm.jsx"],"sourcesContent":["// El segundo paso al entrar: la contraseña fue correcta y la cuenta tiene verificación en dos pasos.\n//\n// Los métodos van en el orden de su fuerza: primero la llave de acceso (no se la roba una página falsa), luego el código de\n// la app, y el de recuperación detrás de un enlace (es para emergencias y se gasta). Cada código equivocado cuenta para el\n// bloqueo de la cuenta: el servidor dice cuántos intentos quedan (problem.remaining_attempts) y se le dice a la persona.\n// Si se acabaron los intentos o se venció el tiempo (un 401 sin intentos, o code account_locked), se regresa a empezar.\n//\n// La pantalla no sabe del servidor: el proyecto le pasa qué hacer.\n//\n// <SecondStepForm methods={['passkey', 'totp', 'recoveryCode']} canUsePasskeys={passkeysSupported()}\n// onUsePasskey={async () => finishWith(await api.post('/auth/second-step/passkey',\n// await signWithPasskey(await api.get('/auth/second-step/passkey-options'))))}\n// onSubmitCode={async ({method, code}) => finishWith(await api.post(`/auth/second-step/${method}`, {code}))}\n// onCancel={(message) => backToSignIn(message)} />\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaSecondStepForm con defaultProps y\n// styleOverrides de sus partes: actions.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport Button from '@mui/material/Button';\nimport Divider from '@mui/material/Divider';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport FingerprintOutlinedIcon from '@mui/icons-material/FingerprintOutlined';\n\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport FormTextField from '@/forms/FormTextField.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport {applyServerProblem} from '@/forms/applyServerProblem.js';\nimport {Yup} from '@/forms/validation.js';\nimport {\n RECOVERY_CODE_MAX_LENGTH, RECOVERY_CODE_MIN_LENGTH, RECOVERY_CODE_PATTERN, TOTP_CODE_MAX_LENGTH, TOTP_CODE_MIN_LENGTH, TOTP_CODE_PATTERN,\n normalizeRecoveryCode, normalizeTotpCode,\n} from '@/forms/security/securityRules.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaSecondStepForm)\nconst THEME_NAME = 'CalcitaSecondStepForm';\nconst SecondStepActions = piecePart(THEME_NAME, 'Actions', Stack);\n\n/** Las formas de confirmar el segundo paso, como las manda el servidor. */\nexport const SECOND_STEP_METHOD = Object.freeze({passkey: 'passkey', totp: 'totp', recoveryCode: 'recoveryCode'});\n\nconst UNAUTHENTICATED_STATUS = 401;\nconst ACCOUNT_LOCKED_CODE = 'account_locked';\n\n/**\n * ¿Se acabó la oportunidad (bloqueo o se venció el tiempo)? Entonces hay que empezar de nuevo desde la contraseña.\n *\n * @param {Error & {status?: number, code?: string, problem?: object}} error\n * @returns {boolean}\n */\nfunction isOutOfAttempts(error) {\n return error?.code === ACCOUNT_LOCKED_CODE\n || (error?.status === UNAUTHENTICATED_STATUS && !Number.isInteger(error?.problem?.remaining_attempts));\n}\n\n/**\n * @param {object} props\n * @param {string[]} props.methods los que tiene la cuenta (SECOND_STEP_METHOD), como los mandó el servidor\n * @param {boolean} [props.canUsePasskeys] passkeysSupported() de /passkeys\n * @param {() => Promise<unknown>} [props.onUsePasskey] firma con la llave y termina de entrar; truena si no se pudo\n * @param {(input: {method: string, code: string}) => Promise<unknown>} props.onSubmitCode manda el código ya limpio y\n * termina de entrar; truena con el ApiError si no\n * @param {(message: string|null) => void} props.onCancel regresa a escribir la contraseña (con el motivo, si lo hay)\n * @param {import('react').ReactNode} [props.footer] abajo del botón, dentro del formulario: puede llenar el código con\n * useFormikContext (p. ej. el atajo de los ejemplos)\n * @param {object} [props.texts] los textos que cambian, con la forma de `security` en los textos del paquete\n */\nexport default function SecondStepForm(props) {\n // lo que el proyecto puso en su tema (components.CalcitaSecondStepForm.defaultProps) y encima lo que llega aquí\n const {methods, canUsePasskeys = false, onUsePasskey, onSubmitCode, onCancel, footer, texts} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('security', texts);\n const hasApp = methods.includes(SECOND_STEP_METHOD.totp);\n const hasRecovery = methods.includes(SECOND_STEP_METHOD.recoveryCode);\n const hasPasskey = methods.includes(SECOND_STEP_METHOD.passkey) && canUsePasskeys && Boolean(onUsePasskey);\n const [method, setMethod] = useState(hasApp ? SECOND_STEP_METHOD.totp : SECOND_STEP_METHOD.recoveryCode);\n const [passkeyError, setPasskeyError] = useState(null);\n const [isUsingPasskey, setIsUsingPasskey] = useState(false);\n const isApp = method === SECOND_STEP_METHOD.totp;\n\n // el esquema se arma al pintar: yup toma sus mensajes al armarlo\n const schema = isApp\n ? Yup.object({code: Yup.string().trim().required(t('twoStep.codeRequired')).min(TOTP_CODE_MIN_LENGTH).max(TOTP_CODE_MAX_LENGTH)\n .matches(TOTP_CODE_PATTERN, t('twoStep.codeFormat'))})\n : Yup.object({code: Yup.string().trim().required(t('secondStep.recoveryCodeRequired')).min(RECOVERY_CODE_MIN_LENGTH)\n .max(RECOVERY_CODE_MAX_LENGTH).matches(RECOVERY_CODE_PATTERN, t('secondStep.recoveryCodeFormat'))});\n\n const usePasskey = async () => {\n setPasskeyError(null);\n setIsUsingPasskey(true);\n try {\n await onUsePasskey();\n } catch (error) {\n // cancelar el aviso del sistema no es un error que enseñar\n if (!error?.cancelled) {\n setPasskeyError(error?.message);\n }\n } finally {\n setIsUsingPasskey(false);\n }\n };\n\n const submit = async (values, formikHelpers) => {\n formikHelpers.setStatus(null);\n try {\n const code = isApp ? normalizeTotpCode(values.code) : normalizeRecoveryCode(values.code);\n await onSubmitCode({method, code});\n } catch (error) {\n formikHelpers.setFieldValue('code', '', false);\n if (isOutOfAttempts(error)) {\n onCancel(error?.message ?? null);\n return;\n }\n applyServerProblem(error, {...formikHelpers, values});\n // cuántos intentos quedan, junto al mensaje del servidor\n const remainingAttempts = error?.problem?.remaining_attempts;\n if (Number.isInteger(remainingAttempts)) {\n formikHelpers.setStatus(`${error.message} ${t('secondStep.remainingAttempts', {count: remainingAttempts})}`);\n }\n }\n };\n\n return (\n <Stack spacing={2.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">{t('secondStep.intro')}</Typography>\n\n {hasPasskey && (\n <>\n <Button variant=\"contained\" size=\"large\" startIcon={<FingerprintOutlinedIcon />} onClick={usePasskey} disabled={isUsingPasskey}>\n {t('secondStep.usePasskey')}\n </Button>\n {passkeyError && <Alert severity=\"error\" role=\"alert\">{passkeyError}</Alert>}\n {(hasApp || hasRecovery) && <Divider>{t('secondStep.or')}</Divider>}\n </>\n )}\n\n {(hasApp || hasRecovery) && (\n // key: al cambiar de método, el formulario empieza limpio\n <Formik key={method} initialValues={{code: ''}} validationSchema={schema} onSubmit={submit}>\n <Stack component={Form} noValidate spacing={2}>\n <FormProblemAlert />\n {isApp ? (\n <FormTextField name=\"code\" label={t('secondStep.appCodeLabel')} autoFocus autoComplete=\"one-time-code\" clearable={false}\n helperText={t('secondStep.appCodeHelp')}\n slotProps={{htmlInput: {inputMode: 'numeric', maxLength: TOTP_CODE_MAX_LENGTH}}} />\n ) : (\n <FormTextField name=\"code\" label={t('secondStep.recoveryCodeLabel')} autoFocus autoComplete=\"off\" clearable={false}\n helperText={t('secondStep.recoveryCodeHelp')} slotProps={{htmlInput: {maxLength: RECOVERY_CODE_MAX_LENGTH}}} />\n )}\n <SubmitButton fullWidth>{t('secondStep.confirm')}</SubmitButton>\n {footer}\n </Stack>\n </Formik>\n )}\n {!hasApp && !hasRecovery && footer}\n\n <SecondStepActions spacing={0.5} ownSx={{alignItems: 'center'}}>\n {hasApp && hasRecovery && (\n <Link component=\"button\" type=\"button\" variant=\"body2\"\n onClick={() => setMethod(isApp ? SECOND_STEP_METHOD.recoveryCode : SECOND_STEP_METHOD.totp)}>\n {isApp ? t('secondStep.useRecovery') : t('secondStep.useApp')}\n </Link>\n )}\n <Link component=\"button\" type=\"button\" variant=\"body2\" onClick={() => onCancel(null)}>{t('secondStep.otherAccount')}</Link>\n </SecondStepActions>\n </Stack>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AA0CA,MAAM,aAAa;AACnB,MAAM,oBAAoB,UAAU,YAAY,WAAW,KAAK;AAGzD,MAAM,qBAAqB,OAAO,OAAO,EAAC,SAAS,WAAW,MAAM,QAAQ,cAAc,eAAA,CAAe;AAEhH,MAAM,yBAAyB;AAC/B,MAAM,sBAAsB;AAQ5B,SAAS,gBAAgB,OAAO;AAC5B,SAAO,OAAO,SAAS,uBACf,OAAO,WAAW,0BAA0B,CAAC,OAAO,UAAU,OAAO,SAAS,kBAAkB;AAC5G;AAcA,SAAwB,eAAe,OAAO;AAE1C,QAAM,EAAC,SAAS,iBAAiB,OAAO,cAAc,cAAc,UAAU,QAAQ,MAAA,IAAS,cAAc,OAAO,UAAU;AAC9H,QAAM,EAAC,EAAA,IAAK,sBAAsB,YAAY,KAAK;AACnD,QAAM,SAAS,QAAQ,SAAS,mBAAmB,IAAI;AACvD,QAAM,cAAc,QAAQ,SAAS,mBAAmB,YAAY;AACpE,QAAM,aAAa,QAAQ,SAAS,mBAAmB,OAAO,KAAK,kBAAkB,QAAQ,YAAY;AACzG,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,SAAS,mBAAmB,OAAO,mBAAmB,YAAY;AACvG,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,IAAI;AACrD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,KAAK;AAC1D,QAAM,QAAQ,WAAW,mBAAmB;AAG5C,QAAM,SAAS,QACT,IAAI,OAAO,EAAC,MAAM,IAAI,OAAA,EAAS,KAAA,EAAO,SAAS,EAAE,sBAAsB,CAAC,EAAE,IAAI,oBAAoB,EAAE,IAAI,oBAAoB,EACzH,QAAQ,mBAAmB,EAAE,oBAAoB,CAAC,EAAA,CAAE,IACvD,IAAI,OAAO,EAAC,MAAM,IAAI,OAAA,EAAS,KAAA,EAAO,SAAS,EAAE,iCAAiC,CAAC,EAAE,IAAI,wBAAwB,EAC9G,IAAI,wBAAwB,EAAE,QAAQ,uBAAuB,EAAE,+BAA+B,CAAC,EAAA,CAAE;AAE1G,QAAM,aAAa,YAAY;AAC3B,oBAAgB,IAAI;AACpB,sBAAkB,IAAI;AACtB,QAAI;AACA,YAAM,aAAA;AAAA,IACV,SAAS,OAAO;AAEZ,UAAI,CAAC,OAAO,WAAW;AACnB,wBAAgB,OAAO,OAAO;AAAA,MAClC;AAAA,IACJ,UAAA;AACI,wBAAkB,KAAK;AAAA,IAC3B;AAAA,EACJ;AAEA,QAAM,SAAS,OAAO,QAAQ,kBAAkB;AAC5C,kBAAc,UAAU,IAAI;AAC5B,QAAI;AACA,YAAM,OAAO,QAAQ,kBAAkB,OAAO,IAAI,IAAI,sBAAsB,OAAO,IAAI;AACvF,YAAM,aAAa,EAAC,QAAQ,MAAK;AAAA,IACrC,SAAS,OAAO;AACZ,oBAAc,cAAc,QAAQ,IAAI,KAAK;AAC7C,UAAI,gBAAgB,KAAK,GAAG;AACxB,iBAAS,OAAO,WAAW,IAAI;AAC/B;AAAA,MACJ;AACA,yBAAmB,OAAO,EAAC,GAAG,eAAe,QAAO;AAEpD,YAAM,oBAAoB,OAAO,SAAS;AAC1C,UAAI,OAAO,UAAU,iBAAiB,GAAG;AACrC,sBAAc,UAAU,GAAG,MAAM,OAAO,IAAI,EAAE,gCAAgC,EAAC,OAAO,kBAAA,CAAkB,CAAC,EAAE;AAAA,MAC/G;AAAA,IACJ;AAAA,EACJ;AAEA,SACI,qBAAC,OAAA,EAAM,SAAS,KACZ,UAAA;AAAA,IAAA,oBAAC,cAAW,SAAQ,SAAQ,OAAM,kBAAkB,UAAA,EAAE,kBAAkB,GAAE;AAAA,IAEzE,cACG,qBAAA,UAAA,EACI,UAAA;AAAA,MAAA,oBAAC,QAAA,EAAO,SAAQ,aAAY,MAAK,SAAQ,WAAW,oBAAC,yBAAA,CAAA,CAAwB,GAAI,SAAS,YAAY,UAAU,gBAC3G,UAAA,EAAE,uBAAuB,GAC9B;AAAA,MACC,gBAAgB,oBAAC,OAAA,EAAM,UAAS,SAAQ,MAAK,SAAS,UAAA,cAAa;AAAA,OAClE,UAAU,gBAAgB,oBAAC,SAAA,EAAS,UAAA,EAAE,eAAe,EAAA,CAAE;AAAA,IAAA,GAC7D;AAAA,KAGF,UAAU;AAAA,wBAEP,QAAA,EAAoB,eAAe,EAAC,MAAM,MAAK,kBAAkB,QAAQ,UAAU,QAChF,+BAAC,OAAA,EAAM,WAAW,MAAM,YAAU,MAAC,SAAS,GACxC,UAAA;AAAA,MAAA,oBAAC,kBAAA,EAAiB;AAAA,MACjB,QACG;AAAA,QAAC;AAAA,QAAA;AAAA,UAAc,MAAK;AAAA,UAAO,OAAO,EAAE,yBAAyB;AAAA,UAAG,WAAS;AAAA,UAAC,cAAa;AAAA,UAAgB,WAAW;AAAA,UACnG,YAAY,EAAE,wBAAwB;AAAA,UACtC,WAAW,EAAC,WAAW,EAAC,WAAW,WAAW,WAAW,uBAAoB;AAAA,QAAC;AAAA,MAAA,IAE7F;AAAA,QAAC;AAAA,QAAA;AAAA,UAAc,MAAK;AAAA,UAAO,OAAO,EAAE,8BAA8B;AAAA,UAAG,WAAS;AAAA,UAAC,cAAa;AAAA,UAAM,WAAW;AAAA,UAC9F,YAAY,EAAE,6BAA6B;AAAA,UAAG,WAAW,EAAC,WAAW,EAAC,WAAW,2BAAwB;AAAA,QAAC;AAAA,MAAA;AAAA,0BAE5H,cAAA,EAAa,WAAS,MAAE,UAAA,EAAE,oBAAoB,GAAE;AAAA,MAChD;AAAA,IAAA,EAAA,CACL,KAbS,MAcb;AAAA,IAEH,CAAC,UAAU,CAAC,eAAe;AAAA,IAE5B,qBAAC,qBAAkB,SAAS,KAAK,OAAO,EAAC,YAAY,YAChD,UAAA;AAAA,MAAA,UAAU,eACP;AAAA,QAAC;AAAA,QAAA;AAAA,UAAK,WAAU;AAAA,UAAS,MAAK;AAAA,UAAS,SAAQ;AAAA,UACzC,SAAS,MAAM,UAAU,QAAQ,mBAAmB,eAAe,mBAAmB,IAAI;AAAA,UAC3F,UAAA,QAAQ,EAAE,wBAAwB,IAAI,EAAE,mBAAmB;AAAA,QAAA;AAAA,MAAA;AAAA,MAGpE,oBAAC,MAAA,EAAK,WAAU,UAAS,MAAK,UAAS,SAAQ,SAAQ,SAAS,MAAM,SAAS,IAAI,GAAI,UAAA,EAAE,yBAAyB,EAAA,CAAE;AAAA,IAAA,EAAA,CACxH;AAAA,EAAA,GACJ;AAER;"}
|