@rapidmx/web-client 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. package/README.md +183 -2
  2. package/apps/admin/_layout.tsx +8 -2
  3. package/apps/admin/index.tsx +7 -1
  4. package/apps/admin/ingest-queue/index.tsx +2 -1
  5. package/apps/admin/mailboxes/[uid].tsx +23 -10
  6. package/apps/admin/mailboxes/new/index.tsx +3 -3
  7. package/apps/admin/quarantine/index.tsx +2 -1
  8. package/apps/admin/signing-certificates/index.tsx +343 -0
  9. package/apps/escrow/_layout.tsx +8 -2
  10. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  11. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  12. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  13. package/apps/shared/appearance/ColorField.tsx +107 -0
  14. package/apps/shared/appearance/appearanceCache.ts +106 -0
  15. package/apps/shared/appearance/appearanceContext.ts +77 -0
  16. package/apps/shared/appearance/bootScript.ts +35 -0
  17. package/apps/shared/appearance/color.ts +169 -0
  18. package/apps/shared/appearance/photo.ts +88 -0
  19. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  20. package/apps/shared/appearance/theme.ts +355 -0
  21. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +4 -4
  22. package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
  23. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
  25. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +60 -14
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +65 -16
  27. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  28. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  29. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  30. package/apps/shared/components/contacts/ContactForm.tsx +11 -5
  31. package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
  32. package/apps/shared/components/contacts/ContactsToolbar.tsx +25 -52
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +10 -5
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +98 -32
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  38. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  39. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  40. package/apps/shared/components/layout/UserMenu.tsx +75 -6
  41. package/apps/shared/components/mail/ConversationList.tsx +4 -2
  42. package/apps/shared/components/mail/ConversationThreadPane.tsx +487 -475
  43. package/apps/shared/components/mail/LazyReadingPane.tsx +14 -16
  44. package/apps/shared/components/mail/MailSelectionBar.tsx +2 -10
  45. package/apps/shared/components/mail/MessageDetailPane.tsx +366 -287
  46. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  47. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  48. package/apps/shared/components/mail/compose/ComposeContext.tsx +35 -2
  49. package/apps/shared/components/mail/compose/ComposeWindow.tsx +239 -437
  50. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  51. package/apps/shared/components/mail/layout/MailShell.tsx +39 -25
  52. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  53. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  54. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  55. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  56. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  57. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  58. package/apps/shared/components/mail/reading/color.ts +245 -0
  59. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  60. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  61. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  62. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  63. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  64. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  65. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  66. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  67. package/apps/shared/components/sharing/PrincipalListField.tsx +82 -0
  68. package/apps/shared/components/sharing/PrincipalPicker.tsx +76 -0
  69. package/apps/shared/components/sharing/PrincipalResolver.tsx +164 -0
  70. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  71. package/apps/shared/components/tasks/layout/TasksShell.tsx +9 -5
  72. package/apps/shared/mail/folderCounts.ts +9 -3
  73. package/apps/shared/mail/folderTree.ts +143 -0
  74. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  75. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  76. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  77. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  78. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  79. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  80. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  81. package/apps/shared/mail/outbox/sendState.ts +35 -0
  82. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  83. package/apps/shared/mail/useMailConnection.ts +81 -28
  84. package/apps/shared/mail/useMailLiveUpdates.ts +55 -10
  85. package/apps/shared/mail/useNewMailNotifications.ts +47 -32
  86. package/apps/shared/navigation/AppRouter.tsx +2 -2
  87. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  88. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  89. package/apps/shared/notifications/apiErrors.ts +98 -0
  90. package/apps/shared/notifications/headerOffset.ts +35 -0
  91. package/apps/shared/notifications/pushStatus.ts +56 -0
  92. package/apps/shared/notifications/store.ts +549 -0
  93. package/apps/shared/notifications/systemErrors.ts +63 -0
  94. package/apps/shared/notifications/useNotifications.ts +41 -0
  95. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  96. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  97. package/apps/shared/signing/enrollmentView.ts +251 -0
  98. package/apps/shared/signing/signingInfo.ts +58 -0
  99. package/apps/shared/signing/useNow.ts +19 -0
  100. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  101. package/apps/shared/styles/app.css +80 -0
  102. package/apps/www/_layout.tsx +8 -2
  103. package/apps/www/_routes.ts +1 -0
  104. package/apps/www/calendar/index.tsx +9 -14
  105. package/apps/www/contacts/[uid].tsx +5 -8
  106. package/apps/www/contacts/index.tsx +587 -584
  107. package/apps/www/index.tsx +38 -27
  108. package/apps/www/messages/[uid].tsx +3 -1
  109. package/apps/www/settings/appearance/index.tsx +21 -0
  110. package/apps/www/settings/auto-reply/index.tsx +3 -7
  111. package/apps/www/settings/encryption/index.tsx +151 -116
  112. package/apps/www/settings/filters/[uid].tsx +3 -1
  113. package/apps/www/settings/filters/new/index.tsx +3 -1
  114. package/apps/www/settings/privacy/index.tsx +3 -8
  115. package/apps/www/settings/read-receipts/index.tsx +3 -7
  116. package/apps/www/settings/sharing/index.tsx +44 -65
  117. package/apps/www/settings/signatures/[uid].tsx +3 -1
  118. package/apps/www/settings/signatures/new/index.tsx +3 -2
  119. package/apps/www/tasks/index.tsx +11 -16
  120. package/dist/apps/admin/_layout.d.ts +5 -1
  121. package/dist/apps/admin/_layout.js +3 -2
  122. package/dist/apps/admin/index.js +3 -2
  123. package/dist/apps/admin/ingest-queue/index.js +2 -1
  124. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  125. package/dist/apps/admin/mailboxes/new/index.js +1 -1
  126. package/dist/apps/admin/quarantine/index.js +2 -1
  127. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  128. package/dist/apps/admin/signing-certificates/index.js +161 -0
  129. package/dist/apps/escrow/_layout.d.ts +5 -1
  130. package/dist/apps/escrow/_layout.js +3 -2
  131. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  132. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  133. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  134. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  135. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  136. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  137. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  138. package/dist/apps/shared/appearance/ColorField.js +40 -0
  139. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  140. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  141. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  142. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  143. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  144. package/dist/apps/shared/appearance/bootScript.js +33 -0
  145. package/dist/apps/shared/appearance/color.d.ts +65 -0
  146. package/dist/apps/shared/appearance/color.js +132 -0
  147. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  148. package/dist/apps/shared/appearance/photo.js +79 -0
  149. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  150. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  151. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  152. package/dist/apps/shared/appearance/theme.js +251 -0
  153. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +3 -2
  154. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  155. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  156. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  157. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  158. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  159. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  160. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +25 -5
  161. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +18 -7
  162. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  163. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  164. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  165. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  166. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  167. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  168. package/dist/apps/shared/components/contacts/ContactsToolbar.js +24 -9
  169. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -1
  170. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  171. package/dist/apps/shared/components/layout/AppShell.d.ts +6 -1
  172. package/dist/apps/shared/components/layout/AppShell.js +59 -13
  173. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  174. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  175. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  176. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  177. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  178. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  179. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  180. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  181. package/dist/apps/shared/components/layout/UserMenu.d.ts +7 -1
  182. package/dist/apps/shared/components/layout/UserMenu.js +41 -7
  183. package/dist/apps/shared/components/mail/ConversationList.js +3 -2
  184. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +2 -1
  185. package/dist/apps/shared/components/mail/ConversationThreadPane.js +42 -20
  186. package/dist/apps/shared/components/mail/LazyReadingPane.js +9 -8
  187. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +3 -4
  188. package/dist/apps/shared/components/mail/MailSelectionBar.js +2 -3
  189. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +18 -7
  190. package/dist/apps/shared/components/mail/MessageDetailPane.js +86 -41
  191. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  192. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  193. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  194. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  195. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +8 -0
  196. package/dist/apps/shared/components/mail/compose/ComposeContext.js +28 -3
  197. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +2 -1
  198. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +229 -328
  199. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  200. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  201. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +2 -0
  202. package/dist/apps/shared/components/mail/layout/MailShell.js +20 -14
  203. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  204. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  205. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  206. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  207. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  208. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  209. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  210. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  211. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  212. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  213. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  214. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  215. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  216. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  217. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  218. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  219. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  220. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  221. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  222. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  223. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  224. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  225. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  226. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  227. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  228. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  229. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  230. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  231. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  232. package/dist/apps/shared/components/sharing/PrincipalListField.d.ts +24 -0
  233. package/dist/apps/shared/components/sharing/PrincipalListField.js +23 -0
  234. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +28 -0
  235. package/dist/apps/shared/components/sharing/PrincipalPicker.js +24 -0
  236. package/dist/apps/shared/components/sharing/PrincipalResolver.d.ts +44 -0
  237. package/dist/apps/shared/components/sharing/PrincipalResolver.js +92 -0
  238. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  239. package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -1
  240. package/dist/apps/shared/mail/folderCounts.d.ts +1 -1
  241. package/dist/apps/shared/mail/folderCounts.js +9 -3
  242. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  243. package/dist/apps/shared/mail/folderTree.js +116 -0
  244. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  245. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  246. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  247. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  248. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  249. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  250. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  251. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  252. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  253. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  254. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  255. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  256. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  257. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  258. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  259. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  260. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  261. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  262. package/dist/apps/shared/mail/useMailConnection.d.ts +9 -8
  263. package/dist/apps/shared/mail/useMailConnection.js +64 -22
  264. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +17 -4
  265. package/dist/apps/shared/mail/useMailLiveUpdates.js +36 -10
  266. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +4 -10
  267. package/dist/apps/shared/mail/useNewMailNotifications.js +43 -23
  268. package/dist/apps/shared/navigation/AppRouter.js +2 -2
  269. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  270. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  271. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  272. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  273. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  274. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  275. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  276. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  277. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  278. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  279. package/dist/apps/shared/notifications/store.d.ts +136 -0
  280. package/dist/apps/shared/notifications/store.js +416 -0
  281. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  282. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  283. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  284. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  285. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  286. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  287. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  288. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  289. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  290. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  291. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  292. package/dist/apps/shared/signing/signingInfo.js +48 -0
  293. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  294. package/dist/apps/shared/signing/useNow.js +18 -0
  295. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  296. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  297. package/dist/apps/shared/styles/app.css +80 -0
  298. package/dist/apps/www/_layout.d.ts +5 -1
  299. package/dist/apps/www/_layout.js +3 -2
  300. package/dist/apps/www/_routes.js +1 -0
  301. package/dist/apps/www/calendar/index.js +3 -4
  302. package/dist/apps/www/contacts/[uid].js +5 -7
  303. package/dist/apps/www/contacts/index.js +14 -21
  304. package/dist/apps/www/index.js +37 -19
  305. package/dist/apps/www/messages/[uid].js +3 -1
  306. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  307. package/dist/apps/www/settings/appearance/index.js +13 -0
  308. package/dist/apps/www/settings/auto-reply/index.js +3 -6
  309. package/dist/apps/www/settings/encryption/index.js +103 -95
  310. package/dist/apps/www/settings/filters/[uid].js +3 -1
  311. package/dist/apps/www/settings/filters/new/index.js +3 -1
  312. package/dist/apps/www/settings/privacy/index.js +5 -8
  313. package/dist/apps/www/settings/read-receipts/index.js +3 -6
  314. package/dist/apps/www/settings/sharing/index.js +19 -33
  315. package/dist/apps/www/settings/signatures/[uid].js +3 -1
  316. package/dist/apps/www/settings/signatures/new/index.js +3 -2
  317. package/dist/apps/www/tasks/index.js +8 -14
  318. package/package.json +2 -2
  319. package/apps/shared/components/mail/NewMailToasts.tsx +0 -141
  320. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  321. package/dist/apps/shared/components/mail/NewMailToasts.d.ts +0 -19
  322. package/dist/apps/shared/components/mail/NewMailToasts.js +0 -55
  323. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  324. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  ///////////////////////////////////////////////////////////////////////////////
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
@@ -7,14 +7,11 @@ import { useRef } from "react";
7
7
  import { HiOutlineArrowDownTray, HiOutlineArrowUpTray, HiOutlineEnvelope, HiOutlinePencil, HiOutlineStar, HiOutlineTag, HiOutlineTrash, HiOutlineUserPlus, HiStar, } from "react-icons/hi2";
