@rapidmx/web-client 0.14.1 → 0.16.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 (314) hide show
  1. package/README.md +373 -373
  2. package/apps/admin/branding/index.tsx +39 -39
  3. package/apps/admin/data-requests/index.tsx +482 -480
  4. package/apps/admin/domains/[uid].tsx +164 -163
  5. package/apps/admin/escrow-scopes/[uid].tsx +348 -348
  6. package/apps/admin/index.tsx +127 -124
  7. package/apps/admin/mailboxes/[uid].tsx +269 -224
  8. package/apps/admin/mailboxes/new/index.tsx +28 -28
  9. package/apps/admin/plugins/index.tsx +15 -15
  10. package/apps/admin/retention-policy/index.tsx +39 -39
  11. package/apps/admin/signing-certificates/index.tsx +343 -343
  12. package/apps/escrow/audit-log/index.tsx +196 -196
  13. package/apps/escrow/matters/[uid].tsx +621 -621
  14. package/apps/shared/appearance/AppearanceForm.tsx +9 -1
  15. package/apps/shared/auth/adminAccess.ts +99 -99
  16. package/apps/shared/calendar/bookingPlugin.ts +100 -0
  17. package/apps/shared/calendar/calendarLiveUpdates.ts +27 -0
  18. package/apps/shared/components/admin/layout/AdminShell.tsx +378 -346
  19. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
  20. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
  21. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
  22. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  23. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  24. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +53 -4
  25. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  26. package/apps/shared/components/admin/settings/PluginsManager.tsx +64 -10
  27. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  28. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  29. package/apps/shared/components/admin/setup/SetupWizard.tsx +444 -444
  30. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  31. package/apps/shared/components/calendar/AppointmentQuickForm.tsx +327 -0
  32. package/apps/shared/components/calendar/DescriptionEditor.tsx +274 -0
  33. package/apps/shared/components/calendar/EventDescriptionView.tsx +70 -0
  34. package/apps/shared/components/calendar/EventDetails.tsx +401 -0
  35. package/apps/shared/components/calendar/EventEditor.tsx +635 -0
  36. package/apps/shared/components/calendar/EventExpandedForm.tsx +210 -0
  37. package/apps/shared/components/calendar/EventFormParts.tsx +591 -0
  38. package/apps/shared/components/calendar/EventModal.tsx +206 -946
  39. package/apps/shared/components/calendar/EventQuickForm.tsx +116 -0
  40. package/apps/shared/components/calendar/EventShell.tsx +202 -0
  41. package/apps/shared/components/calendar/FindATime.tsx +379 -0
  42. package/apps/shared/components/calendar/GuestInput.tsx +100 -0
  43. package/apps/shared/components/calendar/LazyDescriptionEditor.tsx +70 -0
  44. package/apps/shared/components/calendar/MonthView.tsx +185 -173
  45. package/apps/shared/components/calendar/QuickCreateFaces.tsx +87 -0
  46. package/apps/shared/components/calendar/QuickCreateTabs.tsx +81 -0
  47. package/apps/shared/components/calendar/QuickFaceFrame.tsx +81 -0
  48. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -222
  49. package/apps/shared/components/calendar/RequestChangeForm.tsx +240 -0
  50. package/apps/shared/components/calendar/SplitDayView.tsx +144 -143
  51. package/apps/shared/components/calendar/TaskQuickForm.tsx +289 -0
  52. package/apps/shared/components/calendar/TimeGridView.tsx +246 -244
  53. package/apps/shared/components/calendar/allDay.ts +124 -124
  54. package/apps/shared/components/calendar/eventDialogFields.ts +86 -0
  55. package/apps/shared/components/calendar/eventForm.ts +108 -0
  56. package/apps/shared/components/calendar/eventFormat.ts +210 -0
  57. package/apps/shared/components/calendar/findTimeGrid.ts +90 -0
  58. package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
  59. package/apps/shared/components/calendar/swipeNavigation.ts +14 -0
  60. package/apps/shared/components/contacts/ContactForm.tsx +383 -383
  61. package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
  62. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
  63. package/apps/shared/components/contacts/layout/ContactsShell.tsx +217 -216
  64. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  65. package/apps/shared/components/layout/AppShell.tsx +480 -480
  66. package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
  67. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  68. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
  69. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  70. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  71. package/apps/shared/components/layout/UserMenu.tsx +439 -372
  72. package/apps/shared/components/mail/ConversationList.tsx +323 -271
  73. package/apps/shared/components/mail/ConversationThreadPane.tsx +130 -5
  74. package/apps/shared/components/mail/InviteCard.tsx +387 -0
  75. package/apps/shared/components/mail/MailSelectionBar.tsx +236 -227
  76. package/apps/shared/components/mail/MessageDetailPane.tsx +1559 -1537
  77. package/apps/shared/components/mail/SwipeRow.tsx +120 -0
  78. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
  79. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  80. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1673
  81. package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
  82. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  83. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  84. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  85. package/apps/shared/components/mail/invite/InviteRowChip.tsx +104 -0
  86. package/apps/shared/components/mail/invite/InviteRsvpPopover.tsx +271 -0
  87. package/apps/shared/components/mail/invite/InviteTimeline.tsx +200 -0
  88. package/apps/shared/components/mail/invite/ProposeTimeForm.tsx +109 -0
  89. package/apps/shared/components/mail/invite/inviteFormat.ts +110 -0
  90. package/apps/shared/components/mail/invite/inviteStore.ts +114 -0
  91. package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
  92. package/apps/shared/components/mail/layout/MailShell.tsx +546 -490
  93. package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
  94. package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
  95. package/apps/shared/components/mail/verificationSeals.ts +125 -125
  96. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  97. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  98. package/apps/shared/components/settings/layout/SettingsShell.tsx +255 -253
  99. package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
  100. package/apps/shared/components/tasks/layout/TasksShell.tsx +218 -217
  101. package/apps/shared/gestures/useEnterSlide.ts +54 -0
  102. package/apps/shared/gestures/useSwipe.ts +114 -0
  103. package/apps/shared/gestures/useSwipeSlide.ts +113 -0
  104. package/apps/shared/gestures/useWheelPaging.ts +107 -0
  105. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  106. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  107. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  108. package/apps/shared/keyboard/dispatch.ts +124 -124
  109. package/apps/shared/keyboard/format.ts +89 -89
  110. package/apps/shared/keyboard/keymap.ts +114 -114
  111. package/apps/shared/keyboard/registry.ts +65 -65
  112. package/apps/shared/keyboard/targets.ts +79 -79
  113. package/apps/shared/mail/folderCounts.ts +17 -0
  114. package/apps/shared/mail/folderTree.ts +143 -143
  115. package/apps/shared/mail/listAllPages.ts +39 -39
  116. package/apps/shared/mail/newMailNotifications.ts +3 -15
  117. package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
  118. package/apps/shared/mail/outbox/sendJob.ts +19 -1
  119. package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
  120. package/apps/shared/mail/primaryMailbox.ts +71 -0
  121. package/apps/shared/mail/useCollapsedSections.ts +146 -0
  122. package/apps/shared/mail/useMailConnection.ts +205 -203
  123. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  124. package/apps/shared/mail/useMarkMessageRead.ts +47 -47
  125. package/apps/shared/mail/useNewMailNotifications.ts +2 -2
  126. package/apps/shared/navigation/AppRouter.tsx +300 -300
  127. package/apps/shared/navigation/routerContext.tsx +83 -83
  128. package/apps/shared/notifications/preferences.ts +36 -0
  129. package/apps/shared/notifications/store.ts +9 -0
  130. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
  131. package/apps/shared/search/crossMailboxSearch.ts +169 -0
  132. package/apps/shared/search/localIndexBuilder.ts +481 -481
  133. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  134. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  135. package/apps/shared/signing/enrollmentView.ts +251 -251
  136. package/apps/shared/signing/useNow.ts +19 -19
  137. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
  138. package/apps/shared/styles/app.css +396 -386
  139. package/apps/www/_routes.ts +1 -0
  140. package/apps/www/calendar/index.tsx +578 -469
  141. package/apps/www/contacts/[uid].tsx +109 -109
  142. package/apps/www/contacts/index.tsx +6 -0
  143. package/apps/www/index.tsx +2847 -2332
  144. package/apps/www/messages/[uid].tsx +135 -108
  145. package/apps/www/settings/auto-reply/index.tsx +133 -133
  146. package/apps/www/settings/encryption/index.tsx +1287 -1287
  147. package/apps/www/settings/filters/[uid].tsx +176 -176
  148. package/apps/www/settings/filters/index.tsx +102 -102
  149. package/apps/www/settings/filters/new/index.tsx +140 -140
  150. package/apps/www/settings/labels/index.tsx +204 -204
  151. package/apps/www/settings/privacy/index.tsx +492 -492
  152. package/apps/www/settings/profile/index.tsx +248 -0
  153. package/apps/www/settings/read-receipts/index.tsx +147 -147
  154. package/apps/www/settings/sharing/index.tsx +256 -256
  155. package/apps/www/settings/signatures/[uid].tsx +172 -172
  156. package/apps/www/settings/signatures/index.tsx +88 -88
  157. package/apps/www/settings/signatures/new/index.tsx +135 -135
  158. package/apps/www/tasks/index.tsx +649 -649
  159. package/dist/apps/admin/data-requests/index.js +1 -1
  160. package/dist/apps/admin/domains/[uid].js +4 -2
  161. package/dist/apps/admin/index.js +2 -1
  162. package/dist/apps/admin/mailboxes/[uid].js +12 -2
  163. package/dist/apps/shared/appearance/AppearanceForm.js +9 -1
  164. package/dist/apps/shared/calendar/bookingPlugin.d.ts +50 -0
  165. package/dist/apps/shared/calendar/bookingPlugin.js +57 -0
  166. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -0
  167. package/dist/apps/shared/calendar/calendarLiveUpdates.js +23 -0
  168. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  169. package/dist/apps/shared/components/admin/layout/AdminShell.js +7 -3
  170. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
  171. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
  172. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
  173. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
  174. package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
  175. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +28 -4
  176. package/dist/apps/shared/components/admin/settings/PluginsManager.js +40 -6
  177. package/dist/apps/shared/components/calendar/AppointmentQuickForm.d.ts +47 -0
  178. package/dist/apps/shared/components/calendar/AppointmentQuickForm.js +135 -0
  179. package/dist/apps/shared/components/calendar/DescriptionEditor.d.ts +26 -0
  180. package/dist/apps/shared/components/calendar/DescriptionEditor.js +192 -0
  181. package/dist/apps/shared/components/calendar/EventDescriptionView.d.ts +14 -0
  182. package/dist/apps/shared/components/calendar/EventDescriptionView.js +39 -0
  183. package/dist/apps/shared/components/calendar/EventDetails.d.ts +33 -0
  184. package/dist/apps/shared/components/calendar/EventDetails.js +137 -0
  185. package/dist/apps/shared/components/calendar/EventEditor.d.ts +59 -0
  186. package/dist/apps/shared/components/calendar/EventEditor.js +544 -0
  187. package/dist/apps/shared/components/calendar/EventExpandedForm.d.ts +14 -0
  188. package/dist/apps/shared/components/calendar/EventExpandedForm.js +44 -0
  189. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +70 -0
  190. package/dist/apps/shared/components/calendar/EventFormParts.js +191 -0
  191. package/dist/apps/shared/components/calendar/EventModal.d.ts +26 -13
  192. package/dist/apps/shared/components/calendar/EventModal.js +57 -459
  193. package/dist/apps/shared/components/calendar/EventQuickForm.d.ts +18 -0
  194. package/dist/apps/shared/components/calendar/EventQuickForm.js +25 -0
  195. package/dist/apps/shared/components/calendar/EventShell.d.ts +64 -0
  196. package/dist/apps/shared/components/calendar/EventShell.js +134 -0
  197. package/dist/apps/shared/components/calendar/FindATime.d.ts +18 -0
  198. package/dist/apps/shared/components/calendar/FindATime.js +190 -0
  199. package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
  200. package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
  201. package/dist/apps/shared/components/calendar/LazyDescriptionEditor.d.ts +8 -0
  202. package/dist/apps/shared/components/calendar/LazyDescriptionEditor.js +44 -0
  203. package/dist/apps/shared/components/calendar/MonthView.d.ts +5 -1
  204. package/dist/apps/shared/components/calendar/MonthView.js +11 -5
  205. package/dist/apps/shared/components/calendar/QuickCreateFaces.d.ts +30 -0
  206. package/dist/apps/shared/components/calendar/QuickCreateFaces.js +44 -0
  207. package/dist/apps/shared/components/calendar/QuickCreateTabs.d.ts +24 -0
  208. package/dist/apps/shared/components/calendar/QuickCreateTabs.js +46 -0
  209. package/dist/apps/shared/components/calendar/QuickFaceFrame.d.ts +25 -0
  210. package/dist/apps/shared/components/calendar/QuickFaceFrame.js +19 -0
  211. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  212. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +2 -2
  213. package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +36 -0
  214. package/dist/apps/shared/components/calendar/RequestChangeForm.js +127 -0
  215. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +3 -2
  216. package/dist/apps/shared/components/calendar/SplitDayView.js +3 -2
  217. package/dist/apps/shared/components/calendar/TaskQuickForm.d.ts +40 -0
  218. package/dist/apps/shared/components/calendar/TaskQuickForm.js +101 -0
  219. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +4 -2
  220. package/dist/apps/shared/components/calendar/TimeGridView.js +3 -2
  221. package/dist/apps/shared/components/calendar/eventDialogFields.d.ts +28 -0
  222. package/dist/apps/shared/components/calendar/eventDialogFields.js +59 -0
  223. package/dist/apps/shared/components/calendar/eventForm.d.ts +98 -0
  224. package/dist/apps/shared/components/calendar/eventForm.js +1 -0
  225. package/dist/apps/shared/components/calendar/eventFormat.d.ts +80 -0
  226. package/dist/apps/shared/components/calendar/eventFormat.js +190 -0
  227. package/dist/apps/shared/components/calendar/findTimeGrid.d.ts +30 -0
  228. package/dist/apps/shared/components/calendar/findTimeGrid.js +83 -0
  229. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -2
  230. package/dist/apps/shared/components/calendar/swipeNavigation.d.ts +9 -0
  231. package/dist/apps/shared/components/calendar/swipeNavigation.js +8 -0
  232. package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
  233. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  234. package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
  235. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -3
  236. package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
  237. package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
  238. package/dist/apps/shared/components/layout/UserMenu.d.ts +14 -9
  239. package/dist/apps/shared/components/layout/UserMenu.js +74 -26
  240. package/dist/apps/shared/components/mail/ConversationList.d.ts +12 -1
  241. package/dist/apps/shared/components/mail/ConversationList.js +21 -4
  242. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
  243. package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
  244. package/dist/apps/shared/components/mail/InviteCard.d.ts +20 -0
  245. package/dist/apps/shared/components/mail/InviteCard.js +150 -0
  246. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -1
  247. package/dist/apps/shared/components/mail/MailSelectionBar.js +3 -3
  248. package/dist/apps/shared/components/mail/MessageDetailPane.js +13 -2
  249. package/dist/apps/shared/components/mail/SwipeRow.d.ts +22 -0
  250. package/dist/apps/shared/components/mail/SwipeRow.js +71 -0
  251. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +16 -7
  252. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
  253. package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
  254. package/dist/apps/shared/components/mail/invite/InviteRowChip.d.ts +23 -0
  255. package/dist/apps/shared/components/mail/invite/InviteRowChip.js +46 -0
  256. package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.d.ts +20 -0
  257. package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.js +141 -0
  258. package/dist/apps/shared/components/mail/invite/InviteTimeline.d.ts +27 -0
  259. package/dist/apps/shared/components/mail/invite/InviteTimeline.js +120 -0
  260. package/dist/apps/shared/components/mail/invite/ProposeTimeForm.d.ts +17 -0
  261. package/dist/apps/shared/components/mail/invite/ProposeTimeForm.js +45 -0
  262. package/dist/apps/shared/components/mail/invite/inviteFormat.d.ts +28 -0
  263. package/dist/apps/shared/components/mail/invite/inviteFormat.js +94 -0
  264. package/dist/apps/shared/components/mail/invite/inviteStore.d.ts +28 -0
  265. package/dist/apps/shared/components/mail/invite/inviteStore.js +95 -0
  266. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
  267. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
  268. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +10 -0
  269. package/dist/apps/shared/components/mail/layout/MailShell.js +76 -46
  270. package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
  271. package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
  272. package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
  273. package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
  274. package/dist/apps/shared/components/settings/layout/SettingsShell.js +7 -5
  275. package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
  276. package/dist/apps/shared/components/tasks/layout/TasksShell.js +4 -3
  277. package/dist/apps/shared/gestures/useEnterSlide.d.ts +14 -0
  278. package/dist/apps/shared/gestures/useEnterSlide.js +47 -0
  279. package/dist/apps/shared/gestures/useSwipe.d.ts +45 -0
  280. package/dist/apps/shared/gestures/useSwipe.js +90 -0
  281. package/dist/apps/shared/gestures/useSwipeSlide.d.ts +40 -0
  282. package/dist/apps/shared/gestures/useSwipeSlide.js +95 -0
  283. package/dist/apps/shared/gestures/useWheelPaging.d.ts +32 -0
  284. package/dist/apps/shared/gestures/useWheelPaging.js +85 -0
  285. package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
  286. package/dist/apps/shared/mail/folderCounts.js +12 -0
  287. package/dist/apps/shared/mail/newMailNotifications.d.ts +3 -8
  288. package/dist/apps/shared/mail/newMailNotifications.js +3 -12
  289. package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
  290. package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
  291. package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
  292. package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
  293. package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
  294. package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
  295. package/dist/apps/shared/mail/primaryMailbox.js +61 -0
  296. package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
  297. package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
  298. package/dist/apps/shared/mail/useMailConnection.js +3 -1
  299. package/dist/apps/shared/mail/useNewMailNotifications.js +3 -2
  300. package/dist/apps/shared/notifications/preferences.d.ts +13 -0
  301. package/dist/apps/shared/notifications/preferences.js +36 -0
  302. package/dist/apps/shared/notifications/store.d.ts +1 -0
  303. package/dist/apps/shared/notifications/store.js +7 -23
  304. package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
  305. package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
  306. package/dist/apps/shared/styles/app.css +396 -386
  307. package/dist/apps/www/_routes.js +1 -0
  308. package/dist/apps/www/calendar/index.js +78 -7
  309. package/dist/apps/www/contacts/index.js +3 -1
  310. package/dist/apps/www/index.js +548 -210
  311. package/dist/apps/www/messages/[uid].js +13 -5
  312. package/dist/apps/www/settings/profile/index.d.ts +4 -0
  313. package/dist/apps/www/settings/profile/index.js +124 -0
  314. package/package.json +2 -2
