@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
@@ -0,0 +1,64 @@
1
+ import React, { ReactNode } from "react";
2
+ /**
3
+ * Where a new event's quick-create popover hangs: the rectangle (viewport coordinates) of what was clicked - a time slot, a day cell, the
4
+ * "New event" button. `placement` says on which side it opens: beside the rectangle (`"side"`, the default: to its right, or to its left
5
+ * when there is no room) or under it (`"below"`).
6
+ */
7
+ export interface EventAnchor {
8
+ left: number;
9
+ top: number;
10
+ right: number;
11
+ bottom: number;
12
+ placement?: "side" | "below";
13
+ }
14
+ /** The anchor for a clicked element (see `EventAnchor`). */
15
+ export declare function anchorOf(element: Element, placement?: EventAnchor["placement"]): EventAnchor;
16
+ /** How the shell is drawn: a popover next to `anchor`, a bottom sheet (a phone's quick create), or a centered card with a backdrop. */
17
+ export type EventShellVariant = "popover" | "sheet" | "card";
18
+ /**
19
+ * Where a `width` x `height` popover goes for `anchor`, kept wholly inside the viewport (its margin on every side). With no anchor - the
20
+ * keyboard shortcut - it is centered, near the top.
21
+ */
22
+ export declare function placePopover(anchor: EventAnchor | undefined, size: {
23
+ width: number;
24
+ height: number;
25
+ }, viewport: {
26
+ width: number;
27
+ height: number;
28
+ }): {
29
+ left: number;
30
+ top: number;
31
+ };
32
+ /** What a form inside a popover uses to make its grip drag the popover. */
33
+ export interface EventShellContextValue {
34
+ dragHandleProps: {
35
+ onPointerDown: (event: React.PointerEvent) => void;
36
+ } | undefined;
37
+ }
38
+ export declare const EventShellContext: React.Context<EventShellContextValue>;
39
+ export interface EventShellProps {
40
+ variant: EventShellVariant;
41
+ /** The dialog's accessible name. */
42
+ label: string;
43
+ /** Escape, the backdrop (unless `onBackdropPress` says otherwise) and the close buttons' shared way out. */
44
+ onClose: () => void;
45
+ /** What a press on the backdrop does, where it must not just close: a half-typed quick event is not thrown away by a stray click. */
46
+ onBackdropPress?: () => void;
47
+ anchor?: EventAnchor;
48
+ /** Names the face being shown (details, quick create, full form): when it changes, focus moves to the new face's `data-autofocus` element,
49
+ * or to the dialog itself when it has none. */
50
+ focusKey: string;
51
+ /** The widest the popover or card gets, in px. */
52
+ width: number;
53
+ children: ReactNode;
54
+ }
55
+ /**
56
+ * The frame every face of the event dialog shares (the read-only details, the quick-create popover, the full form), so that switching from
57
+ * one to another keeps the focus handling and - the point of drawing them all through one element tree - keeps a form's state when the quick
58
+ * popover grows into the full card. The tree is deliberately the same for every variant (backdrop, dialog, children): only classes and
59
+ * styles change, so React never remounts what is inside.
60
+ *
61
+ * Escape and the Tab focus trap come from the shared overlay stack (`useOverlayDialog`), as for `Modal`. A popover is placed beside its
62
+ * anchor (`placePopover()`), re-placed when its own height changes, and can be dragged by a grip that takes `EventShellContext`'s handle.
63
+ */
64
+ export default function EventShell({ variant, label, onClose, onBackdropPress, anchor, focusKey, width, children }: EventShellProps): React.ReactPortal;
@@ -0,0 +1,134 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { createContext, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
7
+ import { createPortal } from "react-dom";
8
+ import { OverlayDepthContext, useOverlayDialog } from "@rapidmx/react-shared/components/overlays/overlayStack.js";
9
+ /** The anchor for a clicked element (see `EventAnchor`). */
10
+ export function anchorOf(element, placement = "side") {
11
+ const { left, top, right, bottom } = element.getBoundingClientRect();
12
+ return { left, top, right, bottom, placement };
13
+ }
14
+ const VIEWPORT_MARGIN = 8;
15
+ const ANCHOR_GAP = 8;
16
+ /**
17
+ * Where a `width` x `height` popover goes for `anchor`, kept wholly inside the viewport (its margin on every side). With no anchor - the
18
+ * keyboard shortcut - it is centered, near the top.
19
+ */
20
+ export function placePopover(anchor, size, viewport) {
21
+ let left;
22
+ let top;
23
+ if (!anchor) {
24
+ left = (viewport.width - size.width) / 2;
25
+ top = viewport.height * 0.1;
26
+ }
27
+ else if (anchor.placement === "below") {
28
+ left = anchor.left;
29
+ top = anchor.bottom + ANCHOR_GAP;
30
+ }
31
+ else {
32
+ left = anchor.right + ANCHOR_GAP;
33
+ if (left + size.width > viewport.width - VIEWPORT_MARGIN) {
34
+ left = anchor.left - ANCHOR_GAP - size.width;
35
+ }
36
+ top = anchor.top;
37
+ }
38
+ return {
39
+ left: Math.max(VIEWPORT_MARGIN, Math.min(left, viewport.width - size.width - VIEWPORT_MARGIN)),
40
+ top: Math.max(VIEWPORT_MARGIN, Math.min(top, viewport.height - size.height - VIEWPORT_MARGIN)),
41
+ };
42
+ }
43
+ export const EventShellContext = createContext({ dragHandleProps: undefined });
44
+ /**
45
+ * The frame every face of the event dialog shares (the read-only details, the quick-create popover, the full form), so that switching from
46
+ * one to another keeps the focus handling and - the point of drawing them all through one element tree - keeps a form's state when the quick
47
+ * popover grows into the full card. The tree is deliberately the same for every variant (backdrop, dialog, children): only classes and
48
+ * styles change, so React never remounts what is inside.
49
+ *
50
+ * Escape and the Tab focus trap come from the shared overlay stack (`useOverlayDialog`), as for `Modal`. A popover is placed beside its
51
+ * anchor (`placePopover()`), re-placed when its own height changes, and can be dragged by a grip that takes `EventShellContext`'s handle.
52
+ */
53
+ export default function EventShell({ variant, label, onClose, onBackdropPress, anchor, focusKey, width, children }) {
54
+ const dialogRef = useRef(null);
55
+ const depth = useOverlayDialog(true, dialogRef, onClose);
56
+ // How far the grip has dragged the popover from where it was placed.
57
+ const [offset, setOffset] = useState({ x: 0, y: 0 });
58
+ const offsetRef = useRef(offset);
59
+ offsetRef.current = offset;
60
+ const dragStart = useRef(null);
61
+ const place = useCallback(() => {
62
+ const dialog = dialogRef.current;
63
+ // A resize reported after the dialog has gone (the popover closes as soon as its event is saved) has nothing left to place.
64
+ if (!dialog) {
65
+ return;
66
+ }
67
+ if (variant !== "popover") {
68
+ dialog.style.left = "";
69
+ dialog.style.top = "";
70
+ return;
71
+ }
72
+ const size = { width: dialog.offsetWidth, height: dialog.offsetHeight };
73
+ const viewport = { width: window.innerWidth, height: window.innerHeight };
74
+ const base = placePopover(anchor, size, viewport);
75
+ const left = Math.max(VIEWPORT_MARGIN, Math.min(base.left + offsetRef.current.x, viewport.width - size.width - VIEWPORT_MARGIN));
76
+ const top = Math.max(VIEWPORT_MARGIN, Math.min(base.top + offsetRef.current.y, viewport.height - size.height - VIEWPORT_MARGIN));
77
+ dialog.style.left = `${Math.round(left)}px`;
78
+ dialog.style.top = `${Math.round(top)}px`;
79
+ }, [anchor, variant]);
80
+ useLayoutEffect(() => {
81
+ place();
82
+ }, [place, offset]);
83
+ // Declared after `useOverlayDialog()`'s own effect, which focuses the dialog itself: the face's first field wins over that.
84
+ useEffect(() => {
85
+ (dialogRef.current.querySelector("[data-autofocus]") ?? dialogRef.current).focus();
86
+ }, [focusKey]);
87
+ // The popover grows and shrinks with its rows (the time controls expanding, a guest added): keep it inside the window.
88
+ useEffect(() => {
89
+ if (variant !== "popover" || typeof ResizeObserver === "undefined") {
90
+ return;
91
+ }
92
+ const observer = new ResizeObserver(() => place());
93
+ observer.observe(dialogRef.current);
94
+ return () => observer.disconnect();
95
+ }, [place, variant]);
96
+ useEffect(() => {
97
+ function handleMove(event) {
98
+ const start = dragStart.current;
99
+ if (start) {
100
+ setOffset({ x: start.from.x + event.clientX - start.x, y: start.from.y + event.clientY - start.y });
101
+ }
102
+ }
103
+ function handleUp() {
104
+ dragStart.current = null;
105
+ }
106
+ window.addEventListener("pointermove", handleMove);
107
+ window.addEventListener("pointerup", handleUp);
108
+ return () => {
109
+ window.removeEventListener("pointermove", handleMove);
110
+ window.removeEventListener("pointerup", handleUp);
111
+ };
112
+ }, []);
113
+ const context = {
114
+ dragHandleProps: variant === "popover"
115
+ ? {
116
+ onPointerDown: (event) => {
117
+ dragStart.current = { x: event.clientX, y: event.clientY, from: offsetRef.current };
118
+ },
119
+ }
120
+ : undefined,
121
+ };
122
+ const backdropClass = variant === "card"
123
+ ? "fixed inset-0 bg-black/50 flex items-center justify-center p-3 md:p-5 z-[1000]"
124
+ : variant === "sheet"
125
+ ? "fixed inset-0 bg-black/50 flex items-end z-[1000]"
126
+ : "fixed inset-0 z-[1000]";
127
+ const dialogClass = variant === "card"
128
+ ? "w-full max-h-[calc(100vh-1.5rem)] md:max-h-[calc(100vh-2.5rem)] overflow-y-auto bg-surface border border-border rounded-xl shadow-modal focus:outline-none"
129
+ : variant === "sheet"
130
+ ? "w-full max-h-[90vh] overflow-y-auto bg-surface border-t border-border rounded-t-2xl shadow-modal focus:outline-none pb-[env(safe-area-inset-bottom)]"
131
+ : "fixed max-h-[calc(100vh-1rem)] overflow-y-auto bg-surface border border-border rounded-xl shadow-modal focus:outline-none";
132
+ const dialogStyle = variant === "popover" ? { width, maxWidth: `calc(100vw - ${VIEWPORT_MARGIN * 2}px)` } : variant === "card" ? { maxWidth: width } : {};
133
+ return createPortal(_jsx("div", { className: backdropClass, "data-event-shell": variant, onMouseDown: (event) => event.target === event.currentTarget && (onBackdropPress ?? onClose)(), children: _jsx("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-label": label, tabIndex: -1, className: dialogClass, style: dialogStyle, children: _jsx(OverlayDepthContext.Provider, { value: depth, children: _jsx(EventShellContext.Provider, { value: context, children: children }) }) }) }), document.body);
134
+ }
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ import { EventFormController } from "./eventForm.js";
3
+ /** How long the grid waits after the last change of day or guests before asking the server, so typing a list of guests is one look-up. */
4
+ export declare const FIND_TIME_DEBOUNCE_MS = 300;
5
+ /**
6
+ * The event dialog's "Find a time" tab: a day of the guests' calendars side by side, as Google Calendar draws it. A row for you and one for each guest,
7
+ * the hours of the day across (working hours in view first), each person's busy time as a block (a tentative one hatched) and the event's proposed time as a
8
+ * highlight across every row; pressing an hour, or a place in a row, moves the event's start there (its length is kept). Under the grid: whether the proposed
9
+ * time suits everyone, and up to five suggested times - the next slots of the event's length, 8:00am to 6:00pm, when everyone who can be checked is free.
10
+ *
11
+ * Who can be checked: `POST /calendar-events/free-busy` tells a guest's busy time only for a mailbox on this server whose owner shares it with you. A guest it
12
+ * cannot see - an external address, or somebody who hides their calendar - is shown as such, in the grid and in the summary, and is never counted as free;
13
+ * suggestions ignore them (and say so). The look-up is made after a short pause (`FIND_TIME_DEBOUNCE_MS`) whenever the day or the guests change, one answer at
14
+ * a time (an answer for a look-up that has since been replaced is dropped), and a failure - too many look-ups, or no connection - says so with a retry.
15
+ */
16
+ export default function FindATime({ c }: {
17
+ c: EventFormController;
18
+ }): React.JSX.Element;
@@ -0,0 +1,190 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
7
+ import { format } from "date-fns";
8
+ import { FREE_BUSY_MAX_ADDRESSES, availabilityDuring, availabilityOf, getFreeBusy, suggestTimes, summarizeAvailability, withoutOwnBlock, } from "@rapidmx/react-shared/calendar/freeBusyApi.js";
9
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
10
+ import { addDaysToKey } from "./allDay.js";
11
+ import { formatFormWhen, localDateFromKey, msToWallString, wallStringToMs } from "./eventFormat.js";
12
+ import { SEARCH_DAYS, WORK_END_HOUR, WORK_START_HOUR, clockLabel, dayStartMs, freeBusyErrorMessage, isDayKey, minuteOfDay, summaryLine, workingWindows, } from "./findTimeGrid.js";
13
+ /** How long the grid waits after the last change of day or guests before asking the server, so typing a list of guests is one look-up. */
14
+ export const FIND_TIME_DEBOUNCE_MS = 300;
15
+ const HOUR_WIDTH = 52;
16
+ const NAME_WIDTH = 136;
17
+ const HEADER_HEIGHT = 28;
18
+ const ROW_HEIGHT = 44;
19
+ const TIMELINE_WIDTH = HOUR_WIDTH * 24;
20
+ const HOURS = Array.from({ length: 24 }, (_, hour) => hour);
21
+ const GRID_LINES = `repeating-linear-gradient(to right, transparent 0, transparent ${HOUR_WIDTH - 1}px, var(--color-border) ${HOUR_WIDTH - 1}px, var(--color-border) ${HOUR_WIDTH}px)`;
22
+ const HATCH = "repeating-linear-gradient(45deg, transparent 0, transparent 4px, var(--color-text-muted) 4px, var(--color-text-muted) 5px)";
23
+ const TENTATIVE_HATCH = "repeating-linear-gradient(45deg, var(--color-danger-bg) 0, var(--color-danger-bg) 4px, var(--color-danger) 4px, var(--color-danger) 6px)";
24
+ /** "12am", "9am", "12pm" for the header cells and their labels. */
25
+ function hourLabel(hour) {
26
+ return format(new Date(2000, 0, 1, hour), "haaa");
27
+ }
28
+ /** What a row says about the proposed time, under the person's name. */
29
+ function stateText(state, status) {
30
+ if (status === "restricted") {
31
+ return "Hides their calendar";
32
+ }
33
+ if (status === "unknown") {
34
+ return "Availability unknown";
35
+ }
36
+ if (status === undefined) {
37
+ return "Checking…";
38
+ }
39
+ // Their calendar was read, but the proposed time is not on the days it covers.
40
+ if (state === undefined) {
41
+ return "";
42
+ }
43
+ return state === "free" ? "Free" : state === "busy" ? "Busy at this time" : "Maybe busy at this time";
44
+ }
45
+ /**
46
+ * The event dialog's "Find a time" tab: a day of the guests' calendars side by side, as Google Calendar draws it. A row for you and one for each guest,
47
+ * the hours of the day across (working hours in view first), each person's busy time as a block (a tentative one hatched) and the event's proposed time as a
48
+ * highlight across every row; pressing an hour, or a place in a row, moves the event's start there (its length is kept). Under the grid: whether the proposed
49
+ * time suits everyone, and up to five suggested times - the next slots of the event's length, 8:00am to 6:00pm, when everyone who can be checked is free.
50
+ *
51
+ * Who can be checked: `POST /calendar-events/free-busy` tells a guest's busy time only for a mailbox on this server whose owner shares it with you. A guest it
52
+ * cannot see - an external address, or somebody who hides their calendar - is shown as such, in the grid and in the summary, and is never counted as free;
53
+ * suggestions ignore them (and say so). The look-up is made after a short pause (`FIND_TIME_DEBOUNCE_MS`) whenever the day or the guests change, one answer at
54
+ * a time (an answer for a look-up that has since been replaced is dropped), and a failure - too many look-ups, or no connection - says so with a retry.
55
+ */
56
+ export default function FindATime({ c }) {
57
+ const { values, deviceZone, organizerAddress, occurrence } = c;
58
+ const { start, end, allDay, formZone } = values;
59
+ const startDay = start.slice(0, 10);
60
+ const [dayKey, setDayKey] = useState(() => (isDayKey(startDay) ? startDay : format(new Date(), "yyyy-MM-dd")));
61
+ const [retry, setRetry] = useState(0);
62
+ // Picking another date in the form moves the grid to it.
63
+ useEffect(() => {
64
+ if (isDayKey(startDay)) {
65
+ setDayKey(startDay);
66
+ }
67
+ }, [startDay]);
68
+ // You, then each guest once (case-insensitively), at most as many as one look-up may ask about.
69
+ const rows = useMemo(() => {
70
+ const seen = new Set([organizerAddress.toLowerCase()]);
71
+ const guests = [];
72
+ for (const attendee of values.attendees) {
73
+ const key = attendee.address.toLowerCase();
74
+ if (!seen.has(key)) {
75
+ seen.add(key);
76
+ guests.push({ address: attendee.address, name: attendee.displayName || attendee.address, you: false });
77
+ }
78
+ }
79
+ return [{ address: organizerAddress, name: "You", you: true }, ...guests].slice(0, FREE_BUSY_MAX_ADDRESSES);
80
+ }, [values.attendees, organizerAddress]);
81
+ const guestCount = rows.length - 1;
82
+ const addresses = rows.map((row) => row.address);
83
+ const addressesKey = addresses.map((address) => address.toLowerCase()).join(",");
84
+ const rangeStart = dayStartMs(dayKey, formZone, deviceZone);
85
+ const rangeEnd = dayStartMs(addDaysToKey(dayKey, SEARCH_DAYS), formZone, deviceZone);
86
+ const eventStart = allDay ? Number.NaN : wallStringToMs(start, formZone, deviceZone);
87
+ const eventEnd = allDay ? Number.NaN : wallStringToMs(end, formZone, deviceZone);
88
+ const timeIsValid = eventEnd > eventStart;
89
+ const [lookUp, setLookUp] = useState({ status: "loading", people: [] });
90
+ useEffect(() => {
91
+ if (allDay || guestCount === 0 || Number.isNaN(rangeStart) || Number.isNaN(rangeEnd)) {
92
+ return;
93
+ }
94
+ // Each run of this effect owns its answer: once the day or the guests change (or this unmounts) a slower earlier look-up is ignored.
95
+ let current = true;
96
+ setLookUp((previous) => ({ status: "loading", people: previous.people }));
97
+ const timer = setTimeout(() => {
98
+ getFreeBusy(addresses, new Date(rangeStart), new Date(rangeEnd)).then((response) => current && setLookUp({ status: "ready", people: availabilityOf(response) }), (err) => current && setLookUp({ status: "error", people: [], message: freeBusyErrorMessage(err) }));
99
+ }, FIND_TIME_DEBOUNCE_MS);
100
+ return () => {
101
+ current = false;
102
+ clearTimeout(timer);
103
+ };
104
+ // `addresses` is what `addressesKey` says.
105
+ }, [addressesKey, rangeStart, rangeEnd, allDay, retry]);
106
+ // The event's own time, when it is being rescheduled, is on the calendars of the people already invited: not their conflict with themselves.
107
+ const people = useMemo(() => {
108
+ if (!occurrence || occurrence.allDay) {
109
+ return lookUp.people;
110
+ }
111
+ const invited = new Set([occurrence.organizer.address, ...occurrence.attendees.map((attendee) => attendee.address), organizerAddress].map((a) => a.toLowerCase()));
112
+ return withoutOwnBlock(lookUp.people, invited, Date.parse(occurrence.startDate), Date.parse(occurrence.endDate));
113
+ }, [lookUp.people, occurrence, organizerAddress]);
114
+ // Working hours scrolled into view, or the event's own start when that lies outside them.
115
+ const scroller = useRef(null);
116
+ useLayoutEffect(() => {
117
+ if (!scroller.current) {
118
+ return;
119
+ }
120
+ const startHour = timeIsValid && msToWallString(eventStart, formZone, deviceZone).slice(0, 10) === dayKey ? Number(msToWallString(eventStart, formZone, deviceZone).slice(11, 13)) : undefined;
121
+ const firstHour = startHour !== undefined && (startHour < WORK_START_HOUR || startHour >= WORK_END_HOUR - 3) ? Math.max(0, startHour - 1) : WORK_START_HOUR;
122
+ scroller.current.scrollLeft = firstHour * HOUR_WIDTH;
123
+ // Only a change of day moves the view: moving the event within it must not.
124
+ }, [dayKey, guestCount > 0 && !allDay]);
125
+ function startAt(minutes) {
126
+ c.setStart(`${dayKey}T${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`);
127
+ }
128
+ function handleTimelineClick(event) {
129
+ const box = event.currentTarget.getBoundingClientRect();
130
+ if (box.width > 0) {
131
+ // Half hours: the smallest step of the form's own time inputs is a minute, but a row is only 52px an hour wide.
132
+ startAt(Math.min(47, Math.max(0, Math.floor(((event.clientX - box.left) / box.width) * 48))) * 30);
133
+ }
134
+ }
135
+ if (allDay) {
136
+ return _jsx("p", { className: "text-sm text-text-muted", children: "Finding a time works for an event with a start and end time. Uncheck All day to see when your guests are free." });
137
+ }
138
+ if (guestCount === 0) {
139
+ return _jsx("p", { className: "text-sm text-text-muted", children: "Add guests to see when they are free." });
140
+ }
141
+ const dayLabel = format(localDateFromKey(dayKey), "EEEE, MMMM d");
142
+ const proposedFrom = timeIsValid ? minuteOfDay(eventStart, dayKey, formZone, deviceZone) : 0;
143
+ const proposedTo = timeIsValid ? minuteOfDay(eventEnd, dayKey, formZone, deviceZone) : 0;
144
+ const showProposed = timeIsValid && proposedTo > proposedFrom;
145
+ const covered = timeIsValid && eventStart >= rangeStart && eventEnd <= rangeEnd;
146
+ const answered = lookUp.status === "ready";
147
+ const byAddress = new Map(people.map((person) => [person.address.toLowerCase(), person]));
148
+ const summary = answered && covered ? summarizeAvailability(people, eventStart, eventEnd) : undefined;
149
+ const suggestions = answered && timeIsValid
150
+ ? suggestTimes(people, {
151
+ windows: workingWindows(dayKey, formZone, deviceZone),
152
+ durationMs: eventEnd - eventStart,
153
+ notBeforeMs: Date.now(),
154
+ })
155
+ : [];
156
+ const seen = people.filter((person) => person.status === "available").length;
157
+ const unseen = people.length - seen;
158
+ /** What a screen reader hears of a person whose calendar was read: their busy times on the day shown. */
159
+ function busyDescription(person) {
160
+ const dayEnd = dayStartMs(addDaysToKey(dayKey, 1), formZone, deviceZone);
161
+ const blocks = person.busy.filter((block) => block.startMs < dayEnd && block.endMs > rangeStart);
162
+ return blocks.length === 0
163
+ ? "Free all day."
164
+ : blocks
165
+ .map((block) => `${block.tentative ? "Tentatively busy" : "Busy"} ${clockLabel(block.startMs, formZone, deviceZone)} to ${clockLabel(block.endMs, formZone, deviceZone)}`)
166
+ .join(", ");
167
+ }
168
+ return (_jsxs("div", { className: "flex flex-col gap-3 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("button", { type: "button", "aria-label": "Previous day", onClick: () => setDayKey((key) => addDaysToKey(key, -1)), className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: "\u2039" }), _jsx("span", { className: "text-sm font-medium min-w-[9rem] text-center", children: dayLabel }), _jsx("button", { type: "button", "aria-label": "Next day", onClick: () => setDayKey((key) => addDaysToKey(key, 1)), className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: "\u203A" }), _jsx("span", { className: "text-xs text-text-muted", children: formZone.replace(/_/g, " ") })] }), lookUp.status === "error" && (_jsxs("div", { role: "alert", className: "flex flex-wrap items-center gap-3 text-sm text-danger", children: [_jsx("span", { children: lookUp.message }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setRetry((count) => count + 1), children: "Try again" })] })), _jsx("div", { ref: scroller, role: "group", "aria-label": "Guests' availability", "aria-busy": lookUp.status === "loading", className: "overflow-x-auto border border-border rounded-md", children: _jsxs("div", { className: "relative", style: { width: NAME_WIDTH + TIMELINE_WIDTH }, children: [_jsxs("div", { className: "flex bg-surface border-b border-border", style: { height: HEADER_HEIGHT }, children: [_jsx("div", { className: "sticky left-0 z-20 shrink-0 bg-surface border-r border-border", style: { width: NAME_WIDTH } }), HOURS.map((hour) => (_jsx("button", { type: "button", "aria-label": `Start at ${hourLabel(hour)}`, title: `Start at ${hourLabel(hour)}`, onClick: () => startAt(hour * 60), className: "shrink-0 text-[10px] text-text-muted hover:bg-surface-alt hover:text-text text-left pl-1", style: { width: HOUR_WIDTH }, children: hourLabel(hour) }, hour)))] }), rows.map((row) => {
169
+ const person = byAddress.get(row.address.toLowerCase());
170
+ const state = person && timeIsValid && covered ? availabilityDuring(person, eventStart, eventEnd) : undefined;
171
+ const hidden = person && person.status !== "available" ? person : undefined;
172
+ return (_jsxs("div", { className: "flex border-b border-border last:border-b-0", style: { height: ROW_HEIGHT }, children: [_jsxs("div", { className: "sticky left-0 z-10 shrink-0 flex items-center gap-2 px-2 bg-surface border-r border-border", style: { width: NAME_WIDTH }, children: [_jsx("span", { "aria-hidden": "true", className: "w-6 h-6 shrink-0 rounded-full bg-primary/15 text-primary-dark text-[11px] font-semibold flex items-center justify-center uppercase", children: row.name.charAt(0) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-xs font-medium truncate", children: row.name }), _jsx("div", { className: "text-[10px] text-text-muted truncate", children: stateText(state, person?.status) })] })] }), _jsxs("div", { role: "img", "aria-label": `${row.name}: ${hidden ? stateText(undefined, hidden.status) : person ? busyDescription(person) : "Checking availability"}`, onClick: handleTimelineClick, className: ["relative shrink-0 cursor-pointer", !person && lookUp.status === "loading" ? "animate-pulse bg-surface-alt" : ""].join(" "), style: { width: TIMELINE_WIDTH, backgroundImage: GRID_LINES }, children: [_jsx("div", { "aria-hidden": "true", className: "absolute inset-y-0 left-0 bg-surface-alt/70", style: { width: WORK_START_HOUR * HOUR_WIDTH } }), _jsx("div", { "aria-hidden": "true", className: "absolute inset-y-0 right-0 bg-surface-alt/70", style: { width: (24 - WORK_END_HOUR) * HOUR_WIDTH } }), hidden && (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": "true", className: "absolute inset-0 opacity-30", style: { backgroundImage: HATCH } }), _jsx("span", { "aria-hidden": "true", className: "sticky left-2 inline-block px-2 mt-3 text-[11px] text-text bg-surface rounded-sm", children: stateText(undefined, hidden.status) })] })), person?.status === "available" &&
173
+ person.busy.map((block, index) => {
174
+ const from = minuteOfDay(block.startMs, dayKey, formZone, deviceZone);
175
+ const to = minuteOfDay(block.endMs, dayKey, formZone, deviceZone);
176
+ return to > from ? (_jsx("div", { "aria-hidden": "true", "data-busy": block.tentative ? "tentative" : "busy", className: ["absolute top-2 bottom-2 rounded-sm border border-danger", block.tentative ? "" : "bg-danger/60"].join(" "), style: { left: (from / 60) * HOUR_WIDTH, width: ((to - from) / 60) * HOUR_WIDTH, backgroundImage: block.tentative ? TENTATIVE_HATCH : undefined } }, index)) : null;
177
+ })] })] }, row.address.toLowerCase()));
178
+ }), showProposed && (_jsx("div", { "aria-hidden": "true", "data-proposed-time": "", className: "absolute border-x-2 border-primary bg-primary/10 pointer-events-none z-[5]", style: {
179
+ top: HEADER_HEIGHT,
180
+ bottom: 0,
181
+ left: NAME_WIDTH + (proposedFrom / 60) * HOUR_WIDTH,
182
+ width: ((proposedTo - proposedFrom) / 60) * HOUR_WIDTH,
183
+ } }))] }) }), _jsxs("div", { role: "status", "aria-live": "polite", className: "text-sm", children: [lookUp.status === "loading" && "Checking availability…", lookUp.status === "ready" && !timeIsValid && "Set a start and an end time to see who is free.", lookUp.status === "ready" && timeIsValid && !covered && "Pick a time within this week to see who is free.", summary && _jsx("span", { className: summary.conflicts > 0 ? "font-medium text-danger" : "font-medium", children: summaryLine(summary) })] }), answered && timeIsValid && (_jsxs("section", { "aria-labelledby": "find-time-suggestions", className: "flex flex-col gap-1.5", children: [_jsx("h4", { id: "find-time-suggestions", className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: "Suggested times" }), suggestions.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1", children: suggestions.map((ms) => {
184
+ const startWall = msToWallString(ms, formZone, deviceZone);
185
+ const endWall = msToWallString(ms + (eventEnd - eventStart), formZone, deviceZone);
186
+ return (_jsx("li", { children: _jsx("button", { type: "button", onClick: () => c.setStart(startWall), className: "text-sm text-primary-dark hover:underline text-left", children: formatFormWhen(startWall, endWall, false) }) }, ms));
187
+ }) })) : (_jsx("p", { className: "text-sm text-text-muted", children: seen === 0
188
+ ? "There is no calendar to check, so no time can be suggested."
189
+ : `No time this week has everyone free between ${hourLabel(WORK_START_HOUR)} and ${hourLabel(WORK_END_HOUR)}.` })), unseen > 0 && (_jsxs("p", { className: "text-xs text-text-muted", children: ["Suggestions leave out ", unseen, " ", unseen === 1 ? "person whose calendar" : "people whose calendars", " can\u2019t be seen."] }))] }))] }));
190
+ }
@@ -0,0 +1,45 @@
1
+ import React from "react";
2
+ import type { Attendee } from "@rapidmx/react-shared/calendar/calendarApi.js";
3
+ import type { ContactSuggestionOptions, RecipientSuggestion } from "@rapidmx/react-shared/mail/directoryApi.js";
4
+ declare const VARIANT_CLASS: {
5
+ /** Reads as plain text until it is hovered or focused, as the rows of the quick-create popover do. */
6
+ quiet: string;
7
+ /** A bordered box, as the full form's fields are. */
8
+ box: string;
9
+ };
10
+ /** What a guests field holds: the guests added, what was committed but is not an address (kept in the field, flagged, until it is fixed or removed),
11
+ * and what is still being typed. */
12
+ export interface GuestEntries {
13
+ guests: Attendee[];
14
+ invalid: string[];
15
+ draft: string;
16
+ }
17
+ export interface GuestInputProps extends GuestEntries {
18
+ /** The input's id, and the prefix of its suggestions' ids. */
19
+ id: string;
20
+ /** The input's accessible name. */
21
+ label: string;
22
+ placeholder?: string;
23
+ variant: keyof typeof VARIANT_CLASS;
24
+ /** The guests are listed elsewhere (rows with a role and an answer): the field draws chips only for what is not an address. */
25
+ listedElsewhere?: boolean;
26
+ /** Addresses that are never added - the organizer's, or the people already invited. */
27
+ skipAddresses?: string[];
28
+ /** The mailbox the event is on, whose contacts are suggested too. */
29
+ mailboxUid?: string;
30
+ onChange: (next: GuestEntries) => void;
31
+ /** Loads suggestions; defaults to the lookup compose's recipient fields use. */
32
+ fetchSuggestions?: (query: string, options: ContactSuggestionOptions) => Promise<RecipientSuggestion[]>;
33
+ debounceMs?: number;
34
+ }
35
+ /**
36
+ * The guests field of an event: compose's recipient field (`RecipientInput`) reading its chips as guests. Typing looks the name up among the
37
+ * caller's contacts and the server's mailboxes and distribution lists; a suggestion (or an address, `Name <address>`, a comma, a semicolon,
38
+ * Enter or leaving the field) becomes a chip, a paste of several addresses becomes several, and something that is not an address stays as
39
+ * a flagged chip once it is committed - nothing is said while it is being typed. A distribution list is added as its own address, one guest
40
+ * (as compose sends to one), which the mail server delivers to its members.
41
+ *
42
+ * The field is controlled: `guests` are the attendees, and every edit hands back the whole of the entries (see `applyGuestChips()`).
43
+ */
44
+ export default function GuestInput({ id, label, placeholder, variant, listedElsewhere, skipAddresses, mailboxUid, guests, invalid, draft, onChange, fetchSuggestions, debounceMs, }: GuestInputProps): React.JSX.Element;
45
+ export {};
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import RecipientInput from "../mail/compose/RecipientInput.js";
3
+ import { applyGuestChips, guestChip } from "./eventFormat.js";
4
+ /** Above the event dialog's own backdrop (`z-[1000]`), which the suggestions would otherwise sit under. */
5
+ const DIALOG_SUGGESTIONS_Z_INDEX = 1100;
6
+ const VARIANT_CLASS = {
7
+ /** Reads as plain text until it is hovered or focused, as the rows of the quick-create popover do. */
8
+ quiet: "w-full text-sm py-1 border-b border-transparent hover:border-border focus-within:border-primary",
9
+ /** A bordered box, as the full form's fields are. */
10
+ box: "w-full text-sm py-1.5 px-3 border border-border rounded-md bg-surface text-text focus-within:border-primary",
11
+ };
12
+ /** What `RecipientInput` calls `onChange` with; the entries come through `onEdit`. */
13
+ const IGNORED = () => undefined;
14
+ /**
15
+ * The guests field of an event: compose's recipient field (`RecipientInput`) reading its chips as guests. Typing looks the name up among the
16
+ * caller's contacts and the server's mailboxes and distribution lists; a suggestion (or an address, `Name <address>`, a comma, a semicolon,
17
+ * Enter or leaving the field) becomes a chip, a paste of several addresses becomes several, and something that is not an address stays as
18
+ * a flagged chip once it is committed - nothing is said while it is being typed. A distribution list is added as its own address, one guest
19
+ * (as compose sends to one), which the mail server delivers to its members.
20
+ *
21
+ * The field is controlled: `guests` are the attendees, and every edit hands back the whole of the entries (see `applyGuestChips()`).
22
+ */
23
+ export default function GuestInput({ id, label, placeholder, variant, listedElsewhere, skipAddresses, mailboxUid, guests, invalid, draft, onChange, fetchSuggestions, debounceMs, }) {
24
+ const value = [...guests.map(guestChip), ...invalid, draft].filter((part) => part.length > 0).join(", ");
25
+ const listed = new Set(listedElsewhere ? guests.map(guestChip) : []);
26
+ return (_jsx(RecipientInput, { id: id, label: label, ariaLabel: label, listLabel: "Guests", placeholder: placeholder, className: VARIANT_CLASS[variant], dropdownZIndex: DIALOG_SUGGESTIONS_Z_INDEX, value: value, initialPending: draft, mailboxUid: mailboxUid, hideChip: (chip) => listed.has(chip), fetchSuggestions: fetchSuggestions, debounceMs: debounceMs, onChange: IGNORED, onEdit: (chips, pending) => {
27
+ const merged = applyGuestChips(guests, chips, skipAddresses);
28
+ onChange({ guests: merged.attendees, invalid: merged.invalid, draft: pending });
29
+ } }));
30
+ }
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ import type { DescriptionEditorProps } from "./DescriptionEditor.js";
3
+ /**
4
+ * `DescriptionEditor`, loaded when it is first drawn. The editor is TipTap and ProseMirror - a good deal of code that the calendar, and the reading pane's
5
+ * invitation card, only need once somebody opens a description - so it is fetched then rather than with the page. Not `React.lazy()`: a chunk that fails to
6
+ * download (offline, or a deploy that replaced it) is said so here with a retry, instead of throwing through the tree above.
7
+ */
8
+ export default function LazyDescriptionEditor(props: DescriptionEditorProps): React.JSX.Element;
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useState } from "react";
7
+ let loading;
8
+ /** The editor once its chunk has arrived, so an editor drawn again (the quick popover growing into the card) starts as the editor, not as the placeholder. */
9
+ let loaded;
10
+ /** Fetches the editor's chunk once. A failed download is forgotten, so asking again tries again. */
11
+ function loadDescriptionEditor() {
12
+ loading ?? (loading = import("./DescriptionEditor.js").then((module) => (loaded = module.default), (err) => {
13
+ loading = undefined;
14
+ throw err;
15
+ }));
16
+ return loading;
17
+ }
18
+ /**
19
+ * `DescriptionEditor`, loaded when it is first drawn. The editor is TipTap and ProseMirror - a good deal of code that the calendar, and the reading pane's
20
+ * invitation card, only need once somebody opens a description - so it is fetched then rather than with the page. Not `React.lazy()`: a chunk that fails to
21
+ * download (offline, or a deploy that replaced it) is said so here with a retry, instead of throwing through the tree above.
22
+ */
23
+ export default function LazyDescriptionEditor(props) {
24
+ const [Editor, setEditor] = useState(() => loaded ?? null);
25
+ const [failed, setFailed] = useState(false);
26
+ const [attempt, setAttempt] = useState(0);
27
+ useEffect(() => {
28
+ let current = true;
29
+ loadDescriptionEditor().then((component) => current && setEditor(() => component), () => current && setFailed(true));
30
+ return () => {
31
+ current = false;
32
+ };
33
+ }, [attempt]);
34
+ if (Editor) {
35
+ return _jsx(Editor, { ...props });
36
+ }
37
+ if (failed) {
38
+ return (_jsxs("p", { role: "alert", className: "text-sm text-danger", children: ["The description editor couldn\u2019t be loaded.", " ", _jsx("button", { type: "button", className: "underline", onClick: () => {
39
+ setFailed(false);
40
+ setAttempt((count) => count + 1);
41
+ }, children: "Try again" })] }));
42
+ }
43
+ return (_jsx("div", { "aria-busy": "true", className: "min-h-[7rem] rounded-md border border-border bg-surface-alt text-sm text-text-muted px-3 py-2", children: "Loading the editor\u2026" }));
44
+ }
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { CalendarOccurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
3
+ import { EventAnchor } from "./EventShell.js";
3
4
  export interface MonthViewProps {
4
5
  /** Any date within the month to display. */
5
6
  viewDate: Date;
@@ -9,7 +10,10 @@ export interface MonthViewProps {
9
10
  folderColors: Record<string, string>;
10
11
  onSelectDay: (date: Date) => void;
11
12
  onSelectEvent: (occurrence: CalendarOccurrence) => void;
13
+ /** Click on the empty part of a day: the day (from midnight to the next midnight) and the cell, for a new all-day event's quick-create
14
+ * popover to open beside. */
15
+ onSelectSlot?: (start: Date, end: Date, anchor: EventAnchor) => void;
12
16
  }
13
17
  /** A real 6-week month grid (Mon-start), matching Outlook/Gmail's month view. Multi-day and all-day
14
18
  * events are shown on every day they cover (see `allDay.ts`'s `occursOnDay`). */
15
- export default function MonthView({ viewDate, occurrences, folderColors, onSelectDay, onSelectEvent }: MonthViewProps): React.JSX.Element;
19
+ export default function MonthView({ viewDate, occurrences, folderColors, onSelectDay, onSelectEvent, onSelectSlot }: MonthViewProps): React.JSX.Element;
@@ -1,22 +1,28 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useDraggable, useDroppable } from "@dnd-kit/core";
3
- import { eachDayOfInterval, endOfMonth, endOfWeek, format, isSameMonth, isToday, startOfMonth, startOfWeek } from "date-fns";
3
+ import { addDays, eachDayOfInterval, endOfMonth, endOfWeek, format, isSameMonth, isToday, startOfMonth, startOfWeek } from "date-fns";
4
4
  import { dayDropId, eventDragId } from "@rapidmx/react-shared/calendar/calendarDragIds.js";
5
5
  import { occursOnDay, startsOnDay } from "./allDay.js";
6
+ import { anchorOf } from "./EventShell.js";
6
7
  const MAX_CHIPS_PER_DAY = 3;
7
8
  /** A real 6-week month grid (Mon-start), matching Outlook/Gmail's month view. Multi-day and all-day
8
9
  * events are shown on every day they cover (see `allDay.ts`'s `occursOnDay`). */
9
- export default function MonthView({ viewDate, occurrences, folderColors, onSelectDay, onSelectEvent }) {
10
+ export default function MonthView({ viewDate, occurrences, folderColors, onSelectDay, onSelectEvent, onSelectSlot }) {
10
11
  const gridStart = startOfWeek(startOfMonth(viewDate), { weekStartsOn: 1 });
11
12
  const gridEnd = endOfWeek(endOfMonth(viewDate), { weekStartsOn: 1 });
12
13
  const days = eachDayOfInterval({ start: gridStart, end: gridEnd });
13
- return (_jsx("div", { className: "flex-1 grid grid-cols-7 grid-rows-6 min-h-0", role: "grid", "aria-label": "Month", children: days.map((day) => (_jsx(DayCell, { day: day, inCurrentMonth: isSameMonth(day, viewDate), occurrences: occurrences.filter((occ) => occursOnDay(occ, day)), folderColors: folderColors, onSelectDay: onSelectDay, onSelectEvent: onSelectEvent }, day.toISOString()))) }));
14
+ return (_jsx("div", { className: "flex-1 grid grid-cols-7 grid-rows-6 min-h-0", role: "grid", "aria-label": "Month", children: days.map((day) => (_jsx(DayCell, { day: day, inCurrentMonth: isSameMonth(day, viewDate), occurrences: occurrences.filter((occ) => occursOnDay(occ, day)), folderColors: folderColors, onSelectDay: onSelectDay, onSelectEvent: onSelectEvent, onSelectSlot: onSelectSlot }, day.toISOString()))) }));
14
15
  }
15
- function DayCell({ day, inCurrentMonth, occurrences, folderColors, onSelectDay, onSelectEvent }) {
16
+ function DayCell({ day, inCurrentMonth, occurrences, folderColors, onSelectDay, onSelectEvent, onSelectSlot }) {
16
17
  const { setNodeRef, isOver } = useDroppable({ id: dayDropId(day) });
17
18
  const visible = occurrences.slice(0, MAX_CHIPS_PER_DAY);
18
19
  const overflowCount = occurrences.length - visible.length;
19
- return (_jsxs("div", { ref: setNodeRef, className: [
20
+ return (_jsxs("div", { ref: setNodeRef, onClick: (e) => {
21
+ // Only the empty part of the cell: the day number, the chips and "+N more" have their own clicks.
22
+ if (onSelectSlot && !e.target.closest("button")) {
23
+ onSelectSlot(day, addDays(day, 1), anchorOf(e.currentTarget));
24
+ }
25
+ }, className: [
20
26
  "border-b border-r border-border p-1 flex flex-col gap-0.5 min-h-0 overflow-hidden",
21
27
  isOver ? "bg-primary/5" : "",
22
28
  inCurrentMonth ? "bg-surface" : "bg-surface-alt",