8
8
  import { SHORTCUTS } from "../../keyboard/keymap.js";
9
9
  import { useShortcutProps } from "../../keyboard/useShortcutProps.js";
10
- function ToolbarButton({ label, icon: Icon, disabled, onClick, hint, }) {
11
- return (_jsxs("button", { type: "button", onClick: onClick, disabled: disabled, ...hint, className: "flex flex-col items-center gap-1 text-xs text-text-muted hover:text-text disabled:opacity-40 disabled:cursor-not-allowed px-2.5 py-1.5 rounded-sm hover:not-disabled:bg-surface-alt", children: [_jsx(Icon, { size: 18, "aria-hidden": "true" }), _jsx("span", { children: label })] }));
12
- }
13
- function Divider() {
14
- return _jsx("div", { className: "w-px self-stretch my-1 bg-border", "aria-hidden": "true" });
15
- }
10
+ import ResponsiveToolbar from "../layout/ResponsiveToolbar.js";
16
11
  /**
17
- * Contacts' Outlook-style ribbon toolbar. Deliberately presentational — every action is a callback the
12
+ * Contacts' Outlook-style ribbon toolbar, fitted to its column (`ResponsiveToolbar`): captions under the icons while there is room, then icons
13
+ * alone, then the rarely used actions - Import, Export, Add category, and so on up - in a "More" menu; the bar can never spill into the pane
14
+ * beside it. Every button keeps its tooltip (New contact its shortcut's) and accessible name in every layout. Deliberately presentational — every action is a callback the
18
15
  * page (`apps/www/contacts/index.tsx`) implements, since it owns the actual contact data, the current
19
16
  * selection, and the mailbox/folder context every real action (create/delete/email/favorite/category)
20
17
  * needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
@@ -31,5 +28,23 @@ export default function ContactsToolbar({ selectedCount, allSelectedFavorited, o
31
28
  onImportFile(file);
32
29
  }
33
30
  }
34
- return (_jsxs("div", { role: "toolbar", "aria-label": "Contacts actions", className: "border-b border-border bg-surface-alt flex items-center gap-0.5 px-2 py-1", children: [_jsx(ToolbarButton, { label: "New contact", icon: HiOutlineUserPlus, onClick: onNewContact, hint: shortcuts ? newContactHint : undefined }), _jsx(Divider, {}), _jsx(ToolbarButton, { label: "Edit", icon: HiOutlinePencil, disabled: selectedCount !== 1, onClick: onEdit }), _jsx(ToolbarButton, { label: "Delete", icon: HiOutlineTrash, disabled: !hasSelection, onClick: onDelete }), _jsx(Divider, {}), _jsx(ToolbarButton, { label: "Email", icon: HiOutlineEnvelope, disabled: !hasSelection, onClick: onEmail }), _jsx(ToolbarButton, { label: allSelectedFavorited ? "Unfavorite" : "Favorite", icon: allSelectedFavorited ? HiStar : HiOutlineStar, disabled: !hasSelection, onClick: onToggleFavorite }), _jsx(ToolbarButton, { label: "Add category", icon: HiOutlineTag, disabled: !hasSelection, onClick: onAddCategory }), _jsx(Divider, {}), _jsx(ToolbarButton, { label: "Export", icon: HiOutlineArrowUpTray, disabled: !hasSelection, onClick: onExportVCard }), _jsx(ToolbarButton, { label: "Import", icon: HiOutlineArrowDownTray, onClick: () => importInputRef.current?.click() }), _jsx("input", { ref: importInputRef, type: "file", accept: ".vcf", className: "sr-only", "aria-label": "Import contacts file", onChange: handleImportFileChosen })] }));
31
+ const actions = [
32
+ { id: "new", label: "New contact", icon: HiOutlineUserPlus, onClick: onNewContact, hint: shortcuts ? newContactHint : undefined, group: 0, rank: 99, essential: true },
33
+ { id: "edit", label: "Edit", icon: HiOutlinePencil, onClick: onEdit, disabled: selectedCount !== 1, group: 1, rank: 6 },
34
+ { id: "delete", label: "Delete", icon: HiOutlineTrash, onClick: onDelete, disabled: !hasSelection, group: 1, rank: 5 },
35
+ { id: "email", label: "Email", icon: HiOutlineEnvelope, onClick: onEmail, disabled: !hasSelection, group: 2, rank: 4 },
36
+ {
37
+ id: "favorite",
38
+ label: allSelectedFavorited ? "Unfavorite" : "Favorite",
39
+ icon: allSelectedFavorited ? HiStar : HiOutlineStar,
40
+ onClick: onToggleFavorite,
41
+ disabled: !hasSelection,
42
+ group: 2,
43
+ rank: 3,
44
+ },
45
+ { id: "category", label: "Add category", icon: HiOutlineTag, onClick: onAddCategory, disabled: !hasSelection, group: 2, rank: 2 },
46
+ { id: "export", label: "Export", icon: HiOutlineArrowUpTray, onClick: onExportVCard, disabled: !hasSelection, group: 3, rank: 1 },
47
+ { id: "import", label: "Import", icon: HiOutlineArrowDownTray, onClick: () => importInputRef.current?.click(), group: 3, rank: 0 },
48
+ ];
49
+ return (_jsx(ResponsiveToolbar, { label: "Contacts actions", actions: actions, children: _jsx("input", { ref: importInputRef, type: "file", accept: ".vcf", className: "sr-only", "aria-label": "Import contacts file", onChange: handleImportFileChosen }) }));
35
50
  }
@@ -97,7 +97,10 @@ export default function ContactsShell({ userUid, authServerUrl, impersonating, i
97
97
  const sidebarContent = (idPrefix) => (_jsxs(_Fragment, { children: [mailboxes.length > 1 && (_jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold uppercase tracking-wide text-text-muted mb-1", htmlFor: `${idPrefix}-contacts-mailbox-switcher`, children: "Mailbox" }), _jsx("select", { id: `${idPrefix}-contacts-mailbox-switcher`, className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: mailboxUid, onChange: (e) => {
98
98
  navigate(`/contacts?mailboxUid=${encodeURIComponent(e.target.value)}`);
99
99
  }, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), folderError && _jsx(Alert, { children: folderError })] }));
100
- inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [(mailboxes.length > 1 || folderError) && (_jsx("button", { type: "button", className: "md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(ContactsShellContext.Provider, { value: contextValue, children: children })] })] }));
100
+ // The mailbox switcher (or an error) is all this column holds: with a single mailbox and no error there is nothing to put in it, and an empty
101
+ // 224 px column beside the contacts' own menu is wasted room.
102
+ const hasSidebar = mailboxes.length > 1 || !!folderError;
103
+ inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") })), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(ContactsShellContext.Provider, { value: contextValue, children: children })] })] }));
101
104
  }
102
105
  return (_jsx(AppShell, { active: "contacts", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
103
106
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  ///////////////////////////////////////////////////////////////////////////////
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
@@ -13,6 +13,8 @@ import { getBranding } from "@rapidmx/react-shared/branding/brandingApi.js";
13
13
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
14
14
  import BottomTabBar from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
15
15
  import UserMenu from "../../layout/UserMenu.js";
16
+ import RailIcon from "../../layout/RailIcon.js";
17
+ import AppearanceProvider from "../../../appearance/AppearanceProvider.js";
16
18
  import { signOutOfConsole } from "../../admin/signOut.js";
17
19
  const DEFAULT_ICON_SRC = "/images/logo.svg";
18
20
  const NAV_ITEMS = [
@@ -83,12 +85,13 @@ export default function EscrowShell({ active, userUid, authServerUrl, children }
83
85
  }
84
86
  else {
85
87
  const activeItem = NAV_ITEMS.find((item) => item.id === active);
86
- content = (_jsx("div", { className: "min-h-screen flex flex-col bg-surface-alt", children: _jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsxs("nav", { "aria-label": "Escrow sections", className: "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1", children: [_jsx("img", { src: iconSrc, width: "96", height: "96", alt: "", className: "mb-3" }), NAV_ITEMS.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
88
+ content = (_jsx("div", { className: "rr-frame-bg min-h-screen flex flex-col", children: _jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsxs("nav", { "aria-label": "Escrow sections", className: "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center pb-3 gap-1", children: [_jsx(RailIcon, { src: iconSrc }), NAV_ITEMS.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
87
89
  "w-10 h-10 flex items-center justify-center rounded-sm",
88
90
  id === active
89
91
  ? "bg-primary/10 text-primary-dark"
90
92
  : "text-text-muted hover:bg-surface-alt hover:text-text",
91
- ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: NAV_ITEMS, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeItem?.label }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut })] }), _jsx("div", { className: "flex-1 pb-14 md:pb-0", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
93
+ ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: NAV_ITEMS, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeItem?.label }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut })] }), _jsx("div", { id: "app-content", className: "flex-1 pb-14 md:pb-0", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
92
94
  }
93
- return _jsx(_Fragment, { children: content });
95
+ // The user's own colours and background (`AppearanceProvider`), from what the last webmail page cached in this browser: the console never asks the server.
96
+ return (_jsx(AppearanceProvider, { userUid: userUid, lookUp: false, children: content }));
94
97
  }
@@ -2,6 +2,7 @@ import "../../styles/app.css";
2
2
  import React, { PropsWithChildren } from "react";
3
3
  import type { IconType } from "react-icons";
4
4
  import { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
5
+ import type { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
5
6
  import { PluginNav, PluginNavProps } from "../../plugins/pluginNav.js";
6
7
  /** How long sign-out waits for auth-server's logout before navigating anyway. */
