@vertesia/ui 1.5.0-dev.20260910.052912Z → 1.6.0-dev.20260912.110338Z
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/lib/boot/branding.d.ts +58 -0
- package/lib/boot/branding.d.ts.map +1 -0
- package/lib/boot/branding.js +62 -0
- package/lib/boot/branding.js.map +1 -0
- package/lib/boot/index.d.ts +27 -0
- package/lib/boot/index.d.ts.map +1 -0
- package/lib/boot/index.js +325 -0
- package/lib/boot/index.js.map +1 -0
- package/lib/boot/vite.d.ts +63 -0
- package/lib/boot/vite.d.ts.map +1 -0
- package/lib/boot/vite.js +108 -0
- package/lib/boot/vite.js.map +1 -0
- package/lib/core/components/Link.js +1 -1
- package/lib/core/components/Link.js.map +1 -1
- package/lib/core/components/Spinner.js +1 -1
- package/lib/core/components/Spinner.js.map +1 -1
- package/lib/core/components/TagsInput.js +7 -7
- package/lib/core/components/TagsInput.js.map +1 -1
- package/lib/core/components/shadcn/badge.js +1 -1
- package/lib/core/components/shadcn/badge.js.map +1 -1
- package/lib/core/components/shadcn/button.d.ts.map +1 -1
- package/lib/core/components/shadcn/button.js +3 -4
- package/lib/core/components/shadcn/button.js.map +1 -1
- package/lib/core/components/shadcn/filters/comboBox/DateCombobox.js +2 -2
- package/lib/core/components/shadcn/filters/comboBox/DateCombobox.js.map +1 -1
- package/lib/core/components/shadcn/filters/comboBox/SelectCombobox.js +1 -1
- package/lib/core/components/shadcn/filters/comboBox/SelectCombobox.js.map +1 -1
- package/lib/core/components/shadcn/filters/comboBox/StringListCombobox.js +1 -1
- package/lib/core/components/shadcn/filters/comboBox/StringListCombobox.js.map +1 -1
- package/lib/core/components/shadcn/filters/comboBox/TextCombobox.js +1 -1
- package/lib/core/components/shadcn/filters/comboBox/TextCombobox.js.map +1 -1
- package/lib/core/components/shadcn/filters/filter/dateFilter.js +1 -1
- package/lib/core/components/shadcn/filters/filter/dateFilter.js.map +1 -1
- package/lib/core/components/shadcn/filters/filter-styles.d.ts +1 -1
- package/lib/core/components/shadcn/filters/filter-styles.d.ts.map +1 -1
- package/lib/core/components/shadcn/filters/filter-styles.js +4 -4
- package/lib/core/components/shadcn/filters/filters.js +1 -1
- package/lib/core/components/shadcn/filters/filters.js.map +1 -1
- package/lib/core/components/shadcn/label.js +1 -1
- package/lib/core/components/shadcn/label.js.map +1 -1
- package/lib/core/components/shadcn/overflowTabs.js +2 -2
- package/lib/core/components/shadcn/overflowTabs.js.map +1 -1
- package/lib/core/components/shadcn/tabs.js +1 -1
- package/lib/core/components/shadcn/tabs.js.map +1 -1
- package/lib/env/index.d.ts +28 -1
- package/lib/env/index.d.ts.map +1 -1
- package/lib/env/index.js +51 -3
- package/lib/env/index.js.map +1 -1
- package/lib/features/activity-doc/ActivityDoc.js +1 -1
- package/lib/features/activity-doc/ActivityDoc.js.map +1 -1
- package/lib/features/agent/PayloadBuilder.d.ts +4 -0
- package/lib/features/agent/PayloadBuilder.d.ts.map +1 -1
- package/lib/features/agent/PayloadBuilder.js.map +1 -1
- package/lib/features/agent/chat/AgentRunFeedback.d.ts +59 -0
- package/lib/features/agent/chat/AgentRunFeedback.d.ts.map +1 -0
- package/lib/features/agent/chat/AgentRunFeedback.js +286 -0
- package/lib/features/agent/chat/AgentRunFeedback.js.map +1 -0
- package/lib/features/agent/chat/AskUserWidget.d.ts +1 -0
- package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
- package/lib/features/agent/chat/AskUserWidget.js +15 -4
- package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.js +3 -2
- package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts +3 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +16 -13
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/Header.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts +3 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +30 -8
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js +2 -0
- package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js.map +1 -1
- package/lib/features/agent/chat/index.d.ts +1 -0
- package/lib/features/agent/chat/index.d.ts.map +1 -1
- package/lib/features/agent/chat/index.js +1 -0
- package/lib/features/agent/chat/index.js.map +1 -1
- package/lib/features/facets/AgentRunnerFacetsNav.d.ts +8 -0
- package/lib/features/facets/AgentRunnerFacetsNav.d.ts.map +1 -1
- package/lib/features/facets/AgentRunnerFacetsNav.js +12 -0
- package/lib/features/facets/AgentRunnerFacetsNav.js.map +1 -1
- package/lib/features/layout/GenericPageNavHeader.js +1 -1
- package/lib/features/layout/GenericPageNavHeader.js.map +1 -1
- package/lib/features/magic-pdf/AnnotatedImageSlider.js +1 -1
- package/lib/features/magic-pdf/AnnotatedImageSlider.js.map +1 -1
- package/lib/features/magic-pdf/GroundedExtractionView.js +3 -3
- package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -1
- package/lib/features/permissions/UserPermissionsProvider.d.ts +15 -1
- package/lib/features/permissions/UserPermissionsProvider.d.ts.map +1 -1
- package/lib/features/permissions/UserPermissionsProvider.js +20 -12
- package/lib/features/permissions/UserPermissionsProvider.js.map +1 -1
- package/lib/features/store/objects/DocumentPreviewPanel.js +5 -5
- package/lib/features/store/objects/DocumentPreviewPanel.js.map +1 -1
- package/lib/features/store/objects/components/DocumentInput.js +1 -1
- package/lib/features/store/objects/components/DocumentInput.js.map +1 -1
- package/lib/features/store/objects/selection/SelectionActions.js +1 -1
- package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
- package/lib/features/store/objects/upload/DocumentUploadModal.js +1 -1
- package/lib/features/store/objects/upload/DocumentUploadModal.js.map +1 -1
- package/lib/features/store/types/TypeRegistry.d.ts +130 -0
- package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +130 -0
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
- package/lib/features/user/UserInfo.js +1 -1
- package/lib/features/user/UserInfo.js.map +1 -1
- package/lib/i18n/index.d.ts +5 -4
- package/lib/i18n/index.d.ts.map +1 -1
- package/lib/i18n/index.js +16 -2
- package/lib/i18n/index.js.map +1 -1
- package/lib/i18n/locales/ar.json +30 -0
- package/lib/i18n/locales/de.json +30 -0
- package/lib/i18n/locales/en.json +30 -0
- package/lib/i18n/locales/es.json +30 -0
- package/lib/i18n/locales/fr.json +30 -0
- package/lib/i18n/locales/it.json +30 -0
- package/lib/i18n/locales/ja.json +30 -0
- package/lib/i18n/locales/ko.json +30 -0
- package/lib/i18n/locales/pt.json +30 -0
- package/lib/i18n/locales/ru.json +30 -0
- package/lib/i18n/locales/tr.json +30 -0
- package/lib/i18n/locales/zh-TW.json +30 -0
- package/lib/i18n/locales/zh.json +30 -0
- package/lib/layout/ErrorPage.js +1 -1
- package/lib/layout/ErrorPage.js.map +1 -1
- package/lib/layout/Navbar.d.ts.map +1 -1
- package/lib/layout/Navbar.js +3 -2
- package/lib/layout/Navbar.js.map +1 -1
- package/lib/router/RouteComponent.d.ts.map +1 -1
- package/lib/router/RouteComponent.js +15 -12
- package/lib/router/RouteComponent.js.map +1 -1
- package/lib/session/UserSession.d.ts.map +1 -1
- package/lib/session/UserSession.js +6 -3
- package/lib/session/UserSession.js.map +1 -1
- package/lib/session/UserSessionProvider.d.ts +2 -1
- package/lib/session/UserSessionProvider.d.ts.map +1 -1
- package/lib/session/UserSessionProvider.js +10 -1
- package/lib/session/UserSessionProvider.js.map +1 -1
- package/lib/session/auth/composable.d.ts.map +1 -1
- package/lib/session/auth/composable.js +88 -3
- package/lib/session/auth/composable.js.map +1 -1
- package/lib/session/auth/domainRouting.d.ts +22 -12
- package/lib/session/auth/domainRouting.d.ts.map +1 -1
- package/lib/session/auth/domainRouting.js +35 -21
- package/lib/session/auth/domainRouting.js.map +1 -1
- package/lib/session/auth/useAuthState.d.ts +9 -4
- package/lib/session/auth/useAuthState.d.ts.map +1 -1
- package/lib/session/auth/useAuthState.js +9 -4
- package/lib/session/auth/useAuthState.js.map +1 -1
- package/lib/session/index.d.ts.map +1 -1
- package/lib/session/index.js +3 -1
- package/lib/session/index.js.map +1 -1
- package/lib/shell/AuthScreenPreview.d.ts +12 -0
- package/lib/shell/AuthScreenPreview.d.ts.map +1 -0
- package/lib/shell/AuthScreenPreview.js +140 -0
- package/lib/shell/AuthScreenPreview.js.map +1 -0
- package/lib/shell/BrandedAuthScreens.d.ts +19 -0
- package/lib/shell/BrandedAuthScreens.d.ts.map +1 -0
- package/lib/shell/BrandedAuthScreens.js +64 -0
- package/lib/shell/BrandedAuthScreens.js.map +1 -0
- package/lib/shell/SplashScreen.d.ts +13 -2
- package/lib/shell/SplashScreen.d.ts.map +1 -1
- package/lib/shell/SplashScreen.js +21 -20
- package/lib/shell/SplashScreen.js.map +1 -1
- package/lib/shell/VertesiaShell.d.ts +15 -4
- package/lib/shell/VertesiaShell.d.ts.map +1 -1
- package/lib/shell/VertesiaShell.js +8 -3
- package/lib/shell/VertesiaShell.js.map +1 -1
- package/lib/shell/apps/StandaloneApp.js +5 -1
- package/lib/shell/apps/StandaloneApp.js.map +1 -1
- package/lib/shell/index.d.ts +6 -0
- package/lib/shell/index.d.ts.map +1 -1
- package/lib/shell/index.js +5 -0
- package/lib/shell/index.js.map +1 -1
- package/lib/shell/login/SignInFlow.d.ts +68 -0
- package/lib/shell/login/SignInFlow.d.ts.map +1 -0
- package/lib/shell/login/SignInFlow.js +108 -0
- package/lib/shell/login/SignInFlow.js.map +1 -0
- package/lib/shell/login/SignInPageShell.d.ts +13 -0
- package/lib/shell/login/SignInPageShell.d.ts.map +1 -0
- package/lib/shell/login/SignInPageShell.js +14 -0
- package/lib/shell/login/SignInPageShell.js.map +1 -0
- package/lib/shell/login/SignInPrimitives.d.ts +3 -3
- package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
- package/lib/shell/login/SignInPrimitives.js +13 -11
- package/lib/shell/login/SignInPrimitives.js.map +1 -1
- package/lib/shell/login/SignInRestrictedEnvStep.d.ts +5 -0
- package/lib/shell/login/SignInRestrictedEnvStep.d.ts.map +1 -1
- package/lib/shell/login/SignInRestrictedEnvStep.js +8 -4
- package/lib/shell/login/SignInRestrictedEnvStep.js.map +1 -1
- package/lib/shell/login/SigninScreen.d.ts +38 -3
- package/lib/shell/login/SigninScreen.d.ts.map +1 -1
- package/lib/shell/login/SigninScreen.js +33 -86
- package/lib/shell/login/SigninScreen.js.map +1 -1
- package/lib/shell/login/SignupForm.d.ts +3 -1
- package/lib/shell/login/SignupForm.d.ts.map +1 -1
- package/lib/shell/login/SignupForm.js +7 -3
- package/lib/shell/login/SignupForm.js.map +1 -1
- package/lib/shell/login/signInUtils.d.ts +11 -0
- package/lib/shell/login/signInUtils.d.ts.map +1 -1
- package/lib/shell/login/signInUtils.js +29 -5
- package/lib/shell/login/signInUtils.js.map +1 -1
- package/lib/vertesia-ui-boot.js +237 -0
- package/lib/vertesia-ui-boot.js.map +1 -0
- package/lib/vertesia-ui-core.js +46 -46
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-env.js +1 -1
- package/lib/vertesia-ui-env.js.map +1 -1
- package/lib/vertesia-ui-features.js +222 -208
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.js +2 -2
- package/lib/vertesia-ui-i18n.js.map +1 -1
- package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js.map +1 -0
- package/lib/vertesia-ui-layout.js +13 -13
- package/lib/vertesia-ui-layout.js.map +1 -1
- package/lib/vertesia-ui-rich-text.js +68 -68
- package/lib/vertesia-ui-rich-text.js.map +1 -1
- package/lib/vertesia-ui-router.js +1 -1
- package/lib/vertesia-ui-router.js.map +1 -1
- package/lib/vertesia-ui-session.js +1 -1
- package/lib/vertesia-ui-session.js.map +1 -1
- package/lib/vertesia-ui-shell.js +58 -33
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +94 -94
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/lib/widgets/json-view/JSONCode.js +1 -1
- package/lib/widgets/json-view/JSONCode.js.map +1 -1
- package/lib/widgets/upload/DropZone.d.ts.map +1 -1
- package/lib/widgets/upload/DropZone.js +1 -1
- package/lib/widgets/upload/DropZone.js.map +1 -1
- package/llms.txt +6 -2
- package/package.json +44 -35
- package/src/__tests__/a11y.test.tsx +25 -0
- package/src/__tests__/theme.test.ts +91 -0
- package/src/boot/branding.test.ts +72 -0
- package/src/boot/branding.ts +108 -0
- package/src/boot/index.test.ts +216 -0
- package/src/boot/index.ts +356 -0
- package/src/boot/vite.test.ts +93 -0
- package/src/boot/vite.ts +118 -0
- package/src/core/components/Link.tsx +1 -1
- package/src/core/components/Spinner.tsx +1 -1
- package/src/core/components/TagsInput.tsx +7 -7
- package/src/core/components/shadcn/badge.tsx +1 -1
- package/src/core/components/shadcn/button.tsx +3 -5
- package/src/core/components/shadcn/filters/comboBox/DateCombobox.tsx +2 -2
- package/src/core/components/shadcn/filters/comboBox/SelectCombobox.tsx +1 -1
- package/src/core/components/shadcn/filters/comboBox/StringListCombobox.tsx +1 -1
- package/src/core/components/shadcn/filters/comboBox/TextCombobox.tsx +1 -1
- package/src/core/components/shadcn/filters/filter/dateFilter.tsx +1 -1
- package/src/core/components/shadcn/filters/filter-styles.ts +4 -4
- package/src/core/components/shadcn/filters/filters.tsx +1 -1
- package/src/core/components/shadcn/label.tsx +1 -1
- package/src/core/components/shadcn/overflowTabs.tsx +2 -2
- package/src/core/components/shadcn/tabs.tsx +1 -1
- package/src/css/utilities.css +7 -0
- package/src/env/index.test.ts +100 -0
- package/src/env/index.ts +66 -3
- package/src/features/activity-doc/ActivityDoc.tsx +1 -1
- package/src/features/agent/PayloadBuilder.tsx +4 -0
- package/src/features/agent/chat/AgentRunFeedback.test.tsx +330 -0
- package/src/features/agent/chat/AgentRunFeedback.tsx +452 -0
- package/src/features/agent/chat/AskUserWidget.test.tsx +24 -2
- package/src/features/agent/chat/AskUserWidget.tsx +19 -4
- package/src/features/agent/chat/ModernAgentConversation.tsx +46 -42
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +207 -0
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +50 -31
- package/src/features/agent/chat/ModernAgentOutput/Header.tsx +4 -0
- package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +42 -8
- package/src/features/agent/chat/ModernAgentOutput/SummaryConversation.ts +2 -0
- package/src/features/agent/chat/ModernAgentOutput/utils.test.ts +14 -0
- package/src/features/agent/chat/hooks/useFileProcessing.test.tsx +0 -5
- package/src/features/agent/chat/index.ts +6 -0
- package/src/features/facets/AgentRunnerFacetsNav.tsx +26 -0
- package/src/features/layout/GenericPageNavHeader.test.tsx +36 -0
- package/src/features/layout/GenericPageNavHeader.tsx +1 -1
- package/src/features/magic-pdf/AnnotatedImageSlider.tsx +1 -1
- package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
- package/src/features/permissions/UserPermissionsProvider.test.tsx +50 -1
- package/src/features/permissions/UserPermissionsProvider.tsx +82 -44
- package/src/features/store/objects/DocumentPreviewPanel.tsx +10 -10
- package/src/features/store/objects/components/DocumentInput.tsx +1 -1
- package/src/features/store/objects/selection/SelectionActions.tsx +4 -1
- package/src/features/store/objects/upload/DocumentUploadModal.tsx +1 -1
- package/src/features/user/UserInfo.tsx +1 -1
- package/src/i18n/index.tsx +25 -2
- package/src/i18n/locales/ar.json +30 -0
- package/src/i18n/locales/de.json +30 -0
- package/src/i18n/locales/en.json +30 -0
- package/src/i18n/locales/es.json +30 -0
- package/src/i18n/locales/fr.json +30 -0
- package/src/i18n/locales/it.json +30 -0
- package/src/i18n/locales/ja.json +30 -0
- package/src/i18n/locales/ko.json +30 -0
- package/src/i18n/locales/pt.json +30 -0
- package/src/i18n/locales/ru.json +30 -0
- package/src/i18n/locales/tr.json +30 -0
- package/src/i18n/locales/zh-TW.json +30 -0
- package/src/i18n/locales/zh.json +30 -0
- package/src/layout/ErrorPage.tsx +1 -1
- package/src/layout/Navbar.test.tsx +58 -0
- package/src/layout/Navbar.tsx +5 -2
- package/src/router/RouteComponent.test.tsx +65 -1
- package/src/router/RouteComponent.tsx +18 -15
- package/src/session/UserSession.test.ts +65 -0
- package/src/session/UserSession.ts +6 -4
- package/src/session/UserSessionProvider.strict-mode.test.tsx +44 -0
- package/src/session/UserSessionProvider.test.ts +62 -1
- package/src/session/UserSessionProvider.tsx +9 -1
- package/src/session/auth/composable.renewal.test.ts +170 -0
- package/src/session/auth/composable.test.ts +23 -1
- package/src/session/auth/composable.ts +101 -2
- package/src/session/auth/domainRouting.test.ts +35 -65
- package/src/session/auth/domainRouting.ts +51 -34
- package/src/session/auth/useAuthState.ts +11 -4
- package/src/session/index.ts +3 -1
- package/src/shell/AuthScreenPreview.test.tsx +44 -0
- package/src/shell/AuthScreenPreview.tsx +228 -0
- package/src/shell/BrandedAuthScreens.test.tsx +181 -0
- package/src/shell/BrandedAuthScreens.tsx +117 -0
- package/src/shell/SplashScreen.test.tsx +37 -0
- package/src/shell/SplashScreen.tsx +45 -40
- package/src/shell/VertesiaShell.tsx +49 -22
- package/src/shell/apps/StandaloneApp.tsx +5 -1
- package/src/shell/index.tsx +6 -0
- package/src/shell/login/SignInFlow.tsx +213 -0
- package/src/shell/login/SignInPageShell.tsx +62 -0
- package/src/shell/login/SignInPrimitives.tsx +14 -11
- package/src/shell/login/SignInRestrictedEnvStep.tsx +19 -7
- package/src/shell/login/SigninScreen.test.tsx +104 -0
- package/src/shell/login/SigninScreen.tsx +115 -145
- package/src/shell/login/SignupForm.tsx +8 -3
- package/src/shell/login/signInUtils.ts +30 -5
- package/src/widgets/json-view/JSONCode.test.tsx +2 -2
- package/src/widgets/json-view/JSONCode.tsx +1 -1
- package/src/widgets/markdown/markdown.css +21 -21
- package/src/widgets/upload/DropZone.tsx +2 -4
- package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DN79y06J.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DN79y06J.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js.map +0 -1
- package/src/session/auth/authState.test.ts +0 -35
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/** Developer-only visual fixtures. Mount outside the authenticated application tree. */
|
|
2
|
+
import { I18nProvider, useUITranslation } from '@vertesia/ui/i18n';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import type { AppBranding } from '../boot/branding.js';
|
|
5
|
+
import {
|
|
6
|
+
AppBrandingProvider,
|
|
7
|
+
BrandedAuthLoadingScreen,
|
|
8
|
+
BrandedPermissionLoadingScreen,
|
|
9
|
+
BrandedSignInScreen,
|
|
10
|
+
} from './BrandedAuthScreens';
|
|
11
|
+
import { type SignInFlowController, SignInFlowSteps } from './login/SignInFlow';
|
|
12
|
+
import SignInRecoveryStep, { type SignInRecoveryKind } from './login/SignInRecoveryStep';
|
|
13
|
+
import { SignInRestrictedEnvView } from './login/SignInRestrictedEnvStep';
|
|
14
|
+
import SignInTenantBlockedStep from './login/SignInTenantBlockedStep';
|
|
15
|
+
import { SignInErrorNotice, type SignInRecoveryMode } from './login/SigninScreen';
|
|
16
|
+
import SignupForm from './login/SignupForm';
|
|
17
|
+
import type { AuthScreens } from './VertesiaShell';
|
|
18
|
+
|
|
19
|
+
export const AUTH_PREVIEW_SCREENS = [
|
|
20
|
+
'email',
|
|
21
|
+
'providers',
|
|
22
|
+
'tenant',
|
|
23
|
+
'returning',
|
|
24
|
+
'pending',
|
|
25
|
+
'blocked',
|
|
26
|
+
'signup',
|
|
27
|
+
'restricted',
|
|
28
|
+
'scope-project',
|
|
29
|
+
'scope-account',
|
|
30
|
+
'no-account',
|
|
31
|
+
'credentials',
|
|
32
|
+
'service',
|
|
33
|
+
'auth-error',
|
|
34
|
+
'loading',
|
|
35
|
+
'permissions',
|
|
36
|
+
'permission-retry',
|
|
37
|
+
'permission-error',
|
|
38
|
+
'permission-denied',
|
|
39
|
+
'permission-session-expired',
|
|
40
|
+
'boot',
|
|
41
|
+
] as const;
|
|
42
|
+
export type AuthPreviewScreen = (typeof AUTH_PREVIEW_SCREENS)[number];
|
|
43
|
+
|
|
44
|
+
const noop = () => {};
|
|
45
|
+
const identity = { email: 'alex@example.com', name: 'Alex Example' };
|
|
46
|
+
const signupIdentity = { email: identity.email, displayName: identity.name };
|
|
47
|
+
const recoveryKinds: Partial<Record<AuthPreviewScreen, SignInRecoveryKind>> = {
|
|
48
|
+
'scope-project': 'scopeProject',
|
|
49
|
+
'scope-account': 'scopeAccount',
|
|
50
|
+
'no-account': 'noAccessibleAccount',
|
|
51
|
+
credentials: 'credential',
|
|
52
|
+
service: 'service',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function AuthScreenPreview({
|
|
56
|
+
screen,
|
|
57
|
+
branding,
|
|
58
|
+
screens = {},
|
|
59
|
+
}: {
|
|
60
|
+
screen: string;
|
|
61
|
+
branding: AppBranding;
|
|
62
|
+
screens?: AuthScreens;
|
|
63
|
+
}) {
|
|
64
|
+
const { t } = useUITranslation();
|
|
65
|
+
const selected = AUTH_PREVIEW_SCREENS.find((value) => value === screen) ?? 'email';
|
|
66
|
+
const SignIn = screens.SignIn ?? BrandedSignInScreen;
|
|
67
|
+
const Loading = screens.Loading ?? BrandedAuthLoadingScreen;
|
|
68
|
+
const Permissions = screens.Permissions ?? BrandedPermissionLoadingScreen;
|
|
69
|
+
const recovery = recoveryKinds[selected];
|
|
70
|
+
const flow: SignInFlowController<SignInRecoveryMode> = {
|
|
71
|
+
mode:
|
|
72
|
+
selected === 'providers' ||
|
|
73
|
+
selected === 'tenant' ||
|
|
74
|
+
selected === 'returning' ||
|
|
75
|
+
selected === 'pending' ||
|
|
76
|
+
selected === 'blocked' ||
|
|
77
|
+
selected === 'signup' ||
|
|
78
|
+
selected === 'restricted'
|
|
79
|
+
? selected
|
|
80
|
+
: selected === 'scope-project' || selected === 'scope-account'
|
|
81
|
+
? 'scopeUnavailable'
|
|
82
|
+
: selected === 'no-account'
|
|
83
|
+
? 'noAccessibleAccount'
|
|
84
|
+
: selected === 'credentials'
|
|
85
|
+
? 'credentialFailure'
|
|
86
|
+
: selected === 'service'
|
|
87
|
+
? 'serviceFailure'
|
|
88
|
+
: 'email',
|
|
89
|
+
email: identity.email,
|
|
90
|
+
tenant: { firebaseTenantId: 'preview', name: branding.name, provider: 'oidc' },
|
|
91
|
+
storedSession: { ...identity, lastProvider: 'oidc', tenantName: branding.name },
|
|
92
|
+
pendingProvider: 'oidc',
|
|
93
|
+
setMode: noop,
|
|
94
|
+
setEmail: noop,
|
|
95
|
+
setTenant: noop,
|
|
96
|
+
setStoredSession: noop,
|
|
97
|
+
onProceedFromEmail: noop,
|
|
98
|
+
onBack: noop,
|
|
99
|
+
onNotYou: noop,
|
|
100
|
+
onProviderClicked: noop,
|
|
101
|
+
startOver: noop,
|
|
102
|
+
};
|
|
103
|
+
let content: ReactNode = <SignInFlowSteps flow={flow} />;
|
|
104
|
+
if (selected === 'blocked')
|
|
105
|
+
content = <SignInTenantBlockedStep email={identity.email} tenantName={branding.name} onBack={noop} />;
|
|
106
|
+
if (selected === 'restricted') content = <SignInRestrictedEnvView onUseDifferentEmail={noop} onRedirect={noop} />;
|
|
107
|
+
if (selected === 'signup') content = <SignupForm identity={signupIdentity} onSignup={noop} goBack={noop} />;
|
|
108
|
+
if (recovery)
|
|
109
|
+
content = (
|
|
110
|
+
<SignInRecoveryStep
|
|
111
|
+
kind={recovery}
|
|
112
|
+
identity={identity}
|
|
113
|
+
details={{ message: 'Preview error details', status: 403 }}
|
|
114
|
+
onContinue={
|
|
115
|
+
recovery === 'scopeProject' || recovery === 'scopeAccount' || recovery === 'service'
|
|
116
|
+
? noop
|
|
117
|
+
: undefined
|
|
118
|
+
}
|
|
119
|
+
onUseDifferentAccount={noop}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
let page: ReactNode = (
|
|
123
|
+
<SignIn
|
|
124
|
+
flow={flow}
|
|
125
|
+
isNested={false}
|
|
126
|
+
notice={selected === 'auth-error' ? <SignInErrorNotice /> : null}
|
|
127
|
+
onUseDifferentAccount={noop}
|
|
128
|
+
onContinueWithSanitizedScope={noop}
|
|
129
|
+
onRetry={noop}
|
|
130
|
+
onSignup={noop}
|
|
131
|
+
>
|
|
132
|
+
{content}
|
|
133
|
+
</SignIn>
|
|
134
|
+
);
|
|
135
|
+
if (selected === 'loading') page = <Loading />;
|
|
136
|
+
if (
|
|
137
|
+
selected === 'permissions' ||
|
|
138
|
+
selected === 'permission-retry' ||
|
|
139
|
+
selected === 'permission-error' ||
|
|
140
|
+
selected === 'permission-denied' ||
|
|
141
|
+
selected === 'permission-session-expired'
|
|
142
|
+
) {
|
|
143
|
+
const denied = selected === 'permission-denied';
|
|
144
|
+
const expired = selected === 'permission-session-expired';
|
|
145
|
+
const failed = selected === 'permission-error' || denied || expired;
|
|
146
|
+
page = (
|
|
147
|
+
<Permissions
|
|
148
|
+
status={failed ? 'error' : selected === 'permission-retry' ? 'retrying' : 'loading'}
|
|
149
|
+
title={
|
|
150
|
+
expired
|
|
151
|
+
? t('auth.recovery.credential.title')
|
|
152
|
+
: denied
|
|
153
|
+
? t('shell.accessDenied')
|
|
154
|
+
: failed
|
|
155
|
+
? t('permissions.connectionFailed')
|
|
156
|
+
: t('permissions.connecting')
|
|
157
|
+
}
|
|
158
|
+
description={
|
|
159
|
+
expired
|
|
160
|
+
? t('auth.recovery.credential.body')
|
|
161
|
+
: denied
|
|
162
|
+
? t('permissions.accessDenied')
|
|
163
|
+
: failed
|
|
164
|
+
? t('permissions.tryAgainLater')
|
|
165
|
+
: selected === 'permission-retry'
|
|
166
|
+
? t('permissions.retrying')
|
|
167
|
+
: t('permissions.loadingPermissions')
|
|
168
|
+
}
|
|
169
|
+
error={failed ? new Error('Preview permission error') : undefined}
|
|
170
|
+
actionLabel={t(denied || expired ? 'auth.recovery.useDifferentAccount' : 'auth.recovery.tryAgain')}
|
|
171
|
+
onAction={noop}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return (
|
|
176
|
+
<I18nProvider>
|
|
177
|
+
<AppBrandingProvider branding={branding}>
|
|
178
|
+
{/* Inert fixtures cannot submit forms, redirect, or change the real session. */}
|
|
179
|
+
<div inert style={{ display: 'contents' }}>
|
|
180
|
+
{page}
|
|
181
|
+
</div>
|
|
182
|
+
<details
|
|
183
|
+
className="fixed top-2 start-2 max-h-80 overflow-auto rounded border bg-background text-foreground p-3 text-sm"
|
|
184
|
+
style={{ zIndex: 2147483647 }}
|
|
185
|
+
>
|
|
186
|
+
<summary>Auth preview: {selected}</summary>
|
|
187
|
+
<nav aria-label="Auth preview screens" className="flex flex-col gap-1 mt-2">
|
|
188
|
+
{AUTH_PREVIEW_SCREENS.map((value) => {
|
|
189
|
+
const params = new URLSearchParams(window.location.search);
|
|
190
|
+
params.delete('__vertesia_boot');
|
|
191
|
+
params.delete('__vertesia_auth');
|
|
192
|
+
params.set(
|
|
193
|
+
value === 'boot' ? '__vertesia_boot' : '__vertesia_auth',
|
|
194
|
+
value === 'boot' ? 'slow' : value,
|
|
195
|
+
);
|
|
196
|
+
return (
|
|
197
|
+
<a
|
|
198
|
+
key={value}
|
|
199
|
+
href={`?${params}`}
|
|
200
|
+
aria-current={value === selected ? 'page' : undefined}
|
|
201
|
+
>
|
|
202
|
+
{value}
|
|
203
|
+
</a>
|
|
204
|
+
);
|
|
205
|
+
})}
|
|
206
|
+
<a href={window.location.pathname}>Exit preview</a>
|
|
207
|
+
</nav>
|
|
208
|
+
</details>
|
|
209
|
+
</AppBrandingProvider>
|
|
210
|
+
</I18nProvider>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Hosts call this only from their import.meta.env.DEV branch, before mounting auth/session providers. */
|
|
215
|
+
export async function mountAuthScreenPreview(
|
|
216
|
+
container: HTMLElement,
|
|
217
|
+
screen: string,
|
|
218
|
+
branding: AppBranding = {
|
|
219
|
+
name: 'Vertesia',
|
|
220
|
+
logo: { light: '/logo-light.png', dark: '/logo-dark.png' },
|
|
221
|
+
loadingIcon: { light: '/icon.svg' },
|
|
222
|
+
},
|
|
223
|
+
screens: AuthScreens = {},
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
if (screen === 'boot') return;
|
|
226
|
+
const { createRoot } = await import('react-dom/client');
|
|
227
|
+
createRoot(container).render(<AuthScreenPreview screen={screen} branding={branding} screens={screens} />);
|
|
228
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { DefaultPermissionLoadingScreen } from '@vertesia/ui/features';
|
|
3
|
+
import { i18nInstance, NAMESPACE, useUITranslation } from '@vertesia/ui/i18n';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
AppBrandingProvider,
|
|
7
|
+
BrandedAuthLoadingScreen,
|
|
8
|
+
BrandedLoadingIndicator,
|
|
9
|
+
BrandedPermissionLoadingScreen,
|
|
10
|
+
BrandedSignInScreen,
|
|
11
|
+
} from './BrandedAuthScreens';
|
|
12
|
+
import { SignInFlowSteps, useSignInFlow } from './login/SignInFlow';
|
|
13
|
+
import { DefaultSignInScreen, type SignInRecoveryMode } from './login/SigninScreen';
|
|
14
|
+
import { DefaultAuthLoadingScreen } from './SplashScreen';
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
cleanup();
|
|
18
|
+
localStorage.clear();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function CopyProbe() {
|
|
22
|
+
const { t } = useUITranslation();
|
|
23
|
+
return <span>{t('auth.email.title')}</span>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function SignInPreview() {
|
|
27
|
+
const flow = useSignInFlow<SignInRecoveryMode>();
|
|
28
|
+
return (
|
|
29
|
+
<BrandedSignInScreen
|
|
30
|
+
flow={flow}
|
|
31
|
+
notice={null}
|
|
32
|
+
isNested={false}
|
|
33
|
+
onRetry={vi.fn()}
|
|
34
|
+
onUseDifferentAccount={vi.fn()}
|
|
35
|
+
onContinueWithSanitizedScope={vi.fn()}
|
|
36
|
+
onSignup={vi.fn()}
|
|
37
|
+
>
|
|
38
|
+
<SignInFlowSteps flow={flow} />
|
|
39
|
+
</BrandedSignInScreen>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('configuration-driven auth screens', () => {
|
|
44
|
+
it('keeps brand copy local to each shell and preserves the shared translations', () => {
|
|
45
|
+
const original = i18nInstance.t('auth.email.title', { ns: NAMESPACE });
|
|
46
|
+
render(
|
|
47
|
+
<>
|
|
48
|
+
<AppBrandingProvider branding={{ name: 'One', copy: { welcome: 'Welcome to One' } }}>
|
|
49
|
+
<CopyProbe />
|
|
50
|
+
</AppBrandingProvider>
|
|
51
|
+
<AppBrandingProvider branding={{ name: 'Two', copy: { welcome: 'Welcome to Two' } }}>
|
|
52
|
+
<CopyProbe />
|
|
53
|
+
</AppBrandingProvider>
|
|
54
|
+
<CopyProbe />
|
|
55
|
+
</>,
|
|
56
|
+
);
|
|
57
|
+
expect(screen.getByText('Welcome to One')).toBeTruthy();
|
|
58
|
+
expect(screen.getByText('Welcome to Two')).toBeTruthy();
|
|
59
|
+
expect(screen.getByText(original)).toBeTruthy();
|
|
60
|
+
expect(i18nInstance.t('auth.email.title', { ns: NAMESPACE })).toBe(original);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('brands the existing email form without replacing its validation', () => {
|
|
64
|
+
render(
|
|
65
|
+
<AppBrandingProvider
|
|
66
|
+
branding={{
|
|
67
|
+
name: 'Workspace',
|
|
68
|
+
copy: { welcome: 'Welcome here', emailPlaceholder: 'you@example.test' },
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<SignInPreview />
|
|
72
|
+
</AppBrandingProvider>,
|
|
73
|
+
);
|
|
74
|
+
expect(screen.getByRole('heading', { name: 'Welcome here' })).toBeTruthy();
|
|
75
|
+
expect(screen.getByPlaceholderText('you@example.test')).toBeTruthy();
|
|
76
|
+
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
|
77
|
+
expect(screen.getByRole('textbox').getAttribute('aria-invalid')).toBe('true');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('uses shared branding for loading and preserves permission recovery actions', () => {
|
|
81
|
+
const retry = vi.fn();
|
|
82
|
+
const brand = { name: 'Workspace', copy: { loading: 'Preparing Workspace' } };
|
|
83
|
+
const view = render(
|
|
84
|
+
<AppBrandingProvider branding={brand}>
|
|
85
|
+
<BrandedAuthLoadingScreen />
|
|
86
|
+
</AppBrandingProvider>,
|
|
87
|
+
);
|
|
88
|
+
expect(screen.getByRole('status').getAttribute('aria-label')).toBe('Preparing Workspace');
|
|
89
|
+
view.rerender(
|
|
90
|
+
<AppBrandingProvider branding={brand}>
|
|
91
|
+
<BrandedPermissionLoadingScreen
|
|
92
|
+
status="error"
|
|
93
|
+
title="Access failed"
|
|
94
|
+
description="Try again"
|
|
95
|
+
actionLabel="Retry"
|
|
96
|
+
onAction={retry}
|
|
97
|
+
/>
|
|
98
|
+
</AppBrandingProvider>,
|
|
99
|
+
);
|
|
100
|
+
expect(screen.getByRole('alert').textContent).toContain('Access failed');
|
|
101
|
+
fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
|
|
102
|
+
expect(retry).toHaveBeenCalledOnce();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('default layout parity', () => {
|
|
107
|
+
it('retains the default sign-in chrome and legal footer with empty branding', () => {
|
|
108
|
+
const view = render(
|
|
109
|
+
<DefaultSignInScreen>
|
|
110
|
+
<p>Form</p>
|
|
111
|
+
</DefaultSignInScreen>,
|
|
112
|
+
);
|
|
113
|
+
const expected = view.container.innerHTML;
|
|
114
|
+
view.rerender(
|
|
115
|
+
<AppBrandingProvider branding={{ name: 'Test' }}>
|
|
116
|
+
<BrandedSignInScreen
|
|
117
|
+
{...({ isNested: false, children: <p>Form</p> } as Parameters<typeof BrandedSignInScreen>[0])}
|
|
118
|
+
/>
|
|
119
|
+
</AppBrandingProvider>,
|
|
120
|
+
);
|
|
121
|
+
expect(view.container.querySelector('[data-vbrand] > div')?.outerHTML).toBe(expected);
|
|
122
|
+
});
|
|
123
|
+
it('uses the same loading markup with no branding assets or copy', () => {
|
|
124
|
+
const view = render(<DefaultAuthLoadingScreen />);
|
|
125
|
+
const expected = view.container.innerHTML;
|
|
126
|
+
view.rerender(
|
|
127
|
+
<AppBrandingProvider branding={{ name: 'Test' }}>
|
|
128
|
+
<BrandedAuthLoadingScreen />
|
|
129
|
+
</AppBrandingProvider>,
|
|
130
|
+
);
|
|
131
|
+
expect(view.container.querySelector('[role="status"]')?.outerHTML).toBe(expected);
|
|
132
|
+
});
|
|
133
|
+
it('preserves permission error details and the outline recovery button', () => {
|
|
134
|
+
const props = {
|
|
135
|
+
status: 'error' as const,
|
|
136
|
+
title: 'Failed',
|
|
137
|
+
description: 'Retry',
|
|
138
|
+
actionLabel: 'Retry',
|
|
139
|
+
onAction: vi.fn(),
|
|
140
|
+
error: new Error('Details'),
|
|
141
|
+
};
|
|
142
|
+
const view = render(<DefaultPermissionLoadingScreen {...props} />);
|
|
143
|
+
const expected = view.container.innerHTML;
|
|
144
|
+
view.rerender(
|
|
145
|
+
<AppBrandingProvider branding={{ name: 'Test' }}>
|
|
146
|
+
<BrandedPermissionLoadingScreen {...props} />
|
|
147
|
+
</AppBrandingProvider>,
|
|
148
|
+
);
|
|
149
|
+
expect(view.container.querySelector('[data-vbrand] > div')?.outerHTML).toBe(expected);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('delayed resource loading indicator', () => {
|
|
154
|
+
it('avoids flashing on fast loads and cancels its timer on handoff', () => {
|
|
155
|
+
vi.useFakeTimers();
|
|
156
|
+
try {
|
|
157
|
+
const view = render(<BrandedLoadingIndicator delayMs={200} />);
|
|
158
|
+
expect(screen.queryByRole('status')).toBeNull();
|
|
159
|
+
act(() => vi.advanceTimersByTime(100));
|
|
160
|
+
view.unmount();
|
|
161
|
+
act(() => vi.advanceTimersByTime(200));
|
|
162
|
+
expect(screen.queryByRole('status')).toBeNull();
|
|
163
|
+
} finally {
|
|
164
|
+
vi.useRealTimers();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('shows a slow load and disappears immediately when replaced by the view', () => {
|
|
169
|
+
vi.useFakeTimers();
|
|
170
|
+
try {
|
|
171
|
+
const view = render(<BrandedLoadingIndicator delayMs={200} />);
|
|
172
|
+
act(() => vi.advanceTimersByTime(200));
|
|
173
|
+
expect(screen.getByRole('status')).toBeTruthy();
|
|
174
|
+
view.rerender(<main>Resource view</main>);
|
|
175
|
+
expect(screen.queryByRole('status')).toBeNull();
|
|
176
|
+
expect(screen.getByText('Resource view')).toBeTruthy();
|
|
177
|
+
} finally {
|
|
178
|
+
vi.useRealTimers();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { DefaultPermissionLoadingScreen, type PermissionLoadingScreenProps } from '@vertesia/ui/features';
|
|
2
|
+
import { UITranslationOverrides } from '@vertesia/ui/i18n';
|
|
3
|
+
import { createContext, type ReactNode, useContext, useEffect, useId, useMemo, useState } from 'react';
|
|
4
|
+
import { type AppBranding, renderBrandStyles } from '../boot/branding.js';
|
|
5
|
+
import { DefaultSignInScreen, type SignInScreenViewProps } from './login/SigninScreen';
|
|
6
|
+
import { type AuthLoadingScreenProps, DefaultAuthLoadingScreen, LoadingAnimation } from './SplashScreen';
|
|
7
|
+
import type { AuthScreens } from './VertesiaShell';
|
|
8
|
+
|
|
9
|
+
const BrandingContext = createContext<AppBranding>({ name: '' });
|
|
10
|
+
|
|
11
|
+
export function AppBrandingProvider({ branding, children }: { branding: AppBranding; children: ReactNode }) {
|
|
12
|
+
const id = useId().replace(/[^a-zA-Z0-9_-]/g, '');
|
|
13
|
+
const overrides = useMemo(() => {
|
|
14
|
+
const result: Record<string, string> = {};
|
|
15
|
+
for (const [key, value] of Object.entries(branding.copy?.translations ?? {})) {
|
|
16
|
+
if (key.startsWith('auth.') || key.startsWith('signup.')) result[key] = value;
|
|
17
|
+
}
|
|
18
|
+
if (branding.copy?.welcome !== undefined) result['auth.email.title'] = branding.copy.welcome;
|
|
19
|
+
if (branding.copy?.emailPlaceholder !== undefined)
|
|
20
|
+
result['auth.email.placeholder'] = branding.copy.emailPlaceholder;
|
|
21
|
+
return result;
|
|
22
|
+
}, [branding]);
|
|
23
|
+
return (
|
|
24
|
+
<BrandingContext.Provider value={branding}>
|
|
25
|
+
<div data-vbrand={id} style={{ display: 'contents' }}>
|
|
26
|
+
<style>{renderBrandStyles(branding, `[data-vbrand="${id}"]`)}</style>
|
|
27
|
+
<UITranslationOverrides overrides={overrides}>{children}</UITranslationOverrides>
|
|
28
|
+
</div>
|
|
29
|
+
</BrandingContext.Provider>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function BrandLoadingIcon({ brand }: { brand: AppBranding }) {
|
|
34
|
+
if (!brand.loadingIcon) return null;
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<img src={brand.loadingIcon.light} alt="" className="w-10 h-auto rounded-full block dark:hidden" />
|
|
38
|
+
<img
|
|
39
|
+
src={brand.loadingIcon.dark ?? brand.loadingIcon.light}
|
|
40
|
+
alt=""
|
|
41
|
+
className="w-10 h-auto rounded-full hidden dark:block"
|
|
42
|
+
/>
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function BrandedAuthLoadingScreen({ loadingIcon }: AuthLoadingScreenProps) {
|
|
48
|
+
const brand = useContext(BrandingContext);
|
|
49
|
+
return (
|
|
50
|
+
<DefaultAuthLoadingScreen
|
|
51
|
+
loadingIcon={loadingIcon ?? (brand.loadingIcon ? <BrandLoadingIcon brand={brand} /> : undefined)}
|
|
52
|
+
loadingLabel={brand.copy?.loading}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function BrandedSignInScreen({ children, notice, isNested, lightLogo, darkLogo }: SignInScreenViewProps) {
|
|
58
|
+
const brand = useContext(BrandingContext);
|
|
59
|
+
return (
|
|
60
|
+
<DefaultSignInScreen
|
|
61
|
+
isNested={isNested}
|
|
62
|
+
lightLogo={brand.logo?.light ?? lightLogo}
|
|
63
|
+
darkLogo={brand.logo?.dark ?? brand.logo?.light ?? darkLogo}
|
|
64
|
+
logoAlt={brand.logo?.alt ?? brand.name}
|
|
65
|
+
footer={brand.copy?.footer}
|
|
66
|
+
notice={notice}
|
|
67
|
+
>
|
|
68
|
+
{children}
|
|
69
|
+
</DefaultSignInScreen>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function BrandedPermissionLoadingScreen(props: PermissionLoadingScreenProps) {
|
|
74
|
+
const brand = useContext(BrandingContext);
|
|
75
|
+
return (
|
|
76
|
+
<DefaultPermissionLoadingScreen
|
|
77
|
+
{...props}
|
|
78
|
+
description={props.status === 'error' ? props.description : (brand.copy?.loading ?? props.description)}
|
|
79
|
+
loadingIcon={
|
|
80
|
+
props.status === 'error' ? (
|
|
81
|
+
(props.loadingIcon ?? (brand.loadingIcon ? <BrandLoadingIcon brand={brand} /> : undefined))
|
|
82
|
+
) : (
|
|
83
|
+
<BrandedLoadingIndicator loadingIcon={props.loadingIcon} />
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const brandedAuthScreens: AuthScreens = {
|
|
91
|
+
SignIn: BrandedSignInScreen,
|
|
92
|
+
Loading: BrandedAuthLoadingScreen,
|
|
93
|
+
Permissions: BrandedPermissionLoadingScreen,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Brand-aware loading indicator that stays inside its container instead of covering the app shell. */
|
|
97
|
+
export function BrandedLoadingIndicator({ loadingIcon, delayMs = 0 }: AuthLoadingScreenProps & { delayMs?: number }) {
|
|
98
|
+
const brand = useContext(BrandingContext);
|
|
99
|
+
const [visible, setVisible] = useState(delayMs <= 0);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (delayMs <= 0) {
|
|
102
|
+
setVisible(true);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
setVisible(false);
|
|
106
|
+
const timer = setTimeout(() => setVisible(true), delayMs);
|
|
107
|
+
return () => clearTimeout(timer);
|
|
108
|
+
}, [delayMs]);
|
|
109
|
+
if (!visible) return null;
|
|
110
|
+
return (
|
|
111
|
+
<div role="status" aria-label={brand.copy?.loading ?? 'Loading'}>
|
|
112
|
+
<LoadingAnimation
|
|
113
|
+
loadingIcon={loadingIcon ?? (brand.loadingIcon ? <BrandLoadingIcon brand={brand} /> : undefined)}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -7,6 +7,22 @@ const session = { isLoading: true, authToken: undefined as AuthTokenPayload | un
|
|
|
7
7
|
vi.mock('@vertesia/ui/session', () => ({ useUserSession: () => session }));
|
|
8
8
|
|
|
9
9
|
describe('SplashScreen handoff', () => {
|
|
10
|
+
it('never mounts a splash over an already ready sign-in view', () => {
|
|
11
|
+
session.isLoading = false;
|
|
12
|
+
session.authToken = undefined;
|
|
13
|
+
render(<SplashScreen icon={<span>Startup indicator</span>} />);
|
|
14
|
+
expect(screen.queryByText('Startup indicator')).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('removes the default splash immediately when loading completes without a token', () => {
|
|
18
|
+
session.isLoading = true;
|
|
19
|
+
session.authToken = undefined;
|
|
20
|
+
const view = render(<SplashScreen icon={<span>Startup indicator</span>} />);
|
|
21
|
+
session.isLoading = false;
|
|
22
|
+
view.rerender(<SplashScreen icon={<span>Startup indicator</span>} />);
|
|
23
|
+
expect(screen.queryByText('Startup indicator')).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
10
26
|
it.each([true, false])('removes the splash immediately when a token arrives with isLoading=%s', (isLoading) => {
|
|
11
27
|
session.isLoading = true;
|
|
12
28
|
session.authToken = undefined;
|
|
@@ -19,4 +35,25 @@ describe('SplashScreen handoff', () => {
|
|
|
19
35
|
// No fading splash remains over the permission gate's connection screen.
|
|
20
36
|
expect(screen.queryByText('Startup indicator')).toBeNull();
|
|
21
37
|
});
|
|
38
|
+
it('gives a custom screen the whole page without the default animation and unmounts it on completion', () => {
|
|
39
|
+
session.isLoading = true;
|
|
40
|
+
session.authToken = undefined;
|
|
41
|
+
const CustomScreen = () => <main>Branded authentication</main>;
|
|
42
|
+
const view = render(<SplashScreen Screen={CustomScreen} icon={<span>Old spinner</span>} />);
|
|
43
|
+
expect(view.container.firstElementChild?.tagName).toBe('MAIN');
|
|
44
|
+
expect(screen.queryByText('Old spinner')).toBeNull();
|
|
45
|
+
session.isLoading = false;
|
|
46
|
+
view.rerender(<SplashScreen Screen={CustomScreen} />);
|
|
47
|
+
expect(screen.queryByText('Branded authentication')).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('hands a custom screen off to permissions as soon as the token arrives', () => {
|
|
51
|
+
session.isLoading = true;
|
|
52
|
+
session.authToken = undefined;
|
|
53
|
+
const CustomScreen = () => <main>Branded authentication</main>;
|
|
54
|
+
const view = render(<SplashScreen Screen={CustomScreen} />);
|
|
55
|
+
session.authToken = {} as AuthTokenPayload;
|
|
56
|
+
view.rerender(<SplashScreen Screen={CustomScreen} />);
|
|
57
|
+
expect(screen.queryByText('Branded authentication')).toBeNull();
|
|
58
|
+
});
|
|
22
59
|
});
|