@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
@@ -4,11 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { createContext, useContext, useEffect, useMemo, useState } from "react";
7
- import { HiOutlineBars3 } from "react-icons/hi2";
7
+ import { HiOutlineBars3, HiOutlinePencilSquare } from "react-icons/hi2";
8
8
  import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
9
9
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
10
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
11
11
  import AppShell from "../../layout/AppShell.js";
12
+ import FloatingActionButton from "../../layout/FloatingActionButton.js";
12
13
  import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
13
14
  import KeyEnrollmentGate from "../../layout/KeyEnrollmentGate.js";
14
15
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
@@ -17,7 +18,11 @@ import LocalIndexLifecycle from "../../../search/LocalIndexLifecycle.js";
17
18
  import { NO_LIVE_UPDATES } from "../../../mail/useMailLiveUpdates.js";
18
19
  import { MAILBOX_LIST_LIMIT, MailConnectionContext, useMailConnection } from "../../../mail/useMailConnection.js";
19
20
  import { folderRows } from "../../../mail/folderTree.js";
20
- import { badgeFor, badgeLabel, countOfFolder, inboxUnreadTotal, } from "../../../mail/folderCounts.js";
21
+ import { primaryMailboxUid } from "../../../mail/primaryMailbox.js";
22
+ import { badgeFor, countOfFolder, inboxUnreadTotal, unreadBadgeTotal, } from "../../../mail/folderCounts.js";
23
+ import { ALL_MAILBOXES_SECTION, useSidebarSections } from "../../../mail/useCollapsedSections.js";
24
+ import FolderBadgeChip from "./FolderBadgeChip.js";
25
+ import SidebarSection from "./SidebarSection.js";
21
26
  import { useUnreadTitle } from "../../../mail/useUnreadTitle.js";
22
27
  import { pendingCountFor, usePendingSends } from "../../../mail/outbox/pendingSends.js";
23
28
  import OutboxBadge from "../OutboxBadge.js";
@@ -42,6 +47,7 @@ const MailShellContext = createContext({
42
47
  noteFolderUids: () => undefined,
43
48
  live: NO_LIVE_UPDATES,
44
49
  trackMessageChange: () => NO_TRACKER,
50
+ mobileSearchSlot: null,
45
51
  });
46
52
  /** Reads the mailbox/folder a page is currently showing, as resolved by the enclosing `MailShell`. */
47
53
  export function useMailShell() {
@@ -65,13 +71,6 @@ function aggregateBadge(mailboxFolders, type, counts) {
65
71
  }, { unread: 0, total: 0 });
66
72
  return badgeFor(type, sum);
67
73
  }
68
- /** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
69
- function FolderBadgeChip({ badge }) {
70
- return (_jsxs("span", { className: [
71
- "text-xs rounded-pill py-0.5 px-1.5",
72
- badge.kind === "unread" ? "font-bold bg-primary/15 text-primary-dark" : "font-medium text-text-muted",
73
- ].join(" "), children: [_jsx("span", { "aria-hidden": "true", children: badge.value }), _jsxs("span", { className: "sr-only", children: [" ", badgeLabel(badge)] })] }));
74
- }
75
74
  export { MAILBOX_LIST_LIMIT };
76
75
  /**
77
76
  * A separate component (not inlined into `MailShell`'s own render) so `useCompose()` resolves against
@@ -91,6 +90,14 @@ function ComposeButton({ mailboxUid }) {
91
90
  const env = useKeyEnvironment();
92
91
  return (_jsx("button", { type: "button", title: withHint("Compose", SHORTCUTS.mail.create, env), "aria-keyshortcuts": ariaKeyShortcuts(SHORTCUTS.mail.create, env), onClick: () => openCompose({ mailboxUid }), onPointerEnter: prefetchComposeWindow, onFocus: prefetchComposeWindow, className: "block text-center w-full py-2.5 px-4 rounded-sm font-semibold text-sm bg-primary text-white hover:bg-primary-dark", children: "Compose" }));
93
92
  }
93
+ /**
94
+ * A small round Compose button floating at the bottom right on a phone, just above the bottom tab bar (which is `h-14`, fixed), for the
95
+ * layout where the sidebar's Compose button is behind the folders drawer. Opens the same window, for the same mailbox, as that button.
96
+ */
97
+ function MobileComposeButton({ mailboxUid }) {
98
+ const { openCompose } = useCompose();
99
+ return (_jsx(FloatingActionButton, { label: "New message", icon: HiOutlinePencilSquare, onClick: () => openCompose({ mailboxUid }), onPointerEnter: prefetchComposeWindow, onFocus: prefetchComposeWindow }));
100
+ }
94
101
  /**
95
102
  * Mail's keyboard shortcuts that don't belong to a list or a message: "New message". A component of its own for the reason `ComposeButton`
96
103
  * is - `useCompose()` only resolves below `ComposeProvider`, i.e. inside the `AppShell` this shell renders - and mounted once, whatever number
@@ -116,6 +123,11 @@ function MailShortcuts({ mailboxUid }) {
116
123
  * behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
117
124
  * aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
118
125
  * of folders" requirement.
126
+ *
127
+ * An address that names nothing opens "All Mailboxes > Inbox" when there is more than one mailbox (that is when the section exists), and
128
+ * the one mailbox's own Inbox otherwise; a `?mailboxUid=`, `?folderUid=` or `?aggregate=` always wins. Nothing is selected until the address
129
+ * has been read. What needs a single mailbox while an all-mailboxes view is open (Compose, the new-message shortcut, key unlocking) uses the
130
+ * primary mailbox.
119
131
  */
