@rapidmx/web-client 0.15.1 → 0.17.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 (318) hide show
  1. package/README.md +381 -373
  2. package/apps/admin/branding/index.tsx +39 -39
  3. package/apps/admin/data-requests/index.tsx +482 -480
  4. package/apps/admin/distribution-lists/new/index.tsx +3 -1
  5. package/apps/admin/domains/[uid].tsx +166 -164
  6. package/apps/admin/domains/new/index.tsx +3 -1
  7. package/apps/admin/escrow-scopes/[uid].tsx +350 -348
  8. package/apps/admin/escrow-scopes/new/index.tsx +3 -1
  9. package/apps/admin/index.tsx +129 -124
  10. package/apps/admin/mailboxes/[uid].tsx +271 -224
  11. package/apps/admin/mailboxes/new/index.tsx +30 -28
  12. package/apps/admin/plugins/index.tsx +15 -15
  13. package/apps/admin/retention-policy/index.tsx +39 -39
  14. package/apps/admin/signing-certificates/index.tsx +343 -343
  15. package/apps/admin/transport-rules/new/index.tsx +3 -1
  16. package/apps/escrow/audit-log/index.tsx +196 -196
  17. package/apps/escrow/matters/[uid].tsx +621 -621
  18. package/apps/escrow/matters/new/index.tsx +3 -1
  19. package/apps/shared/auth/adminAccess.ts +99 -99
  20. package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
  21. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
  22. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
  23. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
  32. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  33. package/apps/shared/components/calendar/EventEditor.tsx +5 -10
  34. package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
  35. package/apps/shared/components/calendar/EventModal.tsx +206 -206
  36. package/apps/shared/components/calendar/GuestInput.tsx +100 -0
  37. package/apps/shared/components/calendar/MonthView.tsx +185 -185
  38. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
  39. package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
  40. package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
  41. package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
  42. package/apps/shared/components/calendar/allDay.ts +124 -124
  43. package/apps/shared/components/calendar/eventForm.ts +2 -2
  44. package/apps/shared/components/calendar/eventFormat.ts +49 -22
  45. package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
  46. package/apps/shared/components/contacts/ContactForm.tsx +383 -383
  47. package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
  48. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
  49. package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
  50. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  51. package/apps/shared/components/layout/AppShell.tsx +461 -480
  52. package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
  53. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  54. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
  55. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  56. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  57. package/apps/shared/components/layout/UserMenu.tsx +439 -424
  58. package/apps/shared/components/mail/ConversationList.tsx +323 -323
  59. package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
  60. package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
  61. package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
  62. package/apps/shared/components/mail/MenuButton.tsx +31 -11
  63. package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
  64. package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
  65. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
  66. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  67. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
  68. package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
  69. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  70. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  71. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  72. package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
  73. package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
  74. package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
  75. package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
  76. package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
  77. package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
  78. package/apps/shared/components/mail/reading/messageExport.ts +59 -0
  79. package/apps/shared/components/mail/reading/printMessage.ts +99 -0
  80. package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
  81. package/apps/shared/components/mail/verificationSeals.ts +125 -125
  82. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  83. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  84. package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
  85. package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
  86. package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
  87. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  88. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  89. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  90. package/apps/shared/keyboard/dispatch.ts +124 -124
  91. package/apps/shared/keyboard/format.ts +89 -89
  92. package/apps/shared/keyboard/keymap.ts +114 -114
  93. package/apps/shared/keyboard/registry.ts +65 -65
  94. package/apps/shared/keyboard/targets.ts +79 -79
  95. package/apps/shared/mail/folderCounts.ts +17 -0
  96. package/apps/shared/mail/folderOfType.ts +49 -0
  97. package/apps/shared/mail/folderTree.ts +143 -143
  98. package/apps/shared/mail/listAllPages.ts +39 -39
  99. package/apps/shared/mail/newMailNotifications.ts +171 -171
  100. package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +19 -1
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
  103. package/apps/shared/mail/permanentDelete.ts +160 -0
  104. package/apps/shared/mail/primaryMailbox.ts +71 -0
  105. package/apps/shared/mail/reportNotices.ts +66 -0
  106. package/apps/shared/mail/senderBlocking.ts +141 -0
  107. package/apps/shared/mail/useCollapsedSections.ts +146 -0
  108. package/apps/shared/mail/useMailConnection.ts +205 -203
  109. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  110. package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
  111. package/apps/shared/mail/useMarkMessageRead.ts +47 -47
  112. package/apps/shared/mail/useNewMailNotifications.ts +178 -178
  113. package/apps/shared/mail/usePermanentDelete.tsx +160 -0
  114. package/apps/shared/navigation/frameContext.tsx +1 -1
  115. package/apps/shared/navigation/index.ts +43 -0
  116. package/apps/shared/navigation/pageTitle.ts +19 -0
  117. package/apps/shared/notifications/store.ts +560 -560
  118. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
  119. package/apps/shared/search/crossMailboxSearch.ts +169 -0
  120. package/apps/shared/search/localIndexBuilder.ts +481 -481
  121. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  122. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  123. package/apps/shared/signing/enrollmentView.ts +251 -251
  124. package/apps/shared/signing/useNow.ts +19 -19
  125. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
  126. package/apps/shared/styles/app.css +396 -386
  127. package/apps/www/_shell.tsx +82 -0
  128. package/apps/www/calendar/index.tsx +581 -571
  129. package/apps/www/contacts/[uid].tsx +112 -109
  130. package/apps/www/contacts/index.tsx +14 -3
  131. package/apps/www/index.tsx +2962 -2479
  132. package/apps/www/messages/[uid].tsx +139 -135
  133. package/apps/www/settings/appearance/index.tsx +5 -2
  134. package/apps/www/settings/auto-reply/index.tsx +136 -133
  135. package/apps/www/settings/blocked-senders/index.tsx +303 -0
  136. package/apps/www/settings/encryption/index.tsx +1290 -1287
  137. package/apps/www/settings/filters/[uid].tsx +179 -176
  138. package/apps/www/settings/filters/index.tsx +105 -102
  139. package/apps/www/settings/filters/new/index.tsx +165 -140
  140. package/apps/www/settings/labels/index.tsx +207 -204
  141. package/apps/www/settings/privacy/index.tsx +495 -492
  142. package/apps/www/settings/profile/index.tsx +251 -248
  143. package/apps/www/settings/read-receipts/index.tsx +150 -147
  144. package/apps/www/settings/sharing/index.tsx +259 -256
  145. package/apps/www/settings/signatures/[uid].tsx +175 -172
  146. package/apps/www/settings/signatures/index.tsx +91 -88
  147. package/apps/www/settings/signatures/new/index.tsx +138 -135
  148. package/apps/www/tasks/index.tsx +654 -649
  149. package/dist/apps/admin/data-requests/index.js +1 -1
  150. package/dist/apps/admin/distribution-lists/new/index.js +3 -1
  151. package/dist/apps/admin/domains/[uid].js +3 -1
  152. package/dist/apps/admin/domains/new/index.js +3 -1
  153. package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
  154. package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
  155. package/dist/apps/admin/index.js +5 -2
  156. package/dist/apps/admin/mailboxes/[uid].js +15 -3
  157. package/dist/apps/admin/mailboxes/new/index.js +3 -1
  158. package/dist/apps/admin/transport-rules/new/index.js +3 -1
  159. package/dist/apps/escrow/matters/new/index.js +3 -1
  160. package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
  161. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
  163. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
  165. package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
  166. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
  167. package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
  168. package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
  169. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
  170. package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
  171. package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
  172. package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
  173. package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
  174. package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
  175. package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
  176. package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
  177. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
  178. package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
  179. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  180. package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
  181. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
  182. package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
  183. package/dist/apps/shared/components/layout/AppShell.js +24 -33
  184. package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
  185. package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
  186. package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
  187. package/dist/apps/shared/components/layout/UserMenu.js +6 -5
  188. package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
  189. package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
  190. package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
  191. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
  192. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
  193. package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
  194. package/dist/apps/shared/components/mail/MenuButton.js +15 -11
  195. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
  196. package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
  197. package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
  198. package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
  199. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
  200. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
  201. package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
  202. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
  203. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
  204. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
  205. package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
  206. package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
  207. package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
  208. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
  209. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
  210. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
  211. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
  212. package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
  213. package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
  214. package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
  215. package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
  216. package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
  217. package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
  218. package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
  219. package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
  220. package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
  221. package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
  222. package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
  223. package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
  224. package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
  225. package/dist/apps/shared/mail/folderCounts.js +12 -0
  226. package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
  227. package/dist/apps/shared/mail/folderOfType.js +40 -0
  228. package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
  229. package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
  230. package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
  231. package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
  232. package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
  233. package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
  234. package/dist/apps/shared/mail/permanentDelete.js +127 -0
  235. package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
  236. package/dist/apps/shared/mail/primaryMailbox.js +61 -0
  237. package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
  238. package/dist/apps/shared/mail/reportNotices.js +47 -0
  239. package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
  240. package/dist/apps/shared/mail/senderBlocking.js +92 -0
  241. package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
  242. package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
  243. package/dist/apps/shared/mail/useMailConnection.js +3 -1
  244. package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
  245. package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
  246. package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
  247. package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
  248. package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
  249. package/dist/apps/shared/navigation/index.d.ts +16 -0
  250. package/dist/apps/shared/navigation/index.js +34 -0
  251. package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
  252. package/dist/apps/shared/navigation/pageTitle.js +12 -0
  253. package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
  254. package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
  255. package/dist/apps/shared/styles/app.css +396 -386
  256. package/dist/apps/www/_shell.d.ts +26 -0
  257. package/dist/apps/www/_shell.js +31 -0
  258. package/dist/apps/www/calendar/index.d.ts +7 -2
  259. package/dist/apps/www/calendar/index.js +7 -4
  260. package/dist/apps/www/contacts/[uid].d.ts +14 -3
  261. package/dist/apps/www/contacts/[uid].js +5 -3
  262. package/dist/apps/www/contacts/index.d.ts +7 -2
  263. package/dist/apps/www/contacts/index.js +8 -4
  264. package/dist/apps/www/index.d.ts +7 -2
  265. package/dist/apps/www/index.js +542 -207
  266. package/dist/apps/www/messages/[uid].d.ts +14 -3
  267. package/dist/apps/www/messages/[uid].js +7 -5
  268. package/dist/apps/www/settings/appearance/index.d.ts +8 -2
  269. package/dist/apps/www/settings/appearance/index.js +4 -2
  270. package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
  271. package/dist/apps/www/settings/auto-reply/index.js +4 -2
  272. package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
  273. package/dist/apps/www/settings/blocked-senders/index.js +134 -0
  274. package/dist/apps/www/settings/encryption/index.d.ts +7 -2
  275. package/dist/apps/www/settings/encryption/index.js +4 -2
  276. package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
  277. package/dist/apps/www/settings/filters/[uid].js +4 -2
  278. package/dist/apps/www/settings/filters/index.d.ts +7 -2
  279. package/dist/apps/www/settings/filters/index.js +4 -2
  280. package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
  281. package/dist/apps/www/settings/filters/new/index.js +28 -5
  282. package/dist/apps/www/settings/labels/index.d.ts +7 -2
  283. package/dist/apps/www/settings/labels/index.js +4 -2
  284. package/dist/apps/www/settings/privacy/index.d.ts +7 -2
  285. package/dist/apps/www/settings/privacy/index.js +4 -2
  286. package/dist/apps/www/settings/profile/index.d.ts +7 -2
  287. package/dist/apps/www/settings/profile/index.js +4 -2
  288. package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
  289. package/dist/apps/www/settings/read-receipts/index.js +4 -2
  290. package/dist/apps/www/settings/sharing/index.d.ts +7 -2
  291. package/dist/apps/www/settings/sharing/index.js +4 -2
  292. package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
  293. package/dist/apps/www/settings/signatures/[uid].js +4 -2
  294. package/dist/apps/www/settings/signatures/index.d.ts +7 -2
  295. package/dist/apps/www/settings/signatures/index.js +4 -2
  296. package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
  297. package/dist/apps/www/settings/signatures/new/index.js +5 -3
  298. package/dist/apps/www/tasks/index.d.ts +7 -2
  299. package/dist/apps/www/tasks/index.js +5 -3
  300. package/package.json +4 -2
  301. package/apps/shared/navigation/AppRouter.tsx +0 -300
  302. package/apps/shared/navigation/idle.ts +0 -45
  303. package/apps/shared/navigation/routerContext.tsx +0 -83
  304. package/apps/shared/navigation/routes.ts +0 -72
  305. package/apps/www/_routedPage.tsx +0 -24
  306. package/apps/www/_routes.ts +0 -37
  307. package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
  308. package/dist/apps/shared/navigation/AppRouter.js +0 -242
  309. package/dist/apps/shared/navigation/idle.d.ts +0 -14
  310. package/dist/apps/shared/navigation/idle.js +0 -43
  311. package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
  312. package/dist/apps/shared/navigation/routerContext.js +0 -56
  313. package/dist/apps/shared/navigation/routes.d.ts +0 -32
  314. package/dist/apps/shared/navigation/routes.js +0 -37
  315. package/dist/apps/www/_routedPage.d.ts +0 -12
  316. package/dist/apps/www/_routedPage.js +0 -19
  317. package/dist/apps/www/_routes.d.ts +0 -11
  318. package/dist/apps/www/_routes.js +0 -31