@@ -1,315 +1,315 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { KeyboardEvent, ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
6
- import { createPortal } from "react-dom";
7
- import type { IconType } from "react-icons";
8
- import { HiOutlineEllipsisHorizontal } from "react-icons/hi2";
9
-
10
- /** One button of a `ResponsiveToolbar`. */
11
- export interface ToolbarAction {
12
- id: string;
13
- /** The visible caption while there is room, else the tooltip and the accessible name. */
14
- label: string;
15
- icon: IconType;
16
- onClick: () => void;
17
- disabled?: boolean;
18
- /** A toggle: the button's pressed state. */
19
- pressed?: boolean;
20
- /** `title` and `aria-keyshortcuts` for a button with a shortcut - kept as given, whatever the layout. */
21
- hint?: { title: string; "aria-keyshortcuts"?: string };
22
- /** Actions with the same `group` sit together; a divider goes between groups. */
23
- group: number;
24
- /** Which goes into the "More" menu first when there is not room: the lowest number first. Never used for `essential`. */
25
- rank: number;
26
- /** Never moves into the "More" menu (the primary action). */
27
- essential?: boolean;
28
- }
29
-
30
- /** Room (px) each kind of button takes, and what the bar spends on its padding, a divider and the "More" button - the layout is computed from them
31
- * rather than measured, so the first paint on the client is already right and nothing jumps. */
32
- export const LABELLED_BUTTON_WIDTH = 64;
33
- export const ICON_BUTTON_WIDTH = 36;
34
- const BAR_PADDING = 16;
35
- const DIVIDER_WIDTH = 9;
36
-
37
- export interface ToolbarLayout {
38
- /** Captions under the icons. */
39
- labels: boolean;
40
- /** The ids of the actions shown as buttons, in order. The rest are in the "More" menu. */
41
- visible: string[];
42
- }
43
-
44
- function widthOf(actions: ToolbarAction[], perButton: number, more: boolean): number {
45
- const groups = new Set(actions.map((action) => action.group)).size;
46
- return BAR_PADDING + actions.length * perButton + Math.max(0, groups - 1) * DIVIDER_WIDTH + (more ? perButton : 0);
47
- }
48
-
49
- /**
50
- * Which buttons a bar of `width` px shows, and how: every one with its caption when that fits; else every one as an icon (the captions go first);
51
- * else as many icons as fit, the least important (`rank`) going into a "More" menu one at a time - the essential ones stay whatever the width.
52
- * Pure, so it is the same on the server and the client for a given width; `width === undefined` (not measured yet, no ResizeObserver) shows
53
- * everything.
54
- */
55
- export function layoutToolbar(actions: ToolbarAction[], width: number | undefined): ToolbarLayout {
56
- const ordered = [...actions].sort((a, b) => a.group - b.group);
57
- const all = ordered.map((action) => action.id);
58
- if (width === undefined) {
59
- return { labels: true, visible: all };
60
- }
61
- if (widthOf(ordered, LABELLED_BUTTON_WIDTH, false) <= width) {
62
- return { labels: true, visible: all };
63
- }
64
- if (widthOf(ordered, ICON_BUTTON_WIDTH, false) <= width) {
65
- return { labels: false, visible: all };
66
- }
67
- // Not everything fits: the least important actions go into "More" one at a time, until what is left and the "More" button do.
68
- let shown = ordered;
69
- for (const dropped of ordered.filter((action) => !action.essential).sort((x, y) => x.rank - y.rank)) {
70
- if (widthOf(shown, ICON_BUTTON_WIDTH, true) <= width) {
71
- break;
72
- }
73
- shown = shown.filter((action) => action !== dropped);
74
- }
75
- return { labels: false, visible: shown.map((action) => action.id) };
76
- }
77
-
78
- /** The width of `ref`'s element, kept current with a ResizeObserver; `undefined` until measured (and where there is no ResizeObserver). */
79
- export function useElementWidth(ref: React.RefObject<HTMLElement | null>): number | undefined {
80
- const [width, setWidth] = useState<number | undefined>(undefined);
81
- useLayoutEffect(() => {
82
- const element = ref.current;
83
- if (!element || typeof ResizeObserver === "undefined") {
84
- return;
85
- }
86
- setWidth(element.getBoundingClientRect().width);
87
- const observer = new ResizeObserver((entries) => {
88
- const box = entries[0]?.contentRect.width;
89
- if (box !== undefined) {
90
- setWidth(Math.round(box));
91
- }
92
- });
93
- observer.observe(element);
94
- return () => observer.disconnect();
95
- }, [ref]);
96
- return width;
97
- }
98
-
99
- function ToolbarButton({ action, labels }: { action: ToolbarAction; labels: boolean }) {
100
- const Icon = action.icon;
101
- return (
102
- <button
103
- type="button"
104
- onClick={action.onClick}
105
- disabled={action.disabled}
106
- aria-pressed={action.pressed}
107
- // A tooltip when the caption is not on show (the shortcut's own title, when it has one, wins).
108
- title={action.hint?.title ?? (labels ? undefined : action.label)}
109
- aria-keyshortcuts={action.hint?.["aria-keyshortcuts"]}
110
- data-toolbar-item=""
111
- className={[
112
- "flex shrink-0 flex-col items-center justify-center gap-1 rounded-sm text-xs disabled:opacity-40 disabled:cursor-not-allowed",
113
- labels ? "w-16 py-1.5" : "h-9 w-9",
114
- action.pressed ? "bg-primary/10 text-primary-dark" : "text-text-muted hover:not-disabled:bg-surface-alt hover:not-disabled:text-text",
115
- ].join(" ")}
116
- >
117
- <Icon size={18} aria-hidden="true" />
118
- <span className={labels ? "leading-tight text-center" : "sr-only"}>{action.label}</span>
119
- </button>
120
- );
121
- }
122
-
123
- /** The menu that holds what does not fit: a button, and a portalled `role="menu"` so nothing clips it. Arrow keys, Home, End and Escape; focus returns to the button. */
124
- function MoreMenu({ actions }: { actions: ToolbarAction[] }) {
125
- const [open, setOpen] = useState(false);
126
- const [anchor, setAnchor] = useState<{ right: number; top: number } | null>(null);
127
- const buttonRef = useRef<HTMLButtonElement>(null);
128
- const menuRef = useRef<HTMLDivElement>(null);
129
-
130
- useLayoutEffect(() => {
131
- if (!open) {
132
- setAnchor(null);
133
- return;
134
- }
135
- function place() {
136
- const rect = buttonRef.current!.getBoundingClientRect();
137
- setAnchor({ right: Math.max(8, window.innerWidth - rect.right), top: rect.bottom + 4 });
138
- }
139
- place();
140
- window.addEventListener("resize", place);
141
- window.addEventListener("scroll", place, true);
142
- return () => {
143
- window.removeEventListener("resize", place);
144
- window.removeEventListener("scroll", place, true);
145
- };
146
- }, [open]);
147
-
148
- // On open, the first item that can be used gets the focus.
149
- useEffect(() => {
150
- if (open && anchor) {
151
- menuRef.current?.querySelector<HTMLElement>('[role="menuitem"]:not(:disabled)')?.focus();
152
- }
153
- }, [open, anchor !== null]);
154
-
155
- useEffect(() => {
156
- if (!open) {
157
- return;
158
- }
159
- function handlePointerDown(event: MouseEvent) {
160
- if (event.target instanceof Node && !buttonRef.current?.contains(event.target) && !menuRef.current?.contains(event.target)) {
161
- setOpen(false);
162
- }
163
- }
164
- document.addEventListener("mousedown", handlePointerDown);
165
- return () => document.removeEventListener("mousedown", handlePointerDown);
166
- }, [open]);
167
-
168
- function close(refocus: boolean) {
169
- setOpen(false);
170
- if (refocus) {
171
- buttonRef.current?.focus();
172
- }
173
- }
174
-
175
- function handleMenuKeyDown(event: KeyboardEvent<HTMLDivElement>) {
176
- const items = Array.from(menuRef.current!.querySelectorAll<HTMLElement>('[role="menuitem"]:not(:disabled)'));
177
- const index = items.indexOf(document.activeElement as HTMLElement);
178
- const focusAt = (next: number) => {
179
- event.preventDefault();
180
- items[(next + items.length) % items.length]?.focus();
181
- };
182
- if (event.key === "Escape") {
183
- event.preventDefault();
184
- event.stopPropagation();
185
- close(true);
186
- } else if (event.key === "ArrowDown") {
187
- focusAt(index + 1);
188
- } else if (event.key === "ArrowUp") {
189
- focusAt(index - 1);
190
- } else if (event.key === "Home") {
191
- focusAt(0);
192
- } else if (event.key === "End") {
193
- focusAt(items.length - 1);
194
- } else if (event.key === "Tab") {
195
- close(false);
196
- }
197
- }
198
-
199
- return (
200
- <>
201
- <button
202
- ref={buttonRef}
203
- type="button"
204
- aria-haspopup="menu"
205
- aria-expanded={open}
206
- aria-label="More actions"
207
- title="More actions"
208
- data-toolbar-item=""
209
- onClick={() => setOpen((current) => !current)}
210
- onKeyDown={(event) => {
211
- if (event.key === "ArrowDown" && !open) {
212
- event.preventDefault();
213
- setOpen(true);
214
- }
215
- }}
216
- className={[
217
- "flex h-9 w-9 shrink-0 items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text",
218
- open ? "bg-surface-alt text-text" : "",
219
- ].join(" ")}
220
- >
221
- <HiOutlineEllipsisHorizontal size={18} aria-hidden="true" />
222
- </button>
223
- {open &&
224
- anchor &&
225
- createPortal(
226
- <div
227
- ref={menuRef}
228
- role="menu"
229
- aria-label="More actions"
230
- onKeyDown={handleMenuKeyDown}
231
- style={{ position: "fixed", right: anchor.right, top: anchor.top }}
232
- className="z-[70] w-52 max-w-[calc(100vw-1rem)] rounded-md border border-border bg-surface py-1.5 shadow-modal"
233
- >
234
- {actions.map((action) => {
235
- const Icon = action.icon;
236
- return (
237
- <button
238
- key={action.id}
239
- type="button"
240
- role="menuitem"
241
- disabled={action.disabled}
242
- title={action.hint?.title}
243
- aria-keyshortcuts={action.hint?.["aria-keyshortcuts"]}
244
- onClick={() => {
245
- close(true);
246
- action.onClick();
247
- }}
248
- className="flex w-full items-center gap-3 px-3.5 py-2 text-left text-sm text-text hover:not-disabled:bg-surface-alt focus-visible:bg-surface-alt disabled:cursor-not-allowed disabled:opacity-40"
249
- >
250
- <Icon size={18} aria-hidden="true" className="shrink-0 text-text-muted" />
251
- {action.label}
252
- </button>
253
- );
254
- })}
255
- </div>,
256
- document.body,
257
- )}
258
- </>
259
- );
260
- }
261
-
262
- /**
263
- * A toolbar that fits the column it is in - never wider, never over another pane. It measures its own width (`ResizeObserver`) and shows every
264
- * action with a caption when there is room; when there is not, the captions go first (each button keeps its tooltip and accessible name), then the
265
- * least important actions move into a "More" menu (`layoutToolbar()`). Arrow keys, Home and End move between the buttons; the menu is a real
266
- * `role="menu"`. `children` (a hidden file input, say) are rendered inside the toolbar's element but take no room.
267
- */
268
- export default function ResponsiveToolbar({ label, actions, leading, children }: { label: string; actions: ToolbarAction[]; leading?: ReactNode; children?: ReactNode }) {
269
- const ref = useRef<HTMLDivElement>(null);
270
- const width = useElementWidth(ref);
271
- const layout = layoutToolbar(actions, width);
272
- const ordered = [...actions].sort((a, b) => a.group - b.group);
273
- const shown = ordered.filter((action) => layout.visible.includes(action.id));
274
- const hidden = ordered.filter((action) => !layout.visible.includes(action.id));
275
-
276
- function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
277
- if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key) || (event.target as HTMLElement).closest('[role="menu"]')) {
278
- return;
279
- }
280
- const items = Array.from(ref.current!.querySelectorAll<HTMLElement>("[data-toolbar-item]:not(:disabled)"));
281
- const index = items.indexOf(event.target as HTMLElement);
282
- if (index === -1) {
283
- return;
284
- }
285
- event.preventDefault();
286
- const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : index + (event.key === "ArrowRight" ? 1 : -1);
287
- items[(next + items.length) % items.length].focus();
288
- }
289
-
290
- return (
291
- <div
292
- ref={ref}
293
- role="toolbar"
294
- aria-label={label}
295
- onKeyDown={handleKeyDown}
296
- // `overflow-x-clip` is the guarantee: whatever the layout says, the bar can never paint outside its own column.
297
- className="flex min-w-0 items-stretch gap-0.5 overflow-x-clip border-b border-border bg-surface-alt px-2 py-1"
298
- >
299
- {leading}
300
- {shown.map((action, index) => (
301
- <React.Fragment key={action.id}>
302
- {index > 0 && shown[index - 1].group !== action.group && <div className="my-1 w-px shrink-0 self-stretch bg-border" aria-hidden="true" />}
303
- <ToolbarButton action={action} labels={layout.labels} />
304
- </React.Fragment>
305
- ))}
306
- {hidden.length > 0 && (
307
- <>
308
- <div className="my-1 w-px shrink-0 self-stretch bg-border" aria-hidden="true" />
309
- <MoreMenu actions={hidden} />
310
- </>
311
- )}
312
- {children}
313
- </div>
314
- );
315
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { KeyboardEvent, ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
6
+ import { createPortal } from "react-dom";
7
+ import type { IconType } from "react-icons";
8
+ import { HiOutlineEllipsisHorizontal } from "react-icons/hi2";
9
+
10
+ /** One button of a `ResponsiveToolbar`. */
11
+ export interface ToolbarAction {
12
+ id: string;
13
+ /** The visible caption while there is room, else the tooltip and the accessible name. */
14
+ label: string;
15
+ icon: IconType;
16
+ onClick: () => void;
17
+ disabled?: boolean;
18
+ /** A toggle: the button's pressed state. */
19
+ pressed?: boolean;
20
+ /** `title` and `aria-keyshortcuts` for a button with a shortcut - kept as given, whatever the layout. */
21
+ hint?: { title: string; "aria-keyshortcuts"?: string };
22
+ /** Actions with the same `group` sit together; a divider goes between groups. */
23
+ group: number;
24
+ /** Which goes into the "More" menu first when there is not room: the lowest number first. Never used for `essential`. */
25
+ rank: number;
26
+ /** Never moves into the "More" menu (the primary action). */
27
+ essential?: boolean;
28
+ }
29
+
30
+ /** Room (px) each kind of button takes, and what the bar spends on its padding, a divider and the "More" button - the layout is computed from them
31
+ * rather than measured, so the first paint on the client is already right and nothing jumps. */
32
+ export const LABELLED_BUTTON_WIDTH = 64;
33
+ export const ICON_BUTTON_WIDTH = 36;
34
+ const BAR_PADDING = 16;
35
+ const DIVIDER_WIDTH = 9;
36
+
37
+ export interface ToolbarLayout {
38
+ /** Captions under the icons. */
39
+ labels: boolean;
40
+ /** The ids of the actions shown as buttons, in order. The rest are in the "More" menu. */
41
+ visible: string[];
42
+ }
43
+
44
+ function widthOf(actions: ToolbarAction[], perButton: number, more: boolean): number {
45
+ const groups = new Set(actions.map((action) => action.group)).size;
46
+ return BAR_PADDING + actions.length * perButton + Math.max(0, groups - 1) * DIVIDER_WIDTH + (more ? perButton : 0);
47
+ }
48
+
49
+ /**
50
+ * Which buttons a bar of `width` px shows, and how: every one with its caption when that fits; else every one as an icon (the captions go first);
51
+ * else as many icons as fit, the least important (`rank`) going into a "More" menu one at a time - the essential ones stay whatever the width.
52
+ * Pure, so it is the same on the server and the client for a given width; `width === undefined` (not measured yet, no ResizeObserver) shows
53
+ * everything.
54
+ */
55
+ export function layoutToolbar(actions: ToolbarAction[], width: number | undefined): ToolbarLayout {
56
+ const ordered = [...actions].sort((a, b) => a.group - b.group);
57
+ const all = ordered.map((action) => action.id);
58
+ if (width === undefined) {
59
+ return { labels: true, visible: all };
60
+ }
61
+ if (widthOf(ordered, LABELLED_BUTTON_WIDTH, false) <= width) {
62
+ return { labels: true, visible: all };
63
+ }
64
+ if (widthOf(ordered, ICON_BUTTON_WIDTH, false) <= width) {
65
+ return { labels: false, visible: all };
66
+ }
67
+ // Not everything fits: the least important actions go into "More" one at a time, until what is left and the "More" button do.
68
+ let shown = ordered;
69
+ for (const dropped of ordered.filter((action) => !action.essential).sort((x, y) => x.rank - y.rank)) {
70
+ if (widthOf(shown, ICON_BUTTON_WIDTH, true) <= width) {
71
+ break;
72
+ }
73
+ shown = shown.filter((action) => action !== dropped);
74
+ }
75
+ return { labels: false, visible: shown.map((action) => action.id) };
76
+ }
77
+
78
+ /** The width of `ref`'s element, kept current with a ResizeObserver; `undefined` until measured (and where there is no ResizeObserver). */
79
+ export function useElementWidth(ref: React.RefObject<HTMLElement | null>): number | undefined {
80
+ const [width, setWidth] = useState<number | undefined>(undefined);
81
+ useLayoutEffect(() => {
82
+ const element = ref.current;
83
+ if (!element || typeof ResizeObserver === "undefined") {
84
+ return;
85
+ }
86
+ setWidth(element.getBoundingClientRect().width);
87
+ const observer = new ResizeObserver((entries) => {
88
+ const box = entries[0]?.contentRect.width;
89
+ if (box !== undefined) {
90
+ setWidth(Math.round(box));
91
+ }
92
+ });
93
+ observer.observe(element);
94
+ return () => observer.disconnect();
95
+ }, [ref]);
96
+ return width;
97
+ }
98
+
99
+ function ToolbarButton({ action, labels }: { action: ToolbarAction; labels: boolean }) {
100
+ const Icon = action.icon;
101
+ return (
102
+ <button
103
+ type="button"
104
+ onClick={action.onClick}
105
+ disabled={action.disabled}
106
+ aria-pressed={action.pressed}
107
+ // A tooltip when the caption is not on show (the shortcut's own title, when it has one, wins).
108
+ title={action.hint?.title ?? (labels ? undefined : action.label)}
109
+ aria-keyshortcuts={action.hint?.["aria-keyshortcuts"]}
110
+ data-toolbar-item=""
111
+ className={[
112
+ "flex shrink-0 flex-col items-center justify-center gap-1 rounded-sm text-xs disabled:opacity-40 disabled:cursor-not-allowed",
113
+ labels ? "w-16 py-1.5" : "h-9 w-9",
114
+ action.pressed ? "bg-primary/10 text-primary-dark" : "text-text-muted hover:not-disabled:bg-surface-alt hover:not-disabled:text-text",
115
+ ].join(" ")}
116
+ >
117
+ <Icon size={18} aria-hidden="true" />
118
+ <span className={labels ? "leading-tight text-center" : "sr-only"}>{action.label}</span>
119
+ </button>
120
+ );
121
+ }
122
+
123
+ /** The menu that holds what does not fit: a button, and a portalled `role="menu"` so nothing clips it. Arrow keys, Home, End and Escape; focus returns to the button. */
124
+ function MoreMenu({ actions }: { actions: ToolbarAction[] }) {
125
+ const [open, setOpen] = useState(false);
126
+ const [anchor, setAnchor] = useState<{ right: number; top: number } | null>(null);
127
+ const buttonRef = useRef<HTMLButtonElement>(null);
128
+ const menuRef = useRef<HTMLDivElement>(null);
129
+
130
+ useLayoutEffect(() => {
131
+ if (!open) {
132
+ setAnchor(null);
133
+ return;
134
+ }
135
+ function place() {
136
+ const rect = buttonRef.current!.getBoundingClientRect();
137
+ setAnchor({ right: Math.max(8, window.innerWidth - rect.right), top: rect.bottom + 4 });
138
+ }
139
+ place();
140
+ window.addEventListener("resize", place);
141
+ window.addEventListener("scroll", place, true);
142
+ return () => {
143
+ window.removeEventListener("resize", place);
144
+ window.removeEventListener("scroll", place, true);
145
+ };
146
+ }, [open]);
147
+
148
+ // On open, the first item that can be used gets the focus.
149
+ useEffect(() => {
150
+ if (open && anchor) {
151
+ menuRef.current?.querySelector<HTMLElement>('[role="menuitem"]:not(:disabled)')?.focus();
152
+ }
153
+ }, [open, anchor !== null]);
154
+
155
+ useEffect(() => {
156
+ if (!open) {
157
+ return;
158
+ }
159
+ function handlePointerDown(event: MouseEvent) {
160
+ if (event.target instanceof Node && !buttonRef.current?.contains(event.target) && !menuRef.current?.contains(event.target)) {
161
+ setOpen(false);
162
+ }
163
+ }
164
+ document.addEventListener("mousedown", handlePointerDown);
165
+ return () => document.removeEventListener("mousedown", handlePointerDown);
166
+ }, [open]);
167
+
168
+ function close(refocus: boolean) {
169
+ setOpen(false);
170
+ if (refocus) {
171
+ buttonRef.current?.focus();
172
+ }
173
+ }
174
+
175
+ function handleMenuKeyDown(event: KeyboardEvent<HTMLDivElement>) {
176
+ const items = Array.from(menuRef.current!.querySelectorAll<HTMLElement>('[role="menuitem"]:not(:disabled)'));
177
+ const index = items.indexOf(document.activeElement as HTMLElement);
178
+ const focusAt = (next: number) => {
179
+ event.preventDefault();
180
+ items[(next + items.length) % items.length]?.focus();
181
+ };
182
+ if (event.key === "Escape") {
183
+ event.preventDefault();
184
+ event.stopPropagation();
185
+ close(true);
186
+ } else if (event.key === "ArrowDown") {
187
+ focusAt(index + 1);
188
+ } else if (event.key === "ArrowUp") {
189
+ focusAt(index - 1);
190
+ } else if (event.key === "Home") {
191
+ focusAt(0);
192
+ } else if (event.key === "End") {
193
+ focusAt(items.length - 1);
194
+ } else if (event.key === "Tab") {
195
+ close(false);
196
+ }
197
+ }
198
+
199
+ return (
200
+ <>
201
+ <button
202
+ ref={buttonRef}
203
+ type="button"
204
+ aria-haspopup="menu"
205
+ aria-expanded={open}
206
+ aria-label="More actions"
207
+ title="More actions"
208
+ data-toolbar-item=""
209
+ onClick={() => setOpen((current) => !current)}
210
+ onKeyDown={(event) => {
211
+ if (event.key === "ArrowDown" && !open) {
212
+ event.preventDefault();
213
+ setOpen(true);
214
+ }
215
+ }}
216
+ className={[
217
+ "flex h-9 w-9 shrink-0 items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text",
218
+ open ? "bg-surface-alt text-text" : "",
219
+ ].join(" ")}
220
+ >
221
+ <HiOutlineEllipsisHorizontal size={18} aria-hidden="true" />
222
+ </button>
223
+ {open &&
224
+ anchor &&
225
+ createPortal(
226
+ <div
227
+ ref={menuRef}
228
+ role="menu"
229
+ aria-label="More actions"
230
+ onKeyDown={handleMenuKeyDown}
231
+ style={{ position: "fixed", right: anchor.right, top: anchor.top }}
232
+ className="z-[70] w-52 max-w-[calc(100vw-1rem)] rounded-md border border-border bg-surface py-1.5 shadow-modal"
233
+ >
234
+ {actions.map((action) => {
235
+ const Icon = action.icon;
236
+ return (
237
+ <button
238
+ key={action.id}
239
+ type="button"
240
+ role="menuitem"
241
+ disabled={action.disabled}
242
+ title={action.hint?.title}
243
+ aria-keyshortcuts={action.hint?.["aria-keyshortcuts"]}
244
+ onClick={() => {
245
+ close(true);
246
+ action.onClick();
247
+ }}
248
+ className="flex w-full items-center gap-3 px-3.5 py-2 text-left text-sm text-text hover:not-disabled:bg-surface-alt focus-visible:bg-surface-alt disabled:cursor-not-allowed disabled:opacity-40"
249
+ >
250
+ <Icon size={18} aria-hidden="true" className="shrink-0 text-text-muted" />
251
+ {action.label}
252
+ </button>
253
+ );
254
+ })}
255
+ </div>,
256
+ document.body,
257
+ )}
258
+ </>
259
+ );
260
+ }
261
+
262
+ /**
263
+ * A toolbar that fits the column it is in - never wider, never over another pane. It measures its own width (`ResizeObserver`) and shows every
264
+ * action with a caption when there is room; when there is not, the captions go first (each button keeps its tooltip and accessible name), then the
265
+ * least important actions move into a "More" menu (`layoutToolbar()`). Arrow keys, Home and End move between the buttons; the menu is a real
266
+ * `role="menu"`. `children` (a hidden file input, say) are rendered inside the toolbar's element but take no room.
267
+ */
268
+ export default function ResponsiveToolbar({ label, actions, leading, children }: { label: string; actions: ToolbarAction[]; leading?: ReactNode; children?: ReactNode }) {
269
+ const ref = useRef<HTMLDivElement>(null);
270
+ const width = useElementWidth(ref);
271
+ const layout = layoutToolbar(actions, width);
272
+ const ordered = [...actions].sort((a, b) => a.group - b.group);
273
+ const shown = ordered.filter((action) => layout.visible.includes(action.id));
274
+ const hidden = ordered.filter((action) => !layout.visible.includes(action.id));
275
+
276
+ function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
277
+ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key) || (event.target as HTMLElement).closest('[role="menu"]')) {
278
+ return;
279
+ }
280
+ const items = Array.from(ref.current!.querySelectorAll<HTMLElement>("[data-toolbar-item]:not(:disabled)"));
281
+ const index = items.indexOf(event.target as HTMLElement);
282
+ if (index === -1) {
283
+ return;
284
+ }
285
+ event.preventDefault();
286
+ const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : index + (event.key === "ArrowRight" ? 1 : -1);
287
+ items[(next + items.length) % items.length].focus();
288
+ }
289
+
290
+ return (
291
+ <div
292
+ ref={ref}
293
+ role="toolbar"
294
+ aria-label={label}
295
+ onKeyDown={handleKeyDown}
296
+ // `overflow-x-clip` is the guarantee: whatever the layout says, the bar can never paint outside its own column.
297
+ className="flex min-w-0 items-stretch gap-0.5 overflow-x-clip border-b border-border bg-surface-alt px-2 py-1"
298
+ >
299
+ {leading}
300
+ {shown.map((action, index) => (
301
+ <React.Fragment key={action.id}>
302
+ {index > 0 && shown[index - 1].group !== action.group && <div className="my-1 w-px shrink-0 self-stretch bg-border" aria-hidden="true" />}
303
+ <ToolbarButton action={action} labels={layout.labels} />
304
+ </React.Fragment>
305
+ ))}
306
+ {hidden.length > 0 && (
307
+ <>
308
+ <div className="my-1 w-px shrink-0 self-stretch bg-border" aria-hidden="true" />
309
+ <MoreMenu actions={hidden} />
310
+ </>
311
+ )}
312
+ {children}
313
+ </div>
314
+ );
315
+ }