120
132
  export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }) {
121
133
  // The mailboxes, their folders and the live connection come from the persistent app frame when there is one (`AppChrome` owns them, so
@@ -131,6 +143,8 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
131
143
  const [requestedFolderUid, setRequestedFolderUid] = useState(null);
132
144
  const [requestedAggregateType, setRequestedAggregateType] = useState(null);
133
145
  const [drawerOpen, setDrawerOpen] = useState(false);
146
+ // Whether the URL has been read yet (see below): until then the selection is not known, and the sidebar sections have nothing to open for.
147
+ const [selectionRead, setSelectionRead] = useState(false);
134
148
  // The selection lives in the URL, and the router (`AppRouter`) changes the URL without a page load when a folder link is
135
149
  // clicked - so it is read from the router's location, which updates, rather than once from `window.location`. Still read
136
150
  // in an effect, never during the first render, so the server render and the hydrating render agree.
@@ -140,36 +154,51 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
140
154
  setRequestedMailboxUid(params.get("mailboxUid"));
141
155
  setRequestedFolderUid(params.get("folderUid"));
142
156
  setRequestedAggregateType(params.get("aggregate"));
157
+ setSelectionRead(true);
143
158
  }, [search]);
144
159
  // Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
145
160
  // with the page - is closed here.
146
161
  useEffect(() => {
147
162
  setDrawerOpen(false);
148
163
  }, [search]);
149
- const aggregateFolderType = isAggregateFolderType(requestedAggregateType)
150
- ? requestedAggregateType
151
- : undefined;
152
- const mailboxUid = aggregateFolderType
153
- ? undefined
154
- : (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
155
- mailboxes[0]?.uid;
164
+ // What the address asks for. Nothing is selected until it has been read (the first render, before the effect above): choosing a
165
+ // default then would list a folder - the most expensive being every mailbox's - only to throw it away for the one the address names.
166
+ const requestedAggregate = selectionRead && isAggregateFolderType(requestedAggregateType) ? requestedAggregateType : undefined;
167
+ const requestedMailbox = selectionRead && requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined;
168
+ // The mailbox the caller has not chosen: their own, never a shared one that merely sorts first. It is what compose, the new-message
169
+ // shortcut and everything else that needs a single mailbox use while an all-mailboxes view (which has none) is open.
170
+ const defaultMailboxUid = primaryMailboxUid(mailboxes, userUid);
171
+ // With several mailboxes the address that names nothing opens "All Mailboxes > Inbox", the merged Inbox; with one there is no such
172
+ // section, so it opens that mailbox's own Inbox. A mailbox, a folder or an all-mailboxes view in the address wins over either.
173
+ const aggregateByDefault = selectionRead && !requestedAggregate && !requestedMailbox && requestedFolderUid === null && mailboxes.length > 1;
174
+ const aggregateFolderType = requestedAggregate ?? (aggregateByDefault ? "inbox" : undefined);
175
+ const mailboxUid = aggregateFolderType || !selectionRead ? undefined : (requestedMailbox ?? defaultMailboxUid);
156
176
  const selectedMailboxFolders = mailboxFolders.find((mf) => mf.mailbox.uid === mailboxUid)?.folders ?? [];
157
177
  const folderUid = aggregateFolderType
158
178
  ? undefined
159
179
  : (requestedFolderUid && selectedMailboxFolders.some((f) => f.uid === requestedFolderUid) ? requestedFolderUid : undefined) ??
160
180
  selectedMailboxFolders.find((f) => f.type === "inbox")?.uid;
161
181
  // The mailbox `KeyEnrollmentGate`/`LocalIndexLifecycle`/`ComposeButton` treat as "the" mailbox when
162
- // there's no single selected one to use (aggregate mode) - the caller's own owned mailbox if they
163
- // have one, else whichever accessible mailbox happens to be first. See MailShell's own doc comment on
182
+ // there's no single selected one to use (aggregate mode) - the caller's primary mailbox (`primaryMailbox()`). See MailShell's own doc comment on
164
183
  // this being an accepted limitation: an aggregate-view message from a *different*, not-yet-visited
165
184
  // mailbox may still need that mailbox's own folder view opened directly to unlock/decrypt it.
166
- const defaultMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
167
185
  const activeMailboxUid = mailboxUid ?? defaultMailboxUid;
186
+ // Which sidebar sections are open: "All mailboxes" and the primary mailbox unless the reader chose otherwise. Any of them can be collapsed;
187
+ // a change of the open folder into another section opens that one (see `useSidebarSections()`); the first selection, once the address has been read
188
+ // and the mailboxes are here, is where a load lands, not a navigation. A lone mailbox has nothing to collapse, so its section is never a toggle.
189
+ const sections = useSidebarSections({
190
+ userUid,
191
+ primaryUid: defaultMailboxUid,
192
+ activeId: aggregateFolderType ? ALL_MAILBOXES_SECTION : mailboxUid,
193
+ });
194
+ const collapsible = mailboxes.length > 1;
168
195
  const counts = folderCounts.counts;
169
196
  const trackMessageChange = folderCounts.track;
170
197
  // `(3) Acme: Mail` in the tab strip while there is unread mail in an Inbox - kept by the frame when there is one.
171
198
  useUnreadTitle(inboxUnreadTotal(mailboxFolders, counts), { enabled: !hosted });
172
- const contextValue = useMemo(() => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange }), [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange]);
199
+ // A state rather than a ref: the page reads it during its render, so it has to re-render once the row it names is in the document.
200
+ const [mobileSearchSlot, setMobileSearchSlot] = useState(null);
201
+ const contextValue = useMemo(() => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, mobileSearchSlot }), [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, mobileSearchSlot]);
173
202
  // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else for a
