@rapidmx/web-client 0.9.0 → 0.11.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 (443) hide show
  1. package/README.md +373 -88
  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/quarantine/index.tsx +2 -1
  7. package/apps/admin/signing-certificates/index.tsx +343 -0
  8. package/apps/escrow/_layout.tsx +8 -2
  9. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  10. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  11. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  12. package/apps/shared/appearance/ColorField.tsx +107 -0
  13. package/apps/shared/appearance/appearanceCache.ts +106 -0
  14. package/apps/shared/appearance/appearanceContext.ts +77 -0
  15. package/apps/shared/appearance/bootScript.ts +35 -0
  16. package/apps/shared/appearance/color.ts +169 -0
  17. package/apps/shared/appearance/photo.ts +88 -0
  18. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  19. package/apps/shared/appearance/theme.ts +355 -0
  20. package/apps/shared/auth/accountUrl.ts +9 -0
  21. package/apps/shared/auth/adminAccess.ts +99 -0
  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 +2 -1
  26. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  27. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  28. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  29. package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
  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 +98 -115
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +25 -8
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +477 -288
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
  38. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -164
  39. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  40. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  41. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  42. package/apps/shared/components/layout/UserMenu.tsx +372 -185
  43. package/apps/shared/components/mail/ConversationList.tsx +271 -262
  44. package/apps/shared/components/mail/ConversationThreadPane.tsx +147 -94
  45. package/apps/shared/components/mail/LazyReadingPane.tsx +113 -0
  46. package/apps/shared/components/mail/MailSelectionBar.tsx +227 -222
  47. package/apps/shared/components/mail/MessageDetailPane.tsx +1537 -1387
  48. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  49. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  50. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -160
  51. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
  52. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1644 -1736
  53. package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
  54. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
  55. package/apps/shared/components/mail/compose/composePerf.ts +46 -0
  56. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  57. package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
  58. package/apps/shared/components/mail/layout/MailShell.tsx +490 -472
  59. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  60. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  61. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  62. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  63. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  64. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  65. package/apps/shared/components/mail/reading/color.ts +245 -0
  66. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  67. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  68. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  69. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  70. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  71. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  72. package/apps/shared/components/mail/unreadStyle.tsx +75 -0
  73. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  74. package/apps/shared/components/settings/layout/SettingsShell.tsx +253 -240
  75. package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
  76. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  77. package/apps/shared/components/tasks/layout/TasksShell.tsx +24 -8
  78. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
  79. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
  80. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
  81. package/apps/shared/keyboard/dispatch.ts +124 -0
  82. package/apps/shared/keyboard/format.ts +89 -0
  83. package/apps/shared/keyboard/keymap.ts +114 -0
  84. package/apps/shared/keyboard/match.ts +44 -0
  85. package/apps/shared/keyboard/parse.ts +136 -0
  86. package/apps/shared/keyboard/platform.ts +34 -0
  87. package/apps/shared/keyboard/registry.ts +65 -0
  88. package/apps/shared/keyboard/targets.ts +79 -0
  89. package/apps/shared/keyboard/useShortcut.ts +50 -0
  90. package/apps/shared/keyboard/useShortcutProps.ts +17 -0
  91. package/apps/shared/mail/folderCounts.ts +308 -0
  92. package/apps/shared/mail/folderTree.ts +143 -0
  93. package/apps/shared/mail/listSnapshots.ts +87 -0
  94. package/apps/shared/mail/messageReadState.ts +85 -0
  95. package/apps/shared/mail/newMailNotifications.ts +183 -0
  96. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  97. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  98. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  99. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  100. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  103. package/apps/shared/mail/outbox/sendState.ts +35 -0
  104. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  105. package/apps/shared/mail/useMailConnection.ts +203 -0
  106. package/apps/shared/mail/useMailLiveUpdates.ts +277 -224
  107. package/apps/shared/mail/useMarkMessageRead.ts +47 -0
  108. package/apps/shared/mail/useNewMailNotifications.ts +178 -0
  109. package/apps/shared/mail/useUnreadTitle.ts +42 -0
  110. package/apps/shared/navigation/AppRouter.tsx +300 -0
  111. package/apps/shared/navigation/appHrefs.ts +23 -0
  112. package/apps/shared/navigation/frameContext.tsx +35 -0
  113. package/apps/shared/navigation/idle.ts +45 -0
  114. package/apps/shared/navigation/routerContext.tsx +83 -0
  115. package/apps/shared/navigation/routes.ts +72 -0
  116. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  117. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  118. package/apps/shared/notifications/apiErrors.ts +98 -0
  119. package/apps/shared/notifications/headerOffset.ts +35 -0
  120. package/apps/shared/notifications/pushStatus.ts +56 -0
  121. package/apps/shared/notifications/store.ts +549 -0
  122. package/apps/shared/notifications/systemErrors.ts +63 -0
  123. package/apps/shared/notifications/useNotifications.ts +41 -0
  124. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
  125. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  126. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  127. package/apps/shared/signing/enrollmentView.ts +251 -0
  128. package/apps/shared/signing/signingInfo.ts +58 -0
  129. package/apps/shared/signing/useNow.ts +19 -0
  130. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  131. package/apps/shared/styles/app.css +108 -10
  132. package/apps/www/_layout.tsx +8 -2
  133. package/apps/www/_routedPage.tsx +24 -0
  134. package/apps/www/_routes.ts +36 -0
  135. package/apps/www/calendar/index.tsx +64 -33
  136. package/apps/www/contacts/[uid].tsx +109 -107
  137. package/apps/www/contacts/index.tsx +62 -42
  138. package/apps/www/index.tsx +2291 -1917
  139. package/apps/www/messages/[uid].tsx +108 -101
  140. package/apps/www/settings/appearance/index.tsx +21 -0
  141. package/apps/www/settings/auto-reply/index.tsx +7 -8
  142. package/apps/www/settings/encryption/index.tsx +1287 -1249
  143. package/apps/www/settings/filters/[uid].tsx +7 -2
  144. package/apps/www/settings/filters/index.tsx +102 -99
  145. package/apps/www/settings/filters/new/index.tsx +140 -133
  146. package/apps/www/settings/labels/index.tsx +204 -201
  147. package/apps/www/settings/privacy/index.tsx +7 -9
  148. package/apps/www/settings/read-receipts/index.tsx +7 -8
  149. package/apps/www/settings/sharing/index.tsx +256 -274
  150. package/apps/www/settings/signatures/[uid].tsx +172 -167
  151. package/apps/www/settings/signatures/index.tsx +88 -85
  152. package/apps/www/settings/signatures/new/index.tsx +135 -129
  153. package/apps/www/tasks/index.tsx +29 -18
  154. package/dist/apps/admin/_layout.d.ts +5 -1
  155. package/dist/apps/admin/_layout.js +3 -2
  156. package/dist/apps/admin/index.js +3 -2
  157. package/dist/apps/admin/ingest-queue/index.js +2 -1
  158. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  159. package/dist/apps/admin/quarantine/index.js +2 -1
  160. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  161. package/dist/apps/admin/signing-certificates/index.js +161 -0
  162. package/dist/apps/escrow/_layout.d.ts +5 -1
  163. package/dist/apps/escrow/_layout.js +3 -2
  164. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  165. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  166. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  167. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  168. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  169. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  170. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  171. package/dist/apps/shared/appearance/ColorField.js +40 -0
  172. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  173. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  174. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  175. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  176. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  177. package/dist/apps/shared/appearance/bootScript.js +33 -0
  178. package/dist/apps/shared/appearance/color.d.ts +65 -0
  179. package/dist/apps/shared/appearance/color.js +132 -0
  180. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  181. package/dist/apps/shared/appearance/photo.js +79 -0
  182. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  183. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  184. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  185. package/dist/apps/shared/appearance/theme.js +251 -0
  186. package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
  187. package/dist/apps/shared/auth/accountUrl.js +8 -0
  188. package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
  189. package/dist/apps/shared/auth/adminAccess.js +89 -0
  190. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  191. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  192. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  193. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  194. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  195. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  196. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +1 -1
  197. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  198. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  199. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  200. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
  201. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
  202. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  203. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  204. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +6 -2
  205. package/dist/apps/shared/components/contacts/ContactsToolbar.js +28 -10
  206. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
  207. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +19 -6
  208. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  209. package/dist/apps/shared/components/layout/AppShell.d.ts +34 -4
  210. package/dist/apps/shared/components/layout/AppShell.js +122 -17
  211. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  212. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  213. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +3 -1
  215. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  216. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  217. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  218. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  219. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  220. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  221. package/dist/apps/shared/components/layout/UserMenu.d.ts +25 -3
  222. package/dist/apps/shared/components/layout/UserMenu.js +88 -7
  223. package/dist/apps/shared/components/mail/ConversationList.js +5 -11
  224. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +6 -2
  225. package/dist/apps/shared/components/mail/ConversationThreadPane.js +87 -40
  226. package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
  227. package/dist/apps/shared/components/mail/LazyReadingPane.js +83 -0
  228. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -4
  229. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
  230. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +25 -8
  231. package/dist/apps/shared/components/mail/MessageDetailPane.js +167 -73
  232. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  233. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  234. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  235. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  236. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +38 -0
  237. package/dist/apps/shared/components/mail/compose/ComposeContext.js +97 -6
  238. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
  239. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +3 -2
  240. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +316 -339
  241. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
  242. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
  243. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
  244. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
  245. package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
  246. package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
  247. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  248. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  249. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
  250. package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
  251. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +14 -4
  252. package/dist/apps/shared/components/mail/layout/MailShell.js +90 -104
  253. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  254. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  255. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  256. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  257. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  258. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  259. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  260. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  261. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  262. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  263. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  264. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  265. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  266. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  267. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  268. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  269. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  270. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  271. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  272. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  273. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  274. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  275. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  276. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  277. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  278. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  279. package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
  280. package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
  281. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  282. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  283. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
  284. package/dist/apps/shared/components/settings/layout/SettingsShell.js +16 -5
  285. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
  286. package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
  287. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  288. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
  289. package/dist/apps/shared/components/tasks/layout/TasksShell.js +18 -6
  290. package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
  291. package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
  292. package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
  293. package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
  294. package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
  295. package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
  296. package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
  297. package/dist/apps/shared/keyboard/dispatch.js +109 -0
  298. package/dist/apps/shared/keyboard/format.d.ts +12 -0
  299. package/dist/apps/shared/keyboard/format.js +74 -0
  300. package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
  301. package/dist/apps/shared/keyboard/keymap.js +84 -0
  302. package/dist/apps/shared/keyboard/match.d.ts +20 -0
  303. package/dist/apps/shared/keyboard/match.js +31 -0
  304. package/dist/apps/shared/keyboard/parse.d.ts +31 -0
  305. package/dist/apps/shared/keyboard/parse.js +107 -0
  306. package/dist/apps/shared/keyboard/platform.d.ts +15 -0
  307. package/dist/apps/shared/keyboard/platform.js +22 -0
  308. package/dist/apps/shared/keyboard/registry.d.ts +39 -0
  309. package/dist/apps/shared/keyboard/registry.js +33 -0
  310. package/dist/apps/shared/keyboard/targets.d.ts +14 -0
  311. package/dist/apps/shared/keyboard/targets.js +67 -0
  312. package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
  313. package/dist/apps/shared/keyboard/useShortcut.js +35 -0
  314. package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
  315. package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
  316. package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
  317. package/dist/apps/shared/mail/folderCounts.js +218 -0
  318. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  319. package/dist/apps/shared/mail/folderTree.js +116 -0
  320. package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
  321. package/dist/apps/shared/mail/listSnapshots.js +43 -0
  322. package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
  323. package/dist/apps/shared/mail/messageReadState.js +63 -0
  324. package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
  325. package/dist/apps/shared/mail/newMailNotifications.js +138 -0
  326. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  327. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  328. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  329. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  330. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  331. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  332. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  333. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  334. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  335. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  336. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  337. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  338. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  339. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  340. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  341. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  342. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  343. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  344. package/dist/apps/shared/mail/useMailConnection.d.ts +56 -0
  345. package/dist/apps/shared/mail/useMailConnection.js +138 -0
  346. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +29 -9
  347. package/dist/apps/shared/mail/useMailLiveUpdates.js +60 -32
  348. package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
  349. package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
  350. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +35 -0
  351. package/dist/apps/shared/mail/useNewMailNotifications.js +130 -0
  352. package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
  353. package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
  354. package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
  355. package/dist/apps/shared/navigation/AppRouter.js +242 -0
  356. package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
  357. package/dist/apps/shared/navigation/appHrefs.js +20 -0
  358. package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
  359. package/dist/apps/shared/navigation/frameContext.js +24 -0
  360. package/dist/apps/shared/navigation/idle.d.ts +14 -0
  361. package/dist/apps/shared/navigation/idle.js +43 -0
  362. package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
  363. package/dist/apps/shared/navigation/routerContext.js +56 -0
  364. package/dist/apps/shared/navigation/routes.d.ts +32 -0
  365. package/dist/apps/shared/navigation/routes.js +37 -0
  366. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  367. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  368. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  369. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  370. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  371. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  372. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  373. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  374. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  375. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  376. package/dist/apps/shared/notifications/store.d.ts +136 -0
  377. package/dist/apps/shared/notifications/store.js +416 -0
  378. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  379. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  380. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  381. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  382. package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
  383. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  384. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  385. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  386. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  387. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  388. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  389. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  390. package/dist/apps/shared/signing/signingInfo.js +48 -0
  391. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  392. package/dist/apps/shared/signing/useNow.js +18 -0
  393. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  394. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  395. package/dist/apps/shared/styles/app.css +108 -10
  396. package/dist/apps/www/_layout.d.ts +5 -1
  397. package/dist/apps/www/_layout.js +3 -2
  398. package/dist/apps/www/_routedPage.d.ts +12 -0
  399. package/dist/apps/www/_routedPage.js +19 -0
  400. package/dist/apps/www/_routes.d.ts +11 -0
  401. package/dist/apps/www/_routes.js +30 -0
  402. package/dist/apps/www/calendar/index.d.ts +2 -2
  403. package/dist/apps/www/calendar/index.js +41 -11
  404. package/dist/apps/www/contacts/[uid].d.ts +3 -9
  405. package/dist/apps/www/contacts/[uid].js +11 -9
  406. package/dist/apps/www/contacts/index.d.ts +2 -2
  407. package/dist/apps/www/contacts/index.js +30 -24
  408. package/dist/apps/www/index.d.ts +2 -2
  409. package/dist/apps/www/index.js +332 -48
  410. package/dist/apps/www/messages/[uid].d.ts +3 -7
  411. package/dist/apps/www/messages/[uid].js +10 -5
  412. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  413. package/dist/apps/www/settings/appearance/index.js +13 -0
  414. package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
  415. package/dist/apps/www/settings/auto-reply/index.js +6 -7
  416. package/dist/apps/www/settings/encryption/index.d.ts +2 -2
  417. package/dist/apps/www/settings/encryption/index.js +106 -96
  418. package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
  419. package/dist/apps/www/settings/filters/[uid].js +6 -2
  420. package/dist/apps/www/settings/filters/index.d.ts +2 -2
  421. package/dist/apps/www/settings/filters/index.js +3 -1
  422. package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
  423. package/dist/apps/www/settings/filters/new/index.js +9 -3
  424. package/dist/apps/www/settings/labels/index.d.ts +2 -2
  425. package/dist/apps/www/settings/labels/index.js +3 -1
  426. package/dist/apps/www/settings/privacy/index.d.ts +2 -2
  427. package/dist/apps/www/settings/privacy/index.js +8 -9
  428. package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
  429. package/dist/apps/www/settings/read-receipts/index.js +6 -7
  430. package/dist/apps/www/settings/sharing/index.d.ts +2 -2
  431. package/dist/apps/www/settings/sharing/index.js +22 -34
  432. package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
  433. package/dist/apps/www/settings/signatures/[uid].js +6 -2
  434. package/dist/apps/www/settings/signatures/index.d.ts +2 -2
  435. package/dist/apps/www/settings/signatures/index.js +3 -1
  436. package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
  437. package/dist/apps/www/settings/signatures/new/index.js +9 -4
  438. package/dist/apps/www/tasks/index.d.ts +2 -2
  439. package/dist/apps/www/tasks/index.js +22 -16
  440. package/package.json +2 -2
  441. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  442. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  443. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