7
8
  export declare const LOGOUT_TIMEOUT_MS = 3000;
@@ -34,6 +35,10 @@ export interface AppShellProps extends PluginNavProps {
34
35
  /** The role names the server treats as trusted (its `trusted_roles` config, injected via the route's `fetchProps`) -
35
36
  * what that lookup looks for. Absent means the server's own default, `["admin"]`. */
36
37
  trustedRoles?: string[];
38
+ /** The deployment's branding as the server rendered the page (its `branding` prop). Used until the frame's own fetch answers, so a custom header is there from the first paint - and the frame knows to drop its own title bar - instead of appearing (and moving everything) once the request returns. */
39
+ branding?: Branding;
40
+ /** The signed-in user's stored appearance preferences as the server rendered the page (its `appearance` prop), so the first paint already wears the theme and background - see `AppearanceProvider`. */
41
+ appearance?: unknown;
37
42
  }
38
43
  export interface AppDef {
39
44
  id: AppShellApp;
@@ -64,7 +69,7 @@ export interface AppChromeProps extends AppShellProps {
64
69
  * Rendered by `AppRouter`, once, for the life of the page, so that moving between apps replaces only `children` - see
65
70
  * `AppShell` below for what a page's own shell renders instead.
66
71
  */
67
- export declare function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, routeKey, busy, hideChrome, children, }: PropsWithChildren<AppChromeProps>): React.JSX.Element;
72
+ export declare function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, routeKey, busy, hideChrome, children, }: PropsWithChildren<AppChromeProps>): React.JSX.Element;
68
73
  /**
69
74
  * What a page's own shell (`MailShell`, `CalendarShell`, ...) renders around its content. Outside the client-side router it
70
75
  * is the whole `AppChrome`, as it always was. Inside it (`AppRouter`) the chrome is already mounted above the page - and stays
@@ -14,11 +14,14 @@ import { useIdleKeyTimeout } from "@rapidmx/react-shared/crypto/useIdleKeyTimeou
14
14
  import ComposeProvider from "../mail/compose/ComposeContext.js";
15
15
  import { flushComposeDrafts, markSigningOut } from "../mail/compose/composeFlushRegistry.js";
16
16
  import BottomTabBar from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
17
- import { BrandingFooter, BrandingHeader } from "./BrandingChrome.js";
17
+ import { FrameBrandingFooter, FrameBrandingHeader, useBrandingHtml } from "./BrandingChrome.js";
18
+ import RailIcon from "./RailIcon.js";
19
+ import AppearanceProvider from "../../appearance/AppearanceProvider.js";
20
+ import { clearAppearanceCache } from "../../appearance/appearanceCache.js";
18
21
  import UserMenu from "./UserMenu.js";
19
22
  import { UnlockPromptProvider } from "./UnlockPromptProvider.js";
20
23
  import { SIGN_OUT_CHANNEL, destroyAllLocalIndexes } from "../../search/localIndexRpcClient.js";
21
- import { authApiFetch } from "@rapidmx/react-shared/util/api.js";
24
+ import { authApiFetch, setApiUnauthorizedObserver } from "@rapidmx/react-shared/util/api.js";
22
25
  import { destroyUnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
23
26
  import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
24
27
  import { mergePluginNavItems } from "../../plugins/pluginNav.js";
@@ -31,7 +34,13 @@ import ShortcutsDialog from "../../keyboard/ShortcutsDialog.js";
31
34
  import { inboxUnreadTotal } from "../../mail/folderCounts.js";
32
35
  import { MailConnectionContext, useMailConnection } from "../../mail/useMailConnection.js";
33
36
  import { useUnreadTitle } from "../../mail/useUnreadTitle.js";
34
- import NewMailToasts from "../mail/NewMailToasts.js";
37
+ import UnlockBridge from "../../mail/outbox/UnlockBridge.js";
38
+ import NotificationCenter from "../../notifications/NotificationCenter.js";
39
+ import { useHeaderHeightRef } from "../../notifications/headerOffset.js";
40
+ import NotificationHistoryDialog from "../../notifications/NotificationHistoryDialog.js";
41
+ import { notifySessionExpired, setSignInUrl } from "../../notifications/apiErrors.js";
42
+ import { useUnseenErrorCount } from "../../notifications/useNotifications.js";
43
+ import { useSigningEnrollmentWatcher } from "../../signing/useSigningEnrollmentWatcher.js";
35
44
  /** How long sign-out waits for auth-server's logout before navigating anyway. */