174
203
  // mailbox-less caller to do here yet, so the icon rail/header/folder tree don't render at all. Checks
175
204
  // `mailboxes.length` directly (not `!mailboxUid`) since `mailboxUid` is legitimately undefined in
@@ -190,32 +219,33 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
190
219
  // A function, not a plain JSX constant — it's rendered twice (desktop `<aside>` + mobile
191
220
  // `Drawer`), possibly *simultaneously* mounted (the aside is only CSS-hidden below `md`, not
192
221
  // unmounted).
193
- const sidebarContent = () => (_jsxs(_Fragment, { children: [_jsx("div", { className: "p-3", children: _jsx(ComposeButton, { mailboxUid: defaultMailboxUid }) }), foldersLoading ? (_jsx("div", { className: "flex-1 overflow-y-auto px-3 pb-3", children: _jsx(SkeletonList, { count: 6, className: "pt-1" }) })) : (_jsxs("nav", { className: "flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3", children: [mailboxes.length > 1 && (_jsxs("div", { children: [_jsx("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5", children: "All Mailboxes" }), _jsx("div", { className: "flex flex-col gap-0.5", children: AGGREGATE_FOLDER_TYPES.map((type) => {
194
- const badge = aggregateBadge(mailboxFolders, type, counts);
195
- return (_jsxs("a", { href: `/?aggregate=${encodeURIComponent(type)}`, className: [
196
- "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
197
- aggregateFolderType === type
198
- ? "bg-primary/10 text-primary-dark font-semibold"
199
- : "text-text hover:bg-surface-alt",
200
- ].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[type] }), badge && _jsx(FolderBadgeChip, { badge: badge })] }, type));
201
- }) })] })), mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (_jsxs("div", { children: [_jsxs("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate", children: [mailbox.displayName, mailbox.ownerUserUid ? "" : " (shared)"] }), mailboxError && (_jsx("div", { className: "px-2.5 pb-1", children: _jsx(Alert, { children: mailboxError }) })), _jsx("div", { className: "flex flex-col gap-0.5", children: folderRows(folders, pendingCountFor(mailbox.uid, pendingSends)).map((row) => {
202
- if (row.kind === "placeholder") {
203
- // A folder the server has not made (or told us about) yet - the Outbox and Sent Items are made with the first message
204
- // sent - shown the moment a message is on its way. Nothing to open; the real folder replaces it when it arrives.
205
- const sending = pendingCountFor(mailbox.uid, pendingSends);
206
- return (_jsxs("div", { "data-folder-placeholder": row.type, className: "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5 text-text-muted", children: [_jsx("span", { children: FOLDER_LABELS[row.type] }), row.type === "outbox" && _jsx(OutboxBadge, { total: sending, pendingHere: sending })] }, `placeholder-${row.type}`));
207
- }
208
- const folder = row.folder;
209
- const badge = badgeFor(folder.type, countOfFolder(folder, counts));
210
- return (_jsxs("a", { href: `/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`, className: [
211
- "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
212
- folder.uid === folderUid
213
- ? "bg-primary/10 text-primary-dark font-semibold"
214
- : "text-text hover:bg-surface-alt",
215
- ].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[folder.type] ?? folder.name }), badge &&
216
- (folder.type === "outbox" ? (_jsx(OutboxBadge, { total: badge.value, status: outboxStatuses[folder.uid], pendingHere: pendingCountFor(mailbox.uid, pendingSends) })) : (_jsx(FolderBadgeChip, { badge: badge })))] }, folder.uid));
217
- }) })] }, mailbox.uid)))] }))] }));
218
- inner = (_jsxs(_Fragment, { children: [_jsx(MailShortcuts, { mailboxUid: defaultMailboxUid }), _jsx("aside", { className: "hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col", children: sidebarContent() }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Folders", children: _jsx("div", { className: "flex flex-col", children: sidebarContent() }) }), _jsxs("main", { className: "flex-1 min-w-0 overflow-y-auto", children: [_jsx("button", { type: "button", className: "md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open folders", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(MailShellContext.Provider, { value: contextValue, children: children })] })] }));
222
+ // `idPrefix` keeps each section's folder list's id (which its heading's `aria-controls` names) unique between the two.
223
+ const sidebarContent = (idPrefix) => (_jsxs(_Fragment, { children: [_jsx("div", { className: "p-3", children: _jsx(ComposeButton, { mailboxUid: defaultMailboxUid }) }), foldersLoading ? (_jsx("div", { className: "flex-1 overflow-y-auto px-3 pb-3", children: _jsx(SkeletonList, { count: 6, className: "pt-1" }) })) : (_jsxs("nav", { className: "flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3", children: [mailboxes.length > 1 && (_jsx(SidebarSection, { domId: `${idPrefix}-mail-section-${ALL_MAILBOXES_SECTION}`, label: "All Mailboxes", collapsible: collapsible, expanded: sections.isExpanded(ALL_MAILBOXES_SECTION), onToggle: () => sections.toggle(ALL_MAILBOXES_SECTION), unread: unreadBadgeTotal(mailboxFolders.flatMap((mf) => mf.folders), counts), children: AGGREGATE_FOLDER_TYPES.map((type) => {
224
+ const badge = aggregateBadge(mailboxFolders, type, counts);
225
+ return (_jsxs("a", { href: `/?aggregate=${encodeURIComponent(type)}`, className: [
226
+ "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
227
+ aggregateFolderType === type
228
+ ? "bg-primary/10 text-primary-dark font-semibold"
229
+ : "text-text hover:bg-surface-alt",
230
+ ].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[type] }), badge && _jsx(FolderBadgeChip, { badge: badge })] }, type));
231
+ }) })), mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (_jsx(SidebarSection, { domId: `${idPrefix}-mail-section-${mailbox.uid}`, label: _jsxs(_Fragment, { children: [mailbox.displayName, mailbox.ownerUserUid ? "" : " (shared)"] }), collapsible: collapsible, expanded: sections.isExpanded(mailbox.uid), onToggle: () => sections.toggle(mailbox.uid), unread: unreadBadgeTotal(folders, counts), notice: mailboxError && (_jsx("div", { className: "px-2.5 pb-1", children: _jsx(Alert, { children: mailboxError }) })), children: folderRows(folders, pendingCountFor(mailbox.uid, pendingSends)).map((row) => {
232
+ if (row.kind === "placeholder") {
233
+ // A folder the server has not made (or told us about) yet - the Outbox and Sent Items are made with the first message
234
+ // sent - shown the moment a message is on its way. Nothing to open; the real folder replaces it when it arrives.
235
+ const sending = pendingCountFor(mailbox.uid, pendingSends);
236
+ return (_jsxs("div", { "data-folder-placeholder": row.type, className: "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5 text-text-muted", children: [_jsx("span", { children: FOLDER_LABELS[row.type] }), row.type === "outbox" && _jsx(OutboxBadge, { total: sending, pendingHere: sending })] }, `placeholder-${row.type}`));
237
+ }
238
+ const folder = row.folder;
239
+ const badge = badgeFor(folder.type, countOfFolder(folder, counts));
240
+ return (_jsxs("a", { href: `/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`, className: [
241
+ "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
242
+ folder.uid === folderUid
243
+ ? "bg-primary/10 text-primary-dark font-semibold"
244
+ : "text-text hover:bg-surface-alt",
245
+ ].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[folder.type] ?? folder.name }), badge &&
246
+ (folder.type === "outbox" ? (_jsx(OutboxBadge, { total: badge.value, status: outboxStatuses[folder.uid], pendingHere: pendingCountFor(mailbox.uid, pendingSends) })) : (_jsx(FolderBadgeChip, { badge: badge })))] }, folder.uid));
247
+ }) }, mailbox.uid)))] }))] }));
248
+ inner = (_jsxs(_Fragment, { children: [_jsx(MailShortcuts, { mailboxUid: defaultMailboxUid }), _jsx("aside", { className: "hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Folders", fullScreen: true, children: _jsx("div", { className: "flex flex-col", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 overflow-y-auto", children: [_jsxs("div", { className: "md:hidden flex items-center gap-2 p-3", children: [_jsx("button", { type: "button", className: "shrink-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open folders", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("div", { ref: setMobileSearchSlot, className: "flex-1 min-w-0" })] }), _jsx(MobileComposeButton, { mailboxUid: defaultMailboxUid }), _jsx(MailShellContext.Provider, { value: contextValue, children: children })] })] }));
219
249
  }
220
250
  // Wraps AppShell unconditionally, at a stable tree position regardless of whether mailboxUid has
221
251
  // resolved yet - KeyEnrollmentGate itself passes `children` through untouched until a real
@@ -0,0 +1,23 @@
1
+ import React, { ReactNode } from "react";
2
+ export interface SidebarSectionProps {
3
+ /** Unique per rendering of the sidebar (it is on screen twice on a phone: beside the list and in the drawer): names the folder list the heading controls. */
4
+ domId: string;
5
+ /** The heading's text. */
6
+ label: ReactNode;
7
+ /** Whether the heading is a toggle at all. Not for a lone mailbox, whose section has nothing to be hidden for. */
8
+ collapsible: boolean;
9
+ expanded: boolean;
10
+ onToggle: () => void;
11
+ /** The unread mail in the section's folders that carry an unread count (see `unreadBadgeTotal()`), shown on the heading while it is collapsed (and only when there is some) so new mail in a hidden section is still noticed. */
12
+ unread?: number;
13
+ /** Shown between the heading and the folders, collapsed or not: a failure to load the section's folders is not something to hide. */
14
+ notice?: ReactNode;
15
+ /** The section's folder rows. */
16
+ children: ReactNode;
17
+ }
18
+ /**
19
+ * One section of Mail's sidebar ("All mailboxes", or one mailbox): a heading over a list of folders. When `collapsible` the heading is a
20
+ * disclosure button (`aria-expanded`, `aria-controls` the list, a chevron that turns) and the list is `hidden` while it is collapsed -
21
+ * kept in the document, so the button's `aria-controls` always names something and nothing is fetched or rebuilt on opening it.
22
+ */
23
+ export default function SidebarSection({ domId, label, collapsible, expanded, onToggle, unread, notice, children }: SidebarSectionProps): React.JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { HiChevronDown } from "react-icons/hi2";
3
+ import FolderBadgeChip from "./FolderBadgeChip.js";
4
+ /**
5
+ * One section of Mail's sidebar ("All mailboxes", or one mailbox): a heading over a list of folders. When `collapsible` the heading is a
6
+ * disclosure button (`aria-expanded`, `aria-controls` the list, a chevron that turns) and the list is `hidden` while it is collapsed -
7
+ * kept in the document, so the button's `aria-controls` always names something and nothing is fetched or rebuilt on opening it.
8
+ */
9
+ export default function SidebarSection({ domId, label, collapsible, expanded, onToggle, unread = 0, notice, children }) {
10
+ const shown = !collapsible || expanded;
11
+ return (_jsxs("div", { children: [collapsible ? (_jsx("h2", { className: "mb-1", children: _jsxs("button", { type: "button", "aria-expanded": expanded, "aria-controls": domId, onClick: onToggle, className: [
12
+ "flex w-full items-center gap-1 rounded-sm px-2.5 py-0.5 text-left text-xs font-bold uppercase tracking-wide text-text-muted",
13
+ "focus-visible:outline-2 focus-visible:outline-primary",
14
+ "hover:bg-surface-alt hover:text-text",
15
+ ].join(" "), children: [_jsx(HiChevronDown, { size: 14, "aria-hidden": "true", className: ["shrink-0 transition-transform", expanded ? "" : "-rotate-90"].join(" ") }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: label }), !expanded && unread > 0 && _jsx(FolderBadgeChip, { badge: { kind: "unread", value: unread } })] }) })) : (_jsx("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate", children: label })), notice, _jsx("div", { id: domId, hidden: !shown, className: shown ? "flex flex-col gap-0.5" : "hidden", children: children })] }));
16
+ }
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import type { OutgoingReply } from "../../../mail/outbox/outgoingReplies.js";
3
+ /**
4
+ * A reply or forward that has just been sent, drawn where its message will be once the server has filed it: the sender, who it went to, its body as
5
+ * it was composed, and one line saying where it is - "Sending...", "Sent", or, when it did not go, "Not sent: <why>" with the same ways out the
6
+ * pop-up offers (Retry, Open draft ...). It reads like the message's own card, so the real message replaces it without the thread rearranging.
7
+ *
8
+ * The body goes through the reading pane's own sanitizing frame (`MessageBody`), exactly as a received message's does.
9
+ */
10
+ export default function PendingMessageCard({ reply, headerRef, }: {
11
+ reply: OutgoingReply;
12
+ /** Registered so the pane can put the focus here when the message first appears. Focusable, but not in the tab order. */
13
+ headerRef: (node: HTMLHeadingElement | null) => void;
14
+ }): React.JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
3
+ import MailAddress, { RecipientLine } from "../MailAddress.js";
4
+ import MessageBody from "./MessageBody.js";
5
+ import { BODY_FONT_STYLE, CardShell, SenderAvatar } from "./MessageCard.js";
6
+ /**
7
+ * A reply or forward that has just been sent, drawn where its message will be once the server has filed it: the sender, who it went to, its body as
8
+ * it was composed, and one line saying where it is - "Sending...", "Sent", or, when it did not go, "Not sent: <why>" with the same ways out the
9
+ * pop-up offers (Retry, Open draft ...). It reads like the message's own card, so the real message replaces it without the thread rearranging.
10
+ *
11
+ * The body goes through the reading pane's own sanitizing frame (`MessageBody`), exactly as a received message's does.
12
+ */
13
+ export default function PendingMessageCard({ reply, headerRef, }) {
14
+ const failed = reply.state === "failed";
15
+ return (_jsxs(CardShell, { children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [_jsx(SenderAvatar, { from: reply.sender }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-x-2 text-sm", children: [_jsx("h2", { ref: headerRef, tabIndex: -1, style: BODY_FONT_STYLE, className: "flex-1 min-w-[13rem] font-semibold outline-none focus-visible:outline-2 focus-visible:outline-primary", children: _jsx(MailAddress, { recipient: reply.sender }) }), _jsxs("div", { role: failed ? "alert" : "status", "data-testid": "pending-message-status", "data-state": reply.state, className: ["flex items-center gap-1.5 text-xs shrink-0", failed ? "font-semibold text-danger" : "text-text-muted"].join(" "), children: [reply.state === "sending" && _jsx("span", { "aria-hidden": "true", className: "rr-sending-dot h-1.5 w-1.5 shrink-0 rounded-full bg-primary" }), _jsx("span", { children: failed ? `Not sent: ${reply.failure}` : reply.state === "sending" ? "Sending…" : "Sent" })] }, failed ? "failed" : "progress")] }), _jsx(RecipientLine, { label: "To", recipients: reply.to }), _jsx(RecipientLine, { label: "Cc", recipients: reply.cc }), _jsx(RecipientLine, { label: "Bcc", recipients: reply.bcc })] })] }), failed && (_jsx("div", { className: "flex flex-wrap gap-2 px-4 pb-3", children: reply.actions.map((action) => (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: action.onClick, children: action.label }, action.label))) })), _jsx("div", { className: "px-4 pb-4", children: _jsx(MessageBody, { messageUid: reply.uid, messageVersion: 0, title: `Message: ${reply.subject}`, content: { kind: "html", html: reply.html } }) })] }));
16
+ }
@@ -13,9 +13,11 @@ import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedbac
13
13
  import AppShell from "../../layout/AppShell.js";
14
14
  import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
15
15
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
16
+ import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
16
17
  import { mergePluginNavItems } from "../../../plugins/pluginNav.js";
17
18
  /** Every settings section with its own sidebar entry. */
18
19
  export const SETTINGS_SECTIONS = [
20
+ { id: "profile", href: "/settings/profile", label: "Profile" },
19
21
  { id: "appearance", href: "/settings/appearance", label: "Appearance" },
20
22
  { id: "auto-reply", href: "/settings/auto-reply", label: "Automatic Replies" },
21
23
  { id: "filters", href: "/settings/filters", label: "Mail Filters" },
@@ -72,7 +74,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
72
74
  }
73
75
  listMailboxes({ limit: 100 })
74
76
  .then((result) => {
75
- setMailboxes(result);
77
+ setMailboxes(orderMailboxes(result, userUid));
76
78
  setStatus("ready");
77
79
  })
78
80
  .catch((err) => {
@@ -81,7 +83,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
81
83
  });
82
84
  }, [userUid]);
83
85
  const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
84
- mailboxes[0]?.uid;
86
+ primaryMailboxUid(mailboxes, userUid);
85
87
  const contextValue = useMemo(() => ({ mailboxUid, mailboxes }), [mailboxUid, mailboxes]);
86
88
  // A full-screen takeover, not nested inside the rest of the app's chrome — same as ContactsShell/
87
89
  // TasksShell, there's nothing else for a mailbox-less caller to configure here yet.
@@ -109,8 +111,8 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
109
111
  }, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsx("nav", { "aria-label": "Settings sections", className: "flex flex-col gap-0.5", children: sections.map((section) => (_jsx("a", {
110
112
  // `inner` (this whole branch) is only ever computed once `status === "ready"`
111
113
  // and `mailboxUid` has already resolved truthy — a falsy `mailboxUid` returns
112
- // `<MailboxProvisioning />` above instead (and TypeScript's own narrowing of
113
- // this `const` already reflects that, no `!` needed) — so no fallback is
114
+ // `<MailboxProvisioning />` above instead (which is what the `!` below records,
115
+ // `primaryMailboxUid()` being honest that it may find none) — so no fallback is
114
116
  // needed here, matching this plan's established "dead guard the UI
115
117
  // structurally can't trigger" removal precedent (e.g.
116
118
  // `DomainDetailContent.handleVerify`).
@@ -118,7 +120,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
118
120
  "block px-2.5 py-1.5 rounded-sm text-sm",
119
121
  section.id === active ? "bg-primary/10 text-primary-dark font-medium" : "text-text hover:bg-surface-alt",
120
122
  ].join(" "), children: section.label }, section.id))) })] }));
121
- inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Settings", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [_jsx("button", { type: "button", className: "md:hidden m-3 self-start w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open settings menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(SettingsShellContext.Provider, { value: contextValue, children: children })] })] }));
123
+ inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Settings", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [_jsx("button", { type: "button", className: "md:hidden m-3 self-start w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open settings menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(SettingsShellContext.Provider, { value: contextValue, children: children })] })] }));
122
124
  }
