@endora-commerce/admin-shell 0.0.0-stage → 0.100.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/LICENSE +21 -0
- package/README.md +49 -2
- package/dist/AdminRoot.d.ts +45 -0
- package/dist/AdminRoot.d.ts.map +1 -0
- package/dist/AdminRoot.js +39 -0
- package/dist/AdminRoot.js.map +1 -0
- package/dist/App.d.ts +42 -0
- package/dist/App.d.ts.map +1 -0
- package/dist/App.js +94 -0
- package/dist/App.js.map +1 -0
- package/dist/components/AppShell.d.ts +5 -0
- package/dist/components/AppShell.d.ts.map +1 -0
- package/dist/components/AppShell.js +1259 -0
- package/dist/components/AppShell.js.map +1 -0
- package/dist/components/BrandLogo.d.ts +21 -0
- package/dist/components/BrandLogo.d.ts.map +1 -0
- package/dist/components/BrandLogo.js +21 -0
- package/dist/components/BrandLogo.js.map +1 -0
- package/dist/components/FederatedSignIn.d.ts +42 -0
- package/dist/components/FederatedSignIn.d.ts.map +1 -0
- package/dist/components/FederatedSignIn.js +56 -0
- package/dist/components/FederatedSignIn.js.map +1 -0
- package/dist/components/FileDropzone.d.ts +17 -0
- package/dist/components/FileDropzone.d.ts.map +1 -0
- package/dist/components/FileDropzone.js +17 -0
- package/dist/components/FileDropzone.js.map +1 -0
- package/dist/components/IdleLogout.d.ts +2 -0
- package/dist/components/IdleLogout.d.ts.map +1 -0
- package/dist/components/IdleLogout.js +90 -0
- package/dist/components/IdleLogout.js.map +1 -0
- package/dist/components/ImpersonationBanner.d.ts +13 -0
- package/dist/components/ImpersonationBanner.d.ts.map +1 -0
- package/dist/components/ImpersonationBanner.js +10 -0
- package/dist/components/ImpersonationBanner.js.map +1 -0
- package/dist/components/LanguagePicker.d.ts +3 -0
- package/dist/components/LanguagePicker.d.ts.map +1 -0
- package/dist/components/LanguagePicker.js +127 -0
- package/dist/components/LanguagePicker.js.map +1 -0
- package/dist/components/LoginPage.d.ts +11 -0
- package/dist/components/LoginPage.d.ts.map +1 -0
- package/dist/components/LoginPage.js +44 -0
- package/dist/components/LoginPage.js.map +1 -0
- package/dist/components/PaginationFooter.d.ts +18 -0
- package/dist/components/PaginationFooter.d.ts.map +1 -0
- package/dist/components/PaginationFooter.js +17 -0
- package/dist/components/PaginationFooter.js.map +1 -0
- package/dist/components/ResponsiveTable.d.ts +18 -0
- package/dist/components/ResponsiveTable.d.ts.map +1 -0
- package/dist/components/ResponsiveTable.js +17 -0
- package/dist/components/ResponsiveTable.js.map +1 -0
- package/dist/components/RowActionMenu.d.ts +18 -0
- package/dist/components/RowActionMenu.d.ts.map +1 -0
- package/dist/components/RowActionMenu.js +17 -0
- package/dist/components/RowActionMenu.js.map +1 -0
- package/dist/components/SpeechToTextButton.d.ts +14 -0
- package/dist/components/SpeechToTextButton.d.ts.map +1 -0
- package/dist/components/SpeechToTextButton.js +19 -0
- package/dist/components/SpeechToTextButton.js.map +1 -0
- package/dist/components/StickyFormActions.d.ts +18 -0
- package/dist/components/StickyFormActions.d.ts.map +1 -0
- package/dist/components/StickyFormActions.js +17 -0
- package/dist/components/StickyFormActions.js.map +1 -0
- package/dist/components/TouchReorderButtons.d.ts +18 -0
- package/dist/components/TouchReorderButtons.d.ts.map +1 -0
- package/dist/components/TouchReorderButtons.js +17 -0
- package/dist/components/TouchReorderButtons.js.map +1 -0
- package/dist/components/admin-user-picker/AdminUserPicker.d.ts +18 -0
- package/dist/components/admin-user-picker/AdminUserPicker.d.ts.map +1 -0
- package/dist/components/admin-user-picker/AdminUserPicker.js +17 -0
- package/dist/components/admin-user-picker/AdminUserPicker.js.map +1 -0
- package/dist/components/asset-picker/AssetFieldPicker.d.ts +21 -0
- package/dist/components/asset-picker/AssetFieldPicker.d.ts.map +1 -0
- package/dist/components/asset-picker/AssetFieldPicker.js +20 -0
- package/dist/components/asset-picker/AssetFieldPicker.js.map +1 -0
- package/dist/components/category-picker/CategorySelect.d.ts +18 -0
- package/dist/components/category-picker/CategorySelect.d.ts.map +1 -0
- package/dist/components/category-picker/CategorySelect.js +17 -0
- package/dist/components/category-picker/CategorySelect.js.map +1 -0
- package/dist/components/category-tree-picker/CategoryTreePicker.d.ts +18 -0
- package/dist/components/category-tree-picker/CategoryTreePicker.d.ts.map +1 -0
- package/dist/components/category-tree-picker/CategoryTreePicker.js +17 -0
- package/dist/components/category-tree-picker/CategoryTreePicker.js.map +1 -0
- package/dist/components/category-tree-picker/category-tree-utils.d.ts +18 -0
- package/dist/components/category-tree-picker/category-tree-utils.d.ts.map +1 -0
- package/dist/components/category-tree-picker/category-tree-utils.js +17 -0
- package/dist/components/category-tree-picker/category-tree-utils.js.map +1 -0
- package/dist/components/category-tree-picker/index.d.ts +18 -0
- package/dist/components/category-tree-picker/index.d.ts.map +1 -0
- package/dist/components/category-tree-picker/index.js +17 -0
- package/dist/components/category-tree-picker/index.js.map +1 -0
- package/dist/components/charts/echart.d.ts +18 -0
- package/dist/components/charts/echart.d.ts.map +1 -0
- package/dist/components/charts/echart.js +17 -0
- package/dist/components/charts/echart.js.map +1 -0
- package/dist/components/cms-picker/CmsBlockPicker.d.ts +19 -0
- package/dist/components/cms-picker/CmsBlockPicker.d.ts.map +1 -0
- package/dist/components/cms-picker/CmsBlockPicker.js +18 -0
- package/dist/components/cms-picker/CmsBlockPicker.js.map +1 -0
- package/dist/components/cms-picker/CmsPagePicker.d.ts +19 -0
- package/dist/components/cms-picker/CmsPagePicker.d.ts.map +1 -0
- package/dist/components/cms-picker/CmsPagePicker.js +18 -0
- package/dist/components/cms-picker/CmsPagePicker.js.map +1 -0
- package/dist/components/country-select/index.d.ts +18 -0
- package/dist/components/country-select/index.d.ts.map +1 -0
- package/dist/components/country-select/index.js +17 -0
- package/dist/components/country-select/index.js.map +1 -0
- package/dist/components/customer-group-picker/CustomerGroupPicker.d.ts +18 -0
- package/dist/components/customer-group-picker/CustomerGroupPicker.d.ts.map +1 -0
- package/dist/components/customer-group-picker/CustomerGroupPicker.js +17 -0
- package/dist/components/customer-group-picker/CustomerGroupPicker.js.map +1 -0
- package/dist/components/customer-picker/CustomerPicker.d.ts +18 -0
- package/dist/components/customer-picker/CustomerPicker.d.ts.map +1 -0
- package/dist/components/customer-picker/CustomerPicker.js +17 -0
- package/dist/components/customer-picker/CustomerPicker.js.map +1 -0
- package/dist/components/hooks/useFocusTrap.d.ts +17 -0
- package/dist/components/hooks/useFocusTrap.d.ts.map +1 -0
- package/dist/components/hooks/useFocusTrap.js +17 -0
- package/dist/components/hooks/useFocusTrap.js.map +1 -0
- package/dist/components/hooks/useSpeechToText.d.ts +15 -0
- package/dist/components/hooks/useSpeechToText.d.ts.map +1 -0
- package/dist/components/hooks/useSpeechToText.js +63 -0
- package/dist/components/hooks/useSpeechToText.js.map +1 -0
- package/dist/components/hooks/useViewportTier.d.ts +18 -0
- package/dist/components/hooks/useViewportTier.d.ts.map +1 -0
- package/dist/components/hooks/useViewportTier.js +17 -0
- package/dist/components/hooks/useViewportTier.js.map +1 -0
- package/dist/components/notifications/NotificationBell.d.ts +13 -0
- package/dist/components/notifications/NotificationBell.d.ts.map +1 -0
- package/dist/components/notifications/NotificationBell.js +154 -0
- package/dist/components/notifications/NotificationBell.js.map +1 -0
- package/dist/components/notifications/index.d.ts +3 -0
- package/dist/components/notifications/index.d.ts.map +1 -0
- package/dist/components/notifications/index.js +3 -0
- package/dist/components/notifications/index.js.map +1 -0
- package/dist/components/notifications/useAdminNotifications.d.ts +41 -0
- package/dist/components/notifications/useAdminNotifications.d.ts.map +1 -0
- package/dist/components/notifications/useAdminNotifications.js +86 -0
- package/dist/components/notifications/useAdminNotifications.js.map +1 -0
- package/dist/components/organization-picker/OrganizationPicker.d.ts +19 -0
- package/dist/components/organization-picker/OrganizationPicker.d.ts.map +1 -0
- package/dist/components/organization-picker/OrganizationPicker.js +18 -0
- package/dist/components/organization-picker/OrganizationPicker.js.map +1 -0
- package/dist/components/organization-picker/OrganizationPickerMulti.d.ts +19 -0
- package/dist/components/organization-picker/OrganizationPickerMulti.d.ts.map +1 -0
- package/dist/components/organization-picker/OrganizationPickerMulti.js +18 -0
- package/dist/components/organization-picker/OrganizationPickerMulti.js.map +1 -0
- package/dist/components/organization-picker/OrganizationStatusBadge.d.ts +19 -0
- package/dist/components/organization-picker/OrganizationStatusBadge.d.ts.map +1 -0
- package/dist/components/organization-picker/OrganizationStatusBadge.js +18 -0
- package/dist/components/organization-picker/OrganizationStatusBadge.js.map +1 -0
- package/dist/components/organization-picker/index.d.ts +19 -0
- package/dist/components/organization-picker/index.d.ts.map +1 -0
- package/dist/components/organization-picker/index.js +18 -0
- package/dist/components/organization-picker/index.js.map +1 -0
- package/dist/components/organization-picker/useOrganizationsQuery.d.ts +19 -0
- package/dist/components/organization-picker/useOrganizationsQuery.d.ts.map +1 -0
- package/dist/components/organization-picker/useOrganizationsQuery.js +18 -0
- package/dist/components/organization-picker/useOrganizationsQuery.js.map +1 -0
- package/dist/components/payment-method-availability/MethodAvailabilityCell.d.ts +17 -0
- package/dist/components/payment-method-availability/MethodAvailabilityCell.d.ts.map +1 -0
- package/dist/components/payment-method-availability/MethodAvailabilityCell.js +17 -0
- package/dist/components/payment-method-availability/MethodAvailabilityCell.js.map +1 -0
- package/dist/components/prompt-actions/PromptModePanel.d.ts +24 -0
- package/dist/components/prompt-actions/PromptModePanel.d.ts.map +1 -0
- package/dist/components/prompt-actions/PromptModePanel.js +115 -0
- package/dist/components/prompt-actions/PromptModePanel.js.map +1 -0
- package/dist/components/prompt-actions/PromptPlanPreview.d.ts +20 -0
- package/dist/components/prompt-actions/PromptPlanPreview.d.ts.map +1 -0
- package/dist/components/prompt-actions/PromptPlanPreview.js +12 -0
- package/dist/components/prompt-actions/PromptPlanPreview.js.map +1 -0
- package/dist/components/reorder/ReorderAnnouncer.d.ts +18 -0
- package/dist/components/reorder/ReorderAnnouncer.d.ts.map +1 -0
- package/dist/components/reorder/ReorderAnnouncer.js +17 -0
- package/dist/components/reorder/ReorderAnnouncer.js.map +1 -0
- package/dist/components/reorder/useReorderList.d.ts +18 -0
- package/dist/components/reorder/useReorderList.d.ts.map +1 -0
- package/dist/components/reorder/useReorderList.js +17 -0
- package/dist/components/reorder/useReorderList.js.map +1 -0
- package/dist/components/rule-builder/RuleBuilder.d.ts +18 -0
- package/dist/components/rule-builder/RuleBuilder.d.ts.map +1 -0
- package/dist/components/rule-builder/RuleBuilder.js +17 -0
- package/dist/components/rule-builder/RuleBuilder.js.map +1 -0
- package/dist/components/sales-channel-picker/SalesChannelPicker.d.ts +19 -0
- package/dist/components/sales-channel-picker/SalesChannelPicker.d.ts.map +1 -0
- package/dist/components/sales-channel-picker/SalesChannelPicker.js +18 -0
- package/dist/components/sales-channel-picker/SalesChannelPicker.js.map +1 -0
- package/dist/components/scope-notice/ScopeNotice.d.ts +18 -0
- package/dist/components/scope-notice/ScopeNotice.d.ts.map +1 -0
- package/dist/components/scope-notice/ScopeNotice.js +17 -0
- package/dist/components/scope-notice/ScopeNotice.js.map +1 -0
- package/dist/components/ui/alert.d.ts +18 -0
- package/dist/components/ui/alert.d.ts.map +1 -0
- package/dist/components/ui/alert.js +17 -0
- package/dist/components/ui/alert.js.map +1 -0
- package/dist/components/ui/badge.d.ts +18 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +17 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/button.d.ts +18 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +17 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/card.d.ts +17 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +17 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +17 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +17 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/collapsible.d.ts +5 -0
- package/dist/components/ui/collapsible.d.ts.map +1 -0
- package/dist/components/ui/collapsible.js +5 -0
- package/dist/components/ui/collapsible.js.map +1 -0
- package/dist/components/ui/color-picker.d.ts +17 -0
- package/dist/components/ui/color-picker.d.ts.map +1 -0
- package/dist/components/ui/color-picker.js +17 -0
- package/dist/components/ui/color-picker.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +18 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +17 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/input.d.ts +17 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +17 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/label.d.ts +17 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +17 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/multi-select.d.ts +18 -0
- package/dist/components/ui/multi-select.d.ts.map +1 -0
- package/dist/components/ui/multi-select.js +17 -0
- package/dist/components/ui/multi-select.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +18 -0
- package/dist/components/ui/page-header.d.ts.map +1 -0
- package/dist/components/ui/page-header.js +17 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/route-tabs.d.ts +18 -0
- package/dist/components/ui/route-tabs.d.ts.map +1 -0
- package/dist/components/ui/route-tabs.js +17 -0
- package/dist/components/ui/route-tabs.js.map +1 -0
- package/dist/components/ui/save-button-group.d.ts +18 -0
- package/dist/components/ui/save-button-group.d.ts.map +1 -0
- package/dist/components/ui/save-button-group.js +17 -0
- package/dist/components/ui/save-button-group.js.map +1 -0
- package/dist/components/ui/select.d.ts +17 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +17 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/separator.d.ts +17 -0
- package/dist/components/ui/separator.d.ts.map +1 -0
- package/dist/components/ui/separator.js +17 -0
- package/dist/components/ui/separator.js.map +1 -0
- package/dist/components/ui/table.d.ts +17 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +17 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +17 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +17 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/i18n/TranslationProvider.d.ts +18 -0
- package/dist/i18n/TranslationProvider.d.ts.map +1 -0
- package/dist/i18n/TranslationProvider.js +17 -0
- package/dist/i18n/TranslationProvider.js.map +1 -0
- package/dist/i18n/app-language-context.d.ts +18 -0
- package/dist/i18n/app-language-context.d.ts.map +1 -0
- package/dist/i18n/app-language-context.js +17 -0
- package/dist/i18n/app-language-context.js.map +1 -0
- package/dist/i18n/interpolate.d.ts +17 -0
- package/dist/i18n/interpolate.d.ts.map +1 -0
- package/dist/i18n/interpolate.js +17 -0
- package/dist/i18n/interpolate.js.map +1 -0
- package/dist/i18n/language-storage.d.ts +17 -0
- package/dist/i18n/language-storage.d.ts.map +1 -0
- package/dist/i18n/language-storage.js +17 -0
- package/dist/i18n/language-storage.js.map +1 -0
- package/dist/i18n/preauth-login-copy.d.ts +36 -0
- package/dist/i18n/preauth-login-copy.d.ts.map +1 -0
- package/dist/i18n/preauth-login-copy.js +36 -0
- package/dist/i18n/preauth-login-copy.js.map +1 -0
- package/dist/i18n/resolver.d.ts +18 -0
- package/dist/i18n/resolver.d.ts.map +1 -0
- package/dist/i18n/resolver.js +17 -0
- package/dist/i18n/resolver.js.map +1 -0
- package/dist/i18n/types.d.ts +17 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +2 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/i18n/useTranslation.d.ts +17 -0
- package/dist/i18n/useTranslation.d.ts.map +1 -0
- package/dist/i18n/useTranslation.js +17 -0
- package/dist/i18n/useTranslation.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/admin-actions/AdminActionsProvider.d.ts +29 -0
- package/dist/lib/admin-actions/AdminActionsProvider.d.ts.map +1 -0
- package/dist/lib/admin-actions/AdminActionsProvider.js +51 -0
- package/dist/lib/admin-actions/AdminActionsProvider.js.map +1 -0
- package/dist/lib/admin-actions/api.d.ts +10 -0
- package/dist/lib/admin-actions/api.d.ts.map +1 -0
- package/dist/lib/admin-actions/api.js +13 -0
- package/dist/lib/admin-actions/api.js.map +1 -0
- package/dist/lib/admin-actions/icon-map.d.ts +17 -0
- package/dist/lib/admin-actions/icon-map.d.ts.map +1 -0
- package/dist/lib/admin-actions/icon-map.js +17 -0
- package/dist/lib/admin-actions/icon-map.js.map +1 -0
- package/dist/lib/admin-actions/useAdminActions.d.ts +17 -0
- package/dist/lib/admin-actions/useAdminActions.d.ts.map +1 -0
- package/dist/lib/admin-actions/useAdminActions.js +34 -0
- package/dist/lib/admin-actions/useAdminActions.js.map +1 -0
- package/dist/lib/api-client.d.ts +17 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +17 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/auth.d.ts +18 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +17 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/federated-sign-in/api.d.ts +4 -0
- package/dist/lib/federated-sign-in/api.d.ts.map +1 -0
- package/dist/lib/federated-sign-in/api.js +38 -0
- package/dist/lib/federated-sign-in/api.js.map +1 -0
- package/dist/lib/federated-sign-in/index.d.ts +4 -0
- package/dist/lib/federated-sign-in/index.d.ts.map +1 -0
- package/dist/lib/federated-sign-in/index.js +4 -0
- package/dist/lib/federated-sign-in/index.js.map +1 -0
- package/dist/lib/federated-sign-in/resolve.d.ts +47 -0
- package/dist/lib/federated-sign-in/resolve.d.ts.map +1 -0
- package/dist/lib/federated-sign-in/resolve.js +24 -0
- package/dist/lib/federated-sign-in/resolve.js.map +1 -0
- package/dist/lib/federated-sign-in/useFederatedSignIn.d.ts +15 -0
- package/dist/lib/federated-sign-in/useFederatedSignIn.d.ts.map +1 -0
- package/dist/lib/federated-sign-in/useFederatedSignIn.js +37 -0
- package/dist/lib/federated-sign-in/useFederatedSignIn.js.map +1 -0
- package/dist/lib/format.d.ts +17 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +17 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/module-presence/index.d.ts +19 -0
- package/dist/lib/module-presence/index.d.ts.map +1 -0
- package/dist/lib/module-presence/index.js +18 -0
- package/dist/lib/module-presence/index.js.map +1 -0
- package/dist/lib/module-registry/index.d.ts +84 -0
- package/dist/lib/module-registry/index.d.ts.map +1 -0
- package/dist/lib/module-registry/index.js +104 -0
- package/dist/lib/module-registry/index.js.map +1 -0
- package/dist/lib/money.d.ts +17 -0
- package/dist/lib/money.d.ts.map +1 -0
- package/dist/lib/money.js +17 -0
- package/dist/lib/money.js.map +1 -0
- package/dist/lib/prompt-actions/api.d.ts +10 -0
- package/dist/lib/prompt-actions/api.d.ts.map +1 -0
- package/dist/lib/prompt-actions/api.js +41 -0
- package/dist/lib/prompt-actions/api.js.map +1 -0
- package/dist/lib/prompt-actions/usePromptRequest.d.ts +26 -0
- package/dist/lib/prompt-actions/usePromptRequest.d.ts.map +1 -0
- package/dist/lib/prompt-actions/usePromptRequest.js +113 -0
- package/dist/lib/prompt-actions/usePromptRequest.js.map +1 -0
- package/dist/lib/surface-visibility.d.ts +15 -0
- package/dist/lib/surface-visibility.d.ts.map +1 -0
- package/dist/lib/surface-visibility.js +14 -0
- package/dist/lib/surface-visibility.js.map +1 -0
- package/dist/lib/text-normalization.d.ts +17 -0
- package/dist/lib/text-normalization.d.ts.map +1 -0
- package/dist/lib/text-normalization.js +17 -0
- package/dist/lib/text-normalization.js.map +1 -0
- package/dist/lib/use-page-size-preference.d.ts +16 -0
- package/dist/lib/use-page-size-preference.d.ts.map +1 -0
- package/dist/lib/use-page-size-preference.js +15 -0
- package/dist/lib/use-page-size-preference.js.map +1 -0
- package/dist/lib/use-unsaved-changes-prompt.d.ts +17 -0
- package/dist/lib/use-unsaved-changes-prompt.d.ts.map +1 -0
- package/dist/lib/use-unsaved-changes-prompt.js +17 -0
- package/dist/lib/use-unsaved-changes-prompt.js.map +1 -0
- package/dist/lib/utils.d.ts +17 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +17 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/uuid.d.ts +17 -0
- package/dist/lib/uuid.d.ts.map +1 -0
- package/dist/lib/uuid.js +17 -0
- package/dist/lib/uuid.js.map +1 -0
- package/dist/modules/home/HomePage.d.ts +3 -0
- package/dist/modules/home/HomePage.d.ts.map +1 -0
- package/dist/modules/home/HomePage.js +137 -0
- package/dist/modules/home/HomePage.js.map +1 -0
- package/dist/modules/home/RecentActivityCard.d.ts +3 -0
- package/dist/modules/home/RecentActivityCard.d.ts.map +1 -0
- package/dist/modules/home/RecentActivityCard.js +94 -0
- package/dist/modules/home/RecentActivityCard.js.map +1 -0
- package/dist/modules/home/activity-render.d.ts +69 -0
- package/dist/modules/home/activity-render.d.ts.map +1 -0
- package/dist/modules/home/activity-render.js +53 -0
- package/dist/modules/home/activity-render.js.map +1 -0
- package/dist/modules/home/useRecentActivity.d.ts +41 -0
- package/dist/modules/home/useRecentActivity.d.ts.map +1 -0
- package/dist/modules/home/useRecentActivity.js +57 -0
- package/dist/modules/home/useRecentActivity.js.map +1 -0
- package/dist/modules/platform/ModuleActivationControl.d.ts +28 -0
- package/dist/modules/platform/ModuleActivationControl.d.ts.map +1 -0
- package/dist/modules/platform/ModuleActivationControl.js +88 -0
- package/dist/modules/platform/ModuleActivationControl.js.map +1 -0
- package/dist/modules/platform/ModulesPage.d.ts +76 -0
- package/dist/modules/platform/ModulesPage.d.ts.map +1 -0
- package/dist/modules/platform/ModulesPage.js +175 -0
- package/dist/modules/platform/ModulesPage.js.map +1 -0
- package/dist/modules/platform/RecentActivityVisibilityControl.d.ts +38 -0
- package/dist/modules/platform/RecentActivityVisibilityControl.d.ts.map +1 -0
- package/dist/modules/platform/RecentActivityVisibilityControl.js +26 -0
- package/dist/modules/platform/RecentActivityVisibilityControl.js.map +1 -0
- package/dist/modules/platform/api.d.ts +27 -0
- package/dist/modules/platform/api.d.ts.map +1 -0
- package/dist/modules/platform/api.js +33 -0
- package/dist/modules/platform/api.js.map +1 -0
- package/dist/modules/profile/ProfilePage.d.ts +11 -0
- package/dist/modules/profile/ProfilePage.d.ts.map +1 -0
- package/dist/modules/profile/ProfilePage.js +140 -0
- package/dist/modules/profile/ProfilePage.js.map +1 -0
- package/dist/registerSw.d.ts +7 -0
- package/dist/registerSw.d.ts.map +1 -0
- package/dist/registerSw.js +20 -0
- package/dist/registerSw.js.map +1 -0
- package/package.json +74 -3
- package/tailwind.css +14 -0
|
@@ -0,0 +1,1259 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useFocusTrap } from './hooks/useFocusTrap.js';
|
|
4
|
+
import { BrandLogo } from './BrandLogo.js';
|
|
5
|
+
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|
6
|
+
import { Boxes, ChevronDown, ChevronRight, HelpCircle, Home as HomeIcon, Menu, LayoutDashboard, LogOut, PanelLeftClose, PanelLeftOpen, Search, Sparkles, } from 'lucide-react';
|
|
7
|
+
import { useAuth } from '../lib/auth.js';
|
|
8
|
+
import { cn } from '../lib/utils.js';
|
|
9
|
+
import { useTranslation } from '../i18n/useTranslation.js';
|
|
10
|
+
import { useTranslationContext } from '../i18n/TranslationProvider.js';
|
|
11
|
+
import { registryNavFor, useAdminRegistry, } from '../lib/module-registry/index.js';
|
|
12
|
+
import { LanguagePicker } from './LanguagePicker.js';
|
|
13
|
+
import { useViewportTier } from './hooks/useViewportTier.js';
|
|
14
|
+
import { NotificationBell } from './notifications/index.js';
|
|
15
|
+
import { useAdminActions } from '../lib/admin-actions/useAdminActions.js';
|
|
16
|
+
import { normalize } from '../lib/text-normalization.js';
|
|
17
|
+
import { useModulePresence } from '../lib/module-presence/index.js';
|
|
18
|
+
import { useSurfaceVisibility, } from '../lib/surface-visibility.js';
|
|
19
|
+
import { resolveIcon } from '../lib/admin-actions/icon-map.js';
|
|
20
|
+
import { getPromptCapability, listUnseenPromptRequests } from '../lib/prompt-actions/api.js';
|
|
21
|
+
import { PromptModePanel } from './prompt-actions/PromptModePanel.js';
|
|
22
|
+
import { SpeechToTextButton } from './SpeechToTextButton.js';
|
|
23
|
+
/* Design-aligned navigation. The "Home" + "Orders" cluster sits above
|
|
24
|
+
* the first labelled group exactly as the prototype shows. */
|
|
25
|
+
const NAV = [
|
|
26
|
+
{
|
|
27
|
+
key: 'main',
|
|
28
|
+
labelKey: '',
|
|
29
|
+
items: [
|
|
30
|
+
{ to: '/', labelKey: 'appShell.nav.home', icon: LayoutDashboard, module: null },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: 'sales',
|
|
35
|
+
labelKey: 'appShell.section.sales',
|
|
36
|
+
items: [
|
|
37
|
+
// All three `/orders*` rows stood here until feature 091's Phase 4 batch
|
|
38
|
+
// 15; `orders` declares them now, at weights 100, 200 and 300 — the order
|
|
39
|
+
// they stood in — in `packages/modules/orders/src/admin/index.ts`. That
|
|
40
|
+
// *quick order is not a second destination but the other way of getting
|
|
41
|
+
// lines into the same order, so it lives behind a tab on the order-entry
|
|
42
|
+
// page* is still true and is recorded where the routes are.
|
|
43
|
+
//
|
|
44
|
+
// `/quote-requests`, `/invoices` and `/ksef` were declared here until
|
|
45
|
+
// feature 091's Phase 4 batch 12, at weights 400, 500 and 600 — the order
|
|
46
|
+
// they stood in here, and still the order they render in now that the
|
|
47
|
+
// three rows above them are contributions too.
|
|
48
|
+
//
|
|
49
|
+
// The section now holds **no host-declared row**, which has one
|
|
50
|
+
// operator-visible consequence recorded rather than glossed: the
|
|
51
|
+
// breadcrumb `registryCrumbs` derives for every screen in it renders
|
|
52
|
+
// *Sales* unlinked, because there is no host entry left for it to point
|
|
53
|
+
// at. That is batch 13's *Inventory* and *Pricing* arriving again, and it
|
|
54
|
+
// reaches the four contributed rows' trails as well as this module's own.
|
|
55
|
+
//
|
|
56
|
+
// `/ksef`'s glyph changes from `ReceiptText` to `Receipt`:
|
|
57
|
+
// `KnownIconNameSchema` does not carry the first, and `Receipt` is what
|
|
58
|
+
// this module's own `open-ksef` palette action has always named.
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: 'catalog',
|
|
63
|
+
labelKey: 'appShell.section.catalog',
|
|
64
|
+
items: [
|
|
65
|
+
// All five `/catalog*` rows stood here until feature 091's Phase 4 batch
|
|
66
|
+
// 15; `catalog` declares them now, in
|
|
67
|
+
// `packages/modules/catalog/src/admin/index.ts`, at weights 10 to 50 —
|
|
68
|
+
// ten times the position rather than the usual hundred, because this
|
|
69
|
+
// section already holds three contributed rows and every one of them
|
|
70
|
+
// belongs *below* these five. `assets_library` declares 200, so
|
|
71
|
+
// `/catalog/categories` at 200 would tie with it and lose the tie-break
|
|
72
|
+
// on module id.
|
|
73
|
+
//
|
|
74
|
+
// The section now holds **no host-declared row**, so the derived
|
|
75
|
+
// breadcrumb renders *Catalog* unlinked — including on `pim_ergonode`'s
|
|
76
|
+
// five trails, whose own note recorded that crumb keeping its
|
|
77
|
+
// `/catalog/products` href while these rows were the host's.
|
|
78
|
+
//
|
|
79
|
+
// `/pim-ergonode` stood here until feature 091's Phase 4 batch 13.
|
|
80
|
+
// `pim_ergonode` declares it now, at weight 250 — between
|
|
81
|
+
// `assets_library`' 200 and `pim_pimcore`' 300, which is where both of
|
|
82
|
+
// those rows' own comments already said this one belonged and could not
|
|
83
|
+
// be while it was the host's, `composeNav` appending every registry entry
|
|
84
|
+
// after every host one. The reasoning that keeps it *one* row for five
|
|
85
|
+
// screens — the other four are reached through the tab strip on the
|
|
86
|
+
// connector page — is recorded where the routes are, in
|
|
87
|
+
// `packages/modules/pim_ergonode/src/admin/index.ts`.
|
|
88
|
+
//
|
|
89
|
+
// Feature 068's placement note stands and is why the section is *Catalog*
|
|
90
|
+
// rather than *Channels*: it is where catalogue content comes *from*, and
|
|
91
|
+
// the three surfaces it writes are its neighbours here.
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: 'inventory',
|
|
96
|
+
labelKey: 'appShell.section.inventory',
|
|
97
|
+
items: [
|
|
98
|
+
// All five rows of this section stood here until feature 091's Phase 4
|
|
99
|
+
// batch 13; `inventory` declares them now, at weights 100 to 500 — the
|
|
100
|
+
// order they stood in — in
|
|
101
|
+
// `packages/modules/inventory/src/admin/index.ts`. The codes those
|
|
102
|
+
// declarations carry are the ones this table settled on 2026-08-29, when
|
|
103
|
+
// every one of the five stopped naming a module that owns none of the
|
|
104
|
+
// data behind them (`orders:read` for four screens about warehouses and
|
|
105
|
+
// stock, `catalog:write` for the CSV importer), and the importer keeps
|
|
106
|
+
// the `inventory:write` that separates it from the other four.
|
|
107
|
+
//
|
|
108
|
+
// The section now holds **no host-declared row**, which has one
|
|
109
|
+
// operator-visible consequence recorded rather than glossed: the
|
|
110
|
+
// breadcrumb `registryCrumbs` derives for these screens renders
|
|
111
|
+
// *Inventory* unlinked, because there is no host entry left for it to
|
|
112
|
+
// point at. That is batch 11's *Messaging* arriving a second time.
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: 'pricing',
|
|
117
|
+
labelKey: 'appShell.section.pricing',
|
|
118
|
+
items: [
|
|
119
|
+
// `/price-lists` stood here until feature 091's Phase 4 batch 13.
|
|
120
|
+
// `price_lists` declares it now, at weight 100 — first in this section,
|
|
121
|
+
// which is where the operator has always seen it, and which is the whole
|
|
122
|
+
// of what keeps it above `taxes`' and `promotions`' 200 now that the host
|
|
123
|
+
// declares no row here at all. The same *Inventory* consequence applies:
|
|
124
|
+
// the derived breadcrumb renders *Pricing* unlinked.
|
|
125
|
+
//
|
|
126
|
+
// Carrier / payment gateway settings are reached as integrations from the
|
|
127
|
+
// Delivery methods / Payment methods pages (not top-level sidebar).
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
key: 'customers',
|
|
132
|
+
labelKey: 'appShell.section.customers',
|
|
133
|
+
items: [
|
|
134
|
+
// `/customers`, `/customers/online` and `/organizations` stood here until
|
|
135
|
+
// feature 091's Phase 4 batch 14; `/comparisons` left in batch 6,
|
|
136
|
+
// `/customer-groups` in batch 7 and `/credit-limits` in batch 8. Every
|
|
137
|
+
// row in this section is the registry's now, ordered by the weights the
|
|
138
|
+
// four modules declare — 100, 200, 300, then 400 twice and 600 — which is
|
|
139
|
+
// the order the operator already had.
|
|
140
|
+
//
|
|
141
|
+
// **The any-of gate travels with the row.** `/organizations` is gated by
|
|
142
|
+
// `requireAdminAny(['customers:read', 'customers:manage'])`
|
|
143
|
+
// (`packages/modules/organizations/src/backend/routes.admin.ts:169`), and
|
|
144
|
+
// naming only the read code — as that entry did until 2026-08-29 — hid
|
|
145
|
+
// the screen from a role holding just `customers:manage`.
|
|
146
|
+
// `AdminNavDeclaration.requiredPermission` takes a `PermissionRequirement`
|
|
147
|
+
// for exactly this, so the array is declared unchanged in
|
|
148
|
+
// `packages/modules/organizations/src/admin/index.ts`.
|
|
149
|
+
//
|
|
150
|
+
// The section now holds **no host-declared row**, which has one
|
|
151
|
+
// operator-visible consequence recorded rather than glossed: the
|
|
152
|
+
// breadcrumb `registryCrumbs` derives for these screens renders
|
|
153
|
+
// *Customers* unlinked, because there is no host entry left for it to
|
|
154
|
+
// point at. That is batch 13's *Inventory* and *Pricing* arriving again.
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
key: 'channels',
|
|
159
|
+
labelKey: 'appShell.section.channels',
|
|
160
|
+
items: [
|
|
161
|
+
// `/sales-channels` stood here until feature 091's Phase 4 batch 14;
|
|
162
|
+
// `/dictionary` and `/admin/dictionaries/audit` left in batch 10 and
|
|
163
|
+
// `/seo` in batch 8. Every row in this section is the registry's now, and
|
|
164
|
+
// `sales_channels` declares weight 100 against `dictionaries`' 200 and
|
|
165
|
+
// 300 and `seo`'s 400 — which is the order this table gave them while the
|
|
166
|
+
// first row was the host's and `composeNav` appended the rest. The
|
|
167
|
+
// *Customers* consequence above applies here too: the section holds no
|
|
168
|
+
// host-declared row, so the derived breadcrumb renders *Channels*
|
|
169
|
+
// unlinked.
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
key: 'content',
|
|
174
|
+
labelKey: 'appShell.section.content',
|
|
175
|
+
items: [
|
|
176
|
+
// All seven rows of this section stood here until feature 091's Phase 4
|
|
177
|
+
// batch 16 — the four `/cms/…` and the three `/blog/…` — and `/megamenu`
|
|
178
|
+
// left in batch 8. `cms` and `blog` declare theirs now, at weights 100 to
|
|
179
|
+
// 400 and 600 to 800, in
|
|
180
|
+
// `packages/modules/{cms,blog}/src/admin/index.ts`. `megamenu` has
|
|
181
|
+
// declared 500 since batch 8 with a comment saying it sits after the four
|
|
182
|
+
// `/cms/…` rows and before the three `/blog/…` ones; it could not while
|
|
183
|
+
// both neighbours were the host's and `composeNav` appended every
|
|
184
|
+
// registry entry after every host one, and those seven weights are what
|
|
185
|
+
// make it true again.
|
|
186
|
+
//
|
|
187
|
+
// The section now holds **no host-declared row**, with the one
|
|
188
|
+
// operator-visible consequence every emptied section has: the breadcrumb
|
|
189
|
+
// `registryCrumbs` derives for these screens renders *Content* unlinked,
|
|
190
|
+
// because there is no host entry left for it to point at. That is batches
|
|
191
|
+
// 13's *Inventory* and *Pricing* and 14's *Customers* and *Channels*
|
|
192
|
+
// arriving one last time.
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
key: 'messaging',
|
|
197
|
+
labelKey: 'appShell.section.messaging',
|
|
198
|
+
// **Empty on purpose** (feature 091, Phase 4 batch 11), which is
|
|
199
|
+
// `analyticsAds`' shape below. All three entries — `/transactional-emails`
|
|
200
|
+
// and its blocks and templates screens — are declared by
|
|
201
|
+
// `transactional_emails` and arrive through `composeNav` from
|
|
202
|
+
// `modules.generated.ts`. The section stays here because the shell owns the
|
|
203
|
+
// section taxonomy: `composeNav` refuses a contribution naming a section
|
|
204
|
+
// this file does not declare.
|
|
205
|
+
//
|
|
206
|
+
// With no host-declared entry left, the weights the module declares (100,
|
|
207
|
+
// 200, 300) are the whole of this section's order and reproduce exactly the
|
|
208
|
+
// order this table had.
|
|
209
|
+
items: [],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
key: 'newsletter',
|
|
213
|
+
labelKey: 'appShell.section.newsletter',
|
|
214
|
+
// Empty on purpose, for the reason above and in the same batch: all six
|
|
215
|
+
// entries are `newsletter`'s, declared at weights 100 through 600, which is
|
|
216
|
+
// the order they stood in here.
|
|
217
|
+
items: [],
|
|
218
|
+
},
|
|
219
|
+
// Reporting and paid-acquisition surfaces. They were originally filed under
|
|
220
|
+
// System because each arrived as a lone integration, but together they are a
|
|
221
|
+
// domain an operator navigates to on purpose — not a system setting.
|
|
222
|
+
{
|
|
223
|
+
key: 'analyticsAds',
|
|
224
|
+
labelKey: 'appShell.section.analyticsAds',
|
|
225
|
+
// **Empty on purpose, and the first section that is** (feature 091, Phase 4
|
|
226
|
+
// batch three). All four of this section's entries — `/analytics`,
|
|
227
|
+
// `/google-analytics`, `/linkedin-ads`, `/meta-ads` — are now declared by
|
|
228
|
+
// the modules that own them and arrive through `composeNav` from
|
|
229
|
+
// `modules.generated.ts`. The section itself stays here because the shell
|
|
230
|
+
// owns the section taxonomy: `composeNav` refuses a contribution naming a
|
|
231
|
+
// section this file does not declare, since an entry in a section nobody
|
|
232
|
+
// renders is a surface that silently appears nowhere.
|
|
233
|
+
//
|
|
234
|
+
// The consequence is worth stating rather than discovering: with no
|
|
235
|
+
// host-declared entry left, `registryNavFor`'s `weight` is the whole of
|
|
236
|
+
// this section's order, and the weights the four modules declare (100, 200,
|
|
237
|
+
// 300, 400) reproduce exactly the order the hand-written table had. Batch
|
|
238
|
+
// one and batch two each recorded the append-to-the-end ordering as an
|
|
239
|
+
// operator-visible regression; this is where it is repaired.
|
|
240
|
+
items: [],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
key: 'system',
|
|
244
|
+
labelKey: 'appShell.section.system',
|
|
245
|
+
items: [
|
|
246
|
+
// `module: null`, deliberately (D-36). This is the screen that switches
|
|
247
|
+
// modules on and off; attributing it to a module would mean the surface
|
|
248
|
+
// could switch itself out of existence, and nothing would be left to
|
|
249
|
+
// switch it back. It is gated on a permission, never on presence.
|
|
250
|
+
{
|
|
251
|
+
to: '/platform/modules',
|
|
252
|
+
labelKey: 'appShell.nav.platformModules',
|
|
253
|
+
icon: Boxes,
|
|
254
|
+
requiredPermission: 'platform.modules.read',
|
|
255
|
+
module: null,
|
|
256
|
+
},
|
|
257
|
+
// `/admin-users`, `/admin-roles` and `/audit-log` are declared by the
|
|
258
|
+
// modules that own them since feature 091's Phase 4 batch four, and
|
|
259
|
+
// arrive through `composeNav` from `modules.generated.ts` — including the
|
|
260
|
+
// split this file used to carry a comment about: the roles row is
|
|
261
|
+
// `admin_roles`' while the screen it points at is `admin_users`'. Their
|
|
262
|
+
// declarations, and that comment's reasoning, are in
|
|
263
|
+
// `packages/modules/{admin_users,admin_roles,audit_logs}/src/admin/index.ts`.
|
|
264
|
+
//
|
|
265
|
+
// Unlike *Analytics & Ads*, this section still holds host entries, so a
|
|
266
|
+
// registry entry appends after every one of them whatever its weight says
|
|
267
|
+
// — the operator-visible consequence the batch records.
|
|
268
|
+
// `/catalog/bulk-operations` stood here until feature 091's Phase 4 batch
|
|
269
|
+
// 15; `catalog` declares it now, at weight 400 — its position in this
|
|
270
|
+
// table's original thirteen rows, which is behind `/admin-roles`' 300 and
|
|
271
|
+
// ahead of `/custom-fields`' 500. One operator-visible change follows and
|
|
272
|
+
// is recorded rather than glossed: the row rendered *second* while it was
|
|
273
|
+
// the host's, `composeNav` putting every host row above every contributed
|
|
274
|
+
// one, and it renders fourth now — back where the table that wrote it put
|
|
275
|
+
// it. That *bulk operations may span many domains, so the entry lives
|
|
276
|
+
// under System while the URL stays `/catalog/bulk-operations` to keep
|
|
277
|
+
// existing deep links valid* is recorded where the route is.
|
|
278
|
+
// `/api-keys` and `/webhooks` are declared by the modules that own them
|
|
279
|
+
// since feature 091's Phase 4 (the plan's batch 6), and arrive through
|
|
280
|
+
// `composeNav` from `modules.generated.ts`. Their declarations are in
|
|
281
|
+
// `packages/modules/{api_keys,webhooks}/src/admin/index.ts`.
|
|
282
|
+
// `/credentials`, `/settings`, `/settings/groups` and `/settings/cache`
|
|
283
|
+
// are declared by the modules that own them since feature 091's Phase 4
|
|
284
|
+
// batch 10, and arrive through `composeNav` from `modules.generated.ts`.
|
|
285
|
+
// Their declarations are in
|
|
286
|
+
// `packages/modules/{credentials,settings}/src/admin/index.ts`.
|
|
287
|
+
//
|
|
288
|
+
// `/settings/pwa` is `pwa`'s and has been since batch six, when only the
|
|
289
|
+
// advertisement moved because the screen lived under
|
|
290
|
+
// `modules/settings/pages/`. Batch 10 moved that directory, so the screen
|
|
291
|
+
// went with it and the route is `pwa`'s too — the split closes rather
|
|
292
|
+
// than standing.
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
];
|
|
296
|
+
const STORAGE_KEY = 'b2b-admin.nav.collapsed-groups';
|
|
297
|
+
const RAIL_STORAGE_KEY = 'b2b-admin.nav.rail-mode';
|
|
298
|
+
/**
|
|
299
|
+
* Rail (icon-only) mode persistence. The whole sidebar shrinks to a
|
|
300
|
+
* 64px-wide column showing just icons; section labels disappear and
|
|
301
|
+
* each section becomes a single icon (the first item's icon). Hovering
|
|
302
|
+
* or clicking the icon reveals a popover with the section's items.
|
|
303
|
+
*/
|
|
304
|
+
function loadRailMode() {
|
|
305
|
+
if (typeof window === 'undefined')
|
|
306
|
+
return false;
|
|
307
|
+
try {
|
|
308
|
+
return window.localStorage.getItem(RAIL_STORAGE_KEY) === '1';
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function persistRailMode(value) {
|
|
315
|
+
if (typeof window === 'undefined')
|
|
316
|
+
return;
|
|
317
|
+
try {
|
|
318
|
+
window.localStorage.setItem(RAIL_STORAGE_KEY, value ? '1' : '0');
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
/* ignore */
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function loadCollapsed() {
|
|
325
|
+
if (typeof window === 'undefined')
|
|
326
|
+
return new Set();
|
|
327
|
+
try {
|
|
328
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
329
|
+
if (!raw)
|
|
330
|
+
return new Set();
|
|
331
|
+
const parsed = JSON.parse(raw);
|
|
332
|
+
if (Array.isArray(parsed)) {
|
|
333
|
+
return new Set(parsed.filter((v) => typeof v === 'string'));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
/* ignore */
|
|
338
|
+
}
|
|
339
|
+
return new Set();
|
|
340
|
+
}
|
|
341
|
+
function persistCollapsed(value) {
|
|
342
|
+
if (typeof window === 'undefined')
|
|
343
|
+
return;
|
|
344
|
+
try {
|
|
345
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(value)));
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
/* ignore */
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const CRUMB_DICT = [
|
|
352
|
+
{ test: /^\/$/, build: () => [{ labelKey: 'appShell.nav.home', href: null }] },
|
|
353
|
+
// Six hand-written `/catalog*` trails stood here until feature 091's Phase 4
|
|
354
|
+
// batch 15, and the two `/catalog/bulk-operations*` screens never had one at
|
|
355
|
+
// all — both fell to the humanised-segment fallback. `registryCrumbs` derives
|
|
356
|
+
// all eight now, from the sidebar entries `catalog` declares. Three
|
|
357
|
+
// operator-visible changes, stated rather than glossed:
|
|
358
|
+
//
|
|
359
|
+
// - *Catalog* loses its link, this section holding no host-declared entry
|
|
360
|
+
// any more for `registryCrumbs` to point at (batch 13's *Inventory* and
|
|
361
|
+
// *Pricing*, arriving again).
|
|
362
|
+
// - `/catalog/products/:id` loses its *Editor* leaf and reads
|
|
363
|
+
// *Catalog → Products*, which is the shape every converted module's
|
|
364
|
+
// sub-screen already gets.
|
|
365
|
+
// - the two bulk-operation screens gain the label their sidebar row carries,
|
|
366
|
+
// under *System* rather than under *Catalog*, which is the section that row
|
|
367
|
+
// has declared since it was written.
|
|
368
|
+
// Seventeen hand-written trails stood here until feature 091's Phase 4 batch
|
|
369
|
+
// 13 — the five `/pim-ergonode*`, the four `/inventory*`, the three
|
|
370
|
+
// `/warehouses*`, the two `/price-lists*` — plus `/orders/quick-order`, which
|
|
371
|
+
// never had one and fell to the humanised-segment fallback.
|
|
372
|
+
// `registryCrumbs` derives all of them now, from the sidebar entries the four
|
|
373
|
+
// modules declare, which is what keeps a converted module's breadcrumb from
|
|
374
|
+
// being a third shared file to edit. Four operator-visible changes, stated
|
|
375
|
+
// rather than glossed:
|
|
376
|
+
//
|
|
377
|
+
// - *Inventory* and *Pricing* lose their link, because neither section holds
|
|
378
|
+
// a host-declared entry any more for `registryCrumbs` to point at (batch
|
|
379
|
+
// 11's *Messaging*, arriving again).
|
|
380
|
+
// - Ergonode's five trails were three crumbs deep and are two: the derived
|
|
381
|
+
// trail is *Catalog → <the entry's own label>*, and the leaf for the four
|
|
382
|
+
// sub-screens becomes *Ergonode PIM* linked to nothing, because a nav
|
|
383
|
+
// entry is a landing surface and these four have none of their own. The
|
|
384
|
+
// three `appShell.nav.pimErgonode*` keys and `appShell.crumb.importRun`
|
|
385
|
+
// that only these rules rendered are retired from `_i18n` with them.
|
|
386
|
+
// - `/warehouses/new` and `/warehouses/:id` lose their *New* and *Edit*
|
|
387
|
+
// leaves for the same reason, which is the shape every converted module's
|
|
388
|
+
// sub-screen already gets.
|
|
389
|
+
// - the *Catalog* crumb on the Ergonode trails keeps its `/catalog/products`
|
|
390
|
+
// href, that being the section's first host-declared entry — the same
|
|
391
|
+
// answer the hand-written rules gave.
|
|
392
|
+
// The two carrier trails — `/delivery-methods/dhl-parcel` and
|
|
393
|
+
// `/settings/inpost` — stood here until feature 134's wave 1 (FR-023, the
|
|
394
|
+
// extraction's per-wave prologue W1). They are the first two of the seven
|
|
395
|
+
// this table has left, and they retire for a reason the earlier batches did
|
|
396
|
+
// not have: **a host-owned crumb table may not declare a trail whose
|
|
397
|
+
// destination only a module declares.** Both routes are declared in
|
|
398
|
+
// `dhl_parcel`'s and `inpost`'s own admin layers and nowhere else, so this
|
|
399
|
+
// table was asserting a trail whose other end it cannot see — and a module
|
|
400
|
+
// that ships separately from this package takes its route declarations with
|
|
401
|
+
// it, at which point the rule describes nothing
|
|
402
|
+
// (`specs/conventions/module-admin-surfaces.md`).
|
|
403
|
+
// `registryCrumbs` derives both trails now. Three
|
|
404
|
+
// operator-visible changes, stated rather than glossed:
|
|
405
|
+
//
|
|
406
|
+
// - `/delivery-methods/dhl-parcel` loses its *DHL Parcel* leaf and reads
|
|
407
|
+
// *Pricing → Delivery methods*. Neither carrier contributes a sidebar
|
|
408
|
+
// entry and neither is given one — that is the carriers' own
|
|
409
|
+
// `plan.md` Ruling 1, and a nav row added to make a crumb derivable would
|
|
410
|
+
// be a product change bought for a breadcrumb — so there is no contributed
|
|
411
|
+
// row for `registryCrumbs` to build a leaf from. It is the shape every
|
|
412
|
+
// converted module's sub-screen already gets.
|
|
413
|
+
// - `/settings/inpost` reads *System → Settings*, and this one is **worse
|
|
414
|
+
// than it was**, not merely shorter. `registryCrumbs` takes the longest
|
|
415
|
+
// nav entry the path sits under, and `/settings/inpost` sits under
|
|
416
|
+
// `settings`' own `/settings` rather than under anything the carrier
|
|
417
|
+
// declares, so the trail names a screen the operator is not on. The route
|
|
418
|
+
// is `inpost`'s own declaration, and it is moot in any instance that does
|
|
419
|
+
// not install that module; the alternatives were giving `inpost` a sidebar
|
|
420
|
+
// row it has never had, or keeping a host rule whose destination only the
|
|
421
|
+
// module declares, and both are refused above.
|
|
422
|
+
// - three `_i18n` keys retire, not two: the carriers' own pair, and the
|
|
423
|
+
// shared **parent** key both trails named. That third one was kept out of
|
|
424
|
+
// batch 8's sweep *explicitly* to parent these two trails —
|
|
425
|
+
// `packages/modules/delivery_methods/src/admin/index.ts` says so, and says
|
|
426
|
+
// it retires when they do — and after this it has no renderer at all; the
|
|
427
|
+
// sidebar row resolves `nav.deliveryMethods.label` in `delivery_methods`'
|
|
428
|
+
// own namespace. None of the three is spelled here on purpose:
|
|
429
|
+
// `batch-eight-surfaces.module-owned-surface.test.tsx` asserts a retired
|
|
430
|
+
// key's **absence from this file's source**, so naming one in a comment
|
|
431
|
+
// about retiring it reds the assertion that proves it was retired.
|
|
432
|
+
// The two `/organizations*` trails stood here until feature 091's Phase 4
|
|
433
|
+
// batch 14, and the three `/customers*` screens never had one at all — they
|
|
434
|
+
// fell to the humanised-segment fallback. `registryCrumbs` derives all five
|
|
435
|
+
// now, from the sidebar entries the two modules declare. Two operator-visible
|
|
436
|
+
// changes, stated rather than glossed: the *Customers* crumb loses its link,
|
|
437
|
+
// that section holding no host-declared entry any more for `registryCrumbs`
|
|
438
|
+
// to point at (batch 13's *Inventory* and *Pricing*, arriving again), and
|
|
439
|
+
// `/organizations/:id` loses its *Detail* leaf — the shape every converted
|
|
440
|
+
// module's sub-screen already gets. The three `/customers*` screens gain the
|
|
441
|
+
// labels their sidebar rows already carry, where they had none.
|
|
442
|
+
// Five hand-written `/orders*` trails stood here until feature 091's Phase 4
|
|
443
|
+
// batch 15 — the roster, `/orders/new`, `/orders/quick-order`,
|
|
444
|
+
// `/orders/statuses` and the generic `/orders/:id`. `registryCrumbs` derives
|
|
445
|
+
// them from the three sidebar entries `orders` declares, and it gets the
|
|
446
|
+
// ordering property those five had by construction: it takes the **longest**
|
|
447
|
+
// matching entry, where this table took the first, which is why the three
|
|
448
|
+
// named sub-pages had to precede the generic rule here. Three
|
|
449
|
+
// operator-visible changes, stated rather than glossed:
|
|
450
|
+
//
|
|
451
|
+
// - *Orders* becomes *Sales*, unlinked, on every one of these trails: the
|
|
452
|
+
// derived trail names the section the module declares, and the section
|
|
453
|
+
// holds no host entry left to link to.
|
|
454
|
+
// - `/orders/:id` loses its *Detail* leaf and reads *Sales → Orders*, which
|
|
455
|
+
// is the shape every converted module's sub-screen already gets. The
|
|
456
|
+
// `appShell.crumb.detail` key nothing else rendered retires with it.
|
|
457
|
+
// - `/orders/quick-order` loses its *Quick order* leaf for the same reason
|
|
458
|
+
// and reads *Sales → Orders* too. `quick_order` declares a route and no
|
|
459
|
+
// sidebar entry (batch 13), so there is no contributed row to derive a
|
|
460
|
+
// leaf from; `appShell.nav.quickOrder` retires with the rule.
|
|
461
|
+
// The four `/invoices*` and `/quote-requests*` trails stood here until
|
|
462
|
+
// feature 091's Phase 4 batch 12. `registryCrumbs` derives all four now, out
|
|
463
|
+
// of the sidebar entries the two modules declare, and `/ksef` — which never
|
|
464
|
+
// had a trail at all and fell to the humanised-segment fallback — gains the
|
|
465
|
+
// label its sidebar row already carries. One operator-visible change, stated
|
|
466
|
+
// rather than glossed: these four named *Customers* as their section crumb
|
|
467
|
+
// and linked it to `/organizations`, while the sidebar rows they describe sit
|
|
468
|
+
// in *Sales*. The derived trail reads the section the module declares, so the
|
|
469
|
+
// crumb becomes *Sales* linked to `/orders` — the section's first host entry,
|
|
470
|
+
// which is what every hand-written trail in this table uses and what the
|
|
471
|
+
// sidebar has said all along.
|
|
472
|
+
// The two `/sales-channels*` trails stood here until feature 091's Phase 4
|
|
473
|
+
// batch 14. `registryCrumbs` derives both now, out of the sidebar entry
|
|
474
|
+
// `sales_channels` declares, with the same two changes the `/organizations`
|
|
475
|
+
// note above records: the *Channels* crumb loses its link and
|
|
476
|
+
// `/sales-channels/:code` loses its *Detail* leaf.
|
|
477
|
+
//
|
|
478
|
+
// The seven `/cms…` rules stood here until feature 091's Phase 4 batch 16,
|
|
479
|
+
// which is where the note above said they would retire. `registryCrumbs`
|
|
480
|
+
// derives ten of the eleven `/cms…` trails now, out of the four sidebar
|
|
481
|
+
// entries `cms` declares, and the seven `/blog/…` screens — which never had a
|
|
482
|
+
// hand-written trail at all and fell to the humanised-segment fallback — gain
|
|
483
|
+
// the labels their sidebar rows carry. Three operator-visible changes, stated
|
|
484
|
+
// rather than glossed:
|
|
485
|
+
//
|
|
486
|
+
// * the section crumb becomes **Content**, unlinked. These seven rules named
|
|
487
|
+
// *Channels* linked to `/sales-channels`, which was already the wrong
|
|
488
|
+
// section — the sidebar has put these rows under *Content* for as long as
|
|
489
|
+
// the section has existed — and `registryCrumbs` reads the section the
|
|
490
|
+
// module declares. It is unlinked for the reason every emptied section's
|
|
491
|
+
// is: there is no host-declared row left for it to point at;
|
|
492
|
+
// * the six editor screens lose their `Editor` leaf and render
|
|
493
|
+
// `Content / CMS Pages →`, which is the trail every converted module's
|
|
494
|
+
// sub-screen already gets;
|
|
495
|
+
// * `/cms` — the bare alias, which has no sidebar row and which
|
|
496
|
+
// `registryCrumbs` therefore does not match — falls to the
|
|
497
|
+
// humanised-segment fallback and renders a lone *cms*. It is the one trail
|
|
498
|
+
// of the eleven that is worse than it was. Repairing it would mean either a
|
|
499
|
+
// host `<Navigate>`, which moves a `cms` deep link into the admin's own
|
|
500
|
+
// registry and is the direction this feature reverses, or a second sidebar
|
|
501
|
+
// row for a path no operator is shown. Neither is worth an unadvertised
|
|
502
|
+
// alias, and `/dictionaries/audit` took the same answer in batch 10.
|
|
503
|
+
// `/dictionary` and the two audit spellings had a hand-written trail here
|
|
504
|
+
// until feature 091's Phase 4 batch 10. `registryCrumbs` derives them now,
|
|
505
|
+
// from `dictionaries`' own nav contribution — two crumbs where the audit
|
|
506
|
+
// trail built three, the third having been a literal *audit* leaf there is
|
|
507
|
+
// nothing to derive. The unadvertised `/dictionaries/audit` spelling keeps
|
|
508
|
+
// resolving as a route and falls to the humanised-segment trail, which is
|
|
509
|
+
// what an unadvertised alias gets.
|
|
510
|
+
// `/admin-users`, `/admin-roles` and `/audit-log` had a hand-written trail
|
|
511
|
+
// here until feature 091's Phase 4 batch four. They are the modules' own
|
|
512
|
+
// declarations now, so `registryCrumbs` derives the trail from the composed
|
|
513
|
+
// sidebar — the label and the section come off the nav contribution, which is
|
|
514
|
+
// what stops this third registry from being a fifth shared file every
|
|
515
|
+
// converted module has to edit. Two consequences, both operator-visible and
|
|
516
|
+
// both stated rather than discovered: the section crumb's href becomes
|
|
517
|
+
// *System*'s first host entry, `/platform/modules`, where these three trails
|
|
518
|
+
// named `/admin-users` by hand; and the leaf label resolves in the owning
|
|
519
|
+
// module's namespace rather than in `core`.
|
|
520
|
+
{ test: /^\/platform\/modules\/?$/, build: () => [
|
|
521
|
+
{ labelKey: 'appShell.section.system', href: '/admin-users' },
|
|
522
|
+
{ labelKey: 'appShell.nav.platformModules', href: null },
|
|
523
|
+
] },
|
|
524
|
+
// `/settings` and `/settings/groups` had a hand-written trail here until
|
|
525
|
+
// feature 091's Phase 4 batch 10, and `/settings/cache` and `/settings/pwa`
|
|
526
|
+
// never had one at all — both fell to the humanised-segment fallback.
|
|
527
|
+
// `registryCrumbs` derives all four now, so the two that had a trail keep it
|
|
528
|
+
// with the section link corrected (*System*'s first host row is
|
|
529
|
+
// `/platform/modules`, where these named `/admin-users` by hand — batch
|
|
530
|
+
// four's recorded change, arriving again) and the two that had none gain the
|
|
531
|
+
// label their sidebar row already carries.
|
|
532
|
+
{ test: /^\/profile\/?$/, build: () => [
|
|
533
|
+
{ labelKey: 'appShell.crumb.myProfile', href: null },
|
|
534
|
+
] },
|
|
535
|
+
// The six `/transactional-emails*` trails stood here until feature 091's
|
|
536
|
+
// Phase 4 batch 11. `registryCrumbs` derives all six now, out of the sidebar
|
|
537
|
+
// entries `transactional_emails` declares — which is what keeps a converted
|
|
538
|
+
// module's breadcrumb from being a third shared file to edit. Two of them
|
|
539
|
+
// change: the fragment editors lose their `Editor` leaf and render
|
|
540
|
+
// `Messaging / Email Blocks →` instead, which is the trail every other
|
|
541
|
+
// converted module's sub-screen already gets, and the *Messaging* crumb loses
|
|
542
|
+
// its link because that section now holds no host-declared entry for
|
|
543
|
+
// `registryCrumbs` to point at.
|
|
544
|
+
];
|
|
545
|
+
/**
|
|
546
|
+
* The trail for a screen a **module** contributes, derived from the composed
|
|
547
|
+
* sidebar (feature 091).
|
|
548
|
+
*
|
|
549
|
+
* `CRUMB_DICT` is a third host registry naming module routes by hand — this
|
|
550
|
+
* feature converts `App.tsx`'s and `AppShell.tsx`'s, and the plan does not
|
|
551
|
+
* cover this one. Deriving the leaf instead of adding an entry per module is
|
|
552
|
+
* what keeps a converted module's breadcrumb from being a fifth shared file to
|
|
553
|
+
* edit: the label and the section are already declared in the module's nav
|
|
554
|
+
* contribution, and the parent link is the section's own first host entry —
|
|
555
|
+
* exactly what every hand-written trail in this table uses.
|
|
556
|
+
*
|
|
557
|
+
* Returns `null` for a path no module claims, so `CRUMB_DICT` keeps precedence
|
|
558
|
+
* and the URL-segment fallback keeps the last word.
|
|
559
|
+
*/
|
|
560
|
+
function registryCrumbs(pathname, composedNav) {
|
|
561
|
+
const path = pathname.replace(/\/+$/, '') || '/';
|
|
562
|
+
let best = null;
|
|
563
|
+
for (const section of composedNav) {
|
|
564
|
+
for (const item of section.items) {
|
|
565
|
+
const labelScope = item.labelScope;
|
|
566
|
+
if (labelScope === undefined)
|
|
567
|
+
continue;
|
|
568
|
+
// The entry's own destination, or anything beneath it. A module's second
|
|
569
|
+
// and third routes (`/x/new`, `/x/:id`) have no sidebar row of their own
|
|
570
|
+
// and never will — a nav entry is a landing surface — so matching only
|
|
571
|
+
// the exact `to` would drop every converted module's sub-screens onto the
|
|
572
|
+
// humanised-segment fallback below. The hand-written trails this table is
|
|
573
|
+
// draining already covered subpaths that way, with the same shape.
|
|
574
|
+
if (item.to !== path && !path.startsWith(`${item.to}/`))
|
|
575
|
+
continue;
|
|
576
|
+
// **The longest match wins, and it is first-match no longer** (feature
|
|
577
|
+
// 091, batch 10). Until this batch no module contributed both a parent
|
|
578
|
+
// route and a child of it, so "the first entry that matches" and "the
|
|
579
|
+
// entry that matches most" were the same answer. `settings` contributes
|
|
580
|
+
// `/settings`, `/settings/groups` and `/settings/cache` and `pwa`
|
|
581
|
+
// contributes `/settings/pwa`; under first-match, all four screens would
|
|
582
|
+
// have rendered *Settings* as their leaf, because `/settings` sorts
|
|
583
|
+
// ahead of the rest by weight. That is a trail that no longer names where
|
|
584
|
+
// the operator is, which is the whole of what a breadcrumb does.
|
|
585
|
+
//
|
|
586
|
+
// `CRUMB_DICT` gets the same property for free by listing its specific
|
|
587
|
+
// patterns above its general ones; this is that ordering rule, derived
|
|
588
|
+
// instead of maintained.
|
|
589
|
+
if (best !== null && best.item.to.length >= item.to.length)
|
|
590
|
+
continue;
|
|
591
|
+
best = { section, item, labelScope };
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (best === null)
|
|
595
|
+
return null;
|
|
596
|
+
const { section, item, labelScope } = best;
|
|
597
|
+
const parent = section.items.find((other) => other.labelScope === undefined);
|
|
598
|
+
const trail = [];
|
|
599
|
+
if (section.labelKey) {
|
|
600
|
+
trail.push({ labelKey: section.labelKey, href: parent?.to ?? null });
|
|
601
|
+
}
|
|
602
|
+
trail.push({
|
|
603
|
+
labelKey: item.labelKey,
|
|
604
|
+
labelScope,
|
|
605
|
+
href: item.to === path ? null : item.to,
|
|
606
|
+
});
|
|
607
|
+
return trail;
|
|
608
|
+
}
|
|
609
|
+
function buildCrumbs(pathname, composedNav) {
|
|
610
|
+
for (const entry of CRUMB_DICT) {
|
|
611
|
+
const match = pathname.match(entry.test);
|
|
612
|
+
if (match)
|
|
613
|
+
return entry.build(match);
|
|
614
|
+
}
|
|
615
|
+
const contributed = registryCrumbs(pathname, composedNav);
|
|
616
|
+
if (contributed !== null)
|
|
617
|
+
return contributed;
|
|
618
|
+
// Keep real path segments in hrefs; only humanize the visible label.
|
|
619
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
620
|
+
return segments.map((segment, idx) => ({
|
|
621
|
+
literal: segment.replace(/-/g, ' '),
|
|
622
|
+
href: idx === segments.length - 1 ? null : `/${segments.slice(0, idx + 1).join('/')}`,
|
|
623
|
+
}));
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* The palette's one matching rule, for every group it renders.
|
|
627
|
+
*
|
|
628
|
+
* Issue #233. The dialog merges three indexes and used to match with two
|
|
629
|
+
* different rules: the server-fed Actions group filters through `normalize`
|
|
630
|
+
* (`lib/text-normalization.ts`, applied in `lib/admin-actions/useAdminActions.ts`),
|
|
631
|
+
* which folds diacritics *and* the stroked letters NFD leaves standing, while
|
|
632
|
+
* the static Navigate group and the pinned Assistant row used a bare
|
|
633
|
+
* `toLowerCase().includes()`. Typing `zamowienia` therefore found the Actions
|
|
634
|
+
* row for orders and not the Navigate row for the same screen — and Polish
|
|
635
|
+
* operators routinely type without diacritics, so half the palette silently
|
|
636
|
+
* stopped answering them. Issue #236 then found a fourth copy in the
|
|
637
|
+
* page-builder drawer, which is why the fold now lives at
|
|
638
|
+
* `lib/text-normalization.ts` and not under `lib/admin-actions/`.
|
|
639
|
+
*
|
|
640
|
+
* `normalize` is **imported, not re-implemented**: the whole defect was one
|
|
641
|
+
* question answered twice, and a copy is how the two answers drifted apart. It
|
|
642
|
+
* folds `ł` through an explicit stroked-letter map, which matters — `ł` has no
|
|
643
|
+
* NFD decomposition, so the obvious `normalize('NFD').replace(...)` one-liner
|
|
644
|
+
* leaves `płatności` unmatched by `platnosci`.
|
|
645
|
+
*
|
|
646
|
+
* **Both sides are folded.** Folding only the haystack breaks the operator who
|
|
647
|
+
* does type `zamówienia`; folding only the query breaks the one who does not.
|
|
648
|
+
* (Postel's Law — accept what was typed and normalise it.)
|
|
649
|
+
*
|
|
650
|
+
* `needle` must already be normalised; the caller folds the query once per
|
|
651
|
+
* keystroke rather than once per row. `normalize` trims, so a query with a
|
|
652
|
+
* leading space matches what the operator meant rather than nothing at all
|
|
653
|
+
* (issue #236).
|
|
654
|
+
*/
|
|
655
|
+
function matchesQuery(needle, ...haystacks) {
|
|
656
|
+
return haystacks.some((haystack) => normalize(haystack).includes(needle));
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* The static Navigate group.
|
|
660
|
+
*
|
|
661
|
+
* **Every `requiredPermission` here is read from the backend route that gates
|
|
662
|
+
* the destination, not from the sidebar entry beside it.** Copying a neighbour
|
|
663
|
+
* is how a wrong code gets propagated twice, and several of these are not what
|
|
664
|
+
* the neighbourhood suggests:
|
|
665
|
+
*
|
|
666
|
+
* - `/inventory`, `/warehouses`, `/inventory/low-stock` and
|
|
667
|
+
* `/inventory/notifications` by `inventory:read`, and `/inventory/import` by
|
|
668
|
+
* `inventory:write` — the module's own codes since 2026-08-29. Until then
|
|
669
|
+
* all four reads said `orders:read` and the importer said `catalog:write`,
|
|
670
|
+
* which is how a merchandiser came to be able to delete a warehouse and
|
|
671
|
+
* anyone who could read orders came to be able to enumerate every warehouse
|
|
672
|
+
* address (`packages/modules/inventory/src/backend/routes.admin.ts`);
|
|
673
|
+
* - `/payment-methods` by `payment_methods:read` and `/delivery-methods` by
|
|
674
|
+
* `delivery_methods:read`, each its owner's own code since 2026-08-28. They
|
|
675
|
+
* look like a pair and are not one: the codes are separate, they are granted
|
|
676
|
+
* separately, and neither is `catalog:read`, which is what both said until
|
|
677
|
+
* the two modules stopped borrowing the catalogue's authority
|
|
678
|
+
* (`packages/modules/payment_methods/src/backend/routes.ts:125`,
|
|
679
|
+
* `packages/modules/delivery_methods/src/backend/routes.ts:106`);
|
|
680
|
+
* - `/api-keys` and `/webhooks` by one shared `integrations:manage`
|
|
681
|
+
* (`api_keys/routes.ts:20`, `webhooks/routes.ts:21`);
|
|
682
|
+
* - `/dictionary` and its audit view by `dictionary.write` — the module
|
|
683
|
+
* declares no read code, so reading the dictionary requires the write one
|
|
684
|
+
* (`dictionaries/routes.admin.ts:72`);
|
|
685
|
+
* - `/organizations` by `requireAdminAny(['customers:read',
|
|
686
|
+
* 'customers:manage'])`, which is why the field takes an any-of array.
|
|
687
|
+
*
|
|
688
|
+
* **Denied entries are hidden, not shown disabled** — the same treatment module
|
|
689
|
+
* absence already gets, and applied identically in the sidebar and in the
|
|
690
|
+
* dashboard's quick actions. Three reasons, in order of weight:
|
|
691
|
+
*
|
|
692
|
+
* 1. The Actions group in this very dialog already hides on permission;
|
|
693
|
+
* `AdminActionsService` filters server-side before the row is sent. Showing
|
|
694
|
+
* Navigate rows disabled would make one palette use two opposite treatments
|
|
695
|
+
* for two groups the operator does not distinguish — identical-looking rows
|
|
696
|
+
* that behave differently (Law of Similarity).
|
|
697
|
+
* 2. On an empty query the palette lists every Navigate entry. For a
|
|
698
|
+
* single-domain role that is a wall of unusable rows in the surface whose
|
|
699
|
+
* entire job is to shorten the path to the one thing you asked for (Hick).
|
|
700
|
+
* 3. Keyboard and pointer must reach the same conclusion. The Arrow/Enter loop
|
|
701
|
+
* below indexes `items` directly, so a disabled row would still be
|
|
702
|
+
* selectable by keyboard and `Enter` would navigate into a 403 — a
|
|
703
|
+
* disabled-row design would need `aria-disabled`, a linked reason and cursor
|
|
704
|
+
* skipping to be honest. Hiding makes the two paths identical by
|
|
705
|
+
* construction and leaves nothing for a screen reader to announce.
|
|
706
|
+
*
|
|
707
|
+
* The cost of hiding is real and is answered elsewhere: "you do not have access
|
|
708
|
+
* to this" is actionable in a way "this module is not installed" is not, so it
|
|
709
|
+
* belongs at the point of failure — the 403 an operator reaches by bookmark or
|
|
710
|
+
* deep link — rather than in an index the operator is scanning for something
|
|
711
|
+
* else. That screen is not part of this change; see the report.
|
|
712
|
+
*/
|
|
713
|
+
const PALETTE_ITEMS = [
|
|
714
|
+
{ group: 'Navigate', labelMode: 'key', label: 'appShell.nav.home', sub: 'appShell.palette.sub.dashboard', icon: HomeIcon, to: '/', keywords: 'home dashboard strona główna pulpit', module: null },
|
|
715
|
+
// The `/catalog/products`, `/catalog/categories` and `/catalog/attributes`
|
|
716
|
+
// rows left here in feature 091's Phase 4 batch 15, and they left by batch
|
|
717
|
+
// 10's route: `catalog` declared `new-product` and nothing that *opened* any
|
|
718
|
+
// of the three, so `open-products`, `open-categories` and `open-attributes`
|
|
719
|
+
// arrive with these rows' destinations, codes and keywords, and their labels
|
|
720
|
+
// and descriptions are the six strings the rows rendered, moved into that
|
|
721
|
+
// module's own bundle. The `/orders` row left in the same batch and was
|
|
722
|
+
// batch 12's case instead: `orders`' manifest already declared `open-orders`
|
|
723
|
+
// with the same destination and the same code, so the row was a second copy
|
|
724
|
+
// the server was never asked about and nothing replaces it.
|
|
725
|
+
// The `/inventory` and `/price-lists` rows left here in feature 091's Phase 4
|
|
726
|
+
// batch 13, and they left by the two routes batches 10 and 12 already
|
|
727
|
+
// established. `inventory` was batch 12's case: its manifest already declared
|
|
728
|
+
// `open-inventory` with the same destination and the same code, so the row
|
|
729
|
+
// was a second copy the server was never asked about and nothing replaces it.
|
|
730
|
+
// `price_lists` was batch 10's: it declared no action at all, so
|
|
731
|
+
// `open-price-lists` arrives with this row's destination, code and keywords,
|
|
732
|
+
// and its label and description are the two strings the row rendered, moved
|
|
733
|
+
// into that module's own bundle. Either way the advertisement is now the one
|
|
734
|
+
// surface the effective enabled-set filters.
|
|
735
|
+
// The `/organizations` row left here in feature 091's Phase 4 batch 14, and
|
|
736
|
+
// it left by batch 10's route: `organizations` declared no action at all, so
|
|
737
|
+
// `open-organizations` arrives with this row's destination and keywords, and
|
|
738
|
+
// its label and description are the two strings the row rendered, moved into
|
|
739
|
+
// that module's own bundle.
|
|
740
|
+
//
|
|
741
|
+
// **One operator-visible narrowing, recorded rather than glossed.** This row
|
|
742
|
+
// carried an any-of pair — `['customers:read', 'customers:manage']` — and
|
|
743
|
+
// `ModuleActionSchema.requiredPermission` is a single string
|
|
744
|
+
// (`packages/contracts/src/admin-actions.ts:205`), so the action names
|
|
745
|
+
// `customers:read`. A role holding only `customers:manage` therefore loses
|
|
746
|
+
// this palette entry and keeps the sidebar one, whose declaration takes the
|
|
747
|
+
// whole `PermissionRequirement` and keeps the pair. The screen stays
|
|
748
|
+
// reachable; only the second way of reaching it narrows. Widening the action
|
|
749
|
+
// schema would touch the palette resolver, the permission inventory and
|
|
750
|
+
// `check:action-route-permissions`, which is a change of its own and not a
|
|
751
|
+
// batch's.
|
|
752
|
+
// The `/quote-requests` row left here in feature 091's Phase 4 batch 12, and
|
|
753
|
+
// nothing replaced it: `quote_requests`' manifest already declares
|
|
754
|
+
// `open-rfq-inbox` with the same destination, the same code and the same
|
|
755
|
+
// keywords, so the row was a second copy of a declaration the server has been
|
|
756
|
+
// serving all along — and a copy the server was never asked about, which for
|
|
757
|
+
// a module an operator can withdraw means a palette that goes on advertising
|
|
758
|
+
// the screen after the withdrawal.
|
|
759
|
+
// The `/sales-channels` row left here in the same batch and by the same
|
|
760
|
+
// route. `sales_channels` declared `new-sales-channel` and nothing that
|
|
761
|
+
// *opened* the roster, so `open-sales-channels` arrives with this row's
|
|
762
|
+
// destination, code and keywords, and its label and description are the two
|
|
763
|
+
// strings the row rendered.
|
|
764
|
+
// The four `dictionaries`, `settings` and `credentials` rows left here in
|
|
765
|
+
// feature 091's Phase 4 batch 10. A hand-written palette row is a copy the
|
|
766
|
+
// server was never asked about — it went on advertising the screen after an
|
|
767
|
+
// operator withdrew the module — so each is a manifest **action** now, which
|
|
768
|
+
// is the surface the effective enabled-set filters. `settings` and
|
|
769
|
+
// `credentials` already declared theirs (`open-settings`, `open-credentials`,
|
|
770
|
+
// `new-credential`); `dictionaries` declares `open-dictionary` and
|
|
771
|
+
// `open-dictionary-audit` for the first time, with the destinations, codes
|
|
772
|
+
// and keywords these rows carried.
|
|
773
|
+
// Feature 020 — the Actions group is now sourced from the module
|
|
774
|
+
// registry via useAdminActions(); the previously-hardcoded "New
|
|
775
|
+
// product" and "Import products" entries are declared by the
|
|
776
|
+
// catalog and import_export module manifests respectively.
|
|
777
|
+
];
|
|
778
|
+
/**
|
|
779
|
+
* `[...hostNav, ...registryNav]`, per section (feature 091, FR-010;
|
|
780
|
+
* `contracts/admin-registry.md` R11).
|
|
781
|
+
*
|
|
782
|
+
* Built once at module scope because the registry is a static import: a
|
|
783
|
+
* contribution set cannot change without a rebuild, and recomputing it per
|
|
784
|
+
* render would allocate a new array on every keystroke in the palette input.
|
|
785
|
+
*
|
|
786
|
+
* **A module's entries append to their section**, ordered among themselves by
|
|
787
|
+
* `weight` then module id. R11 describes the end state — the whole section
|
|
788
|
+
* weight-ordered — which needs a weight on all 97 host entries and is Story 3's
|
|
789
|
+
* to deliver as it drains them; inventing one per host entry now would be a
|
|
790
|
+
* hundred numbers nothing derives. The consequence is visible and worth
|
|
791
|
+
* stating: `/import-export` used to sit between Credentials and Settings in the
|
|
792
|
+
* System group and now sits at that group's end.
|
|
793
|
+
*
|
|
794
|
+
* **A module may not invent a section** (D-23): `AdminNavSectionNameSchema` is
|
|
795
|
+
* closed, and a contribution naming a section this shell does not declare
|
|
796
|
+
* renders nowhere — so the composition refuses it here rather than dropping it
|
|
797
|
+
* silently.
|
|
798
|
+
*/
|
|
799
|
+
function composeNav(sections, entries) {
|
|
800
|
+
const declared = new Set(sections.map((section) => section.key));
|
|
801
|
+
for (const entry of entries) {
|
|
802
|
+
for (const item of entry.contributions.nav ?? []) {
|
|
803
|
+
if (declared.has(item.section))
|
|
804
|
+
continue;
|
|
805
|
+
throw new Error(`Module '${entry.moduleId}' contributes a sidebar entry in section '${item.section}', ` +
|
|
806
|
+
`which this shell does not declare. An entry in a section nobody renders is a ` +
|
|
807
|
+
`surface that silently appears nowhere.`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return sections.map((section) => ({
|
|
811
|
+
...section,
|
|
812
|
+
items: [
|
|
813
|
+
...section.items,
|
|
814
|
+
...registryNavFor(section.key, entries).map((item) => ({
|
|
815
|
+
to: item.to,
|
|
816
|
+
labelKey: item.labelKey,
|
|
817
|
+
labelScope: item.module,
|
|
818
|
+
icon: resolveIcon(item.icon),
|
|
819
|
+
// `exactOptionalPropertyTypes` — an ungated entry has no property,
|
|
820
|
+
// not a property holding `undefined`.
|
|
821
|
+
...(item.requiredPermission === undefined
|
|
822
|
+
? {}
|
|
823
|
+
: { requiredPermission: item.requiredPermission }),
|
|
824
|
+
module: item.module,
|
|
825
|
+
})),
|
|
826
|
+
],
|
|
827
|
+
}));
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* The host's sections with every module's contributed entry folded in.
|
|
831
|
+
*
|
|
832
|
+
* A hook rather than a module constant (feature 110, T120), because the
|
|
833
|
+
* registry is now the admin project's value and arrives through
|
|
834
|
+
* `AdminRegistryProvider`: a constant evaluated at import time would have to
|
|
835
|
+
* read a file this package may not name. `useMemo` keys on the registry
|
|
836
|
+
* identity, which is a module-scope array in every real build, so the
|
|
837
|
+
* composition runs once per mount exactly as the constant did once per process
|
|
838
|
+
* — and `composeNav`'s refusal of an undeclared section now raises during
|
|
839
|
+
* render instead of during import, where a test can see which section it was.
|
|
840
|
+
*/
|
|
841
|
+
function useComposedNav() {
|
|
842
|
+
const entries = useAdminRegistry();
|
|
843
|
+
return useMemo(() => composeNav(NAV, entries), [entries]);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* `t` for a nav entry's label, in whichever scope the entry declares.
|
|
847
|
+
*
|
|
848
|
+
* The host's entries resolve in `core` and a module's in its own namespace, and
|
|
849
|
+
* both go through the one translation context — a second resolver would be the
|
|
850
|
+
* drift issue #230 removed from the visibility question, in the label.
|
|
851
|
+
*/
|
|
852
|
+
function useNavLabel() {
|
|
853
|
+
const { t } = useTranslationContext();
|
|
854
|
+
return useCallback((item) => t(item.labelScope ?? 'core', item.labelKey), [t]);
|
|
855
|
+
}
|
|
856
|
+
export function AppShell() {
|
|
857
|
+
const { me, logout, hasPermission } = useAuth();
|
|
858
|
+
// The host's nav with every installed module's contributed entry folded in.
|
|
859
|
+
const composedNav = useComposedNav();
|
|
860
|
+
// Feature 073 — the effective enabled-set. A module that is off contributes
|
|
861
|
+
// no sidebar entry and no palette entry; neither surface recombines the two
|
|
862
|
+
// presence axes, because the server already did.
|
|
863
|
+
const { isPresent: isModulePresent } = useModulePresence();
|
|
864
|
+
const isVisible = useSurfaceVisibility();
|
|
865
|
+
const navigate = useNavigate();
|
|
866
|
+
const location = useLocation();
|
|
867
|
+
const [collapsed, setCollapsed] = useState(() => loadCollapsed());
|
|
868
|
+
const [railMode, setRailMode] = useState(() => loadRailMode());
|
|
869
|
+
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
870
|
+
const [navDrawerOpen, setNavDrawerOpen] = useState(false);
|
|
871
|
+
const sidebarRef = useRef(null);
|
|
872
|
+
const viewportTier = useViewportTier();
|
|
873
|
+
const isMobile = viewportTier === 'mobile';
|
|
874
|
+
useFocusTrap(sidebarRef, isMobile && navDrawerOpen);
|
|
875
|
+
const toggleRailMode = useCallback(() => {
|
|
876
|
+
setRailMode((prev) => {
|
|
877
|
+
const next = !prev;
|
|
878
|
+
persistRailMode(next);
|
|
879
|
+
return next;
|
|
880
|
+
});
|
|
881
|
+
}, []);
|
|
882
|
+
const t = useTranslation('core');
|
|
883
|
+
const navLabel = useNavLabel();
|
|
884
|
+
// The raw, scope-taking resolver, for the breadcrumb: a crumb a module
|
|
885
|
+
// contributed resolves its label in that module's own namespace.
|
|
886
|
+
const { t: tScoped } = useTranslationContext();
|
|
887
|
+
const fullName = me ? `${me.adminUser.firstName} ${me.adminUser.lastName}`.trim() : '';
|
|
888
|
+
const role = me?.role?.name ?? 'Admin';
|
|
889
|
+
const toggleSection = useCallback((key) => {
|
|
890
|
+
setCollapsed((prev) => {
|
|
891
|
+
const next = new Set(prev);
|
|
892
|
+
if (next.has(key))
|
|
893
|
+
next.delete(key);
|
|
894
|
+
else
|
|
895
|
+
next.add(key);
|
|
896
|
+
persistCollapsed(next);
|
|
897
|
+
return next;
|
|
898
|
+
});
|
|
899
|
+
}, []);
|
|
900
|
+
// ⌘K opens the palette anywhere in the admin; ⌘B toggles the
|
|
901
|
+
// sidebar rail (matches the shadcn / VS Code shortcut convention).
|
|
902
|
+
useEffect(() => {
|
|
903
|
+
const onKey = (e) => {
|
|
904
|
+
// `!e.shiftKey` keeps ⌘K off Ctrl/⌘+Shift+K, which pages claim for
|
|
905
|
+
// their own in-page search (e.g. the Settings page's filter box).
|
|
906
|
+
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === 'k') {
|
|
907
|
+
e.preventDefault();
|
|
908
|
+
setPaletteOpen(true);
|
|
909
|
+
}
|
|
910
|
+
else if (!isMobile &&
|
|
911
|
+
(e.metaKey || e.ctrlKey) &&
|
|
912
|
+
e.key.toLowerCase() === 'b') {
|
|
913
|
+
e.preventDefault();
|
|
914
|
+
toggleRailMode();
|
|
915
|
+
}
|
|
916
|
+
else if (e.key === 'Escape' && navDrawerOpen) {
|
|
917
|
+
setNavDrawerOpen(false);
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
window.addEventListener('keydown', onKey);
|
|
921
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
922
|
+
}, [toggleRailMode, isMobile, navDrawerOpen]);
|
|
923
|
+
useEffect(() => {
|
|
924
|
+
setNavDrawerOpen(false);
|
|
925
|
+
}, [location.pathname]);
|
|
926
|
+
const crumbs = useMemo(() => buildCrumbs(location.pathname, composedNav), [location.pathname, composedNav]);
|
|
927
|
+
const isCmsRoute = location.pathname.startsWith('/cms');
|
|
928
|
+
return (_jsxs("div", { className: cn('b2b-app-layout', isMobile ? null : railMode ? 'b2b-app-layout--rail' : 'b2b-app-layout--expanded'), children: [isMobile && navDrawerOpen ? (_jsx("button", { type: "button", className: "b2b-nav-drawer-backdrop", "aria-label": t('appShell.mobileMenu.close'), onClick: () => setNavDrawerOpen(false) })) : null, _jsxs("aside", { ref: sidebarRef, className: cn('b2b-sidebar', railMode && !isMobile && 'b2b-sidebar--rail', isMobile && navDrawerOpen && 'b2b-sidebar--drawer-open'), "aria-hidden": isMobile && !navDrawerOpen ? true : undefined, role: "navigation", "aria-label": t('appShell.mobileMenu.title'), children: [_jsxs(NavLink, { to: "/", end: true, className: "b2b-sidebar__brand", style: { textDecoration: 'none', color: 'inherit', cursor: 'pointer' }, "aria-label": t('appShell.brand.dashboardLink'), title: railMode ? t('appShell.brand.text') : undefined, children: [_jsx("span", { className: "b2b-sidebar__brand-logo", children: _jsx(BrandLogo, { label: t('appShell.brand.text') }) }), _jsxs("span", { className: "b2b-sidebar__brand-text", children: [t('appShell.brand.textPrimary'), ' ', _jsx("span", { className: "b2b-sidebar__brand-text-accent", children: t('appShell.brand.textAccent') })] })] }), _jsx("div", { className: "b2b-sidebar__search", children: railMode ? (_jsx("button", { type: "button", className: "b2b-sidebar__search-rail", onClick: () => setPaletteOpen(true), title: t('appShell.search.openPalette'), "aria-label": t('appShell.search.openPalette'), children: _jsx(Search, { size: 16 }) })) : (_jsxs(_Fragment, { children: [_jsx(Search, { size: 14, className: "b2b-sidebar__search-icon" }), _jsx("input", { className: "b2b-sidebar__search-input", placeholder: t('appShell.search.placeholder'), readOnly: true, onClick: () => setPaletteOpen(true), onFocus: () => setPaletteOpen(true) }), _jsx("span", { className: "b2b-sidebar__search-kbd", children: t('appShell.search.shortcutSymbol') })] })) }), _jsx("nav", { className: "b2b-sidebar__nav", children: composedNav.map((section) => {
|
|
929
|
+
const isOpen = !collapsed.has(section.key);
|
|
930
|
+
// Permission-gate every entry, then presence-gate it (feature 073 /
|
|
931
|
+
// FR-031). Sections with no remaining visible items fold away
|
|
932
|
+
// entirely, which is what makes a whole domain disappear when every
|
|
933
|
+
// module in it is off — the same behaviour restricted admins have
|
|
934
|
+
// always had.
|
|
935
|
+
//
|
|
936
|
+
// Issue #230 — the predicate is `isSurfaceVisible`, shared with the
|
|
937
|
+
// command palette and the dashboard quick actions. The palette used
|
|
938
|
+
// to run a second, weaker copy of it; a shared helper is what stops
|
|
939
|
+
// the three from disagreeing again.
|
|
940
|
+
const visibleItems = section.items.filter(isVisible);
|
|
941
|
+
if (visibleItems.length === 0)
|
|
942
|
+
return null;
|
|
943
|
+
// Feature 019 / 021 — section labels go through useTranslation('core').
|
|
944
|
+
// The empty-labelKey "main" cluster keeps no label; every other
|
|
945
|
+
// group resolves its declared `labelKey`. Polish strings live
|
|
946
|
+
// in packages/modules/_i18n/i18n/pl.json under the same key.
|
|
947
|
+
const translatedLabel = section.labelKey ? t(section.labelKey) : '';
|
|
948
|
+
// In rail mode the section reduces to one icon (the first
|
|
949
|
+
// visible item's icon). Hover or click reveals a popover
|
|
950
|
+
// listing every visible item in the section.
|
|
951
|
+
if (railMode) {
|
|
952
|
+
return (_jsx(RailSection, { section: section, visibleItems: visibleItems, translatedLabel: translatedLabel }, section.key));
|
|
953
|
+
}
|
|
954
|
+
return (_jsxs("div", { className: cn('b2b-sidebar__group', !isOpen && section.labelKey && 'b2b-sidebar__group--collapsed'), children: [section.labelKey ? (_jsxs("button", { type: "button", className: "b2b-sidebar__group-label", onClick: () => toggleSection(section.key), children: [_jsx(ChevronDown, { size: 11 }), _jsx("span", { children: translatedLabel })] })) : null, _jsx("div", { className: "b2b-sidebar__items", children: visibleItems.map((item) => {
|
|
955
|
+
const Icon = item.icon;
|
|
956
|
+
// Force exact-match (`end`) when another item in the same
|
|
957
|
+
// section nests under this item's path. Without this, a
|
|
958
|
+
// parent route like `/inventory` lights up alongside its
|
|
959
|
+
// children (`/inventory/low-stock`, `/inventory/import`,
|
|
960
|
+
// …) because NavLink's default match is prefix-based.
|
|
961
|
+
// The home link (`/`) is the canonical example and is
|
|
962
|
+
// covered by the same rule (every other item starts with
|
|
963
|
+
// `/`).
|
|
964
|
+
const hasNestedSibling = section.items.some((other) => other !== item &&
|
|
965
|
+
other.to.startsWith(item.to === '/' ? '/' : `${item.to}/`));
|
|
966
|
+
return (_jsxs(NavLink, { to: item.to, end: item.to === '/' || hasNestedSibling, className: ({ isActive }) => cn('b2b-nav-item', isActive && 'is-active'), children: [_jsx(Icon, { size: 16 }), _jsx("span", { style: { flex: 1 }, children: navLabel(item) })] }, item.to));
|
|
967
|
+
}) })] }, section.key));
|
|
968
|
+
}) }), me ? (_jsxs("div", { className: "b2b-sidebar__foot", children: [_jsxs(NavLink, { to: "/profile", className: "b2b-sidebar__foot-identity", title: railMode
|
|
969
|
+
? `${fullName || me.adminUser.email} — ${t('appShell.profileMenu.profile')}`
|
|
970
|
+
: t('appShell.profileMenu.profile'), "aria-label": t('appShell.profileMenu.profile'), children: [_jsx("div", { className: "b2b-avatar", children: (fullName || me.adminUser.email).slice(0, 2).toUpperCase() }), _jsxs("div", { className: "meta", children: [_jsx("div", { className: "name", children: fullName || me.adminUser.email }), _jsx("div", { className: "role", children: role })] })] }), _jsx("button", { type: "button", className: "icon-btn", title: t('appShell.profileMenu.signOut'), "aria-label": t('appShell.profileMenu.signOut'), onClick: () => void logout(), children: _jsx(LogOut, { size: 14 }) })] })) : null, !isMobile ? (_jsxs("button", { type: "button", className: "b2b-sidebar__rail-toggle", onClick: toggleRailMode, title: railMode
|
|
971
|
+
? (t('appShell.sidebarToggle.expand') || 'Expand sidebar (⌘B)')
|
|
972
|
+
: (t('appShell.sidebarToggle.collapse') || 'Collapse sidebar (⌘B)'), "aria-label": railMode
|
|
973
|
+
? (t('appShell.sidebarToggle.expand') || 'Expand sidebar')
|
|
974
|
+
: (t('appShell.sidebarToggle.collapse') || 'Collapse sidebar'), "aria-pressed": railMode, children: [railMode ? _jsx(PanelLeftOpen, { size: 14 }) : _jsx(PanelLeftClose, { size: 14 }), !railMode ? (_jsx("span", { className: "b2b-sidebar__rail-toggle-label", children: t('appShell.sidebarToggle.collapse') || 'Collapse' })) : null] })) : null] }), _jsxs("div", { style: { minWidth: 0, display: 'flex', flexDirection: 'column' }, children: [_jsxs("div", { className: "b2b-topbar", children: [isMobile ? (_jsx("button", { type: "button", className: "b2b-topbar__menu-btn", "aria-label": t('appShell.mobileMenu.open'), "aria-expanded": navDrawerOpen, onClick: () => setNavDrawerOpen((open) => !open), children: _jsx(Menu, { size: 20 }) })) : null, _jsx("div", { className: isCmsRoute ? 'b2b-topbar__crumbs-inner b2b-page b2b-page--wide' : 'b2b-topbar__crumbs', children: crumbs.map((c, i) => (_jsxs("span", { style: { display: 'inline-flex', alignItems: 'center', gap: 6 }, children: [i > 0 ? _jsx(ChevronRight, { size: 12, className: "crumb-sep" }) : null, c.href ? (_jsx(NavLink, { to: c.href, className: "crumb-link", style: { color: 'inherit', textDecoration: 'none' }, children: c.labelKey !== undefined ? tScoped(c.labelScope ?? 'core', c.labelKey) : c.literal })) : (_jsx("span", { className: "crumb-cur", children: c.labelKey !== undefined ? tScoped(c.labelScope ?? 'core', c.labelKey) : c.literal }))] }, i))) }), _jsxs("div", { className: "b2b-topbar__actions", children: [_jsx("button", { type: "button", className: "b2b-topbar__icon-btn", title: t('appShell.search.openPalette'), "aria-label": t('appShell.search.openPalette'), onClick: () => setPaletteOpen(true), children: _jsx(Search, { size: 16 }) }), isModulePresent('admin_notifications') && hasPermission('admin:read') ? (_jsx(NotificationBell, {})) : null, _jsx("button", { type: "button", className: "b2b-topbar__icon-btn b2b-topbar__hide-mobile", title: t('appShell.topbar.help'), "aria-label": t('appShell.topbar.help'), children: _jsx(HelpCircle, { size: 16 }) }), _jsx("div", { style: { width: 1, height: 22, background: 'var(--border-color)', margin: '0 4px' } }), _jsx(LanguagePicker, {}), me ? (_jsx(NavLink, { to: "/profile", className: "b2b-avatar", title: `${fullName || me.adminUser.email} — ${t('appShell.profileMenu.profile')}`, "aria-label": t('appShell.profileMenu.profile'), style: { textDecoration: 'none', cursor: 'pointer' }, children: (fullName || me.adminUser.email).slice(0, 2).toUpperCase() })) : null] })] }), _jsxs("main", { className: "b2b-main", style: { flex: 1, overflow: 'auto' }, children: [_jsx("div", { className: "b2b-impersonation-slot", "data-impersonation-banner": true }), _jsx(Outlet, {})] })] }), _jsx(CommandPalette, { open: paletteOpen, onClose: () => setPaletteOpen(false), onNavigate: (to) => {
|
|
975
|
+
navigate(to);
|
|
976
|
+
setPaletteOpen(false);
|
|
977
|
+
} })] }));
|
|
978
|
+
}
|
|
979
|
+
function RailSection(props) {
|
|
980
|
+
const { section, visibleItems, translatedLabel } = props;
|
|
981
|
+
const navLabel = useNavLabel();
|
|
982
|
+
const [hover, setHover] = useState(false);
|
|
983
|
+
const [sticky, setSticky] = useState(false);
|
|
984
|
+
const wrapperRef = useRef(null);
|
|
985
|
+
const closeTimerRef = useRef(null);
|
|
986
|
+
const open = hover || sticky;
|
|
987
|
+
const location = useLocation();
|
|
988
|
+
// Active-state for the rail icon: light up when ANY visible item in
|
|
989
|
+
// the section matches the current URL (exact for the home route,
|
|
990
|
+
// prefix-with-segment-boundary for everything else).
|
|
991
|
+
const sectionActive = visibleItems.some((item) => {
|
|
992
|
+
if (item.to === '/')
|
|
993
|
+
return location.pathname === '/';
|
|
994
|
+
return (location.pathname === item.to ||
|
|
995
|
+
location.pathname.startsWith(`${item.to}/`));
|
|
996
|
+
});
|
|
997
|
+
const cancelClose = useCallback(() => {
|
|
998
|
+
if (closeTimerRef.current !== null) {
|
|
999
|
+
window.clearTimeout(closeTimerRef.current);
|
|
1000
|
+
closeTimerRef.current = null;
|
|
1001
|
+
}
|
|
1002
|
+
}, []);
|
|
1003
|
+
const scheduleClose = useCallback(() => {
|
|
1004
|
+
cancelClose();
|
|
1005
|
+
closeTimerRef.current = window.setTimeout(() => setHover(false), 120);
|
|
1006
|
+
}, [cancelClose]);
|
|
1007
|
+
// Click outside the wrapper closes the sticky popover.
|
|
1008
|
+
useEffect(() => {
|
|
1009
|
+
if (!sticky)
|
|
1010
|
+
return;
|
|
1011
|
+
const onDown = (e) => {
|
|
1012
|
+
if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
|
|
1013
|
+
setSticky(false);
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
window.addEventListener('mousedown', onDown);
|
|
1017
|
+
return () => window.removeEventListener('mousedown', onDown);
|
|
1018
|
+
}, [sticky]);
|
|
1019
|
+
useEffect(() => () => cancelClose(), [cancelClose]);
|
|
1020
|
+
const railSource = visibleItems[0] ?? section.items[0];
|
|
1021
|
+
if (!railSource)
|
|
1022
|
+
return null;
|
|
1023
|
+
const RailIcon = railSource.icon;
|
|
1024
|
+
const headerLabel = translatedLabel || (visibleItems[0] ? navLabel(visibleItems[0]) : '');
|
|
1025
|
+
// Single-item section: render a plain link with no popover. The
|
|
1026
|
+
// hover area is the link itself; tooltip carries the label.
|
|
1027
|
+
if (visibleItems.length === 1) {
|
|
1028
|
+
const only = visibleItems[0];
|
|
1029
|
+
const Icon = only.icon;
|
|
1030
|
+
const onlyLabel = navLabel(only);
|
|
1031
|
+
return (_jsx("div", { className: "b2b-sidebar__rail-row", children: _jsx(NavLink, { to: only.to, end: only.to === '/', className: ({ isActive }) => cn('b2b-nav-item b2b-nav-item--rail', isActive && 'is-active'), title: onlyLabel, "aria-label": onlyLabel, children: _jsx(Icon, { size: 18 }) }) }));
|
|
1032
|
+
}
|
|
1033
|
+
return (_jsxs("div", { ref: wrapperRef, className: "b2b-sidebar__rail-row", onMouseEnter: () => {
|
|
1034
|
+
cancelClose();
|
|
1035
|
+
setHover(true);
|
|
1036
|
+
}, onMouseLeave: scheduleClose, children: [_jsx("button", { type: "button", className: cn('b2b-nav-item b2b-nav-item--rail', sectionActive && 'is-active', open && 'is-open'), onClick: () => setSticky((prev) => !prev), "aria-haspopup": "menu", "aria-expanded": open, "aria-label": headerLabel, title: headerLabel, children: _jsx(RailIcon, { size: 18 }) }), open ? (_jsxs("div", { className: "b2b-sidebar__rail-popover", role: "menu", children: [headerLabel ? (_jsx("div", { className: "b2b-sidebar__rail-popover-header", children: headerLabel })) : null, visibleItems.map((item) => {
|
|
1037
|
+
const Icon = item.icon;
|
|
1038
|
+
const hasNestedSibling = visibleItems.some((other) => other !== item &&
|
|
1039
|
+
other.to.startsWith(item.to === '/' ? '/' : `${item.to}/`));
|
|
1040
|
+
return (_jsxs(NavLink, { to: item.to, end: item.to === '/' || hasNestedSibling, role: "menuitem", className: ({ isActive }) => cn('b2b-sidebar__rail-popover-item', isActive && 'is-active'), onClick: () => {
|
|
1041
|
+
setSticky(false);
|
|
1042
|
+
setHover(false);
|
|
1043
|
+
}, children: [_jsx(Icon, { size: 14 }), _jsx("span", { children: navLabel(item) })] }, item.to));
|
|
1044
|
+
})] })) : null] }));
|
|
1045
|
+
}
|
|
1046
|
+
/** Sentinel route for the assistant entry row — switches the palette mode
|
|
1047
|
+
* instead of navigating (feature 043; module_actions are navigation-only). */
|
|
1048
|
+
const PROMPT_MODE_SENTINEL = '__prompt_actions_mode__';
|
|
1049
|
+
/** Module-level capability cache: one fetch per session unless it failed. */
|
|
1050
|
+
let promptCapabilityCache = null;
|
|
1051
|
+
/** Test seam — interaction tests exercise multiple capability states. */
|
|
1052
|
+
export function resetPromptCapabilityCacheForTesting() {
|
|
1053
|
+
promptCapabilityCache = null;
|
|
1054
|
+
}
|
|
1055
|
+
function CommandPalette(props) {
|
|
1056
|
+
const { open, onClose, onNavigate } = props;
|
|
1057
|
+
const t = useTranslation('core');
|
|
1058
|
+
const tp = useTranslation('prompt_actions');
|
|
1059
|
+
const { hasPermission } = useAuth();
|
|
1060
|
+
const isVisible = useSurfaceVisibility();
|
|
1061
|
+
const [query, setQuery] = useState('');
|
|
1062
|
+
const [cursor, setCursor] = useState(0);
|
|
1063
|
+
const [mode, setMode] = useState('search');
|
|
1064
|
+
const [assistantReady, setAssistantReady] = useState(promptCapabilityCache === 'ready');
|
|
1065
|
+
const [unseen, setUnseen] = useState([]);
|
|
1066
|
+
const [initialRequest, setInitialRequest] = useState(undefined);
|
|
1067
|
+
// Seed text carried into prompt mode by the `/ai <command>` palette shortcut.
|
|
1068
|
+
const [initialPrompt, setInitialPrompt] = useState('');
|
|
1069
|
+
const inputRef = useRef(null);
|
|
1070
|
+
const { actions: registryActions } = useAdminActions(query);
|
|
1071
|
+
// Feature 043 — capability probe, only when the operator may use the
|
|
1072
|
+
// assistant. With the feature disabled/unconfigured (or the permission
|
|
1073
|
+
// missing) the palette renders exactly as before (FR-015/FR-020, SC-006).
|
|
1074
|
+
const mayUseAssistant = hasPermission('prompt_actions:use');
|
|
1075
|
+
useEffect(() => {
|
|
1076
|
+
if (!open || !mayUseAssistant || promptCapabilityCache !== null)
|
|
1077
|
+
return;
|
|
1078
|
+
void getPromptCapability()
|
|
1079
|
+
.then((cap) => {
|
|
1080
|
+
promptCapabilityCache = cap.status === 'ready' ? 'ready' : 'unavailable';
|
|
1081
|
+
setAssistantReady(promptCapabilityCache === 'ready');
|
|
1082
|
+
})
|
|
1083
|
+
.catch(() => {
|
|
1084
|
+
promptCapabilityCache = 'unavailable';
|
|
1085
|
+
});
|
|
1086
|
+
}, [open, mayUseAssistant]);
|
|
1087
|
+
// FR-018 — completion notice: prompts that finished while the palette was
|
|
1088
|
+
// closed surface on the next open until acknowledged.
|
|
1089
|
+
useEffect(() => {
|
|
1090
|
+
if (!open || !mayUseAssistant || !assistantReady) {
|
|
1091
|
+
setUnseen([]);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
void listUnseenPromptRequests()
|
|
1095
|
+
.then(setUnseen)
|
|
1096
|
+
.catch(() => setUnseen([]));
|
|
1097
|
+
}, [open, mayUseAssistant, assistantReady]);
|
|
1098
|
+
// Map registry-supplied actions into the local PaletteItem shape so
|
|
1099
|
+
// the rendering loop stays uniform across Navigate (static) and
|
|
1100
|
+
// Actions (registry-driven) groups.
|
|
1101
|
+
const actionItems = useMemo(() => registryActions.map((a) => ({
|
|
1102
|
+
group: 'Actions',
|
|
1103
|
+
labelMode: 'literal',
|
|
1104
|
+
label: a.label,
|
|
1105
|
+
sub: a.description ?? '',
|
|
1106
|
+
icon: resolveIcon(a.icon),
|
|
1107
|
+
to: a.targetRoute,
|
|
1108
|
+
// Carried for shape uniformity only: an Actions row is filtered by
|
|
1109
|
+
// `useAdminActions(query)` before it reaches this mapper, so nothing
|
|
1110
|
+
// below reads this field. Kept unfolded — `matchesQuery` normalises
|
|
1111
|
+
// its haystack at compare time, and a second, differently-normalised
|
|
1112
|
+
// copy of the same data is what issue #233 was about.
|
|
1113
|
+
keywords: a.keywords.join(' '),
|
|
1114
|
+
})), [registryActions]);
|
|
1115
|
+
// Feature 073 / FR-032 plus issue #230 — the Navigate group, filtered on both
|
|
1116
|
+
// axes by the same predicate the sidebar uses. The Actions group needs nothing
|
|
1117
|
+
// here: the server already resolves both axes for it.
|
|
1118
|
+
const navigateItems = useMemo(() => PALETTE_ITEMS.filter(isVisible), [isVisible]);
|
|
1119
|
+
// Build a derived view that translates Navigate-group keys at render
|
|
1120
|
+
// time and leaves the registry-supplied Actions strings untouched.
|
|
1121
|
+
const resolveLabel = (it) => it.labelMode === 'key' ? t(it.label) : it.label;
|
|
1122
|
+
const resolveSub = (it) => it.labelMode === 'key' ? t(it.sub) : it.sub;
|
|
1123
|
+
// Feature 043 — pinned assistant entry (only when enabled + configured +
|
|
1124
|
+
// permitted). Always listed first; matched generously while typing.
|
|
1125
|
+
const assistantItems = useMemo(() => {
|
|
1126
|
+
if (!mayUseAssistant || !assistantReady)
|
|
1127
|
+
return [];
|
|
1128
|
+
return [
|
|
1129
|
+
{
|
|
1130
|
+
group: 'Assistant',
|
|
1131
|
+
labelMode: 'literal',
|
|
1132
|
+
label: tp('palette.entry.label'),
|
|
1133
|
+
sub: tp('palette.entry.description'),
|
|
1134
|
+
icon: Sparkles,
|
|
1135
|
+
to: PROMPT_MODE_SENTINEL,
|
|
1136
|
+
keywords: 'assistant ai prompt ask asystent zapytaj',
|
|
1137
|
+
},
|
|
1138
|
+
];
|
|
1139
|
+
}, [mayUseAssistant, assistantReady, tp]);
|
|
1140
|
+
const items = useMemo(() => {
|
|
1141
|
+
if (!query.trim())
|
|
1142
|
+
return [...assistantItems, ...navigateItems, ...actionItems];
|
|
1143
|
+
// Folded once here, then compared against every row — see `matchesQuery`.
|
|
1144
|
+
const q = normalize(query);
|
|
1145
|
+
const filteredAssistant = assistantItems.filter((i) => matchesQuery(q, i.label, i.sub, i.keywords));
|
|
1146
|
+
// Navigate group: filter against the *translated* label + sub plus
|
|
1147
|
+
// the raw keywords list so a Polish user can search in Polish and
|
|
1148
|
+
// an English user in English.
|
|
1149
|
+
const filteredNav = navigateItems.filter((i) => matchesQuery(q, resolveLabel(i), resolveSub(i), i.keywords));
|
|
1150
|
+
return [...filteredAssistant, ...filteredNav, ...actionItems];
|
|
1151
|
+
// `resolveLabel` / `resolveSub` are render-scoped closures over `t`; the
|
|
1152
|
+
// list re-derives on every language change through the parent re-render.
|
|
1153
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1154
|
+
}, [query, actionItems, assistantItems, navigateItems]);
|
|
1155
|
+
// Activating the assistant row switches the palette body instead of
|
|
1156
|
+
// navigating (feature 043); everything else keeps the navigation path.
|
|
1157
|
+
const activate = useCallback((to) => {
|
|
1158
|
+
if (to === PROMPT_MODE_SENTINEL) {
|
|
1159
|
+
setInitialPrompt('');
|
|
1160
|
+
setMode('prompt');
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
onNavigate(to);
|
|
1164
|
+
}, [onNavigate]);
|
|
1165
|
+
// `/ai` shortcut: typing `/ai` (optionally followed by a command) in the
|
|
1166
|
+
// palette input launches prompt mode directly, carrying any typed command
|
|
1167
|
+
// into the assistant input — equivalent to opening "Ask the assistant" and
|
|
1168
|
+
// typing it. Only intercepts when the assistant is actually usable.
|
|
1169
|
+
const handleQueryChange = useCallback((value) => {
|
|
1170
|
+
const match = /^\/ai(?:\s+(.*))?$/i.exec(value);
|
|
1171
|
+
if (match && mayUseAssistant && assistantReady) {
|
|
1172
|
+
setQuery('');
|
|
1173
|
+
setInitialPrompt(match[1] ?? '');
|
|
1174
|
+
setMode('prompt');
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
setQuery(value);
|
|
1178
|
+
}, [mayUseAssistant, assistantReady]);
|
|
1179
|
+
useEffect(() => {
|
|
1180
|
+
setCursor(0);
|
|
1181
|
+
}, [query, open]);
|
|
1182
|
+
useEffect(() => {
|
|
1183
|
+
if (!open) {
|
|
1184
|
+
setQuery('');
|
|
1185
|
+
setMode('search');
|
|
1186
|
+
setInitialRequest(undefined);
|
|
1187
|
+
setInitialPrompt('');
|
|
1188
|
+
}
|
|
1189
|
+
}, [open]);
|
|
1190
|
+
useEffect(() => {
|
|
1191
|
+
if (!open)
|
|
1192
|
+
return;
|
|
1193
|
+
if (mode === 'prompt') {
|
|
1194
|
+
// Prompt mode owns its own inputs; only Esc is handled here — it
|
|
1195
|
+
// returns to the classic palette rather than closing (research §R9).
|
|
1196
|
+
const onKey = (e) => {
|
|
1197
|
+
if (e.key === 'Escape') {
|
|
1198
|
+
e.stopPropagation();
|
|
1199
|
+
setMode('search');
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
window.addEventListener('keydown', onKey);
|
|
1203
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
1204
|
+
}
|
|
1205
|
+
inputRef.current?.focus();
|
|
1206
|
+
const onKey = (e) => {
|
|
1207
|
+
if (e.key === 'ArrowDown') {
|
|
1208
|
+
e.preventDefault();
|
|
1209
|
+
setCursor((c) => Math.min(items.length - 1, c + 1));
|
|
1210
|
+
}
|
|
1211
|
+
else if (e.key === 'ArrowUp') {
|
|
1212
|
+
e.preventDefault();
|
|
1213
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
1214
|
+
}
|
|
1215
|
+
else if (e.key === 'Enter') {
|
|
1216
|
+
e.preventDefault();
|
|
1217
|
+
const it = items[cursor];
|
|
1218
|
+
if (it)
|
|
1219
|
+
activate(it.to);
|
|
1220
|
+
}
|
|
1221
|
+
else if (e.key === 'Escape') {
|
|
1222
|
+
onClose();
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
window.addEventListener('keydown', onKey);
|
|
1226
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
1227
|
+
}, [open, mode, items, cursor, onClose, activate]);
|
|
1228
|
+
if (!open)
|
|
1229
|
+
return null;
|
|
1230
|
+
if (mode === 'prompt') {
|
|
1231
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "b2b-scrim", onClick: onClose }), _jsx("div", { className: "b2b-palette", role: "dialog", "aria-modal": "true", children: _jsx(PromptModePanel, { onExit: () => {
|
|
1232
|
+
setInitialRequest(undefined);
|
|
1233
|
+
setInitialPrompt('');
|
|
1234
|
+
setMode('search');
|
|
1235
|
+
}, ...(initialRequest ? { initialRequest } : {}), ...(initialPrompt ? { initialPrompt } : {}) }) })] }));
|
|
1236
|
+
}
|
|
1237
|
+
let lastGroup = '';
|
|
1238
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "b2b-scrim", onClick: onClose }), _jsxs("div", { className: "b2b-palette", role: "dialog", "aria-modal": "true", children: [_jsxs("div", { className: "b2b-palette__input", children: [_jsx(Search, { size: 18, style: { color: 'var(--fg-muted)' } }), _jsx("input", { ref: inputRef, placeholder: t('appShell.search.commandPalettePlaceholder'), value: query, onChange: (e) => handleQueryChange(e.target.value) }), _jsx(SpeechToTextButton, { startTitle: t('appShell.search.voiceInput'), stopTitle: t('appShell.search.voiceInputStop'), onTranscript: (text) => {
|
|
1239
|
+
setQuery((q) => (q ? `${q} ${text}` : text));
|
|
1240
|
+
inputRef.current?.focus();
|
|
1241
|
+
} }), _jsx("span", { className: "b2b-kbd-sm", children: "esc" })] }), _jsxs("div", { className: "b2b-palette__list", children: [unseen.length > 0 ? (_jsxs("div", { "data-testid": "prompt-unseen-notice", className: "b2b-palette__item", style: { cursor: 'pointer' }, onClick: () => {
|
|
1242
|
+
setInitialRequest(unseen[0]);
|
|
1243
|
+
setUnseen((u) => u.slice(1));
|
|
1244
|
+
setMode('prompt');
|
|
1245
|
+
}, children: [_jsx(Sparkles, { size: 16 }), _jsx("div", { children: _jsx("div", { children: tp('panel.unseenNotice') }) })] })) : null, items.length === 0 ? (_jsx("div", { style: { padding: 32, textAlign: 'center', color: 'var(--fg-muted)', fontSize: 13 }, children: t('appShell.search.noMatches') })) : (items.map((it, i) => {
|
|
1246
|
+
const showGroup = it.group !== lastGroup;
|
|
1247
|
+
lastGroup = it.group;
|
|
1248
|
+
const Icon = it.icon;
|
|
1249
|
+
const groupHeader = it.group === 'Navigate'
|
|
1250
|
+
? t('appShell.palette.group.navigate')
|
|
1251
|
+
: it.group === 'Assistant'
|
|
1252
|
+
? tp('palette.group.label')
|
|
1253
|
+
: t('appShell.palette.group.actions');
|
|
1254
|
+
const label = resolveLabel(it);
|
|
1255
|
+
const sub = resolveSub(it);
|
|
1256
|
+
return (_jsxs("div", { children: [showGroup ? _jsx("div", { className: "b2b-palette__group", children: groupHeader }) : null, _jsxs("div", { className: cn('b2b-palette__item', i === cursor && 'is-cur'), onMouseEnter: () => setCursor(i), onClick: () => activate(it.to), children: [_jsx(Icon, { size: 16 }), _jsxs("div", { children: [_jsx("div", { children: label }), _jsx("div", { style: { fontSize: 11, color: 'var(--fg-muted)' }, children: sub })] }), _jsx("span", { className: "meta", children: i === cursor ? _jsx("span", { className: "b2b-kbd-sm", children: "\u21B5" }) : null })] })] }, `${it.group}-${it.label}`));
|
|
1257
|
+
}))] })] })] }));
|
|
1258
|
+
}
|
|
1259
|
+
//# sourceMappingURL=AppShell.js.map
|