@@ -2,14 +2,16 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
5
+ import React, { createContext, PropsWithChildren, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
6
6
  import { HiOutlineBars3, HiOutlinePencilSquare } from "react-icons/hi2";
7
7
  import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
8
8
  import { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
9
9
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
10
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
11
11
  import AppShell, { AppShellProps } from "../../layout/AppShell.js";
12
- import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
12
+ import FloatingActionButton from "../../layout/FloatingActionButton.js";
13
+ import { useLocation } from "@rapidrest/react/client";
14
+ import { useNavigate } from "../../../navigation/index.js";
13
15
  import KeyEnrollmentGate from "../../layout/KeyEnrollmentGate.js";
14
16
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
15
17
  import { prefetchComposeWindow, useCompose } from "../compose/ComposeContext.js";
@@ -17,15 +19,19 @@ import LocalIndexLifecycle from "../../../search/LocalIndexLifecycle.js";
17
19
  import { LiveUpdates, NO_LIVE_UPDATES } from "../../../mail/useMailLiveUpdates.js";
18
20
  import { MAILBOX_LIST_LIMIT, MailConnectionContext, useMailConnection } from "../../../mail/useMailConnection.js";
19
21
  import { folderRows } from "../../../mail/folderTree.js";
22
+ import { primaryMailboxUid } from "../../../mail/primaryMailbox.js";
20
23
  import {
21
24
  CountTracker,
22
25
  FolderBadge,
23
26
  FolderCount,
24
27
  badgeFor,
25
- badgeLabel,
26
28
  countOfFolder,
27
29
  inboxUnreadTotal,
30
+ unreadBadgeTotal,
28
31
  } from "../../../mail/folderCounts.js";
32
+ import { ALL_MAILBOXES_SECTION, useSidebarSections } from "../../../mail/useCollapsedSections.js";
33
+ import FolderBadgeChip from "./FolderBadgeChip.js";
34
+ import SidebarSection from "./SidebarSection.js";
29
35
  import { useUnreadTitle } from "../../../mail/useUnreadTitle.js";
30
36
  import { pendingCountFor, usePendingSends } from "../../../mail/outbox/pendingSends.js";
31
37
  import OutboxBadge from "../OutboxBadge.js";
@@ -96,6 +102,13 @@ export interface MailShellContextValue {
96
102
  * context value, which is only ever read outside a real shell.
97
103
  */
98
104
  trackMessageChange: (previous: Message, next: Message | null) => CountTracker;
105
+ /**
106
+ * Reads every mailbox's real folder counts back and shows them (after `delayMs`, 600 ms by default) - for a change too big to track message by
107
+ * message, such as emptying a folder. A no-op on the default context value, which is only ever read outside a real shell.
108
+ */
109
+ refreshFolderCounts: (delayMs?: number) => void;
110
+ /** A folder's counts as the sidebar shows them: what the shell has worked out or been told since it loaded, else what the folder was listed with. */
111
+ folderCountOf: (folder: Folder) => FolderCount;
99
112
  /**
100
113
  * The phone layout's header row beside the folders button, for the page to put its search box in (`createPortal()`): the shell
101
114
  * owns the row, the page owns the search state. `null` until the row has rendered, and on the default context value.
@@ -112,6 +125,8 @@ const MailShellContext = createContext<MailShellContextValue>({
112
125
  noteFolderUids: () => undefined,
113
126
  live: NO_LIVE_UPDATES,
114
127
  trackMessageChange: () => NO_TRACKER,
128
+ refreshFolderCounts: () => undefined,
129
+ folderCountOf: (folder) => ({ unread: folder.unreadCount, total: folder.totalCount }),
115
130
  mobileSearchSlot: null,
116
131
  });
117
132
 
@@ -143,21 +158,6 @@ function aggregateBadge(mailboxFolders: MailboxFolders[], type: AggregateFolderT
143
158
  return badgeFor(type, sum);
144
159
  }
145
160
 
146
- /** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
147
- function FolderBadgeChip({ badge }: { badge: FolderBadge }) {
148
- return (
149
- <span
150
- className={[
151
- "text-xs rounded-pill py-0.5 px-1.5",
152
- badge.kind === "unread" ? "font-bold bg-primary/15 text-primary-dark" : "font-medium text-text-muted",
153
- ].join(" ")}
154
- >
155
- <span aria-hidden="true">{badge.value}</span>
156
- <span className="sr-only"> {badgeLabel(badge)}</span>
157
- </span>
158
- );
159
- }
160
-
161
161
  type Status = "checking" | "error" | "ready";
162
162
 
163
163
  export { MAILBOX_LIST_LIMIT };
@@ -200,16 +200,13 @@ function ComposeButton({ mailboxUid }: { mailboxUid?: string }) {
200
200
  function MobileComposeButton({ mailboxUid }: { mailboxUid?: string }) {
201
201
  const { openCompose } = useCompose();
202
202
  return (
203
- <button
204
- type="button"
205
- aria-label="New message"
203
+ <FloatingActionButton
204
+ label="New message"
205
+ icon={HiOutlinePencilSquare}
206
206
  onClick={() => openCompose({ mailboxUid })}
207
207
  onPointerEnter={prefetchComposeWindow}
208
208
  onFocus={prefetchComposeWindow}
209
- className="md:hidden fixed right-4 bottom-[4.5rem] z-30 w-12 h-12 flex items-center justify-center rounded-full bg-primary text-white shadow-lg hover:bg-primary-dark"
210
- >
211
- <HiOutlinePencilSquare size={22} aria-hidden="true" />
212
- </button>
209
+ />
213
210
  );
214
211
  }
215
212
 
@@ -227,18 +224,20 @@ function MailShortcuts({ mailboxUid }: { mailboxUid?: string }) {
227
224
  /**
228
225
  * Mail's own contextual sidebar (every accessible mailbox's own folder tree, plus a merged "All Mailboxes"
229
226
  * aggregate section) + content area, rendered inside the shared `AppShell` chrome (icon rail, header,
230
- * impersonation banner — see that component). There is no client-side router in this framework (see
231
- * `ReactRoute`'s file-convention resolver) — the selected mailbox/folder (or aggregate pseudo-folder) live
232
- * in the URL's `?mailboxUid=`/`?folderUid=`/`?aggregate=` query params, read once on mount (never during
233
- * the initial render itself, matching every other query-param reader in this codebase — e.g.
234
- * `apps/admin/quarantine`'s `readMailboxUid()` — so the server-rendered and just-hydrated client markup
235
- * match).
227
+ * impersonation banner — see that component). The selected mailbox/folder (or aggregate pseudo-folder) live
228
+ * in the URL's `?mailboxUid=`/`?folderUid=`/`?aggregate=` query params, read from the router's location
229
+ * (`@rapidrest/react`), which is the request's on the server and while hydrating and follows a shallow
230
+ * navigation (a folder link) without remounting the page.
236
231
  *
237
232
  * Every accessible mailbox's folder tree renders at once - there is no "switch mailbox" affordance
238
233
  * anymore (the mailbox `<select>` this shell used to have is gone) - so a single-mailbox user sees zero
239
234
  * behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
240
235
  * aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
241
236
  * of folders" requirement.
237
+ *
238
+ * An address that names nothing opens "All Mailboxes > Inbox" when there is more than one mailbox (that is when the section exists), and
239
+ * the one mailbox's own Inbox otherwise; a `?mailboxUid=`, `?folderUid=` or `?aggregate=` always wins. What needs a single mailbox while an all-mailboxes view is open (Compose, the new-message shortcut, key unlocking) uses the
240
+ * primary mailbox.
242
241
  */
243
242
  export default function MailShell({
244
243
  userUid,
@@ -259,20 +258,16 @@ export default function MailShell({
259
258
  const { status, error, mailboxes, mailboxFolders, foldersLoading, onFolderCreated, noteFolderUids, live, folderCounts, outbox: outboxStatuses } = hosted ?? own;
260
259
  // The messages this tab is still handing to the server: the Outbox pill counts them at once.
261
260
  const pendingSends = usePendingSends();
262
- const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
263
- const [requestedFolderUid, setRequestedFolderUid] = useState<string | null>(null);
264
- const [requestedAggregateType, setRequestedAggregateType] = useState<string | null>(null);
265
261
  const [drawerOpen, setDrawerOpen] = useState(false);
266
262
 
267
- // The selection lives in the URL, and the router (`AppRouter`) changes the URL without a page load when a folder link is
268
- // clicked - so it is read from the router's location, which updates, rather than once from `window.location`. Still read
269
- // in an effect, never during the first render, so the server render and the hydrating render agree.
270
- const search = useLocationSearch();
271
- useEffect(() => {
263
+ // The selection lives in the URL, and the router (`@rapidrest/react`) changes the URL without a page load when a folder link is
264
+ // clicked - a shallow navigation, which keeps this page and its state - so it is read from the router's location, which updates,
265
+ // rather than once from `window.location`. The location is the request's on the server and while hydrating (its `#fragment` is not
266
+ // sent, and no one here reads it), so the server render and the hydrating render agree and the selection is known from the first render.
267
+ const { search } = useLocation();
268
+ const { requestedMailboxUid, requestedFolderUid, requestedAggregateType } = useMemo(() => {
272
269
  const params = new URLSearchParams(search);
273
- setRequestedMailboxUid(params.get("mailboxUid"));
274
- setRequestedFolderUid(params.get("folderUid"));
275
- setRequestedAggregateType(params.get("aggregate"));
270
+ return { requestedMailboxUid: params.get("mailboxUid"), requestedFolderUid: params.get("folderUid"), requestedAggregateType: params.get("aggregate") };
276
271
  }, [search]);
277
272
  // Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
278
273
  // with the page - is closed here.
@@ -280,14 +275,20 @@ export default function MailShell({
280
275
  setDrawerOpen(false);
281
276
  }, [search]);
282
277
 
283
- const aggregateFolderType: AggregateFolderType | undefined = isAggregateFolderType(requestedAggregateType)
284
- ? requestedAggregateType
285
- : undefined;
278
+ // What the address asks for.
279
+ const requestedAggregate: AggregateFolderType | undefined = isAggregateFolderType(requestedAggregateType) ? requestedAggregateType : undefined;
280
+ const requestedMailbox: string | undefined =
281
+ requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined;
286
282
 
287
- const mailboxUid: string | undefined = aggregateFolderType
288
- ? undefined
289
- : (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
290
- mailboxes[0]?.uid;
283
+ // The mailbox the caller has not chosen: their own, never a shared one that merely sorts first. It is what compose, the new-message
284
+ // shortcut and everything else that needs a single mailbox use while an all-mailboxes view (which has none) is open.
285
+ const defaultMailboxUid = primaryMailboxUid(mailboxes, userUid);
286
+
287
+ // With several mailboxes the address that names nothing opens "All Mailboxes > Inbox", the merged Inbox; with one there is no such
288
+ // section, so it opens that mailbox's own Inbox. A mailbox, a folder or an all-mailboxes view in the address wins over either.
289
+ const aggregateByDefault = !requestedAggregate && !requestedMailbox && requestedFolderUid === null && mailboxes.length > 1;
290
+ const aggregateFolderType: AggregateFolderType | undefined = requestedAggregate ?? (aggregateByDefault ? "inbox" : undefined);
291
+ const mailboxUid: string | undefined = aggregateFolderType ? undefined : (requestedMailbox ?? defaultMailboxUid);
291
292
 
292
293
  const selectedMailboxFolders = mailboxFolders.find((mf) => mf.mailbox.uid === mailboxUid)?.folders ?? [];
293
294
  const folderUid: string | undefined = aggregateFolderType
@@ -296,23 +297,33 @@ export default function MailShell({
296
297
  selectedMailboxFolders.find((f) => f.type === "inbox")?.uid;
297
298
 
298
299
  // The mailbox `KeyEnrollmentGate`/`LocalIndexLifecycle`/`ComposeButton` treat as "the" mailbox when
299
- // there's no single selected one to use (aggregate mode) - the caller's own owned mailbox if they
300
- // have one, else whichever accessible mailbox happens to be first. See MailShell's own doc comment on
300
+ // there's no single selected one to use (aggregate mode) - the caller's primary mailbox (`primaryMailbox()`). See MailShell's own doc comment on
301
301
  // this being an accepted limitation: an aggregate-view message from a *different*, not-yet-visited
302
302
  // mailbox may still need that mailbox's own folder view opened directly to unlock/decrypt it.
303
- const defaultMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
304
303
  const activeMailboxUid = mailboxUid ?? defaultMailboxUid;
305
304
 
305
+ // Which sidebar sections are open: "All mailboxes" and the primary mailbox unless the reader chose otherwise. Any of them can be collapsed;
306
+ // a change of the open folder into another section opens that one (see `useSidebarSections()`); the first selection, once the address has been read
307
+ // and the mailboxes are here, is where a load lands, not a navigation. A lone mailbox has nothing to collapse, so its section is never a toggle.
308
+ const sections = useSidebarSections({
309
+ userUid,
310
+ primaryUid: defaultMailboxUid,
311
+ activeId: aggregateFolderType ? ALL_MAILBOXES_SECTION : mailboxUid,
312
+ });
313
+ const collapsible = mailboxes.length > 1;
314
+
306
315
  const counts = folderCounts.counts;
307
316
  const trackMessageChange = folderCounts.track;
317
+ const refreshFolderCounts = folderCounts.refresh;
318
+ const folderCountOf = useCallback((folder: Folder) => countOfFolder(folder, counts), [counts]);
308
319
  // `(3) Acme: Mail` in the tab strip while there is unread mail in an Inbox - kept by the frame when there is one.
309
320
  useUnreadTitle(inboxUnreadTotal(mailboxFolders, counts), { enabled: !hosted });
310
321
 
311
322
  // A state rather than a ref: the page reads it during its render, so it has to re-render once the row it names is in the document.
312
323
  const [mobileSearchSlot, setMobileSearchSlot] = useState<HTMLElement | null>(null);
313
324
  const contextValue = useMemo<MailShellContextValue>(
314
- () => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, mobileSearchSlot }),
315
- [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, mobileSearchSlot],
325
+ () => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, refreshFolderCounts, folderCountOf, mobileSearchSlot }),
326
+ [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, refreshFolderCounts, folderCountOf, mobileSearchSlot],
316
327
  );
317
328
 
318
329
  // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else for a
@@ -345,7 +356,8 @@ export default function MailShell({
345
356
  // A function, not a plain JSX constant — it's rendered twice (desktop `<aside>` + mobile
346
357
  // `Drawer`), possibly *simultaneously* mounted (the aside is only CSS-hidden below `md`, not
347
358
  // unmounted).
348
- const sidebarContent = () => (
359
+ // `idPrefix` keeps each section's folder list's id (which its heading's `aria-controls` names) unique between the two.
360
+ const sidebarContent = (idPrefix: string) => (
349
361
  <>
350
362
  <div className="p-3">
351
363
  <ComposeButton mailboxUid={defaultMailboxUid} />
@@ -357,91 +369,106 @@ export default function MailShell({
357
369
  ) : (
358
370
  <nav className="flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3">
359
371
  {mailboxes.length > 1 && (
360
- <div>
361
- <div className="text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5">
362
- All Mailboxes
363
- </div>
364
- <div className="flex flex-col gap-0.5">
365
- {AGGREGATE_FOLDER_TYPES.map((type) => {
366
- const badge = aggregateBadge(mailboxFolders, type, counts);
367
- return (
368
- <a
369
- key={type}
370
- href={`/?aggregate=${encodeURIComponent(type)}`}
371
- className={[
372
- "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
373
- aggregateFolderType === type
374
- ? "bg-primary/10 text-primary-dark font-semibold"
375
- : "text-text hover:bg-surface-alt",
376
- ].join(" ")}
377
- >
378
- <span className={badge?.kind === "unread" ? "font-semibold" : undefined}>{FOLDER_LABELS[type]}</span>
379
- {badge && <FolderBadgeChip badge={badge} />}
380
- </a>
381
- );
382
- })}
383
- </div>
384
- </div>
385
- )}
386
- {mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (
387
- <div key={mailbox.uid}>
388
- <div className="text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate">
389
- {mailbox.displayName}
390
- {mailbox.ownerUserUid ? "" : " (shared)"}
391
- </div>
392
- {mailboxError && (
393
- <div className="px-2.5 pb-1">
394
- <Alert>{mailboxError}</Alert>
395
- </div>
396
- )}
397
- <div className="flex flex-col gap-0.5">
398
- {folderRows(folders, pendingCountFor(mailbox.uid, pendingSends)).map((row) => {
399
- if (row.kind === "placeholder") {
400
- // A folder the server has not made (or told us about) yet - the Outbox and Sent Items are made with the first message
401
- // sent - shown the moment a message is on its way. Nothing to open; the real folder replaces it when it arrives.
402
- const sending = pendingCountFor(mailbox.uid, pendingSends);
403
- return (
404
- <div
405
- key={`placeholder-${row.type}`}
406
- data-folder-placeholder={row.type}
407
- className="flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5 text-text-muted"
408
- >
409
- <span>{FOLDER_LABELS[row.type]}</span>
410
- {row.type === "outbox" && <OutboxBadge total={sending} pendingHere={sending} />}
411
- </div>
412
- );
413
- }
414
- const folder = row.folder;
415
- const badge = badgeFor(folder.type, countOfFolder(folder, counts));
416
- return (
372
+ <SidebarSection
373
+ domId={`${idPrefix}-mail-section-${ALL_MAILBOXES_SECTION}`}
374
+ label="All Mailboxes"
375
+ collapsible={collapsible}
376
+ expanded={sections.isExpanded(ALL_MAILBOXES_SECTION)}
377
+ onToggle={() => sections.toggle(ALL_MAILBOXES_SECTION)}
378
+ unread={unreadBadgeTotal(mailboxFolders.flatMap((mf) => mf.folders), counts)}
379
+ >
380
+ {AGGREGATE_FOLDER_TYPES.map((type) => {
381
+ const badge = aggregateBadge(mailboxFolders, type, counts);
382
+ return (
417
383
  <a
418
- key={folder.uid}
419
- href={`/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`}
384
+ key={type}
385
+ href={`/?aggregate=${encodeURIComponent(type)}`}
386
+ data-router-shallow=""
387
+ data-router-prefetch="false"
420
388
  className={[
421
389
  "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
422
- folder.uid === folderUid
390
+ aggregateFolderType === type
423
391
  ? "bg-primary/10 text-primary-dark font-semibold"
424
392
  : "text-text hover:bg-surface-alt",
425
393
  ].join(" ")}
426
394
  >
427
- <span className={badge?.kind === "unread" ? "font-semibold" : undefined}>
428
- {FOLDER_LABELS[folder.type] ?? folder.name}
429
- </span>
430
- {badge &&
431
- (folder.type === "outbox" ? (
432
- <OutboxBadge
433
- total={badge.value}
434
- status={outboxStatuses[folder.uid]}
435
- pendingHere={pendingCountFor(mailbox.uid, pendingSends)}
436
- />
437
- ) : (
438
- <FolderBadgeChip badge={badge} />
439
- ))}
395
+ <span className={badge?.kind === "unread" ? "font-semibold" : undefined}>{FOLDER_LABELS[type]}</span>
396
+ {badge && <FolderBadgeChip badge={badge} />}
440
397
  </a>
398
+ );
399
+ })}
400
+ </SidebarSection>
401
+ )}
402
+ {mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (
403
+ <SidebarSection
404
+ key={mailbox.uid}
405
+ domId={`${idPrefix}-mail-section-${mailbox.uid}`}
406
+ label={
407
+ <>
408
+ {mailbox.displayName}
409
+ {mailbox.ownerUserUid ? "" : " (shared)"}
410
+ </>
411
+ }
412
+ collapsible={collapsible}
413
+ expanded={sections.isExpanded(mailbox.uid)}
414
+ onToggle={() => sections.toggle(mailbox.uid)}
415
+ unread={unreadBadgeTotal(folders, counts)}
416
+ notice={
417
+ mailboxError && (
418
+ <div className="px-2.5 pb-1">
419
+ <Alert>{mailboxError}</Alert>
420
+ </div>
421
+ )
422
+ }
423
+ >
424
+ {folderRows(folders, pendingCountFor(mailbox.uid, pendingSends)).map((row) => {
425
+ if (row.kind === "placeholder") {
426
+ // A folder the server has not made (or told us about) yet - the Outbox and Sent Items are made with the first message
427
+ // sent - shown the moment a message is on its way. Nothing to open; the real folder replaces it when it arrives.
428
+ const sending = pendingCountFor(mailbox.uid, pendingSends);
429
+ return (
430
+ <div
431
+ key={`placeholder-${row.type}`}
432
+ data-folder-placeholder={row.type}
433
+ className="flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5 text-text-muted"
434
+ >
435
+ <span>{FOLDER_LABELS[row.type]}</span>
436
+ {row.type === "outbox" && <OutboxBadge total={sending} pendingHere={sending} />}
437
+ </div>
441
438
  );
442
- })}
443
- </div>
444
- </div>
439
+ }
440
+ const folder = row.folder;
441
+ const badge = badgeFor(folder.type, countOfFolder(folder, counts));
442
+ return (
443
+ <a
444
+ key={folder.uid}
445
+ href={`/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`}
446
+ data-router-shallow=""
447
+ data-router-prefetch="false"
448
+ className={[
449
+ "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
450
+ folder.uid === folderUid
451
+ ? "bg-primary/10 text-primary-dark font-semibold"
452
+ : "text-text hover:bg-surface-alt",
453
+ ].join(" ")}
454
+ >
455
+ <span className={badge?.kind === "unread" ? "font-semibold" : undefined}>
456
+ {FOLDER_LABELS[folder.type] ?? folder.name}
457
+ </span>
458
+ {badge &&
459
+ (folder.type === "outbox" ? (
460
+ <OutboxBadge
461
+ total={badge.value}
462
+ status={outboxStatuses[folder.uid]}
463
+ pendingHere={pendingCountFor(mailbox.uid, pendingSends)}
464
+ />
465
+ ) : (
466
+ <FolderBadgeChip badge={badge} />
467
+ ))}
468
+ </a>
469
+ );
470
+ })}
471
+ </SidebarSection>
445
472
  ))}
446
473
  </nav>
447
474
  )}
@@ -451,9 +478,9 @@ export default function MailShell({
451
478
  inner = (
452
479
  <>
453
480
  <MailShortcuts mailboxUid={defaultMailboxUid} />
454
- <aside className="hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col">{sidebarContent()}</aside>
455
- <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Folders">
456
- <div className="flex flex-col">{sidebarContent()}</div>
481
+ <aside className="hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col">{sidebarContent("desktop")}</aside>
482
+ <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Folders" fullScreen>
483
+ <div className="flex flex-col">{sidebarContent("mobile")}</div>
457
484
  </Drawer>
458
485
  <main className="flex-1 min-w-0 overflow-y-auto">
459
486
  <div className="md:hidden flex items-center gap-2 p-3">
@@ -0,0 +1,66 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { ReactNode } from "react";
6
+ import { HiChevronDown } from "react-icons/hi2";
7
+ import FolderBadgeChip from "./FolderBadgeChip.js";
8
+
9
+ export interface SidebarSectionProps {
10
+ /** Unique per rendering of the sidebar (it is on screen twice on a phone: beside the list and in the drawer): names the folder list the heading controls. */
11
+ domId: string;
12
+ /** The heading's text. */
13
+ label: ReactNode;
14
+ /** Whether the heading is a toggle at all. Not for a lone mailbox, whose section has nothing to be hidden for. */
15
+ collapsible: boolean;
16
+ expanded: boolean;
17
+ onToggle: () => void;
18
+ /** The unread mail in the section's folders that carry an unread count (see `unreadBadgeTotal()`), shown on the heading while it is collapsed (and only when there is some) so new mail in a hidden section is still noticed. */
19
+ unread?: number;
20
+ /** Shown between the heading and the folders, collapsed or not: a failure to load the section's folders is not something to hide. */
21
+ notice?: ReactNode;
22
+ /** The section's folder rows. */
23
+ children: ReactNode;
24
+ }
25
+
26
+ /**
27
+ * One section of Mail's sidebar ("All mailboxes", or one mailbox): a heading over a list of folders. When `collapsible` the heading is a
28
+ * disclosure button (`aria-expanded`, `aria-controls` the list, a chevron that turns) and the list is `hidden` while it is collapsed -
29
+ * kept in the document, so the button's `aria-controls` always names something and nothing is fetched or rebuilt on opening it.
30
+ */
31
+ export default function SidebarSection({ domId, label, collapsible, expanded, onToggle, unread = 0, notice, children }: SidebarSectionProps) {
32
+ const shown = !collapsible || expanded;
33
+ return (
34
+ <div>
35
+ {collapsible ? (
36
+ <h2 className="mb-1">
37
+ <button
38
+ type="button"
39
+ aria-expanded={expanded}
40
+ aria-controls={domId}
41
+ onClick={onToggle}
42
+ className={[
43
+ "flex w-full items-center gap-1 rounded-sm px-2.5 py-0.5 text-left text-xs font-bold uppercase tracking-wide text-text-muted",
44
+ "focus-visible:outline-2 focus-visible:outline-primary",
45
+ "hover:bg-surface-alt hover:text-text",
46
+ ].join(" ")}
47
+ >
48
+ <HiChevronDown
49
+ size={14}
50
+ aria-hidden="true"
51
+ className={["shrink-0 transition-transform", expanded ? "" : "-rotate-90"].join(" ")}
52
+ />
53
+ <span className="min-w-0 flex-1 truncate">{label}</span>
54
+ {!expanded && unread > 0 && <FolderBadgeChip badge={{ kind: "unread", value: unread }} />}
55
+ </button>
56
+ </h2>
57
+ ) : (
58
+ <div className="text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate">{label}</div>
59
+ )}
60
+ {notice}
61
+ <div id={domId} hidden={!shown} className={shown ? "flex flex-col gap-0.5" : "hidden"}>
62
+ {children}
63
+ </div>
64
+ </div>
65
+ );
66
+ }