123
125
  return (_jsx(AppShell, { active: "settings", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
124
126
  }
@@ -79,5 +79,5 @@ export default function TasksSidebar({ mailboxUid, tasks, userUid, active, onSel
79
79
  // `Drawer` (see `ContactsSidebar` for the identical pattern and the duplicate-id reasoning behind
80
80
  // the `idPrefix`).
81
81
  const navContent = (idPrefix) => (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(NavItem, { label: "My Day", count: myDayCount, active: active.type === "myDay", onClick: () => handleSelect({ type: "myDay" }) }), _jsx(NavItem, { label: "Important", count: importantCount, active: active.type === "important", onClick: () => handleSelect({ type: "important" }) }), _jsx(NavItem, { label: "Planned", count: plannedCount, active: active.type === "planned", onClick: () => handleSelect({ type: "planned" }) }), _jsx(NavItem, { label: "Assigned to me", count: assignedToMeCount, active: active.type === "assignedToMe", onClick: () => handleSelect({ type: "assignedToMe" }) }), _jsx(NavItem, { label: "Flagged email", active: active.type === "flagged", onClick: () => handleSelect({ type: "flagged" }) })] }), _jsx(NavItem, { label: "Tasks", count: tasks.length, active: active.type === "all", onClick: () => handleSelect({ type: "all" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between px-2.5 mb-1", children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: "Lists" }), _jsx("button", { type: "button", onClick: () => setAddingList(true), "aria-label": "New list", className: "text-xs font-bold text-primary-dark hover:underline", children: "+" })] }), listsError && _jsx("p", { className: "px-2.5 text-xs text-danger", children: listsError }), _jsx("div", { className: "flex flex-col gap-0.5", children: lists.map((list) => (_jsx(NavItem, { label: list.name, count: listCounts.get(list.uid) ?? 0, active: active.type === "list" && active.uid === list.uid, onClick: () => handleSelect({ type: "list", uid: list.uid, name: list.name }) }, list.uid))) }), addingList && (_jsxs("form", { onSubmit: handleAddList, className: "flex items-center gap-1 px-2.5 mt-1", children: [_jsx("input", { type: "text", autoFocus: true, "aria-label": "New list name", id: `${idPrefix}-new-list-name`, value: newListName, onChange: (e) => setNewListName(e.target.value), className: "flex-1 text-xs py-1 px-1.5 border border-border rounded-sm bg-surface" }), _jsx("button", { type: "submit", className: "text-xs font-semibold text-primary-dark", children: "Add" })] }))] })] }));
82
- return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "lg:hidden m-3 mb-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open tasks menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("nav", { "aria-label": "Tasks", className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto", children: navContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Tasks", children: _jsx("div", { className: "flex flex-col gap-4", children: navContent("mobile") }) })] }));
82
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "lg:hidden m-3 mb-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open tasks menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("nav", { "aria-label": "Tasks", className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto", children: navContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Tasks", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-4", children: navContent("mobile") }) })] }));
83
83
  }