36
45
  export const LOGOUT_TIMEOUT_MS = 3000;
37
46
  /**
@@ -77,12 +86,24 @@ export function appRailItems(pluginNav) {
77
86
  * Rendered by `AppRouter`, once, for the life of the page, so that moving between apps replaces only `children` - see
78
87
  * `AppShell` below for what a page's own shell renders instead.
79
88
  */
80
- export function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, routeKey, busy, hideChrome, children, }) {
89
+ export function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, routeKey, busy, hideChrome, children, }) {
81
90
  const [stoppingImpersonation, setStoppingImpersonation] = useState(false);
82
91
  // The keyboard shortcuts dialog: opened by `?`/Ctrl+/ (`GlobalShortcuts`) and by the account menu's item.
83
92
  const [shortcutsOpen, setShortcutsOpen] = useState(false);
93
+ // The pop-up history ("Recent notifications" in the account menu). The pop-ups themselves are drawn by `NotificationCenter`, below.
94
+ const [historyOpen, setHistoryOpen] = useState(false);
95
+ // Only the count is read, so the whole frame does not render again for every pop-up that comes and goes.
96
+ const unseenErrors = useUnseenErrorCount();
97
+ // The title bar's height for the pop-up stack, which sticks just below the header (a branding header publishes its own - `FrameBrandingHeader`).
98
+ const headerRef = useHeaderHeightRef();
84
99
  const signingOutRef = useRef(false);
85
- const { branding, iconSrc } = useBranding();
100
+ const { branding: fetchedBranding, iconSrc: fetchedIconSrc } = useBranding();
101
+ // The server's copy until the fetch answers, so the frame's shape (a custom header replaces the title bar) never changes after the first paint.
102
+ const branding = fetchedBranding ?? initialBranding ?? null;
103
+ const iconSrc = fetchedBranding ? fetchedIconSrc : initialBranding?.iconUrl || initialBranding?.logoUrl || fetchedIconSrc;
104
+ // The custom header and footer, sanitized and with their `{USER_MENU}` / `{APP_TITLE}` placeholders (`undefined` until parsed, `null` when there is none).
105
+ const header = useBrandingHtml(branding?.headerHtml);
106
+ const footer = useBrandingHtml(branding?.footerHtml);
86
107
  // The mailboxes, their folders and the one push connection live here, in the frame that stays mounted as the router swaps pages - so
87
108
  // new-mail pop-ups, the folder counters and the tab title's unread count work in Calendar, Contacts, Tasks and Settings too, and moving
88
109
  // between apps never opens a second socket (the server allows ten per user). `MailShell` reads them from `MailConnectionContext`. Outside
@@ -90,7 +111,19 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
90
111
  const inFrame = useInAppFrame();
91
112
  const navigate = useNavigate();
92
113
  const mail = useMailConnection({ userUid, enabled: inFrame, open: navigate });
114
+ // A signing certificate the user asked for is announced when it is issued (or fails), on whichever page they are - see the hook.
115
+ useSigningEnrollmentWatcher({ userUid, mailboxes: mail.mailboxes, enabled: inFrame });
93
116
  useRedirectIfUnauthenticated(userUid, authServerUrl);
117
+ // Any request this app makes that the server answers with a 401 - the session ended - raises one "Your session expired" pop-up with a
118
+ // Sign in action (see `notifySessionExpired()`), whichever request noticed first, a background refresh included.
119
+ useEffect(() => {
120
+ setSignInUrl(authServerUrl);
121
+ if (!userUid) {
122
+ return;
123
+ }
124
+ setApiUnauthorizedObserver(() => void notifySessionExpired());
125
+ return () => setApiUnauthorizedObserver(undefined);
126
+ }, [userUid, authServerUrl]);
94
127
  // Mounted here, not scoped to Mail/Settings (the only shells that actually read unlocked keys),
95
128
  // specifically so activity in *any* app resets the idle clock - see that hook's own doc comment.
96
129
  useIdleKeyTimeout();
@@ -130,6 +163,7 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
130
163
  markSigningOut();
131
164
  destroyUnlockedKeys();
132
165
  clearPinnedSignerCache();
166
+ clearAppearanceCache();
133
167
  // Bounded by its own timeout and never rejects - awaited so navigating doesn't kill the Worker mid-delete.
134
168
  void destroyAllLocalIndexes().then(() => {
135
169
  window.location.href = authServerUrl ?? "/";
@@ -146,6 +180,7 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
146
180
  destroyUnlockedKeys();
147
181
  // Trusted signer pins read from contacts don't outlive the session either.
148
182
  clearPinnedSignerCache();
183
+ clearAppearanceCache();
149
184
  // Open compose windows save edits still waiting on their autosave debounce while the session is still
150
185
  // valid - logout invalidates it. Bounded the same way as logout itself, and never rejects.
151
186
  await flushComposeDrafts(LOGOUT_TIMEOUT_MS);
@@ -204,14 +239,25 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
204
239
  const apps = appRailItems(pluginNav);
205
240
  // The header title: "settings" has no rail item, everything else is labelled by its own rail item.
206
241
  const activeLabel = active === "settings" ? "Settings" : apps.find((app) => app.id === active)?.label;
207
- return (_jsx(ShortcutProvider, { children: _jsxs(MailConnectionContext.Provider, { value: inFrame ? mail : null, children: [_jsx(GlobalShortcuts, { authServerUrl: authServerUrl, onToggleHelp: () => setShortcutsOpen((open) => !open) }), _jsx(ShortcutsDialog, { open: shortcutsOpen, onClose: () => setShortcutsOpen(false) }), _jsx(UnlockPromptProvider, { children: _jsx(ComposeProvider, { userUid: userUid, trusted: !!trusted && !impersonating, children: _jsxs("div", { className: "min-h-screen flex flex-col bg-surface-alt", children: [!hideChrome && _jsx(BrandingHeader, { branding: branding }), impersonating && !hideChrome && (_jsxs("div", { className: "h-10 shrink-0 bg-warning text-warning-contrast flex items-center justify-center gap-3 text-sm font-medium px-4", children: [_jsxs("span", { children: ["You are viewing as ", _jsx("strong", { children: userUid }), "."] }), _jsx("button", { type: "button", onClick: handleStopImpersonating, disabled: stoppingImpersonation, className: "underline hover:no-underline disabled:opacity-60", children: stoppingImpersonation ? "Returning to admin…" : "Return to admin" })] })), _jsxs("div", { className: "flex-1 flex min-h-0", children: [!hideChrome && (_jsxs("nav", { "aria-label": "Apps", className: "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1", children: [_jsx("img", { src: iconSrc, width: "96", height: "96", alt: "", className: "mb-3" }), apps.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
208
- "w-10 h-10 flex items-center justify-center rounded-sm",
209
- id === active
210
- ? "bg-primary/10 text-primary-dark"
211
- : "text-text-muted hover:bg-surface-alt hover:text-text",
212
- ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] })), !hideChrome && _jsx(BottomTabBar, { apps: apps, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [!hideChrome && (_jsxs("header", { className: "h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeLabel }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut,
213
- // An impersonating administrator acts as the impersonated user, so the console link is hidden then.
214
- showAdminLink: !!trusted && !impersonating, detectAdmin: !trusted && !impersonating, trustedRoles: trustedRoles, showSettingsLink: true, showNotificationSettings: true, onShowShortcuts: () => setShortcutsOpen(true) })] })), _jsx("div", { ref: contentRef, id: "app-content", tabIndex: -1, "aria-busy": busy || undefined, className: ["flex-1 flex min-h-0 outline-none", hideChrome ? "" : "pb-14 md:pb-0"].join(" "), children: children })] })] }), routeKey !== undefined && (_jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement })), !hideChrome && _jsx(BrandingFooter, { branding: branding })] }) }) }), inFrame && (_jsx(NewMailToasts, { toasts: mail.notifications.toasts, onDismiss: mail.notifications.dismiss, offerDesktop: mail.notifications.offerDesktop, onEnableDesktop: () => void mail.notifications.enableDesktop(), onDeclineDesktop: mail.notifications.declineDesktop }))] }) }));
242
+ // A custom header (`Branding.headerHtml`) replaces the app's own title bar and the icon at the top of the rail: it is the top of the app, and the
243
+ // account menu moves into it. While it is still being parsed it already counts, so the frame's shape doesn't change under the user.
244
+ const customHeader = header !== null;
245
+ // Where the one account menu lives: the header's `{USER_MENU}`, else the footer's, else a small cell at the header's right end - never lost.
246
+ const menuInHeader = !!header?.hasUserMenu;
247
+ const menuInFooter = customHeader && !!header && !menuInHeader && !!footer?.hasUserMenu;
248
+ const renderUserMenu = (placement) => (_jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut,
249
+ // An impersonating administrator acts as the impersonated user, so the console link is hidden then.
250
+ showAdminLink: !!trusted && !impersonating, detectAdmin: !trusted && !impersonating, trustedRoles: trustedRoles, showSettingsLink: true, showNotificationSettings: true, onShowShortcuts: () => setShortcutsOpen(true), onShowNotifications: () => setHistoryOpen(true), unseenErrors: unseenErrors, placement: placement }));
251
+ return (_jsx(AppearanceProvider, { userUid: userUid, initial: appearance, lookUp: inFrame, children: _jsx(ShortcutProvider, { children: _jsxs(MailConnectionContext.Provider, { value: inFrame ? mail : null, children: [_jsx(GlobalShortcuts, { authServerUrl: authServerUrl, onToggleHelp: () => setShortcutsOpen((open) => !open) }), _jsx(ShortcutsDialog, { open: shortcutsOpen, onClose: () => setShortcutsOpen(false) }), _jsxs(UnlockPromptProvider, { children: [_jsx(UnlockBridge, {}), _jsx(ComposeProvider, { userUid: userUid, trusted: !!trusted && !impersonating, children: _jsxs("div", { className: "rr-frame-bg min-h-screen flex flex-col", children: [!hideChrome && customHeader && (_jsx(FrameBrandingHeader, { parsed: header, userMenu: menuInHeader ? renderUserMenu("down") : undefined, fallbackMenu: header && !menuInHeader && !menuInFooter ? renderUserMenu("down") : undefined, appTitle: activeLabel })), impersonating && !hideChrome && (_jsxs("div", { className: "h-10 shrink-0 bg-warning text-warning-contrast flex items-center justify-center gap-3 text-sm font-medium px-4", children: [_jsxs("span", { children: ["You are viewing as ", _jsx("strong", { children: userUid }), "."] }), _jsx("button", { type: "button", onClick: handleStopImpersonating, disabled: stoppingImpersonation, className: "underline hover:no-underline disabled:opacity-60", children: stoppingImpersonation ? "Returning to admin…" : "Return to admin" })] })), _jsxs("div", { className: "flex-1 flex min-h-0", children: [!hideChrome && (_jsxs("nav", { "aria-label": "Apps", className: [
252
+ "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center gap-1",
253
+ // Under a custom header the rail starts with the app icons; without one its own icon is flush with the top of the window.
254
+ customHeader ? "py-3" : "pb-3",
255
+ ].join(" "), children: [!customHeader && (_jsx(RailIcon, { src: iconSrc })), apps.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
256
+ "w-10 h-10 flex items-center justify-center rounded-sm",
257
+ id === active
258
+ ? "bg-primary/10 text-primary-dark"
259
+ : "text-text-muted hover:bg-surface-alt hover:text-text",
260
+ ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] })), !hideChrome && _jsx(BottomTabBar, { apps: apps, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [!hideChrome && !customHeader && (_jsxs("header", { ref: headerRef, className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeLabel }), renderUserMenu("down")] })), _jsx(NotificationCenter, {}), _jsx("div", { ref: contentRef, id: "app-content", tabIndex: -1, "aria-busy": busy || undefined, className: ["flex-1 flex min-h-0 outline-none", hideChrome ? "" : "pb-14 md:pb-0"].join(" "), children: children })] })] }), routeKey !== undefined && (_jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement })), !hideChrome && _jsx(FrameBrandingFooter, { parsed: footer, userMenu: menuInFooter ? renderUserMenu("up") : undefined, appTitle: activeLabel })] }) })] }), _jsx(NotificationHistoryDialog, { open: historyOpen, onClose: () => setHistoryOpen(false) })] }) }) }));
215
261
  }
