@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
@@ -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
+ });