@stackcraftmx/calcita 0.23.0 → 0.26.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 +127 -22
- package/cli/projectFiles.js +5 -3
- package/cli/templates/project/README.md.tpl +28 -0
- package/cli/templates/project/public/manifest.webmanifest.tpl +6 -6
- package/cli/templates/project/src/app/AppProviders.jsx.tpl +9 -4
- package/cli/templates/project/src/app/appRouter.jsx.tpl +4 -2
- package/cli/templates/project/src/app/appSettings.js.tpl +14 -0
- package/cli/templates/project/src/core/AppLogo.jsx.tpl +3 -1
- package/cli/templates/project/src/core/index.js.tpl +1 -0
- package/cli/templates/project/src/core/publicPaths.js.tpl +24 -0
- package/cli/templates/project/vite.config.js.tpl +27 -1
- package/dist/DataTable/AuditTrailTable.js +5 -3
- package/dist/DataTable/AuditTrailTable.js.map +1 -1
- package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js +2 -1
- package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js.map +1 -1
- package/dist/DataTable/EmptyState.js +2 -2
- package/dist/DataTable/EmptyState.js.map +1 -1
- package/dist/DataTable/FilterChips.js +2 -2
- package/dist/DataTable/FilterChips.js.map +1 -1
- package/dist/DataTable/GoToPage.js +4 -3
- package/dist/DataTable/GoToPage.js.map +1 -1
- package/dist/DataTable/ServerTable.js +153 -83
- package/dist/DataTable/ServerTable.js.map +1 -1
- package/dist/DataTable/rowSelection.js +61 -0
- package/dist/DataTable/rowSelection.js.map +1 -0
- package/dist/access/PermissionMatrix.js +3 -2
- package/dist/access/PermissionMatrix.js.map +1 -1
- package/dist/account/AccountSecuritySection.js +2 -2
- package/dist/account/AccountSecuritySection.js.map +1 -1
- package/dist/account/ForgotPasswordScreen.js +5 -4
- package/dist/account/ForgotPasswordScreen.js.map +1 -1
- package/dist/account/ProfileScreen.js +39 -13
- package/dist/account/ProfileScreen.js.map +1 -1
- package/dist/account/ResetPasswordScreen.js +3 -2
- package/dist/account/ResetPasswordScreen.js.map +1 -1
- package/dist/account/SignInScreen.js +7 -6
- package/dist/account/SignInScreen.js.map +1 -1
- package/dist/account/accountConnector.js.map +1 -1
- package/dist/account/restAccountConnector.js +2 -1
- package/dist/account/restAccountConnector.js.map +1 -1
- package/dist/adapters/reactRouterRoutes.js +7 -6
- package/dist/adapters/reactRouterRoutes.js.map +1 -1
- package/dist/api/createApiClient.js +40 -5
- package/dist/api/createApiClient.js.map +1 -1
- package/dist/appShell/AccountMenu.js +15 -23
- package/dist/appShell/AccountMenu.js.map +1 -1
- package/dist/appShell/AppLayout.js +42 -15
- package/dist/appShell/AppLayout.js.map +1 -1
- package/dist/appShell/AppearanceSettings.js +29 -6
- package/dist/appShell/AppearanceSettings.js.map +1 -1
- package/dist/appShell/AuthLayout.js +2 -0
- package/dist/appShell/AuthLayout.js.map +1 -1
- package/dist/appShell/BarActionButton.js +3 -3
- package/dist/appShell/BarActionButton.js.map +1 -1
- package/dist/appShell/BarActions.js +19 -8
- package/dist/appShell/BarActions.js.map +1 -1
- package/dist/appShell/BarCapsule.js +19 -4
- package/dist/appShell/BarCapsule.js.map +1 -1
- package/dist/appShell/BarSearch.js +92 -0
- package/dist/appShell/BarSearch.js.map +1 -0
- package/dist/appShell/HeaderTools.js +4 -2
- package/dist/appShell/HeaderTools.js.map +1 -1
- package/dist/appShell/MenuLinks.js +36 -0
- package/dist/appShell/MenuLinks.js.map +1 -0
- package/dist/appShell/MobileTabBar.js +6 -4
- package/dist/appShell/MobileTabBar.js.map +1 -1
- package/dist/appShell/SettingsMenu.js +10 -5
- package/dist/appShell/SettingsMenu.js.map +1 -1
- package/dist/appShell/SideMenu.js +5 -3
- package/dist/appShell/SideMenu.js.map +1 -1
- package/dist/appShell/SideMenuEntry.js +3 -3
- package/dist/appShell/SideMenuEntry.js.map +1 -1
- package/dist/appShell/SubmenuActionsMenu.js +3 -4
- package/dist/appShell/SubmenuActionsMenu.js.map +1 -1
- package/dist/appShell/TopMenu.js +5 -6
- package/dist/appShell/TopMenu.js.map +1 -1
- package/dist/appShell/Wordmark.js +11 -0
- package/dist/appShell/Wordmark.js.map +1 -0
- package/dist/appShell/menuItemText.js +4 -0
- package/dist/appShell/menuItemText.js.map +1 -1
- package/dist/appShell/shellSizes.js +2 -0
- package/dist/appShell/shellSizes.js.map +1 -1
- package/dist/auth/PasswordChecklist.js +2 -2
- package/dist/auth/PasswordChecklist.js.map +1 -1
- package/dist/calendar/EventCalendar.js +16 -5
- package/dist/calendar/EventCalendar.js.map +1 -1
- package/dist/calendar/EventDetails.js +74 -0
- package/dist/calendar/EventDetails.js.map +1 -0
- package/dist/calendar/calendarEvents.js +31 -0
- package/dist/calendar/calendarEvents.js.map +1 -0
- package/dist/calendar.js +8 -1
- package/dist/calendar.js.map +1 -1
- package/dist/charts.js +10 -1
- package/dist/charts.js.map +1 -1
- package/dist/datePickers/DateInput.js +3 -2
- package/dist/datePickers/DateInput.js.map +1 -1
- package/dist/downloads/DownloadManager.js +14 -20
- package/dist/downloads/DownloadManager.js.map +1 -1
- package/dist/downloads/downloadQueue.js +4 -2
- package/dist/downloads/downloadQueue.js.map +1 -1
- package/dist/downloads/downloadsMachine.js +4 -1
- package/dist/downloads/downloadsMachine.js.map +1 -1
- package/dist/downloads/fileTypes.js +22 -0
- package/dist/downloads/fileTypes.js.map +1 -0
- package/dist/feedback/ConfirmDialog.js +3 -2
- package/dist/feedback/ConfirmDialog.js.map +1 -1
- package/dist/feedback/ModalWaiting.js +2 -2
- package/dist/feedback/ModalWaiting.js.map +1 -1
- package/dist/feedback/NewBuildNotice.js +2 -2
- package/dist/feedback/NewBuildNotice.js.map +1 -1
- package/dist/filepond/FileDropzone.js +5 -3
- package/dist/filepond/FileDropzone.js.map +1 -1
- package/dist/formatting.js +171 -143
- package/dist/formatting.js.map +1 -1
- package/dist/forms/FormWizard.js +18 -45
- package/dist/forms/FormWizard.js.map +1 -1
- package/dist/forms/auth/ChangePasswordForm.js +5 -4
- package/dist/forms/auth/ChangePasswordForm.js.map +1 -1
- package/dist/forms/auth/ForgotPasswordForm.js +2 -2
- package/dist/forms/auth/ForgotPasswordForm.js.map +1 -1
- package/dist/forms/auth/SignInForm.js +2 -2
- package/dist/forms/auth/SignInForm.js.map +1 -1
- package/dist/forms/security/SecondStepForm.js +2 -2
- package/dist/forms/security/SecondStepForm.js.map +1 -1
- package/dist/forms/security/TwoStepVerificationCard.js +8 -6
- package/dist/forms/security/TwoStepVerificationCard.js.map +1 -1
- package/dist/hookForm/FormWizard.js +59 -0
- package/dist/hookForm/FormWizard.js.map +1 -0
- package/dist/inbox/InboxLayout.js +44 -8
- package/dist/inbox/InboxLayout.js.map +1 -1
- package/dist/inbox/MessageList.js +71 -21
- package/dist/inbox/MessageList.js.map +1 -1
- package/dist/inbox/MessageView.js +80 -29
- package/dist/inbox/MessageView.js.map +1 -1
- package/dist/inbox/inboxDates.js +4 -3
- package/dist/inbox/inboxDates.js.map +1 -1
- package/dist/index.js +163 -136
- package/dist/index.js.map +1 -1
- package/dist/inputs/CatalogInput.js +8 -4
- package/dist/inputs/CatalogInput.js.map +1 -1
- package/dist/inputs/ColorInput.js +3 -2
- package/dist/inputs/ColorInput.js.map +1 -1
- package/dist/inputs/FormSection.js +3 -3
- package/dist/inputs/FormSection.js.map +1 -1
- package/dist/inputs/NumberInput.js +3 -2
- package/dist/inputs/NumberInput.js.map +1 -1
- package/dist/inputs/SelectInput.js +29 -11
- package/dist/inputs/SelectInput.js.map +1 -1
- package/dist/inputs/TextInput.js +6 -2
- package/dist/inputs/TextInput.js.map +1 -1
- package/dist/inputs/WizardLayout.js +54 -0
- package/dist/inputs/WizardLayout.js.map +1 -0
- package/dist/kanban/KanbanBoard.js +239 -14
- package/dist/kanban/KanbanBoard.js.map +1 -1
- package/dist/kanban/kanbanCards.js +19 -0
- package/dist/kanban/kanbanCards.js.map +1 -0
- package/dist/kanban.js +7 -1
- package/dist/kanban.js.map +1 -1
- package/dist/locales/en.js +74 -1
- package/dist/locales/en.js.map +1 -1
- package/dist/locales/es.js +74 -1
- package/dist/locales/es.js.map +1 -1
- package/dist/maps/MapView.js +28 -8
- package/dist/maps/MapView.js.map +1 -1
- package/dist/navigation/appMenu.js +13 -1
- package/dist/navigation/appMenu.js.map +1 -1
- package/dist/observability/logger.js +18 -0
- package/dist/observability/logger.js.map +1 -1
- package/dist/observability/usePublishedBuildWatch.js +4 -3
- package/dist/observability/usePublishedBuildWatch.js.map +1 -1
- package/dist/pwa/InstallAppSetting.js +6 -5
- package/dist/pwa/InstallAppSetting.js.map +1 -1
- package/dist/pwa/InstallInstructionsDialog.js +2 -2
- package/dist/pwa/InstallInstructionsDialog.js.map +1 -1
- package/dist/pwa/InstallPromptBanner.js +15 -9
- package/dist/pwa/InstallPromptBanner.js.map +1 -1
- package/dist/pwa/useInstallPrompt.js +2 -1
- package/dist/pwa/useInstallPrompt.js.map +1 -1
- package/dist/react-hook-form.js +6 -4
- package/dist/react-hook-form.js.map +1 -1
- package/dist/security/ActivityLog.js +4 -3
- package/dist/security/ActivityLog.js.map +1 -1
- package/dist/security/RecoveryCodesDialog.js +4 -3
- package/dist/security/RecoveryCodesDialog.js.map +1 -1
- package/dist/security/SessionsCard.js +8 -6
- package/dist/security/SessionsCard.js.map +1 -1
- package/dist/table.js +2 -0
- package/dist/table.js.map +1 -1
- package/dist/theme/components/MuiAppBar.js +6 -5
- package/dist/theme/components/MuiAppBar.js.map +1 -1
- package/dist/theme/components/MuiDialog.js +13 -0
- package/dist/theme/components/MuiDialog.js.map +1 -0
- package/dist/theme/components/glassComponents.js +32 -31
- package/dist/theme/components/glassComponents.js.map +1 -1
- package/dist/theme/components/minimalComponents.js +8 -4
- package/dist/theme/components/minimalComponents.js.map +1 -1
- package/dist/theme/createToolkitTheme.js.map +1 -1
- package/dist/theme/glassSurface.js +36 -38
- package/dist/theme/glassSurface.js.map +1 -1
- package/dist/theme/lightDarkStyles.js +48 -0
- package/dist/theme/lightDarkStyles.js.map +1 -0
- package/dist/theme/toolkitComponents.js +2 -0
- package/dist/theme/toolkitComponents.js.map +1 -1
- package/dist/toolkit/ComponentTexts.js +13 -0
- package/dist/toolkit/ComponentTexts.js.map +1 -0
- package/dist/toolkit/ToolkitProvider.js +36 -28
- package/dist/toolkit/ToolkitProvider.js.map +1 -1
- package/dist/toolkit/browserNavigation.js +19 -20
- package/dist/toolkit/browserNavigation.js.map +1 -1
- package/dist/toolkit/componentTextsContext.js +15 -0
- package/dist/toolkit/componentTextsContext.js.map +1 -0
- package/dist/toolkit/hashNavigation.js +40 -0
- package/dist/toolkit/hashNavigation.js.map +1 -0
- package/dist/toolkit/navigationPaths.js +49 -0
- package/dist/toolkit/navigationPaths.js.map +1 -0
- package/dist/toolkit/toolkitSettings.js +24 -4
- package/dist/toolkit/toolkitSettings.js.map +1 -1
- package/dist/toolkit/useFormatters.js +12 -0
- package/dist/toolkit/useFormatters.js.map +1 -0
- package/dist/toolkit/useToolkitTranslation.js +11 -7
- package/dist/toolkit/useToolkitTranslation.js.map +1 -1
- package/dist/types/DataTable/AuditTrailTable.d.ts +3 -1
- package/dist/types/DataTable/EmptyState.d.ts +7 -2
- package/dist/types/DataTable/FilterChips.d.ts +3 -1
- package/dist/types/DataTable/GoToPage.d.ts +7 -2
- package/dist/types/DataTable/SelectionBar.d.ts +32 -0
- package/dist/types/DataTable/ServerTable.d.ts +64 -3
- package/dist/types/DataTable/index.d.ts +1 -1
- package/dist/types/DataTable/rowSelection.d.ts +113 -0
- package/dist/types/access/PermissionMatrix.d.ts +3 -1
- package/dist/types/account/AccountSecuritySection.d.ts +3 -1
- package/dist/types/account/ForgotPasswordScreen.d.ts +3 -1
- package/dist/types/account/ProfileScreen.d.ts +31 -1
- package/dist/types/account/ResetPasswordScreen.d.ts +3 -1
- package/dist/types/account/SignInScreen.d.ts +3 -1
- package/dist/types/account/accountConnector.d.ts +7 -2
- package/dist/types/adapters/reactRouterRoutes.d.ts +14 -2
- package/dist/types/api/createApiClient.d.ts +46 -1
- package/dist/types/appShell/AppLayout.d.ts +15 -1
- package/dist/types/appShell/AppearanceSettings.d.ts +8 -1
- package/dist/types/appShell/BarActionButton.d.ts +2 -2
- package/dist/types/appShell/BarActions.d.ts +6 -1
- package/dist/types/appShell/BarCapsule.d.ts +8 -0
- package/dist/types/appShell/BarSearch.d.ts +24 -0
- package/dist/types/appShell/MenuLinks.d.ts +17 -0
- package/dist/types/appShell/MobileTabBar.d.ts +4 -1
- package/dist/types/appShell/SettingsMenu.d.ts +3 -1
- package/dist/types/appShell/SideMenu.d.ts +3 -1
- package/dist/types/appShell/Wordmark.d.ts +6 -0
- package/dist/types/appShell/menuItemText.d.ts +16 -0
- package/dist/types/appShell/shellSizes.d.ts +1 -0
- package/dist/types/auth/PasswordChecklist.d.ts +3 -1
- package/dist/types/calendar/EventCalendar.d.ts +18 -1
- package/dist/types/calendar/EventDetails.d.ts +36 -0
- package/dist/types/calendar/calendarEvents.d.ts +27 -0
- package/dist/types/calendar.d.ts +2 -0
- package/dist/types/charts.d.ts +4 -0
- package/dist/types/datePickers/DateInput.d.ts +3 -1
- package/dist/types/downloads/DownloadManager.d.ts +7 -1
- package/dist/types/downloads/downloadQueue.d.ts +8 -3
- package/dist/types/downloads/fileTypes.d.ts +39 -0
- package/dist/types/downloads/index.d.ts +1 -0
- package/dist/types/feedback/ConfirmDialog.d.ts +3 -1
- package/dist/types/feedback/ModalWaiting.d.ts +7 -2
- package/dist/types/feedback/NewBuildNotice.d.ts +3 -1
- package/dist/types/filepond/FileDropzone.d.ts +3 -1
- package/dist/types/formatting.d.ts +45 -90
- package/dist/types/forms/FormWizard.d.ts +3 -1
- package/dist/types/forms/auth/ChangePasswordForm.d.ts +3 -1
- package/dist/types/forms/auth/ForgotPasswordForm.d.ts +3 -1
- package/dist/types/forms/auth/SignInForm.d.ts +3 -1
- package/dist/types/forms/security/SecondStepForm.d.ts +3 -1
- package/dist/types/forms/security/TwoStepVerificationCard.d.ts +3 -1
- package/dist/types/hookForm/FormWizard.d.ts +22 -0
- package/dist/types/inbox/InboxLayout.d.ts +14 -2
- package/dist/types/inbox/MessageList.d.ts +30 -5
- package/dist/types/inbox/MessageView.d.ts +35 -3
- package/dist/types/inbox/inboxDates.d.ts +6 -1
- package/dist/types/index.d.ts +15 -8
- package/dist/types/inputs/CatalogInput.d.ts +9 -5
- package/dist/types/inputs/ColorInput.d.ts +3 -1
- package/dist/types/inputs/FormSection.d.ts +2 -2
- package/dist/types/inputs/NumberInput.d.ts +3 -1
- package/dist/types/inputs/SelectInput.d.ts +9 -5
- package/dist/types/inputs/TextInput.d.ts +3 -1
- package/dist/types/inputs/WizardLayout.d.ts +34 -0
- package/dist/types/kanban/KanbanBoard.d.ts +61 -6
- package/dist/types/kanban/kanbanCards.d.ts +22 -0
- package/dist/types/kanban.d.ts +1 -0
- package/dist/types/locales/en.d.ts +77 -0
- package/dist/types/locales/es.d.ts +77 -0
- package/dist/types/maps/MapView.d.ts +12 -3
- package/dist/types/navigation/appMenu.d.ts +8 -0
- package/dist/types/observability/logger.d.ts +13 -0
- package/dist/types/observability/usePublishedBuildWatch.d.ts +3 -1
- package/dist/types/pwa/InstallAppSetting.d.ts +7 -1
- package/dist/types/pwa/InstallInstructionsDialog.d.ts +3 -1
- package/dist/types/pwa/InstallPromptBanner.d.ts +10 -1
- package/dist/types/pwa/useInstallPrompt.d.ts +3 -2
- package/dist/types/react-hook-form.d.ts +1 -0
- package/dist/types/security/ActivityLog.d.ts +3 -1
- package/dist/types/security/RecoveryCodesDialog.d.ts +6 -1
- package/dist/types/security/SessionsCard.d.ts +3 -1
- package/dist/types/table.d.ts +3 -1
- package/dist/types/theme/components/MuiAppBar.d.ts +3 -1
- package/dist/types/theme/components/MuiDialog.d.ts +9 -0
- package/dist/types/theme/components/glassComponents.d.ts +5 -0
- package/dist/types/theme/lightDarkStyles.d.ts +8 -0
- package/dist/types/toolkit/ComponentTexts.d.ts +11 -0
- package/dist/types/toolkit/ToolkitProvider.d.ts +18 -2
- package/dist/types/toolkit/browserNavigation.d.ts +23 -28
- package/dist/types/toolkit/componentTextsContext.d.ts +20 -0
- package/dist/types/toolkit/hashNavigation.d.ts +12 -0
- package/dist/types/toolkit/navigationPaths.d.ts +43 -0
- package/dist/types/toolkit/toolkitSettings.d.ts +49 -4
- package/dist/types/toolkit/useFormatters.d.ts +6 -0
- package/dist/types/toolkit/useToolkitTranslation.d.ts +2 -1
- package/dist/types/ui/actions/ActionsMenu.d.ts +20 -0
- package/dist/types/ui/actions/CopyButton.d.ts +3 -1
- package/dist/types/ui/actions/DownloadMenu.d.ts +8 -3
- package/dist/types/ui/actions/SecretField.d.ts +5 -1
- package/dist/types/ui/actions/SelectionActions.d.ts +39 -0
- package/dist/types/ui/filters/DateRangeFilter.d.ts +3 -1
- package/dist/types/ui/filters/QuickFilters.d.ts +3 -1
- package/dist/types/ui/index.d.ts +1 -1
- package/dist/types/ui/labels/tagOf.d.ts +13 -0
- package/dist/types/ui/layout/CollapsibleSection.d.ts +4 -1
- package/dist/types/ui/layout/ContentDialog.d.ts +3 -1
- package/dist/types/ui/layout/ErrorPage.d.ts +14 -1
- package/dist/types/ui/layout/FullPageLoader.d.ts +7 -1
- package/dist/types/ui/layout/PageHeading.d.ts +3 -1
- package/dist/types/ui/metrics/BarList.d.ts +7 -1
- package/dist/types/ui/metrics/CalendarHeatmap.d.ts +7 -1
- package/dist/types/ui/metrics/ChartTooltip.d.ts +9 -3
- package/dist/types/ui/metrics/ColumnChart.d.ts +23 -11
- package/dist/types/ui/metrics/LineChart.d.ts +41 -0
- package/dist/types/ui/metrics/MetricCard.d.ts +9 -1
- package/dist/types/ui/metrics/TableToggle.d.ts +9 -2
- package/dist/types/ui/metrics/chartParts.d.ts +112 -0
- package/dist/types/ui/navigation/BackButton.d.ts +3 -1
- package/dist/types/ui/navigation/ColorSchemeToggle.d.ts +8 -2
- package/dist/types/ui/navigation/FontSizeToggle.d.ts +7 -2
- package/dist/types/ui/navigation/LanguageToggle.d.ts +8 -3
- package/dist/types/ui/navigation/PageBreadcrumbs.d.ts +5 -2
- package/dist/types/ui/timeline/GroupedTimeline.d.ts +4 -1
- package/dist/types/upload/FileDropzone.d.ts +3 -1
- package/dist/ui/actions/ActionsMenu.js +51 -0
- package/dist/ui/actions/ActionsMenu.js.map +1 -0
- package/dist/ui/actions/CopyButton.js +2 -2
- package/dist/ui/actions/CopyButton.js.map +1 -1
- package/dist/ui/actions/DownloadMenu.js +11 -12
- package/dist/ui/actions/DownloadMenu.js.map +1 -1
- package/dist/ui/actions/SecretField.js +8 -4
- package/dist/ui/actions/SecretField.js.map +1 -1
- package/dist/ui/actions/SelectionActions.js +142 -0
- package/dist/ui/actions/SelectionActions.js.map +1 -0
- package/dist/ui/filters/DateRangeFilter.js +4 -3
- package/dist/ui/filters/DateRangeFilter.js.map +1 -1
- package/dist/ui/filters/QuickFilters.js +2 -2
- package/dist/ui/filters/QuickFilters.js.map +1 -1
- package/dist/ui/labels/tagOf.js +7 -0
- package/dist/ui/labels/tagOf.js.map +1 -0
- package/dist/ui/layout/CollapsibleSection.js +6 -2
- package/dist/ui/layout/CollapsibleSection.js.map +1 -1
- package/dist/ui/layout/ContentDialog.js +2 -2
- package/dist/ui/layout/ContentDialog.js.map +1 -1
- package/dist/ui/layout/ErrorPage.js +21 -7
- package/dist/ui/layout/ErrorPage.js.map +1 -1
- package/dist/ui/layout/FullPageLoader.js +2 -2
- package/dist/ui/layout/FullPageLoader.js.map +1 -1
- package/dist/ui/layout/PageHeading.js +2 -2
- package/dist/ui/layout/PageHeading.js.map +1 -1
- package/dist/ui/metrics/BarList.js +18 -13
- package/dist/ui/metrics/BarList.js.map +1 -1
- package/dist/ui/metrics/CalendarHeatmap.js +11 -6
- package/dist/ui/metrics/CalendarHeatmap.js.map +1 -1
- package/dist/ui/metrics/ChartTooltip.js +1 -1
- package/dist/ui/metrics/ChartTooltip.js.map +1 -1
- package/dist/ui/metrics/ColumnChart.js +59 -63
- package/dist/ui/metrics/ColumnChart.js.map +1 -1
- package/dist/ui/metrics/LineChart.js +117 -0
- package/dist/ui/metrics/LineChart.js.map +1 -0
- package/dist/ui/metrics/MetricCard.js +41 -8
- package/dist/ui/metrics/MetricCard.js.map +1 -1
- package/dist/ui/metrics/TableToggle.js +4 -2
- package/dist/ui/metrics/TableToggle.js.map +1 -1
- package/dist/ui/metrics/chartParts.js +93 -0
- package/dist/ui/metrics/chartParts.js.map +1 -0
- package/dist/ui/navigation/BackButton.js +2 -2
- package/dist/ui/navigation/BackButton.js.map +1 -1
- package/dist/ui/navigation/ColorSchemeToggle.js +6 -3
- package/dist/ui/navigation/ColorSchemeToggle.js.map +1 -1
- package/dist/ui/navigation/FontSizeToggle.js +3 -2
- package/dist/ui/navigation/FontSizeToggle.js.map +1 -1
- package/dist/ui/navigation/LanguageToggle.js +40 -19
- package/dist/ui/navigation/LanguageToggle.js.map +1 -1
- package/dist/ui/navigation/PageBreadcrumbs.js +2 -2
- package/dist/ui/navigation/PageBreadcrumbs.js.map +1 -1
- package/dist/ui/timeline/GroupedTimeline.js +10 -8
- package/dist/ui/timeline/GroupedTimeline.js.map +1 -1
- package/dist/upload/FileDropzone.js +5 -3
- package/dist/upload/FileDropzone.js.map +1 -1
- package/package.json +16 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EventCalendar.js","sources":["../../src/calendar/EventCalendar.jsx"],"sourcesContent":["// Un calendario de eventos (@stackcraftmx/calcita/calendar): FullCalendar por dentro, con la barra del framework (anterior y\n// siguiente, «Hoy», el título y la vista con el control segmentado) y los colores del tema. En el idioma de\n// ToolkitProvider, la semana empieza en lunes y marca la hora actual. Las citas van en bloques de su color (el color es\n// información: el tipo de cita), no en puntitos.\n//\n// <EventCalendar events={[{id: '1', title: 'Entrega P-2375', start: '2026-10-20T10:00', end: '2026-10-20T11:00', tone: 'info'}]}\n// onEventClick={openDetail} onDateSelect={openNewEvent} onEventChange={(change) => api.patch(...)} />\n//\n// Arrastrar un evento lo cambia de día u hora: onEventChange recibe lo nuevo y, si truena (el servidor dijo que no), el\n// evento regresa a donde estaba. Sin onEventChange no se puede arrastrar; sin onDateSelect no se pueden elegir días.\n//\n// OJO: las horas se pintan en la zona del navegador (FullCalendar pide un complemento aparte para zonas con nombre).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useRef, useState} from 'react';\nimport FullCalendar from '@fullcalendar/react';\nimport dayGridPlugin from '@fullcalendar/daygrid';\nimport interactionPlugin from '@fullcalendar/interaction';\nimport listPlugin from '@fullcalendar/list';\nimport timeGridPlugin from '@fullcalendar/timegrid';\nimport spanishLocale from '@fullcalendar/core/locales/es';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport IconButton from '@mui/material/IconButton';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport {useTheme} from '@mui/material/styles';\nimport ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded';\nimport ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';\n\nimport {paletteOf} from '@/theme/components/paletteOf.js';\nimport {useToolkitSettings} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport SegmentedControl from '@/ui/filters/SegmentedControl.jsx';\n\n/** Las vistas del calendario (los nombres de FullCalendar). */\nexport const CALENDAR_VIEW = Object.freeze({month: 'dayGridMonth', week: 'timeGridWeek', day: 'timeGridDay', list: 'listWeek'});\n\n// el texto de cada vista en los textos del paquete (calendar.views.*)\nconst VIEW_TEXT_KEY = Object.freeze({\n [CALENDAR_VIEW.month]: 'month', [CALENDAR_VIEW.week]: 'week', [CALENDAR_VIEW.day]: 'day', [CALENDAR_VIEW.list]: 'list',\n});\n// cuántos eventos caben en un día del mes antes del «+2 más»\nconst DAY_MAX_EVENTS = 3;\nconst MONDAY = 1;\n\n/**\n * La primera letra en mayúscula y lo demás como viene: «octubre de 2026» → «Octubre de 2026». OJO: con CSS\n * (text-transform: capitalize) salía «Octubre De 2026»: en español solo va la primera.\n *\n * @param {string} text\n * @returns {string}\n */\nfunction capitalizeFirst(text) {\n return text ? text.charAt(0).toLocaleUpperCase() + text.slice(1) : text;\n}\n\n/**\n * Los colores del calendario sobre el tema (las variables CSS de FullCalendar), en claro y en oscuro.\n *\n * @param {object} theme\n * @returns {object}\n */\nfunction calendarStylesOf(theme) {\n const palette = paletteOf(theme);\n return {\n '--fc-border-color': palette.divider,\n '--fc-page-bg-color': 'transparent',\n '--fc-neutral-bg-color': palette.action.hover,\n '--fc-today-bg-color': `color-mix(in srgb, ${palette.primary.main} 8%, transparent)`,\n '--fc-list-event-hover-bg-color': palette.action.hover,\n '--fc-now-indicator-color': palette.error.main,\n '--fc-highlight-color': `color-mix(in srgb, ${palette.primary.main} 14%, transparent)`,\n '& .fc': {fontFamily: 'inherit', fontSize: theme.typography.body2.fontSize},\n // los números de los días y los encabezados son ligas en FullCalendar: sin subrayado y del color del texto\n '& .fc a': {color: 'inherit', textDecoration: 'none'},\n '& .fc-col-header-cell-cushion': {fontWeight: 600, py: 1},\n '& .fc-event': {borderRadius: '6px', px: 0.5},\n // la hora no se encoge: en un día angosto se recortaba («1C» en lugar de «10»); lo que se corta es el nombre\n '& .fc-event-time': {flexShrink: 0, overflow: 'visible'},\n '& .fc-scrollgrid': {borderRadius: '12px', overflow: 'hidden'},\n '& .fc-list': {borderRadius: '12px'},\n };\n}\n\n/**\n * @param {object} props\n * @param {Array<{id: string, title: string, start: string, end?: string, allDay?: boolean, tone?: string}>} props.events\n * start y end como texto ISO; tone: un color del tema (por omisión primary)\n * @param {string} [props.initialView] una de CALENDAR_VIEW\n * @param {string[]} [props.views] las que se ofrecen en el control segmentado\n * @param {(eventId: string) => void} [props.onEventClick]\n * @param {(range: {start: string, end: string, allDay: boolean}) => void} [props.onDateSelect] al elegir días u horas\n * @param {(change: {id: string, start: string, end: string|null, allDay: boolean}) => Promise<unknown>|void} [props.onEventChange]\n * al arrastrar un evento; si truena, el evento regresa\n * @param {number|'auto'} [props.height]\n * @param {string} props.ariaLabel ya traducido\n */\nexport default function EventCalendar({\n events, initialView = CALENDAR_VIEW.month, views = Object.values(CALENDAR_VIEW), onEventClick, onDateSelect, onEventChange, height = 'auto', ariaLabel,\n}) {\n const theme = useTheme();\n const palette = paletteOf(theme);\n const {t} = useToolkitTranslation('calendar');\n const {language} = useToolkitSettings();\n const calendarRef = useRef(null);\n const [title, setTitle] = useState('');\n const [view, setView] = useState(initialView);\n const calendarApi = () => calendarRef.current?.getApi();\n\n const coloredEvents = events.map((event) => {\n const color = palette[event.tone] ?? palette.primary;\n return {...event, backgroundColor: color.main, borderColor: color.main, textColor: color.contrastText};\n });\n\n const changeEvent = async ({event, revert}) => {\n try {\n await onEventChange({id: event.id, start: event.startStr, end: event.endStr || null, allDay: event.allDay});\n } catch {\n // el servidor dijo que no: el evento regresa a donde estaba (quien llama ya avisó el error)\n revert();\n }\n };\n\n return (\n <Stack spacing={2} role=\"region\" aria-label={ariaLabel}>\n <Stack direction={{xs: 'column', md: 'row'}} spacing={1.5} sx={{alignItems: {md: 'center'}, justifyContent: 'space-between'}}>\n <Stack direction=\"row\" spacing={1} sx={{alignItems: 'center'}}>\n <Tooltip title={t('previous')}>\n <IconButton aria-label={t('previous')} onClick={() => calendarApi()?.prev()}><ChevronLeftRoundedIcon /></IconButton>\n </Tooltip>\n <Tooltip title={t('next')}>\n <IconButton aria-label={t('next')} onClick={() => calendarApi()?.next()}><ChevronRightRoundedIcon /></IconButton>\n </Tooltip>\n <Button variant=\"outlined\" size=\"small\" onClick={() => calendarApi()?.today()}>{t('today')}</Button>\n <Typography variant=\"h6\" component=\"h2\" sx={{pl: 1}} aria-live=\"polite\">{capitalizeFirst(title)}</Typography>\n </Stack>\n {views.length > 1 && (\n <SegmentedControl ariaLabel={t('view')} value={view} onChange={(nextView) => calendarApi()?.changeView(nextView)}\n options={views.map((value) => ({value, label: t(`views.${VIEW_TEXT_KEY[value]}`)}))} />\n )}\n </Stack>\n <Box sx={calendarStylesOf}>\n <FullCalendar ref={calendarRef} plugins={[dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin]} initialView={initialView}\n headerToolbar={false} locale={language === 'es' ? spanishLocale : 'en'} firstDay={MONDAY} height={height}\n nowIndicator dayMaxEvents={DAY_MAX_EVENTS} events={coloredEvents} eventDisplay=\"block\"\n datesSet={(dateInfo) => {\n setTitle(dateInfo.view.title);\n setView(dateInfo.view.type);\n }}\n eventClick={onEventClick ? (clickInfo) => onEventClick(clickInfo.event.id) : undefined}\n selectable={Boolean(onDateSelect)} selectMirror\n select={onDateSelect ? (selectInfo) => {\n onDateSelect({start: selectInfo.startStr, end: selectInfo.endStr, allDay: selectInfo.allDay});\n calendarApi()?.unselect();\n } : undefined}\n editable={Boolean(onEventChange)} eventChange={onEventChange ? changeEvent : undefined} />\n </Box>\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EventCalendar.js","sources":["../../src/calendar/EventCalendar.jsx"],"sourcesContent":["// Un calendario de eventos (@stackcraftmx/calcita/calendar): FullCalendar por dentro, con la barra del framework (anterior y\n// siguiente, «Hoy», el título y la vista con el control segmentado) y los colores del tema. En el idioma de\n// ToolkitProvider, la semana empieza en lunes y marca la hora actual. Las citas van en bloques de su color (el color es\n// información: el tipo de cita), no en puntitos.\n//\n// <EventCalendar events={[{id: '1', title: 'Entrega P-2375', start: '2026-10-20T10:00', end: '2026-10-20T11:00', tone: 'info'}]}\n// onEventClick={openDetail} onDateSelect={openNewEvent} onEventChange={(change) => api.patch(...)} />\n//\n// Arrastrar un evento lo cambia de día u hora: onEventChange recibe lo nuevo y, si truena (el servidor dijo que no), el\n// evento regresa a donde estaba. Sin onEventChange no se puede arrastrar; sin onDateSelect no se pueden elegir días.\n//\n// Se extiende sin tocar el framework: onDatesChange avisa el rango que se ve (para que el backend mande solo esas citas),\n// onViewChange la vista, renderEvent pinta cada cita a su manera (recibe la cita tal cual, con sus campos propios),\n// fullCalendarProps pasa lo demás de FullCalendar (horario laboral, días ocultos, horas visibles…) y texts los textos. El\n// detalle de una cita para su ventana: EventDetails.\n//\n// OJO: las horas se pintan en la zona del navegador (FullCalendar pide un complemento aparte para zonas con nombre).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useRef, useState} from 'react';\nimport FullCalendar from '@fullcalendar/react';\nimport dayGridPlugin from '@fullcalendar/daygrid';\nimport interactionPlugin from '@fullcalendar/interaction';\nimport listPlugin from '@fullcalendar/list';\nimport timeGridPlugin from '@fullcalendar/timegrid';\nimport spanishLocale from '@fullcalendar/core/locales/es';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport IconButton from '@mui/material/IconButton';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport {useTheme} from '@mui/material/styles';\nimport ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded';\nimport ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';\n\nimport {paletteOf} from '@/theme/components/paletteOf.js';\nimport {useToolkitSettings} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport SegmentedControl from '@/ui/filters/SegmentedControl.jsx';\n\n/** Las vistas del calendario (los nombres de FullCalendar). */\nexport const CALENDAR_VIEW = Object.freeze({month: 'dayGridMonth', week: 'timeGridWeek', day: 'timeGridDay', list: 'listWeek'});\n\n// el texto de cada vista en los textos del paquete (calendar.views.*)\nconst VIEW_TEXT_KEY = Object.freeze({\n [CALENDAR_VIEW.month]: 'month', [CALENDAR_VIEW.week]: 'week', [CALENDAR_VIEW.day]: 'day', [CALENDAR_VIEW.list]: 'list',\n});\n// cuántos eventos caben en un día del mes antes del «+2 más»\nconst DAY_MAX_EVENTS = 3;\nconst MONDAY = 1;\n\n/**\n * La primera letra en mayúscula y lo demás como viene: «octubre de 2026» → «Octubre de 2026». OJO: con CSS\n * (text-transform: capitalize) salía «Octubre De 2026»: en español solo va la primera.\n *\n * @param {string} text\n * @returns {string}\n */\nfunction capitalizeFirst(text) {\n return text ? text.charAt(0).toLocaleUpperCase() + text.slice(1) : text;\n}\n\n/**\n * Los colores del calendario sobre el tema (las variables CSS de FullCalendar), en claro y en oscuro.\n *\n * @param {object} theme\n * @returns {object}\n */\nfunction calendarStylesOf(theme) {\n const palette = paletteOf(theme);\n return {\n '--fc-border-color': palette.divider,\n '--fc-page-bg-color': 'transparent',\n '--fc-neutral-bg-color': palette.action.hover,\n '--fc-today-bg-color': `color-mix(in srgb, ${palette.primary.main} 8%, transparent)`,\n '--fc-list-event-hover-bg-color': palette.action.hover,\n '--fc-now-indicator-color': palette.error.main,\n '--fc-highlight-color': `color-mix(in srgb, ${palette.primary.main} 14%, transparent)`,\n '& .fc': {fontFamily: 'inherit', fontSize: theme.typography.body2.fontSize},\n // los números de los días y los encabezados son ligas en FullCalendar: sin subrayado y del color del texto\n '& .fc a': {color: 'inherit', textDecoration: 'none'},\n '& .fc-col-header-cell-cushion': {fontWeight: 600, py: 1},\n '& .fc-event': {borderRadius: '6px', px: 0.5},\n // la hora no se encoge: en un día angosto se recortaba («1C» en lugar de «10»); lo que se corta es el nombre\n '& .fc-event-time': {flexShrink: 0, overflow: 'visible'},\n '& .fc-scrollgrid': {borderRadius: '12px', overflow: 'hidden'},\n '& .fc-list': {borderRadius: '12px'},\n };\n}\n\n/**\n * @param {object} props\n * @param {Array<{id: string, title: string, start: string, end?: string, allDay?: boolean, tone?: string}>} props.events\n * start y end como texto ISO; tone: un color del tema (por omisión primary)\n * @param {string} [props.initialView] una de CALENDAR_VIEW\n * @param {string[]} [props.views] las que se ofrecen en el control segmentado\n * @param {(eventId: string) => void} [props.onEventClick]\n * @param {(range: {start: string, end: string, allDay: boolean}) => void} [props.onDateSelect] al elegir días u horas\n * @param {(change: {id: string, start: string, end: string|null, allDay: boolean}) => Promise<unknown>|void} [props.onEventChange]\n * al arrastrar un evento; si truena, el evento regresa\n * @param {number|'auto'} [props.height]\n * @param {string} props.ariaLabel ya traducido\n * @param {(range: {start: string, end: string, view: string}) => void} [props.onDatesChange] cada que cambia lo que se ve (al\n * abrir, al ir al anterior o siguiente, al cambiar de vista): para pedir al backend solo las citas de ese rango\n * @param {(view: string) => void} [props.onViewChange] al cambiar de vista (CALENDAR_VIEW)\n * @param {(event: any) => import('react').ReactNode} [props.renderEvent] lo de adentro de cada cita (recibe la cita de events\n * tal cual, con sus campos propios)\n * @param {object} [props.fullCalendarProps] lo demás de FullCalendar (businessHours, hiddenDays, slotMinTime, locale…); lo\n * que el calendario controla (las citas, la barra, los eventos de arriba) no se reemplaza desde aquí\n * @param {object} [props.texts] los textos que cambian, con la forma de `calendar` en los textos del paquete\n */\nexport default function EventCalendar({\n events, initialView = CALENDAR_VIEW.month, views = Object.values(CALENDAR_VIEW), onEventClick, onDateSelect, onEventChange, height = 'auto', ariaLabel,\n onDatesChange, onViewChange, renderEvent, fullCalendarProps, texts,\n}) {\n const theme = useTheme();\n const palette = paletteOf(theme);\n const {t} = useToolkitTranslation('calendar', texts);\n const {language} = useToolkitSettings();\n const calendarRef = useRef(null);\n const [title, setTitle] = useState('');\n const [view, setView] = useState(initialView);\n const calendarApi = () => calendarRef.current?.getApi();\n\n const coloredEvents = events.map((event) => {\n const color = palette[event.tone] ?? palette.primary;\n return {...event, backgroundColor: color.main, borderColor: color.main, textColor: color.contrastText};\n });\n\n const changeEvent = async ({event, revert}) => {\n try {\n await onEventChange({id: event.id, start: event.startStr, end: event.endStr || null, allDay: event.allDay});\n } catch {\n // el servidor dijo que no: el evento regresa a donde estaba (quien llama ya avisó el error)\n revert();\n }\n };\n\n return (\n <Stack spacing={2} role=\"region\" aria-label={ariaLabel}>\n <Stack direction={{xs: 'column', md: 'row'}} spacing={1.5} sx={{alignItems: {md: 'center'}, justifyContent: 'space-between'}}>\n <Stack direction=\"row\" spacing={1} sx={{alignItems: 'center'}}>\n <Tooltip title={t('previous')}>\n <IconButton aria-label={t('previous')} onClick={() => calendarApi()?.prev()}><ChevronLeftRoundedIcon /></IconButton>\n </Tooltip>\n <Tooltip title={t('next')}>\n <IconButton aria-label={t('next')} onClick={() => calendarApi()?.next()}><ChevronRightRoundedIcon /></IconButton>\n </Tooltip>\n <Button variant=\"outlined\" size=\"small\" onClick={() => calendarApi()?.today()}>{t('today')}</Button>\n <Typography variant=\"h6\" component=\"h2\" sx={{pl: 1}} aria-live=\"polite\">{capitalizeFirst(title)}</Typography>\n </Stack>\n {views.length > 1 && (\n <SegmentedControl ariaLabel={t('view')} value={view} onChange={(nextView) => calendarApi()?.changeView(nextView)}\n options={views.map((value) => ({value, label: t(`views.${VIEW_TEXT_KEY[value]}`)}))} />\n )}\n </Stack>\n <Box sx={calendarStylesOf}>\n {/* primero lo del proyecto (fullCalendarProps); lo de abajo es lo que el calendario controla y le gana */}\n <FullCalendar locale={language === 'es' ? spanishLocale : 'en'} firstDay={MONDAY} {...fullCalendarProps}\n ref={calendarRef} plugins={[dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin, ...(fullCalendarProps?.plugins ?? [])]}\n initialView={initialView} headerToolbar={false} height={height}\n nowIndicator dayMaxEvents={DAY_MAX_EVENTS} events={coloredEvents} eventDisplay=\"block\"\n eventContent={renderEvent ? (contentInfo) => renderEvent(events.find((event) => event.id === contentInfo.event.id) ?? contentInfo.event) : undefined}\n datesSet={(dateInfo) => {\n setTitle(dateInfo.view.title);\n if (dateInfo.view.type !== view) {\n onViewChange?.(dateInfo.view.type);\n }\n setView(dateInfo.view.type);\n onDatesChange?.({start: dateInfo.startStr, end: dateInfo.endStr, view: dateInfo.view.type});\n }}\n eventClick={onEventClick ? (clickInfo) => onEventClick(clickInfo.event.id) : undefined}\n selectable={Boolean(onDateSelect)} selectMirror\n select={onDateSelect ? (selectInfo) => {\n onDateSelect({start: selectInfo.startStr, end: selectInfo.endStr, allDay: selectInfo.allDay});\n calendarApi()?.unselect();\n } : undefined}\n editable={Boolean(onEventChange)} eventChange={onEventChange ? changeEvent : undefined} />\n </Box>\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
import Avatar from "@mui/material/Avatar";
|
|
4
|
+
import Box from "@mui/material/Box";
|
|
5
|
+
import Button from "@mui/material/Button";
|
|
6
|
+
import Chip from "@mui/material/Chip";
|
|
7
|
+
import Stack from "@mui/material/Stack";
|
|
8
|
+
import Typography from "@mui/material/Typography";
|
|
9
|
+
import AccessTimeRoundedIcon from "@mui/icons-material/AccessTimeRounded";
|
|
10
|
+
import NotesRoundedIcon from "@mui/icons-material/NotesRounded";
|
|
11
|
+
import NotificationsNoneRoundedIcon from "@mui/icons-material/NotificationsNoneRounded";
|
|
12
|
+
import PeopleOutlineRoundedIcon from "@mui/icons-material/PeopleOutlineRounded";
|
|
13
|
+
import PlaceOutlinedIcon from "@mui/icons-material/PlaceOutlined";
|
|
14
|
+
import VideocamOutlinedIcon from "@mui/icons-material/VideocamOutlined";
|
|
15
|
+
import { initialsOf } from "../appShell/menuItemText.js";
|
|
16
|
+
import { reminderOf, isOpenableMeetingUrl, REMINDER_UNIT } from "./calendarEvents.js";
|
|
17
|
+
import { useFormatters } from "../toolkit/useFormatters.js";
|
|
18
|
+
import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
|
|
19
|
+
const ATTENDEE_RESPONSE = Object.freeze({ accepted: "accepted", declined: "declined", pending: "pending" });
|
|
20
|
+
const RESPONSE_COLOR = Object.freeze({
|
|
21
|
+
[ATTENDEE_RESPONSE.accepted]: "success",
|
|
22
|
+
[ATTENDEE_RESPONSE.declined]: "error",
|
|
23
|
+
[ATTENDEE_RESPONSE.pending]: "default"
|
|
24
|
+
});
|
|
25
|
+
const WHOLE_DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
26
|
+
const ICON_SIZE = 20;
|
|
27
|
+
function DetailRow({ Icon, label, children }) {
|
|
28
|
+
return /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1.5, sx: { alignItems: "flex-start" }, "aria-label": label, role: "group", children: [
|
|
29
|
+
/* @__PURE__ */ jsx(Icon, { sx: { fontSize: ICON_SIZE, color: "text.secondary", mt: 0.25 }, "aria-hidden": "true" }),
|
|
30
|
+
/* @__PURE__ */ jsx(Box, { sx: { minWidth: 0, flexGrow: 1 }, children })
|
|
31
|
+
] });
|
|
32
|
+
}
|
|
33
|
+
function EventDetails({ event, actions, titleId, texts }) {
|
|
34
|
+
const { t } = useToolkitTranslation("calendar", texts);
|
|
35
|
+
const { formatDate, formatTime } = useFormatters();
|
|
36
|
+
const isWholeDay = event.allDay || WHOLE_DAY_PATTERN.test(event.start);
|
|
37
|
+
let when = formatDate(event.start);
|
|
38
|
+
if (!isWholeDay) {
|
|
39
|
+
when = t("details.timeRange", { date: formatDate(event.start), start: formatTime(event.start), end: event.end ? formatTime(event.end) : "" });
|
|
40
|
+
}
|
|
41
|
+
const reminder = reminderOf(event.reminderMinutes);
|
|
42
|
+
const canJoin = isOpenableMeetingUrl(event.meetingUrl);
|
|
43
|
+
return /* @__PURE__ */ jsxs(Stack, { spacing: 2, children: [
|
|
44
|
+
/* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1.5, sx: { alignItems: "center" }, children: [
|
|
45
|
+
/* @__PURE__ */ jsx(Box, { "aria-hidden": "true", sx: { width: 12, height: 12, borderRadius: "4px", flexShrink: 0, bgcolor: `${event.tone ?? "primary"}.main` } }),
|
|
46
|
+
/* @__PURE__ */ jsx(Typography, { id: titleId, variant: "h6", component: "h2", children: event.title })
|
|
47
|
+
] }),
|
|
48
|
+
/* @__PURE__ */ jsx(DetailRow, { Icon: AccessTimeRoundedIcon, label: t("details.when"), children: /* @__PURE__ */ jsx(Typography, { variant: "body2", children: isWholeDay ? t("details.allDay", { date: when }) : when }) }),
|
|
49
|
+
event.location && /* @__PURE__ */ jsx(DetailRow, { Icon: PlaceOutlinedIcon, label: t("details.location"), children: /* @__PURE__ */ jsx(Typography, { variant: "body2", children: event.location }) }),
|
|
50
|
+
event.meetingUrl && /* @__PURE__ */ jsx(DetailRow, { Icon: VideocamOutlinedIcon, label: t("details.meeting"), children: /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, sx: { alignItems: "center", flexWrap: "wrap" }, useFlexGap: true, children: [
|
|
51
|
+
canJoin && /* @__PURE__ */ jsx(Button, { variant: "contained", size: "small", href: event.meetingUrl, target: "_blank", rel: "noreferrer", children: t("details.join") }),
|
|
52
|
+
/* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", sx: { wordBreak: "break-all" }, children: event.meetingUrl })
|
|
53
|
+
] }) }),
|
|
54
|
+
event.attendees?.length > 0 && /* @__PURE__ */ jsxs(DetailRow, { Icon: PeopleOutlineRoundedIcon, label: t("details.attendees"), children: [
|
|
55
|
+
/* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { mb: 0.75 }, children: t("details.attendeesCount", { count: event.attendees.length }) }),
|
|
56
|
+
/* @__PURE__ */ jsx(Stack, { spacing: 0.75, children: event.attendees.map((attendee) => {
|
|
57
|
+
const response = attendee.response ?? ATTENDEE_RESPONSE.pending;
|
|
58
|
+
return /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, sx: { alignItems: "center" }, children: [
|
|
59
|
+
/* @__PURE__ */ jsx(Avatar, { sx: { width: 24, height: 24, fontSize: 11 }, children: initialsOf(attendee.name) }),
|
|
60
|
+
/* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { flexGrow: 1, minWidth: 0 }, noWrap: true, children: attendee.name }),
|
|
61
|
+
/* @__PURE__ */ jsx(Chip, { size: "small", variant: "outlined", color: RESPONSE_COLOR[response] ?? "default", label: t(`details.response.${response}`) })
|
|
62
|
+
] }, attendee.id);
|
|
63
|
+
}) })
|
|
64
|
+
] }),
|
|
65
|
+
reminder.unit !== REMINDER_UNIT.none && /* @__PURE__ */ jsx(DetailRow, { Icon: NotificationsNoneRoundedIcon, label: t("details.reminder"), children: /* @__PURE__ */ jsx(Typography, { variant: "body2", children: t(`details.reminderUnit.${reminder.unit}`, { count: reminder.count }) }) }),
|
|
66
|
+
event.description && /* @__PURE__ */ jsx(DetailRow, { Icon: NotesRoundedIcon, label: t("details.notes"), children: /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { whiteSpace: "pre-wrap" }, children: event.description }) }),
|
|
67
|
+
actions && /* @__PURE__ */ jsx(Stack, { direction: "row", spacing: 1, useFlexGap: true, sx: { justifyContent: "flex-end", flexWrap: "wrap" }, children: actions })
|
|
68
|
+
] });
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
ATTENDEE_RESPONSE,
|
|
72
|
+
EventDetails as default
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=EventDetails.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventDetails.js","sources":["../../src/calendar/EventDetails.jsx"],"sourcesContent":["// El detalle de una cita, para la ventana que se abre al picarla en el calendario: su título con el color de su tipo,\n// cuándo (el día y las horas), dónde, la liga de la reunión con «Unirse», los invitados con su respuesta, el recordatorio y\n// las notas. Pinta lo que la cita traiga; lo que no trae no ocupa lugar.\n//\n// <EventDetails event={{title: 'Junta de cierre', start: '2026-10-20T10:00', end: '2026-10-20T11:00', location: 'Sala 2',\n// meetingUrl: 'https://meet.example.com/abc', attendees: [{id: 'ana', name: 'Ana', response: 'accepted'}],\n// reminderMinutes: 15}}\n// actions={<Button onClick={edit}>Editar</Button>} />\n//\n// No llama a ningún servidor ni abre la reunión por su cuenta: «Unirse» es una liga (solo http o https) y lo demás lo pone la\n// pantalla en actions. Sus textos se cambian con texts (la forma de `calendar` en los textos del paquete).\n//\n// Genérico del paquete: function (regla de la casa).\nimport Avatar from '@mui/material/Avatar';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Chip from '@mui/material/Chip';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport AccessTimeRoundedIcon from '@mui/icons-material/AccessTimeRounded';\nimport NotesRoundedIcon from '@mui/icons-material/NotesRounded';\nimport NotificationsNoneRoundedIcon from '@mui/icons-material/NotificationsNoneRounded';\nimport PeopleOutlineRoundedIcon from '@mui/icons-material/PeopleOutlineRounded';\nimport PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined';\nimport VideocamOutlinedIcon from '@mui/icons-material/VideocamOutlined';\n\nimport {initialsOf} from '@/appShell/menuItemText.js';\nimport {REMINDER_UNIT, isOpenableMeetingUrl, reminderOf} from '@/calendar/calendarEvents.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/** Las respuestas de un invitado. */\nexport const ATTENDEE_RESPONSE = Object.freeze({accepted: 'accepted', declined: 'declined', pending: 'pending'});\n// el color de cada respuesta (el color es información: verde va, rojo no va, gris no ha contestado)\nconst RESPONSE_COLOR = Object.freeze({\n [ATTENDEE_RESPONSE.accepted]: 'success', [ATTENDEE_RESPONSE.declined]: 'error', [ATTENDEE_RESPONSE.pending]: 'default',\n});\n// un día completo viene sin hora («2026-10-20»)\nconst WHOLE_DAY_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst ICON_SIZE = 20;\n\n/** Un renglón del detalle: su ícono a la izquierda y lo de adentro. */\nfunction DetailRow({Icon, label, children}) {\n return (\n <Stack direction=\"row\" spacing={1.5} sx={{alignItems: 'flex-start'}} aria-label={label} role=\"group\">\n <Icon sx={{fontSize: ICON_SIZE, color: 'text.secondary', mt: 0.25}} aria-hidden=\"true\" />\n <Box sx={{minWidth: 0, flexGrow: 1}}>{children}</Box>\n </Stack>\n );\n}\n\n/**\n * @param {object} props\n * @param {{title: string, start: string, end?: string|null, allDay?: boolean, tone?: string, location?: string, meetingUrl?: string, attendees?: Array<{id: string, name: string, email?: string, response?: 'accepted'|'declined'|'pending'}>, reminderMinutes?: number|null, description?: string}} props.event\n * start y end como texto ISO (o AAAA-MM-DD un día completo); tone: el color de su tipo\n * @param {import('react').ReactNode} [props.actions] botones de la pantalla (editar, borrar, responder…)\n * @param {string} [props.titleId] el id del título: la ventana que lo contiene se nombra con él (aria-labelledby)\n * @param {object} [props.texts] los textos que cambian, con la forma de `calendar` en los textos del paquete\n */\nexport default function EventDetails({event, actions, titleId, texts}) {\n const {t} = useToolkitTranslation('calendar', texts);\n const {formatDate, formatTime} = useFormatters();\n const isWholeDay = event.allDay || WHOLE_DAY_PATTERN.test(event.start);\n let when = formatDate(event.start);\n if (!isWholeDay) {\n when = t('details.timeRange', {date: formatDate(event.start), start: formatTime(event.start), end: event.end ? formatTime(event.end) : ''});\n }\n const reminder = reminderOf(event.reminderMinutes);\n const canJoin = isOpenableMeetingUrl(event.meetingUrl);\n\n return (\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={1.5} sx={{alignItems: 'center'}}>\n <Box aria-hidden=\"true\" sx={{width: 12, height: 12, borderRadius: '4px', flexShrink: 0, bgcolor: `${event.tone ?? 'primary'}.main`}} />\n <Typography id={titleId} variant=\"h6\" component=\"h2\">{event.title}</Typography>\n </Stack>\n <DetailRow Icon={AccessTimeRoundedIcon} label={t('details.when')}>\n <Typography variant=\"body2\">{isWholeDay ? t('details.allDay', {date: when}) : when}</Typography>\n </DetailRow>\n {event.location && (\n <DetailRow Icon={PlaceOutlinedIcon} label={t('details.location')}>\n <Typography variant=\"body2\">{event.location}</Typography>\n </DetailRow>\n )}\n {event.meetingUrl && (\n <DetailRow Icon={VideocamOutlinedIcon} label={t('details.meeting')}>\n <Stack direction=\"row\" spacing={1} sx={{alignItems: 'center', flexWrap: 'wrap'}} useFlexGap>\n {canJoin && (\n <Button variant=\"contained\" size=\"small\" href={event.meetingUrl} target=\"_blank\" rel=\"noreferrer\">{t('details.join')}</Button>\n )}\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{wordBreak: 'break-all'}}>{event.meetingUrl}</Typography>\n </Stack>\n </DetailRow>\n )}\n {event.attendees?.length > 0 && (\n <DetailRow Icon={PeopleOutlineRoundedIcon} label={t('details.attendees')}>\n <Typography variant=\"body2\" sx={{mb: 0.75}}>{t('details.attendeesCount', {count: event.attendees.length})}</Typography>\n <Stack spacing={0.75}>\n {event.attendees.map((attendee) => {\n const response = attendee.response ?? ATTENDEE_RESPONSE.pending;\n return (\n <Stack key={attendee.id} direction=\"row\" spacing={1} sx={{alignItems: 'center'}}>\n <Avatar sx={{width: 24, height: 24, fontSize: 11}}>{initialsOf(attendee.name)}</Avatar>\n <Typography variant=\"body2\" sx={{flexGrow: 1, minWidth: 0}} noWrap>{attendee.name}</Typography>\n <Chip size=\"small\" variant=\"outlined\" color={RESPONSE_COLOR[response] ?? 'default'} label={t(`details.response.${response}`)} />\n </Stack>\n );\n })}\n </Stack>\n </DetailRow>\n )}\n {reminder.unit !== REMINDER_UNIT.none && (\n <DetailRow Icon={NotificationsNoneRoundedIcon} label={t('details.reminder')}>\n <Typography variant=\"body2\">{t(`details.reminderUnit.${reminder.unit}`, {count: reminder.count})}</Typography>\n </DetailRow>\n )}\n {event.description && (\n <DetailRow Icon={NotesRoundedIcon} label={t('details.notes')}>\n <Typography variant=\"body2\" sx={{whiteSpace: 'pre-wrap'}}>{event.description}</Typography>\n </DetailRow>\n )}\n {actions && <Stack direction=\"row\" spacing={1} useFlexGap sx={{justifyContent: 'flex-end', flexWrap: 'wrap'}}>{actions}</Stack>}\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const MINUTES_PER_HOUR = 60;
|
|
2
|
+
const MINUTES_PER_DAY = 24 * MINUTES_PER_HOUR;
|
|
3
|
+
const REMINDER_UNIT = Object.freeze({ none: "none", atStart: "atStart", minutes: "minutes", hours: "hours", days: "days" });
|
|
4
|
+
function reminderOf(minutes) {
|
|
5
|
+
if (minutes === null || minutes === void 0 || minutes < 0) {
|
|
6
|
+
return { unit: REMINDER_UNIT.none, count: 0 };
|
|
7
|
+
}
|
|
8
|
+
if (minutes === 0) {
|
|
9
|
+
return { unit: REMINDER_UNIT.atStart, count: 0 };
|
|
10
|
+
}
|
|
11
|
+
if (minutes % MINUTES_PER_DAY === 0) {
|
|
12
|
+
return { unit: REMINDER_UNIT.days, count: minutes / MINUTES_PER_DAY };
|
|
13
|
+
}
|
|
14
|
+
if (minutes % MINUTES_PER_HOUR === 0) {
|
|
15
|
+
return { unit: REMINDER_UNIT.hours, count: minutes / MINUTES_PER_HOUR };
|
|
16
|
+
}
|
|
17
|
+
return { unit: REMINDER_UNIT.minutes, count: minutes };
|
|
18
|
+
}
|
|
19
|
+
function isOpenableMeetingUrl(url) {
|
|
20
|
+
try {
|
|
21
|
+
return ["http:", "https:"].includes(new URL(url).protocol);
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
REMINDER_UNIT,
|
|
28
|
+
isOpenableMeetingUrl,
|
|
29
|
+
reminderOf
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=calendarEvents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendarEvents.js","sources":["../../src/calendar/calendarEvents.js"],"sourcesContent":["// Lo que el detalle de una cita decide sin pintar nada: cómo se dice su recordatorio (minutos, horas o días antes) y si su\n// liga de reunión es una dirección web de verdad. Funciones puras: se prueban sin navegador (calendarEvents.test.js).\n//\n// Funciones puras, no renderizan: function (regla de la casa).\n\nconst MINUTES_PER_HOUR = 60;\nconst MINUTES_PER_DAY = 24 * MINUTES_PER_HOUR;\n\n/** La unidad del recordatorio (la llave de su texto: calendar.reminder.<unidad>). */\nexport const REMINDER_UNIT = Object.freeze({none: 'none', atStart: 'atStart', minutes: 'minutes', hours: 'hours', days: 'days'});\n\n/**\n * Cómo se dice un recordatorio: «sin recordatorio», «al empezar», «15 minutos antes», «2 horas antes», «1 día antes». Usa la\n * unidad más grande que lo dice exacto (90 minutos se quedan en minutos).\n *\n * @param {number|null|undefined} minutes cuántos minutos antes; null sin recordatorio\n * @returns {{unit: string, count: number}}\n */\nexport function reminderOf(minutes) {\n if (minutes === null || minutes === undefined || minutes < 0) {\n return {unit: REMINDER_UNIT.none, count: 0};\n }\n if (minutes === 0) {\n return {unit: REMINDER_UNIT.atStart, count: 0};\n }\n if (minutes % MINUTES_PER_DAY === 0) {\n return {unit: REMINDER_UNIT.days, count: minutes / MINUTES_PER_DAY};\n }\n if (minutes % MINUTES_PER_HOUR === 0) {\n return {unit: REMINDER_UNIT.hours, count: minutes / MINUTES_PER_HOUR};\n }\n return {unit: REMINDER_UNIT.minutes, count: minutes};\n}\n\n/**\n * ¿La liga de reunión se puede abrir? Solo http y https: una liga «javascript:» o con otro esquema no se ofrece para abrir\n * (la pinta quien la escribió, pero nadie le da clic a algo raro desde la app).\n *\n * @param {string|null|undefined} url\n * @returns {boolean}\n */\nexport function isOpenableMeetingUrl(url) {\n try {\n return ['http:', 'https:'].includes(new URL(url).protocol);\n } catch {\n return false;\n }\n}\n"],"names":[],"mappings":"AAKA,MAAM,mBAAmB;AACzB,MAAM,kBAAkB,KAAK;AAGjB,MAAC,gBAAgB,OAAO,OAAO,EAAC,MAAM,QAAQ,SAAS,WAAW,SAAS,WAAW,OAAO,SAAS,MAAM,OAAM,CAAC;AASxH,SAAS,WAAW,SAAS;AAChC,MAAI,YAAY,QAAQ,YAAY,UAAa,UAAU,GAAG;AAC1D,WAAO,EAAC,MAAM,cAAc,MAAM,OAAO,EAAC;AAAA,EAC9C;AACA,MAAI,YAAY,GAAG;AACf,WAAO,EAAC,MAAM,cAAc,SAAS,OAAO,EAAC;AAAA,EACjD;AACA,MAAI,UAAU,oBAAoB,GAAG;AACjC,WAAO,EAAC,MAAM,cAAc,MAAM,OAAO,UAAU,gBAAe;AAAA,EACtE;AACA,MAAI,UAAU,qBAAqB,GAAG;AAClC,WAAO,EAAC,MAAM,cAAc,OAAO,OAAO,UAAU,iBAAgB;AAAA,EACxE;AACA,SAAO,EAAC,MAAM,cAAc,SAAS,OAAO,QAAO;AACvD;AASO,SAAS,qBAAqB,KAAK;AACtC,MAAI;AACA,WAAO,CAAC,SAAS,QAAQ,EAAE,SAAS,IAAI,IAAI,GAAG,EAAE,QAAQ;AAAA,EAC7D,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;"}
|
package/dist/calendar.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { CALENDAR_VIEW, default as default2 } from "./calendar/EventCalendar.js";
|
|
2
|
+
import { ATTENDEE_RESPONSE, default as default3 } from "./calendar/EventDetails.js";
|
|
3
|
+
import { REMINDER_UNIT, isOpenableMeetingUrl, reminderOf } from "./calendar/calendarEvents.js";
|
|
2
4
|
export {
|
|
5
|
+
ATTENDEE_RESPONSE,
|
|
3
6
|
CALENDAR_VIEW,
|
|
4
|
-
default2 as EventCalendar
|
|
7
|
+
default2 as EventCalendar,
|
|
8
|
+
default3 as EventDetails,
|
|
9
|
+
REMINDER_UNIT,
|
|
10
|
+
isOpenableMeetingUrl,
|
|
11
|
+
reminderOf
|
|
5
12
|
};
|
|
6
13
|
//# sourceMappingURL=calendar.js.map
|
package/dist/calendar.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"calendar.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
|
package/dist/charts.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { default as default2 } from "./ui/metrics/BarList.js";
|
|
2
2
|
import { default as default3 } from "./ui/metrics/ColumnChart.js";
|
|
3
|
+
import { default as default4 } from "./ui/metrics/LineChart.js";
|
|
4
|
+
import { default as default5 } from "./ui/metrics/ChartTooltip.js";
|
|
5
|
+
import { CHART_LINE_DASHES, CHART_SERIES_TONES } from "./ui/metrics/chartParts.js";
|
|
6
|
+
import { CHART_VIEW } from "./ui/metrics/TableToggle.js";
|
|
3
7
|
export {
|
|
4
8
|
default2 as BarList,
|
|
5
|
-
|
|
9
|
+
CHART_LINE_DASHES,
|
|
10
|
+
CHART_SERIES_TONES,
|
|
11
|
+
CHART_VIEW,
|
|
12
|
+
default5 as ChartTooltip,
|
|
13
|
+
default3 as ColumnChart,
|
|
14
|
+
default4 as LineChart
|
|
6
15
|
};
|
|
7
16
|
//# sourceMappingURL=charts.js.map
|
package/dist/charts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"charts.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"charts.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
|
|
@@ -32,9 +32,10 @@ function DateInput({
|
|
|
32
32
|
disablePast = false,
|
|
33
33
|
openTo = "day",
|
|
34
34
|
disabled = false,
|
|
35
|
-
inputRef
|
|
35
|
+
inputRef,
|
|
36
|
+
texts
|
|
36
37
|
}) {
|
|
37
|
-
const { t, language } = useToolkitTranslation("common");
|
|
38
|
+
const { t, language } = useToolkitTranslation("common", texts);
|
|
38
39
|
const generatedId = useId();
|
|
39
40
|
const inputId = `${name ?? "date"}-${generatedId}`;
|
|
40
41
|
const errorText = translateValidationMessage(t, error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DateInput.js","sources":["../../src/datePickers/DateInput.jsx"],"sourcesContent":["// Campo de FECHA suelto con el DatePicker de MUI X (@stackcraftmx/calcita/date-pickers), con la etiqueta arriba como los\n// demás campos del paquete.\n//\n// Al formulario (y al servidor) viaja la fecha de calendario 'AAAA-MM-DD' en texto; en pantalla se ve DD/MM/AAAA en español\n// y MM/DD/AAAA en inglés, con el calendario y sus textos en el idioma activo. '' = sin fecha. Una fecha de calendario no\n// tiene hora ni zona: nunca se pasa por Date (que la movería de día según la zona).\n//\n// Cada campo trae su LocalizationProvider con dayjs: la pantalla no tiene que poner nada alrededor.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId} from 'react';\nimport dayjs from 'dayjs';\nimport 'dayjs/locale/es';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {AdapterDayjs} from '@mui/x-date-pickers/AdapterDayjs';\nimport {DatePicker} from '@mui/x-date-pickers/DatePicker';\nimport {LocalizationProvider} from '@mui/x-date-pickers/LocalizationProvider';\nimport {enUS, esES} from '@mui/x-date-pickers/locales';\n\nimport {translateValidationMessage} from '@/forms/validationMessages.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\n\n/** Cómo viaja una fecha de calendario. */\nexport const CALENDAR_DATE_FORMAT = 'YYYY-MM-DD';\n\n// cómo se ve y con qué textos sale el calendario en cada idioma\nconst DATE_LOOK_BY_LANGUAGE = Object.freeze({\n es: {adapterLocale: 'es', displayFormat: 'DD/MM/YYYY', localeText: esES.components.MuiLocalizationProvider.defaultProps.localeText},\n en: {adapterLocale: 'en', displayFormat: 'MM/DD/YYYY', localeText: enUS.components.MuiLocalizationProvider.defaultProps.localeText},\n});\n\n/**\n * Una fecha de calendario 'AAAA-MM-DD' como la quiere el DatePicker; null si está vacía.\n *\n * @param {string|null|undefined} calendarDate\n * @returns {import('dayjs').Dayjs|null}\n */\nfunction pickerValueOf(calendarDate) {\n // dayjs lee 'AAAA-MM-DD' como día local (a diferencia de new Date, que lo toma en UTC y lo mueve de día)\n return calendarDate ? dayjs(calendarDate) : null;\n}\n\n/**\n * @param {object} props\n * @param {string} [props.name]\n * @param {string} props.label ya traducido\n * @param {string|null|undefined} props.value 'AAAA-MM-DD' o ''\n * @param {(value: string) => void} props.onChange recibe 'AAAA-MM-DD' ('' al borrar o con una fecha a medias)\n * @param {() => void} [props.onBlur]\n * @param {string|{key: string, values?: object}} [props.error]\n * @param {string} [props.helperText]\n * @param {string} [props.minDate] 'AAAA-MM-DD'\n * @param {string} [props.maxDate] 'AAAA-MM-DD'\n * @param {boolean} [props.disableFuture] p. ej. una fecha de nacimiento\n * @param {boolean} [props.disablePast] p. ej. una cita\n * @param {'day'|'month'|'year'} [props.openTo] por dónde abre el calendario (year para un nacimiento)\n * @param {boolean} [props.disabled]\n * @param {import('react').Ref<HTMLInputElement>} [props.inputRef]\n */\nexport default function DateInput({\n name, label, value, onChange, onBlur, error, helperText = ' ', minDate, maxDate, disableFuture = false, disablePast = false,\n openTo = 'day', disabled = false, inputRef,\n}) {\n const {t, language} = useToolkitTranslation('common');\n const generatedId = useId();\n const inputId = `${name ?? 'date'}-${generatedId}`;\n const errorText = translateValidationMessage(t, error);\n const look = DATE_LOOK_BY_LANGUAGE[language] ?? DATE_LOOK_BY_LANGUAGE.es;\n\n return (\n <FieldFrame inputId={inputId} label={label}>\n <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={look.adapterLocale} localeText={look.localeText}>\n <DatePicker\n format={look.displayFormat}\n value={pickerValueOf(value)}\n minDate={pickerValueOf(minDate) ?? undefined}\n maxDate={pickerValueOf(maxDate) ?? undefined}\n disableFuture={disableFuture}\n disablePast={disablePast}\n disabled={disabled}\n openTo={openTo}\n views={openTo === 'year' ? ['year', 'month', 'day'] : undefined}\n inputRef={inputRef}\n // una fecha a medias (25/1_/____) todavía no es fecha: viaja como vacía hasta que se complete\n onChange={(pickedDate) => onChange(pickedDate?.isValid() ? pickedDate.format(CALENDAR_DATE_FORMAT) : '')}\n onClose={onBlur}\n slotProps={{\n textField: {\n id: inputId, name, size: 'small', fullWidth: true, onBlur, error: Boolean(errorText), helperText: errorText ?? helperText,\n },\n }}\n />\n </LocalizationProvider>\n </FieldFrame>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAuBO,MAAM,uBAAuB;AAGpC,MAAM,wBAAwB,OAAO,OAAO;AAAA,EACxC,IAAI,EAAC,eAAe,MAAM,eAAe,cAAc,YAAY,KAAK,WAAW,wBAAwB,aAAa,WAAA;AAAA,EACxH,IAAI,EAAC,eAAe,MAAM,eAAe,cAAc,YAAY,KAAK,WAAW,wBAAwB,aAAa,WAAA;AAC5H,CAAC;AAQD,SAAS,cAAc,cAAc;AAEjC,SAAO,eAAe,MAAM,YAAY,IAAI;AAChD;
|
|
1
|
+
{"version":3,"file":"DateInput.js","sources":["../../src/datePickers/DateInput.jsx"],"sourcesContent":["// Campo de FECHA suelto con el DatePicker de MUI X (@stackcraftmx/calcita/date-pickers), con la etiqueta arriba como los\n// demás campos del paquete.\n//\n// Al formulario (y al servidor) viaja la fecha de calendario 'AAAA-MM-DD' en texto; en pantalla se ve DD/MM/AAAA en español\n// y MM/DD/AAAA en inglés, con el calendario y sus textos en el idioma activo. '' = sin fecha. Una fecha de calendario no\n// tiene hora ni zona: nunca se pasa por Date (que la movería de día según la zona).\n//\n// Cada campo trae su LocalizationProvider con dayjs: la pantalla no tiene que poner nada alrededor.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId} from 'react';\nimport dayjs from 'dayjs';\nimport 'dayjs/locale/es';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {AdapterDayjs} from '@mui/x-date-pickers/AdapterDayjs';\nimport {DatePicker} from '@mui/x-date-pickers/DatePicker';\nimport {LocalizationProvider} from '@mui/x-date-pickers/LocalizationProvider';\nimport {enUS, esES} from '@mui/x-date-pickers/locales';\n\nimport {translateValidationMessage} from '@/forms/validationMessages.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\n\n/** Cómo viaja una fecha de calendario. */\nexport const CALENDAR_DATE_FORMAT = 'YYYY-MM-DD';\n\n// cómo se ve y con qué textos sale el calendario en cada idioma\nconst DATE_LOOK_BY_LANGUAGE = Object.freeze({\n es: {adapterLocale: 'es', displayFormat: 'DD/MM/YYYY', localeText: esES.components.MuiLocalizationProvider.defaultProps.localeText},\n en: {adapterLocale: 'en', displayFormat: 'MM/DD/YYYY', localeText: enUS.components.MuiLocalizationProvider.defaultProps.localeText},\n});\n\n/**\n * Una fecha de calendario 'AAAA-MM-DD' como la quiere el DatePicker; null si está vacía.\n *\n * @param {string|null|undefined} calendarDate\n * @returns {import('dayjs').Dayjs|null}\n */\nfunction pickerValueOf(calendarDate) {\n // dayjs lee 'AAAA-MM-DD' como día local (a diferencia de new Date, que lo toma en UTC y lo mueve de día)\n return calendarDate ? dayjs(calendarDate) : null;\n}\n\n/**\n * @param {object} props\n * @param {string} [props.name]\n * @param {string} props.label ya traducido\n * @param {string|null|undefined} props.value 'AAAA-MM-DD' o ''\n * @param {(value: string) => void} props.onChange recibe 'AAAA-MM-DD' ('' al borrar o con una fecha a medias)\n * @param {() => void} [props.onBlur]\n * @param {string|{key: string, values?: object}} [props.error]\n * @param {string} [props.helperText]\n * @param {string} [props.minDate] 'AAAA-MM-DD'\n * @param {string} [props.maxDate] 'AAAA-MM-DD'\n * @param {boolean} [props.disableFuture] p. ej. una fecha de nacimiento\n * @param {boolean} [props.disablePast] p. ej. una cita\n * @param {'day'|'month'|'year'} [props.openTo] por dónde abre el calendario (year para un nacimiento)\n * @param {boolean} [props.disabled]\n * @param {import('react').Ref<HTMLInputElement>} [props.inputRef]\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function DateInput({\n name, label, value, onChange, onBlur, error, helperText = ' ', minDate, maxDate, disableFuture = false, disablePast = false,\n openTo = 'day', disabled = false, inputRef, texts,\n}) {\n const {t, language} = useToolkitTranslation('common', texts);\n const generatedId = useId();\n const inputId = `${name ?? 'date'}-${generatedId}`;\n const errorText = translateValidationMessage(t, error);\n const look = DATE_LOOK_BY_LANGUAGE[language] ?? DATE_LOOK_BY_LANGUAGE.es;\n\n return (\n <FieldFrame inputId={inputId} label={label}>\n <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={look.adapterLocale} localeText={look.localeText}>\n <DatePicker\n format={look.displayFormat}\n value={pickerValueOf(value)}\n minDate={pickerValueOf(minDate) ?? undefined}\n maxDate={pickerValueOf(maxDate) ?? undefined}\n disableFuture={disableFuture}\n disablePast={disablePast}\n disabled={disabled}\n openTo={openTo}\n views={openTo === 'year' ? ['year', 'month', 'day'] : undefined}\n inputRef={inputRef}\n // una fecha a medias (25/1_/____) todavía no es fecha: viaja como vacía hasta que se complete\n onChange={(pickedDate) => onChange(pickedDate?.isValid() ? pickedDate.format(CALENDAR_DATE_FORMAT) : '')}\n onClose={onBlur}\n slotProps={{\n textField: {\n id: inputId, name, size: 'small', fullWidth: true, onBlur, error: Boolean(errorText), helperText: errorText ?? helperText,\n },\n }}\n />\n </LocalizationProvider>\n </FieldFrame>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAuBO,MAAM,uBAAuB;AAGpC,MAAM,wBAAwB,OAAO,OAAO;AAAA,EACxC,IAAI,EAAC,eAAe,MAAM,eAAe,cAAc,YAAY,KAAK,WAAW,wBAAwB,aAAa,WAAA;AAAA,EACxH,IAAI,EAAC,eAAe,MAAM,eAAe,cAAc,YAAY,KAAK,WAAW,wBAAwB,aAAa,WAAA;AAC5H,CAAC;AAQD,SAAS,cAAc,cAAc;AAEjC,SAAO,eAAe,MAAM,YAAY,IAAI;AAChD;AAoBA,SAAwB,UAAU;AAAA,EAC9B;AAAA,EAAM;AAAA,EAAO;AAAA,EAAO;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAO,aAAa;AAAA,EAAK;AAAA,EAAS;AAAA,EAAS,gBAAgB;AAAA,EAAO,cAAc;AAAA,EACtH,SAAS;AAAA,EAAO,WAAW;AAAA,EAAO;AAAA,EAAU;AAChD,GAAG;AACC,QAAM,EAAC,GAAG,SAAA,IAAY,sBAAsB,UAAU,KAAK;AAC3D,QAAM,cAAc,MAAA;AACpB,QAAM,UAAU,GAAG,QAAQ,MAAM,IAAI,WAAW;AAChD,QAAM,YAAY,2BAA2B,GAAG,KAAK;AACrD,QAAM,OAAO,sBAAsB,QAAQ,KAAK,sBAAsB;AAEtE,SACI,oBAAC,YAAA,EAAW,SAAkB,OAC1B,UAAA,oBAAC,sBAAA,EAAqB,aAAa,cAAc,eAAe,KAAK,eAAe,YAAY,KAAK,YACjG,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACG,QAAQ,KAAK;AAAA,MACb,OAAO,cAAc,KAAK;AAAA,MAC1B,SAAS,cAAc,OAAO,KAAK;AAAA,MACnC,SAAS,cAAc,OAAO,KAAK;AAAA,MACnC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,WAAW,SAAS,CAAC,QAAQ,SAAS,KAAK,IAAI;AAAA,MACtD;AAAA,MAEA,UAAU,CAAC,eAAe,SAAS,YAAY,YAAY,WAAW,OAAO,oBAAoB,IAAI,EAAE;AAAA,MACvG,SAAS;AAAA,MACT,WAAW;AAAA,QACP,WAAW;AAAA,UACP,IAAI;AAAA,UAAS;AAAA,UAAM,MAAM;AAAA,UAAS,WAAW;AAAA,UAAM;AAAA,UAAQ,OAAO,QAAQ,SAAS;AAAA,UAAG,YAAY,aAAa;AAAA,QAAA;AAAA,MACnH;AAAA,IACJ;AAAA,EAAA,GAER,EAAA,CACJ;AAER;"}
|
|
@@ -21,30 +21,23 @@ import CloseIcon from "@mui/icons-material/Close";
|
|
|
21
21
|
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
|
22
22
|
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
|
23
23
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
24
|
-
import InsertDriveFileOutlinedIcon from "@mui/icons-material/InsertDriveFileOutlined";
|
|
25
|
-
import PictureAsPdfOutlinedIcon from "@mui/icons-material/PictureAsPdfOutlined";
|
|
26
|
-
import TableViewOutlinedIcon from "@mui/icons-material/TableViewOutlined";
|
|
27
|
-
import TextSnippetOutlinedIcon from "@mui/icons-material/TextSnippetOutlined";
|
|
28
24
|
import { downloadsActor, countActiveDownloads, clearFinishedDownloads, cancelDownload } from "./downloadQueue.js";
|
|
25
|
+
import { useFileTypes, UNKNOWN_FILE_TYPE } from "./fileTypes.js";
|
|
29
26
|
import { DOWNLOAD_STATUS } from "./downloadsMachine.js";
|
|
30
|
-
import {
|
|
27
|
+
import { useFormatters } from "../toolkit/useFormatters.js";
|
|
31
28
|
import { isGlassTheme, glassStylesOf } from "../theme/glassSurface.js";
|
|
32
|
-
|
|
33
|
-
xlsx: { Icon: TableViewOutlinedIcon, color: "success.main" },
|
|
34
|
-
csv: { Icon: TextSnippetOutlinedIcon, color: "info.main" },
|
|
35
|
-
pdf: { Icon: PictureAsPdfOutlinedIcon, color: "error.main" }
|
|
36
|
-
};
|
|
37
|
-
const DEFAULT_FILE_TYPE_APPEARANCE = { Icon: InsertDriveFileOutlinedIcon, color: "text.secondary" };
|
|
29
|
+
import ComponentTexts from "../toolkit/ComponentTexts.js";
|
|
38
30
|
const AUTO_DISMISS_MILLISECONDS = 5e3;
|
|
39
31
|
const SCREEN_EDGE_SPACING = 3;
|
|
40
32
|
const PANEL_WIDTH_PIXELS = 360;
|
|
41
33
|
const LIST_MAX_HEIGHT = "40vh";
|
|
42
34
|
const PROGRESS_RING_PIXELS = 22;
|
|
43
|
-
function fileTypeAppearanceOf(download) {
|
|
35
|
+
function fileTypeAppearanceOf(download, fileTypes) {
|
|
44
36
|
const extension = download.fileName?.split(".").pop()?.toLowerCase();
|
|
45
|
-
|
|
37
|
+
const fileType = fileTypes[download.format ?? extension];
|
|
38
|
+
return fileType ? { Icon: fileType.Icon ?? UNKNOWN_FILE_TYPE.Icon, color: fileType.color ?? UNKNOWN_FILE_TYPE.color } : UNKNOWN_FILE_TYPE;
|
|
46
39
|
}
|
|
47
|
-
function detailTextOf(download, t) {
|
|
40
|
+
function detailTextOf(download, t, formatFileSize) {
|
|
48
41
|
if (download.status === DOWNLOAD_STATUS.completed) {
|
|
49
42
|
return formatFileSize(download.loadedBytes);
|
|
50
43
|
}
|
|
@@ -108,7 +101,8 @@ function DownloadStatusIndicator({ download, displayName }) {
|
|
|
108
101
|
}
|
|
109
102
|
function DownloadRow({ download }) {
|
|
110
103
|
const { t } = useToolkitTranslation("common");
|
|
111
|
-
const {
|
|
104
|
+
const { formatFileSize } = useFormatters();
|
|
105
|
+
const { Icon, color } = fileTypeAppearanceOf(download, useFileTypes());
|
|
112
106
|
const displayName = download.fileName ?? download.label;
|
|
113
107
|
const isFailed = download.status === DOWNLOAD_STATUS.failed;
|
|
114
108
|
return (
|
|
@@ -124,7 +118,7 @@ function DownloadRow({ download }) {
|
|
|
124
118
|
component: "p",
|
|
125
119
|
noWrap: !isFailed,
|
|
126
120
|
sx: { color: isFailed ? "error.main" : "text.secondary" },
|
|
127
|
-
children: detailTextOf(download, t)
|
|
121
|
+
children: detailTextOf(download, t, formatFileSize)
|
|
128
122
|
}
|
|
129
123
|
)
|
|
130
124
|
] }),
|
|
@@ -132,8 +126,8 @@ function DownloadRow({ download }) {
|
|
|
132
126
|
] })
|
|
133
127
|
);
|
|
134
128
|
}
|
|
135
|
-
function DownloadManager() {
|
|
136
|
-
const { t } = useToolkitTranslation("common");
|
|
129
|
+
function DownloadManager({ texts } = {}) {
|
|
130
|
+
const { t } = useToolkitTranslation("common", texts);
|
|
137
131
|
const downloads = useSelector(downloadsActor, (snapshot) => snapshot.context.downloads);
|
|
138
132
|
const [isExpanded, setIsExpanded] = useState(true);
|
|
139
133
|
const previousCountRef = useRef(downloads.length);
|
|
@@ -152,7 +146,7 @@ function DownloadManager() {
|
|
|
152
146
|
const dismissTimer = setTimeout(clearFinishedDownloads, AUTO_DISMISS_MILLISECONDS);
|
|
153
147
|
return () => clearTimeout(dismissTimer);
|
|
154
148
|
}, [allSucceeded, downloads.length]);
|
|
155
|
-
return /* @__PURE__ */ jsx(Slide, { direction: "up", in: downloads.length > 0, mountOnEnter: true, unmountOnExit: true, children: /* @__PURE__ */ jsxs(
|
|
149
|
+
return /* @__PURE__ */ jsx(ComponentTexts, { namespace: "common", texts, children: /* @__PURE__ */ jsx(Slide, { direction: "up", in: downloads.length > 0, mountOnEnter: true, unmountOnExit: true, children: /* @__PURE__ */ jsxs(
|
|
156
150
|
Paper,
|
|
157
151
|
{
|
|
158
152
|
elevation: 8,
|
|
@@ -193,7 +187,7 @@ function DownloadManager() {
|
|
|
193
187
|
] })
|
|
194
188
|
]
|
|
195
189
|
}
|
|
196
|
-
) });
|
|
190
|
+
) }) });
|
|
197
191
|
}
|
|
198
192
|
export {
|
|
199
193
|
DownloadManager as default
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DownloadManager.js","sources":["../../src/downloads/DownloadManager.jsx"],"sourcesContent":["// Panel de descargas al estilo de Google Drive: se pega abajo a la derecha SOLO mientras hay descargas, con una barra que\n// resume («Descargando 2 archivos…», «3 descargas listas») y la lista con el ícono del tipo de archivo y un anillo de\n// avance que termina en ✓. Con MUI y XState, sin Redux.\n//\n// Qué hace para no hacer ruido:\n// - no hay botón fijo: sin descargas no se pinta nada;\n// - se pliega a la barra con la flecha y se vuelve a abrir cuando entra otra descarga;\n// - cuando todo terminó bien se va solo a los AUTO_DISMISS_MILLISECONDS; si algo falló o se canceló se queda hasta que\n// la persona lo cierra, para que vea el motivo;\n// - la X solo aparece cuando ya nada descarga (cerrar a media descarga dejaría el archivo sin aviso de que terminó).\n//\n// Se monta UNA sola vez en la raíz de la aplicación. Lee la lista de downloadsActor; las descargas las arranca startDownload desde cada\n// pantalla. Los avisos (sonner) salen abajo a la izquierda, así que el panel va a la derecha.\n// Con el vidrio del tema (createToolkitTheme({glass: true})) el panel es de vidrio: flota sobre la pantalla.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {Fragment, useEffect, useRef, useState} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useSelector} from '@xstate/react';\nimport Box from '@mui/material/Box';\nimport CircularProgress from '@mui/material/CircularProgress';\nimport Collapse from '@mui/material/Collapse';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListItem from '@mui/material/ListItem';\nimport Paper from '@mui/material/Paper';\nimport Slide from '@mui/material/Slide';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport BlockOutlinedIcon from '@mui/icons-material/BlockOutlined';\nimport CheckCircleIcon from '@mui/icons-material/CheckCircle';\nimport CloseIcon from '@mui/icons-material/Close';\nimport ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';\nimport ExpandLessIcon from '@mui/icons-material/ExpandLess';\nimport ExpandMoreIcon from '@mui/icons-material/ExpandMore';\nimport InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';\nimport PictureAsPdfOutlinedIcon from '@mui/icons-material/PictureAsPdfOutlined';\nimport TableViewOutlinedIcon from '@mui/icons-material/TableViewOutlined';\nimport TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';\n\nimport {cancelDownload, clearFinishedDownloads, countActiveDownloads, downloadsActor} from '@/downloads/downloadQueue.js';\nimport {DOWNLOAD_STATUS} from '@/downloads/downloadsMachine.js';\nimport {formatFileSize} from '@/formatting.js';\nimport {glassStylesOf, isGlassTheme} from '@/theme/glassSurface.js';\n\n// ícono y color del tipo de archivo (el color sale de la paleta del tema, claro y oscuro)\nconst FILE_TYPE_APPEARANCE = {\n xlsx: {Icon: TableViewOutlinedIcon, color: 'success.main'},\n csv: {Icon: TextSnippetOutlinedIcon, color: 'info.main'},\n pdf: {Icon: PictureAsPdfOutlinedIcon, color: 'error.main'},\n};\nconst DEFAULT_FILE_TYPE_APPEARANCE = {Icon: InsertDriveFileOutlinedIcon, color: 'text.secondary'};\n\n// cuánto se queda el panel después de que TODAS las descargas terminaron bien\nconst AUTO_DISMISS_MILLISECONDS = 5000;\n\n// separación del borde derecho y ancho del panel (en el teléfono ocupa el ancho menos los márgenes)\nconst SCREEN_EDGE_SPACING = 3;\nconst PANEL_WIDTH_PIXELS = 360;\nconst LIST_MAX_HEIGHT = '40vh';\nconst PROGRESS_RING_PIXELS = 22;\n\n/**\n * Tipo de archivo de una descarga: el que dijo quien la arrancó o, al terminar, la extensión del nombre.\n *\n * @param {object} download renglón de la lista (downloadsMachine)\n * @returns {{Icon: import('react').ElementType, color: string}}\n */\nfunction fileTypeAppearanceOf(download) {\n const extension = download.fileName?.split('.').pop()?.toLowerCase();\n return FILE_TYPE_APPEARANCE[download.format ?? extension] ?? DEFAULT_FILE_TYPE_APPEARANCE;\n}\n\n/**\n * Segunda línea del renglón: cuánto lleva, cuánto pesó o por qué no se pudo.\n *\n * @param {object} download\n * @param {Function} t traductor de common\n * @returns {string}\n */\nfunction detailTextOf(download, t) {\n if (download.status === DOWNLOAD_STATUS.completed) {\n return formatFileSize(download.loadedBytes);\n }\n if (download.status === DOWNLOAD_STATUS.failed) {\n return download.errorMessage ?? t('downloads.failed');\n }\n if (download.status === DOWNLOAD_STATUS.canceled) {\n return t('downloads.canceled');\n }\n if (download.loadedBytes === 0) {\n return t('downloads.preparing');\n }\n // un archivo que se arma al vuelo (el log) no dice su tamaño: se enseña lo que ya bajó\n return download.totalBytes\n ? t('downloads.progressOf', {loaded: formatFileSize(download.loadedBytes), total: formatFileSize(download.totalBytes)})\n : t('downloads.progressLoaded', {loaded: formatFileSize(download.loadedBytes)});\n}\n\n/**\n * Resumen de la barra, como en Drive: lo que está pasando ahora o cómo terminó.\n *\n * @param {Array<object>} downloads\n * @param {Function} t traductor de common\n * @returns {string}\n */\nfunction summaryTextOf(downloads, t) {\n const activeCount = countActiveDownloads(downloads);\n if (activeCount > 0) {\n return t('downloads.summaryDownloading', {count: activeCount});\n }\n const failedCount = downloads.filter((download) => download.status === DOWNLOAD_STATUS.failed).length;\n if (failedCount > 0) {\n return t('downloads.summaryFailed', {count: failedCount});\n }\n const completedCount = downloads.filter((download) => download.status === DOWNLOAD_STATUS.completed).length;\n return t('downloads.summaryCompleted', {count: completedCount});\n}\n\n/**\n * Lo que va a la derecha del renglón: el anillo de avance con su botón para cancelar, o cómo terminó.\n *\n * @param {{download: object, displayName: string}} props\n */\nfunction DownloadStatusIndicator({download, displayName}) {\n const {t} = useToolkitTranslation('common');\n\n if (download.status === DOWNLOAD_STATUS.completed) {\n return <CheckCircleIcon sx={{color: 'success.main'}} aria-label={t('downloads.completedOf', {name: displayName})} />;\n }\n if (download.status === DOWNLOAD_STATUS.failed) {\n return <ErrorOutlineIcon sx={{color: 'error.main'}} aria-label={t('downloads.failedOf', {name: displayName})} />;\n }\n if (download.status === DOWNLOAD_STATUS.canceled) {\n return <BlockOutlinedIcon sx={{color: 'text.disabled'}} aria-label={t('downloads.canceledOf', {name: displayName})} />;\n }\n\n const hasKnownSize = Boolean(download.totalBytes);\n // el anillo y, encima, la X para cancelar: como en Drive, un solo lugar para ver y detener\n return (\n <Tooltip title={t('downloads.cancel')}>\n <IconButton size=\"small\" aria-label={t('downloads.cancelOf', {name: displayName})} onClick={() => cancelDownload(download.id)}\n sx={{position: 'relative', width: PROGRESS_RING_PIXELS + 10, height: PROGRESS_RING_PIXELS + 10}}>\n <CircularProgress\n size={PROGRESS_RING_PIXELS}\n thickness={4}\n variant={hasKnownSize ? 'determinate' : 'indeterminate'}\n value={hasKnownSize ? Math.min(100, (download.loadedBytes * 100) / download.totalBytes) : undefined}\n sx={{position: 'absolute'}}\n />\n <CloseIcon sx={{fontSize: 12}} />\n </IconButton>\n </Tooltip>\n );\n}\n\n/**\n * Un renglón: ícono del tipo de archivo, nombre, detalle y estatus.\n *\n * @param {{download: object}} props\n */\nfunction DownloadRow({download}) {\n const {t} = useToolkitTranslation('common');\n const {Icon, color} = fileTypeAppearanceOf(download);\n const displayName = download.fileName ?? download.label;\n const isFailed = download.status === DOWNLOAD_STATUS.failed;\n\n return (\n // minWidth 0: sin él un nombre largo empuja el renglón y la lista enseña una barra horizontal\n <ListItem disableGutters sx={{gap: 1.5, px: 2, py: 1, minWidth: 0}}>\n <Icon fontSize=\"small\" sx={{color, flexShrink: 0}} />\n <Box sx={{minWidth: 0, flex: 1}}>\n <Tooltip title={displayName}>\n <Typography variant=\"body2\" noWrap>{displayName}</Typography>\n </Tooltip>\n <Typography variant=\"caption\" component=\"p\" noWrap={!isFailed}\n sx={{color: isFailed ? 'error.main' : 'text.secondary'}}>\n {detailTextOf(download, t)}\n </Typography>\n </Box>\n <Box sx={{flexShrink: 0, display: 'flex', alignItems: 'center'}}>\n <DownloadStatusIndicator download={download} displayName={displayName} />\n </Box>\n </ListItem>\n );\n}\n\nexport default function DownloadManager() {\n const {t} = useToolkitTranslation('common');\n const downloads = useSelector(downloadsActor, (snapshot) => snapshot.context.downloads);\n const [isExpanded, setIsExpanded] = useState(true);\n const previousCountRef = useRef(downloads.length);\n\n const activeCount = countActiveDownloads(downloads);\n const allSucceeded = downloads.length > 0 && downloads.every((download) => download.status === DOWNLOAD_STATUS.completed);\n\n // una descarga nueva vuelve a desplegar el panel aunque se haya plegado\n useEffect(() => {\n if (downloads.length > previousCountRef.current) {\n setIsExpanded(true);\n }\n previousCountRef.current = downloads.length;\n }, [downloads.length]);\n\n // todo terminó bien: el panel se va solo (con un error o una cancelada se queda para que se lea)\n useEffect(() => {\n if (!allSucceeded) {\n return undefined;\n }\n const dismissTimer = setTimeout(clearFinishedDownloads, AUTO_DISMISS_MILLISECONDS);\n return () => clearTimeout(dismissTimer);\n }, [allSucceeded, downloads.length]);\n\n return (\n <Slide direction=\"up\" in={downloads.length > 0} mountOnEnter unmountOnExit>\n <Paper\n elevation={8}\n role=\"region\"\n aria-label={t('downloads.title')}\n sx={[{\n position: 'fixed',\n right: (theme) => theme.spacing(SCREEN_EDGE_SPACING),\n bottom: 0,\n width: (theme) => `min(${PANEL_WIDTH_PIXELS}px, calc(100vw - ${theme.spacing(SCREEN_EDGE_SPACING * 2)}))`,\n // pegado al borde de abajo como en Drive: solo se redondean las esquinas de arriba\n borderBottomLeftRadius: 0,\n borderBottomRightRadius: 0,\n overflow: 'hidden',\n zIndex: (theme) => theme.zIndex.snackbar,\n }, (theme) => (isGlassTheme(theme) ? glassStylesOf(theme) : {})]}\n >\n <Stack direction=\"row\" sx={{alignItems: 'center', gap: 0.5, pl: 2, pr: 1, py: 1, bgcolor: 'action.hover'}}>\n <Typography variant=\"subtitle2\" component=\"h2\" noWrap sx={{flex: 1}} aria-live=\"polite\">\n {summaryTextOf(downloads, t)}\n </Typography>\n <Tooltip title={isExpanded ? t('downloads.collapse') : t('downloads.expand')}>\n <IconButton size=\"small\" aria-expanded={isExpanded}\n aria-label={isExpanded ? t('downloads.collapse') : t('downloads.expand')}\n onClick={() => setIsExpanded((wasExpanded) => !wasExpanded)}>\n {isExpanded ? <ExpandMoreIcon fontSize=\"small\" /> : <ExpandLessIcon fontSize=\"small\" />}\n </IconButton>\n </Tooltip>\n {/* cerrar a media descarga dejaría el archivo sin aviso de que terminó: la X sale cuando ya nada baja */}\n {activeCount === 0 && (\n <Tooltip title={t('downloads.close')}>\n <IconButton size=\"small\" aria-label={t('downloads.close')} onClick={clearFinishedDownloads}>\n <CloseIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n )}\n </Stack>\n <Collapse in={isExpanded}>\n <Divider />\n {/* OJO: overflowY auto también prende el scroll horizontal ante 1 px de más; se apaga explícito */}\n <List dense disablePadding sx={{maxHeight: LIST_MAX_HEIGHT, overflowY: 'auto', overflowX: 'hidden'}}>\n {downloads.map((download, index) => (\n <Fragment key={download.id}>\n {index > 0 && <Divider component=\"li\" />}\n <DownloadRow download={download} />\n </Fragment>\n ))}\n </List>\n </Collapse>\n </Paper>\n </Slide>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DownloadManager.js","sources":["../../src/downloads/DownloadManager.jsx"],"sourcesContent":["// Panel de descargas al estilo de Google Drive: se pega abajo a la derecha SOLO mientras hay descargas, con una barra que\n// resume («Descargando 2 archivos…», «3 descargas listas») y la lista con el ícono del tipo de archivo y un anillo de\n// avance que termina en ✓. Con MUI y XState, sin Redux.\n//\n// Qué hace para no hacer ruido:\n// - no hay botón fijo: sin descargas no se pinta nada;\n// - se pliega a la barra con la flecha y se vuelve a abrir cuando entra otra descarga;\n// - cuando todo terminó bien se va solo a los AUTO_DISMISS_MILLISECONDS; si algo falló o se canceló se queda hasta que\n// la persona lo cierra, para que vea el motivo;\n// - la X solo aparece cuando ya nada descarga (cerrar a media descarga dejaría el archivo sin aviso de que terminó).\n//\n// Se monta UNA sola vez en la raíz de la aplicación. Lee la lista de downloadsActor; las descargas las arranca startDownload desde cada\n// pantalla. Los avisos (sonner) salen abajo a la izquierda, así que el panel va a la derecha.\n// Con el vidrio del tema (createToolkitTheme({glass: true})) el panel es de vidrio: flota sobre la pantalla.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {Fragment, useEffect, useRef, useState} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useSelector} from '@xstate/react';\nimport Box from '@mui/material/Box';\nimport CircularProgress from '@mui/material/CircularProgress';\nimport Collapse from '@mui/material/Collapse';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListItem from '@mui/material/ListItem';\nimport Paper from '@mui/material/Paper';\nimport Slide from '@mui/material/Slide';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport BlockOutlinedIcon from '@mui/icons-material/BlockOutlined';\nimport CheckCircleIcon from '@mui/icons-material/CheckCircle';\nimport CloseIcon from '@mui/icons-material/Close';\nimport ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';\nimport ExpandLessIcon from '@mui/icons-material/ExpandLess';\nimport ExpandMoreIcon from '@mui/icons-material/ExpandMore';\n\nimport {cancelDownload, clearFinishedDownloads, countActiveDownloads, downloadsActor} from '@/downloads/downloadQueue.js';\nimport {UNKNOWN_FILE_TYPE, useFileTypes} from '@/downloads/fileTypes.js';\nimport {DOWNLOAD_STATUS} from '@/downloads/downloadsMachine.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {glassStylesOf, isGlassTheme} from '@/theme/glassSurface.js';\nimport ComponentTexts from '@/toolkit/ComponentTexts.jsx';\n\n// cuánto se queda el panel después de que TODAS las descargas terminaron bien\nconst AUTO_DISMISS_MILLISECONDS = 5000;\n\n// separación del borde derecho y ancho del panel (en el teléfono ocupa el ancho menos los márgenes)\nconst SCREEN_EDGE_SPACING = 3;\nconst PANEL_WIDTH_PIXELS = 360;\nconst LIST_MAX_HEIGHT = '40vh';\nconst PROGRESS_RING_PIXELS = 22;\n\n/**\n * Tipo de archivo de una descarga: el que dijo quien la arrancó o, al terminar, la extensión del nombre. Los tipos son los de\n * la casa y los del proyecto (ToolkitProvider fileTypes).\n *\n * @param {object} download renglón de la lista (downloadsMachine)\n * @param {Record<string, {Icon: import('react').ElementType, color?: string}>} fileTypes\n * @returns {{Icon: import('react').ElementType, color: string}}\n */\nfunction fileTypeAppearanceOf(download, fileTypes) {\n const extension = download.fileName?.split('.').pop()?.toLowerCase();\n const fileType = fileTypes[download.format ?? extension];\n return fileType ? {Icon: fileType.Icon ?? UNKNOWN_FILE_TYPE.Icon, color: fileType.color ?? UNKNOWN_FILE_TYPE.color} : UNKNOWN_FILE_TYPE;\n}\n\n/**\n * Segunda línea del renglón: cuánto lleva, cuánto pesó o por qué no se pudo.\n *\n * @param {object} download\n * @param {Function} t traductor de common\n * @param {(bytes: number) => string} formatFileSize el de useFormatters (el idioma del proveedor más cercano)\n * @returns {string}\n */\nfunction detailTextOf(download, t, formatFileSize) {\n if (download.status === DOWNLOAD_STATUS.completed) {\n return formatFileSize(download.loadedBytes);\n }\n if (download.status === DOWNLOAD_STATUS.failed) {\n return download.errorMessage ?? t('downloads.failed');\n }\n if (download.status === DOWNLOAD_STATUS.canceled) {\n return t('downloads.canceled');\n }\n if (download.loadedBytes === 0) {\n return t('downloads.preparing');\n }\n // un archivo que se arma al vuelo (el log) no dice su tamaño: se enseña lo que ya bajó\n return download.totalBytes\n ? t('downloads.progressOf', {loaded: formatFileSize(download.loadedBytes), total: formatFileSize(download.totalBytes)})\n : t('downloads.progressLoaded', {loaded: formatFileSize(download.loadedBytes)});\n}\n\n/**\n * Resumen de la barra, como en Drive: lo que está pasando ahora o cómo terminó.\n *\n * @param {Array<object>} downloads\n * @param {Function} t traductor de common\n * @returns {string}\n */\nfunction summaryTextOf(downloads, t) {\n const activeCount = countActiveDownloads(downloads);\n if (activeCount > 0) {\n return t('downloads.summaryDownloading', {count: activeCount});\n }\n const failedCount = downloads.filter((download) => download.status === DOWNLOAD_STATUS.failed).length;\n if (failedCount > 0) {\n return t('downloads.summaryFailed', {count: failedCount});\n }\n const completedCount = downloads.filter((download) => download.status === DOWNLOAD_STATUS.completed).length;\n return t('downloads.summaryCompleted', {count: completedCount});\n}\n\n/**\n * Lo que va a la derecha del renglón: el anillo de avance con su botón para cancelar, o cómo terminó.\n *\n * @param {{download: object, displayName: string}} props\n */\nfunction DownloadStatusIndicator({download, displayName}) {\n const {t} = useToolkitTranslation('common');\n\n if (download.status === DOWNLOAD_STATUS.completed) {\n return <CheckCircleIcon sx={{color: 'success.main'}} aria-label={t('downloads.completedOf', {name: displayName})} />;\n }\n if (download.status === DOWNLOAD_STATUS.failed) {\n return <ErrorOutlineIcon sx={{color: 'error.main'}} aria-label={t('downloads.failedOf', {name: displayName})} />;\n }\n if (download.status === DOWNLOAD_STATUS.canceled) {\n return <BlockOutlinedIcon sx={{color: 'text.disabled'}} aria-label={t('downloads.canceledOf', {name: displayName})} />;\n }\n\n const hasKnownSize = Boolean(download.totalBytes);\n // el anillo y, encima, la X para cancelar: como en Drive, un solo lugar para ver y detener\n return (\n <Tooltip title={t('downloads.cancel')}>\n <IconButton size=\"small\" aria-label={t('downloads.cancelOf', {name: displayName})} onClick={() => cancelDownload(download.id)}\n sx={{position: 'relative', width: PROGRESS_RING_PIXELS + 10, height: PROGRESS_RING_PIXELS + 10}}>\n <CircularProgress\n size={PROGRESS_RING_PIXELS}\n thickness={4}\n variant={hasKnownSize ? 'determinate' : 'indeterminate'}\n value={hasKnownSize ? Math.min(100, (download.loadedBytes * 100) / download.totalBytes) : undefined}\n sx={{position: 'absolute'}}\n />\n <CloseIcon sx={{fontSize: 12}} />\n </IconButton>\n </Tooltip>\n );\n}\n\n/**\n * Un renglón: ícono del tipo de archivo, nombre, detalle y estatus.\n *\n * @param {{download: object}} props\n */\nfunction DownloadRow({download}) {\n const {t} = useToolkitTranslation('common');\n const {formatFileSize} = useFormatters();\n const {Icon, color} = fileTypeAppearanceOf(download, useFileTypes());\n const displayName = download.fileName ?? download.label;\n const isFailed = download.status === DOWNLOAD_STATUS.failed;\n\n return (\n // minWidth 0: sin él un nombre largo empuja el renglón y la lista enseña una barra horizontal\n <ListItem disableGutters sx={{gap: 1.5, px: 2, py: 1, minWidth: 0}}>\n <Icon fontSize=\"small\" sx={{color, flexShrink: 0}} />\n <Box sx={{minWidth: 0, flex: 1}}>\n <Tooltip title={displayName}>\n <Typography variant=\"body2\" noWrap>{displayName}</Typography>\n </Tooltip>\n <Typography variant=\"caption\" component=\"p\" noWrap={!isFailed}\n sx={{color: isFailed ? 'error.main' : 'text.secondary'}}>\n {detailTextOf(download, t, formatFileSize)}\n </Typography>\n </Box>\n <Box sx={{flexShrink: 0, display: 'flex', alignItems: 'center'}}>\n <DownloadStatusIndicator download={download} displayName={displayName} />\n </Box>\n </ListItem>\n );\n}\n\n/**\n * @param {object} [props]\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function DownloadManager({texts} = {}) {\n const {t} = useToolkitTranslation('common', texts);\n const downloads = useSelector(downloadsActor, (snapshot) => snapshot.context.downloads);\n const [isExpanded, setIsExpanded] = useState(true);\n const previousCountRef = useRef(downloads.length);\n\n const activeCount = countActiveDownloads(downloads);\n const allSucceeded = downloads.length > 0 && downloads.every((download) => download.status === DOWNLOAD_STATUS.completed);\n\n // una descarga nueva vuelve a desplegar el panel aunque se haya plegado\n useEffect(() => {\n if (downloads.length > previousCountRef.current) {\n setIsExpanded(true);\n }\n previousCountRef.current = downloads.length;\n }, [downloads.length]);\n\n // todo terminó bien: el panel se va solo (con un error o una cancelada se queda para que se lea)\n useEffect(() => {\n if (!allSucceeded) {\n return undefined;\n }\n const dismissTimer = setTimeout(clearFinishedDownloads, AUTO_DISMISS_MILLISECONDS);\n return () => clearTimeout(dismissTimer);\n }, [allSucceeded, downloads.length]);\n\n return (\n <ComponentTexts namespace=\"common\" texts={texts}>\n <Slide direction=\"up\" in={downloads.length > 0} mountOnEnter unmountOnExit>\n <Paper\n elevation={8}\n role=\"region\"\n aria-label={t('downloads.title')}\n sx={[{\n position: 'fixed',\n right: (theme) => theme.spacing(SCREEN_EDGE_SPACING),\n bottom: 0,\n width: (theme) => `min(${PANEL_WIDTH_PIXELS}px, calc(100vw - ${theme.spacing(SCREEN_EDGE_SPACING * 2)}))`,\n // pegado al borde de abajo como en Drive: solo se redondean las esquinas de arriba\n borderBottomLeftRadius: 0,\n borderBottomRightRadius: 0,\n overflow: 'hidden',\n zIndex: (theme) => theme.zIndex.snackbar,\n }, (theme) => (isGlassTheme(theme) ? glassStylesOf(theme) : {})]}\n >\n <Stack direction=\"row\" sx={{alignItems: 'center', gap: 0.5, pl: 2, pr: 1, py: 1, bgcolor: 'action.hover'}}>\n <Typography variant=\"subtitle2\" component=\"h2\" noWrap sx={{flex: 1}} aria-live=\"polite\">\n {summaryTextOf(downloads, t)}\n </Typography>\n <Tooltip title={isExpanded ? t('downloads.collapse') : t('downloads.expand')}>\n <IconButton size=\"small\" aria-expanded={isExpanded}\n aria-label={isExpanded ? t('downloads.collapse') : t('downloads.expand')}\n onClick={() => setIsExpanded((wasExpanded) => !wasExpanded)}>\n {isExpanded ? <ExpandMoreIcon fontSize=\"small\" /> : <ExpandLessIcon fontSize=\"small\" />}\n </IconButton>\n </Tooltip>\n {/* cerrar a media descarga dejaría el archivo sin aviso de que terminó: la X sale cuando ya nada baja */}\n {activeCount === 0 && (\n <Tooltip title={t('downloads.close')}>\n <IconButton size=\"small\" aria-label={t('downloads.close')} onClick={clearFinishedDownloads}>\n <CloseIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n )}\n </Stack>\n <Collapse in={isExpanded}>\n <Divider />\n {/* OJO: overflowY auto también prende el scroll horizontal ante 1 px de más; se apaga explícito */}\n <List dense disablePadding sx={{maxHeight: LIST_MAX_HEIGHT, overflowY: 'auto', overflowX: 'hidden'}}>\n {downloads.map((download, index) => (\n <Fragment key={download.id}>\n {index > 0 && <Divider component=\"li\" />}\n <DownloadRow download={download} />\n </Fragment>\n ))}\n </List>\n </Collapse>\n </Paper>\n </Slide>\n </ComponentTexts>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,MAAM,4BAA4B;AAGlC,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;AAC3B,MAAM,kBAAkB;AACxB,MAAM,uBAAuB;AAU7B,SAAS,qBAAqB,UAAU,WAAW;AAC/C,QAAM,YAAY,SAAS,UAAU,MAAM,GAAG,EAAE,IAAA,GAAO,YAAA;AACvD,QAAM,WAAW,UAAU,SAAS,UAAU,SAAS;AACvD,SAAO,WAAW,EAAC,MAAM,SAAS,QAAQ,kBAAkB,MAAM,OAAO,SAAS,SAAS,kBAAkB,MAAA,IAAS;AAC1H;AAUA,SAAS,aAAa,UAAU,GAAG,gBAAgB;AAC/C,MAAI,SAAS,WAAW,gBAAgB,WAAW;AAC/C,WAAO,eAAe,SAAS,WAAW;AAAA,EAC9C;AACA,MAAI,SAAS,WAAW,gBAAgB,QAAQ;AAC5C,WAAO,SAAS,gBAAgB,EAAE,kBAAkB;AAAA,EACxD;AACA,MAAI,SAAS,WAAW,gBAAgB,UAAU;AAC9C,WAAO,EAAE,oBAAoB;AAAA,EACjC;AACA,MAAI,SAAS,gBAAgB,GAAG;AAC5B,WAAO,EAAE,qBAAqB;AAAA,EAClC;AAEA,SAAO,SAAS,aACV,EAAE,wBAAwB,EAAC,QAAQ,eAAe,SAAS,WAAW,GAAG,OAAO,eAAe,SAAS,UAAU,EAAA,CAAE,IACpH,EAAE,4BAA4B,EAAC,QAAQ,eAAe,SAAS,WAAW,GAAE;AACtF;AASA,SAAS,cAAc,WAAW,GAAG;AACjC,QAAM,cAAc,qBAAqB,SAAS;AAClD,MAAI,cAAc,GAAG;AACjB,WAAO,EAAE,gCAAgC,EAAC,OAAO,aAAY;AAAA,EACjE;AACA,QAAM,cAAc,UAAU,OAAO,CAAC,aAAa,SAAS,WAAW,gBAAgB,MAAM,EAAE;AAC/F,MAAI,cAAc,GAAG;AACjB,WAAO,EAAE,2BAA2B,EAAC,OAAO,aAAY;AAAA,EAC5D;AACA,QAAM,iBAAiB,UAAU,OAAO,CAAC,aAAa,SAAS,WAAW,gBAAgB,SAAS,EAAE;AACrG,SAAO,EAAE,8BAA8B,EAAC,OAAO,gBAAe;AAClE;AAOA,SAAS,wBAAwB,EAAC,UAAU,eAAc;AACtD,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ;AAE1C,MAAI,SAAS,WAAW,gBAAgB,WAAW;AAC/C,WAAO,oBAAC,iBAAA,EAAgB,IAAI,EAAC,OAAO,eAAA,GAAiB,cAAY,EAAE,yBAAyB,EAAC,MAAM,YAAA,CAAY,EAAA,CAAG;AAAA,EACtH;AACA,MAAI,SAAS,WAAW,gBAAgB,QAAQ;AAC5C,WAAO,oBAAC,kBAAA,EAAiB,IAAI,EAAC,OAAO,aAAA,GAAe,cAAY,EAAE,sBAAsB,EAAC,MAAM,YAAA,CAAY,EAAA,CAAG;AAAA,EAClH;AACA,MAAI,SAAS,WAAW,gBAAgB,UAAU;AAC9C,WAAO,oBAAC,mBAAA,EAAkB,IAAI,EAAC,OAAO,gBAAA,GAAkB,cAAY,EAAE,wBAAwB,EAAC,MAAM,YAAA,CAAY,EAAA,CAAG;AAAA,EACxH;AAEA,QAAM,eAAe,QAAQ,SAAS,UAAU;AAEhD,SACI,oBAAC,SAAA,EAAQ,OAAO,EAAE,kBAAkB,GAChC,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MAAW,MAAK;AAAA,MAAQ,cAAY,EAAE,sBAAsB,EAAC,MAAM,aAAY;AAAA,MAAG,SAAS,MAAM,eAAe,SAAS,EAAE;AAAA,MAChH,IAAI,EAAC,UAAU,YAAY,OAAO,uBAAuB,IAAI,QAAQ,uBAAuB,GAAA;AAAA,MACpG,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACG,MAAM;AAAA,YACN,WAAW;AAAA,YACX,SAAS,eAAe,gBAAgB;AAAA,YACxC,OAAO,eAAe,KAAK,IAAI,KAAM,SAAS,cAAc,MAAO,SAAS,UAAU,IAAI;AAAA,YAC1F,IAAI,EAAC,UAAU,WAAA;AAAA,UAAU;AAAA,QAAA;AAAA,4BAE5B,WAAA,EAAU,IAAI,EAAC,UAAU,KAAE,CAAG;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAEvC;AAER;AAOA,SAAS,YAAY,EAAC,YAAW;AAC7B,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ;AAC1C,QAAM,EAAC,eAAA,IAAkB,cAAA;AACzB,QAAM,EAAC,MAAM,MAAA,IAAS,qBAAqB,UAAU,cAAc;AACnE,QAAM,cAAc,SAAS,YAAY,SAAS;AAClD,QAAM,WAAW,SAAS,WAAW,gBAAgB;AAErD;AAAA;AAAA,IAEI,qBAAC,UAAA,EAAS,gBAAc,MAAC,IAAI,EAAC,KAAK,KAAK,IAAI,GAAG,IAAI,GAAG,UAAU,KAC5D,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAK,UAAS,SAAQ,IAAI,EAAC,OAAO,YAAY,KAAI;AAAA,MACnD,qBAAC,OAAI,IAAI,EAAC,UAAU,GAAG,MAAM,KACzB,UAAA;AAAA,QAAA,oBAAC,SAAA,EAAQ,OAAO,aACZ,UAAA,oBAAC,YAAA,EAAW,SAAQ,SAAQ,QAAM,MAAE,UAAA,YAAA,CAAY,GACpD;AAAA,QACA;AAAA,UAAC;AAAA,UAAA;AAAA,YAAW,SAAQ;AAAA,YAAU,WAAU;AAAA,YAAI,QAAQ,CAAC;AAAA,YACzC,IAAI,EAAC,OAAO,WAAW,eAAe,iBAAA;AAAA,YAC7C,UAAA,aAAa,UAAU,GAAG,cAAc;AAAA,UAAA;AAAA,QAAA;AAAA,MAC7C,GACJ;AAAA,MACA,oBAAC,KAAA,EAAI,IAAI,EAAC,YAAY,GAAG,SAAS,QAAQ,YAAY,YAClD,UAAA,oBAAC,yBAAA,EAAwB,UAAoB,aAA0B,EAAA,CAC3E;AAAA,IAAA,EAAA,CACJ;AAAA;AAER;AAMA,SAAwB,gBAAgB,EAAC,MAAA,IAAS,IAAI;AAClD,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AACjD,QAAM,YAAY,YAAY,gBAAgB,CAAC,aAAa,SAAS,QAAQ,SAAS;AACtF,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,IAAI;AACjD,QAAM,mBAAmB,OAAO,UAAU,MAAM;AAEhD,QAAM,cAAc,qBAAqB,SAAS;AAClD,QAAM,eAAe,UAAU,SAAS,KAAK,UAAU,MAAM,CAAC,aAAa,SAAS,WAAW,gBAAgB,SAAS;AAGxH,YAAU,MAAM;AACZ,QAAI,UAAU,SAAS,iBAAiB,SAAS;AAC7C,oBAAc,IAAI;AAAA,IACtB;AACA,qBAAiB,UAAU,UAAU;AAAA,EACzC,GAAG,CAAC,UAAU,MAAM,CAAC;AAGrB,YAAU,MAAM;AACZ,QAAI,CAAC,cAAc;AACf,aAAO;AAAA,IACX;AACA,UAAM,eAAe,WAAW,wBAAwB,yBAAyB;AACjF,WAAO,MAAM,aAAa,YAAY;AAAA,EAC1C,GAAG,CAAC,cAAc,UAAU,MAAM,CAAC;AAEnC,6BACK,gBAAA,EAAe,WAAU,UAAS,OAC/B,8BAAC,OAAA,EAAM,WAAU,MAAK,IAAI,UAAU,SAAS,GAAG,cAAY,MAAC,eAAa,MACtE,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACG,WAAW;AAAA,MACX,MAAK;AAAA,MACL,cAAY,EAAE,iBAAiB;AAAA,MAC/B,IAAI,CAAC;AAAA,QACD,UAAU;AAAA,QACV,OAAO,CAAC,UAAU,MAAM,QAAQ,mBAAmB;AAAA,QACnD,QAAQ;AAAA,QACR,OAAO,CAAC,UAAU,OAAO,kBAAkB,oBAAoB,MAAM,QAAQ,sBAAsB,CAAC,CAAC;AAAA;AAAA,QAErG,wBAAwB;AAAA,QACxB,yBAAyB;AAAA,QACzB,UAAU;AAAA,QACV,QAAQ,CAAC,UAAU,MAAM,OAAO;AAAA,MAAA,GACjC,CAAC,UAAW,aAAa,KAAK,IAAI,cAAc,KAAK,IAAI,EAAG;AAAA,MAE/D,UAAA;AAAA,QAAA,qBAAC,SAAM,WAAU,OAAM,IAAI,EAAC,YAAY,UAAU,KAAK,KAAK,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,SAAS,kBACtF,UAAA;AAAA,UAAA,oBAAC,cAAW,SAAQ,aAAY,WAAU,MAAK,QAAM,MAAC,IAAI,EAAC,MAAM,KAAI,aAAU,UAC1E,UAAA,cAAc,WAAW,CAAC,GAC/B;AAAA,UACA,oBAAC,WAAQ,OAAO,aAAa,EAAE,oBAAoB,IAAI,EAAE,kBAAkB,GACvE,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cAAW,MAAK;AAAA,cAAQ,iBAAe;AAAA,cAC5B,cAAY,aAAa,EAAE,oBAAoB,IAAI,EAAE,kBAAkB;AAAA,cACvE,SAAS,MAAM,cAAc,CAAC,gBAAgB,CAAC,WAAW;AAAA,cACjE,UAAA,iCAAc,gBAAA,EAAe,UAAS,SAAQ,IAAK,oBAAC,gBAAA,EAAe,UAAS,QAAA,CAAQ;AAAA,YAAA;AAAA,UAAA,GAE7F;AAAA,UAEC,gBAAgB,KACb,oBAAC,SAAA,EAAQ,OAAO,EAAE,iBAAiB,GAC/B,UAAA,oBAAC,YAAA,EAAW,MAAK,SAAQ,cAAY,EAAE,iBAAiB,GAAG,SAAS,wBAChE,8BAAC,WAAA,EAAU,UAAS,QAAA,CAAQ,EAAA,CAChC,EAAA,CACJ;AAAA,QAAA,GAER;AAAA,QACA,qBAAC,UAAA,EAAS,IAAI,YACV,UAAA;AAAA,UAAA,oBAAC,SAAA,EAAQ;AAAA,UAET,oBAAC,QAAK,OAAK,MAAC,gBAAc,MAAC,IAAI,EAAC,WAAW,iBAAiB,WAAW,QAAQ,WAAW,YACrF,UAAA,UAAU,IAAI,CAAC,UAAU,UACtB,qBAAC,UAAA,EACI,UAAA;AAAA,YAAA,QAAQ,KAAK,oBAAC,SAAA,EAAQ,WAAU,MAAK;AAAA,YACtC,oBAAC,eAAY,SAAA,CAAoB;AAAA,UAAA,KAFtB,SAAS,EAGxB,CACH,EAAA,CACL;AAAA,QAAA,EAAA,CACJ;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAER,EAAA,CACJ;AAER;"}
|
|
@@ -4,7 +4,7 @@ import { saveBlob } from "../files.js";
|
|
|
4
4
|
import { newIdentifier } from "../identifiers.js";
|
|
5
5
|
const downloadsActor = createActor(downloadsMachine).start();
|
|
6
6
|
const abortControllerByDownloadId = /* @__PURE__ */ new Map();
|
|
7
|
-
async function startDownload({ label, format, request, onCompleted }) {
|
|
7
|
+
async function startDownload({ label, format, request, onCompleted, onFailed, onCanceled }) {
|
|
8
8
|
const downloadId = newIdentifier();
|
|
9
9
|
const abortController = new AbortController();
|
|
10
10
|
abortControllerByDownloadId.set(downloadId, abortController);
|
|
@@ -22,13 +22,15 @@ async function startDownload({ label, format, request, onCompleted }) {
|
|
|
22
22
|
})
|
|
23
23
|
});
|
|
24
24
|
saveBlob(blob, fileName);
|
|
25
|
-
downloadsActor.send({ type: "COMPLETED", id: downloadId, fileName });
|
|
25
|
+
downloadsActor.send({ type: "COMPLETED", id: downloadId, fileName, sizeBytes: blob.size });
|
|
26
26
|
onCompleted?.(fileName);
|
|
27
27
|
} catch (error) {
|
|
28
28
|
if (abortController.signal.aborted) {
|
|
29
29
|
downloadsActor.send({ type: "CANCELED", id: downloadId });
|
|
30
|
+
onCanceled?.();
|
|
30
31
|
} else {
|
|
31
32
|
downloadsActor.send({ type: "FAILED", id: downloadId, errorMessage: error?.message ?? null });
|
|
33
|
+
onFailed?.(error);
|
|
32
34
|
}
|
|
33
35
|
} finally {
|
|
34
36
|
abortControllerByDownloadId.delete(downloadId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"downloadQueue.js","sources":["../../src/downloads/downloadQueue.js"],"sourcesContent":["// Arranca descargas y las anota en la lista que pinta DownloadManager (el administrador de descargas).\n//\n// Uso (desde un módulo, que es quien conoce el API):\n// startDownload({\n// label: t('detail.downloadLog'),\n// request: (transferOptions) => ingestionApi.downloadProcessingLog(batchId, transferOptions),\n// onCompleted: () => refreshPeriods(),\n// });\n//\n// startDownload no espera a que termine quien la llama: el diálogo de confirmación se cierra en cuanto la descarga entra\n// a la lista y la persona sigue trabajando mientras baja (el log más grande tarda ~30 s). El panel (tipo Google Drive) es\n// el ÚNICO lugar que avisa: avance, lista y error con su texto del API (RFC 9457). Sin aviso aparte, para no avisar dos\n// veces lo mismo.\n//\n// Una sola lista para toda la app (un actor de módulo, como el cliente de createApiClient): la llenan varias pantallas y\n// la lee un solo panel montado en la raíz de la aplicación.\n//\n// Funciones puras del paquete, no renderizan: function (regla de la casa). No conoce el API: el archivo lo pide la\n// `request` que le pasan.\nimport {createActor} from 'xstate';\n\nimport {DOWNLOAD_STATUS, downloadsMachine} from '@/downloads/downloadsMachine.js';\nimport {saveBlob} from '@/files.js';\nimport {newIdentifier} from '@/identifiers.js';\n\nexport const downloadsActor = createActor(downloadsMachine).start();\n\n// cómo cancelar cada descarga en curso; vive fuera del contexto de la máquina porque un AbortController no es un dato\nconst abortControllerByDownloadId = new Map();\n\n/**\n * Descarga un archivo con avance en el panel, lo guarda en disco y avisa al terminar.\n *\n * @param {object} options\n * @param {string} options.label ya traducido; lo que se ve en el panel mientras todavía no se sabe el nombre del\n * archivo\n * @param {
|
|
1
|
+
{"version":3,"file":"downloadQueue.js","sources":["../../src/downloads/downloadQueue.js"],"sourcesContent":["// Arranca descargas y las anota en la lista que pinta DownloadManager (el administrador de descargas).\n//\n// Uso (desde un módulo, que es quien conoce el API):\n// startDownload({\n// label: t('detail.downloadLog'),\n// request: (transferOptions) => ingestionApi.downloadProcessingLog(batchId, transferOptions),\n// onCompleted: () => refreshPeriods(),\n// });\n//\n// startDownload no espera a que termine quien la llama: el diálogo de confirmación se cierra en cuanto la descarga entra\n// a la lista y la persona sigue trabajando mientras baja (el log más grande tarda ~30 s). El panel (tipo Google Drive) es\n// el ÚNICO lugar que avisa: avance, lista y error con su texto del API (RFC 9457). Sin aviso aparte, para no avisar dos\n// veces lo mismo.\n//\n// Una sola lista para toda la app (un actor de módulo, como el cliente de createApiClient): la llenan varias pantallas y\n// la lee un solo panel montado en la raíz de la aplicación.\n//\n// Funciones puras del paquete, no renderizan: function (regla de la casa). No conoce el API: el archivo lo pide la\n// `request` que le pasan.\nimport {createActor} from 'xstate';\n\nimport {DOWNLOAD_STATUS, downloadsMachine} from '@/downloads/downloadsMachine.js';\nimport {saveBlob} from '@/files.js';\nimport {newIdentifier} from '@/identifiers.js';\n\nexport const downloadsActor = createActor(downloadsMachine).start();\n\n// cómo cancelar cada descarga en curso; vive fuera del contexto de la máquina porque un AbortController no es un dato\nconst abortControllerByDownloadId = new Map();\n\n/**\n * Descarga un archivo con avance en el panel, lo guarda en disco y avisa al terminar.\n *\n * @param {object} options\n * @param {string} options.label ya traducido; lo que se ve en el panel mientras todavía no se sabe el nombre del\n * archivo\n * @param {string} [options.format] el tipo de archivo, para su ícono en el panel: xlsx, csv, pdf o uno del proyecto\n * (ToolkitProvider fileTypes)\n * @param {(transferOptions: {signal: AbortSignal, onDownloadProgress: (progress: {loaded: number, total?: number|null}) => void}) => Promise<{blob: Blob, fileName: string}>} options.request\n * @param {(fileName: string) => void} [options.onCompleted] p. ej. volver a pedir la tabla (si la descarga cambia algo\n * en el servidor)\n * @param {(error: unknown) => void} [options.onFailed] cuando no se pudo (el panel ya lo dice; para avisar o reportarlo)\n * @param {() => void} [options.onCanceled] cuando la persona la canceló\n * @returns {Promise<string>} id de la descarga; la promesa se cumple cuando termina (bien, mal o cancelada), nunca falla\n */\nexport async function startDownload({label, format, request, onCompleted, onFailed, onCanceled}) {\n const downloadId = newIdentifier();\n const abortController = new AbortController();\n abortControllerByDownloadId.set(downloadId, abortController);\n downloadsActor.send({type: 'STARTED', id: downloadId, label, format});\n\n try {\n const {blob, fileName} = await request({\n signal: abortController.signal,\n // la petición avisa cada pedazo que llega; total viene en null cuando el servidor arma el archivo al vuelo (sin\n // Content-Length) y el panel enseña lo que lleva en vez del porcentaje\n onDownloadProgress: (progressEvent) => downloadsActor.send({\n type: 'PROGRESSED', id: downloadId, loadedBytes: progressEvent.loaded, totalBytes: progressEvent.total,\n }),\n });\n saveBlob(blob, fileName);\n downloadsActor.send({type: 'COMPLETED', id: downloadId, fileName, sizeBytes: blob.size});\n onCompleted?.(fileName);\n } catch (error) {\n // OJO: cancelar también llega aquí como error (el AbortError del navegador); se distingue por la señal, no por el\n // tipo de error, porque cada `request` del proyecto puede tronar con el suyo\n if (abortController.signal.aborted) {\n downloadsActor.send({type: 'CANCELED', id: downloadId});\n onCanceled?.();\n } else {\n // el mensaje ya viene traducido del API (ApiError con el detail de RFC 9457)\n downloadsActor.send({type: 'FAILED', id: downloadId, errorMessage: error?.message ?? null});\n onFailed?.(error);\n }\n } finally {\n abortControllerByDownloadId.delete(downloadId);\n }\n\n return downloadId;\n}\n\n/**\n * Cancela una descarga en curso (el panel la marca como cancelada).\n *\n * @param {string} downloadId\n */\nexport function cancelDownload(downloadId) {\n abortControllerByDownloadId.get(downloadId)?.abort();\n}\n\n/** Quita del panel todas las descargas terminadas (completadas, fallidas o canceladas). */\nexport function clearFinishedDownloads() {\n downloadsActor.send({type: 'CLEAR_FINISHED'});\n}\n\n/**\n * ¿Cuántas siguen descargando? Decide el resumen del panel y si se puede cerrar.\n *\n * @param {Array<object>} downloads\n * @returns {number}\n */\nexport function countActiveDownloads(downloads) {\n return downloads.filter((download) => download.status === DOWNLOAD_STATUS.downloading).length;\n}\n"],"names":[],"mappings":";;;;AAyBY,MAAC,iBAAiB,YAAY,gBAAgB,EAAE,MAAK;AAGjE,MAAM,8BAA8B,oBAAI,IAAG;AAiBpC,eAAe,cAAc,EAAC,OAAO,QAAQ,SAAS,aAAa,UAAU,WAAU,GAAG;AAC7F,QAAM,aAAa,cAAa;AAChC,QAAM,kBAAkB,IAAI,gBAAe;AAC3C,8BAA4B,IAAI,YAAY,eAAe;AAC3D,iBAAe,KAAK,EAAC,MAAM,WAAW,IAAI,YAAY,OAAO,OAAM,CAAC;AAEpE,MAAI;AACA,UAAM,EAAC,MAAM,SAAQ,IAAI,MAAM,QAAQ;AAAA,MACnC,QAAQ,gBAAgB;AAAA;AAAA;AAAA,MAGxB,oBAAoB,CAAC,kBAAkB,eAAe,KAAK;AAAA,QACvD,MAAM;AAAA,QAAc,IAAI;AAAA,QAAY,aAAa,cAAc;AAAA,QAAQ,YAAY,cAAc;AAAA,MACjH,CAAa;AAAA,IACb,CAAS;AACD,aAAS,MAAM,QAAQ;AACvB,mBAAe,KAAK,EAAC,MAAM,aAAa,IAAI,YAAY,UAAU,WAAW,KAAK,KAAI,CAAC;AACvF,kBAAc,QAAQ;AAAA,EAC1B,SAAS,OAAO;AAGZ,QAAI,gBAAgB,OAAO,SAAS;AAChC,qBAAe,KAAK,EAAC,MAAM,YAAY,IAAI,WAAU,CAAC;AACtD,mBAAU;AAAA,IACd,OAAO;AAEH,qBAAe,KAAK,EAAC,MAAM,UAAU,IAAI,YAAY,cAAc,OAAO,WAAW,KAAI,CAAC;AAC1F,iBAAW,KAAK;AAAA,IACpB;AAAA,EACJ,UAAC;AACG,gCAA4B,OAAO,UAAU;AAAA,EACjD;AAEA,SAAO;AACX;AAOO,SAAS,eAAe,YAAY;AACvC,8BAA4B,IAAI,UAAU,GAAG,MAAK;AACtD;AAGO,SAAS,yBAAyB;AACrC,iBAAe,KAAK,EAAC,MAAM,iBAAgB,CAAC;AAChD;AAQO,SAAS,qBAAqB,WAAW;AAC5C,SAAO,UAAU,OAAO,CAAC,aAAa,SAAS,WAAW,gBAAgB,WAAW,EAAE;AAC3F;"}
|
|
@@ -44,9 +44,12 @@ const downloadsMachine = setup({}).createMachine({
|
|
|
44
44
|
},
|
|
45
45
|
COMPLETED: {
|
|
46
46
|
actions: assign({
|
|
47
|
+
// con el tamaño del archivo ya guardado: un archivo armado en el navegador (CSV, JSON) no avisa avance y el
|
|
48
|
+
// panel decía «0 bytes» de un archivo con datos
|
|
47
49
|
downloads: ({ context, event }) => updateActiveDownload(context.downloads, event.id, {
|
|
48
50
|
status: DOWNLOAD_STATUS.completed,
|
|
49
|
-
fileName: event.fileName
|
|
51
|
+
fileName: event.fileName,
|
|
52
|
+
...typeof event.sizeBytes === "number" && { loadedBytes: event.sizeBytes, totalBytes: event.sizeBytes }
|
|
50
53
|
})
|
|
51
54
|
})
|
|
52
55
|
},
|