216
262
  /**
217
263
  * What a page's own shell (`MailShell`, `CalendarShell`, ...) renders around its content. Outside the client-side router it
@@ -1,19 +1,78 @@
1
- import React from "react";
2
- import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
1
+ import React, { ReactNode } from "react";
2
+ import type { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
3
+ /** The attribute that marks the elements React fills in (`{USER_MENU}`, `{APP_TITLE}`). Stripped from the author's own HTML, so only
4
+ * this module's placeholders can carry it. */
5
+ export declare const SLOT_ATTRIBUTE = "data-rr-slot";
6
+ /** The variables a branding header or footer may contain, written in the HTML's text. */
7
+ export declare const USER_MENU_VARIABLE = "{USER_MENU}";
8
+ export declare const APP_TITLE_VARIABLE = "{APP_TITLE}";
3
9
  /**
4
10
  * Sanitizes admin-configured branding HTML before it's rendered. The server sanitizes on save too; this also
5
11
  * covers anything stored before that. Returns `""` where there's no DOM to sanitize with (server-side
6
12
  * rendering), so unsanitized HTML is never emitted.
7
13
  */
8
14
  export declare function sanitizeBrandingHtml(html: string): string;
15
+ /** Branding HTML ready to render: sanitized, with its variables replaced by placeholder elements for React to fill. */
16
+ export interface ParsedBrandingHtml {
17
+ html: string;
18
+ /** It contains a `{USER_MENU}` where the account menu can be rendered. */
19
+ hasUserMenu: boolean;
20
+ }
9
21
  /**
10
- * Renders the admin-configured `Branding.headerHtml`/`footerHtml` (see `useBranding()`), sanitized. The webmail
11
- * chrome (`AppShell`) shows both; the admin console shows only the footer - its header is the console's own - and the
12
- * escrow console deliberately renders no branding HTML at all.
22
+ * Sanitizes `html` (the same rules as `sanitizeBrandingHtml()`) and swaps the variables in its **text** for placeholder elements:
23
+ * `{USER_MENU}` becomes the slot the account menu is rendered into, `{APP_TITLE}` a slot holding the current app's name.
24
+ *
25
+ * - Only text is searched, after sanitizing - a variable in an attribute (`title="{USER_MENU}"`), a comment, or a `<script>` (which
26
+ * is gone) is never touched, and the author cannot write a placeholder themselves (`data-rr-slot` is stripped).
27
+ * - `{USER_MENU}` is replaced **once**, at its first place in the document; a second (or third) one is removed, so there is never
28
+ * more than one account menu (two would give two sets of duplicate ids and focus targets). One inside a link, or in text that
29
+ * can't hold an element (`<title>`, `<textarea>`), is removed too: a menu inside an `<a>` would navigate when clicked.
30
+ * - `{APP_TITLE}` is replaced everywhere it appears. Inside `<title>`-like raw text it stays as written.
31
+ * - Nothing is matched loosely: the names are upper-case, in braces, with no spaces.
32
+ *
33
+ * `null` when there is nothing to show (no DOM to sanitize with - server-side rendering - or the HTML is empty once sanitized).
13
34
  */
