@vertesia/ui 1.5.0 → 1.6.0-dev.20260918.061604Z

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.
Files changed (336) hide show
  1. package/README.md +125 -0
  2. package/lib/boot/branding.d.ts +58 -0
  3. package/lib/boot/branding.d.ts.map +1 -0
  4. package/lib/boot/branding.js +62 -0
  5. package/lib/boot/branding.js.map +1 -0
  6. package/lib/boot/index.d.ts +27 -0
  7. package/lib/boot/index.d.ts.map +1 -0
  8. package/lib/boot/index.js +325 -0
  9. package/lib/boot/index.js.map +1 -0
  10. package/lib/boot/vite.d.ts +63 -0
  11. package/lib/boot/vite.d.ts.map +1 -0
  12. package/lib/boot/vite.js +108 -0
  13. package/lib/boot/vite.js.map +1 -0
  14. package/lib/core/components/shadcn/button.d.ts.map +1 -1
  15. package/lib/core/components/shadcn/button.js +1 -2
  16. package/lib/core/components/shadcn/button.js.map +1 -1
  17. package/lib/core/components/shadcn/filters/filter-styles.d.ts +1 -1
  18. package/lib/core/components/shadcn/filters/filter-styles.d.ts.map +1 -1
  19. package/lib/core/components/shadcn/filters/filter-styles.js +4 -4
  20. package/lib/core/components/shadcn/label.js +1 -1
  21. package/lib/core/components/shadcn/label.js.map +1 -1
  22. package/lib/env/index.d.ts +32 -1
  23. package/lib/env/index.d.ts.map +1 -1
  24. package/lib/env/index.js +55 -3
  25. package/lib/env/index.js.map +1 -1
  26. package/lib/features/agent/PayloadBuilder.d.ts +4 -0
  27. package/lib/features/agent/PayloadBuilder.d.ts.map +1 -1
  28. package/lib/features/agent/PayloadBuilder.js +6 -1
  29. package/lib/features/agent/PayloadBuilder.js.map +1 -1
  30. package/lib/features/agent/chat/AgentRunFeedback.d.ts +66 -0
  31. package/lib/features/agent/chat/AgentRunFeedback.d.ts.map +1 -0
  32. package/lib/features/agent/chat/AgentRunFeedback.js +298 -0
  33. package/lib/features/agent/chat/AgentRunFeedback.js.map +1 -0
  34. package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  35. package/lib/features/agent/chat/ModernAgentConversation.js +7 -5
  36. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  37. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts +3 -1
  38. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
  39. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +11 -10
  40. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  41. package/lib/features/agent/chat/ModernAgentOutput/Header.d.ts.map +1 -1
  42. package/lib/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
  43. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts +3 -1
  44. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
  45. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +28 -7
  46. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
  47. package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.d.ts.map +1 -1
  48. package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js +2 -0
  49. package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js.map +1 -1
  50. package/lib/features/agent/chat/hooks/useAgentPlans.d.ts +1 -1
  51. package/lib/features/agent/chat/hooks/useAgentPlans.d.ts.map +1 -1
  52. package/lib/features/agent/chat/hooks/useAgentPlans.js +5 -5
  53. package/lib/features/agent/chat/hooks/useAgentPlans.js.map +1 -1
  54. package/lib/features/agent/chat/index.d.ts +1 -0
  55. package/lib/features/agent/chat/index.d.ts.map +1 -1
  56. package/lib/features/agent/chat/index.js +1 -0
  57. package/lib/features/agent/chat/index.js.map +1 -1
  58. package/lib/features/facets/AgentRunnerFacetsNav.d.ts +8 -0
  59. package/lib/features/facets/AgentRunnerFacetsNav.d.ts.map +1 -1
  60. package/lib/features/facets/AgentRunnerFacetsNav.js +12 -0
  61. package/lib/features/facets/AgentRunnerFacetsNav.js.map +1 -1
  62. package/lib/features/permissions/UserPermissionsProvider.d.ts +15 -1
  63. package/lib/features/permissions/UserPermissionsProvider.d.ts.map +1 -1
  64. package/lib/features/permissions/UserPermissionsProvider.js +20 -12
  65. package/lib/features/permissions/UserPermissionsProvider.js.map +1 -1
  66. package/lib/features/store/objects/search/DocumentSearchContext.d.ts.map +1 -1
  67. package/lib/features/store/objects/search/DocumentSearchContext.js +6 -1
  68. package/lib/features/store/objects/search/DocumentSearchContext.js.map +1 -1
  69. package/lib/features/store/objects/selection/SelectionActions.js +1 -1
  70. package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
  71. package/lib/features/store/types/TypeRegistry.d.ts +130 -0
  72. package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
  73. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +130 -0
  74. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
  75. package/lib/i18n/index.d.ts +5 -4
  76. package/lib/i18n/index.d.ts.map +1 -1
  77. package/lib/i18n/index.js +16 -2
  78. package/lib/i18n/index.js.map +1 -1
  79. package/lib/i18n/locales/ar.json +30 -0
  80. package/lib/i18n/locales/de.json +30 -0
  81. package/lib/i18n/locales/en.json +30 -0
  82. package/lib/i18n/locales/es.json +30 -0
  83. package/lib/i18n/locales/fr.json +30 -0
  84. package/lib/i18n/locales/it.json +30 -0
  85. package/lib/i18n/locales/ja.json +30 -0
  86. package/lib/i18n/locales/ko.json +30 -0
  87. package/lib/i18n/locales/pt.json +30 -0
  88. package/lib/i18n/locales/ru.json +30 -0
  89. package/lib/i18n/locales/tr.json +30 -0
  90. package/lib/i18n/locales/zh-TW.json +30 -0
  91. package/lib/i18n/locales/zh.json +30 -0
  92. package/lib/layout/ErrorPage.js +1 -1
  93. package/lib/layout/ErrorPage.js.map +1 -1
  94. package/lib/layout/Navbar.d.ts.map +1 -1
  95. package/lib/layout/Navbar.js +3 -2
  96. package/lib/layout/Navbar.js.map +1 -1
  97. package/lib/session/UserSession.d.ts.map +1 -1
  98. package/lib/session/UserSession.js +2 -3
  99. package/lib/session/UserSession.js.map +1 -1
  100. package/lib/session/UserSessionProvider.d.ts +2 -1
  101. package/lib/session/UserSessionProvider.d.ts.map +1 -1
  102. package/lib/session/UserSessionProvider.js +13 -4
  103. package/lib/session/UserSessionProvider.js.map +1 -1
  104. package/lib/session/auth/composable.d.ts.map +1 -1
  105. package/lib/session/auth/composable.js +88 -3
  106. package/lib/session/auth/composable.js.map +1 -1
  107. package/lib/session/auth/domainRouting.d.ts +22 -12
  108. package/lib/session/auth/domainRouting.d.ts.map +1 -1
  109. package/lib/session/auth/domainRouting.js +35 -21
  110. package/lib/session/auth/domainRouting.js.map +1 -1
  111. package/lib/session/auth/firebase.d.ts.map +1 -1
  112. package/lib/session/auth/firebase.js +10 -0
  113. package/lib/session/auth/firebase.js.map +1 -1
  114. package/lib/session/auth/useAuthState.d.ts +9 -4
  115. package/lib/session/auth/useAuthState.d.ts.map +1 -1
  116. package/lib/session/auth/useAuthState.js +9 -4
  117. package/lib/session/auth/useAuthState.js.map +1 -1
  118. package/lib/session/index.d.ts.map +1 -1
  119. package/lib/session/index.js +3 -1
  120. package/lib/session/index.js.map +1 -1
  121. package/lib/shell/AuthScreenPreview.d.ts +12 -0
  122. package/lib/shell/AuthScreenPreview.d.ts.map +1 -0
  123. package/lib/shell/AuthScreenPreview.js +140 -0
  124. package/lib/shell/AuthScreenPreview.js.map +1 -0
  125. package/lib/shell/BrandedAuthScreens.d.ts +19 -0
  126. package/lib/shell/BrandedAuthScreens.d.ts.map +1 -0
  127. package/lib/shell/BrandedAuthScreens.js +64 -0
  128. package/lib/shell/BrandedAuthScreens.js.map +1 -0
  129. package/lib/shell/SplashScreen.d.ts +13 -2
  130. package/lib/shell/SplashScreen.d.ts.map +1 -1
  131. package/lib/shell/SplashScreen.js +20 -19
  132. package/lib/shell/SplashScreen.js.map +1 -1
  133. package/lib/shell/VertesiaShell.d.ts +15 -4
  134. package/lib/shell/VertesiaShell.d.ts.map +1 -1
  135. package/lib/shell/VertesiaShell.js +8 -3
  136. package/lib/shell/VertesiaShell.js.map +1 -1
  137. package/lib/shell/apps/StandaloneApp.js +5 -1
  138. package/lib/shell/apps/StandaloneApp.js.map +1 -1
  139. package/lib/shell/index.d.ts +6 -0
  140. package/lib/shell/index.d.ts.map +1 -1
  141. package/lib/shell/index.js +5 -0
  142. package/lib/shell/index.js.map +1 -1
  143. package/lib/shell/login/SignInAuthPending.js +1 -1
  144. package/lib/shell/login/SignInAuthPending.js.map +1 -1
  145. package/lib/shell/login/SignInEmailStep.d.ts.map +1 -1
  146. package/lib/shell/login/SignInEmailStep.js +2 -2
  147. package/lib/shell/login/SignInEmailStep.js.map +1 -1
  148. package/lib/shell/login/SignInFlow.d.ts +68 -0
  149. package/lib/shell/login/SignInFlow.d.ts.map +1 -0
  150. package/lib/shell/login/SignInFlow.js +108 -0
  151. package/lib/shell/login/SignInFlow.js.map +1 -0
  152. package/lib/shell/login/SignInIcons.d.ts +1 -1
  153. package/lib/shell/login/SignInIcons.d.ts.map +1 -1
  154. package/lib/shell/login/SignInPageShell.d.ts +13 -0
  155. package/lib/shell/login/SignInPageShell.d.ts.map +1 -0
  156. package/lib/shell/login/SignInPageShell.js +14 -0
  157. package/lib/shell/login/SignInPageShell.js.map +1 -0
  158. package/lib/shell/login/SignInPrimitives.d.ts +3 -3
  159. package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
  160. package/lib/shell/login/SignInPrimitives.js +13 -11
  161. package/lib/shell/login/SignInPrimitives.js.map +1 -1
  162. package/lib/shell/login/SignInRestrictedEnvStep.d.ts +5 -0
  163. package/lib/shell/login/SignInRestrictedEnvStep.d.ts.map +1 -1
  164. package/lib/shell/login/SignInRestrictedEnvStep.js +8 -4
  165. package/lib/shell/login/SignInRestrictedEnvStep.js.map +1 -1
  166. package/lib/shell/login/SigninScreen.d.ts +38 -3
  167. package/lib/shell/login/SigninScreen.d.ts.map +1 -1
  168. package/lib/shell/login/SigninScreen.js +33 -86
  169. package/lib/shell/login/SigninScreen.js.map +1 -1
  170. package/lib/shell/login/SignupForm.d.ts +3 -1
  171. package/lib/shell/login/SignupForm.d.ts.map +1 -1
  172. package/lib/shell/login/SignupForm.js +7 -3
  173. package/lib/shell/login/SignupForm.js.map +1 -1
  174. package/lib/shell/login/signInUtils.d.ts +12 -1
  175. package/lib/shell/login/signInUtils.d.ts.map +1 -1
  176. package/lib/shell/login/signInUtils.js +34 -11
  177. package/lib/shell/login/signInUtils.js.map +1 -1
  178. package/lib/vertesia-ui-boot.js +237 -0
  179. package/lib/vertesia-ui-boot.js.map +1 -0
  180. package/lib/vertesia-ui-core.js +88 -46
  181. package/lib/vertesia-ui-core.js.map +1 -1
  182. package/lib/vertesia-ui-env.js +1 -1
  183. package/lib/vertesia-ui-env.js.map +1 -1
  184. package/lib/vertesia-ui-features.js +350 -317
  185. package/lib/vertesia-ui-features.js.map +1 -1
  186. package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js +5 -0
  187. package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js.map +1 -0
  188. package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js +5 -0
  189. package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js.map +1 -0
  190. package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js +5 -0
  191. package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js.map +1 -0
  192. package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js +5 -0
  193. package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js.map +1 -0
  194. package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js +5 -0
  195. package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js.map +1 -0
  196. package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js +5 -0
  197. package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js.map +1 -0
  198. package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js +5 -0
  199. package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js.map +1 -0
  200. package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js +5 -0
  201. package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js.map +1 -0
  202. package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js +5 -0
  203. package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js.map +1 -0
  204. package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js +5 -0
  205. package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js.map +1 -0
  206. package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js +5 -0
  207. package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js.map +1 -0
  208. package/lib/vertesia-ui-i18n.js +2 -2
  209. package/lib/vertesia-ui-i18n.js.map +1 -1
  210. package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js +5 -0
  211. package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js.map +1 -0
  212. package/lib/vertesia-ui-layout.js +43 -19
  213. package/lib/vertesia-ui-layout.js.map +1 -1
  214. package/lib/vertesia-ui-rich-text.js +123 -74
  215. package/lib/vertesia-ui-rich-text.js.map +1 -1
  216. package/lib/vertesia-ui-router.js.map +1 -1
  217. package/lib/vertesia-ui-session.js +1 -1
  218. package/lib/vertesia-ui-session.js.map +1 -1
  219. package/lib/vertesia-ui-shell.js +87 -33
  220. package/lib/vertesia-ui-shell.js.map +1 -1
  221. package/lib/vertesia-ui-widgets.js +154 -98
  222. package/lib/vertesia-ui-widgets.js.map +1 -1
  223. package/lib/widgets/xml-viewer/hooks/useCollapsible.d.ts +1 -1
  224. package/llms.txt +6 -2
  225. package/package.json +45 -36
  226. package/src/__tests__/a11y.test.tsx +25 -0
  227. package/src/__tests__/theme.test.ts +91 -0
  228. package/src/boot/branding.test.ts +72 -0
  229. package/src/boot/branding.ts +108 -0
  230. package/src/boot/index.test.ts +216 -0
  231. package/src/boot/index.ts +356 -0
  232. package/src/boot/vite.test.ts +93 -0
  233. package/src/boot/vite.ts +118 -0
  234. package/src/core/components/shadcn/button.tsx +1 -2
  235. package/src/core/components/shadcn/filters/filter-styles.ts +4 -4
  236. package/src/core/components/shadcn/label.tsx +1 -1
  237. package/src/env/index.test.ts +127 -0
  238. package/src/env/index.ts +73 -3
  239. package/src/features/agent/PayloadBuilder.tsx +10 -1
  240. package/src/features/agent/chat/AgentRunFeedback.test.tsx +347 -0
  241. package/src/features/agent/chat/AgentRunFeedback.tsx +465 -0
  242. package/src/features/agent/chat/ModernAgentConversation.test.tsx +1 -1
  243. package/src/features/agent/chat/ModernAgentConversation.tsx +52 -47
  244. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +145 -0
  245. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +42 -24
  246. package/src/features/agent/chat/ModernAgentOutput/Header.tsx +4 -0
  247. package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +38 -5
  248. package/src/features/agent/chat/ModernAgentOutput/SummaryConversation.ts +2 -0
  249. package/src/features/agent/chat/ModernAgentOutput/utils.test.ts +14 -0
  250. package/src/features/agent/chat/hooks/useAgentPlans.test.ts +52 -0
  251. package/src/features/agent/chat/hooks/useAgentPlans.ts +5 -5
  252. package/src/features/agent/chat/hooks/useFileProcessing.test.tsx +0 -5
  253. package/src/features/agent/chat/index.ts +6 -0
  254. package/src/features/facets/AgentRunnerFacetsNav.tsx +26 -0
  255. package/src/features/permissions/UserPermissionsProvider.test.tsx +50 -1
  256. package/src/features/permissions/UserPermissionsProvider.tsx +82 -44
  257. package/src/features/store/objects/search/DocumentSearchContext.ts +8 -3
  258. package/src/features/store/objects/selection/SelectionActions.tsx +4 -1
  259. package/src/i18n/index.tsx +25 -2
  260. package/src/i18n/locales/ar.json +30 -0
  261. package/src/i18n/locales/de.json +30 -0
  262. package/src/i18n/locales/en.json +30 -0
  263. package/src/i18n/locales/es.json +30 -0
  264. package/src/i18n/locales/fr.json +30 -0
  265. package/src/i18n/locales/it.json +30 -0
  266. package/src/i18n/locales/ja.json +30 -0
  267. package/src/i18n/locales/ko.json +30 -0
  268. package/src/i18n/locales/pt.json +30 -0
  269. package/src/i18n/locales/ru.json +30 -0
  270. package/src/i18n/locales/tr.json +30 -0
  271. package/src/i18n/locales/zh-TW.json +30 -0
  272. package/src/i18n/locales/zh.json +30 -0
  273. package/src/layout/ErrorPage.tsx +1 -1
  274. package/src/layout/Navbar.test.tsx +58 -0
  275. package/src/layout/Navbar.tsx +5 -2
  276. package/src/router/path.test.ts +5 -5
  277. package/src/session/UserSession.ts +2 -4
  278. package/src/session/UserSessionProvider.strict-mode.test.tsx +44 -0
  279. package/src/session/UserSessionProvider.test.ts +62 -1
  280. package/src/session/UserSessionProvider.tsx +12 -4
  281. package/src/session/auth/composable.renewal.test.ts +170 -0
  282. package/src/session/auth/composable.test.ts +23 -1
  283. package/src/session/auth/composable.ts +101 -2
  284. package/src/session/auth/domainRouting.test.ts +35 -65
  285. package/src/session/auth/domainRouting.ts +51 -34
  286. package/src/session/auth/firebase.test.ts +25 -0
  287. package/src/session/auth/firebase.ts +10 -0
  288. package/src/session/auth/useAuthState.ts +11 -4
  289. package/src/session/index.ts +3 -1
  290. package/src/shell/AuthScreenPreview.test.tsx +44 -0
  291. package/src/shell/AuthScreenPreview.tsx +228 -0
  292. package/src/shell/BrandedAuthScreens.test.tsx +181 -0
  293. package/src/shell/BrandedAuthScreens.tsx +117 -0
  294. package/src/shell/SplashScreen.test.tsx +37 -0
  295. package/src/shell/SplashScreen.tsx +44 -39
  296. package/src/shell/VertesiaShell.tsx +49 -22
  297. package/src/shell/apps/StandaloneApp.tsx +5 -1
  298. package/src/shell/index.tsx +6 -0
  299. package/src/shell/login/SignInAuthPending.tsx +3 -3
  300. package/src/shell/login/SignInEmailStep.test.tsx +32 -0
  301. package/src/shell/login/SignInEmailStep.tsx +13 -9
  302. package/src/shell/login/SignInFlow.tsx +213 -0
  303. package/src/shell/login/SignInPageShell.tsx +62 -0
  304. package/src/shell/login/SignInPrimitives.tsx +14 -11
  305. package/src/shell/login/SignInRestrictedEnvStep.tsx +19 -7
  306. package/src/shell/login/SigninScreen.test.tsx +104 -0
  307. package/src/shell/login/SigninScreen.tsx +115 -145
  308. package/src/shell/login/SignupForm.tsx +8 -3
  309. package/src/shell/login/signInUtils.ts +42 -9
  310. package/src/widgets/markdown/CollaborativeMarkdownRenderer.test.tsx +11 -3
  311. package/src/widgets/markdown/markdown.css +21 -21
  312. package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js +0 -5
  313. package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js.map +0 -1
  314. package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js +0 -5
  315. package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js.map +0 -1
  316. package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js +0 -5
  317. package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js.map +0 -1
  318. package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js +0 -5
  319. package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js.map +0 -1
  320. package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js +0 -5
  321. package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js.map +0 -1
  322. package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js +0 -5
  323. package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js.map +0 -1
  324. package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js +0 -5
  325. package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js.map +0 -1
  326. package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js +0 -5
  327. package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js.map +0 -1
  328. package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js +0 -5
  329. package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js.map +0 -1
  330. package/lib/vertesia-ui-i18n.DN79y06J.chunk.js +0 -5
  331. package/lib/vertesia-ui-i18n.DN79y06J.chunk.js.map +0 -1
  332. package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js +0 -5
  333. package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js.map +0 -1
  334. package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js +0 -5
  335. package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js.map +0 -1
  336. package/src/session/auth/authState.test.ts +0 -35
