@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.
Files changed (389) 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/Link.js +1 -1
  15. package/lib/core/components/Link.js.map +1 -1
  16. package/lib/core/components/Spinner.js +1 -1
  17. package/lib/core/components/Spinner.js.map +1 -1
  18. package/lib/core/components/TagsInput.js +7 -7
  19. package/lib/core/components/TagsInput.js.map +1 -1
  20. package/lib/core/components/shadcn/badge.js +1 -1
  21. package/lib/core/components/shadcn/badge.js.map +1 -1
  22. package/lib/core/components/shadcn/button.d.ts.map +1 -1
  23. package/lib/core/components/shadcn/button.js +3 -4
  24. package/lib/core/components/shadcn/button.js.map +1 -1
  25. package/lib/core/components/shadcn/filters/comboBox/DateCombobox.js +2 -2
  26. package/lib/core/components/shadcn/filters/comboBox/DateCombobox.js.map +1 -1
  27. package/lib/core/components/shadcn/filters/comboBox/SelectCombobox.js +1 -1
  28. package/lib/core/components/shadcn/filters/comboBox/SelectCombobox.js.map +1 -1
  29. package/lib/core/components/shadcn/filters/comboBox/StringListCombobox.js +1 -1
  30. package/lib/core/components/shadcn/filters/comboBox/StringListCombobox.js.map +1 -1
  31. package/lib/core/components/shadcn/filters/comboBox/TextCombobox.js +1 -1
  32. package/lib/core/components/shadcn/filters/comboBox/TextCombobox.js.map +1 -1
  33. package/lib/core/components/shadcn/filters/filter/dateFilter.js +1 -1
  34. package/lib/core/components/shadcn/filters/filter/dateFilter.js.map +1 -1
  35. package/lib/core/components/shadcn/filters/filter-styles.d.ts +1 -1
  36. package/lib/core/components/shadcn/filters/filter-styles.d.ts.map +1 -1
  37. package/lib/core/components/shadcn/filters/filter-styles.js +4 -4
  38. package/lib/core/components/shadcn/filters/filters.js +1 -1
  39. package/lib/core/components/shadcn/filters/filters.js.map +1 -1
  40. package/lib/core/components/shadcn/label.js +1 -1
  41. package/lib/core/components/shadcn/label.js.map +1 -1
  42. package/lib/core/components/shadcn/overflowTabs.js +2 -2
  43. package/lib/core/components/shadcn/overflowTabs.js.map +1 -1
  44. package/lib/core/components/shadcn/tabs.js +1 -1
  45. package/lib/core/components/shadcn/tabs.js.map +1 -1
  46. package/lib/env/index.d.ts +28 -1
  47. package/lib/env/index.d.ts.map +1 -1
  48. package/lib/env/index.js +51 -3
  49. package/lib/env/index.js.map +1 -1
  50. package/lib/features/activity-doc/ActivityDoc.js +1 -1
  51. package/lib/features/activity-doc/ActivityDoc.js.map +1 -1
  52. package/lib/features/agent/PayloadBuilder.d.ts +4 -0
  53. package/lib/features/agent/PayloadBuilder.d.ts.map +1 -1
  54. package/lib/features/agent/PayloadBuilder.js.map +1 -1
  55. package/lib/features/agent/chat/AgentRunFeedback.d.ts +59 -0
  56. package/lib/features/agent/chat/AgentRunFeedback.d.ts.map +1 -0
  57. package/lib/features/agent/chat/AgentRunFeedback.js +286 -0
  58. package/lib/features/agent/chat/AgentRunFeedback.js.map +1 -0
  59. package/lib/features/agent/chat/AskUserWidget.d.ts +1 -0
  60. package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
  61. package/lib/features/agent/chat/AskUserWidget.js +15 -4
  62. package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
  63. package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  64. package/lib/features/agent/chat/ModernAgentConversation.js +3 -2
  65. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  66. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts +3 -1
  67. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
  68. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +16 -13
  69. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  70. package/lib/features/agent/chat/ModernAgentOutput/Header.d.ts.map +1 -1
  71. package/lib/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
  72. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts +3 -1
  73. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
  74. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +30 -8
  75. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
  76. package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.d.ts.map +1 -1
  77. package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js +2 -0
  78. package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js.map +1 -1
  79. package/lib/features/agent/chat/index.d.ts +1 -0
  80. package/lib/features/agent/chat/index.d.ts.map +1 -1
  81. package/lib/features/agent/chat/index.js +1 -0
  82. package/lib/features/agent/chat/index.js.map +1 -1
  83. package/lib/features/facets/AgentRunnerFacetsNav.d.ts +8 -0
  84. package/lib/features/facets/AgentRunnerFacetsNav.d.ts.map +1 -1
  85. package/lib/features/facets/AgentRunnerFacetsNav.js +12 -0
  86. package/lib/features/facets/AgentRunnerFacetsNav.js.map +1 -1
  87. package/lib/features/layout/GenericPageNavHeader.js +1 -1
  88. package/lib/features/layout/GenericPageNavHeader.js.map +1 -1
  89. package/lib/features/magic-pdf/AnnotatedImageSlider.js +1 -1
  90. package/lib/features/magic-pdf/AnnotatedImageSlider.js.map +1 -1
  91. package/lib/features/magic-pdf/GroundedExtractionView.js +3 -3
  92. package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -1
  93. package/lib/features/permissions/UserPermissionsProvider.d.ts +15 -1
  94. package/lib/features/permissions/UserPermissionsProvider.d.ts.map +1 -1
  95. package/lib/features/permissions/UserPermissionsProvider.js +20 -12
  96. package/lib/features/permissions/UserPermissionsProvider.js.map +1 -1
  97. package/lib/features/store/objects/DocumentPreviewPanel.js +5 -5
  98. package/lib/features/store/objects/DocumentPreviewPanel.js.map +1 -1
  99. package/lib/features/store/objects/components/DocumentInput.js +1 -1
  100. package/lib/features/store/objects/components/DocumentInput.js.map +1 -1
  101. package/lib/features/store/objects/selection/SelectionActions.js +1 -1
  102. package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
  103. package/lib/features/store/objects/upload/DocumentUploadModal.js +1 -1
  104. package/lib/features/store/objects/upload/DocumentUploadModal.js.map +1 -1
  105. package/lib/features/store/types/TypeRegistry.d.ts +130 -0
  106. package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
  107. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +130 -0
  108. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
  109. package/lib/features/user/UserInfo.js +1 -1
  110. package/lib/features/user/UserInfo.js.map +1 -1
  111. package/lib/i18n/index.d.ts +5 -4
  112. package/lib/i18n/index.d.ts.map +1 -1
  113. package/lib/i18n/index.js +16 -2
  114. package/lib/i18n/index.js.map +1 -1
  115. package/lib/i18n/locales/ar.json +30 -0
  116. package/lib/i18n/locales/de.json +30 -0
  117. package/lib/i18n/locales/en.json +30 -0
  118. package/lib/i18n/locales/es.json +30 -0
  119. package/lib/i18n/locales/fr.json +30 -0
  120. package/lib/i18n/locales/it.json +30 -0
  121. package/lib/i18n/locales/ja.json +30 -0
  122. package/lib/i18n/locales/ko.json +30 -0
  123. package/lib/i18n/locales/pt.json +30 -0
  124. package/lib/i18n/locales/ru.json +30 -0
  125. package/lib/i18n/locales/tr.json +30 -0
  126. package/lib/i18n/locales/zh-TW.json +30 -0
  127. package/lib/i18n/locales/zh.json +30 -0
  128. package/lib/layout/ErrorPage.js +1 -1
  129. package/lib/layout/ErrorPage.js.map +1 -1
  130. package/lib/layout/Navbar.d.ts.map +1 -1
  131. package/lib/layout/Navbar.js +3 -2
  132. package/lib/layout/Navbar.js.map +1 -1
  133. package/lib/router/RouteComponent.d.ts.map +1 -1
  134. package/lib/router/RouteComponent.js +15 -12
  135. package/lib/router/RouteComponent.js.map +1 -1
  136. package/lib/session/UserSession.d.ts.map +1 -1
  137. package/lib/session/UserSession.js +6 -3
  138. package/lib/session/UserSession.js.map +1 -1
  139. package/lib/session/UserSessionProvider.d.ts +2 -1
  140. package/lib/session/UserSessionProvider.d.ts.map +1 -1
  141. package/lib/session/UserSessionProvider.js +10 -1
  142. package/lib/session/UserSessionProvider.js.map +1 -1
  143. package/lib/session/auth/composable.d.ts.map +1 -1
  144. package/lib/session/auth/composable.js +88 -3
  145. package/lib/session/auth/composable.js.map +1 -1
  146. package/lib/session/auth/domainRouting.d.ts +22 -12
  147. package/lib/session/auth/domainRouting.d.ts.map +1 -1
  148. package/lib/session/auth/domainRouting.js +35 -21
  149. package/lib/session/auth/domainRouting.js.map +1 -1
  150. package/lib/session/auth/useAuthState.d.ts +9 -4
  151. package/lib/session/auth/useAuthState.d.ts.map +1 -1
  152. package/lib/session/auth/useAuthState.js +9 -4
  153. package/lib/session/auth/useAuthState.js.map +1 -1
  154. package/lib/session/index.d.ts.map +1 -1
  155. package/lib/session/index.js +3 -1
  156. package/lib/session/index.js.map +1 -1
  157. package/lib/shell/AuthScreenPreview.d.ts +12 -0
  158. package/lib/shell/AuthScreenPreview.d.ts.map +1 -0
  159. package/lib/shell/AuthScreenPreview.js +140 -0
  160. package/lib/shell/AuthScreenPreview.js.map +1 -0
  161. package/lib/shell/BrandedAuthScreens.d.ts +19 -0
  162. package/lib/shell/BrandedAuthScreens.d.ts.map +1 -0
  163. package/lib/shell/BrandedAuthScreens.js +64 -0
  164. package/lib/shell/BrandedAuthScreens.js.map +1 -0
  165. package/lib/shell/SplashScreen.d.ts +13 -2
  166. package/lib/shell/SplashScreen.d.ts.map +1 -1
  167. package/lib/shell/SplashScreen.js +21 -20
  168. package/lib/shell/SplashScreen.js.map +1 -1
  169. package/lib/shell/VertesiaShell.d.ts +15 -4
  170. package/lib/shell/VertesiaShell.d.ts.map +1 -1
  171. package/lib/shell/VertesiaShell.js +8 -3
  172. package/lib/shell/VertesiaShell.js.map +1 -1
  173. package/lib/shell/apps/StandaloneApp.js +5 -1
  174. package/lib/shell/apps/StandaloneApp.js.map +1 -1
  175. package/lib/shell/index.d.ts +6 -0
  176. package/lib/shell/index.d.ts.map +1 -1
  177. package/lib/shell/index.js +5 -0
  178. package/lib/shell/index.js.map +1 -1
  179. package/lib/shell/login/SignInFlow.d.ts +68 -0
  180. package/lib/shell/login/SignInFlow.d.ts.map +1 -0
  181. package/lib/shell/login/SignInFlow.js +108 -0
  182. package/lib/shell/login/SignInFlow.js.map +1 -0
  183. package/lib/shell/login/SignInPageShell.d.ts +13 -0
  184. package/lib/shell/login/SignInPageShell.d.ts.map +1 -0
  185. package/lib/shell/login/SignInPageShell.js +14 -0
  186. package/lib/shell/login/SignInPageShell.js.map +1 -0
  187. package/lib/shell/login/SignInPrimitives.d.ts +3 -3
  188. package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
  189. package/lib/shell/login/SignInPrimitives.js +13 -11
  190. package/lib/shell/login/SignInPrimitives.js.map +1 -1
  191. package/lib/shell/login/SignInRestrictedEnvStep.d.ts +5 -0
  192. package/lib/shell/login/SignInRestrictedEnvStep.d.ts.map +1 -1
  193. package/lib/shell/login/SignInRestrictedEnvStep.js +8 -4
  194. package/lib/shell/login/SignInRestrictedEnvStep.js.map +1 -1
  195. package/lib/shell/login/SigninScreen.d.ts +38 -3
  196. package/lib/shell/login/SigninScreen.d.ts.map +1 -1
  197. package/lib/shell/login/SigninScreen.js +33 -86
  198. package/lib/shell/login/SigninScreen.js.map +1 -1
  199. package/lib/shell/login/SignupForm.d.ts +3 -1
  200. package/lib/shell/login/SignupForm.d.ts.map +1 -1
  201. package/lib/shell/login/SignupForm.js +7 -3
  202. package/lib/shell/login/SignupForm.js.map +1 -1
  203. package/lib/shell/login/signInUtils.d.ts +11 -0
  204. package/lib/shell/login/signInUtils.d.ts.map +1 -1
  205. package/lib/shell/login/signInUtils.js +29 -5
  206. package/lib/shell/login/signInUtils.js.map +1 -1
  207. package/lib/vertesia-ui-boot.js +237 -0
  208. package/lib/vertesia-ui-boot.js.map +1 -0
  209. package/lib/vertesia-ui-core.js +46 -46
  210. package/lib/vertesia-ui-core.js.map +1 -1
  211. package/lib/vertesia-ui-env.js +1 -1
  212. package/lib/vertesia-ui-env.js.map +1 -1
  213. package/lib/vertesia-ui-features.js +222 -208
  214. package/lib/vertesia-ui-features.js.map +1 -1
  215. package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js +5 -0
  216. package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js.map +1 -0
  217. package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js +5 -0
  218. package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js.map +1 -0
  219. package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js +5 -0
  220. package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js.map +1 -0
  221. package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js +5 -0
  222. package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js.map +1 -0
  223. package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js +5 -0
  224. package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js.map +1 -0
  225. package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js +5 -0
  226. package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js.map +1 -0
  227. package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js +5 -0
  228. package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js.map +1 -0
  229. package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js +5 -0
  230. package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js.map +1 -0
  231. package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js +5 -0
  232. package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js.map +1 -0
  233. package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js +5 -0
  234. package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js.map +1 -0
  235. package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js +5 -0
  236. package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js.map +1 -0
  237. package/lib/vertesia-ui-i18n.js +2 -2
  238. package/lib/vertesia-ui-i18n.js.map +1 -1
  239. package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js +5 -0
  240. package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js.map +1 -0
  241. package/lib/vertesia-ui-layout.js +13 -13
  242. package/lib/vertesia-ui-layout.js.map +1 -1
  243. package/lib/vertesia-ui-rich-text.js +68 -68
  244. package/lib/vertesia-ui-rich-text.js.map +1 -1
  245. package/lib/vertesia-ui-router.js +1 -1
  246. package/lib/vertesia-ui-router.js.map +1 -1
  247. package/lib/vertesia-ui-session.js +1 -1
  248. package/lib/vertesia-ui-session.js.map +1 -1
  249. package/lib/vertesia-ui-shell.js +58 -33
  250. package/lib/vertesia-ui-shell.js.map +1 -1
  251. package/lib/vertesia-ui-widgets.js +94 -94
  252. package/lib/vertesia-ui-widgets.js.map +1 -1
  253. package/lib/widgets/json-view/JSONCode.js +1 -1
  254. package/lib/widgets/json-view/JSONCode.js.map +1 -1
  255. package/lib/widgets/upload/DropZone.d.ts.map +1 -1
  256. package/lib/widgets/upload/DropZone.js +1 -1
  257. package/lib/widgets/upload/DropZone.js.map +1 -1
  258. package/llms.txt +6 -2
  259. package/package.json +44 -35
  260. package/src/__tests__/a11y.test.tsx +25 -0
  261. package/src/__tests__/theme.test.ts +91 -0
  262. package/src/boot/branding.test.ts +72 -0
  263. package/src/boot/branding.ts +108 -0
  264. package/src/boot/index.test.ts +216 -0
  265. package/src/boot/index.ts +356 -0
  266. package/src/boot/vite.test.ts +93 -0
  267. package/src/boot/vite.ts +118 -0
  268. package/src/core/components/Link.tsx +1 -1
  269. package/src/core/components/Spinner.tsx +1 -1
  270. package/src/core/components/TagsInput.tsx +7 -7
  271. package/src/core/components/shadcn/badge.tsx +1 -1
  272. package/src/core/components/shadcn/button.tsx +3 -5
  273. package/src/core/components/shadcn/filters/comboBox/DateCombobox.tsx +2 -2
  274. package/src/core/components/shadcn/filters/comboBox/SelectCombobox.tsx +1 -1
  275. package/src/core/components/shadcn/filters/comboBox/StringListCombobox.tsx +1 -1
  276. package/src/core/components/shadcn/filters/comboBox/TextCombobox.tsx +1 -1
  277. package/src/core/components/shadcn/filters/filter/dateFilter.tsx +1 -1
  278. package/src/core/components/shadcn/filters/filter-styles.ts +4 -4
  279. package/src/core/components/shadcn/filters/filters.tsx +1 -1
  280. package/src/core/components/shadcn/label.tsx +1 -1
  281. package/src/core/components/shadcn/overflowTabs.tsx +2 -2
  282. package/src/core/components/shadcn/tabs.tsx +1 -1
  283. package/src/css/utilities.css +7 -0
  284. package/src/env/index.test.ts +100 -0
  285. package/src/env/index.ts +66 -3
  286. package/src/features/activity-doc/ActivityDoc.tsx +1 -1
  287. package/src/features/agent/PayloadBuilder.tsx +4 -0
  288. package/src/features/agent/chat/AgentRunFeedback.test.tsx +330 -0
  289. package/src/features/agent/chat/AgentRunFeedback.tsx +452 -0
  290. package/src/features/agent/chat/AskUserWidget.test.tsx +24 -2
  291. package/src/features/agent/chat/AskUserWidget.tsx +19 -4
  292. package/src/features/agent/chat/ModernAgentConversation.tsx +46 -42
  293. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +207 -0
  294. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +50 -31
  295. package/src/features/agent/chat/ModernAgentOutput/Header.tsx +4 -0
  296. package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +42 -8
  297. package/src/features/agent/chat/ModernAgentOutput/SummaryConversation.ts +2 -0
  298. package/src/features/agent/chat/ModernAgentOutput/utils.test.ts +14 -0
  299. package/src/features/agent/chat/hooks/useFileProcessing.test.tsx +0 -5
  300. package/src/features/agent/chat/index.ts +6 -0
  301. package/src/features/facets/AgentRunnerFacetsNav.tsx +26 -0
  302. package/src/features/layout/GenericPageNavHeader.test.tsx +36 -0
  303. package/src/features/layout/GenericPageNavHeader.tsx +1 -1
  304. package/src/features/magic-pdf/AnnotatedImageSlider.tsx +1 -1
  305. package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
  306. package/src/features/permissions/UserPermissionsProvider.test.tsx +50 -1
  307. package/src/features/permissions/UserPermissionsProvider.tsx +82 -44
  308. package/src/features/store/objects/DocumentPreviewPanel.tsx +10 -10
  309. package/src/features/store/objects/components/DocumentInput.tsx +1 -1
  310. package/src/features/store/objects/selection/SelectionActions.tsx +4 -1
  311. package/src/features/store/objects/upload/DocumentUploadModal.tsx +1 -1
  312. package/src/features/user/UserInfo.tsx +1 -1
  313. package/src/i18n/index.tsx +25 -2
  314. package/src/i18n/locales/ar.json +30 -0
  315. package/src/i18n/locales/de.json +30 -0
  316. package/src/i18n/locales/en.json +30 -0
  317. package/src/i18n/locales/es.json +30 -0
  318. package/src/i18n/locales/fr.json +30 -0
  319. package/src/i18n/locales/it.json +30 -0
  320. package/src/i18n/locales/ja.json +30 -0
  321. package/src/i18n/locales/ko.json +30 -0
  322. package/src/i18n/locales/pt.json +30 -0
  323. package/src/i18n/locales/ru.json +30 -0
  324. package/src/i18n/locales/tr.json +30 -0
  325. package/src/i18n/locales/zh-TW.json +30 -0
  326. package/src/i18n/locales/zh.json +30 -0
  327. package/src/layout/ErrorPage.tsx +1 -1
  328. package/src/layout/Navbar.test.tsx +58 -0
  329. package/src/layout/Navbar.tsx +5 -2
  330. package/src/router/RouteComponent.test.tsx +65 -1
  331. package/src/router/RouteComponent.tsx +18 -15
  332. package/src/session/UserSession.test.ts +65 -0
  333. package/src/session/UserSession.ts +6 -4
  334. package/src/session/UserSessionProvider.strict-mode.test.tsx +44 -0
  335. package/src/session/UserSessionProvider.test.ts +62 -1
  336. package/src/session/UserSessionProvider.tsx +9 -1
  337. package/src/session/auth/composable.renewal.test.ts +170 -0
  338. package/src/session/auth/composable.test.ts +23 -1
  339. package/src/session/auth/composable.ts +101 -2
  340. package/src/session/auth/domainRouting.test.ts +35 -65
  341. package/src/session/auth/domainRouting.ts +51 -34
  342. package/src/session/auth/useAuthState.ts +11 -4
  343. package/src/session/index.ts +3 -1
  344. package/src/shell/AuthScreenPreview.test.tsx +44 -0
  345. package/src/shell/AuthScreenPreview.tsx +228 -0
  346. package/src/shell/BrandedAuthScreens.test.tsx +181 -0
  347. package/src/shell/BrandedAuthScreens.tsx +117 -0
  348. package/src/shell/SplashScreen.test.tsx +37 -0
  349. package/src/shell/SplashScreen.tsx +45 -40
  350. package/src/shell/VertesiaShell.tsx +49 -22
  351. package/src/shell/apps/StandaloneApp.tsx +5 -1
  352. package/src/shell/index.tsx +6 -0
  353. package/src/shell/login/SignInFlow.tsx +213 -0
  354. package/src/shell/login/SignInPageShell.tsx +62 -0
  355. package/src/shell/login/SignInPrimitives.tsx +14 -11
  356. package/src/shell/login/SignInRestrictedEnvStep.tsx +19 -7
  357. package/src/shell/login/SigninScreen.test.tsx +104 -0
  358. package/src/shell/login/SigninScreen.tsx +115 -145
  359. package/src/shell/login/SignupForm.tsx +8 -3
  360. package/src/shell/login/signInUtils.ts +30 -5
  361. package/src/widgets/json-view/JSONCode.test.tsx +2 -2
  362. package/src/widgets/json-view/JSONCode.tsx +1 -1
  363. package/src/widgets/markdown/markdown.css +21 -21
  364. package/src/widgets/upload/DropZone.tsx +2 -4
  365. package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js +0 -5
  366. package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js.map +0 -1
  367. package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js +0 -5
  368. package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js.map +0 -1
  369. package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js +0 -5
  370. package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js.map +0 -1
  371. package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js +0 -5
  372. package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js.map +0 -1
  373. package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js +0 -5
  374. package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js.map +0 -1
  375. package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js +0 -5
  376. package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js.map +0 -1
  377. package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js +0 -5
  378. package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js.map +0 -1
  379. package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js +0 -5
  380. package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js.map +0 -1
  381. package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js +0 -5
  382. package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js.map +0 -1
  383. package/lib/vertesia-ui-i18n.DN79y06J.chunk.js +0 -5
  384. package/lib/vertesia-ui-i18n.DN79y06J.chunk.js.map +0 -1
  385. package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js +0 -5
  386. package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js.map +0 -1
  387. package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js +0 -5
  388. package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js.map +0 -1
  389. package/src/session/auth/authState.test.ts +0 -35