package/README.md CHANGED
@@ -1,88 +1,373 @@
1
- # RapidMX: Web Client
2
-
3
- [![npm version](https://img.shields.io/npm/v/@rapidmx/web-client)](https://www.npmjs.com/package/@rapidmx/web-client)
4
-
5
- RapidMX's webmail (`apps/www`), admin console (`apps/admin`) and escrow console (`apps/escrow`) React UI. The pages are
6
- served and hydrated by [`rapidmx/server`](https://github.com/RapidMX/server) through `@rapidrest/react`'s file-convention
7
- routes, and `@rapidmx/electron-client` reuses the same components. Platform-agnostic API clients, hooks and generic UI
8
- primitives live in [`@rapidmx/react-shared`](https://github.com/RapidMX/react-shared).
9
-
10
- ## Package layout
11
-
12
- The package ships the TSX sources (`apps/`) and a compiled mirror (`dist/apps/`, JavaScript plus `.d.ts` declarations).
13
- There is no root export. Every module is its own subpath, mapped by `package.json`'s `exports` from
14
- `@rapidmx/web-client/<path>.js` to `dist/apps/<path>.js`:
15
-
16
- ```ts
17
- import SettingsShell from "@rapidmx/web-client/shared/components/settings/layout/SettingsShell.js";
18
- ```
19
-
20
- `@rapidmx/web-client/shared/styles/app.css` is the Tailwind entry point and design tokens.
21
-
22
- ## Plugin UI surface
23
-
24
- Server plugins can ship their own pages (see the plugin manifest's `ui` field in `@rapidmx/restapi`). The server builds
25
- them together with this package, so they share one React, one `@rapidmx/react-shared` state and one stylesheet. The
26
- modules below are the **supported surface for plugin pages**. Anything else under `apps/` is internal and may change in
27
- any release.
28
-
29
- ### Shells
30
-
31
- | Import | Use |
32
- | --- | --- |
33
- | `shared/components/layout/AppShell.js` | Chrome for webmail apps: app rail, header, user menu, impersonation banner, compose and unlock providers. `active` is a core app or the plugin's `appRail` item id. |
34
- | `shared/components/settings/layout/SettingsShell.js` | Settings chrome with the section list and mailbox switcher. `active` is the plugin's `settingsSections` item id. `useSettingsShell()` gives the selected `mailboxUid` and the accessible `mailboxes`. |
35
- | `shared/components/admin/layout/AdminShell.js` | Admin console chrome, gated on administrator access - an administrator whose session isn't elevated is sent to auth-server's `/auth/elevate` page and returned. `active` is the plugin's `adminNav` item id. |
36
- | `shared/components/layout/BrandingChrome.js` | `BrandingHeader` and `BrandingFooter`, for pages that don't use a shell, such as public pages. |
37
- | `shared/plugins/pluginNav.js` | The `PluginNav`, `PluginUiNavItem` and `PluginNavProps` types. |
38
-
39
- Every www and admin page receives a `pluginNav` prop from the server. It lists the settings sections, admin sections and
40
- app rail entries of every enabled plugin whose UI built. The shells append those entries after their own, with a generic
41
- icon. An entry whose id matches a core entry is skipped, and so is one whose `href` isn't a same-origin path. Pass the
42
- page props straight to the shell so the navigation shows:
43
-
44
- ```tsx
45
- import React from "react";
46
- import SettingsShell, {
47
- SettingsShellProps,
48
- useSettingsShell,
49
- } from "@rapidmx/web-client/shared/components/settings/layout/SettingsShell.js";
50
-
51
- export default function RemindersSettingsPage(props: Omit<SettingsShellProps, "active">) {
52
- return (
53
- <SettingsShell {...props} active="reminders">
54
- <RemindersSettings />
55
- </SettingsShell>
56
- );
57
- }
58
-
59
- function RemindersSettings() {
60
- const { mailboxUid } = useSettingsShell();
61
- return <p>Settings for {mailboxUid}</p>;
62
- }
63
- ```
64
-
65
- Public and escrow pages get no `pluginNav`.
66
-
67
- ### From `@rapidmx/react-shared`
68
-
69
- Plugin pages import these directly from `@rapidmx/react-shared`, which the server resolves to the same copy the shells
70
- use:
71
-
72
- - `branding/useBranding.js`: `useBranding()`, for the branding and icon of pages outside a shell;
73
- - `auth/session.js`: `useRedirectIfUnauthenticated()`, already called by every shell;
74
- - `util/api.js`: `apiFetch()` and `ApiRequestError`, for calling the plugin's own API routes;
75
- - `mail/mailApi.js`: mailboxes and folders, such as `listMailboxes()` and `listFolders()`;
76
- - `components/buttons/Button.js`, `components/feedback/Alert.js` and `components/feedback/Skeleton.js`;
77
- - `components/forms/FormField.js`;
78
- - `components/overlays/Modal.js` and `components/overlays/Drawer.js`;
79
- - `components/pickers/MiniDatePicker.js`.
80
-
81
- ## Development
82
-
83
- ```sh
84
- yarn install
85
- yarn test # vitest with coverage gates
86
- yarn lint
87
- yarn build # tsc into dist/apps
88
- ```
1
+ # RapidMX: Web Client
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@rapidmx/web-client)](https://www.npmjs.com/package/@rapidmx/web-client)
4
+
5
+ RapidMX's webmail (`apps/www`), admin console (`apps/admin`) and escrow console (`apps/escrow`) React UI. The pages are
6
+ served and hydrated by [`rapidmx/server`](https://github.com/RapidMX/server) through `@rapidrest/react`'s file-convention
7
+ routes, and `@rapidmx/electron-client` reuses the same components. Platform-agnostic API clients, hooks and generic UI
8
+ primitives live in [`@rapidmx/react-shared`](https://github.com/RapidMX/react-shared).
9
+
10
+ ## Package layout
11
+
12
+ The package ships the TSX sources (`apps/`) and a compiled mirror (`dist/apps/`, JavaScript plus `.d.ts` declarations).
13
+ There is no root export. Every module is its own subpath, mapped by `package.json`'s `exports` from
14
+ `@rapidmx/web-client/<path>.js` to `dist/apps/<path>.js`:
15
+
16
+ ```ts
17
+ import SettingsShell from "@rapidmx/web-client/shared/components/settings/layout/SettingsShell.js";
18
+ ```
19
+
20
+ `@rapidmx/web-client/shared/styles/app.css` is the Tailwind entry point and design tokens.
21
+
22
+ ## Navigation without page loads
23
+
24
+ `@rapidrest/react` has no router: every page is a server-rendered document that hydrates only its own page component. The
25
+ `www` pages (Mail, Calendar, Contacts, Tasks, Settings and their subpages) therefore render a small client-side router
26
+ themselves, and go between one another - and between the folders of Mail - without a page load:
27
+
28
+ - each `apps/www` page's default export is `routedPage("/its/route", Page)` (`apps/www/_routedPage.tsx`). The server
29
+ renders and the browser hydrates exactly what it did before; the first load is unchanged. `apps/www/_routes.ts` lists the
30
+ pages, each with a dynamic `import()` so it is a chunk of its own (a test keeps it in step with the files);
31
+ - one `AppShell` chrome (the app rail, header, user menu, impersonation banner, compose windows in progress, the unlock
32
+ prompt and the idle-key timer) stays mounted, and only the page inside it is replaced. A page's own shell
33
+ (`MailShell`, `CalendarShell`, ...) still renders `AppShell`; inside the router that is only its children;
34
+ - **links stay ordinary links.** Every same-origin `<a href>` to a route in the table is taken over - plain left clicks
35
+ only, so ctrl/cmd/shift/middle click, `target`, `download`, `#hash` links and links marked `data-full-reload` keep
36
+ the browser's behaviour, and pages that are not in the table (the admin and escrow consoles, plugin pages, other sites) are
37
+ reached with a page load. The address bar always holds the real, shareable URL (`/?mailboxUid=&folderUid=`), and back and
38
+ forward work;
39
+ - a page's code is fetched when the pointer, focus or a press reaches a link to it, and for the app rail's pages when the
40
+ browser is idle after load (not with data saving on); if it can't be loaded the router falls back to a page load;
41
+ - after a page change focus moves to the content region (`#app-content`), the window scrolls to the top, `document.title`
42
+ follows the page and a polite live region announces it.
43
+
44
+ Code that decides where to go uses the two hooks in `shared/navigation/AppRouter.js`:
45
+
46
+ ```tsx
47
+ import { useLocation, useNavigate } from "@rapidmx/web-client/shared/navigation/AppRouter.js";
48
+
49
+ const navigate = useNavigate(); // navigate("/contacts"), navigate("/?mailboxUid=a&folderUid=b", { replace: true })
50
+ const { pathname, search, hash } = useLocation(); // empty until read after the first render, so server and browser agree
51
+ ```
52
+
53
+ `navigate()` changes the page without a load when the URL is a route of the app and is an ordinary navigation otherwise
54
+ (outside the router too, where it is `window.location.href = ...`). Page props are the same for every `www` page except
55
+ `params`, which the router recomputes from the URL. Plugin pages are not part of the router (they render their own
56
+ `AppShell` chrome) and are opened with a page load.
57
+
58
+ The compose window, the reading pane, S/MIME and the emoji list are also chunks of their own, loaded on demand or fetched
59
+ when the browser is idle, so a page's first JavaScript is React and what the first screen draws.
60
+
61
+ ## Keyboard shortcuts
62
+
63
+ One keyboard layer, `shared/keyboard/`, lives in the persistent app frame (`AppChrome`), so the shortcuts work in every view -
64
+ Mail, Calendar, Contacts, Tasks and Settings - and a page change only changes which of them are registered. `?` (or `Ctrl+/`)
65
+ opens a "Keyboard shortcuts" dialog, also reached from the account menu, that lists what is available *here* - the global
66
+ shortcuts plus those of the view on screen (and of an open compose window) - with the platform's own key names. `mod` below is
67
+ **Ctrl** on Windows and Linux and **Cmd** on a Mac; the navigation set is Ctrl+Shift on every platform (Cmd+Shift collides with the
68
+ browsers' own, so it is not offered).
69
+
70
+ | Where | Key | Does |
71
+ | --- | --- | --- |
72
+ | Everywhere | `Ctrl+Shift+A` | Account (auth-server's account page; only offered when one is configured) |
73
+ | | `Ctrl+Shift+S` / `B` / `M` / `C` / `L` | Settings / Contacts / Mail / Calendar / To-Do |
74
+ | | `?` or `Ctrl+/` | Keyboard shortcuts |
75
+ | Mail | `Alt+N` | New message |
76
+ | | `mod+R` / `mod+Shift+R` / `mod+Shift+F` | Reply / Reply all / Forward the selected message |
77
+ | | `mod+D` or `Delete` | Delete the selected message or conversation (moves it to Deleted Items) |
78
+ | | `E` or `Backspace` | Archive |
79
+ | | `mod+Shift+V` | Move to folder |
80
+ | | `Ctrl+Q` / `mod+U` | Mark as read / unread (`Ctrl+Q` on a Mac too: `Cmd+Q` quits) |
81
+ | | `Insert` | Flag or unflag |
82
+ | | `Down` or `J` / `Up` or `K` | Next / previous message (conversation) |
83
+ | | `Ctrl+.` / `Ctrl+,` | Next / previous unread |
84
+ | | `Enter` / `Escape` | Open the selected message on its own page / clear the selection (leave select mode, clear the search) |
85
+ | | `/` or `mod+E` | Search |
86
+ | Compose window | `mod+Enter` | Send |
87
+ | | `mod+S` | Save draft |
88
+ | | `Escape` | Close, keeping the draft (the existing "keep draft / discard" question still applies) |
89
+ | | `Alt+N` | Another new message |
90
+ | Calendar | `Alt+N` | New event |
91
+ | | `T` | Today |
92
+ | | `Left` / `Right` (or `mod+Left` / `mod+Right`) | Previous / next period |
93
+ | | `Ctrl+Alt+1` / `2` / `3` / `4` | Day / work week / week / month |
94
+ | Contacts | `Alt+N` | New contact |
95
+ | | `/` or `mod+E` | Search |
96
+ | Tasks | `Alt+N` | New task (moves to the "Add a task" field) |
97
+
98
+ In the desktop client (`@rapidmx/electron-client`, which exposes `window.rapidmx`) `Ctrl+N` (`Cmd+N` on a Mac) also creates - a new
99
+ message, event, contact or task in the current view - and `Ctrl+Shift+T` also goes to Tasks; browsers keep both for themselves, which is
100
+ why the web client uses `Alt+N` and `Ctrl+Shift+L`.
101
+
102
+ How it behaves:
103
+
104
+ - **Views register only what they can do.** A view claims a shortcut with `useShortcut(SHORTCUTS.mail.reply, handler, { enabled })`
105
+ for as long as it is mounted and able to do it (Reply exists only while a message is selected, Archive not for Drafts or Outbox),
106
+ so there are no dead keys and the help dialog is always accurate. `SHORTCUTS` in `shared/keyboard/keymap.js` is the one key map;
107
+ handlers are looked up when the key is pressed. A handler that returns `false` declines the key.
108
+ - **Scopes.** `global`, the view's own (`mail`, `calendar`, `contacts`, `tasks`), `compose` (while focus is inside a compose window; it
109
+ beats the view behind it) and `dialog`: while a modal dialog (`aria-modal`) is open only its own shortcuts and its own Escape work.
110
+ - **Typing is never taken.** Bare keys (`J`, `E`, `?`, `Delete`) do nothing while focus is in a text field, select or the rich-text
111
+ editor, and caret keys with a modifier (word jumps) stay the field's. Chords with Ctrl/Alt/Cmd work from a field, except Option on
112
+ a Mac and Ctrl+Alt (AltGr) elsewhere, which type characters. Copy, Cut, Paste, Select all, Undo, Redo and Find are never
113
+ bound. Enter and Space are left to a focused button or link, and an open menu keeps its own keys. Events already
114
+ `defaultPrevented` (the editor's own bindings, a menu), IME composition and held-key repeats of one-shot actions are ignored, and
115
+ the browser's default is prevented only when a handler actually ran.
116
+ - **Layouts.** A key is matched on `event.key` (so it follows AZERTY or Dvorak) with `event.code` as the fallback when `event.key` is
117
+ not a Latin character - macOS Option+N, or a Cyrillic layout.
118
+ - **Hints.** Buttons that a shortcut also does carry `aria-keyshortcuts` and a tooltip such as "Reply (Ctrl+R)"; their accessible
119
+ names are unchanged (`useShortcutProps()` gives a control both).
120
+
121
+ Known limits: a browser keeps a few keys for itself (`Ctrl+N`, `Ctrl+T`, `Ctrl+W` are never delivered to a page, and some browsers claim
122
+ `Ctrl+Shift+A/B/C/M/S`); a message body is shown in a frame of its own, which hands the keys pressed in it on to the layer, so they work there too. In the compose body the editor's own bindings win over `Ctrl+Shift+S`, `B`
123
+ and `L` (strike-through, quote, align left).
124
+
125
+ ## The reading pane
126
+
127
+ `shared/components/mail/` draws a message as a **card** and a conversation as a stack of them, under a **subject card** that stays pinned at the top
128
+ while the cards scroll (`MessageDetailPane`, `ConversationThreadPane`, and `reading/` for the pieces):
129
+
130
+ - **Subject card** - the conversation's (or the message's) subject as the page's `h1`, "N messages", a note when a very long thread was cut. In a
131
+ thread it is on screen at once, from what the list row already knows, with a skeleton card per message (up to three) until the messages arrive.
132
+ - **Message card** - rounded, bordered and lifted, in the app's own tokens (`bg-surface`, `text-text`, `border-border`), so it follows the theme, a
133
+ branding palette and the Appearance colours. A header row with the sender's initials, `Name <address>`, To/Cc/Bcc, the time and the actions (Reply,
134
+ Reply All, Forward, Archive, Move to, Labels, and "View original" when it applies); the security badge and every notice (signature, key change,
135
+ receipts, Outbox state); attachments; the body; a slim "You replied to this message." bar; and, on a single message and on the newest open one in a
136
+ thread, Reply and Forward at its foot. An older message of a thread is a collapsed card (sender, date, first line) that expands on click or Enter
137
+ (`aria-expanded`, `aria-controls`; the focus stays on the message's header button as it swaps between the two states).
138
+ - **Each card is exactly as tall as its message.** The body is shown at its natural height - no inner scrollbar, no dead space, no fixed height - and
139
+ follows it as images load and the width changes. A table or image wider than the card scrolls **inside** the card and never widens the pane. The
140
+ pane scrolls as a whole. Everything is on screen before the body: the header card, the message card's header and a skeleton where the body will be.
141
+
142
+ - **Encrypted messages** are themed card content in the same place as the body, never a frame: while the security state is being worked out, a skeleton;
143
+ locked (no unlocked keys on this device), a lock, "This message is encrypted", "Unlock your keys to read it" and an **Unlock** button that asks for the unlock
144
+ prompt and, once unlocked - by it or from anywhere - decrypts the message in place and moves the focus to it; unreadable with the keys in hand (not a recipient,
145
+ damaged), "This message can’t be decrypted" with the reason and no button; decrypted, the message like any other. The placeholder subject `[...]` reads
146
+ "Encrypted message", and list rows and collapsed cards with no preview say "Encrypted message" with a small lock.
147
+
148
+ **How a body is shown.** The server sanitizes a message's HTML when it ingests it (`GET /mail/messages/:id/content`); the client then treats what it
149
+ receives - and everything it recovers by decrypting or verifying, which the server never saw - as hostile. It is sanitized again with DOMPurify (scripts,
150
+ event handlers, `javascript:` URLs, every remote resource), stripped of forms, frames, media, plug-ins, SVG's active elements and every link that is not
151
+ `http(s)`/`mailto`/`tel`, and shown in a **sandboxed iframe without `allow-scripts`**, behind a Content-Security-Policy that allows no script, object, frame,
152
+ form or fetch (images are `data:` URIs and this server's own attachment URLs for inline `cid:` images; **remote images are never loaded**). `allow-same-origin`
153
+ is what lets the app measure the frame and adapt its colours, and it is safe only because nothing can run there: any one of the four layers (server
154
+ sanitizer, client sanitizer, CSP, sandbox) stops a script on its own - see `.claude/NOTES.md`, 2026-09-21, for the threat model and the hostile-mail corpus it
155
+ was tried against. Every link opens in a new tab without an opener. A message over 1.5 MB of markup or 20,000 elements is offered on its own page instead.
156
+
157
+ **Mail takes the theme.** A message follows the app's scheme wherever its author left the colours to the reader, and is shown as authored where the
158
+ author chose them. Plain-text mail and HTML with no colours at all (a typed reply) are drawn in the theme's surface and text colours, links in its link
159
+ colour. Where the HTML sets a colour: text on the theme's surface is kept if it reads (4.5:1) and adapted if it doesn't (black becomes white in a dark
160
+ theme, white becomes dark in a light one, a low-contrast navy is only lightened as far as it takes); anything on an authored, opaque background - a white table,
161
+ a banner, a gradient - is shown exactly as authored, except text nobody coloured, which gets black or white by contrast with that background, never the theme's
162
+ colour. A message that declares its own dark styles (a `color-scheme` meta or property, or a `prefers-color-scheme: dark` block) gets them when the theme is
163
+ dark. "View original" (the sun, in the card's header; the moon then goes back) shows just that message exactly as authored for the rest of the session; it is
164
+ offered only where adapting changes something. Known limits: a dark logo or PNG with a transparent background on a dark theme, text baked into images,
165
+ `mix-blend-mode` and SVG paint are left as they are.
166
+
167
+ Printing puts a message back to the colours it was written with for the print and adapts it again afterwards; the actions and the footer are not printed.
168
+
169
+ ## Notifications, sending and the Outbox
170
+
171
+ **One pop-up system** (`shared/notifications/`). `AppChrome` mounts `NotificationCenter` once, right under the header row (a zero-height `sticky`
172
+ line that sticks just below the header - the title bar's or a branding header's height, published as `--rr-header-h` - so the stack starts below the
173
+ account menu and never covers it, the header's buttons or a compose window's title bar and Send button; its height is capped by `--rr-compose-top`), and
174
+ everything the app has to say goes through the framework-free store:
175
+
176
+ ```ts
177
+ import { notify, update, dismiss } from "@rapidmx/web-client/shared/notifications/store.js";
178
+ import { notifyApiError } from "@rapidmx/web-client/shared/notifications/apiErrors.js";
179
+
180
+ const id = notify({ kind: "error", title: "Couldn't archive the message", message: "The server said no.",
181
+ details: ["Status: 502 (api-1)"], actions: [{ label: "Retry", onClick: retry }], dedupeKey: "archive" });
182
+ update(id, { kind: "success", title: "Archived", actions: [] }); // resolve it
183
+ try { await archive(); } catch (err) { notifyApiError(err, "Couldn't archive the message"); }
184
+ ```
185
+
186
+ `kind` is `mail`, `info`, `success`, `warning` or `error`. At most three show at once (the rest queue); errors and anything with `actions` are sticky
187
+ until dismissed or resolved by `update()`/`dismiss()`, the others go after 5 to 10 seconds - clocks pause on hover, focus and a hidden tab; a repeated
188
+ `dedupeKey` is one pop-up with a count; errors are announced assertively (`role="alert"` inside an `aria-live="assertive"` region), the rest politely;
189
+ `details` is an expandable, monospace, copyable block. The last 30 (not new-mail ones) are kept in memory and `sessionStorage` and listed by "Recent
190
+ notifications" in the account menu. `notifyApiError()` turns an `ApiRequestError` into an error pop-up with the server's message (a `401` becomes
191
+ "Your session expired" with a Sign in button, anywhere in the app: the frame registers `setApiUnauthorizedObserver()`). The frame also catches unhandled
192
+ errors and rejections as one "Something went wrong", and says so, subtly, when the live connection has been down for more than ten seconds.
193
+
194
+ **Sending does not wait** (`shared/mail/outbox/`). Send validates what it can from what the window knows (a recipient, keys that must be unlocked, an
195
+ attachment that can't be signed - shown inline, the window stays open; an attachment still uploading is waited for), then closes the window at once and
196
+ hands a snapshot to `startSend()`: save, sign or encrypt in the browser if that applies, then `POST /mail/messages/:id/send` with `{ "background": true }`,
197
+ which answers `202 { status: "queued", message }` and relays in the background. Failures at any stage are a sticky "This message wasn't sent" with the
198
+ reason, technical details, **Retry** and **Open draft**; the server's later outcome arrives as `send-succeeded` / `send-retrying` / `send-failed` push
199
+ events. The Outbox row in the folder list is a pill with the count (optimistic at the click, then the server's), animated while anything is on its way and
200
+ red while one failed; rows in the Outbox list say what each message is doing.
201
+
202
+ **Encryption fails open.** A message is treated as unencrypted - its draft saves, Close and Send work, nothing about encryption is shown - unless you turned
203
+ encryption on (or replied to an encrypted message) or the encryption policy *was loaded* and encrypts for every recipient; a request that failed, is slow or
204
+ is pending never blocks anything (`compose/encryptionRequirement.ts`).
205
+
206
+ ## The header, the footer and their variables
207
+
208
+ The admin console's Branding page sets a **header** and a **footer** as HTML. The server sanitizes them when they are saved and this client
209
+ sanitizes them again when it shows them: no scripts, styles, forms, inputs or SVG, no `on*` handlers, only `http`, `https` and `mailto` links.
210
+
211
+ **A custom header is the top of the app.** When one is set it replaces the app's own title bar (the app's name and the account menu) and the icon
212
+ at the top of the icon rail: the rail then starts with the app icons, and the header stays at the top of the window while a page scrolls. The
213
+ account menu moves into it. Two variables can be written in the *text* of the header or the footer:
214
+
215
+ | Variable | Replaced with |
216
+ | --- | --- |
217
+ | `{USER_MENU}` | The account menu: the avatar button and its drop-down (Account, Settings, notifications, keyboard shortcuts, Admin Console, Sign Out). |
218
+ | `{APP_TITLE}` | The name of the app on screen ("Mail", "Calendar", "Contacts", "Tasks", "Settings"), kept current as you move between apps without a page load. |
219
+
220
+ ```html
221
+ <div style="display:flex; align-items:center; justify-content:space-between; padding:0.5rem 1rem">
222
+ <span>{APP_TITLE}</span>
223
+ <span>{USER_MENU}</span>
224
+ </div>
225
+ ```
226
+
227
+ - A variable is matched exactly - upper case, in braces, no spaces - and only in text. One in an attribute (`title="{USER_MENU}"`), a comment,
228
+ a `<script>` or `<style>` is left as written or gone with them, and the author can't write the placeholders React fills in (`data-rr-slot` is
229
+ stripped).
230
+ - `{USER_MENU}` is replaced **once**, where it first appears in the header; if the header has none, at its first place in the footer (the menu then
231
+ opens upward); if neither has one, the menu sits in a small cell at the right end of the header, so the account menu, Sign Out and the shortcuts
232
+ help are never lost. A second or third copy is removed (two menus would mean duplicate ids and focus targets), and so is one inside a link or in
233
+ text that can't hold an element (`<title>`, `<textarea>`).
234
+ - `{APP_TITLE}` is replaced **everywhere** it appears, as text (never markup). It changes on client-side navigation, and works in the footer too.
235
+ - The menu is drawn into `<body>` with `position: fixed`, so a header with `overflow: hidden`, its own `z-index` or a stacking context can neither
236
+ clip it nor hide it behind the page. It follows its button when the window resizes or scrolls, and fits a 390 px window.
237
+ - Put `{USER_MENU}` where your CSS doesn't hide it: a header whose stylesheet hides its `<nav>` on phones should keep the variable outside the nav.
238
+ - Focus order is document order: the menu button is reached after the text before it and before what follows it.
239
+ - Pages that don't use a shell (the booking pages) render `BrandingHeader` and `BrandingFooter`, which have no menu: `{USER_MENU}` renders nothing
240
+ there and `{APP_TITLE}` is the `appTitle` prop, if any. The variable names are `USER_MENU_VARIABLE` and `APP_TITLE_VARIABLE` in
241
+ `shared/components/layout/BrandingChrome.js`.
242
+
243
+ The frame draws the header from the `branding` prop the server rendered the page with (`useBranding()`'s own fetch takes over when it answers), so
244
+ the frame is the right shape - no title bar, no rail icon - from the first paint, and the icon of a frame without a custom header sits flush with the
245
+ top of the window (`RailIcon`: it is measured on a canvas and raised by whatever empty margin the image file has above its artwork).
246
+
247
+ ## Appearance
248
+
249
+ **Settings > Appearance** (`/settings/appearance`) lets each user choose the colour scheme (System, Light or Dark), four theme colours (primary,
250
+ accent, surface and text - each a colour picker with a hex field and its own "Reset"), and a background (none, a colour, or an uploaded picture with
251
+ dim 0-80%, blur 0-20 px and fit cover, contain or tile). Every change is applied to the real app in the same frame - the page you are on is the preview -
252
+ and saved in the background (coalesced, only what changed); a save that fails puts the change back and says so. A picture is checked before it is
253
+ uploaded (PNG, JPEG, WebP or AVIF, up to 8 MB). Text on the surface, and text on an accent button, under 4.5:1 contrast gets a warning, never a block.
254
+ "Reset all" returns everything to the defaults and removes the picture.
255
+
256
+ **How it reaches the app.** `AppearanceProvider` (mounted by `AppChrome`, `AdminShell` and `EscrowShell`) owns the preferences and writes one
257
+ `<style id="rr-appearance">` into `<head>` (`appearance/theme.js`), sets `<html data-theme="light|dark">` (for System, the operating system's scheme,
258
+ live) and remembers the result in `localStorage` (`rapidmx-appearance`). The server's `_layout.tsx` renders the same stylesheet from the `appearance`
259
+ page prop and a tiny inline script (`APPEARANCE_BOOT_SCRIPT`) applies the browser's copy before the first paint, so there is no flash on a load and none
260
+ on a client-side navigation. The server caches a rendered page for up to a minute, so whichever copy - the page's or the browser's - was changed last wins,
261
+ and the server is asked once the page is up.
262
+
263
+ - **Precedence:** the app's defaults < the deployment's branding stylesheet < the user's choices. A user's colours are `--rr-color-*` custom properties
264
+ declared `!important` on `<html>`, so no branding rule can beat them; the branding stylesheet's own rules (say a header's background) still decide their
265
+ own elements, exactly as before. Only the colours a user chose are set: primary and accent get their full scales (darker and lighter steps mixed from the
266
+ one colour) and readable text colours, surface and text get their alt, border and muted derivatives.
267
+ - **The background** is two fixed pseudo-elements of `<html>` (`::before` the picture or colour, `::after` the dim, the scheme's surface colour at the
268
+ chosen opacity), behind everything, with no markup, no layout shift and nothing to repaint while a list scrolls. With a background set the icon rail, the
269
+ header and the content region become translucent panels (`--color-surface` and `--color-surface-alt` are the surface colour at an alpha, chosen so text
270
+ and muted text stay at least 4.5:1 against the worst place the picture can be - the picture's lightest and darkest twentieth, measured on a canvas -
271
+ more see-through for a plain photo or a high dim, more opaque for a busy one); menus, dialogs, drawers and pop-ups take the opaque colours back. Without a
272
+ background nothing changes.
273
+ - **Hooks:** `useResolvedTheme()` (`appearance/resolvedTheme.js`) is `"light"` or `"dark"`, live, needing no provider; `useAppearance()` gives
274
+ `{ prefs, resolved, setPrefs(patch), uploadBackground(file), removeBackground(), reset(), saving, error, ... }`.
275
+ - **API** (`@rapidmx/react-shared/appearance/preferencesApi.js`): `GET/PUT /mail/preferences/appearance` (a merge), `POST/DELETE
276
+ /mail/preferences/appearance/background`, `GET .../background/:version`; a change is also pushed as `AppearancePreferences...` `update` events on
277
+ the user's own uid channel.
278
+
279
+ ## Toolbars, folders and the signing certificate
280
+
281
+ **Toolbars that fit (`components/layout/ResponsiveToolbar.tsx`).** A bar measures its own width (`ResizeObserver`) and lays itself out from a table of actions (`ToolbarAction`: label,
282
+ icon, group, `rank` - the lowest goes into "More" first - and `essential`): captions under the icons while everything fits; else icons only (each keeps a tooltip and, for New contact, its
283
+ shortcut's `title` and `aria-keyshortcuts`); else as many icons as fit and the rest in a portalled `role="menu"` behind a **More** button. `layoutToolbar(actions, width)` is the pure
284
+ function behind it (`width === undefined` - server render, no ResizeObserver - shows everything), and the bar is `overflow-x-clip` so it can never paint over the pane beside it. Contacts
285
+ uses it; the panes around it follow one rule: a list column of `clamp(16rem, 32vw, 26rem)`, a detail pane that keeps the rest, each column scrolling by itself (`md:h-[calc(100dvh - var(--rr-header-h))]` on the
286
+ page root - the frame's own height is open-ended), the side menu a drawer below `lg`, and no empty shell column for a single mailbox.
287
+
288
+ **The folder tree (`mail/folderTree.ts`).** `FOLDER_ORDER` (Inbox, Drafts, Outbox, Sent Items, Deleted Items, Junk, Archive, then custom), `upsertFolder()`, `removeFolder()` and
289
+ `reconcileFolders()` return the same array when nothing changed (everything that reads the tree starts over when it is a new one), never touch the counts (the overlay in `folderCounts.ts`
290
+ owns them) and never remove on a listing (deletions arrive as events). `useMailConnection()` files what `Folder` create / update / delete events say and what every counts read-back lists
291
+ (`useFolderCounts(mailboxes, folders, onFoldersListed)`), so a folder is found by an event, a message in it, a send outcome, the poll, a focus or a reconnect, whichever comes first. `folderRows()`
292
+ is the one place a folder that has not arrived is drawn - a muted Outbox / Sent Items placeholder while a message is on its way - so it can never be drawn twice. Push channels are ordered inboxes,
293
+ mailbox uids, then the other folders in sidebar order (`pushChannelsFor()`).
294
+
295
+ **The signing certificate (`signing/`).** `enrollmentTracker.ts` follows one enrollment per mailbox for everybody who wants to show or react to it: a first read, then 15 s, 30 s and every 60 s
296
+ while pending - only while the page is visible (a hidden tab asks nothing; coming back or a focus asks at once and restarts the backoff), stopping when it ends; `checkEnrollmentNow()` is the
297
+ "Check status" (a 10 s cooldown, the server's 429 honoured, a plain read on a server without the check endpoint); a pending enrollment stays followed after the page that started it is left.
298
+ `useSigningEnrollmentWatcher()` (in `AppChrome`) seeds it for the mailboxes you own and raises the "issued" / "failed" pop-up. `enrollmentView.ts` holds the wording, the steps, the percentage
299
+ and the expiry maths; `SigningCertificateCard` draws them. The account menu's **Theme** row is `ThemeSwitch`, over `useAppearance().setPrefs({ mode })`.
300
+
301
+ ## Plugin UI surface
302
+
303
+ Server plugins can ship their own pages (see the plugin manifest's `ui` field in `@rapidmx/restapi`). The server builds
304
+ them together with this package, so they share one React, one `@rapidmx/react-shared` state and one stylesheet. The
305
+ modules below are the **supported surface for plugin pages**. Anything else under `apps/` is internal and may change in
306
+ any release.
307
+
308
+ ### Shells
309
+
310
+ | Import | Use |
311
+ | --- | --- |
312
+ | `shared/components/layout/AppShell.js` | Chrome for webmail apps: app rail, header, user menu, impersonation banner, compose and unlock providers. `active` is a core app or the plugin's `appRail` item id. The user menu shows an "Admin Console" item to an administrator even when their session isn't elevated (it asks auth-server for the user's own roles, using the `trustedRoles` page prop) and, in Mail, the new-mail pop-up switch. |
313
+ | `shared/components/settings/layout/SettingsShell.js` | Settings chrome with the section list and mailbox switcher. `active` is the plugin's `settingsSections` item id. `useSettingsShell()` gives the selected `mailboxUid` and the accessible `mailboxes`. |
314
+ | `shared/components/admin/layout/AdminShell.js` | Admin console chrome, gated on administrator access - an administrator whose session isn't elevated is sent to auth-server's `/auth/elevate` page and returned. `active` is the plugin's `adminNav` item id. |
315
+ | `shared/components/layout/BrandingChrome.js` | `BrandingHeader` and `BrandingFooter`, for pages that don't use a shell, such as public pages. |
316
+ | `shared/plugins/pluginNav.js` | The `PluginNav`, `PluginUiNavItem` and `PluginNavProps` types. |
317
+
318
+ Every www and admin page receives a `pluginNav` prop from the server. It lists the settings sections, admin sections and
319
+ app rail entries of every enabled plugin whose UI built. The shells append those entries after their own, with a generic
320
+ icon. An entry whose id matches a core entry is skipped, and so is one whose `href` isn't a same-origin path. Pass the
321
+ page props straight to the shell so the navigation shows:
322
+
323
+ ```tsx
324
+ import React from "react";
325
+ import SettingsShell, {
326
+ SettingsShellProps,
327
+ useSettingsShell,
328
+ } from "@rapidmx/web-client/shared/components/settings/layout/SettingsShell.js";
329
+
330
+ export default function RemindersSettingsPage(props: Omit<SettingsShellProps, "active">) {
331
+ return (
332
+ <SettingsShell {...props} active="reminders">
333
+ <RemindersSettings />
334
+ </SettingsShell>
335
+ );
336
+ }
337
+
338
+ function RemindersSettings() {
339
+ const { mailboxUid } = useSettingsShell();
340
+ return <p>Settings for {mailboxUid}</p>;
341
+ }
342
+ ```
343
+
344
+ Public and escrow pages get no `pluginNav`.
345
+
346
+ ### From `@rapidmx/react-shared`
347
+
348
+ Plugin pages import these directly from `@rapidmx/react-shared`, which the server resolves to the same copy the shells
349
+ use:
350
+
351
+ - `branding/useBranding.js`: `useBranding()`, for the branding and icon of pages outside a shell;
352
+ - `auth/session.js`: `useRedirectIfUnauthenticated()`, already called by every shell;
353
+ - `util/api.js`: `apiFetch()` and `ApiRequestError`, for calling the plugin's own API routes;
354
+ - `mail/mailApi.js`: mailboxes and folders, such as `listMailboxes()` and `listFolders()`;
355
+ - `components/buttons/Button.js`, `components/feedback/Alert.js` and `components/feedback/Skeleton.js`;
356
+ - `components/forms/FormField.js`;
357
+ - `components/overlays/Modal.js` and `components/overlays/Drawer.js`;
358
+ - `components/pickers/MiniDatePicker.js`.
359
+
360
+ ## Uninstalling a plugin with its data
361
+
362
+ The admin console's Plugins page (`PluginsManager`, also embedded in the setup wizard) uninstalls a plugin through a dialog with an unchecked **Also delete all data this plugin stored** box. Ticking it lists what will be deleted (collections and tables, saved settings, cached package and pages, whatever the plugin cleans up itself), says **This can't be undone**, turns the confirm button into the red **Uninstall and delete data** and keeps it disabled until the plugin's display name is typed (case and surrounding spaces ignored). The dialog is a `Modal` (focus moves in and stays in, Escape closes, the checkbox is described by the list) and a `<form>`, so Enter in the name field confirms once it matches; at 390 px it keeps the modal's 20 px gutters and the list wraps.
363
+
364
+ The request is `removePlugin(uid, { purgeData: true })` (`@rapidmx/react-shared/admin/pluginsApi.js`); the server accepts it only from an elevated administrator, and an `api-104` answer is shown as a request to reload or sign in again. Deletion happens on the servers after the last copy stops running the plugin, so `GET /api/system/plugins/status` carries each deletion as `purges` and the page lists the uninstalled plugin with its state - *Uninstalled - data will be deleted after servers restart* (and how many servers still run it), *Data deleted <date>*, or *Data deletion failed: <reason>* with the failed steps and a Retry button. It reads the status every 5 seconds while a deletion is waiting or running, and raises an `apps/shared/notifications` pop-up when one that was under way is deleted or fails, and a warning when adding a plugin cancels one. A plugin that is installed is never shown as uninstalled, whatever the server still lists about an earlier deletion.
365
+
366
+ ## Development
367
+
368
+ ```sh
369
+ yarn install
370
+ yarn test # vitest with coverage gates
371
+ yarn lint
372
+ yarn build # tsc into dist/apps
373
+ ```
@@ -4,15 +4,20 @@
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { PropsWithChildren } from "react";
6
6
  import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
7
+ import { AppearanceHead, appearanceHtmlAttributes } from "../shared/appearance/AppearanceHead.js";
7
8
  import { CUSTOM_STYLESHEET_LINK_ID } from "@rapidmx/react-shared/branding/useBranding.js";
8
9
 
9
10
  export interface LayoutProps {
10
11
  /** See `apps/www/_layout.tsx`'s `LayoutProps` doc comment - identical mechanism, supplied by
11
12
  * `AdminConsoleRoute`'s `fetchProps()` override. */
12
13
  branding?: Branding;
14
+ /** The signed-in user's stored appearance preferences (a page prop from the same `fetchProps()`, present only on the www pages and only when the user has any): rendered as a stylesheet in the `<head>` so the very first paint is themed - see `AppearanceHead`. */
15
+ appearance?: unknown;
16
+ /** The signed-in user (supplied by the framework), so a cached appearance from another account on this browser is never applied. */
17
+ userUid?: string;
13
18
  }
14
19
 
15
- export default function Layout({ children, branding }: PropsWithChildren<LayoutProps>) {
20
+ export default function Layout({ children, branding, appearance, userUid }: PropsWithChildren<LayoutProps>) {
16
21
  const title = branding?.title || branding?.companyName
17
22
  ? `${branding?.title || branding?.companyName}: Mail Admin Console`
18
23
  : "RapidMX: Mail Admin Console";
@@ -20,7 +25,7 @@ export default function Layout({ children, branding }: PropsWithChildren<LayoutP
20
25
  const stylesheetHref = branding?.stylesheetUrl;
21
26
 
22
27
  return (
23
- <html lang="en">
28
+ <html lang="en" {...appearanceHtmlAttributes(appearance, userUid)}>
24
29
  <head>
25
30
  <meta charSet="utf-8" />
26
31
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -34,6 +39,7 @@ export default function Layout({ children, branding }: PropsWithChildren<LayoutP
34
39
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
35
40
  />
36
41
  {stylesheetHref && <link rel="stylesheet" href={stylesheetHref} id={CUSTOM_STYLESHEET_LINK_ID} />}
42
+ <AppearanceHead appearance={appearance} />
37
43
  </head>
38
44
  <body>{children}</body>
39
45
  </html>
@@ -31,7 +31,8 @@ function MailboxesListContent() {
31
31
  useEffect(() => {
32
32
  setLoading(true);
33
33
  setError(null);
34
- listMailboxes({ page, limit: PAGE_SIZE })
34
+ // The administration scope: every mailbox as administrative metadata - the console never shows anybody's mail.
35
+ listMailboxes({ page, limit: PAGE_SIZE, scope: "admin" })
35
36
  .then(setMailboxes)
36
37
  .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load mailboxes."))
37
38
  .finally(() => setLoading(false));
@@ -69,6 +70,11 @@ function MailboxesListContent() {
69
70
  </div>
70
71
  </div>
71
72
 
73
+ <p className="text-sm text-text-muted mb-4">
74
+ Administrative details only (addresses, owners, quota). The console never shows a mailbox&rsquo;s mail:
75
+ to see someone&rsquo;s account, impersonate them from their mailbox page.
76
+ </p>
77
+
72
78
  {error && <Alert>{error}</Alert>}
73
79
 
74
80
  <Modal open={confirmingSetup} onClose={() => setConfirmingSetup(false)} title="Run setup again?">
@@ -50,7 +50,8 @@ function IngestQueueContent() {
50
50
  let cancelled = false;
51
51
  setLoading(true);
52
52
  setError(null);
53
- listIngestQueue(mailboxUid, { page, limit: PAGE_SIZE })
53
+ // The administration scope: any mailbox's pending mail, recorded in the audit log.
54
+ listIngestQueue(mailboxUid, { page, limit: PAGE_SIZE, scope: "admin" })
54
55
  .then((data) => {
55
56
  if (!cancelled) setEntries(data);
56
57
  })