@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":"createApiClient.js","sources":["../../src/api/createApiClient.js"],"sourcesContent":["// La petición de la casa: TODO lo que habla con el servidor pasa por un cliente armado aquí, con fetch del navegador (sin\n// axios ni otra librería). Lo que resuelve UNA vez y no en cada pantalla:\n// - Accept-Language con el idioma vigente de ToolkitProvider en CADA petición: el servidor responde title y detail ya\n// traducidos. Cambiar el idioma aplica a la siguiente petición sin recargar.\n// - errores como ApiError (ApiError.js): el problema RFC 9457 del servidor con el mensaje ya traducido; si la\n// respuesta llegó en otro idioma (un error que armó nginx), el texto genérico del paquete. Sin red o se acabó el\n// tiempo: un ApiError con status 0, la misma forma, para que la pantalla no tenga dos formas de error.\n// - cancelar NO es un error: el AbortError se relanza intacto (quien canceló ya sabe por qué).\n// - 401 (la sesión se venció a media consulta): avisa a onUnauthenticated, que normalmente lleva a iniciar sesión. Una\n// petición que espera el 401 (preguntar si hay sesión al arrancar, el login) pasa skipUnauthenticatedHandler.\n// - el aviso «Hay una versión nueva»: cada respuesta trae en un header qué compilación del front está publicada y se\n// le entrega a reportPublishedBuild.\n// - Laravel (Sanctum): con csrf, manda la cookie XSRF-TOKEN en su header (solo al mismo origen) y ante un 419 pide una\n// cookie nueva y repite la petición UNA vez.\n// - cuerpo objeto → JSON; FormData, Blob y URLSearchParams van tal cual (el navegador pone su boundary).\n// - descargas: el Blob y el nombre que mandó el servidor, con avance; un Excel que llegó cortado (el servidor falló a\n// media escritura después de responder 200) no se entrega.\n//\n// const api = createApiClient({baseUrl: '/api/v1', onUnauthenticated: () => navigate('/sign-in')});\n// const orders = await api.get('/orders', {params: {page: 2}, signal});\n// await api.post('/orders', {customerId: 7});\n//\n// Se arma uno por proyecto (en su core) y las pantallas lo usan desde su *Api.js.\n//\n// Funciones puras, no renderizan: function (regla de la casa).\nimport {ApiError, API_ERROR_STATUS} from '@/api/ApiError.js';\nimport {XLSX_CONTENT_TYPE, fileNameFromContentDisposition, isCompleteZip} from '@/files.js';\nimport {CSRF_TOKEN_MISMATCH_STATUS} from '@/laravel/csrfRetry.js';\nimport {createLogger} from '@/observability/logger.js';\nimport {reportPublishedBuild} from '@/observability/publishedBuild.js';\nimport {DEFAULT_LANGUAGE, toolkitSettings} from '@/toolkit/toolkitSettings.js';\n\nconst log = createLogger('api');\n\n/** Cómo se lee una respuesta que salió bien. */\nexport const API_RESPONSE_PARSE = Object.freeze({json: 'json', blob: 'blob', response: 'response'});\n\n// Las consultas se cortan a los 60 s; las descargas van sin límite (0): el servidor arma el archivo mientras lo manda y\n// uno grande no cabe en 60 s. No se queda colgada: se cancela desde el panel de descargas.\nconst DEFAULT_TIMEOUT_MILLISECONDS = 60_000;\nconst NO_TIMEOUT = 0;\nconst DEFAULT_PUBLISHED_BUILD_HEADER = 'X-Frontend-Build';\nconst NO_CONTENT_STATUS = 204;\nconst SERVER_ERROR_STATUS = 500;\n\n/**\n * La dirección completa: la base del cliente más el camino, con los parámetros. Un camino que ya trae http(s) va tal cual.\n * Los parámetros vacíos (undefined, null) no viajan; una lista viaja repetida (estado=a&estado=b).\n *\n * @param {string} baseUrl\n * @param {string} path\n * @param {Record<string, unknown>} [params]\n * @returns {string}\n */\nexport function apiUrlOf(baseUrl, path, params) {\n const url = /^https?:/i.test(path) ? path : `${baseUrl}${path}`;\n const query = new URLSearchParams();\n for (const [name, value] of Object.entries(params ?? {})) {\n for (const item of [].concat(value)) {\n if (item !== undefined && item !== null) {\n query.append(name, String(item));\n }\n }\n }\n const queryText = query.toString();\n if (!queryText) {\n return url;\n }\n return `${url}${url.includes('?') ? '&' : '?'}${queryText}`;\n}\n\n/**\n * ¿El cuerpo va tal cual? FormData (archivos), Blob y URLSearchParams los arma el navegador con su propio tipo.\n *\n * @param {unknown} body\n * @returns {boolean}\n */\nfunction isRawBody(body) {\n return (typeof FormData !== 'undefined' && body instanceof FormData)\n || (typeof Blob !== 'undefined' && body instanceof Blob)\n || body instanceof URLSearchParams\n || body instanceof ArrayBuffer;\n}\n\n/**\n * El valor de una cookie del navegador (la XSRF-TOKEN de Laravel), o null.\n *\n * @param {string} cookieName\n * @returns {string|null}\n */\nfunction readCookie(cookieName) {\n if (typeof document === 'undefined') {\n return null;\n }\n const prefix = `${cookieName}=`;\n const cookie = document.cookie.split(';').map((part) => part.trim()).find((part) => part.startsWith(prefix));\n return cookie ? decodeURIComponent(cookie.slice(prefix.length)) : null;\n}\n\n/**\n * ¿La dirección es del mismo origen que la página? El token CSRF solo viaja al servidor propio: mandarlo a otro dominio\n * se lo regalaría.\n *\n * @param {string} url\n * @returns {boolean}\n */\nfunction isSameOrigin(url) {\n if (typeof window === 'undefined' || !/^https?:/i.test(url)) {\n return true;\n }\n return new URL(url).origin === window.location.origin;\n}\n\n/**\n * Retry-After en segundos: el estándar admite segundos o una fecha HTTP.\n *\n * @param {string|null} retryAfterHeader\n * @returns {number|null}\n */\nexport function retryAfterSecondsOf(retryAfterHeader) {\n if (!retryAfterHeader) {\n return null;\n }\n if (/^\\d+$/.test(retryAfterHeader.trim())) {\n return Number(retryAfterHeader.trim());\n }\n const retryAt = Date.parse(retryAfterHeader);\n return Number.isNaN(retryAt) ? null : Math.max(0, Math.ceil((retryAt - Date.now()) / 1000));\n}\n\n/**\n * El cuerpo RFC 9457 de una respuesta de error, o null si no es JSON (un 502 del proxy en HTML).\n *\n * @param {Response} response\n * @returns {Promise<object|null>}\n */\nasync function readProblem(response) {\n try {\n const body = JSON.parse(await response.text());\n return body && typeof body === 'object' && !Array.isArray(body) ? body : null;\n } catch {\n return null;\n }\n}\n\n/**\n * La señal de la petición: la de quien la pidió (para cancelar) y la del tiempo máximo, juntas.\n *\n * @param {AbortSignal|undefined} signal\n * @param {number} timeoutMilliseconds 0 = sin límite\n * @returns {AbortSignal|undefined}\n */\nfunction combinedSignalOf(signal, timeoutMilliseconds) {\n const signals = [signal, timeoutMilliseconds > 0 ? AbortSignal.timeout(timeoutMilliseconds) : null].filter(Boolean);\n if (signals.length < 2) {\n return signals[0];\n }\n return AbortSignal.any(signals);\n}\n\n/**\n * Lee el cuerpo de una descarga avisando cuánto lleva. total viene en null cuando el servidor arma el archivo al vuelo\n * (sin Content-Length).\n *\n * @param {Response} response\n * @param {((progress: {loaded: number, total: number|null}) => void)|undefined} onDownloadProgress\n * @returns {Promise<Blob>}\n */\nasync function readBlobWithProgress(response, onDownloadProgress) {\n if (!onDownloadProgress || !response.body) {\n return response.blob();\n }\n const total = Number(response.headers.get('content-length')) || null;\n const reader = response.body.getReader();\n const chunks = [];\n let loaded = 0;\n for (let part = await reader.read(); !part.done; part = await reader.read()) {\n chunks.push(part.value);\n loaded += part.value.byteLength;\n onDownloadProgress({loaded, total});\n }\n return new Blob(chunks, {type: response.headers.get('content-type') ?? ''});\n}\n\n/**\n * Lo que acepta cada petición.\n *\n * @typedef {object} ApiRequestOptions\n * @property {unknown} [body] objeto → JSON; FormData, Blob o URLSearchParams tal cual\n * @property {Record<string, unknown>} [params] los de la dirección (?page=2)\n * @property {AbortSignal} [signal] para cancelar\n * @property {Record<string, string>} [headers]\n * @property {'json'|'blob'|'response'} [parse] cómo se lee la respuesta (API_RESPONSE_PARSE); por omisión JSON\n * @property {boolean} [skipUnauthenticatedHandler] un 401 que se espera (¿hay sesión?, el login)\n * @property {number} [timeoutMilliseconds] el de esta petición\n */\n\n/**\n * Lo que acepta una descarga.\n *\n * @typedef {object} ApiDownloadOptions\n * @property {Record<string, unknown>} [params]\n * @property {AbortSignal} [signal]\n * @property {(progress: {loaded: number, total: number|null}) => void} [onDownloadProgress]\n * @property {string} [fallbackFileName] si el servidor no manda el nombre\n * @property {number} [timeoutMilliseconds] por omisión sin límite\n */\n\n/**\n * El cliente del proyecto: cada método regresa el JSON de la respuesta (null si vino vacía) o truena con un ApiError.\n *\n * @typedef {object} ApiClient\n * @property {(method: string, path: string, options?: ApiRequestOptions) => Promise<any>} request\n * @property {(path: string, options?: ApiRequestOptions) => Promise<any>} get\n * @property {(path: string, body?: unknown, options?: ApiRequestOptions) => Promise<any>} post\n * @property {(path: string, body?: unknown, options?: ApiRequestOptions) => Promise<any>} put\n * @property {(path: string, body?: unknown, options?: ApiRequestOptions) => Promise<any>} patch\n * @property {(path: string, options?: ApiRequestOptions) => Promise<any>} delete\n * @property {(path: string, options?: ApiDownloadOptions) => Promise<{blob: Blob, fileName: string}>} download\n */\n\n/**\n * Arma el cliente del proyecto.\n *\n * @param {object} [options]\n * @param {string} [options.baseUrl] lo que va antes de cada camino, p. ej. '/api/v1'\n * @param {RequestCredentials} [options.credentials] 'same-origin' (por omisión, la cookie de sesión del mismo dominio) o\n * 'include' (un API en otro dominio con cookies)\n * @param {() => string|null|undefined} [options.getToken] con él cada petición lleva Authorization: Bearer; sin token, va\n * sin el header (el servidor responde 401)\n * @param {Record<string, string>} [options.headers] los que van en todas las peticiones\n * @param {string|null} [options.publishedBuildHeader] el header con la compilación publicada; null para no leerlo\n * @param {{cookieName?: string, headerName?: string, refreshPath?: string}|null} [options.csrf] Laravel Sanctum:\n * {refreshPath: '/auth/csrf-token'}; por omisión la cookie XSRF-TOKEN y el header X-XSRF-TOKEN\n * @param {number} [options.timeoutMilliseconds] el tiempo máximo de una consulta (0 = sin límite); por omisión 60 s\n * @param {(error: ApiError) => void} [options.onUnauthenticated] qué hacer con un 401 (normalmente ir a iniciar sesión)\n * @param {typeof fetch} [options.fetch] para un servidor de mentira o para las pruebas; por omisión el del navegador\n * @returns {ApiClient}\n */\nexport function createApiClient({\n baseUrl = '',\n credentials = 'same-origin',\n getToken,\n headers: commonHeaders = {},\n publishedBuildHeader = DEFAULT_PUBLISHED_BUILD_HEADER,\n csrf = null,\n timeoutMilliseconds: defaultTimeoutMilliseconds = DEFAULT_TIMEOUT_MILLISECONDS,\n onUnauthenticated,\n fetch: fetchImplementation,\n} = {}) {\n // OJO: se lee al pedir, no al armar el cliente: así un servidor de mentira que se conecta después también sirve\n const fetchOf = () => fetchImplementation ?? globalThis.fetch;\n const csrfCookieName = csrf?.cookieName ?? 'XSRF-TOKEN';\n const csrfHeaderName = csrf?.headerName ?? 'X-XSRF-TOKEN';\n\n /** Los headers de una petición: los comunes, el idioma vigente, el token y el CSRF. */\n const headersOf = (url, body, extraHeaders) => {\n const headers = {\n Accept: 'application/json',\n 'Accept-Language': toolkitSettings().language ?? DEFAULT_LANGUAGE,\n ...commonHeaders,\n ...extraHeaders,\n };\n if (body !== undefined && !isRawBody(body)) {\n headers['Content-Type'] = 'application/json';\n }\n const token = getToken?.();\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n const csrfToken = csrf && isSameOrigin(url) ? readCookie(csrfCookieName) : null;\n if (csrfToken) {\n headers[csrfHeaderName] = csrfToken;\n }\n return headers;\n };\n\n /** La petición al servidor, ya con su respuesta o con el ApiError de «sin respuesta». */\n const send = async (method, url, {body, signal, headers, timeoutMilliseconds}) => {\n const finalHeaders = headersOf(url, body, headers);\n try {\n return await fetchOf()(url, {\n method,\n headers: finalHeaders,\n body: body === undefined || isRawBody(body) ? body : JSON.stringify(body),\n credentials,\n signal: combinedSignalOf(signal, timeoutMilliseconds),\n });\n } catch (error) {\n // cancelar no es un error de negocio: quien canceló ya sabe por qué\n if (signal?.aborted) {\n throw error;\n }\n const reason = error?.name === 'TimeoutError' ? 'se acabó el tiempo' : 'la petición no llegó al servidor';\n log.error(reason, {method, url, cause: error?.name});\n throw new ApiError({status: API_ERROR_STATUS.offline});\n }\n };\n\n /**\n * Cualquier petición.\n *\n * @param {string} method 'GET', 'POST', 'PUT', 'PATCH' o 'DELETE'\n * @param {string} path relativo a baseUrl (o completo con http)\n * @param {object} [options]\n * @param {unknown} [options.body] objeto → JSON; FormData, Blob o URLSearchParams tal cual\n * @param {Record<string, unknown>} [options.params] los de la dirección (?page=2)\n * @param {AbortSignal} [options.signal] para cancelar\n * @param {Record<string, string>} [options.headers]\n * @param {'json'|'blob'|'response'} [options.parse] cómo se lee la respuesta (API_RESPONSE_PARSE); por omisión JSON\n * @param {boolean} [options.skipUnauthenticatedHandler] un 401 que se espera (¿hay sesión?, el login)\n * @param {number} [options.timeoutMilliseconds] el de esta petición\n * @returns {Promise<any>} el JSON (null si vino vacío o 204), el Blob o la Response\n */\n const request = async (method, path, {\n body, params, signal, headers, parse = API_RESPONSE_PARSE.json, skipUnauthenticatedHandler = false,\n timeoutMilliseconds = defaultTimeoutMilliseconds,\n } = {}) => {\n const url = apiUrlOf(baseUrl, path, params);\n let response = await send(method, url, {body, signal, headers, timeoutMilliseconds});\n\n // 419 de Laravel: la cookie CSRF venció; se pide una nueva y se repite la MISMA petición una sola vez\n if (response.status === CSRF_TOKEN_MISMATCH_STATUS && csrf?.refreshPath) {\n log.info('token CSRF vencido: se renueva y se reintenta', {method, url});\n await send('GET', apiUrlOf(baseUrl, csrf.refreshPath), {signal, timeoutMilliseconds});\n response = await send(method, url, {body, signal, headers, timeoutMilliseconds});\n }\n\n // cada respuesta (también las de error) dice qué compilación del front está publicada\n if (publishedBuildHeader) {\n reportPublishedBuild(response.headers.get(publishedBuildHeader));\n }\n\n if (!response.ok) {\n const problem = await readProblem(response);\n // un error que armó otro (nginx) puede llegar en otro idioma: ahí se usa el texto genérico del paquete\n const requestedLanguage = headersOf(url, undefined, headers)['Accept-Language'];\n const responseLanguage = response.headers.get('content-language');\n const apiError = new ApiError({\n status: response.status,\n problem,\n translatedByServer: !responseLanguage || responseLanguage.startsWith(requestedLanguage),\n retryAfterSeconds: retryAfterSecondsOf(response.headers.get('retry-after')),\n });\n // los 4xx son parte del flujo (un 422 es un formulario, un 401 al arrancar es «sin sesión»); los 5xx no\n log[response.status >= SERVER_ERROR_STATUS ? 'error' : 'debug'](`${method} ${url} → ${response.status}`, {\n code: apiError.code, requestId: apiError.requestId ?? response.headers.get('x-request-id'),\n });\n if (apiError.isUnauthenticated && !skipUnauthenticatedHandler) {\n onUnauthenticated?.(apiError);\n }\n throw apiError;\n }\n\n if (parse === API_RESPONSE_PARSE.response) {\n return response;\n }\n if (parse === API_RESPONSE_PARSE.blob) {\n return response.blob();\n }\n if (response.status === NO_CONTENT_STATUS) {\n return null;\n }\n // un 200 con el cuerpo vacío pasa de verdad; JSON.parse('') tronaría\n const text = await response.text();\n if (!text) {\n return null;\n }\n try {\n return JSON.parse(text);\n } catch {\n // un 200 que no es JSON: casi siempre la página de la app que respondió en lugar del API (el camino está mal)\n log.error(`${method} ${url} respondió algo que no es JSON`, {contentType: response.headers.get('content-type')});\n throw new ApiError({status: SERVER_ERROR_STATUS});\n }\n };\n\n /**\n * Descarga un archivo: el Blob y el nombre que mandó el servidor, con avance. Un Excel cortado no se entrega.\n *\n * @param {string} path\n * @param {object} [options]\n * @param {Record<string, unknown>} [options.params]\n * @param {AbortSignal} [options.signal]\n * @param {(progress: {loaded: number, total: number|null}) => void} [options.onDownloadProgress]\n * @param {string} [options.fallbackFileName] si el servidor no manda el nombre\n * @param {number} [options.timeoutMilliseconds] por omisión sin límite\n * @returns {Promise<{blob: Blob, fileName: string}>}\n * @throws {ApiError} status 500 si un Excel llegó incompleto\n */\n const download = async (path, {params, signal, onDownloadProgress, fallbackFileName = 'descarga', timeoutMilliseconds = NO_TIMEOUT} = {}) => {\n const response = await request('GET', path, {params, signal, parse: API_RESPONSE_PARSE.response, timeoutMilliseconds});\n const blob = await readBlobWithProgress(response, onDownloadProgress);\n const fileName = fileNameFromContentDisposition(response.headers.get('content-disposition'), fallbackFileName);\n // el tipo puede traer parámetros después del «;» (charset): solo cuenta lo de antes\n const contentType = String(response.headers.get('content-type') ?? '').split(';')[0].trim();\n if (contentType === XLSX_CONTENT_TYPE && !(await isCompleteZip(blob))) {\n const requestId = response.headers.get('x-request-id');\n log.error(`GET ${path} → el Excel llegó incompleto: el servidor falló a media escritura`, {requestId, fileName});\n throw new ApiError({status: SERVER_ERROR_STATUS, problem: {request_id: requestId}});\n }\n return {blob, fileName};\n };\n\n return {\n request,\n get: (path, options) => request('GET', path, options),\n post: (path, body, options) => request('POST', path, {...options, body}),\n put: (path, body, options) => request('PUT', path, {...options, body}),\n patch: (path, body, options) => request('PATCH', path, {...options, body}),\n delete: (path, options) => request('DELETE', path, options),\n download,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createApiClient.js","sources":["../../src/api/createApiClient.js"],"sourcesContent":["// La petición de la casa: TODO lo que habla con el servidor pasa por un cliente armado aquí, con fetch del navegador (sin\n// axios ni otra librería). Lo que resuelve UNA vez y no en cada pantalla:\n// - Accept-Language con el idioma vigente de ToolkitProvider en CADA petición: el servidor responde title y detail ya\n// traducidos. Cambiar el idioma aplica a la siguiente petición sin recargar.\n// - errores como ApiError (ApiError.js): el problema RFC 9457 del servidor con el mensaje ya traducido; si la\n// respuesta llegó en otro idioma (un error que armó nginx), el texto genérico del paquete. Sin red o se acabó el\n// tiempo: un ApiError con status 0, la misma forma, para que la pantalla no tenga dos formas de error.\n// - cancelar NO es un error: el AbortError se relanza intacto (quien canceló ya sabe por qué).\n// - 401 (la sesión se venció a media consulta): avisa a onUnauthenticated, que normalmente lleva a iniciar sesión. Una\n// petición que espera el 401 (preguntar si hay sesión al arrancar, el login) pasa skipUnauthenticatedHandler.\n// - el aviso «Hay una versión nueva»: cada respuesta trae en un header qué compilación del front está publicada y se\n// le entrega a reportPublishedBuild.\n// - Laravel (Sanctum): con csrf, manda la cookie XSRF-TOKEN en su header (solo al mismo origen) y ante un 419 pide una\n// cookie nueva y repite la petición UNA vez.\n// - cuerpo objeto → JSON; FormData, Blob y URLSearchParams van tal cual (el navegador pone su boundary).\n// - descargas: el Blob y el nombre que mandó el servidor, con avance; un Excel que llegó cortado (el servidor falló a\n// media escritura después de responder 200) no se entrega.\n// - eventos hacia afuera, para lo que cada proyecto haga a su manera: onRequest (sale), onResponse (llegó, con su status\n// y cuánto tardó) y onError (CADA error en un solo lugar: Sentry, un aviso propio, una métrica). Cancelar no es error.\n// Si un evento del proyecto truena, la petición sigue y se anota en el registro.\n// - un backend que ya existe y contesta a su manera, sin cambiarlo: toProblem convierte su error a la forma RFC 9457\n// (Laravel manda {message, errors}) y mapResponse desenvuelve sus respuestas buenas ({data: …}).\n//\n// const api = createApiClient({baseUrl: '/api/v1', onUnauthenticated: () => navigate('/sign-in')});\n// const orders = await api.get('/orders', {params: {page: 2}, signal});\n// await api.post('/orders', {customerId: 7});\n//\n// Se arma uno por proyecto (en su core) y las pantallas lo usan desde su *Api.js.\n//\n// Funciones puras, no renderizan: function (regla de la casa).\nimport {ApiError, API_ERROR_STATUS} from '@/api/ApiError.js';\nimport {XLSX_CONTENT_TYPE, fileNameFromContentDisposition, isCompleteZip} from '@/files.js';\nimport {CSRF_TOKEN_MISMATCH_STATUS} from '@/laravel/csrfRetry.js';\nimport {createLogger} from '@/observability/logger.js';\nimport {reportPublishedBuild} from '@/observability/publishedBuild.js';\nimport {DEFAULT_LANGUAGE, toolkitSettings} from '@/toolkit/toolkitSettings.js';\n\nconst log = createLogger('api');\n\n/** Cómo se lee una respuesta que salió bien. */\nexport const API_RESPONSE_PARSE = Object.freeze({json: 'json', blob: 'blob', response: 'response'});\n\n// Las consultas se cortan a los 60 s; las descargas van sin límite (0): el servidor arma el archivo mientras lo manda y\n// uno grande no cabe en 60 s. No se queda colgada: se cancela desde el panel de descargas.\nconst DEFAULT_TIMEOUT_MILLISECONDS = 60_000;\nconst NO_TIMEOUT = 0;\nconst DEFAULT_PUBLISHED_BUILD_HEADER = 'X-Frontend-Build';\nconst NO_CONTENT_STATUS = 204;\nconst SERVER_ERROR_STATUS = 500;\n\n/**\n * La dirección completa: la base del cliente más el camino, con los parámetros. Un camino que ya trae http(s) va tal cual.\n * Los parámetros vacíos (undefined, null) no viajan; una lista viaja repetida (estado=a&estado=b).\n *\n * @param {string} baseUrl\n * @param {string} path\n * @param {Record<string, unknown>} [params]\n * @returns {string}\n */\nexport function apiUrlOf(baseUrl, path, params) {\n const url = /^https?:/i.test(path) ? path : `${baseUrl}${path}`;\n const query = new URLSearchParams();\n for (const [name, value] of Object.entries(params ?? {})) {\n for (const item of [].concat(value)) {\n if (item !== undefined && item !== null) {\n query.append(name, String(item));\n }\n }\n }\n const queryText = query.toString();\n if (!queryText) {\n return url;\n }\n return `${url}${url.includes('?') ? '&' : '?'}${queryText}`;\n}\n\n/**\n * ¿El cuerpo va tal cual? FormData (archivos), Blob y URLSearchParams los arma el navegador con su propio tipo.\n *\n * @param {unknown} body\n * @returns {boolean}\n */\nfunction isRawBody(body) {\n return (typeof FormData !== 'undefined' && body instanceof FormData)\n || (typeof Blob !== 'undefined' && body instanceof Blob)\n || body instanceof URLSearchParams\n || body instanceof ArrayBuffer;\n}\n\n/**\n * El valor de una cookie del navegador (la XSRF-TOKEN de Laravel), o null.\n *\n * @param {string} cookieName\n * @returns {string|null}\n */\nfunction readCookie(cookieName) {\n if (typeof document === 'undefined') {\n return null;\n }\n const prefix = `${cookieName}=`;\n const cookie = document.cookie.split(';').map((part) => part.trim()).find((part) => part.startsWith(prefix));\n return cookie ? decodeURIComponent(cookie.slice(prefix.length)) : null;\n}\n\n/**\n * ¿La dirección es del mismo origen que la página? El token CSRF solo viaja al servidor propio: mandarlo a otro dominio\n * se lo regalaría.\n *\n * @param {string} url\n * @returns {boolean}\n */\nfunction isSameOrigin(url) {\n if (typeof window === 'undefined' || !/^https?:/i.test(url)) {\n return true;\n }\n return new URL(url).origin === window.location.origin;\n}\n\n/**\n * Retry-After en segundos: el estándar admite segundos o una fecha HTTP.\n *\n * @param {string|null} retryAfterHeader\n * @returns {number|null}\n */\nexport function retryAfterSecondsOf(retryAfterHeader) {\n if (!retryAfterHeader) {\n return null;\n }\n if (/^\\d+$/.test(retryAfterHeader.trim())) {\n return Number(retryAfterHeader.trim());\n }\n const retryAt = Date.parse(retryAfterHeader);\n return Number.isNaN(retryAt) ? null : Math.max(0, Math.ceil((retryAt - Date.now()) / 1000));\n}\n\n/**\n * El cuerpo RFC 9457 de una respuesta de error, o null si no es JSON (un 502 del proxy en HTML).\n *\n * @param {Response} response\n * @returns {Promise<object|null>}\n */\n/**\n * El problema RFC 9457 del cuerpo de error, pasado por el adaptador del proyecto (toProblem). Lo que no sea un objeto\n * cuenta como sin problema (se usa el texto genérico del paquete).\n *\n * @param {(body: any, response: Response) => any} toProblem\n * @param {object|null} body\n * @param {Response} response\n * @returns {object|null}\n */\nfunction toProblemOrNull(toProblem, body, response) {\n const problem = body === null ? null : toProblem(body, response);\n return problem && typeof problem === 'object' && !Array.isArray(problem) ? problem : null;\n}\n\nasync function readProblem(response) {\n try {\n const body = JSON.parse(await response.text());\n return body && typeof body === 'object' && !Array.isArray(body) ? body : null;\n } catch {\n return null;\n }\n}\n\n/**\n * El ApiError de una respuesta que salió mal, para lo que no pasa por createApiClient: la subida con avance\n * (uploadWithProgress regresa la respuesta tal cual) o un fetch propio. Es el mismo error que da el cliente: el problema RFC\n * 9457 del servidor ({title, detail, errors}), su status (401 = sesión vencida, 422 = errores por campo) y el texto del\n * paquete si el cuerpo no es JSON (un 502 del proxy). Así la pantalla no lee el error a mano.\n *\n * const response = await uploadWithProgress(url, form, {headers, onProgress, signal});\n * if (!response.ok) throw await apiErrorOf(response);\n *\n * @param {Response} response\n * @param {{toProblem?: (body: any, response: Response) => any}} [options] el mismo adaptador de createApiClient, para un\n * backend que contesta a su manera (Laravel manda {message, errors})\n * @returns {Promise<ApiError>}\n */\nexport async function apiErrorOf(response, {toProblem = (body) => body} = {}) {\n const problem = toProblemOrNull(toProblem, await readProblem(response), response);\n return new ApiError({status: response.status, problem, retryAfterSeconds: retryAfterSecondsOf(response.headers.get('retry-after'))});\n}\n\n/**\n * La señal de la petición: la de quien la pidió (para cancelar) y la del tiempo máximo, juntas.\n *\n * @param {AbortSignal|undefined} signal\n * @param {number} timeoutMilliseconds 0 = sin límite\n * @returns {AbortSignal|undefined}\n */\nfunction combinedSignalOf(signal, timeoutMilliseconds) {\n const signals = [signal, timeoutMilliseconds > 0 ? AbortSignal.timeout(timeoutMilliseconds) : null].filter(Boolean);\n if (signals.length < 2) {\n return signals[0];\n }\n return AbortSignal.any(signals);\n}\n\n/**\n * Lee el cuerpo de una descarga avisando cuánto lleva. total viene en null cuando el servidor arma el archivo al vuelo\n * (sin Content-Length).\n *\n * @param {Response} response\n * @param {((progress: {loaded: number, total: number|null}) => void)|undefined} onDownloadProgress\n * @returns {Promise<Blob>}\n */\nasync function readBlobWithProgress(response, onDownloadProgress) {\n if (!onDownloadProgress || !response.body) {\n return response.blob();\n }\n const total = Number(response.headers.get('content-length')) || null;\n const reader = response.body.getReader();\n const chunks = [];\n let loaded = 0;\n for (let part = await reader.read(); !part.done; part = await reader.read()) {\n chunks.push(part.value);\n loaded += part.value.byteLength;\n onDownloadProgress({loaded, total});\n }\n return new Blob(chunks, {type: response.headers.get('content-type') ?? ''});\n}\n\n/**\n * Lo que acepta cada petición.\n *\n * @typedef {object} ApiRequestOptions\n * @property {unknown} [body] objeto → JSON; FormData, Blob o URLSearchParams tal cual\n * @property {Record<string, unknown>} [params] los de la dirección (?page=2)\n * @property {AbortSignal} [signal] para cancelar\n * @property {Record<string, string>} [headers]\n * @property {'json'|'blob'|'response'} [parse] cómo se lee la respuesta (API_RESPONSE_PARSE); por omisión JSON\n * @property {boolean} [skipUnauthenticatedHandler] un 401 que se espera (¿hay sesión?, el login)\n * @property {number} [timeoutMilliseconds] el de esta petición\n */\n\n/**\n * Lo que acepta una descarga.\n *\n * @typedef {object} ApiDownloadOptions\n * @property {Record<string, unknown>} [params]\n * @property {AbortSignal} [signal]\n * @property {(progress: {loaded: number, total: number|null}) => void} [onDownloadProgress]\n * @property {string} [fallbackFileName] si el servidor no manda el nombre\n * @property {number} [timeoutMilliseconds] por omisión sin límite\n */\n\n/**\n * El cliente del proyecto: cada método regresa el JSON de la respuesta (null si vino vacía) o truena con un ApiError.\n *\n * @typedef {object} ApiClient\n * @property {(method: string, path: string, options?: ApiRequestOptions) => Promise<any>} request\n * @property {(path: string, options?: ApiRequestOptions) => Promise<any>} get\n * @property {(path: string, body?: unknown, options?: ApiRequestOptions) => Promise<any>} post\n * @property {(path: string, body?: unknown, options?: ApiRequestOptions) => Promise<any>} put\n * @property {(path: string, body?: unknown, options?: ApiRequestOptions) => Promise<any>} patch\n * @property {(path: string, options?: ApiRequestOptions) => Promise<any>} delete\n * @property {(path: string, options?: ApiDownloadOptions) => Promise<{blob: Blob, fileName: string}>} download\n */\n\n/**\n * Arma el cliente del proyecto.\n *\n * @param {object} [options]\n * @param {string} [options.baseUrl] lo que va antes de cada camino, p. ej. '/api/v1'\n * @param {RequestCredentials} [options.credentials] 'same-origin' (por omisión, la cookie de sesión del mismo dominio) o\n * 'include' (un API en otro dominio con cookies)\n * @param {() => string|null|undefined} [options.getToken] con él cada petición lleva Authorization: Bearer; sin token, va\n * sin el header (el servidor responde 401)\n * @param {Record<string, string>} [options.headers] los que van en todas las peticiones\n * @param {string|null} [options.publishedBuildHeader] el header con la compilación publicada; null para no leerlo\n * @param {{cookieName?: string, headerName?: string, refreshPath?: string}|null} [options.csrf] Laravel Sanctum:\n * {refreshPath: '/auth/csrf-token'}; por omisión la cookie XSRF-TOKEN y el header X-XSRF-TOKEN\n * @param {number} [options.timeoutMilliseconds] el tiempo máximo de una consulta (0 = sin límite); por omisión 60 s\n * @param {(error: ApiError) => void} [options.onUnauthenticated] qué hacer con un 401 (normalmente ir a iniciar sesión)\n * @param {(request: {method: string, url: string}) => void} [options.onRequest] cada petición, al salir\n * @param {(response: {method: string, url: string, status: number, durationMilliseconds: number}) => void} [options.onResponse]\n * cada respuesta, buena o mala, al llegar\n * @param {(error: ApiError, request: {method: string, url: string}) => void} [options.onError] cada error (sin red, 4xx, 5xx,\n * una respuesta que no es JSON), en un solo lugar: Sentry, un aviso propio. Cancelar no es error\n * @param {(body: any, response: Response) => object|null} [options.toProblem] el error de un backend que no contesta en\n * RFC 9457, a esa forma: (body) => ({title: body.message, errors: body.errors}). Por omisión el cuerpo tal cual\n * @param {(body: any, request: {method: string, url: string}) => any} [options.mapResponse] la respuesta buena de un backend\n * que la envuelve: (body) => body.data. Por omisión tal cual\n * @param {typeof fetch} [options.fetch] para un servidor de mentira o para las pruebas; por omisión el del navegador\n * @returns {ApiClient}\n */\nexport function createApiClient({\n baseUrl = '',\n credentials = 'same-origin',\n getToken,\n headers: commonHeaders = {},\n publishedBuildHeader = DEFAULT_PUBLISHED_BUILD_HEADER,\n csrf = null,\n timeoutMilliseconds: defaultTimeoutMilliseconds = DEFAULT_TIMEOUT_MILLISECONDS,\n onUnauthenticated,\n onRequest,\n onResponse,\n onError,\n toProblem = (body) => body,\n mapResponse = (body) => body,\n fetch: fetchImplementation,\n} = {}) {\n // un evento del proyecto que truena no tumba la petición: se anota y se sigue (nunca en silencio)\n const notify = (eventName, handler, ...eventArguments) => {\n try {\n handler?.(...eventArguments);\n } catch (error) {\n log.error(`el evento ${eventName} del proyecto tronó`, {message: error?.message});\n }\n };\n // OJO: se lee al pedir, no al armar el cliente: así un servidor de mentira que se conecta después también sirve\n const fetchOf = () => fetchImplementation ?? globalThis.fetch;\n const csrfCookieName = csrf?.cookieName ?? 'XSRF-TOKEN';\n const csrfHeaderName = csrf?.headerName ?? 'X-XSRF-TOKEN';\n\n /** Los headers de una petición: los comunes, el idioma vigente, el token y el CSRF. */\n const headersOf = (url, body, extraHeaders) => {\n const headers = {\n Accept: 'application/json',\n 'Accept-Language': toolkitSettings().language ?? DEFAULT_LANGUAGE,\n ...commonHeaders,\n ...extraHeaders,\n };\n if (body !== undefined && !isRawBody(body)) {\n headers['Content-Type'] = 'application/json';\n }\n const token = getToken?.();\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n const csrfToken = csrf && isSameOrigin(url) ? readCookie(csrfCookieName) : null;\n if (csrfToken) {\n headers[csrfHeaderName] = csrfToken;\n }\n return headers;\n };\n\n /** La petición al servidor, ya con su respuesta o con el ApiError de «sin respuesta». */\n const send = async (method, url, {body, signal, headers, timeoutMilliseconds}) => {\n const finalHeaders = headersOf(url, body, headers);\n try {\n return await fetchOf()(url, {\n method,\n headers: finalHeaders,\n body: body === undefined || isRawBody(body) ? body : JSON.stringify(body),\n credentials,\n signal: combinedSignalOf(signal, timeoutMilliseconds),\n });\n } catch (error) {\n // cancelar no es un error de negocio: quien canceló ya sabe por qué\n if (signal?.aborted) {\n throw error;\n }\n const reason = error?.name === 'TimeoutError' ? 'se acabó el tiempo' : 'la petición no llegó al servidor';\n log.error(reason, {method, url, cause: error?.name});\n throw new ApiError({status: API_ERROR_STATUS.offline});\n }\n };\n\n /**\n * Cualquier petición.\n *\n * @param {string} method 'GET', 'POST', 'PUT', 'PATCH' o 'DELETE'\n * @param {string} path relativo a baseUrl (o completo con http)\n * @param {object} [options]\n * @param {unknown} [options.body] objeto → JSON; FormData, Blob o URLSearchParams tal cual\n * @param {Record<string, unknown>} [options.params] los de la dirección (?page=2)\n * @param {AbortSignal} [options.signal] para cancelar\n * @param {Record<string, string>} [options.headers]\n * @param {'json'|'blob'|'response'} [options.parse] cómo se lee la respuesta (API_RESPONSE_PARSE); por omisión JSON\n * @param {boolean} [options.skipUnauthenticatedHandler] un 401 que se espera (¿hay sesión?, el login)\n * @param {number} [options.timeoutMilliseconds] el de esta petición\n * @returns {Promise<any>} el JSON (null si vino vacío o 204), el Blob o la Response\n */\n const request = async (method, path, options = {}) => {\n const url = apiUrlOf(baseUrl, path, options.params);\n try {\n return await requestOnce(method, url, options);\n } catch (error) {\n // cada error del servidor (o sin red) sale por onError; cancelar no es error\n if (error instanceof ApiError) {\n notify('onError', onError, error, {method, url});\n }\n throw error;\n }\n };\n\n const requestOnce = async (method, url, {\n body, signal, headers, parse = API_RESPONSE_PARSE.json, skipUnauthenticatedHandler = false,\n timeoutMilliseconds = defaultTimeoutMilliseconds,\n } = {}) => {\n notify('onRequest', onRequest, {method, url});\n const startedAt = Date.now();\n let response = await send(method, url, {body, signal, headers, timeoutMilliseconds});\n\n // 419 de Laravel: la cookie CSRF venció; se pide una nueva y se repite la MISMA petición una sola vez\n if (response.status === CSRF_TOKEN_MISMATCH_STATUS && csrf?.refreshPath) {\n log.info('token CSRF vencido: se renueva y se reintenta', {method, url});\n await send('GET', apiUrlOf(baseUrl, csrf.refreshPath), {signal, timeoutMilliseconds});\n response = await send(method, url, {body, signal, headers, timeoutMilliseconds});\n }\n\n notify('onResponse', onResponse, {method, url, status: response.status, durationMilliseconds: Date.now() - startedAt});\n\n // cada respuesta (también las de error) dice qué compilación del front está publicada\n if (publishedBuildHeader) {\n reportPublishedBuild(response.headers.get(publishedBuildHeader));\n }\n\n if (!response.ok) {\n const problem = toProblemOrNull(toProblem, await readProblem(response), response);\n // un error que armó otro (nginx) puede llegar en otro idioma: ahí se usa el texto genérico del paquete\n const requestedLanguage = headersOf(url, undefined, headers)['Accept-Language'];\n const responseLanguage = response.headers.get('content-language');\n const apiError = new ApiError({\n status: response.status,\n problem,\n translatedByServer: !responseLanguage || responseLanguage.startsWith(requestedLanguage),\n retryAfterSeconds: retryAfterSecondsOf(response.headers.get('retry-after')),\n });\n // los 4xx son parte del flujo (un 422 es un formulario, un 401 al arrancar es «sin sesión»); los 5xx no\n log[response.status >= SERVER_ERROR_STATUS ? 'error' : 'debug'](`${method} ${url} → ${response.status}`, {\n code: apiError.code, requestId: apiError.requestId ?? response.headers.get('x-request-id'),\n });\n if (apiError.isUnauthenticated && !skipUnauthenticatedHandler) {\n onUnauthenticated?.(apiError);\n }\n throw apiError;\n }\n\n if (parse === API_RESPONSE_PARSE.response) {\n return response;\n }\n if (parse === API_RESPONSE_PARSE.blob) {\n return response.blob();\n }\n if (response.status === NO_CONTENT_STATUS) {\n return null;\n }\n // un 200 con el cuerpo vacío pasa de verdad; JSON.parse('') tronaría\n const text = await response.text();\n if (!text) {\n return null;\n }\n let json;\n try {\n json = JSON.parse(text);\n } catch {\n // un 200 que no es JSON: casi siempre la página de la app que respondió en lugar del API (el camino está mal)\n log.error(`${method} ${url} respondió algo que no es JSON`, {contentType: response.headers.get('content-type')});\n throw new ApiError({status: SERVER_ERROR_STATUS});\n }\n return mapResponse(json, {method, url});\n };\n\n /**\n * Descarga un archivo: el Blob y el nombre que mandó el servidor, con avance. Un Excel cortado no se entrega.\n *\n * @param {string} path\n * @param {object} [options]\n * @param {Record<string, unknown>} [options.params]\n * @param {AbortSignal} [options.signal]\n * @param {(progress: {loaded: number, total: number|null}) => void} [options.onDownloadProgress]\n * @param {string} [options.fallbackFileName] si el servidor no manda el nombre\n * @param {number} [options.timeoutMilliseconds] por omisión sin límite\n * @returns {Promise<{blob: Blob, fileName: string}>}\n * @throws {ApiError} status 500 si un Excel llegó incompleto\n */\n const download = async (path, {params, signal, onDownloadProgress, fallbackFileName = 'descarga', timeoutMilliseconds = NO_TIMEOUT} = {}) => {\n const response = await request('GET', path, {params, signal, parse: API_RESPONSE_PARSE.response, timeoutMilliseconds});\n const blob = await readBlobWithProgress(response, onDownloadProgress);\n const fileName = fileNameFromContentDisposition(response.headers.get('content-disposition'), fallbackFileName);\n // el tipo puede traer parámetros después del «;» (charset): solo cuenta lo de antes\n const contentType = String(response.headers.get('content-type') ?? '').split(';')[0].trim();\n if (contentType === XLSX_CONTENT_TYPE && !(await isCompleteZip(blob))) {\n const requestId = response.headers.get('x-request-id');\n log.error(`GET ${path} → el Excel llegó incompleto: el servidor falló a media escritura`, {requestId, fileName});\n throw new ApiError({status: SERVER_ERROR_STATUS, problem: {request_id: requestId}});\n }\n return {blob, fileName};\n };\n\n return {\n request,\n get: (path, options) => request('GET', path, options),\n post: (path, body, options) => request('POST', path, {...options, body}),\n put: (path, body, options) => request('PUT', path, {...options, body}),\n patch: (path, body, options) => request('PATCH', path, {...options, body}),\n delete: (path, options) => request('DELETE', path, options),\n download,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { jsxs, Fragment, jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
|
|
5
|
-
import { useToolkitNavigation } from "../toolkit/toolkitContext.js";
|
|
6
5
|
import Avatar from "@mui/material/Avatar";
|
|
7
6
|
import Box from "@mui/material/Box";
|
|
8
7
|
import Divider from "@mui/material/Divider";
|
|
@@ -15,11 +14,13 @@ import Tooltip from "@mui/material/Tooltip";
|
|
|
15
14
|
import Typography from "@mui/material/Typography";
|
|
16
15
|
import LogoutIcon from "@mui/icons-material/Logout";
|
|
17
16
|
import PersonOutlineIcon from "@mui/icons-material/PersonOutline";
|
|
18
|
-
import { initialsOf
|
|
17
|
+
import { initialsOf } from "./menuItemText.js";
|
|
19
18
|
import { barMenuPlacementOf } from "./barMenuPlacement.js";
|
|
19
|
+
import MenuLinks from "./MenuLinks.js";
|
|
20
|
+
import { WHITE_LABEL, MONOGRAM_GRAY } from "../theme/components/glassComponents.js";
|
|
21
|
+
import { isGlassTheme } from "../theme/glassSurface.js";
|
|
20
22
|
function AccountMenu({ user, profilePath, accountMenuItems = [], onSignOut, isSigningOut = false, settings }) {
|
|
21
23
|
const { t } = useToolkitTranslation("layout");
|
|
22
|
-
const { Link: RouterLink } = useToolkitNavigation();
|
|
23
24
|
const [anchorElement, setAnchorElement] = useState(null);
|
|
24
25
|
const closeMenu = () => setAnchorElement(null);
|
|
25
26
|
const menuLinks = [
|
|
@@ -32,7 +33,16 @@ function AccountMenu({ user, profilePath, accountMenuItems = [], onSignOut, isSi
|
|
|
32
33
|
onSignOut();
|
|
33
34
|
};
|
|
34
35
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
35
|
-
/* @__PURE__ */ jsx(Tooltip, { title: t("account.open"), children: /* @__PURE__ */ jsx(IconButton, { size: "small", "aria-label": t("account.open"), "aria-haspopup": "menu", onClick: (event) => setAnchorElement(event.currentTarget), sx: { p: 0.5 }, children: /* @__PURE__ */ jsx(Avatar, { src: user.avatarUrl, alt: "", sx:
|
|
36
|
+
/* @__PURE__ */ jsx(Tooltip, { title: t("account.open"), children: /* @__PURE__ */ jsx(IconButton, { size: "small", "aria-label": t("account.open"), "aria-haspopup": "menu", onClick: (event) => setAnchorElement(event.currentTarget), sx: { p: 0.5 }, children: /* @__PURE__ */ jsx(Avatar, { src: user.avatarUrl, alt: "", sx: (theme) => ({
|
|
37
|
+
width: 32,
|
|
38
|
+
height: 32,
|
|
39
|
+
fontSize: "0.875rem",
|
|
40
|
+
bgcolor: "secondary.main",
|
|
41
|
+
color: "secondary.contrastText",
|
|
42
|
+
// con vidrio, en oscuro el monograma de Apple (gris oscuro con letra blanca), el que se aprobó. Antes lo
|
|
43
|
+
// ponía el tema porque su regla de oscuro le ganaba a este sx; ahora el sx gana en los dos modos
|
|
44
|
+
...isGlassTheme(theme) && theme.applyStyles("dark", { bgcolor: MONOGRAM_GRAY.dark, color: WHITE_LABEL })
|
|
45
|
+
}), children: initialsOf(user.name) }) }) }),
|
|
36
46
|
/* @__PURE__ */ jsxs(
|
|
37
47
|
Menu,
|
|
38
48
|
{
|
|
@@ -51,25 +61,7 @@ function AccountMenu({ user, profilePath, accountMenuItems = [], onSignOut, isSi
|
|
|
51
61
|
settings && /* @__PURE__ */ jsx(Divider, {}),
|
|
52
62
|
settings && /* @__PURE__ */ jsx(Box, { sx: { py: 0.5 }, children: settings }),
|
|
53
63
|
menuLinks.length > 0 && /* @__PURE__ */ jsx(Divider, {}),
|
|
54
|
-
|
|
55
|
-
const Icon = menuLink.Icon;
|
|
56
|
-
const linkProps = menuLink.to ? { component: RouterLink, to: menuLink.to } : {};
|
|
57
|
-
return /* @__PURE__ */ jsxs(
|
|
58
|
-
MenuItem,
|
|
59
|
-
{
|
|
60
|
-
...linkProps,
|
|
61
|
-
onClick: () => {
|
|
62
|
-
closeMenu();
|
|
63
|
-
menuLink.onClick?.();
|
|
64
|
-
},
|
|
65
|
-
children: [
|
|
66
|
-
Icon && /* @__PURE__ */ jsx(ListItemIcon, { children: /* @__PURE__ */ jsx(Icon, { fontSize: "small" }) }),
|
|
67
|
-
/* @__PURE__ */ jsx(ListItemText, { primary: menuItemLabelOf(menuLink, t) })
|
|
68
|
-
]
|
|
69
|
-
},
|
|
70
|
-
menuLink.to ?? menuItemLabelOf(menuLink, t)
|
|
71
|
-
);
|
|
72
|
-
}),
|
|
64
|
+
/* @__PURE__ */ jsx(MenuLinks, { links: menuLinks, onClose: closeMenu }),
|
|
73
65
|
onSignOut && /* @__PURE__ */ jsx(Divider, {}),
|
|
74
66
|
onSignOut && /* @__PURE__ */ jsxs(MenuItem, { onClick: signOut, disabled: isSigningOut, children: [
|
|
75
67
|
/* @__PURE__ */ jsx(ListItemIcon, { children: /* @__PURE__ */ jsx(LogoutIcon, { fontSize: "small" }) }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AccountMenu.js","sources":["../../src/appShell/AccountMenu.jsx"],"sourcesContent":["// La cuenta de quien está dentro, arriba a la derecha (como en las plantillas de MUI): un avatar con sus iniciales (o su\n// foto) que abre un menú con su nombre y correo, Mi perfil, lo que el proyecto agregue (accountMenuItems: Facturación,\n// Mis llaves…) y Salir.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport
|
|
1
|
+
{"version":3,"file":"AccountMenu.js","sources":["../../src/appShell/AccountMenu.jsx"],"sourcesContent":["// La cuenta de quien está dentro, arriba a la derecha (como en las plantillas de MUI): un avatar con sus iniciales (o su\n// foto) que abre un menú con su nombre y correo, Mi perfil, lo que el proyecto agregue (accountMenuItems: Facturación,\n// Mis llaves…) y Salir.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Avatar from '@mui/material/Avatar';\nimport Box from '@mui/material/Box';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport Menu from '@mui/material/Menu';\nimport MenuItem from '@mui/material/MenuItem';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport LogoutIcon from '@mui/icons-material/Logout';\nimport PersonOutlineIcon from '@mui/icons-material/PersonOutline';\n\nimport {initialsOf} from '@/appShell/menuItemText.js';\nimport {barMenuPlacementOf} from '@/appShell/barMenuPlacement.js';\nimport MenuLinks from '@/appShell/MenuLinks.jsx';\nimport {MONOGRAM_GRAY, WHITE_LABEL} from '@/theme/components/glassComponents.js';\nimport {isGlassTheme} from '@/theme/glassSurface.js';\n\n/**\n * @param {object} props\n * @param {{name: string, email?: string, subtitle?: string, avatarUrl?: string}} props.user\n * @param {string} [props.profilePath] sin ella no sale «Mi perfil»\n * @param {Array<{to?: string, onClick?: Function, label?: string, labelKey?: string, Icon?: import('react').ElementType}>} [props.accountMenuItems]\n * @param {() => void} [props.onSignOut] sin él no sale «Salir»\n * @param {boolean} [props.isSigningOut]\n * @param {import('react').ReactNode} [props.settings] los ajustes de la app, entre los datos de la persona y sus ligas\n */\nexport default function AccountMenu({user, profilePath, accountMenuItems = [], onSignOut, isSigningOut = false, settings}) {\n const {t} = useToolkitTranslation('layout');\n const [anchorElement, setAnchorElement] = useState(null);\n const closeMenu = () => setAnchorElement(null);\n const menuLinks = [\n ...(profilePath ? [{to: profilePath, labelKey: 'layout:account.myProfile', Icon: PersonOutlineIcon}] : []),\n ...accountMenuItems,\n ];\n\n // hacia arriba si la barra está abajo (teléfono con vidrio)\n const placement = barMenuPlacementOf(anchorElement);\n\n const signOut = () => {\n closeMenu();\n onSignOut();\n };\n\n return (\n <>\n <Tooltip title={t('account.open')}>\n <IconButton size=\"small\" aria-label={t('account.open')} aria-haspopup=\"menu\" onClick={(event) => setAnchorElement(event.currentTarget)} sx={{p: 0.5}}>\n <Avatar src={user.avatarUrl} alt=\"\" sx={(theme) => ({\n width: 32, height: 32, fontSize: '0.875rem', bgcolor: 'secondary.main', color: 'secondary.contrastText',\n // con vidrio, en oscuro el monograma de Apple (gris oscuro con letra blanca), el que se aprobó. Antes lo\n // ponía el tema porque su regla de oscuro le ganaba a este sx; ahora el sx gana en los dos modos\n ...(isGlassTheme(theme) && theme.applyStyles('dark', {bgcolor: MONOGRAM_GRAY.dark, color: WHITE_LABEL})),\n })}>\n {initialsOf(user.name)}\n </Avatar>\n </IconButton>\n </Tooltip>\n <Menu anchorEl={anchorElement} open={Boolean(anchorElement)} onClose={closeMenu}\n anchorOrigin={placement.anchorOrigin} transformOrigin={placement.transformOrigin}\n slotProps={{paper: {sx: {minWidth: settings ? 300 : 240, maxWidth: 360, ...placement.gapSx}}}}>\n <Box sx={{px: 2, py: 1.25}}>\n <Typography variant=\"subtitle2\" noWrap sx={{fontWeight: 600}}>{user.name}</Typography>\n {user.email && <Typography variant=\"body2\" color=\"text.secondary\" noWrap>{user.email}</Typography>}\n {user.subtitle && <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\" noWrap>{user.subtitle}</Typography>}\n </Box>\n {/* los ajustes de la app (letra, idioma, claro u oscuro y los del proyecto): en la cuenta y no sueltos en la barra */}\n {settings && <Divider />}\n {settings && <Box sx={{py: 0.5}}>{settings}</Box>}\n {menuLinks.length > 0 && <Divider />}\n <MenuLinks links={menuLinks} onClose={closeMenu} />\n {onSignOut && <Divider />}\n {onSignOut && (\n <MenuItem onClick={signOut} disabled={isSigningOut}>\n <ListItemIcon><LogoutIcon fontSize=\"small\" /></ListItemIcon>\n <ListItemText primary={t('signOut')} />\n </MenuItem>\n )}\n </Menu>\n </>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;AAmCA,SAAwB,YAAY,EAAC,MAAM,aAAa,mBAAmB,IAAI,WAAW,eAAe,OAAO,YAAW;AACvH,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ;AAC1C,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,IAAI;AACvD,QAAM,YAAY,MAAM,iBAAiB,IAAI;AAC7C,QAAM,YAAY;AAAA,IACd,GAAI,cAAc,CAAC,EAAC,IAAI,aAAa,UAAU,4BAA4B,MAAM,kBAAA,CAAkB,IAAI,CAAA;AAAA,IACvG,GAAG;AAAA,EAAA;AAIP,QAAM,YAAY,mBAAmB,aAAa;AAElD,QAAM,UAAU,MAAM;AAClB,cAAA;AACA,cAAA;AAAA,EACJ;AAEA,SACI,qBAAA,UAAA,EACI,UAAA;AAAA,IAAA,oBAAC,WAAQ,OAAO,EAAE,cAAc,GAC5B,8BAAC,YAAA,EAAW,MAAK,SAAQ,cAAY,EAAE,cAAc,GAAG,iBAAc,QAAO,SAAS,CAAC,UAAU,iBAAiB,MAAM,aAAa,GAAG,IAAI,EAAC,GAAG,OAC5I,UAAA,oBAAC,QAAA,EAAO,KAAK,KAAK,WAAW,KAAI,IAAG,IAAI,CAAC,WAAW;AAAA,MAChD,OAAO;AAAA,MAAI,QAAQ;AAAA,MAAI,UAAU;AAAA,MAAY,SAAS;AAAA,MAAkB,OAAO;AAAA;AAAA;AAAA,MAG/E,GAAI,aAAa,KAAK,KAAK,MAAM,YAAY,QAAQ,EAAC,SAAS,cAAc,MAAM,OAAO,aAAY;AAAA,IAAA,IAErG,UAAA,WAAW,KAAK,IAAI,EAAA,CACzB,GACJ,GACJ;AAAA,IACA;AAAA,MAAC;AAAA,MAAA;AAAA,QAAK,UAAU;AAAA,QAAe,MAAM,QAAQ,aAAa;AAAA,QAAG,SAAS;AAAA,QAChE,cAAc,UAAU;AAAA,QAAc,iBAAiB,UAAU;AAAA,QACjE,WAAW,EAAC,OAAO,EAAC,IAAI,EAAC,UAAU,WAAW,MAAM,KAAK,UAAU,KAAK,GAAG,UAAU,MAAA,IAAM;AAAA,QAC7F,UAAA;AAAA,UAAA,qBAAC,OAAI,IAAI,EAAC,IAAI,GAAG,IAAI,QACjB,UAAA;AAAA,YAAA,oBAAC,YAAA,EAAW,SAAQ,aAAY,QAAM,MAAC,IAAI,EAAC,YAAY,IAAA,GAAO,UAAA,KAAK,KAAA,CAAK;AAAA,YACxE,KAAK,SAAS,oBAAC,YAAA,EAAW,SAAQ,SAAQ,OAAM,kBAAiB,QAAM,MAAE,UAAA,KAAK,MAAA,CAAM;AAAA,YACpF,KAAK,YAAY,oBAAC,YAAA,EAAW,SAAQ,WAAU,OAAM,kBAAiB,WAAU,KAAI,QAAM,MAAE,eAAK,SAAA,CAAS;AAAA,UAAA,GAC/G;AAAA,UAEC,gCAAa,SAAA,EAAQ;AAAA,UACrB,gCAAa,KAAA,EAAI,IAAI,EAAC,IAAI,IAAA,GAAO,UAAA,UAAS;AAAA,UAC1C,UAAU,SAAS,KAAK,oBAAC,SAAA,CAAA,CAAQ;AAAA,UAClC,oBAAC,WAAA,EAAU,OAAO,WAAW,SAAS,WAAW;AAAA,UAChD,iCAAc,SAAA,EAAQ;AAAA,UACtB,aACG,qBAAC,UAAA,EAAS,SAAS,SAAS,UAAU,cAClC,UAAA;AAAA,YAAA,oBAAC,cAAA,EAAa,UAAA,oBAAC,YAAA,EAAW,UAAS,SAAQ,GAAE;AAAA,YAC7C,oBAAC,cAAA,EAAa,SAAS,EAAE,SAAS,EAAA,CAAG;AAAA,UAAA,EAAA,CACzC;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAER,GACJ;AAER;"}
|
|
@@ -22,14 +22,16 @@ import BarCapsule from "./BarCapsule.js";
|
|
|
22
22
|
import { BarActionsSlotContext } from "./barActionsSlot.js";
|
|
23
23
|
import HeaderTools from "./HeaderTools.js";
|
|
24
24
|
import MobileTabBar from "./MobileTabBar.js";
|
|
25
|
+
import Wordmark from "./Wordmark.js";
|
|
25
26
|
import { PageTitleProvider, usePageTitle } from "./pageTitle.js";
|
|
26
|
-
import { GLASS_PANEL_GAP_PIXELS, DESKTOP_MEDIA_QUERY, APP_BAR_HEIGHT_PIXELS, PHONE_MEDIA_QUERY, DRAWER_WIDTH_COLLAPSED, DRAWER_WIDTH_EXPANDED, drawerTransitionOf, MAIN_CONTENT_ID, MOBILE_TAB_BAR_HEIGHT_PIXELS } from "./shellSizes.js";
|
|
27
|
+
import { GLASS_PANEL_GAP_PIXELS, DESKTOP_MEDIA_QUERY, APP_BAR_HEIGHT_PIXELS, PHONE_MEDIA_QUERY, DRAWER_WIDTH_COLLAPSED, DRAWER_WIDTH_EXPANDED, drawerTransitionOf, FLOATING_BOTTOM_SPACE_CSS_VARIABLE, MAIN_CONTENT_ID, MOBILE_TAB_BAR_HEIGHT_PIXELS } from "./shellSizes.js";
|
|
27
28
|
import SideMenu from "./SideMenu.js";
|
|
28
29
|
import { usesMobileTabBar } from "./tabBarRule.js";
|
|
29
30
|
import TopMenu from "./TopMenu.js";
|
|
30
31
|
import { useDrawerCollapsed } from "./useDrawerCollapsed.js";
|
|
32
|
+
import ComponentTexts from "../toolkit/ComponentTexts.js";
|
|
31
33
|
import NewBuildNotice, { NEW_BUILD_NOTICE_VARIANT } from "../feedback/NewBuildNotice.js";
|
|
32
|
-
import { activeMenuItemOf } from "../navigation/appMenu.js";
|
|
34
|
+
import { menuItemsWithIconsOf, activeMenuItemOf } from "../navigation/appMenu.js";
|
|
33
35
|
import { useNavigationTrail } from "../navigationTrail.js";
|
|
34
36
|
import { THEME_LOOK, TOP_BAR } from "../theme/createToolkitTheme.js";
|
|
35
37
|
import { isGlassTheme, ambientBackgroundOf, scrollEdgeStylesOf, GLASS_PANEL_RADIUS_PIXELS } from "../theme/glassSurface.js";
|
|
@@ -58,21 +60,22 @@ const SkipToContentLink = () => {
|
|
|
58
60
|
"&:focus": { top: 8 }
|
|
59
61
|
}, children: t("skipToContent") });
|
|
60
62
|
};
|
|
61
|
-
const BarBrand = ({ logo, brandName, brandCaption, homePath, sx }) => {
|
|
63
|
+
const BarBrand = ({ logo, wordmark, brandName, brandCaption, homePath, sx }) => {
|
|
62
64
|
const { Link: RouterLink } = useToolkitNavigation();
|
|
63
|
-
return /* @__PURE__ */
|
|
65
|
+
return /* @__PURE__ */ jsx(
|
|
64
66
|
ButtonBase,
|
|
65
67
|
{
|
|
66
68
|
component: RouterLink,
|
|
67
69
|
to: homePath,
|
|
70
|
+
"aria-label": wordmark ? brandName : void 0,
|
|
68
71
|
sx: [{ alignItems: "center", gap: 1.25, borderRadius: 1, flexShrink: 0, minWidth: 0, pr: 1, textAlign: "left" }, sx],
|
|
69
|
-
children: [
|
|
72
|
+
children: wordmark ? /* @__PURE__ */ jsx(Wordmark, { children: wordmark }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
70
73
|
logo && /* @__PURE__ */ jsx(Box, { sx: { width: 28, height: 28, flex: "0 0 auto", display: "flex", alignItems: "center", justifyContent: "center" }, children: logo }),
|
|
71
74
|
/* @__PURE__ */ jsxs(Box, { sx: { minWidth: 0 }, children: [
|
|
72
75
|
/* @__PURE__ */ jsx(Typography, { variant: "subtitle2", component: "span", noWrap: true, sx: { display: "block", fontWeight: 600, lineHeight: 1.3 }, children: brandName }),
|
|
73
76
|
brandCaption && /* @__PURE__ */ jsx(Typography, { variant: "caption", component: "span", noWrap: true, sx: { display: "block", lineHeight: 1.3, opacity: 0.8 }, children: brandCaption })
|
|
74
77
|
] })
|
|
75
|
-
]
|
|
78
|
+
] })
|
|
76
79
|
}
|
|
77
80
|
);
|
|
78
81
|
};
|
|
@@ -112,6 +115,7 @@ const CollapseMenuButton = ({ collapsed, onToggle }) => {
|
|
|
112
115
|
};
|
|
113
116
|
const AppLayoutFrame = ({
|
|
114
117
|
logo,
|
|
118
|
+
wordmark,
|
|
115
119
|
brandName,
|
|
116
120
|
brandCaption,
|
|
117
121
|
homePath,
|
|
@@ -134,13 +138,24 @@ const AppLayoutFrame = ({
|
|
|
134
138
|
defaultCollapsed,
|
|
135
139
|
minimizeTabBarOnScroll,
|
|
136
140
|
contentMaxWidth,
|
|
141
|
+
onMenuCollapse,
|
|
142
|
+
onMobileMenuChange,
|
|
143
|
+
onMobileTabsChange,
|
|
137
144
|
children
|
|
138
145
|
}) => {
|
|
139
146
|
const { t } = useToolkitTranslation("layout");
|
|
140
147
|
const { useLocation } = useToolkitNavigation();
|
|
141
148
|
const { pathname } = useLocation();
|
|
142
149
|
const { collapsed, toggle } = useDrawerCollapsed(defaultCollapsed);
|
|
143
|
-
const [isMobileMenuOpen,
|
|
150
|
+
const [isMobileMenuOpen, setIsMobileMenuOpenState] = useState(false);
|
|
151
|
+
const toggleCollapsed = () => {
|
|
152
|
+
toggle();
|
|
153
|
+
onMenuCollapse?.(!collapsed);
|
|
154
|
+
};
|
|
155
|
+
const setIsMobileMenuOpen = (isOpen) => {
|
|
156
|
+
setIsMobileMenuOpenState(isOpen);
|
|
157
|
+
onMobileMenuChange?.(isOpen);
|
|
158
|
+
};
|
|
144
159
|
const appBarRef = useAppBarHeightPublisher();
|
|
145
160
|
useNavigationTrail();
|
|
146
161
|
const theme = useTheme();
|
|
@@ -153,7 +168,7 @@ const AppLayoutFrame = ({
|
|
|
153
168
|
const isBarMerged = isGlass && isSideMenu && collapsed && !ribbons && !usesBarCapsules;
|
|
154
169
|
const drawerWidth = collapsed ? DRAWER_WIDTH_COLLAPSED : DRAWER_WIDTH_EXPANDED;
|
|
155
170
|
const activeItem = activeMenuItemOf(menuItems, pathname);
|
|
156
|
-
const sideMenuProps = { logo, brandName, brandCaption, homePath, menuItems, activeItem, menuBadges, footer: sidebarFooter };
|
|
171
|
+
const sideMenuProps = { logo, wordmark, brandName, brandCaption, homePath, menuItems, activeItem, menuBadges, footer: sidebarFooter };
|
|
157
172
|
const [barActionsSlot, setBarActionsSlot] = useState(null);
|
|
158
173
|
const usesPhoneFloatingActions = usesTabBar && usesBarCapsules;
|
|
159
174
|
const [phoneBarActionsSlot, setPhoneBarActionsSlot] = useState(null);
|
|
@@ -232,11 +247,12 @@ const AppLayoutFrame = ({
|
|
|
232
247
|
children: /* @__PURE__ */ jsx(MenuIcon, {})
|
|
233
248
|
}
|
|
234
249
|
),
|
|
235
|
-
isSideMenu && /* @__PURE__ */ jsx(CollapseMenuButton, { collapsed, onToggle:
|
|
250
|
+
isSideMenu && /* @__PURE__ */ jsx(CollapseMenuButton, { collapsed, onToggle: toggleCollapsed }),
|
|
236
251
|
/* @__PURE__ */ jsx(
|
|
237
252
|
BarBrand,
|
|
238
253
|
{
|
|
239
254
|
logo,
|
|
255
|
+
wordmark,
|
|
240
256
|
brandName,
|
|
241
257
|
brandCaption,
|
|
242
258
|
homePath,
|
|
@@ -287,6 +303,7 @@ const AppLayoutFrame = ({
|
|
|
287
303
|
menuBadges,
|
|
288
304
|
moreFooter: sidebarFooter,
|
|
289
305
|
minimizeOnScroll: minimizeTabBarOnScroll,
|
|
306
|
+
onPinnedTabsChange: onMobileTabsChange,
|
|
290
307
|
account: /* @__PURE__ */ jsx(
|
|
291
308
|
HeaderTools,
|
|
292
309
|
{
|
|
@@ -340,7 +357,7 @@ const AppLayoutFrame = ({
|
|
|
340
357
|
showBrand: false,
|
|
341
358
|
collapsed,
|
|
342
359
|
railTop: isBarMerged ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
343
|
-
/* @__PURE__ */ jsx(CollapseMenuButton, { collapsed, onToggle:
|
|
360
|
+
/* @__PURE__ */ jsx(CollapseMenuButton, { collapsed, onToggle: toggleCollapsed }),
|
|
344
361
|
logo && /* @__PURE__ */ jsx(RailBrand, { logo, brandName, homePath })
|
|
345
362
|
] }) : void 0,
|
|
346
363
|
railBottom: isBarMerged ? /* @__PURE__ */ jsx(
|
|
@@ -381,7 +398,7 @@ const AppLayoutFrame = ({
|
|
|
381
398
|
...usesTabBar && {
|
|
382
399
|
[PHONE_MEDIA_QUERY]: {
|
|
383
400
|
pt: `calc(${theme2.spacing(3)} + env(safe-area-inset-top))`,
|
|
384
|
-
pb: `calc(${MOBILE_TAB_BAR_HEIGHT_PIXELS.portrait + GLASS_PANEL_GAP_PIXELS * 2}px + env(safe-area-inset-bottom) + ${theme2.spacing(3)})`
|
|
401
|
+
pb: `calc(${MOBILE_TAB_BAR_HEIGHT_PIXELS.portrait + GLASS_PANEL_GAP_PIXELS * 2}px + env(safe-area-inset-bottom) + ${theme2.spacing(3)} + var(${FLOATING_BOTTOM_SPACE_CSS_VARIABLE}, 0px))`
|
|
385
402
|
}
|
|
386
403
|
}
|
|
387
404
|
}),
|
|
@@ -430,6 +447,7 @@ const AppLayoutFrame = ({
|
|
|
430
447
|
};
|
|
431
448
|
const AppLayout = ({
|
|
432
449
|
logo,
|
|
450
|
+
wordmark,
|
|
433
451
|
brandName,
|
|
434
452
|
brandCaption,
|
|
435
453
|
homePath = "/",
|
|
@@ -452,18 +470,24 @@ const AppLayout = ({
|
|
|
452
470
|
defaultCollapsed = false,
|
|
453
471
|
minimizeTabBarOnScroll = true,
|
|
454
472
|
contentMaxWidth = 1400,
|
|
473
|
+
onMenuCollapse,
|
|
474
|
+
onMobileMenuChange,
|
|
475
|
+
onMobileTabsChange,
|
|
476
|
+
texts,
|
|
455
477
|
children
|
|
456
478
|
}) => {
|
|
457
479
|
const { appName } = useToolkitSettings();
|
|
458
|
-
const
|
|
459
|
-
|
|
480
|
+
const shownMenuItems = useMemo(() => menuItemsWithIconsOf(menuItems), [menuItems]);
|
|
481
|
+
const appMenu = useMemo(() => ({ menuItems: shownMenuItems }), [shownMenuItems]);
|
|
482
|
+
return /* @__PURE__ */ jsx(ComponentTexts, { namespace: "layout", texts, children: /* @__PURE__ */ jsx(PageTitleProvider, { children: /* @__PURE__ */ jsx(AppMenuContext.Provider, { value: appMenu, children: /* @__PURE__ */ jsx(
|
|
460
483
|
AppLayoutFrame,
|
|
461
484
|
{
|
|
462
485
|
logo,
|
|
486
|
+
wordmark,
|
|
463
487
|
brandName: brandName ?? appName,
|
|
464
488
|
brandCaption,
|
|
465
489
|
homePath,
|
|
466
|
-
menuItems,
|
|
490
|
+
menuItems: shownMenuItems,
|
|
467
491
|
menuPosition,
|
|
468
492
|
user,
|
|
469
493
|
profilePath,
|
|
@@ -482,9 +506,12 @@ const AppLayout = ({
|
|
|
482
506
|
defaultCollapsed,
|
|
483
507
|
minimizeTabBarOnScroll,
|
|
484
508
|
contentMaxWidth,
|
|
509
|
+
onMenuCollapse,
|
|
510
|
+
onMobileMenuChange,
|
|
511
|
+
onMobileTabsChange,
|
|
485
512
|
children
|
|
486
513
|
}
|
|
487
|
-
) }) });
|
|
514
|
+
) }) }) });
|
|
488
515
|
};
|
|
489
516
|
export {
|
|
490
517
|
AppLayout,
|