@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
@@ -0,0 +1,31 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect } from "react";
6
+ /** A count this hook put in front of the title, so it can be taken off again: `(3) `. */
7
+ const TITLE_PREFIX = /^\(\d+\) /;
8
+ /** `title` with `count` unread in front - `(3) Acme: Mail` - or as it is when there are none. */
9
+ export function titleWithUnread(title, count) {
10
+ const base = title.replace(TITLE_PREFIX, "");
11
+ return count > 0 ? `(${count > 99 ? "99+" : count}) ${base}` : base;
12
+ }
13
+ /**
14
+ * Keeps the browser tab's title in step with the unread count, as Outlook on the web does - `(3) Acme: Mail` - so mail that
15
+ * arrives while the tab is in the background can be seen from the tab strip. The count is put in front of whatever the title is
16
+ * (the page sets its own, from the branding) and taken off again when it drops to zero or the component goes away.
17
+ *
18
+ * Order matters when the same component also sets the title: declare this hook after the effect that does, so that on a change of `resetKey`
19
+ * the count is taken off the old title, the new title is written, and the count goes back in front of it.
20
+ */
21
+ export function useUnreadTitle(count, { enabled = true, resetKey } = {}) {
22
+ useEffect(() => {
23
+ if (!enabled) {
24
+ return;
25
+ }
26
+ document.title = titleWithUnread(document.title, count);
27
+ return () => {
28
+ document.title = titleWithUnread(document.title, 0);
29
+ };
30
+ }, [count, enabled, resetKey]);
31
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The web client's client-side router: what lets Mail, Calendar, Contacts, Tasks and Settings replace one another - and a
3
+ * folder replace a folder - without a page load.
4
+ *
5
+ * `@rapidrest/react` has no router of its own: every page is a server-rendered document with its own hydration entry, and
6
+ * hydrates only the page component (never `_layout.tsx`). So the router is a component the pages render themselves - each
7
+ * page's default export is `routedPage(path, Page)` (see `apps/www/_routedPage.tsx`), which renders `AppRouter` with the page
8
+ * as its first content. The first load is unchanged: the server renders the same tree, the browser hydrates it.
9
+ *
10
+ * `AppRouter` keeps ONE `AppChrome` (the icon rail, header, user menu, impersonation banner, compose windows, unlock prompt,
11
+ * idle-key timer, sign-out listener) mounted and swaps only what is inside it. A page's own shell (`MailShell`,
12
+ * `CalendarShell`, ...) still renders `AppShell` as it always did; inside the frame that renders nothing but its children.
13
+ *
14
+ * Navigation:
15
+ *
16
+ * Every `<a href>` to a same-origin path in the route table is intercepted (plain left clicks only - not with a modifier key, not
17
+ * `target`/`download`, not a hash-only link, not one marked `data-full-reload`), so pages just write ordinary links and
18
+ * middle-click, "open in new tab" and copy-link keep working. Anything not in the table - the admin and escrow consoles, plugin
19
+ * pages, other sites - is left to the browser. `useNavigate()` does the same for code (`navigate("/contacts")`).
20
+ *
21
+ * Back and forward work (`popstate`), and the URL is always the real, shareable one (`?mailboxUid=&folderUid=` and all).
22
+ *
23
+ * A route's code is loaded (`route.load()`) before the URL and the page change, so the old page stays up meanwhile (the frame is
24
+ * `aria-busy`); a route that fails to load falls back to a real navigation, which also picks up a new deploy.
25
+ *
26
+ * The next page's code is fetched on hover, focus and pointer-down of a link to it, and for the app rail's pages when the browser
27
+ * is idle.
28
+ *
29
+ * After a page change the frame moves keyboard focus to the content region, scrolls to the top, sets `document.title` and
30
+ * announces the new page to screen readers.
31
+ *
32
+ * The props of every `apps/www` page are the same for all of them (`WwwRoute.fetchProps()`: the user, auth-server URL,
33
+ * branding, plugin navigation, impersonation) except `params`, which the router recomputes from the URL - so the props of the
34
+ * page that was loaded are what every later page gets.
35
+ */
36
+ import React, { ComponentType } from "react";
37
+ import { RouteDefinition } from "./routes.js";
38
+ export { useLocation, useLocationSearch, useNavigate } from "./routerContext.js";
39
+ export type { NavigateFn, NavigateOptions, RouterLocation } from "./routerContext.js";
40
+ /** What `routedPage()` gives the router: the page component itself, without the router around it. */
41
+ export type RoutedPageComponent<P = any> = ComponentType<P> & {
42
+ page: ComponentType<P>;
43
+ };
44
+ export interface AppRouterProps {
45
+ routes: readonly RouteDefinition[];
46
+ /** The template of the route this document was rendered for. */
47
+ initialPath: string;
48
+ initialPage: ComponentType<any>;
49
+ /** The props the server rendered the page with. */
50
+ pageProps: Record<string, any>;
51
+ }
52
+ export default function AppRouter({ routes, initialPath, initialPage, pageProps }: AppRouterProps): React.JSX.Element;
@@ -0,0 +1,242 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ /**
7
+ * The web client's client-side router: what lets Mail, Calendar, Contacts, Tasks and Settings replace one another - and a
8
+ * folder replace a folder - without a page load.
9
+ *
10
+ * `@rapidrest/react` has no router of its own: every page is a server-rendered document with its own hydration entry, and
11
+ * hydrates only the page component (never `_layout.tsx`). So the router is a component the pages render themselves - each
12
+ * page's default export is `routedPage(path, Page)` (see `apps/www/_routedPage.tsx`), which renders `AppRouter` with the page
13
+ * as its first content. The first load is unchanged: the server renders the same tree, the browser hydrates it.
14
+ *
15
+ * `AppRouter` keeps ONE `AppChrome` (the icon rail, header, user menu, impersonation banner, compose windows, unlock prompt,
16
+ * idle-key timer, sign-out listener) mounted and swaps only what is inside it. A page's own shell (`MailShell`,
17
+ * `CalendarShell`, ...) still renders `AppShell` as it always did; inside the frame that renders nothing but its children.
18
+ *
19
+ * Navigation:
20
+ *
21
+ * Every `<a href>` to a same-origin path in the route table is intercepted (plain left clicks only - not with a modifier key, not
22
+ * `target`/`download`, not a hash-only link, not one marked `data-full-reload`), so pages just write ordinary links and
23
+ * middle-click, "open in new tab" and copy-link keep working. Anything not in the table - the admin and escrow consoles, plugin
24
+ * pages, other sites - is left to the browser. `useNavigate()` does the same for code (`navigate("/contacts")`).
25
+ *
26
+ * Back and forward work (`popstate`), and the URL is always the real, shareable one (`?mailboxUid=&folderUid=` and all).
27
+ *
28
+ * A route's code is loaded (`route.load()`) before the URL and the page change, so the old page stays up meanwhile (the frame is
29
+ * `aria-busy`); a route that fails to load falls back to a real navigation, which also picks up a new deploy.
30
+ *
31
+ * The next page's code is fetched on hover, focus and pointer-down of a link to it, and for the app rail's pages when the browser
32
+ * is idle.
33
+ *
34
+ * After a page change the frame moves keyboard focus to the content region, scrolls to the top, sets `document.title` and
35
+ * announces the new page to screen readers.
36
+ *
37
+ * The props of every `apps/www` page are the same for all of them (`WwwRoute.fetchProps()`: the user, auth-server URL,
38
+ * branding, plugin navigation, impersonation) except `params`, which the router recomputes from the URL - so the props of the
39
+ * page that was loaded are what every later page gets.
40
+ */
41
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
42
+ import { AppChrome } from "../components/layout/AppShell.js";
43
+ import { AppFrameContext } from "./frameContext.js";
44
+ import { whenIdle, shouldSaveData } from "./idle.js";
45
+ import { matchRoute } from "./routes.js";
46
+ import { RouterContext, UNKNOWN_LOCATION, readWindowLocation } from "./routerContext.js";
47
+ export { useLocation, useLocationSearch, useNavigate } from "./routerContext.js";
48
+ function pickChromeProps(props) {
49
+ const { userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, branding, appearance } = props;
50
+ return { userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, branding, appearance };
51
+ }
52
+ /** The anchor a click/pointer event is on or in, if it is a link. */
53
+ function linkOf(event) {
54
+ const target = event.target;
55
+ return target instanceof Element ? target.closest("a[href]") : null;
56
+ }
57
+ /** The URL of a link the router may take over, `null` for one the browser must handle itself. */
58
+ function routerUrlOf(link) {
59
+ if ((link.target && link.target !== "_self") || link.hasAttribute("download") || link.hasAttribute("data-full-reload")) {
60
+ return null;
61
+ }
62
+ const url = new URL(link.href, window.location.href);
63
+ if (url.origin !== window.location.origin) {
64
+ return null;
65
+ }
66
+ // A link that only moves within the page (`#section`) is the browser's own.
67
+ if (url.hash !== "" && url.pathname === window.location.pathname && url.search === window.location.search) {
68
+ return null;
69
+ }
70
+ return url;
71
+ }
72
+ export default function AppRouter({ routes, initialPath, initialPage, pageProps }) {
73
+ const [current, setCurrent] = useState(() => ({
74
+ Page: initialPage,
75
+ routePath: initialPath,
76
+ active: routes.find((route) => route.path === initialPath)?.active ?? "mail",
77
+ params: pageProps.params ?? {},
78
+ key: "initial",
79
+ }));
80
+ const [location, setLocation] = useState(UNKNOWN_LOCATION);
81
+ const [pending, setPending] = useState(false);
82
+ const [takeovers, setTakeovers] = useState(0);
83
+ const currentRef = useRef(current);
84
+ currentRef.current = current;
85
+ /** Identifies the latest navigation; one that finds it changed while loading has been overtaken and does nothing. */
86
+ const navigationRef = useRef(0);
87
+ /** The page components loaded or loading, by route template - one request per page however many links are touched. */
88
+ const pagesRef = useRef(new Map([[initialPath, Promise.resolve(initialPage)]]));
89
+ const routesRef = useRef(routes);
90
+ routesRef.current = routes;
91
+ /** Loads a route's page component (once). Rejects if its code can't be loaded - and then a later attempt tries again. */
92
+ const pageFor = useCallback((route) => {
93
+ let loading = pagesRef.current.get(route.path);
94
+ if (!loading) {
95
+ loading = route.load().then((module) => {
96
+ const page = module.default.page;
97
+ if (!page) {
98
+ throw new Error(`The page for ${route.path} is not a routedPage().`);
99
+ }
100
+ return page;
101
+ });
102
+ pagesRef.current.set(route.path, loading);
103
+ loading.catch(() => pagesRef.current.delete(route.path));
104
+ }
105
+ return loading;
106
+ }, []);
107
+ /** The pathname of the page on screen (the address bar's, once a navigation has been committed). */
108
+ const shownPathRef = useRef(null);
109
+ /**
110
+ * Shows `match` (loading its page first if it is a different page from the one on screen), calling `commit` - which puts
111
+ * the URL in the address bar - at the moment the new page replaces the old one. Resolves `false` when the page's code
112
+ * couldn't be loaded, and `true` otherwise, including when a newer navigation overtook this one while it loaded.
113
+ */
114
+ const show = useCallback(async (match, target, sequence, commit) => {
115
+ const pathChanged = target.pathname !== shownPathRef.current;
116
+ let Page = currentRef.current.Page;
117
+ if (pathChanged && match.route.path !== currentRef.current.routePath) {
118
+ try {
119
+ Page = await pageFor(match.route);
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
125
+ if (sequence !== navigationRef.current) {
126
+ return true;
127
+ }
128
+ commit();
129
+ shownPathRef.current = target.pathname;
130
+ if (pathChanged) {
131
+ setCurrent({ Page, routePath: match.route.path, active: match.route.active, params: match.params, key: target.pathname });
132
+ }
133
+ setLocation(target);
134
+ return true;
135
+ }, [pageFor]);
136
+ const navigate = useCallback((href, options = {}) => {
137
+ const url = new URL(href, window.location.href);
138
+ const leave = () => (options.replace ? window.location.replace(url.href) : (window.location.href = url.href));
139
+ const match = url.origin === window.location.origin ? matchRoute(routesRef.current, url.pathname) : undefined;
140
+ if (!match) {
141
+ leave();
142
+ return;
143
+ }
144
+ const target = { pathname: url.pathname, search: url.search, hash: url.hash };
145
+ const now = readWindowLocation();
146
+ if (target.pathname === now.pathname && target.search === now.search && target.hash === now.hash) {
147
+ return;
148
+ }
149
+ const sequence = ++navigationRef.current;
150
+ setPending(true);
151
+ void show(match, target, sequence, () => {
152
+ window.history[options.replace ? "replaceState" : "pushState"](null, "", target.pathname + target.search + target.hash);
153
+ }).then((ok) => {
154
+ if (!ok) {
155
+ leave();
156
+ }
157
+ if (sequence === navigationRef.current) {
158
+ setPending(false);
159
+ }
160
+ });
161
+ }, [show]);
162
+ // The browser's own location is read after the first render, never during it: the server can't know the query string.
163
+ useEffect(() => {
164
+ const here = readWindowLocation();
165
+ shownPathRef.current = here.pathname;
166
+ setLocation(here);
167
+ }, []);
168
+ // Back and forward.
169
+ useEffect(() => {
170
+ function handlePopState() {
171
+ const here = readWindowLocation();
172
+ const match = matchRoute(routesRef.current, here.pathname);
173
+ if (!match) {
174
+ window.location.reload();
175
+ return;
176
+ }
177
+ const sequence = ++navigationRef.current;
178
+ setPending(true);
179
+ void show(match, here, sequence, () => undefined).then((ok) => {
180
+ if (!ok) {
181
+ window.location.reload();
182
+ }
183
+ if (sequence === navigationRef.current) {
184
+ setPending(false);
185
+ }
186
+ });
187
+ }
188
+ window.addEventListener("popstate", handlePopState);
189
+ return () => window.removeEventListener("popstate", handlePopState);
190
+ }, [show]);
191
+ // Plain links, and fetching what they lead to before they are clicked.
192
+ useEffect(() => {
193
+ function handleClick(event) {
194
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
195
+ return;
196
+ }
197
+ const link = linkOf(event);
198
+ const url = link && routerUrlOf(link);
199
+ if (!url || !matchRoute(routesRef.current, url.pathname)) {
200
+ return;
201
+ }
202
+ event.preventDefault();
203
+ navigate(url.pathname + url.search + url.hash);
204
+ }
205
+ function handleIntent(event) {
206
+ const link = linkOf(event);
207
+ const url = link && routerUrlOf(link);
208
+ const match = url && matchRoute(routesRef.current, url.pathname);
209
+ if (match) {
210
+ pageFor(match.route).catch(() => undefined);
211
+ }
212
+ }
213
+ document.addEventListener("click", handleClick);
214
+ document.addEventListener("pointerover", handleIntent);
215
+ document.addEventListener("pointerdown", handleIntent);
216
+ document.addEventListener("focusin", handleIntent);
217
+ return () => {
218
+ document.removeEventListener("click", handleClick);
219
+ document.removeEventListener("pointerover", handleIntent);
220
+ document.removeEventListener("pointerdown", handleIntent);
221
+ document.removeEventListener("focusin", handleIntent);
222
+ };
223
+ }, [navigate, pageFor]);
224
+ // The app rail's other pages, once the browser has nothing else to do - so that the likeliest next click is instant.
225
+ useEffect(() => {
226
+ if (shouldSaveData()) {
227
+ return;
228
+ }
229
+ const cancels = routesRef.current
230
+ .filter((route) => route.idlePrefetch)
231
+ .map((route) => whenIdle(() => void pageFor(route).catch(() => undefined)));
232
+ return () => cancels.forEach((cancel) => cancel());
233
+ }, [pageFor]);
234
+ const enterTakeover = useCallback(() => {
235
+ setTakeovers((n) => n + 1);
236
+ return () => setTakeovers((n) => n - 1);
237
+ }, []);
238
+ const frame = useMemo(() => ({ enterTakeover }), [enterTakeover]);
239
+ const router = useMemo(() => ({ location, navigate }), [location, navigate]);
240
+ const Page = current.Page;
241
+ return (_jsx(RouterContext.Provider, { value: router, children: _jsx(AppFrameContext.Provider, { value: frame, children: _jsx(AppChrome, { active: current.active, routeKey: current.key, busy: pending, hideChrome: takeovers > 0, ...pickChromeProps(pageProps), children: _jsx(Page, { ...pageProps, params: current.params }, current.key) }) }) }));
242
+ }
@@ -0,0 +1,14 @@
1
+ /** Where each of the four rail apps lives - what the icon rail links to and what the keyboard's "Go to ..." shortcuts navigate to. */
2
+ export declare const APP_HREFS: {
3
+ readonly mail: "/";
4
+ readonly calendar: "/calendar";
5
+ readonly contacts: "/contacts";
6
+ readonly tasks: "/tasks";
7
+ };
8
+ /**
9
+ * Settings is reached from the account menu, not the rail. It links straight to the first settings section, the only one that always
10
+ * exists - repoint this at a real `/settings` landing page once there is one.
11
+ */
12
+ export declare const SETTINGS_HREF = "/settings/auto-reply";
13
+ /** Whether `pathname` is a page of Settings (any section), which is what "already there" means for it. */
14
+ export declare function isSettingsPath(pathname: string): boolean;
@@ -0,0 +1,20 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /** Where each of the four rail apps lives - what the icon rail links to and what the keyboard's "Go to ..." shortcuts navigate to. */
6
+ export const APP_HREFS = {
7
+ mail: "/",
8
+ calendar: "/calendar",
9
+ contacts: "/contacts",
10
+ tasks: "/tasks",
11
+ };
12
+ /**
13
+ * Settings is reached from the account menu, not the rail. It links straight to the first settings section, the only one that always
14
+ * exists - repoint this at a real `/settings` landing page once there is one.
15
+ */
16
+ export const SETTINGS_HREF = "/settings/auto-reply";
17
+ /** Whether `pathname` is a page of Settings (any section), which is what "already there" means for it. */
18
+ export function isSettingsPath(pathname) {
19
+ return pathname === "/settings" || pathname.startsWith("/settings/");
20
+ }
@@ -0,0 +1,19 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ /**
3
+ * What the persistent app frame (`AppRouter`'s one mounted `AppShell` chrome) offers to what is rendered inside it. `null`
4
+ * outside a frame - a page not shown by the router (an admin page, a plugin page, a test) renders its own chrome.
5
+ */
6
+ export interface AppFrameContextValue {
7
+ /** Hides the frame's chrome (icon rail, header, footer) until the returned function is called - see `FrameTakeover`. */
8
+ enterTakeover: () => () => void;
9
+ }
10
+ export declare const AppFrameContext: React.Context<AppFrameContextValue | null>;
11
+ /** `true` inside the persistent app frame. */
12
+ export declare function useInAppFrame(): boolean;
13
+ /**
14
+ * Wraps a screen that takes over the whole window - first-time key setup, "your mailbox is being set up" - which used to
15
+ * replace the page's shell entirely. Inside the persistent frame the shell can't be replaced (it stays mounted), so the frame
16
+ * hides its chrome for as long as this is mounted instead (and gives the screen the full width); outside a frame it renders its children as they are. A layout effect, so the chrome
17
+ * is gone before the screen is first painted.
18
+ */
19
+ export declare function FrameTakeover({ children }: PropsWithChildren): React.JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { createContext, useContext, useLayoutEffect } from "react";
7
+ export const AppFrameContext = createContext(null);
8
+ /** `true` inside the persistent app frame. */
9
+ export function useInAppFrame() {
10
+ return useContext(AppFrameContext) !== null;
11
+ }
12
+ /**
13
+ * Wraps a screen that takes over the whole window - first-time key setup, "your mailbox is being set up" - which used to
14
+ * replace the page's shell entirely. Inside the persistent frame the shell can't be replaced (it stays mounted), so the frame
15
+ * hides its chrome for as long as this is mounted instead (and gives the screen the full width); outside a frame it renders its children as they are. A layout effect, so the chrome
16
+ * is gone before the screen is first painted.
17
+ */
18
+ export function FrameTakeover({ children }) {
19
+ const frame = useContext(AppFrameContext);
20
+ useLayoutEffect(() => frame?.enterTakeover(), [frame]);
21
+ // The frame puts a page in `#app-content`, a flex row: a full-window screen there would shrink to its content and sit at the left
22
+ // edge instead of centring, so inside the frame it is given the whole row, as a block its own root fills.
23
+ return frame ? _jsx("div", { className: "flex-1 min-w-0", children: children }) : _jsx(_Fragment, { children: children });
24
+ }
@@ -0,0 +1,14 @@
1
+ /** How long an idle callback may be held back on a busy page before it runs anyway. */
2
+ export declare const IDLE_TIMEOUT_MS = 3000;
3
+ /** The delay used where the browser has no `requestIdleCallback` (Safari). */
4
+ export declare const IDLE_FALLBACK_DELAY_MS = 200;
5
+ /**
6
+ * Runs `callback` once the page has finished loading (the window `load` event, which waits for every script and image the page
7
+ * asked for) and the browser has nothing better to do (`requestIdleCallback`), or shortly after where there is no such thing.
8
+ * For work that only makes later things faster - fetching a code chunk, warming a cache - and must never compete with what
9
+ * the user is waiting for. Returns a function that cancels it if it hasn't run yet.
10
+ */
11
+ export declare function whenIdle(callback: () => void): () => void;
12
+ /** `true` when the user has asked the browser to save data (or is on a very slow connection): work that only speculates
13
+ * about what will be needed - prefetching - should not run then. */
14
+ export declare function shouldSaveData(): boolean;
@@ -0,0 +1,43 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /** How long an idle callback may be held back on a busy page before it runs anyway. */
6
+ export const IDLE_TIMEOUT_MS = 3000;
7
+ /** The delay used where the browser has no `requestIdleCallback` (Safari). */
8
+ export const IDLE_FALLBACK_DELAY_MS = 200;
9
+ /**
10
+ * Runs `callback` once the page has finished loading (the window `load` event, which waits for every script and image the page
11
+ * asked for) and the browser has nothing better to do (`requestIdleCallback`), or shortly after where there is no such thing.
12
+ * For work that only makes later things faster - fetching a code chunk, warming a cache - and must never compete with what
13
+ * the user is waiting for. Returns a function that cancels it if it hasn't run yet.
14
+ */
15
+ export function whenIdle(callback) {
16
+ let cancelIdle = () => undefined;
17
+ function schedule() {
18
+ if (typeof window.requestIdleCallback === "function") {
19
+ const handle = window.requestIdleCallback(callback, { timeout: IDLE_TIMEOUT_MS });
20
+ cancelIdle = () => window.cancelIdleCallback(handle);
21
+ }
22
+ else {
23
+ const timer = setTimeout(callback, IDLE_FALLBACK_DELAY_MS);
24
+ cancelIdle = () => clearTimeout(timer);
25
+ }
26
+ }
27
+ if (document.readyState === "complete") {
28
+ schedule();
29
+ }
30
+ else {
31
+ window.addEventListener("load", schedule, { once: true });
32
+ }
33
+ return () => {
34
+ window.removeEventListener("load", schedule);
35
+ cancelIdle();
36
+ };
37
+ }
38
+ /** `true` when the user has asked the browser to save data (or is on a very slow connection): work that only speculates
39
+ * about what will be needed - prefetching - should not run then. */
40
+ export function shouldSaveData() {
41
+ const connection = navigator.connection;
42
+ return connection?.saveData === true || connection?.effectiveType === "slow-2g" || connection?.effectiveType === "2g";
43
+ }
@@ -0,0 +1,37 @@
1
+ export interface RouterLocation {
2
+ pathname: string;
3
+ /** With the leading `?`, or `""`. */
4
+ search: string;
5
+ /** With the leading `#`, or `""`. */
6
+ hash: string;
7
+ }
8
+ export interface NavigateOptions {
9
+ /** Replaces the current history entry instead of adding one (a redirect, or a state change that isn't a "place"). */
10
+ replace?: boolean;
11
+ }
12
+ /** Goes to `href`: without a page load when it is a route of the app, else as an ordinary navigation. */
13
+ export type NavigateFn = (href: string, options?: NavigateOptions) => void;
14
+ export interface RouterContextValue {
15
+ location: RouterLocation;
16
+ navigate: NavigateFn;
17
+ }
18
+ export declare const RouterContext: import("react").Context<RouterContextValue | null>;
19
+ /** Before the browser's own location has been read (the server render and the first client render, which must match). */
20
+ export declare const UNKNOWN_LOCATION: RouterLocation;
21
+ export declare function readWindowLocation(): RouterLocation;
22
+ /**
23
+ * The function to go somewhere with. `navigate("/calendar")` and `navigate("/?mailboxUid=a&folderUid=b")` change the page and
24
+ * the URL without a page load when the router knows the route (Mail, Calendar, Contacts, Tasks, Settings and their subpages),
25
+ * and load the page normally otherwise; outside the router it is always a normal navigation. Prefer a plain `<a href>` for
26
+ * anything the user clicks - the router intercepts it, and it stays a real link - and this for what code decides (after a
27
+ * save, a delete, a select). The returned function is stable across renders.
28
+ */
29
+ export declare function useNavigate(): NavigateFn;
30
+ /**
31
+ * Where the app is now, as `{ pathname, search, hash }`, updating as the router navigates (folder changes are `search`
32
+ * changes, not page changes). Empty until the browser's location has been read in an effect after the first render, so that
33
+ * the server render and the first client render agree - read query parameters from it in an effect, not during render.
34
+ */
35
+ export declare function useLocation(): RouterLocation;
36
+ /** `useLocation().search` - what the shells read `?mailboxUid=` and the like from. */
37
+ export declare function useLocationSearch(): string;
@@ -0,0 +1,56 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * What the client-side router (`AppRouter.tsx`) offers to the tree under it - the location and a way to navigate - and the hooks that read
7
+ * it. Kept apart from the router itself so that the app frame (`AppShell`), which the router renders, can use `useNavigate()` without a
8
+ * circular import. `AppRouter.tsx` re-exports everything here, so callers keep importing from there.
9
+ */
10
+ import { createContext, useContext, useEffect, useState } from "react";
11
+ export const RouterContext = createContext(null);
12
+ /** Before the browser's own location has been read (the server render and the first client render, which must match). */
13
+ export const UNKNOWN_LOCATION = { pathname: "", search: "", hash: "" };
14
+ export function readWindowLocation() {
15
+ const { pathname, search, hash } = window.location;
16
+ return { pathname, search, hash };
17
+ }
18
+ /** What `useNavigate()` does outside a router: an ordinary navigation, exactly as `window.location.href = href` always was. */
19
+ function navigateWithBrowser(href, options = {}) {
20
+ if (options.replace) {
21
+ window.location.replace(href);
22
+ }
23
+ else {
24
+ window.location.href = href;
25
+ }
26
+ }
27
+ /**
28
+ * The function to go somewhere with. `navigate("/calendar")` and `navigate("/?mailboxUid=a&folderUid=b")` change the page and
29
+ * the URL without a page load when the router knows the route (Mail, Calendar, Contacts, Tasks, Settings and their subpages),
30
+ * and load the page normally otherwise; outside the router it is always a normal navigation. Prefer a plain `<a href>` for
31
+ * anything the user clicks - the router intercepts it, and it stays a real link - and this for what code decides (after a
32
+ * save, a delete, a select). The returned function is stable across renders.
33
+ */
34
+ export function useNavigate() {
35
+ const router = useContext(RouterContext);
36
+ return router ? router.navigate : navigateWithBrowser;
37
+ }
38
+ /**
39
+ * Where the app is now, as `{ pathname, search, hash }`, updating as the router navigates (folder changes are `search`
40
+ * changes, not page changes). Empty until the browser's location has been read in an effect after the first render, so that
41
+ * the server render and the first client render agree - read query parameters from it in an effect, not during render.
42
+ */
43
+ export function useLocation() {
44
+ const router = useContext(RouterContext);
45
+ const [own, setOwn] = useState(UNKNOWN_LOCATION);
46
+ useEffect(() => {
47
+ if (!router) {
48
+ setOwn(readWindowLocation());
49
+ }
50
+ }, [router]);
51
+ return router ? router.location : own;
52
+ }
53
+ /** `useLocation().search` - what the shells read `?mailboxUid=` and the like from. */
54
+ export function useLocationSearch() {
55
+ return useLocation().search;
56
+ }
@@ -0,0 +1,32 @@
1
+ import type { ComponentType } from "react";
2
+ /**
3
+ * One page the client-side router (`AppRouter`) can show without a page load. The table of them (`apps/www/_routes.ts`)
4
+ * mirrors the files under `apps/www`: what isn't in it (the admin and escrow consoles, plugin pages, anything else) is
5
+ * reached by a normal navigation.
6
+ */
7
+ export interface RouteDefinition {
8
+ /** The route template: literal segments and `:name` parameters, the same shape as the file path (`/messages/:uid`
9
+ * for `messages/[uid].tsx`). The first entry that matches a URL wins, so list literal routes before parameterized
10
+ * ones (`/settings/filters/new` before `/settings/filters/:uid`). */
11
+ path: string;
12
+ /** The app-rail entry (or `"settings"`) highlighted while this page shows - what the page's shell passes as `active`. */
13
+ active: string;
14
+ /**
15
+ * Loads the page's module, which must have a `routedPage()` component as its default export. Written as
16
+ * `() => import("./calendar/index.js")` so the bundler makes the page its own chunk; it is called the first time the
17
+ * page is shown or prefetched (pointer over or focus on a link to it, or when the browser is idle).
18
+ */
19
+ load: () => Promise<{
20
+ default: ComponentType<any>;
21
+ }>;
22
+ /** Downloaded when the browser is idle after the first page has loaded - the app rail's own pages, which are the
23
+ * likeliest next click. */
24
+ idlePrefetch?: boolean;
25
+ }
26
+ export interface RouteMatch {
27
+ route: RouteDefinition;
28
+ /** The values of the template's `:name` segments, decoded. */
29
+ params: Record<string, string>;
30
+ }
31
+ /** The first route in `routes` whose template matches `pathname`, with its parameters. */
32
+ export declare function matchRoute(routes: readonly RouteDefinition[], pathname: string): RouteMatch | undefined;
@@ -0,0 +1,37 @@
1
+ /** Splits a path into segments, ignoring a trailing slash: `/` and `""` have none. */
2
+ function segmentsOf(path) {
3
+ return path.split("/").filter((segment) => segment !== "");
4
+ }
5
+ /** Matches `pathname` against one route template - `undefined` when it doesn't, or a parameter isn't valid percent-encoding. */
6
+ function matchTemplate(template, pathname) {
7
+ const expected = segmentsOf(template);
8
+ const actual = segmentsOf(pathname);
9
+ if (expected.length !== actual.length) {
10
+ return undefined;
11
+ }
12
+ const params = {};
13
+ for (let i = 0; i < expected.length; i++) {
14
+ if (expected[i].startsWith(":")) {
15
+ try {
16
+ params[expected[i].slice(1)] = decodeURIComponent(actual[i]);
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ else if (expected[i] !== actual[i]) {
23
+ return undefined;
24
+ }
25
+ }
26
+ return params;
27
+ }
28
+ /** The first route in `routes` whose template matches `pathname`, with its parameters. */
29
+ export function matchRoute(routes, pathname) {
30
+ for (const route of routes) {
31
+ const params = matchTemplate(route.path, pathname);
32
+ if (params) {
33
+ return { route, params };
34
+ }
35
+ }
36
+ return undefined;
37
+ }