14
- export declare function BrandingHeader({ branding }: {
15
- branding: Branding | null;
16
- }): React.JSX.Element;
17
- export declare function BrandingFooter({ branding }: {
35
+ export declare function parseBrandingHtml(html: string): ParsedBrandingHtml | null;
36
+ /**
37
+ * `parseBrandingHtml()` of `html`, done once the component is on the page (never while rendering: the server has no DOM to sanitize
38
+ * with, and rendering different HTML there than the hydrating client would leave the page without it). `null` when there is no HTML,
39
+ * or nothing is left of it; `undefined` while it has not been parsed yet - the frame reserves the header's place meanwhile.
40
+ */
41
+ export declare function useBrandingHtml(html: string | undefined): ParsedBrandingHtml | null | undefined;
42
+ export interface FrameBrandingHeaderProps {
43
+ /** `useBrandingHtml(branding.headerHtml)`. `null` renders nothing. */
44
+ parsed: ParsedBrandingHtml | null | undefined;
45
+ /** The account menu, rendered into the header's `{USER_MENU}` (when it has one). */
46
+ userMenu?: ReactNode;
47
+ /** Rendered at the right end of the header when it has no `{USER_MENU}` and the footer doesn't host the menu either - the account menu is never lost. */
48
+ fallbackMenu?: ReactNode;
49
+ appTitle?: string;
50
+ }
51
+ /**
52
+ * The frame's version of the admin-configured `Branding.headerHtml` (see `useBranding()`), sanitized, in a `<header>` that stays at the top of the window as the
53
+ * page scrolls. When it is there the app has no title bar of its own (`AppChrome`): the header is the top of the app, and the account
54
+ * menu lives in it - where the HTML writes `{USER_MENU}`, else in a small cell at its right end. The webmail chrome (`AppShell`) shows
55
+ * it; the admin console has no custom header (its header is the console's own) and the escrow console renders no branding HTML at all.
56
+ */
57
+ export declare function FrameBrandingHeader({ parsed, userMenu, fallbackMenu, appTitle }: FrameBrandingHeaderProps): React.JSX.Element | null;
58
+ export interface FrameBrandingFooterProps {
59
+ /** `useBrandingHtml(branding.footerHtml)`. */
60
+ parsed: ParsedBrandingHtml | null | undefined;
61
+ /** Rendered into the footer's `{USER_MENU}` - passed only when the header has none. */
62
+ userMenu?: ReactNode;
63
+ appTitle?: string;
64
+ }
65
+ /** The frame's version of the admin-configured `Branding.footerHtml`, sanitized. Its variables work as in the header; a `{USER_MENU}` here only takes the menu when the header has none. */
66
+ export declare function FrameBrandingFooter({ parsed, userMenu, appTitle }: FrameBrandingFooterProps): React.JSX.Element;
67
+ export interface BrandingChromeProps {
18
68
  branding: Branding | null;
19
- }): React.JSX.Element;
69
+ /** What `{APP_TITLE}` is replaced with - the page's own name. Nothing when absent. */
70
+ appTitle?: string;
71
+ }
72
+ /**
73
+ * The admin-configured `Branding.headerHtml`, sanitized, for pages that don't use a shell, such as public pages (the booking pages). There is no
74
+ * account menu there, so a `{USER_MENU}` in the HTML renders nothing. Renders nothing without branding or HTML.
75
+ */
76
+ export declare function BrandingHeader({ branding, appTitle }: BrandingChromeProps): React.JSX.Element;
77
+ /** The admin-configured `Branding.footerHtml`, sanitized, for pages that don't use a shell - see `BrandingHeader`. */
78
+ export declare function BrandingFooter({ branding, appTitle }: BrandingChromeProps): React.JSX.Element;
@@ -1,5 +1,12 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
7
+ import { createPortal } from "react-dom";
2
8
  import DOMPurify from "dompurify";