@@ -0,0 +1,216 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ BOOT_SCREEN_STYLES,
4
+ createBootScreenVitePlugin,
5
+ injectBootScreenHtml,
6
+ renderBootScreenHead,
7
+ renderBootScreenRuntime,
8
+ renderDefaultBootContent,
9
+ } from './index.js';
10
+
11
+ interface BootScreenApi {
12
+ ensureLoadingIndicator: () => void;
13
+ hideLoadingIndicatorOnFirstRender: () => void;
14
+ scheduleSlowLoadingNotice: () => void;
15
+ hideLoadingIndicator: () => void;
16
+ }
17
+
18
+ function bootApi(): BootScreenApi {
19
+ return (window as unknown as { __vertesiaBoot: BootScreenApi }).__vertesiaBoot;
20
+ }
21
+
22
+ function runRuntime({ dark = false, storageKey = 'test-theme' }: { dark?: boolean; storageKey?: string } = {}) {
23
+ Object.defineProperty(window, 'matchMedia', {
24
+ configurable: true,
25
+ value: vi.fn(() => ({ matches: dark })),
26
+ });
27
+ new Function(renderBootScreenRuntime({ storageKey }))();
28
+ }
29
+
30
+ afterEach(() => {
31
+ document.getElementById('loading-indicator')?.remove();
32
+ vi.useRealTimers();
33
+ localStorage.clear();
34
+ document.documentElement.className = '';
35
+ document.documentElement.removeAttribute('style');
36
+ document.body.removeAttribute('style');
37
+ document.body.innerHTML = '';
38
+ document.title = '';
39
+ vi.restoreAllMocks();
40
+ });
41
+
42
+ describe('boot screen HTML integration', () => {
43
+ it('injects the shared styles and runtime before app assets', () => {
44
+ const plugin = createBootScreenVitePlugin({ storageKey: 'auth-ui-theme', iconSrc: '/auth-icon.svg' });
45
+ const html = plugin.transformIndexHtml(
46
+ '<html><head><meta charset="UTF-8"><script src="/app.js"></script></head><body><div id="root"></div></body></html>',
47
+ );
48
+
49
+ expect(html.indexOf('<meta charset="UTF-8">')).toBeLessThan(html.indexOf('id="vertesia-boot-styles"'));
50
+ expect(html.indexOf('id="vertesia-boot-styles"')).toBeLessThan(html.indexOf('src="/app.js"'));
51
+ expect(html).toContain(BOOT_SCREEN_STYLES);
52
+ expect(html).toContain('auth-ui-theme');
53
+ expect(html).toContain('/auth-icon.svg');
54
+ const content = document.createElement('div');
55
+ content.innerHTML = renderDefaultBootContent();
56
+ expect(content.querySelector('.vboot-spinner')?.getAttribute('width')).toBe('40');
57
+ expect(content.querySelector('.vboot-spinner')?.getAttribute('height')).toBe('40');
58
+ expect(html).toContain('window.__vertesiaBoot.hideLoadingIndicatorOnFirstRender()');
59
+ });
60
+
61
+ it('can render the same bootstrap into server-generated HTML', () => {
62
+ const html = renderBootScreenHead({ storageKey: 'vite-ui-theme' });
63
+
64
+ expect(html).toContain('id="vertesia-boot-styles"');
65
+ expect(html).toContain('window.__vertesiaBoot');
66
+ expect(html).toContain('vite-ui-theme');
67
+ });
68
+
69
+ it('injects the bootstrap before scripts in a document without a charset meta tag', () => {
70
+ const html = injectBootScreenHtml(
71
+ '<!doctype html><html><head><script src="handler.js"></script></head><body></body></html>',
72
+ { storageKey: 'auth-ui-theme', iconSrc: '/auth-icon.svg' },
73
+ );
74
+
75
+ expect(html.indexOf('id="vertesia-boot-styles"')).toBeLessThan(html.indexOf('src="handler.js"'));
76
+ expect(html).toContain('auth-ui-theme');
77
+ expect(html).toContain('/auth-icon.svg');
78
+ expect(html).toContain('window.__vertesiaBoot.ensureLoadingIndicator()');
79
+ });
80
+
81
+ it('handles tag and attribute casing without regular expressions', () => {
82
+ const html = injectBootScreenHtml(
83
+ '<!doctype html><HTML><HEAD><META CHARSET = "UTF-8"><script src="handler.js"></script></HEAD><BODY></BODY></HTML>',
84
+ );
85
+
86
+ expect(html.indexOf('<META CHARSET = "UTF-8">')).toBeLessThan(html.indexOf('id="vertesia-boot-styles"'));
87
+ expect(html.indexOf('id="vertesia-boot-styles"')).toBeLessThan(html.indexOf('src="handler.js"'));
88
+ expect(html.indexOf('window.__vertesiaBoot.ensureLoadingIndicator()')).toBeLessThan(html.indexOf('</BODY>'));
89
+ });
90
+
91
+ it('scans repeated malformed tag prefixes in linear time', () => {
92
+ const malformedPrefixes = '<metadata>'.repeat(10_000);
93
+ const html = `<!doctype html><html><head>${malformedPrefixes}<script src="handler.js"></script></head><body></body></html>`;
94
+
95
+ const injected = injectBootScreenHtml(html);
96
+
97
+ expect(injected.indexOf('id="vertesia-boot-styles"')).toBeLessThan(injected.indexOf(malformedPrefixes));
98
+ expect(injected).toContain('window.__vertesiaBoot.ensureLoadingIndicator()');
99
+ });
100
+ });
101
+
102
+ describe('boot screen runtime', () => {
103
+ it('paints dark before React when the system prefers dark mode', () => {
104
+ runRuntime({ dark: true });
105
+
106
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
107
+ expect(document.documentElement.style.backgroundColor).toBe('rgb(10, 10, 10)');
108
+ expect(document.documentElement.style.colorScheme).toBe('dark');
109
+ });
110
+
111
+ it('lets an explicit stored theme override the system preference', () => {
112
+ localStorage.setItem('test-theme', 'light');
113
+ runRuntime({ dark: true });
114
+
115
+ expect(document.documentElement.classList.contains('light')).toBe(true);
116
+ expect(document.documentElement.style.backgroundColor).toBe('rgb(255, 255, 255)');
117
+ expect(document.documentElement.style.colorScheme).toBe('light');
118
+ });
119
+
120
+ it('shows the shared loader and hands the document back after the app renders', async () => {
121
+ document.body.innerHTML = '<div id="root"></div>';
122
+ runRuntime({ dark: true });
123
+ bootApi().ensureLoadingIndicator();
124
+ bootApi().hideLoadingIndicatorOnFirstRender();
125
+
126
+ const loadingIndicator = document.getElementById('loading-indicator');
127
+ expect(loadingIndicator).not.toBeNull();
128
+ expect(loadingIndicator?.parentElement).toBe(document.documentElement);
129
+ document.getElementById('root')?.append(document.createElement('main'));
130
+ await vi.waitFor(() => expect(document.getElementById('loading-indicator')?.style.display).toBe('none'));
131
+
132
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
133
+ expect(document.documentElement.style.backgroundColor).toBe('');
134
+ expect(document.documentElement.style.colorScheme).toBe('');
135
+ });
136
+ });
137
+
138
+ describe('custom boot branding', () => {
139
+ it('copies the document title as text into app-owned title elements', () => {
140
+ document.title = '<My application>';
141
+ new Function(renderBootScreenRuntime({ html: '<h1 data-boot-title>Welcome</h1>' }))();
142
+ bootApi().ensureLoadingIndicator();
143
+ expect(document.querySelector('[data-boot-title]')?.textContent).toBe('<My application>');
144
+ expect(document.querySelector('[data-boot-title]')?.children.length).toBe(0);
145
+ });
146
+
147
+ it('renders app HTML without the default spinner and hands off even when React already rendered', () => {
148
+ document.body.innerHTML = '<div id="root"><main>React application</main></div>';
149
+ new Function(renderBootScreenRuntime({ html: '<section>My custom brand</section>' }))();
150
+ bootApi().ensureLoadingIndicator();
151
+ const loader = document.getElementById('loading-indicator');
152
+ expect(loader?.parentElement).toBe(document.body);
153
+ expect(loader?.textContent).toBe('My custom brand');
154
+ expect(loader?.querySelector('.vboot-spinner')).toBeNull();
155
+ bootApi().hideLoadingIndicatorOnFirstRender();
156
+ expect(loader?.style.display).toBe('none');
157
+ expect(document.getElementById('root')?.textContent).toBe('React application');
158
+ });
159
+
160
+ it('escapes script terminators in trusted HTML while preserving the supplied content', () => {
161
+ const html = '<p>Brand</p><!-- </script><script>unexpected()</script> -->';
162
+ const runtime = renderBootScreenRuntime({ html });
163
+ expect(runtime).not.toContain('</script>');
164
+ new Function(runtime)();
165
+ bootApi().ensureLoadingIndicator();
166
+ expect(document.getElementById('loading-indicator')?.innerHTML).toBe(html);
167
+ });
168
+
169
+ it('inlines app styles after the defaults and respects a custom document background', () => {
170
+ const styles = ':root { --vertesia-boot-background: rgb(12, 34, 56); }';
171
+ const html = renderBootScreenHead({ styles });
172
+ expect(html.indexOf(styles)).toBeGreaterThan(html.indexOf(BOOT_SCREEN_STYLES));
173
+ document.documentElement.style.setProperty('--vertesia-boot-background', 'rgb(12, 34, 56)');
174
+ runRuntime();
175
+ expect(document.documentElement.style.backgroundColor).toBe('rgb(12, 34, 56)');
176
+ });
177
+
178
+ it('reveals app-owned slow-load recovery and cancels timers on handoff', async () => {
179
+ vi.useFakeTimers();
180
+ document.body.innerHTML = '<div id="root"></div>';
181
+ new Function(
182
+ renderBootScreenRuntime({
183
+ html: `
184
+ <p>Branded startup</p>
185
+ <div id="loading-slow-notice" style="display:none">Taking longer
186
+ <div id="loading-slow-reload" style="display:none"><button data-boot-reload>Reload</button></div>
187
+ </div>`,
188
+ }),
189
+ )();
190
+ bootApi().ensureLoadingIndicator();
191
+ bootApi().hideLoadingIndicatorOnFirstRender();
192
+ bootApi().scheduleSlowLoadingNotice();
193
+ await vi.advanceTimersByTimeAsync(10000);
194
+ expect(document.getElementById('loading-slow-notice')?.style.display).toBe('');
195
+ expect(document.getElementById('loading-slow-reload')?.style.display).toBe('none');
196
+ await vi.advanceTimersByTimeAsync(20000);
197
+ expect(document.getElementById('loading-slow-reload')?.style.display).toBe('');
198
+ document.getElementById('root')?.append(document.createElement('main'));
199
+ await Promise.resolve();
200
+ expect(document.getElementById('loading-indicator')?.style.display).toBe('none');
201
+ expect(vi.getTimerCount()).toBe(0);
202
+ });
203
+
204
+ it('cancels pending slow-load notices when React renders promptly', async () => {
205
+ vi.useFakeTimers();
206
+ document.body.innerHTML = '<div id="root"></div>';
207
+ runRuntime();
208
+ bootApi().ensureLoadingIndicator();
209
+ bootApi().hideLoadingIndicatorOnFirstRender();
210
+ bootApi().scheduleSlowLoadingNotice();
211
+ document.getElementById('root')?.append(document.createElement('main'));
212
+ await Promise.resolve();
213
+ expect(vi.getTimerCount()).toBe(0);
214
+ expect(document.getElementById('loading-slow-notice')?.style.display).toBe('none');
215
+ });
216
+ });
@@ -0,0 +1,356 @@
1
+ export interface BootScreenOptions {
2
+ storageKey?: string;
3
+ iconSrc?: string;
4
+ darkIconSrc?: string;
5
+ slowLoadingLabel?: string;
6
+ reloadLabel?: string;
7
+ loadingLabel?: string;
8
+ autoStart?: boolean;
9
+ /** Trusted, app-authored HTML inside the loading container. Never pass user content. */
10
+ html?: string;
11
+ /** Inline CSS appended after the defaults; available before application styles load. */
12
+ styles?: string;
13
+ }
14
+
15
+ export interface BootScreenHtmlPlugin {
16
+ name: string;
17
+ transformIndexHtml: (html: string) => string;
18
+ }
19
+
20
+ const DEFAULT_STORAGE_KEY = 'vite-ui-theme';
21
+ const DEFAULT_ICON_SRC = '/icon.svg';
22
+ const DEFAULT_LOADING_LABEL = 'Loading';
23
+
24
+ export const BOOT_SCREEN_STYLES = `
25
+ .vboot {
26
+ --vb-bg: #ffffff;
27
+ --vb-fg: #0a0a0a;
28
+ --vb-muted: #636363;
29
+ --vb-muted-bg: #f5f5f5;
30
+ --vb-border: #e5e5e5;
31
+ --vb-primary: #0048bd;
32
+ --vb-primary-fg: #ffffff;
33
+ --vb-glow: rgba(50, 49, 189, 0.12);
34
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
35
+ color: var(--vb-fg);
36
+ }
37
+ .vboot.vboot-dark {
38
+ --vb-bg: #0a0a0a;
39
+ --vb-fg: #fafafa;
40
+ --vb-muted: #a1a1a1;
41
+ --vb-muted-bg: #262626;
42
+ --vb-border: rgba(255, 255, 255, 0.1);
43
+ --vb-primary: #2b69d1;
44
+ --vb-primary-fg: #fafafa;
45
+ --vb-glow: rgba(79, 70, 229, 0.2);
46
+ }
47
+ @keyframes vboot-spin {
48
+ from { transform: rotate(0deg); }
49
+ to { transform: rotate(360deg); }
50
+ }
51
+ .vboot-overlay {
52
+ position: absolute !important;
53
+ inset: 0 !important;
54
+ box-sizing: border-box !important;
55
+ width: 100% !important;
56
+ height: 100% !important;
57
+ margin: 0 !important;
58
+ padding: 0 !important;
59
+ display: flex !important;
60
+ flex-direction: column !important;
61
+ align-items: center !important;
62
+ justify-content: center !important;
63
+ gap: 1.5rem !important;
64
+ transform: none !important;
65
+ background: var(--vb-bg) !important;
66
+ }
67
+ #loading-indicator {
68
+ position: fixed !important;
69
+ inset: 0 !important;
70
+ z-index: 2147483647 !important;
71
+ box-sizing: border-box !important;
72
+ width: 100vw !important;
73
+ height: 100vh !important;
74
+ margin: 0 !important;
75
+ padding: 0 !important;
76
+ transform: none !important;
77
+ }
78
+ .vboot-spinner {
79
+ width: 2.5rem !important;
80
+ min-width: 2.5rem !important;
81
+ max-width: 2.5rem !important;
82
+ height: 2.5rem !important;
83
+ min-height: 2.5rem !important;
84
+ max-height: 2.5rem !important;
85
+ margin: 0 !important;
86
+ padding: 0 !important;
87
+ object-fit: contain !important;
88
+ flex: none !important;
89
+ border-radius: 100% !important;
90
+ animation: var(--vertesia-loading-animation, vboot-spin 2s linear infinite) !important;
91
+ }
92
+ .vboot-icon-dark { display: none; }
93
+ .vboot-dark .vboot-icon-light { display: none; }
94
+ .vboot-dark .vboot-icon-dark { display: block; }
95
+ .vboot-slow {
96
+ text-align: center;
97
+ }
98
+ .vboot-slow p {
99
+ color: var(--vb-muted);
100
+ font-size: 0.875rem;
101
+ margin: 0 0 0.75rem;
102
+ }
103
+ .vboot-btn {
104
+ display: inline-block;
105
+ padding: 0.5rem 1rem;
106
+ background: var(--vb-primary);
107
+ color: var(--vb-primary-fg);
108
+ border: none;
109
+ border-radius: 0.375rem;
110
+ font: inherit;
111
+ font-size: 0.875rem;
112
+ font-weight: 500;
113
+ cursor: pointer;
114
+ }
115
+ .vboot-btn:hover {
116
+ opacity: 0.9;
117
+ }
118
+ @media (prefers-reduced-motion: reduce) {
119
+ .vboot-spinner { animation: none !important; }
120
+ }
121
+ `;
122
+
123
+ function inlineJson(value: string): string {
124
+ return JSON.stringify(value).replaceAll('<', '\\u003c');
125
+ }
126
+
127
+ function normalizedOptions(options: BootScreenOptions) {
128
+ return {
129
+ storageKey: options.storageKey ?? DEFAULT_STORAGE_KEY,
130
+ iconSrc: options.iconSrc ?? DEFAULT_ICON_SRC,
131
+ loadingLabel: options.loadingLabel ?? DEFAULT_LOADING_LABEL,
132
+ autoStart: options.autoStart ?? true,
133
+ html: options.html,
134
+ styles: options.styles,
135
+ };
136
+ }
137
+
138
+ /** Default markup shared by Studio and configurable applications. */
139
+ export function renderDefaultBootContent(options: BootScreenOptions = {}): string {
140
+ const escapeMarkup = (value: string) =>
141
+ value.replace(
142
+ /[&<>"']/g,
143
+ (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char] ?? char,
144
+ );
145
+ const { iconSrc, loadingLabel } = normalizedOptions(options);
146
+ const icon = (src: string, className = '') =>
147
+ `<img class="vboot-spinner ${className}" width="40" height="40" src="${escapeMarkup(src)}" alt="${escapeMarkup(loadingLabel)}" />`;
148
+ return `<div class="vboot-overlay" role="status" aria-live="polite">${options.darkIconSrc ? icon(iconSrc, 'vboot-icon-light') + icon(options.darkIconSrc, 'vboot-icon-dark') : icon(iconSrc)}
149
+ <div id="loading-slow-notice" class="vboot-slow" style="display: none;">
150
+ <p>${escapeMarkup(options.slowLoadingLabel ?? 'Still loading — this is taking longer than usual.')}</p>
151
+ <div id="loading-slow-reload" style="display: none;"><button type="button" class="vboot-btn" data-boot-reload>${escapeMarkup(options.reloadLabel ?? 'Reload page')}</button></div></div></div>`;
152
+ }
153
+
154
+ export function renderBootScreenRuntime(options: BootScreenOptions = {}): string {
155
+ const { storageKey, html } = normalizedOptions(options);
156
+ const defaultHtml = renderDefaultBootContent(options);
157
+ return `(() => {
158
+ const storageKey = ${inlineJson(storageKey)};
159
+
160
+ const customHtml = ${html === undefined ? 'undefined' : inlineJson(html)};
161
+ let slowNoticeTimer;
162
+ let slowReloadTimer;
163
+ const previewTheme = (() => {
164
+ try {
165
+ const value = new URL(window.location.href).searchParams.get('__vertesia_boot_theme');
166
+ return value === 'dark' || value === 'light' ? value : null;
167
+ } catch (_error) {
168
+ return null;
169
+ }
170
+ })();
171
+ const prefersDarkTheme = () => {
172
+ if (previewTheme) return previewTheme === 'dark';
173
+ try {
174
+ const stored = window.localStorage.getItem(storageKey);
175
+ if (stored === 'dark') return true;
176
+ if (stored === 'light') return false;
177
+ } catch (_error) {
178
+ // Storage can be unavailable in private or restricted browsing contexts.
179
+ }
180
+ try {
181
+ return typeof window.matchMedia === 'function' && window.matchMedia('(prefers-color-scheme: dark)').matches;
182
+ } catch (_error) {
183
+ return false;
184
+ }
185
+ };
186
+ const applyTheme = () => {
187
+ const dark = prefersDarkTheme();
188
+ const root = document.documentElement;
189
+ root.classList.remove('light', 'dark');
190
+ root.classList.add(dark ? 'dark' : 'light');
191
+ return dark;
192
+ };
193
+ const paintDocumentBackground = () => {
194
+ const dark = applyTheme();
195
+ const root = document.documentElement;
196
+ const background = window.getComputedStyle(root).getPropertyValue('--vertesia-boot-background').trim()
197
+ || (dark ? '#0a0a0a' : '#ffffff');
198
+ root.style.backgroundColor = background;
199
+ root.style.colorScheme = dark ? 'dark' : 'light';
200
+ if (document.body) document.body.style.backgroundColor = background;
201
+ };
202
+ const restoreDocumentBackground = () => {
203
+ applyTheme();
204
+ const root = document.documentElement;
205
+ root.style.removeProperty('background-color');
206
+ root.style.removeProperty('color-scheme');
207
+ if (document.body) document.body.style.removeProperty('background-color');
208
+ };
209
+ const bootThemeClass = () => prefersDarkTheme() ? 'vboot vboot-dark' : 'vboot';
210
+ const ensureLoadingIndicator = () => {
211
+ if (document.getElementById('loading-indicator') || !document.body) return;
212
+ paintDocumentBackground();
213
+ const loading = document.createElement('div');
214
+ loading.id = 'loading-indicator';
215
+ loading.className = bootThemeClass();
216
+ loading.innerHTML = customHtml ?? ${inlineJson(defaultHtml)};
217
+ loading.querySelectorAll('[data-boot-title]').forEach((heading) => {
218
+ if (document.title) heading.textContent = document.title;
219
+ });
220
+ loading.querySelectorAll('[data-boot-reload]').forEach((button) => {
221
+ button.addEventListener('click', () => window.location.reload());
222
+ });
223
+ // Custom landmarks and controls must stay in body to enter the accessibility tree.
224
+ const loadingParent = customHtml === undefined && typeof document.documentElement.appendChild === 'function'
225
+ ? document.documentElement
226
+ : document.body;
227
+ loadingParent.appendChild(loading);
228
+ };
229
+ const hideLoadingIndicator = () => {
230
+ clearTimeout(slowNoticeTimer);
231
+ clearTimeout(slowReloadTimer);
232
+ const loading = document.getElementById('loading-indicator');
233
+ if (loading) loading.style.display = 'none';
234
+ };
235
+ const hasAppRendered = (rootId = 'root') => {
236
+ const root = document.getElementById(rootId);
237
+ return Boolean(root && root.innerHTML && root.innerHTML.trim() !== '');
238
+ };
239
+ const hideLoadingIndicatorOnFirstRender = (rootId = 'root') => {
240
+ if (typeof MutationObserver !== 'function') return;
241
+ const root = document.getElementById(rootId);
242
+ if (!root) return;
243
+ if (hasAppRendered(rootId)) {
244
+ hideLoadingIndicator();
245
+ restoreDocumentBackground();
246
+ return;
247
+ }
248
+ const observer = new MutationObserver(() => {
249
+ if (!hasAppRendered(rootId)) return;
250
+ hideLoadingIndicator();
251
+ restoreDocumentBackground();
252
+ observer.disconnect();
253
+ });
254
+ observer.observe(root, { childList: true });
255
+ };
256
+ // Used by standalone apps. Hosts with their own error handler keep autoStart disabled.
257
+ const scheduleSlowLoadingNotice = () => {
258
+ clearTimeout(slowNoticeTimer);
259
+ clearTimeout(slowReloadTimer);
260
+ if (hasAppRendered()) return;
261
+ slowNoticeTimer = setTimeout(() => {
262
+ const notice = document.getElementById('loading-slow-notice');
263
+ if (notice) notice.style.display = '';
264
+ }, 10000);
265
+ slowReloadTimer = setTimeout(() => {
266
+ const reload = document.getElementById('loading-slow-reload');
267
+ if (reload) reload.style.display = '';
268
+ }, 30000);
269
+ };
270
+ window.__vertesiaBoot = {
271
+ scheduleSlowLoadingNotice,
272
+ applyTheme,
273
+ bootThemeClass,
274
+ ensureLoadingIndicator,
275
+ hasAppRendered,
276
+ hideLoadingIndicator,
277
+ hideLoadingIndicatorOnFirstRender,
278
+ paintDocumentBackground,
279
+ prefersDarkTheme,
280
+ restoreDocumentBackground,
281
+ };
282
+ paintDocumentBackground();
283
+ })();`;
284
+ }
285
+
286
+ export function renderBootScreenHead(options: BootScreenOptions = {}): string {
287
+ return `<style id="vertesia-boot-styles">${BOOT_SCREEN_STYLES}\n${options.styles ?? ''}</style>\n<script>${renderBootScreenRuntime(options)}</script>`;
288
+ }
289
+
290
+ export function renderBootScreenInitializer(): string {
291
+ return `<script>window.__vertesiaBoot.ensureLoadingIndicator(); window.__vertesiaBoot.hideLoadingIndicatorOnFirstRender(); window.__vertesiaBoot.scheduleSlowLoadingNotice();</script>`;
292
+ }
293
+
294
+ function isHtmlWhitespace(character: string | undefined): boolean {
295
+ return character === ' ' || character === '\t' || character === '\n' || character === '\r' || character === '\f';
296
+ }
297
+
298
+ function hasAttribute(openingTag: string, attributeName: string): boolean {
299
+ const lowerTag = openingTag.toLowerCase();
300
+ let searchFrom = 0;
301
+ while (searchFrom < lowerTag.length) {
302
+ const attributeIndex = lowerTag.indexOf(attributeName, searchFrom);
303
+ if (attributeIndex === -1) return false;
304
+
305
+ const before = lowerTag[attributeIndex - 1];
306
+ let afterIndex = attributeIndex + attributeName.length;
307
+ while (isHtmlWhitespace(lowerTag[afterIndex])) afterIndex += 1;
308
+ if (isHtmlWhitespace(before) && lowerTag[afterIndex] === '=') return true;
309
+ searchFrom = attributeIndex + attributeName.length;
310
+ }
311
+ return false;
312
+ }
313
+
314
+ function findOpeningTagEnd(html: string, tagName: string, attributeName?: string): number | undefined {
315
+ const lowerHtml = html.toLowerCase();
316
+ const tagStart = `<${tagName}`;
317
+ let searchFrom = 0;
318
+ while (searchFrom < lowerHtml.length) {
319
+ const start = lowerHtml.indexOf(tagStart, searchFrom);
320
+ if (start === -1) return undefined;
321
+
322
+ const boundary = lowerHtml[start + tagStart.length];
323
+ if (boundary === '>' || isHtmlWhitespace(boundary)) {
324
+ const end = lowerHtml.indexOf('>', start + tagStart.length);
325
+ if (end === -1) return undefined;
326
+ if (!attributeName || hasAttribute(lowerHtml.slice(start, end + 1), attributeName)) return end + 1;
327
+ searchFrom = end + 1;
328
+ } else {
329
+ searchFrom = start + tagStart.length;
330
+ }
331
+ }
332
+ return undefined;
333
+ }
334
+
335
+ function insertAt(html: string, index: number, content: string): string {
336
+ return `${html.slice(0, index)}${content}${html.slice(index)}`;
337
+ }
338
+
339
+ export function injectBootScreenHtml(html: string, options: BootScreenOptions = {}): string {
340
+ const normalized = normalizedOptions(options);
341
+ const head = renderBootScreenHead(normalized);
342
+ const insertionPoint = findOpeningTagEnd(html, 'meta', 'charset') ?? findOpeningTagEnd(html, 'head');
343
+ const withHead = insertionPoint === undefined ? html : insertAt(html, insertionPoint, `\n${head}`);
344
+ if (!normalized.autoStart) return withHead;
345
+ const bodyEnd = withHead.toLowerCase().indexOf('</body>');
346
+ return bodyEnd === -1 ? withHead : insertAt(withHead, bodyEnd, `${renderBootScreenInitializer()}\n`);
347
+ }
348
+
349
+ export function createBootScreenVitePlugin(options: BootScreenOptions = {}): BootScreenHtmlPlugin {
350
+ return {
351
+ name: 'vertesia-boot-screen',
352
+ transformIndexHtml: (html) => injectBootScreenHtml(html, options),
353
+ };
354
+ }
355
+
356
+ export * from './branding.js';
@@ -0,0 +1,93 @@
1
+ // @vitest-environment node
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { afterEach, describe, expect, it, vi } from 'vitest';
7
+ import { createAppBrandingPlugin, resolveBrandingAssets } from './vite.js';
8
+
9
+ const directories: string[] = [];
10
+ function fixture() {
11
+ const dir = mkdtempSync(join(tmpdir(), 'ui-branding-'));
12
+ directories.push(dir);
13
+ mkdirSync(join(dir, 'assets'));
14
+ writeFileSync(join(dir, 'assets/logo.svg'), '<svg xmlns="http://www.w3.org/2000/svg"/>');
15
+ writeFileSync(join(dir, 'assets/font.woff2'), 'font-bytes');
16
+ return { dir, moduleUrl: pathToFileURL(join(dir, 'index.ts')) };
17
+ }
18
+ afterEach(() => {
19
+ for (const dir of directories.splice(0)) rmSync(dir, { recursive: true, force: true });
20
+ });
21
+
22
+ describe('branding Vite adapter', () => {
23
+ it('resolves local assets once for both React and first paint independently of deployment paths', () => {
24
+ const { moduleUrl } = fixture();
25
+ const plugin = createAppBrandingPlugin(
26
+ {
27
+ name: 'A & B',
28
+ logo: { light: './assets/logo.svg' },
29
+ loadingIcon: { light: './assets/logo.svg', dark: './assets/logo.svg', animation: 'none' },
30
+ font: { family: 'Brand', regular: './assets/font.woff2' },
31
+ favicon: './assets/logo.svg',
32
+ },
33
+ moduleUrl,
34
+ );
35
+ const addWatchFile = vi.fn();
36
+ const id = plugin.resolveId('virtual:vertesia-branding') ?? '';
37
+ const code = plugin.load.call({ addWatchFile }, id) ?? '';
38
+ const resolved = JSON.parse(code.slice('export default '.length, -1));
39
+ expect(resolved.logo.light).toMatch(/^data:image\/svg\+xml;base64,/);
40
+ expect(resolved.loadingIcon).toEqual({
41
+ light: resolved.logo.light,
42
+ dark: resolved.logo.light,
43
+ animation: 'none',
44
+ });
45
+ expect(resolved.font.regular).toBe('data:font/woff2;base64,Zm9udC1ieXRlcw==');
46
+ expect(addWatchFile).toHaveBeenCalledTimes(2);
47
+ const html = plugin.transformIndexHtml(
48
+ '<html><head><title>Old</title><link rel="icon" href="old.ico"></head><body></body></html>',
49
+ );
50
+ expect(html).toContain('<title>A &amp; B</title>');
51
+ expect(html).toContain(resolved.logo.light);
52
+ expect(html).not.toContain('old.ico');
53
+ expect(html).not.toContain('./assets/');
54
+ });
55
+
56
+ it('fails for missing assets and resolves custom boot assets relative to their own files', () => {
57
+ const { moduleUrl, dir } = fixture();
58
+ expect(() => resolveBrandingAssets({ name: 'Test', logo: { light: './missing.svg' } }, moduleUrl)).toThrow();
59
+ writeFileSync(
60
+ join(dir, 'assets/boot.html'),
61
+ '<img src="logo.svg"><a href="./help">Help</a><svg><use href="#icon"/></svg>',
62
+ );
63
+ writeFileSync(join(dir, 'assets/boot.css'), '@font-face{src:url(font.woff2)}');
64
+ const brand = resolveBrandingAssets(
65
+ { name: 'Test', boot: { html: './assets/boot.html', styles: './assets/boot.css' } },
66
+ moduleUrl,
67
+ );
68
+ expect(brand.boot?.html).toContain('data:image/svg+xml;base64,');
69
+ expect(brand.boot?.styles).toContain('data:font/woff2;base64,');
70
+ expect(brand.boot?.html).toContain('<a href="./help">Help</a>');
71
+ expect(brand.boot?.html).toContain('href="#icon"');
72
+ });
73
+
74
+ it('refreshes the virtual module when a watched logo changes', () => {
75
+ const { moduleUrl, dir } = fixture();
76
+ const plugin = createAppBrandingPlugin({ name: 'Test', logo: { light: './assets/logo.svg' } }, moduleUrl);
77
+ const id = plugin.resolveId('virtual:vertesia-branding') ?? '';
78
+ plugin.load.call({ addWatchFile: vi.fn() }, id);
79
+ const module = {};
80
+ const invalidateModule = vi.fn();
81
+ const send = vi.fn();
82
+ plugin.handleHotUpdate({
83
+ file: join(dir, 'assets/logo.svg'),
84
+ server: { moduleGraph: { getModuleById: () => module, invalidateModule }, ws: { send } },
85
+ });
86
+ expect(invalidateModule).toHaveBeenCalledWith(module);
87
+ expect(send).toHaveBeenCalledWith({ type: 'full-reload' });
88
+ writeFileSync(join(dir, 'assets/logo.svg'), '<svg>changed</svg>');
89
+ expect(plugin.load.call({ addWatchFile: vi.fn() }, id)).toContain(
90
+ Buffer.from('<svg>changed</svg>').toString('base64'),
91
+ );
92
+ });
93
+ });