@@ -13,6 +13,7 @@ import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedbac
13
13
  import AppShell from "../../layout/AppShell.js";
14
14
  import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
15
15
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
16
+ import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
16
17
  const TasksShellContext = createContext({ mailboxes: [] });
17
18
  /** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
18
19
  export function useTasksShell() {
@@ -51,7 +52,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
51
52
  }
52
53
  listMailboxes({ limit: 100 })
53
54
  .then((result) => {
54
- setMailboxes(result);
55
+ setMailboxes(orderMailboxes(result, userUid));
55
56
  setStatus("ready");
56
57
  })
57
58
  .catch((err) => {
@@ -60,7 +61,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
60
61
  });
61
62
  }, [userUid]);
62
63
  const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
63
- mailboxes[0]?.uid;
64
+ primaryMailboxUid(mailboxes, userUid);
64
65
  useEffect(() => {
65
66
  if (!mailboxUid) {
66
67
  setFolders([]);
@@ -98,7 +99,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
98
99
  }, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), folderError && _jsx(Alert, { children: folderError })] }));
99
100
  // With a single mailbox and no error this column would be empty: no 224 px of nothing beside the tasks' own menu.
100
101
  const hasSidebar = mailboxes.length > 1 || !!folderError;
101
- inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") })), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(TasksShellContext.Provider, { value: contextValue, children: children })] })] }));
102
+ inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") })), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(TasksShellContext.Provider, { value: contextValue, children: children })] })] }));
102
103
  }
103
104
  return (_jsx(AppShell, { active: "tasks", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
104
105
  }
@@ -0,0 +1,14 @@
1
+ import { type CSSProperties } from "react";
2
+ /** How long the new contents take to settle. Short, so a run of steps (a wheel kept turning) never waits on the last one. */
3
+ export declare const ENTER_SLIDE_MS = 140;
4
+ /**
5
+ * The look of a page that has just been swapped for the next or previous one by a vertical step (the wheel): the new contents start
6
+ * a little way off in the direction the step went - below for `1` (the next), above for `-1` - and faded, and settle into place.
7
+ * The old contents are not animated out: the swap is immediate, so a run of steps never queues, and a step in the middle of a
8
+ * settle just starts the next one from its start. Put `style` on the element that holds the contents and call `enter()` as the
9
+ * contents change. Nothing moves for someone who asked for less motion.
10
+ */
11
+ export declare function useEnterSlide(): {
12
+ style: CSSProperties;
13
+ enter: (direction: 1 | -1) => void;
14
+ };
@@ -0,0 +1,47 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useRef, useState } from "react";
6
+ /** How long the new contents take to settle. Short, so a run of steps (a wheel kept turning) never waits on the last one. */
7
+ export const ENTER_SLIDE_MS = 140;
8
+ /** How far off, in pixels, and how faded the new contents start. */
9
+ const ENTER_OFFSET_PX = 56;
10
+ const ENTER_START_OPACITY = 0.35;
11
+ /** The pause that lets the browser draw the contents at their starting place before they move. */
12
+ const ENTER_SETTLE_MS = 20;
13
+ /** Whether the user asked their device for less motion. */
14
+ function prefersReducedMotion() {
15
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
16
+ }
17
+ /**
18
+ * The look of a page that has just been swapped for the next or previous one by a vertical step (the wheel): the new contents start
19
+ * a little way off in the direction the step went - below for `1` (the next), above for `-1` - and faded, and settle into place.
20
+ * The old contents are not animated out: the swap is immediate, so a run of steps never queues, and a step in the middle of a
21
+ * settle just starts the next one from its start. Put `style` on the element that holds the contents and call `enter()` as the
22
+ * contents change. Nothing moves for someone who asked for less motion.
23
+ */
24
+ export function useEnterSlide() {
25
+ const [offset, setOffset] = useState(0);
26
+ const [animated, setAnimated] = useState(false);
27
+ const timer = useRef(undefined);
28
+ useEffect(() => () => window.clearTimeout(timer.current), []);
29
+ function enter(direction) {
30
+ if (prefersReducedMotion()) {
31
+ return;
32
+ }
33
+ window.clearTimeout(timer.current);
34
+ setAnimated(false);
35
+ setOffset(direction * ENTER_OFFSET_PX);
36
+ timer.current = window.setTimeout(() => {
37
+ setAnimated(true);
38
+ setOffset(0);
39
+ }, ENTER_SETTLE_MS);
40
+ }
41
+ const style = {
42
+ transform: offset === 0 ? undefined : `translate3d(0, ${offset}px, 0)`,
43
+ opacity: offset === 0 ? undefined : ENTER_START_OPACITY,
44
+ transition: animated ? `transform ${ENTER_SLIDE_MS}ms ease-out, opacity ${ENTER_SLIDE_MS}ms ease-out` : "none",
45
+ };
46
+ return { style, enter };
47
+ }
@@ -0,0 +1,45 @@
1
+ import { type TouchEvent } from "react";
2
+ /** Which way the finger moved: `"left"` is right to left. */
3
+ export type SwipeDirection = "left" | "right";
4
+ export interface UseSwipeOptions {
5
+ /** Nothing is tracked while this is `false` (a desktop pointer, select mode). Default `true`. */
6
+ enabled?: boolean;
7
+ /** The directions that mean something; a drag the other way doesn't move at all. Default both. */
8
+ directions?: readonly SwipeDirection[];
9
+ /** How far the finger has to travel to commit, in px. Default `MIN_SWIPE_DISTANCE`, or a third of the element's width when that is more. */
10
+ distance?: number;
11
+ /** Called as the finger moves, with the horizontal offset (the direction filter applied), and with `0` once it lifts. */
12
+ onDrag?: (offset: number) => void;
13
+ /** Called once when a swipe commits. `onDrag(0)` follows. */
14
+ onSwipe: (direction: SwipeDirection) => void;
15
+ }
16
+ /** The least distance that commits a swipe, and how far the finger must move before its direction is decided. */
17
+ export declare const MIN_SWIPE_DISTANCE = 80;
18
+ export declare const SWIPE_SLOP = 10;
19
+ /** A quicker flick than this (px per ms) commits after `FLING_MIN_DISTANCE` alone. */
20
+ export declare const FLING_VELOCITY = 0.5;
21
+ export declare const FLING_MIN_DISTANCE = 40;
22
+ /**
23
+ * A horizontal swipe by one finger, for the touch screens the app's phone layout is for: mouse and pen are left alone.
24
+ *
25
+ * Spread the handlers on the element and give it `touch-action: pan-y` (`style`), so the browser keeps scrolling vertically and leaves
26
+ * the horizontal movement to this. A gesture that starts out mostly vertical is a scroll and is never a swipe, however far it later
27
+ * strays sideways; one that starts out horizontal is a swipe, and stays one.
28
+ */
29
+ export declare function useSwipe({ enabled, directions, distance, onDrag, onSwipe }: UseSwipeOptions): {
30
+ handlers: {
31
+ onTouchStart: (event: TouchEvent<HTMLElement>) => void;
32
+ onTouchMove: (event: TouchEvent<HTMLElement>) => void;
33
+ onTouchEnd: () => void;
34
+ onTouchCancel: () => void;
35
+ } | {
36
+ onTouchStart?: undefined;
37
+ onTouchMove?: undefined;
38
+ onTouchEnd?: undefined;
39
+ onTouchCancel?: undefined;
40
+ };
41
+ /** Leaves vertical panning to the browser. */
42
+ style: {
43
+ readonly touchAction: "pan-y";
44
+ } | undefined;
45
+ };