9
+ import { useHeaderHeightRef } from "../../notifications/headerOffset.js";
3
10
  /** Elements that could run script, restyle the whole page, collect input, or embed another page - stripped on
4
11
  * top of DOMPurify's defaults (which already drop `<script>`, `on*` handlers, and `javascript:` URLs). */
5
12
  const FORBIDDEN_TAGS = [
@@ -23,6 +30,15 @@ const FORBIDDEN_TAGS = [
23
30
  "math",
24
31
  ];
25
32
  const FORBIDDEN_ATTRS = ["action", "formaction", "srcdoc"];
33
+ /** The attribute that marks the elements React fills in (`{USER_MENU}`, `{APP_TITLE}`). Stripped from the author's own HTML, so only
34
+ * this module's placeholders can carry it. */
35
+ export const SLOT_ATTRIBUTE = "data-rr-slot";
36
+ /** The variables a branding header or footer may contain, written in the HTML's text. */
37
+ export const USER_MENU_VARIABLE = "{USER_MENU}";
38
+ export const APP_TITLE_VARIABLE = "{APP_TITLE}";
39
+ const VARIABLE = /\{(USER_MENU|APP_TITLE)\}/g;
40
+ /** Elements whose text is not markup a placeholder element could live in (raw text and RCDATA), and the ones a menu must not sit inside. */
41
+ const RAW_TEXT_PARENTS = new Set(["TITLE", "TEXTAREA", "OPTION", "STYLE", "SCRIPT", "NOSCRIPT", "XMP", "IFRAME", "NOEMBED", "NOFRAMES", "PLAINTEXT"]);
26
42
  /**
27
43
  * Sanitizes admin-configured branding HTML before it's rendered. The server sanitizes on save too; this also
28
44
  * covers anything stored before that. Returns `""` where there's no DOM to sanitize with (server-side
@@ -34,21 +50,133 @@ export function sanitizeBrandingHtml(html) {
34
50
  }
35
51
  return DOMPurify.sanitize(html, { FORBID_TAGS: FORBIDDEN_TAGS, FORBID_ATTR: FORBIDDEN_ATTRS });
36
52
  }
37
- function BrandingHtml({ html }) {
38
- const clean = html ? sanitizeBrandingHtml(html) : "";
39
- if (!clean) {
53
+ function slotElement(name) {
54
+ const span = document.createElement("span");
55
+ span.setAttribute(SLOT_ATTRIBUTE, name);
56
+ return span;
57
+ }
58
+ /**
59
+ * Sanitizes `html` (the same rules as `sanitizeBrandingHtml()`) and swaps the variables in its **text** for placeholder elements:
60
+ * `{USER_MENU}` becomes the slot the account menu is rendered into, `{APP_TITLE}` a slot holding the current app's name.
61
+ *
62
+ * - Only text is searched, after sanitizing - a variable in an attribute (`title="{USER_MENU}"`), a comment, or a `<script>` (which
63
+ * is gone) is never touched, and the author cannot write a placeholder themselves (`data-rr-slot` is stripped).
64
+ * - `{USER_MENU}` is replaced **once**, at its first place in the document; a second (or third) one is removed, so there is never
65
+ * more than one account menu (two would give two sets of duplicate ids and focus targets). One inside a link, or in text that
66
+ * can't hold an element (`<title>`, `<textarea>`), is removed too: a menu inside an `<a>` would navigate when clicked.
67
+ * - `{APP_TITLE}` is replaced everywhere it appears. Inside `<title>`-like raw text it stays as written.
68
+ * - Nothing is matched loosely: the names are upper-case, in braces, with no spaces.
69
+ *
70
+ * `null` when there is nothing to show (no DOM to sanitize with - server-side rendering - or the HTML is empty once sanitized).
71
+ */
72
+ export function parseBrandingHtml(html) {
73
+ if (!DOMPurify.isSupported) {
74
+ return null;
75
+ }
76
+ const fragment = DOMPurify.sanitize(html, {
77
+ FORBID_TAGS: FORBIDDEN_TAGS,
78
+ FORBID_ATTR: [...FORBIDDEN_ATTRS, SLOT_ATTRIBUTE],
79
+ RETURN_DOM_FRAGMENT: true,
80
+ });
81
+ const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_TEXT);
82
+ const texts = [];
83
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
84
+ texts.push(node);
85
+ }
86
+ let hasUserMenu = false;
87
+ for (const text of texts) {
88
+ const parent = text.parentElement;
89
+ if (parent && RAW_TEXT_PARENTS.has(parent.tagName)) {
90
+ continue;
91
+ }
92
+ const inLink = !!parent?.closest("a");
93
+ // `split` with a capturing group alternates text and variable names: [text, name, text, name, ..., text].
94
+ const pieces = text.data.split(VARIABLE);
95
+ if (pieces.length === 1) {
96
+ continue;
97
+ }
98
+ const replacement = document.createDocumentFragment();
99
+ pieces.forEach((piece, index) => {
100
+ if (index % 2 === 0) {
101
+ if (piece) {
102
+ replacement.appendChild(document.createTextNode(piece));
103
+ }
104
+ }
105
+ else if (piece === "APP_TITLE") {
106
+ replacement.appendChild(slotElement("app-title"));
107
+ }
108
+ else if (!hasUserMenu && !inLink) {
109
+ hasUserMenu = true;
110
+ replacement.appendChild(slotElement("user-menu"));
111
+ }
112
+ });
113
+ text.replaceWith(replacement);
114
+ }
115
+ const holder = document.createElement("div");
116
+ holder.appendChild(fragment);
117
+ const result = holder.innerHTML;
118
+ return result.trim() ? { html: result, hasUserMenu } : null;
119
+ }
120
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
121
+ /**
122
+ * `parseBrandingHtml()` of `html`, done once the component is on the page (never while rendering: the server has no DOM to sanitize
123
+ * with, and rendering different HTML there than the hydrating client would leave the page without it). `null` when there is no HTML,
124
+ * or nothing is left of it; `undefined` while it has not been parsed yet - the frame reserves the header's place meanwhile.
125
+ */
126
+ export function useBrandingHtml(html) {
127
+ const source = html?.trim() ? html : undefined;
128
+ const [parsed, setParsed] = useState(undefined);
129
+ useIsomorphicLayoutEffect(() => {
130
+ setParsed(source === undefined ? undefined : { source, value: parseBrandingHtml(source) });
131
+ }, [source]);
132
+ if (source === undefined) {
133
+ return null;
134
+ }
135
+ return parsed?.source === source ? parsed.value : undefined;
136
+ }
137
+ function BrandingHtml({ parsed, userMenu, hostMenu, appTitle, className }) {
138
+ const ref = useRef(null);
139
+ const [targets, setTargets] = useState({ menu: null, titles: [] });
140
+ const html = parsed?.html ?? "";
141
+ // The placeholders exist once React has set the HTML: find them here, before the browser paints, and portal into them.
142
+ useIsomorphicLayoutEffect(() => {
143
+ const root = ref.current;
144
+ setTargets({
145
+ menu: root?.querySelector(`[${SLOT_ATTRIBUTE}="user-menu"]`) ?? null,
146
+ titles: root ? Array.from(root.querySelectorAll(`[${SLOT_ATTRIBUTE}="app-title"]`)) : [],
147
+ });
148
+ }, [html]);
149
+ if (parsed === null) {
150
+ return null;
151
+ }
152
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ref: ref, className: className, dangerouslySetInnerHTML: { __html: html } }), hostMenu && targets.menu && createPortal(userMenu, targets.menu), targets.titles.map((element, index) => createPortal(appTitle, element, `app-title-${index}`))] }));
153
+ }
154
+ /**
155
+ * The frame's version of the admin-configured `Branding.headerHtml` (see `useBranding()`), sanitized, in a `<header>` that stays at the top of the window as the
156
+ * page scrolls. When it is there the app has no title bar of its own (`AppChrome`): the header is the top of the app, and the account
157
+ * menu lives in it - where the HTML writes `{USER_MENU}`, else in a small cell at its right end. The webmail chrome (`AppShell`) shows
158
+ * it; the admin console has no custom header (its header is the console's own) and the escrow console renders no branding HTML at all.
159
+ */
160
+ export function FrameBrandingHeader({ parsed, userMenu, fallbackMenu, appTitle }) {
161
+ // Publishes the header's height (`--rr-header-h`) for the pop-up stack, which sticks just below it.
162
+ const headerRef = useHeaderHeightRef();
163
+ if (parsed === null) {
40
164
  return null;
41
165
  }
42
- return _jsx("div", { dangerouslySetInnerHTML: { __html: clean } });
166
+ return (_jsxs("header", { ref: headerRef, className: "sticky top-0 z-30 shrink-0 flex items-stretch bg-surface-alt", "data-branding-header": "", children: [_jsx(BrandingHtml, { parsed: parsed, userMenu: userMenu, hostMenu: parsed?.hasUserMenu, appTitle: appTitle, className: "flex-1 min-w-0" }), parsed && !parsed.hasUserMenu && fallbackMenu ? (_jsx("div", { className: "shrink-0 flex items-center px-3 bg-surface border-l border-border", "data-branding-menu-cell": "", children: fallbackMenu })) : null] }));
167
+ }
168
+ /** The frame's version of the admin-configured `Branding.footerHtml`, sanitized. Its variables work as in the header; a `{USER_MENU}` here only takes the menu when the header has none. */
169
+ export function FrameBrandingFooter({ parsed, userMenu, appTitle }) {
170
+ return _jsx(BrandingHtml, { parsed: parsed, userMenu: userMenu, hostMenu: userMenu !== undefined, appTitle: appTitle });
43
171
  }
