@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,217 +1,218 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
6
- import { HiOutlineBars3 } from "react-icons/hi2";
7
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
- import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
9
- import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
10
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
- import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
- import AppShell, { AppShellProps } from "../../layout/AppShell.js";
13
- import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
14
- import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
15
-
16
- export type TasksShellProps = Omit<AppShellProps, "active">;
17
-
18
- export interface TasksShellContextValue {
19
- /** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox). */
20
- mailboxUid?: string;
21
- /** The selected mailbox's single `tasks`-type folder. */
22
- folderUid?: string;
23
- mailboxes: Mailbox[];
24
- /** The caller's own uid — exposed here (rather than only as `AppShell`'s prop) so the page content
25
- * can compute "assigned to me" without needing it threaded through separately. */
26
- userUid?: string;
27
- }
28
-
29
- const TasksShellContext = createContext<TasksShellContextValue>({ mailboxes: [] });
30
-
31
- /** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
32
- export function useTasksShell(): TasksShellContextValue {
33
- return useContext(TasksShellContext);
34
- }
35
-
36
- type Status = "checking" | "error" | "ready";
37
-
38
- /**
39
- * The Tasks app's shell — structurally identical to `ContactsShell` (a mailbox has exactly one
40
- * well-known `tasks` folder, guaranteed to exist by `BaseMailboxRoute.create()`'s eager provisioning
41
- * in `@rapidmx/restapi`), just bound to a different folder type. Kept as its own small component
42
- * (rather than a single generic "SingleFolderShell" parameterized by type) so each app's sidebar can
43
- * grow its own app-specific controls later without threading extra props through a shared one.
44
- */
45
- export default function TasksShell({
46
- userUid,
47
- authServerUrl,
48
- impersonating,
49
- impersonationBaseUrl,
50
- trusted,
51
- trustedRoles,
52
- pluginNav,
53
- children,
54
- }: PropsWithChildren<TasksShellProps>) {
55
- const [status, setStatus] = useState<Status>("checking");
56
- const [error, setError] = useState<string | null>(null);
57
- const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
58
- const [folders, setFolders] = useState<Folder[]>([]);
59
- const [folderError, setFolderError] = useState<string | null>(null);
60
- const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
61
- const [drawerOpen, setDrawerOpen] = useState(false);
62
-
63
- // Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
64
- // effect, not during render, so the server render and the hydrating render agree.
65
- const search = useLocationSearch();
66
- const navigate = useNavigate();
67
- useEffect(() => {
68
- setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
69
- }, [search]);
70
- // Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
71
- // with the page - is closed here.
72
- useEffect(() => {
73
- setDrawerOpen(false);
74
- }, [search]);
75
-
76
- useEffect(() => {
77
- if (!userUid) {
78
- return;
79
- }
80
- listMailboxes({ limit: 100 })
81
- .then((result) => {
82
- setMailboxes(result);
83
- setStatus("ready");
84
- })
85
- .catch((err) => {
86
- setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
87
- setStatus("error");
88
- });
89
- }, [userUid]);
90
-
91
- const mailboxUid: string | undefined =
92
- (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
93
- mailboxes[0]?.uid;
94
-
95
- useEffect(() => {
96
- if (!mailboxUid) {
97
- setFolders([]);
98
- return;
99
- }
100
- setFolderError(null);
101
- listFolders(mailboxUid)
102
- .then(setFolders)
103
- .catch((err) => setFolderError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox's tasks folder."));
104
- }, [mailboxUid]);
105
-
106
- const folderUid: string | undefined = folders.find((f) => f.type === "tasks")?.uid;
107
-
108
- const contextValue = useMemo<TasksShellContextValue>(
109
- () => ({ mailboxUid, folderUid, mailboxes, userUid }),
110
- [mailboxUid, folderUid, mailboxes, userUid],
111
- );
112
-
113
- // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
114
- // for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
115
- if (userUid && status === "ready" && !mailboxUid) {
116
- return <MailboxProvisioning />;
117
- }
118
-
119
- let inner: ReactNode = null;
120
- if (userUid && status === "checking") {
121
- // Renders immediately (no network round trip needed) so switching into Tasks never shows a
122
- // blank pane while `listMailboxes()` is in flight — the real sidebar (My Day/Important/Planned/
123
- // lists) lives in `apps/www/tasks/index.tsx`'s own content, so this mimics its rough shape rather
124
- // than the (minimal, mailbox-switcher-only) shape of this shell's own sidebar.
125
- inner = (
126
- <aside className="w-56 shrink-0 bg-surface border-r border-border flex flex-col p-3 gap-4">
127
- <Skeleton height="h-9" />
128
- <SkeletonList count={5} />
129
- </aside>
130
- );
131
- } else if (userUid && status === "error") {
132
- inner = (
133
- <div className="flex-1 flex items-center justify-center p-8">
134
- <div className="w-full max-w-md">
135
- <Alert>{error}</Alert>
136
- </div>
137
- </div>
138
- );
139
- } else if (userUid && status === "ready") {
140
- // A function, not a plain JSX constant — see MailShell/ContactsShell/CalendarShell's identical
141
- // comment: it's rendered twice (desktop `<aside>` + mobile `Drawer`), possibly simultaneously
142
- // mounted, so the `<select>`'s `id`/its `<label>`'s `htmlFor` need a distinct value per instance.
143
- const sidebarContent = (idPrefix: string) => (
144
- <>
145
- {mailboxes.length > 1 && (
146
- <div>
147
- <label
148
- className="block text-xs font-bold uppercase tracking-wide text-text-muted mb-1"
149
- htmlFor={`${idPrefix}-tasks-mailbox-switcher`}
150
- >
151
- Mailbox
152
- </label>
153
- <select
154
- id={`${idPrefix}-tasks-mailbox-switcher`}
155
- className="w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface"
156
- value={mailboxUid}
157
- onChange={(e) => {
158
- navigate(`/tasks?mailboxUid=${encodeURIComponent(e.target.value)}`);
159
- }}
160
- >
161
- {mailboxes.map((mb) => (
162
- <option key={mb.uid} value={mb.uid}>
163
- {mb.displayName}
164
- {mb.ownerUserUid ? "" : " (shared)"}
165
- </option>
166
- ))}
167
- </select>
168
- </div>
169
- )}
170
- {folderError && <Alert>{folderError}</Alert>}
171
- </>
172
- );
173
-
174
- // With a single mailbox and no error this column would be empty: no 224 px of nothing beside the tasks' own menu.
175
- const hasSidebar = mailboxes.length > 1 || !!folderError;
176
- inner = (
177
- <>
178
- {hasSidebar && (
179
- <aside className="hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3">
180
- {sidebarContent("desktop")}
181
- </aside>
182
- )}
183
- <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Mailbox">
184
- <div className="flex flex-col gap-3">{sidebarContent("mobile")}</div>
185
- </Drawer>
186
- <main className="flex-1 min-w-0 flex flex-col">
187
- {hasSidebar && (
188
- <button
189
- type="button"
190
- 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"
191
- aria-label="Open mailbox switcher"
192
- onClick={() => setDrawerOpen(true)}
193
- >
194
- <HiOutlineBars3 size={20} aria-hidden="true" />
195
- </button>
196
- )}
197
- <TasksShellContext.Provider value={contextValue}>{children}</TasksShellContext.Provider>
198
- </main>
199
- </>
200
- );
201
- }
202
-
203
- return (
204
- <AppShell
205
- active="tasks"
206
- userUid={userUid}
207
- authServerUrl={authServerUrl}
208
- impersonating={impersonating}
209
- impersonationBaseUrl={impersonationBaseUrl}
210
- trusted={trusted}
211
- trustedRoles={trustedRoles}
212
- pluginNav={pluginNav}
213
- >
214
- {inner}
215
- </AppShell>
216
- );
217
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
6
+ import { HiOutlineBars3 } from "react-icons/hi2";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
9
+ import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
+ import AppShell, { AppShellProps } from "../../layout/AppShell.js";
13
+ import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
14
+ import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
15
+ import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
16
+
17
+ export type TasksShellProps = Omit<AppShellProps, "active">;
18
+
19
+ export interface TasksShellContextValue {
20
+ /** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox). */
21
+ mailboxUid?: string;
22
+ /** The selected mailbox's single `tasks`-type folder. */
23
+ folderUid?: string;
24
+ mailboxes: Mailbox[];
25
+ /** The caller's own uid — exposed here (rather than only as `AppShell`'s prop) so the page content
26
+ * can compute "assigned to me" without needing it threaded through separately. */
27
+ userUid?: string;
28
+ }
29
+
30
+ const TasksShellContext = createContext<TasksShellContextValue>({ mailboxes: [] });
31
+
32
+ /** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
33
+ export function useTasksShell(): TasksShellContextValue {
34
+ return useContext(TasksShellContext);
35
+ }
36
+
37
+ type Status = "checking" | "error" | "ready";
38
+
39
+ /**
40
+ * The Tasks app's shell — structurally identical to `ContactsShell` (a mailbox has exactly one
41
+ * well-known `tasks` folder, guaranteed to exist by `BaseMailboxRoute.create()`'s eager provisioning
42
+ * in `@rapidmx/restapi`), just bound to a different folder type. Kept as its own small component
43
+ * (rather than a single generic "SingleFolderShell" parameterized by type) so each app's sidebar can
44
+ * grow its own app-specific controls later without threading extra props through a shared one.
45
+ */
46
+ export default function TasksShell({
47
+ userUid,
48
+ authServerUrl,
49
+ impersonating,
50
+ impersonationBaseUrl,
51
+ trusted,
52
+ trustedRoles,
53
+ pluginNav,
54
+ children,
55
+ }: PropsWithChildren<TasksShellProps>) {
56
+ const [status, setStatus] = useState<Status>("checking");
57
+ const [error, setError] = useState<string | null>(null);
58
+ const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
59
+ const [folders, setFolders] = useState<Folder[]>([]);
60
+ const [folderError, setFolderError] = useState<string | null>(null);
61
+ const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
62
+ const [drawerOpen, setDrawerOpen] = useState(false);
63
+
64
+ // Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
65
+ // effect, not during render, so the server render and the hydrating render agree.
66
+ const search = useLocationSearch();
67
+ const navigate = useNavigate();
68
+ useEffect(() => {
69
+ setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
70
+ }, [search]);
71
+ // Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
72
+ // with the page - is closed here.
73
+ useEffect(() => {
74
+ setDrawerOpen(false);
75
+ }, [search]);
76
+
77
+ useEffect(() => {
78
+ if (!userUid) {
79
+ return;
80
+ }
81
+ listMailboxes({ limit: 100 })
82
+ .then((result) => {
83
+ setMailboxes(orderMailboxes(result, userUid));
84
+ setStatus("ready");
85
+ })
86
+ .catch((err) => {
87
+ setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
88
+ setStatus("error");
89
+ });
90
+ }, [userUid]);
91
+
92
+ const mailboxUid: string | undefined =
93
+ (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
94
+ primaryMailboxUid(mailboxes, userUid);
95
+
96
+ useEffect(() => {
97
+ if (!mailboxUid) {
98
+ setFolders([]);
99
+ return;
100
+ }
101
+ setFolderError(null);
102
+ listFolders(mailboxUid)
103
+ .then(setFolders)
104
+ .catch((err) => setFolderError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox's tasks folder."));
105
+ }, [mailboxUid]);
106
+
107
+ const folderUid: string | undefined = folders.find((f) => f.type === "tasks")?.uid;
108
+
109
+ const contextValue = useMemo<TasksShellContextValue>(
110
+ () => ({ mailboxUid, folderUid, mailboxes, userUid }),
111
+ [mailboxUid, folderUid, mailboxes, userUid],
112
+ );
113
+
114
+ // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
115
+ // for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
116
+ if (userUid && status === "ready" && !mailboxUid) {
117
+ return <MailboxProvisioning />;
118
+ }
119
+
120
+ let inner: ReactNode = null;
121
+ if (userUid && status === "checking") {
122
+ // Renders immediately (no network round trip needed) so switching into Tasks never shows a
123
+ // blank pane while `listMailboxes()` is in flight — the real sidebar (My Day/Important/Planned/
124
+ // lists) lives in `apps/www/tasks/index.tsx`'s own content, so this mimics its rough shape rather
125
+ // than the (minimal, mailbox-switcher-only) shape of this shell's own sidebar.
126
+ inner = (
127
+ <aside className="w-56 shrink-0 bg-surface border-r border-border flex flex-col p-3 gap-4">
128
+ <Skeleton height="h-9" />
129
+ <SkeletonList count={5} />
130
+ </aside>
131
+ );
132
+ } else if (userUid && status === "error") {
133
+ inner = (
134
+ <div className="flex-1 flex items-center justify-center p-8">
135
+ <div className="w-full max-w-md">
136
+ <Alert>{error}</Alert>
137
+ </div>
138
+ </div>
139
+ );
140
+ } else if (userUid && status === "ready") {
141
+ // A function, not a plain JSX constant — see MailShell/ContactsShell/CalendarShell's identical
142
+ // comment: it's rendered twice (desktop `<aside>` + mobile `Drawer`), possibly simultaneously
143
+ // mounted, so the `<select>`'s `id`/its `<label>`'s `htmlFor` need a distinct value per instance.
144
+ const sidebarContent = (idPrefix: string) => (
145
+ <>
146
+ {mailboxes.length > 1 && (
147
+ <div>
148
+ <label
149
+ className="block text-xs font-bold uppercase tracking-wide text-text-muted mb-1"
150
+ htmlFor={`${idPrefix}-tasks-mailbox-switcher`}
151
+ >
152
+ Mailbox
153
+ </label>
154
+ <select
155
+ id={`${idPrefix}-tasks-mailbox-switcher`}
156
+ className="w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface"
157
+ value={mailboxUid}
158
+ onChange={(e) => {
159
+ navigate(`/tasks?mailboxUid=${encodeURIComponent(e.target.value)}`);
160
+ }}
161
+ >
162
+ {mailboxes.map((mb) => (
163
+ <option key={mb.uid} value={mb.uid}>
164
+ {mb.displayName}
165
+ {mb.ownerUserUid ? "" : " (shared)"}
166
+ </option>
167
+ ))}
168
+ </select>
169
+ </div>
170
+ )}
171
+ {folderError && <Alert>{folderError}</Alert>}
172
+ </>
173
+ );
174
+
175
+ // With a single mailbox and no error this column would be empty: no 224 px of nothing beside the tasks' own menu.
176
+ const hasSidebar = mailboxes.length > 1 || !!folderError;
177
+ inner = (
178
+ <>
179
+ {hasSidebar && (
180
+ <aside className="hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3">
181
+ {sidebarContent("desktop")}
182
+ </aside>
183
+ )}
184
+ <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Mailbox" fullScreen>
185
+ <div className="flex flex-col gap-3">{sidebarContent("mobile")}</div>
186
+ </Drawer>
187
+ <main className="flex-1 min-w-0 flex flex-col">
188
+ {hasSidebar && (
189
+ <button
190
+ type="button"
191
+ 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"
192
+ aria-label="Open mailbox switcher"
193
+ onClick={() => setDrawerOpen(true)}
194
+ >
195
+ <HiOutlineBars3 size={20} aria-hidden="true" />
196
+ </button>
197
+ )}
198
+ <TasksShellContext.Provider value={contextValue}>{children}</TasksShellContext.Provider>
199
+ </main>
200
+ </>
201
+ );
202
+ }
203
+
204
+ return (
205
+ <AppShell
206
+ active="tasks"
207
+ userUid={userUid}
208
+ authServerUrl={authServerUrl}
209
+ impersonating={impersonating}
210
+ impersonationBaseUrl={impersonationBaseUrl}
211
+ trusted={trusted}
212
+ trustedRoles={trustedRoles}
213
+ pluginNav={pluginNav}
214
+ >
215
+ {inner}
216
+ </AppShell>
217
+ );
218
+ }
@@ -0,0 +1,54 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useRef, useState, type CSSProperties } from "react";
6
+
7
+ /** How long the new contents take to settle. Short, so a run of steps (a wheel kept turning) never waits on the last one. */
8
+ export const ENTER_SLIDE_MS = 140;
9
+ /** How far off, in pixels, and how faded the new contents start. */
10
+ const ENTER_OFFSET_PX = 56;
11
+ const ENTER_START_OPACITY = 0.35;
12
+ /** The pause that lets the browser draw the contents at their starting place before they move. */
13
+ const ENTER_SETTLE_MS = 20;
14
+
15
+ /** Whether the user asked their device for less motion. */
16
+ function prefersReducedMotion(): boolean {
17
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
18
+ }
19
+
20
+ /**
21
+ * 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
22
+ * a little way off in the direction the step went - below for `1` (the next), above for `-1` - and faded, and settle into place.
23
+ * 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
24
+ * settle just starts the next one from its start. Put `style` on the element that holds the contents and call `enter()` as the
25
+ * contents change. Nothing moves for someone who asked for less motion.
26
+ */
27
+ export function useEnterSlide() {
28
+ const [offset, setOffset] = useState(0);
29
+ const [animated, setAnimated] = useState(false);
30
+ const timer = useRef<number | undefined>(undefined);
31
+
32
+ useEffect(() => () => window.clearTimeout(timer.current), []);
33
+
34
+ function enter(direction: 1 | -1) {
35
+ if (prefersReducedMotion()) {
36
+ return;
37
+ }
38
+ window.clearTimeout(timer.current);
39
+ setAnimated(false);
40
+ setOffset(direction * ENTER_OFFSET_PX);
41
+ timer.current = window.setTimeout(() => {
42
+ setAnimated(true);
43
+ setOffset(0);
44
+ }, ENTER_SETTLE_MS);
45
+ }
46
+
47
+ const style: CSSProperties = {
48
+ transform: offset === 0 ? undefined : `translate3d(0, ${offset}px, 0)`,
49
+ opacity: offset === 0 ? undefined : ENTER_START_OPACITY,
50
+ transition: animated ? `transform ${ENTER_SLIDE_MS}ms ease-out, opacity ${ENTER_SLIDE_MS}ms ease-out` : "none",
51
+ };
52
+
53
+ return { style, enter };
54
+ }
@@ -0,0 +1,114 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useRef, type TouchEvent } from "react";
6
+
7
+ /** Which way the finger moved: `"left"` is right to left. */
8
+ export type SwipeDirection = "left" | "right";
9
+
10
+ export interface UseSwipeOptions {
11
+ /** Nothing is tracked while this is `false` (a desktop pointer, select mode). Default `true`. */
12
+ enabled?: boolean;
13
+ /** The directions that mean something; a drag the other way doesn't move at all. Default both. */
14
+ directions?: readonly SwipeDirection[];
15
+ /** 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. */
16
+ distance?: number;
17
+ /** Called as the finger moves, with the horizontal offset (the direction filter applied), and with `0` once it lifts. */
18
+ onDrag?: (offset: number) => void;
19
+ /** Called once when a swipe commits. `onDrag(0)` follows. */
20
+ onSwipe: (direction: SwipeDirection) => void;
21
+ }
22
+
23
+ /** The least distance that commits a swipe, and how far the finger must move before its direction is decided. */
24
+ export const MIN_SWIPE_DISTANCE = 80;
25
+ export const SWIPE_SLOP = 10;
26
+ /** A quicker flick than this (px per ms) commits after `FLING_MIN_DISTANCE` alone. */
27
+ export const FLING_VELOCITY = 0.5;
28
+ export const FLING_MIN_DISTANCE = 40;
29
+
30
+ /**
31
+ * A horizontal swipe by one finger, for the touch screens the app's phone layout is for: mouse and pen are left alone.
32
+ *
33
+ * Spread the handlers on the element and give it `touch-action: pan-y` (`style`), so the browser keeps scrolling vertically and leaves
34
+ * the horizontal movement to this. A gesture that starts out mostly vertical is a scroll and is never a swipe, however far it later
35
+ * strays sideways; one that starts out horizontal is a swipe, and stays one.
36
+ */
37
+ export function useSwipe({ enabled = true, directions = ["left", "right"], distance, onDrag, onSwipe }: UseSwipeOptions) {
38
+ const start = useRef<{ x: number; y: number; time: number; width: number; mode?: "swipe" | "scroll" } | null>(null);
39
+ const offset = useRef(0);
40
+ const latest = useRef({ directions, distance, onDrag, onSwipe });
41
+ latest.current = { directions, distance, onDrag, onSwipe };
42
+
43
+ function clamp(dx: number): number {
44
+ const allowed = latest.current.directions;
45
+ return (dx < 0 && !allowed.includes("left")) || (dx > 0 && !allowed.includes("right")) ? 0 : dx;
46
+ }
47
+
48
+ function onTouchStart(event: TouchEvent<HTMLElement>) {
49
+ if (!enabled || event.touches.length !== 1) {
50
+ start.current = null;
51
+ return;
52
+ }
53
+ const touch = event.touches[0];
54
+ start.current = { x: touch.clientX, y: touch.clientY, time: Date.now(), width: event.currentTarget.clientWidth };
55
+ offset.current = 0;
56
+ }
57
+
58
+ function onTouchMove(event: TouchEvent<HTMLElement>) {
59
+ const gesture = start.current;
60
+ if (!gesture || gesture.mode === "scroll") {
61
+ return;
62
+ }
63
+ if (event.touches.length !== 1) {
64
+ cancel();
65
+ return;
66
+ }
67
+ const dx = event.touches[0].clientX - gesture.x;
68
+ const dy = event.touches[0].clientY - gesture.y;
69
+ if (!gesture.mode) {
70
+ if (Math.max(Math.abs(dx), Math.abs(dy)) < SWIPE_SLOP) {
71
+ return;
72
+ }
73
+ gesture.mode = Math.abs(dx) > Math.abs(dy) * 1.5 ? "swipe" : "scroll";
74
+ if (gesture.mode === "scroll") {
75
+ return;
76
+ }
77
+ }
78
+ offset.current = clamp(dx);
79
+ latest.current.onDrag?.(offset.current);
80
+ }
81
+
82
+ function onTouchEnd() {
83
+ const gesture = start.current;
84
+ start.current = null;
85
+ if (!gesture || gesture.mode !== "swipe") {
86
+ return;
87
+ }
88
+ const dx = offset.current;
89
+ offset.current = 0;
90
+ const travelled = Math.abs(dx);
91
+ const velocity = travelled / Math.max(1, Date.now() - gesture.time);
92
+ const needed = latest.current.distance ?? Math.max(MIN_SWIPE_DISTANCE, gesture.width / 3);
93
+ const committed = dx !== 0 && (travelled >= needed || (velocity >= FLING_VELOCITY && travelled >= FLING_MIN_DISTANCE));
94
+ if (committed) {
95
+ latest.current.onSwipe(dx < 0 ? "left" : "right");
96
+ }
97
+ latest.current.onDrag?.(0);
98
+ }
99
+
100
+ function cancel() {
101
+ const moved = start.current?.mode === "swipe";
102
+ start.current = null;
103
+ offset.current = 0;
104
+ if (moved) {
105
+ latest.current.onDrag?.(0);
106
+ }
107
+ }
108
+
109
+ return {
110
+ handlers: enabled ? { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel: cancel } : {},
111
+ /** Leaves vertical panning to the browser. */
112
+ style: enabled ? ({ touchAction: "pan-y" } as const) : undefined,
113
+ };
114
+ }