@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
@@ -0,0 +1,41 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React from "react";
6
+ import { HiOutlineTrash } from "react-icons/hi2";
7
+
8
+ export interface EmptyFolderBarProps {
9
+ /** The folder the list is showing ("Deleted Items", "Junk Email"). */
10
+ folderName: string;
11
+ /** How many messages it holds, when that is known. */
12
+ count?: number;
13
+ /** Nothing to empty, or not allowed to. */
14
+ disabled: boolean;
15
+ /** Why, shown as the button's tooltip while it is disabled. */
16
+ disabledReason?: string;
17
+ onEmpty: () => void;
18
+ }
19
+
20
+ /**
21
+ * Outlook's "Empty folder", at the top of the list of Deleted Items and of Junk Email: the whole folder is permanently deleted, after the
22
+ * page has asked (this only reports the click). A row of its own under the toolbar rather than a toolbar button, so the toolbar keeps the
23
+ * width a phone gives it and the action is never taken for one of the list's arrangement controls.
24
+ */
25
+ export default function EmptyFolderBar({ folderName, count, disabled, disabledReason, onEmpty }: EmptyFolderBarProps) {
26
+ return (
27
+ <div className="flex items-center justify-between gap-2 px-3 py-1 border-b border-border text-xs text-text-muted">
28
+ <span>{count === undefined ? "" : `${count} ${count === 1 ? "item" : "items"}`}</span>
29
+ <button
30
+ type="button"
31
+ onClick={onEmpty}
32
+ disabled={disabled}
33
+ title={disabled ? disabledReason : `Permanently delete everything in ${folderName}`}
34
+ className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-sm text-danger hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent"
35
+ >
36
+ <HiOutlineTrash size={16} aria-hidden="true" className="shrink-0" />
37
+ {`Empty ${folderName}`}
38
+ </button>
39
+ </div>
40
+ );
41
+ }
@@ -1,227 +1,240 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { useState } from "react";
6
- import { Folder, Message } from "@rapidmx/react-shared/mail/mailApi.js";
7
- import { Label } from "@rapidmx/react-shared/mail/labelsApi.js";
8
- import LabelMenuButton from "./labelMenu.js";
9
- import MoveToFolderDialog, { MOVE_TARGET_TYPES } from "./MoveToFolderDialog.js";
10
- import { ariaKeyShortcuts, withHint } from "../../keyboard/format.js";
11
- import { SHORTCUTS, ShortcutDef } from "../../keyboard/keymap.js";
12
- import { useKeyEnvironment } from "../../keyboard/ShortcutProvider.js";
13
-
14
- export interface MailSelectionBarProps {
15
- selected: Message[];
16
- /** Every message currently listed - what "Select all" selects and what `allSelected` is measured against. */
17
- listed: Message[];
18
- /** What the bar *counts*, when the rows being ticked aren't messages themselves: the conversation list
19
- * ticks conversations, each standing for several messages, so "3 conversations selected" is what the
20
- * reader picked while `selected` stays the messages every action below acts on. Left out by the flat
21
- * message list, which counts `selected`/`listed` directly. */
22
- totals?: { selected: number; listed: number; noun: string };
23
- onSelectAll: () => void;
24
- onClearSelection: () => void;
25
- /** Leaves select mode entirely (and clears the selection). */
26
- onCancel: () => void;
27
- folders: Folder[];
28
- currentFolderUid?: string;
29
- /** Every label this mailbox has, for the Apply label action. */
30
- labels: Label[];
31
- /** The mailbox a label created from Apply label belongs to. */
32
- mailboxUid: string;
33
- onLabelCreated: (label: Label) => void;
34
- /** A folder created from the Move to prompt, for the caller's own folder list and the sidebar. */
35
- onFolderCreated: (folder: Folder) => void;
36
- /** Sets the selection's labels: every message ends up with `labelUids`, plus whichever of
37
- * `keepPartial` it already had (those rows were left partially applied, so each message keeps what
38
- * it has). */
39
- onApplyLabels: (labelUids: string[], keepPartial: string[]) => void;
40
- onSetRead: (read: boolean) => void;
41
- onSetFlagged: (flagged: boolean) => void;
42
- onArchive: () => void;
43
- /** Moves the selection into `folderUid`, resolving once the bulk update has settled. It resolves
44
- * whether or not that update was rejected: a bulk update is applied element by element, so a failure
45
- * is explained by a pop-up (see `notifyApiError()`), raised by the page along with the reload it triggers, rather than in the
46
- * prompt, which would be claiming the move simply didn't happen. */
47
- onMoveTo: (folderUid: string) => Promise<void>;
48
- onReportJunk: () => void;
49
- onDelete: () => void;
50
- /** A bulk action is in flight - every action is held until it settles, since the next one would send
51
- * `version`s the first has already superseded. */
52
- busy: boolean;
53
- /** The keyboard acts on this selection (Delete, Ctrl+Q, Ctrl+U, Insert - registered by the page): Mark read, Mark unread, Flag and
54
- * Delete name their shortcut in the tooltip and `aria-keyshortcuts`. */
55
- shortcuts?: boolean;
56
- }
57
-
58
- function actionClassName(): string {
59
- return "px-2 py-1 rounded-md text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent";
60
- }
61
-
62
- /**
63
- * The header that replaces the list's own toolbar while select mode is on: how many rows are selected,
64
- * select-all/clear, and the bulk actions Outlook offers for a multi-selection - mark read/unread, flag,
65
- * archive, move, report junk and delete.
66
- *
67
- * "Delete" means *move to Deleted Items*, never a permanent erase: there is no bulk permanent-delete API,
68
- * and the collection-level DELETE truncates the folder rather than removing a selection. An action whose
69
- * target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
70
- * of actions doesn't shift around between folders.
71
- */
72
- export default function MailSelectionBar({
73
- selected,
74
- listed,
75
- totals,
76
- onSelectAll,
77
- onClearSelection,
78
- onCancel,
79
- folders,
80
- currentFolderUid,
81
- labels,
82
- mailboxUid,
83
- onLabelCreated,
84
- onFolderCreated,
85
- onApplyLabels,
86
- onSetRead,
87
- onSetFlagged,
88
- onArchive,
89
- onMoveTo,
90
- onReportJunk,
91
- onDelete,
92
- busy,
93
- shortcuts,
94
- }: MailSelectionBarProps) {
95
- const env = useKeyEnvironment();
96
- /** `title` and `aria-keyshortcuts` for an action the keyboard also does; `reason` (why it is disabled) wins the tooltip. */
97
- const hint = (label: string, shortcut: ShortcutDef, reason?: string) =>
98
- shortcuts
99
- ? { title: reason ?? withHint(label, shortcut, env), "aria-keyshortcuts": ariaKeyShortcuts(shortcut, env) }
100
- : { title: reason };
101
- const selectedCount = totals?.selected ?? selected.length;
102
- const listedCount = totals?.listed ?? listed.length;
103
- // Counted on the rows that were ticked, but *emptied* on the messages: a ticked conversation whose
104
- // messages are still being fetched has nothing for an action to act on yet.
105
- const none = selectedCount === 0 || selected.length === 0;
106
- const allSelected = listedCount > 0 && selectedCount === listedCount;
107
- const currentType = folders.find((folder) => folder.uid === currentFolderUid)?.type;
108
- const moveTargets = folders.filter((folder) => MOVE_TARGET_TYPES.has(folder.type) && folder.uid !== currentFolderUid);
109
- // The same prompt the reading pane's own Move to opens, so one list of destinations and one way to
110
- // create a folder serve both.
111
- const [movePrompt, setMovePrompt] = useState(false);
112
-
113
- // None of the three needs its folder to exist first - the caller creates or lazily provisions it (see
114
- // `resolveFolderOfType()` and `bulkArchive()` in `apps/www/index.tsx`) - so the only thing that disables
115
- // them is already being in the folder they would move to.
116
- const archiveReason = currentType === "archive" ? "These messages are already in Archive" : undefined;
117
- const junkReason = currentType === "junk" ? "These messages are already in Junk" : undefined;
118
- const deleteReason = currentType === "deleted_items" ? "These messages are already in Deleted Items" : undefined;
119
-
120
- // A label every selected message already carries starts ticked; one only some of them carry starts
121
- // partially applied, and is left exactly as it is unless the reader touches that row.
122
- const appliedToAll = labels.filter((label) => !none && selected.every((m) => m.labelUids?.includes(label.uid)));
123
- const appliedToSome = labels.filter(
124
- (label) => selected.some((m) => m.labelUids?.includes(label.uid)) && !appliedToAll.includes(label),
125
- );
126
-
127
-
128
- return (
129
- <div className="border-b border-border">
130
- <MoveToFolderDialog
131
- open={movePrompt}
132
- onClose={() => setMovePrompt(false)}
133
- mailboxUid={mailboxUid}
134
- folders={folders}
135
- currentFolderUid={currentFolderUid}
136
- count={selected.length}
137
- onMove={onMoveTo}
138
- onFolderCreated={onFolderCreated}
139
- />
140
- <div className="flex items-center gap-2 px-3 py-1.5 bg-surface-alt">
141
- <span aria-live="polite" className="text-sm font-semibold text-text">
142
- {selectedCount}
143
- {totals ? ` ${totals.noun}${selectedCount === 1 ? "" : "s"}` : ""} selected
144
- </span>
145
- <button type="button" onClick={onSelectAll} disabled={allSelected || listedCount === 0} className={actionClassName()}>
146
- Select all
147
- </button>
148
- <button type="button" onClick={onClearSelection} disabled={selectedCount === 0} className={actionClassName()}>
149
- Clear
150
- </button>
151
- <button type="button" onClick={onCancel} className={`${actionClassName()} ml-auto`}>
152
- Cancel
153
- </button>
154
- </div>
155
- <div className="flex flex-wrap items-center gap-0.5 px-1.5 py-1">
156
- <button type="button" onClick={() => onSetRead(true)} disabled={none || busy} className={actionClassName()} {...hint("Mark read", SHORTCUTS.mail.markRead)}>
157
- Mark read
158
- </button>
159
- <button type="button" onClick={() => onSetRead(false)} disabled={none || busy} className={actionClassName()} {...hint("Mark unread", SHORTCUTS.mail.markUnread)}>
160
- Mark unread
161
- </button>
162
- <button type="button" onClick={() => onSetFlagged(true)} disabled={none || busy} className={actionClassName()} {...hint("Flag", SHORTCUTS.mail.flag)}>
163
- Flag
164
- </button>
165
- <button type="button" onClick={() => onSetFlagged(false)} disabled={none || busy} className={actionClassName()}>
166
- Unflag
167
- </button>
168
- <button
169
- type="button"
170
- onClick={onArchive}
171
- disabled={none || busy || !!archiveReason}
172
- title={archiveReason}
173
- className={actionClassName()}
174
- >
175
- Archive
176
- </button>
177
- <LabelMenuButton
178
- aria-label="Apply label"
179
- label="Apply label"
180
- className="text-sm"
181
- labels={labels}
182
- mailboxUid={mailboxUid}
183
- onLabelCreated={onLabelCreated}
184
- applied={appliedToAll.map((label) => label.uid)}
185
- partial={appliedToSome.map((label) => label.uid)}
186
- onCommit={onApplyLabels}
187
- busy={busy}
188
- disabled={none || busy}
189
- note={
190
- selected.length === 1
191
- ? "Ticked labels are applied, unticked ones removed."
192
- : "Ticked labels are applied to every selected message and unticked ones removed from all of them; a dash means only some have that label, and leaving it alone keeps it that way."
193
- }
194
- emptyNote="This mailbox has no labels yet."
195
- commit={{ label: "Apply" }}
196
- clear={{ label: "Remove all labels" }}
197
- />
198
- <button
199
- type="button"
200
- onClick={() => setMovePrompt(true)}
201
- disabled={none || busy}
202
- className={actionClassName()}
203
- >
204
- Move to
205
- </button>
206
- <button
207
- type="button"
208
- onClick={onReportJunk}
209
- disabled={none || busy || !!junkReason}
210
- title={junkReason}
211
- className={actionClassName()}
212
- >
213
- Report junk
214
- </button>
215
- <button
216
- type="button"
217
- onClick={onDelete}
218
- disabled={none || busy || !!deleteReason}
219
- className={actionClassName()}
220
- {...hint("Delete", SHORTCUTS.mail.delete, deleteReason)}
221
- >
222
- Delete
223
- </button>
224
- </div>
225
- </div>
226
- );
227
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { useState } from "react";
6
+ import { Folder, Message } from "@rapidmx/react-shared/mail/mailApi.js";
7
+ import { Label } from "@rapidmx/react-shared/mail/labelsApi.js";
8
+ import LabelMenuButton from "./labelMenu.js";
9
+ import MoveToFolderDialog, { MOVE_TARGET_TYPES } from "./MoveToFolderDialog.js";
10
+ import { ariaKeyShortcuts, withHint } from "../../keyboard/format.js";
11
+ import { SHORTCUTS, ShortcutDef } from "../../keyboard/keymap.js";
12
+ import { useKeyEnvironment } from "../../keyboard/ShortcutProvider.js";
13
+
14
+ export interface MailSelectionBarProps {
15
+ selected: Message[];
16
+ /** Every message currently listed - what "Select all" selects and what `allSelected` is measured against. */
17
+ listed: Message[];
18
+ /** What the bar *counts*, when the rows being ticked aren't messages themselves: the conversation list
19
+ * ticks conversations, each standing for several messages, so "3 conversations selected" is what the
20
+ * reader picked while `selected` stays the messages every action below acts on. Left out by the flat
21
+ * message list, which counts `selected`/`listed` directly. */
22
+ totals?: { selected: number; listed: number; noun: string };
23
+ onSelectAll: () => void;
24
+ onClearSelection: () => void;
25
+ /** Leaves select mode entirely (and clears the selection). */
26
+ onCancel: () => void;
27
+ folders: Folder[];
28
+ currentFolderUid?: string;
29
+ /** Every label this mailbox has, for the Apply label action. */
30
+ labels: Label[];
31
+ /** The mailbox a label created from Apply label belongs to. */
32
+ mailboxUid: string;
33
+ onLabelCreated: (label: Label) => void;
34
+ /** A folder created from the Move to prompt, for the caller's own folder list and the sidebar. */
35
+ onFolderCreated: (folder: Folder) => void;
36
+ /** Sets the selection's labels: every message ends up with `labelUids`, plus whichever of
37
+ * `keepPartial` it already had (those rows were left partially applied, so each message keeps what
38
+ * it has). */
39
+ onApplyLabels: (labelUids: string[], keepPartial: string[]) => void;
40
+ onSetRead: (read: boolean) => void;
41
+ onSetFlagged: (flagged: boolean) => void;
42
+ onArchive: () => void;
43
+ /** Moves the selection into `folderUid`, resolving once the bulk update has settled. It resolves
44
+ * whether or not that update was rejected: a bulk update is applied element by element, so a failure
45
+ * is explained by a pop-up (see `notifyApiError()`), raised by the page along with the reload it triggers, rather than in the
46
+ * prompt, which would be claiming the move simply didn't happen. */
47
+ onMoveTo: (folderUid: string) => Promise<void>;
48
+ onReportJunk: () => void;
49
+ /** Delete: moves the selection to Deleted Items - or, when `deletesPermanently`, permanently deletes it (the page asks first). */
50
+ onDelete: () => void;
51
+ /** The selection is in Deleted Items, so Delete is the permanent one: the button says "Delete permanently" and is styled as the destructive
52
+ * action it is. The page decides (a search over several mailboxes lists messages from more than one folder, so the folder being viewed is not
53
+ * always the answer). */
54
+ deletesPermanently?: boolean;
55
+ /** A bulk action is in flight - every action is held until it settles, since the next one would send
56
+ * `version`s the first has already superseded. */
57
+ busy: boolean;
58
+ /** The keyboard acts on this selection (Delete, Ctrl+Q, Ctrl+U, Insert - registered by the page): Mark read, Mark unread, Flag and
59
+ * Delete name their shortcut in the tooltip and `aria-keyshortcuts`. */
60
+ shortcuts?: boolean;
61
+ /** Why Move to is unavailable, when the selection is spread over mailboxes and a folder can only belong to one (search results over several
62
+ * mailboxes). Shown as the button's tooltip. */
63
+ moveDisabledReason?: string;
64
+ /** The same for Apply label: a label belongs to one mailbox, and `labels` are one mailbox's. */
65
+ labelsDisabledReason?: string;
66
+ }
67
+
68
+ function actionClassName(destructive = false): string {
69
+ return `px-2 py-1 rounded-md text-sm ${destructive ? "text-danger font-semibold" : "text-text"} hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent`;
70
+ }
71
+
72
+ /**
73
+ * The header that replaces the list's own toolbar while select mode is on: how many rows are selected,
74
+ * select-all/clear, and the bulk actions Outlook offers for a multi-selection - mark read/unread, flag,
75
+ * archive, move, report junk and delete.
76
+ *
77
+ * "Delete" means *move to Deleted Items* - except on messages that are already in Deleted Items (`deletesPermanently`),
78
+ * where it is "Delete permanently": the page confirms, then erases them for good, as Outlook's Delete does there. An
79
+ * action whose target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
80
+ * of actions doesn't shift around between folders.
81
+ */
82
+ export default function MailSelectionBar({
83
+ selected,
84
+ listed,
85
+ totals,
86
+ onSelectAll,
87
+ onClearSelection,
88
+ onCancel,
89
+ folders,
90
+ currentFolderUid,
91
+ labels,
92
+ mailboxUid,
93
+ onLabelCreated,
94
+ onFolderCreated,
95
+ onApplyLabels,
96
+ onSetRead,
97
+ onSetFlagged,
98
+ onArchive,
99
+ onMoveTo,
100
+ onReportJunk,
101
+ onDelete,
102
+ deletesPermanently = false,
103
+ busy,
104
+ shortcuts,
105
+ moveDisabledReason,
106
+ labelsDisabledReason,
107
+ }: MailSelectionBarProps) {
108
+ const env = useKeyEnvironment();
109
+ /** `title` and `aria-keyshortcuts` for an action the keyboard also does. */
110
+ const hint = (label: string, shortcut: ShortcutDef) =>
111
+ shortcuts ? { title: withHint(label, shortcut, env), "aria-keyshortcuts": ariaKeyShortcuts(shortcut, env) } : {};
112
+ const selectedCount = totals?.selected ?? selected.length;
113
+ const listedCount = totals?.listed ?? listed.length;
114
+ // Counted on the rows that were ticked, but *emptied* on the messages: a ticked conversation whose
115
+ // messages are still being fetched has nothing for an action to act on yet.
116
+ const none = selectedCount === 0 || selected.length === 0;
117
+ const allSelected = listedCount > 0 && selectedCount === listedCount;
118
+ const currentType = folders.find((folder) => folder.uid === currentFolderUid)?.type;
119
+ const moveTargets = folders.filter((folder) => MOVE_TARGET_TYPES.has(folder.type) && folder.uid !== currentFolderUid);
120
+ // The same prompt the reading pane's own Move to opens, so one list of destinations and one way to
121
+ // create a folder serve both.
122
+ const [movePrompt, setMovePrompt] = useState(false);
123
+
124
+ // None of the three needs its folder to exist first - the caller creates or lazily provisions it (see
125
+ // `resolveFolderOfType()` and `bulkArchive()` in `apps/www/index.tsx`) - so the only thing that disables
126
+ // them is already being in the folder they would move to.
127
+ const archiveReason = currentType === "archive" ? "These messages are already in Archive" : undefined;
128
+ const junkReason = currentType === "junk" ? "These messages are already in Junk" : undefined;
129
+ const deleteLabel = deletesPermanently ? "Delete permanently" : "Delete";
130
+
131
+ // A label every selected message already carries starts ticked; one only some of them carry starts
132
+ // partially applied, and is left exactly as it is unless the reader touches that row.
133
+ const appliedToAll = labels.filter((label) => !none && selected.every((m) => m.labelUids?.includes(label.uid)));
134
+ const appliedToSome = labels.filter(
135
+ (label) => selected.some((m) => m.labelUids?.includes(label.uid)) && !appliedToAll.includes(label),
136
+ );
137
+
138
+
139
+ return (
140
+ <div className="border-b border-border">
141
+ <MoveToFolderDialog
142
+ open={movePrompt}
143
+ onClose={() => setMovePrompt(false)}
144
+ mailboxUid={mailboxUid}
145
+ folders={folders}
146
+ currentFolderUid={currentFolderUid}
147
+ count={selected.length}
148
+ onMove={onMoveTo}
149
+ onFolderCreated={onFolderCreated}
150
+ />
151
+ <div className="flex items-center gap-2 px-3 py-1.5 bg-surface-alt">
152
+ <span aria-live="polite" className="text-sm font-semibold text-text">
153
+ {selectedCount}
154
+ {totals ? ` ${totals.noun}${selectedCount === 1 ? "" : "s"}` : ""} selected
155
+ </span>
156
+ <button type="button" onClick={onSelectAll} disabled={allSelected || listedCount === 0} className={actionClassName()}>
157
+ Select all
158
+ </button>
159
+ <button type="button" onClick={onClearSelection} disabled={selectedCount === 0} className={actionClassName()}>
160
+ Clear
161
+ </button>
162
+ <button type="button" onClick={onCancel} className={`${actionClassName()} ml-auto`}>
163
+ Cancel
164
+ </button>
165
+ </div>
166
+ <div className="flex flex-wrap items-center gap-0.5 px-1.5 py-1">
167
+ <button type="button" onClick={() => onSetRead(true)} disabled={none || busy} className={actionClassName()} {...hint("Mark read", SHORTCUTS.mail.markRead)}>
168
+ Mark read
169
+ </button>
170
+ <button type="button" onClick={() => onSetRead(false)} disabled={none || busy} className={actionClassName()} {...hint("Mark unread", SHORTCUTS.mail.markUnread)}>
171
+ Mark unread
172
+ </button>
173
+ <button type="button" onClick={() => onSetFlagged(true)} disabled={none || busy} className={actionClassName()} {...hint("Flag", SHORTCUTS.mail.flag)}>
174
+ Flag
175
+ </button>
176
+ <button type="button" onClick={() => onSetFlagged(false)} disabled={none || busy} className={actionClassName()}>
177
+ Unflag
178
+ </button>
179
+ <button
180
+ type="button"
181
+ onClick={onArchive}
182
+ disabled={none || busy || !!archiveReason}
183
+ title={archiveReason}
184
+ className={actionClassName()}
185
+ >
186
+ Archive
187
+ </button>
188
+ <LabelMenuButton
189
+ aria-label="Apply label"
190
+ label="Apply label"
191
+ className="text-sm"
192
+ labels={labels}
193
+ mailboxUid={mailboxUid}
194
+ onLabelCreated={onLabelCreated}
195
+ applied={appliedToAll.map((label) => label.uid)}
196
+ partial={appliedToSome.map((label) => label.uid)}
197
+ onCommit={onApplyLabels}
198
+ busy={busy}
199
+ disabled={none || busy || !!labelsDisabledReason}
200
+ title={labelsDisabledReason}
201
+ note={
202
+ selected.length === 1
203
+ ? "Ticked labels are applied, unticked ones removed."
204
+ : "Ticked labels are applied to every selected message and unticked ones removed from all of them; a dash means only some have that label, and leaving it alone keeps it that way."
205
+ }
206
+ emptyNote="This mailbox has no labels yet."
207
+ commit={{ label: "Apply" }}
208
+ clear={{ label: "Remove all labels" }}
209
+ />
210
+ <button
211
+ type="button"
212
+ onClick={() => setMovePrompt(true)}
213
+ disabled={none || busy || !!moveDisabledReason}
214
+ title={moveDisabledReason}
215
+ className={actionClassName()}
216
+ >
217
+ Move to
218
+ </button>
219
+ <button
220
+ type="button"
221
+ onClick={onReportJunk}
222
+ disabled={none || busy || !!junkReason}
223
+ title={junkReason}
224
+ className={actionClassName()}
225
+ >
226
+ Report junk
227
+ </button>
228
+ <button
229
+ type="button"
230
+ onClick={onDelete}
231
+ disabled={none || busy}
232
+ className={actionClassName(deletesPermanently)}
233
+ {...hint(deleteLabel, SHORTCUTS.mail.delete)}
234
+ >
235
+ {deleteLabel}
236
+ </button>
237
+ </div>
238
+ </div>
239
+ );
240
+ }
@@ -19,6 +19,10 @@ interface MenuItemBase {
19
19
  checked?: boolean | "mixed";
20
20
  /** A colour swatch before the label - a label's own colour. */
21
21
  swatchColor?: string;
22
+ /** An icon before the label, for a menu whose rows are commands (the reading pane's "More actions"). */
23
+ icon?: ReactNode;
24
+ /** A tooltip for the row - the full text of a label the row truncates. */
25
+ title?: string;
22
26
  disabled?: boolean;
23
27
  /** Leaves the menu open after choosing this row, for a multi-select list where several rows are ticked
24
28
  * before one command commits them all. */
@@ -97,8 +101,11 @@ export function menuHeight(sections: MenuSectionSpec[], width: number = DEFAULT_
97
101
  }
98
102
 
99
103
  export interface MenuButtonProps {
100
- /** The trigger's visible label. */
104
+ /** The trigger's visible label. Not drawn (nor is the chevron) with `iconOnly`, where `aria-label` is all the trigger says. */
101
105
  label: ReactNode;
106
+ /** The trigger is just its `icon` - a round icon button like the reading pane's own command row, whose `className` it takes over
107
+ * completely. `aria-label` (and `title`) name it. */
108
+ iconOnly?: boolean;
102
109
  /** The trigger's accessible name, which also names the menu itself - includes the current selection
103
110
  * where there is one ("Filter: Unread"), so the button says what it's set to, not just what it does. */
104
111
  "aria-label": string;
@@ -127,6 +134,7 @@ export interface MenuButtonProps {
127
134
  */
128
135
  export default function MenuButton({
129
136
  label,
137
+ iconOnly = false,
130
138
  icon,
131
139
  disabled,
132
140
  title,
@@ -190,8 +198,10 @@ export default function MenuButton({
190
198
 
191
199
  function enterSubmenu(item: MenuSubmenuSpec) {
192
200
  setSubmenuKey(item.key);
193
- // Past the Back row, onto the first row of the submenu itself.
194
- setActiveIndex(1 + initialActiveIndex(item.submenu.flatMap((section) => section.items)));
201
+ const rows = item.submenu.flatMap((section) => section.items);
202
+ // Past the Back row, onto the first row of the submenu itself - or, when none of them can take focus (every row is unavailable right now), onto
203
+ // the Back row, so the keyboard still has a place to be.
204
+ setActiveIndex(rows.some((row) => !row.disabled) ? 1 + initialActiveIndex(rows) : 0);
195
205
  }
196
206
 
197
207
  function leaveSubmenu() {
@@ -285,16 +295,20 @@ export default function MenuButton({
285
295
  disabled={disabled}
286
296
  onClick={() => (open ? closeMenu(false) : openMenu())}
287
297
  onKeyDown={handleTriggerKeyDown}
288
- className={[
289
- // `min-w-0` so a long label ("Filter: Has attachments") truncates instead of pushing
290
- // whatever sits beside it in the toolbar off the edge of a 384px list column.
291
- "inline-flex min-w-0 items-center gap-1 px-2 py-1 rounded-md text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent",
292
- className,
293
- ].join(" ")}
298
+ className={
299
+ iconOnly
300
+ ? className
301
+ : [
302
+ // `min-w-0` so a long label ("Filter: Has attachments") truncates instead of pushing
303
+ // whatever sits beside it in the toolbar off the edge of a 384px list column.
304
+ "inline-flex min-w-0 items-center gap-1 px-2 py-1 rounded-md text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent",
305
+ className,
306
+ ].join(" ")
307
+ }
294
308
  >
295
309
  {icon}
296
- <span className="truncate">{label}</span>
297
- <HiChevronDown size={14} aria-hidden="true" className="shrink-0 text-text-muted" />
310
+ {!iconOnly && <span className="truncate">{label}</span>}
311
+ {!iconOnly && <HiChevronDown size={14} aria-hidden="true" className="shrink-0 text-text-muted" />}
298
312
  </button>
299
313
  {open && (
300
314
  <PopoverPortal
@@ -332,6 +346,7 @@ export default function MenuButton({
332
346
  type="button"
333
347
  role={role}
334
348
  disabled={item.disabled}
349
+ title={item.title}
335
350
  tabIndex={index === activeIndex ? 0 : -1}
336
351
  {...(role === "menuitem"
337
352
  ? {}
@@ -352,6 +367,11 @@ export default function MenuButton({
352
367
  </span>
353
368
  <span className="min-w-0 flex-1">
354
369
  <span className="flex h-5 items-center gap-1.5">
370
+ {item.icon && (
371
+ <span aria-hidden="true" className="shrink-0 text-text-muted">
372
+ {item.icon}
373
+ </span>
374
+ )}
355
375
  {item.swatchColor && (
356
376
  <span
357
377
  aria-hidden="true"