44
172
  /**
45
- * Renders the admin-configured `Branding.headerHtml`/`footerHtml` (see `useBranding()`), sanitized. The webmail
46
- * chrome (`AppShell`) shows both; the admin console shows only the footer - its header is the console's own - and the
47
- * escrow console deliberately renders no branding HTML at all.
173
+ * The admin-configured `Branding.headerHtml`, sanitized, for pages that don't use a shell, such as public pages (the booking pages). There is no
174
+ * account menu there, so a `{USER_MENU}` in the HTML renders nothing. Renders nothing without branding or HTML.
48
175
  */
49
- export function BrandingHeader({ branding }) {
50
- return _jsx(BrandingHtml, { html: branding?.headerHtml });
176
+ export function BrandingHeader({ branding, appTitle }) {
177
+ return _jsx(BrandingHtml, { parsed: useBrandingHtml(branding?.headerHtml), appTitle: appTitle });
51
178
  }
52
- export function BrandingFooter({ branding }) {
53
- return _jsx(BrandingHtml, { html: branding?.footerHtml });
179
+ /** The admin-configured `Branding.footerHtml`, sanitized, for pages that don't use a shell - see `BrandingHeader`. */
180
+ export function BrandingFooter({ branding, appTitle }) {
181
+ return _jsx(BrandingHtml, { parsed: useBrandingHtml(branding?.footerHtml), appTitle: appTitle });
54
182
  }
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ /** The number of rows at the top of `size` x `size` RGBA pixel data that are empty - the icon's own margin above its first visible pixel. */
3
+ export declare function emptyRowsAtTop(rgba: ArrayLike<number>, size: number): number;
4
+ /** Whether this environment can decode an image and read it back - not a server, and not jsdom, where there is nothing to measure. */
5
+ export declare function canMeasureImages(): boolean;
6
+ /**
7
+ * How many pixels of empty margin an icon has above its first visible pixel when fitted (`object-fit: contain`) into a `SIZE` square.
8
+ * Drawn on a canvas and read back, so it works for any picture the browser can decode from the same origin (or with CORS); `0` where
9
+ * that isn't possible (no canvas, a picture that won't load, one from another origin that taints the canvas).
10
+ */
11
+ export declare function measureTopMargin(src: string): Promise<number>;
12
+ /**
13
+ * The deployment's icon at the top of an app rail (`AppShell`, `AdminShell`, `EscrowShell`): a block as wide as the rail and as tall as the
14
+ * title bar beside it (4rem), flush with the top and left edges of the window - no padding above it - with the bar's own bottom border
15
+ * continued under it, so the two read as one block. The picture is fitted inside the block, never cropped, and raised by whatever empty
16
+ * margin the file itself has above its artwork, so the first visible pixel is at the very top of the window (a logo exported with 6% of
17
+ * air around it would otherwise still leave a gap). The icon is shown once measured (a few milliseconds after it loads).
18
+ */
19
+ export default function RailIcon({ src }: {
20
+ src: string;
21
+ }): React.JSX.Element;