@@ -37,11 +37,21 @@ export function getFirebaseAnalytics(): Analytics {
37
37
  export function getFirebaseAuth(): Auth {
38
38
  if (!_firebaseAuth) {
39
39
  _firebaseAuth = getAuth(getFirebaseApp());
40
+ _firebaseAuth.tenantId = Env.firebase?.tenantId ?? null;
40
41
  }
41
42
  return _firebaseAuth;
42
43
  }
43
44
 
44
45
  export async function setFirebaseTenant(tenantEmail?: string) {
46
+ if (Env.firebase?.tenantId) {
47
+ getFirebaseAuth().tenantId = Env.firebase.tenantId;
48
+ return {
49
+ firebaseTenantId: Env.firebase.tenantId,
50
+ provider: Env.firebase.providerType ?? 'oidc',
51
+ name: '',
52
+ label: '',
53
+ };
54
+ }
45
55
  if (!tenantEmail) {
46
56
  console.log('No tenant name or email specified, skipping tenant setup');
47
57
  return;
@@ -1,10 +1,17 @@
1
- /** React wrappers for the state record shared with boot-time Central Auth redirects. */
1
+ /**
2
+ * This hook is used to generate and verify state for OAuth2 authorization requests.
3
+ *
4
+ * The implementation lives in ./authState so the token layer can start the same round-trip when it
5
+ * renews an expired Central Auth session, where no component is available to call a hook.
6
+ */
7
+
2
8
  import { useCallback } from 'react';
3
9
  import { clearAuthState, generateAuthState, verifyAuthState } from './authState';
4
10
 
5
11
  export function useAuthState() {
6
- const generateState = useCallback(generateAuthState, []);
7
- const verifyState = useCallback(verifyAuthState, []);
8
- const clearState = useCallback(clearAuthState, []);
12
+ const generateState = useCallback(() => generateAuthState(), []);
13
+ const verifyState = useCallback((returnedState: string | null) => verifyAuthState(returnedState), []);
14
+ const clearState = useCallback(() => clearAuthState(), []);
15
+
9
16
  return { generateState, verifyState, clearState };
10
17
  }
@@ -1,5 +1,7 @@
1
1
  export * from './auth/composable';
2
- // Publish the boot-time auth entry points.
2
+ // Explicit, not `export *`: domainRouting also holds buildCentralAuthRedirectUrl, which
3
+ // UserSessionProvider already re-exports below, and two star exports of one name resolve to
4
+ // nothing. Only the boot-time entry points an app needs are published here.
3
5
  export {
4
6
  clearCentralAuthRoundTripMarker,
5
7
  isCentralAuthRedirectPending,
@@ -0,0 +1,44 @@
1
+ import { cleanup, render, screen } from '@testing-library/react';
2
+ import * as session from '@vertesia/ui/session';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ import { AUTH_PREVIEW_SCREENS, AuthScreenPreview, mountAuthScreenPreview } from './AuthScreenPreview';
5
+ import * as signin from './login/signInUtils';
6
+
7
+ afterEach(() => {
8
+ cleanup();
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ describe('shared auth visual previews', () => {
13
+ it.each(AUTH_PREVIEW_SCREENS.filter((name) => name !== 'boot'))(
14
+ 'renders %s without initializing or changing authentication',
15
+ (name) => {
16
+ const firebase = vi.spyOn(session, 'getFirebaseAuth');
17
+ const reset = vi.spyOn(signin, 'resetSignInState');
18
+ const { container } = render(<AuthScreenPreview screen={name} branding={{ name: 'Example' }} />);
19
+ expect(container.querySelector('[inert]')?.textContent?.trim()).not.toBe('');
20
+ expect(screen.getByText(`Auth preview: ${name}`)).toBeTruthy();
21
+ expect(firebase).not.toHaveBeenCalled();
22
+ expect(reset).not.toHaveBeenCalled();
23
+ },
24
+ );
25
+
26
+ it('retains branding and full screen overrides, with links to the shared boot URL', () => {
27
+ render(
28
+ <AuthScreenPreview
29
+ screen="loading"
30
+ branding={{ name: 'Example' }}
31
+ screens={{ Loading: () => <p>Custom loader fixture</p> }}
32
+ />,
33
+ );
34
+ expect(screen.getByText('Custom loader fixture')).toBeTruthy();
35
+ expect(screen.getByText('boot').getAttribute('href')).toContain('__vertesia_boot=slow');
36
+ expect(screen.getByText('pending').getAttribute('href')).toContain('__vertesia_auth=pending');
37
+ });
38
+
39
+ it('keeps the React root empty for boot previews', async () => {
40
+ const container = document.createElement('div');
41
+ await mountAuthScreenPreview(container, 'boot');
42
+ expect(container.childNodes).toHaveLength(0);
43
+ });
44
+ });
@@ -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
  });