@rapidmx/web-client 0.4.0 → 0.6.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 (320) hide show
  1. package/README.md +88 -0
  2. package/apps/admin/audit-log/index.tsx +35 -8
  3. package/apps/admin/data-requests/index.tsx +480 -485
  4. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  5. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  6. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  7. package/apps/admin/index.tsx +19 -1
  8. package/apps/admin/ingest-queue/index.tsx +44 -5
  9. package/apps/admin/mailboxes/[uid].tsx +211 -184
  10. package/apps/admin/quarantine/index.tsx +46 -14
  11. package/apps/admin/transport-rules/[uid].tsx +30 -1
  12. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  13. package/apps/admin/transport-rules/index.tsx +24 -6
  14. package/apps/admin/transport-rules/new/index.tsx +30 -1
  15. package/apps/escrow/_layout.tsx +2 -3
  16. package/apps/escrow/audit-log/index.tsx +196 -168
  17. package/apps/escrow/matters/[uid].tsx +621 -521
  18. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  19. package/apps/shared/components/admin/layout/AdminShell.tsx +28 -9
  20. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  21. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  22. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  23. package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
  24. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
  32. package/apps/shared/components/admin/signOut.ts +79 -0
  33. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  34. package/apps/shared/components/calendar/EventModal.tsx +717 -549
  35. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  36. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  37. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  38. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  39. package/apps/shared/components/calendar/allDay.ts +124 -0
  40. package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
  41. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  42. package/apps/shared/components/contacts/ContactForm.tsx +377 -348
  43. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  44. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  45. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  46. package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
  47. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  48. package/apps/shared/components/layout/AppShell.tsx +123 -33
  49. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  50. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
  51. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  52. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  53. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
  54. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  55. package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
  56. package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
  57. package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
  58. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  59. package/apps/shared/components/mail/layout/MailShell.tsx +420 -408
  60. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  61. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  62. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  63. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  64. package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -9
  65. package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
  66. package/apps/shared/mail/listAllPages.ts +39 -0
  67. package/apps/shared/plugins/pluginNav.ts +73 -0
  68. package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
  69. package/apps/shared/search/localIndexBuilder.ts +481 -179
  70. package/apps/shared/search/localIndexRpcClient.ts +231 -29
  71. package/apps/shared/search/localIndexSchema.ts +15 -6
  72. package/apps/shared/search/localIndexStorage.ts +98 -0
  73. package/apps/shared/search/localIndexVFS.ts +32 -1
  74. package/apps/shared/search/localIndexWorker.ts +648 -186
  75. package/apps/www/calendar/index.tsx +438 -417
  76. package/apps/www/contacts/[uid].tsx +21 -2
  77. package/apps/www/contacts/index.tsx +116 -12
  78. package/apps/www/index.tsx +1314 -1041
  79. package/apps/www/messages/[uid].tsx +18 -6
  80. package/apps/www/settings/auto-reply/index.tsx +134 -129
  81. package/apps/www/settings/encryption/index.tsx +739 -216
  82. package/apps/www/settings/filters/[uid].tsx +171 -155
  83. package/apps/www/settings/filters/new/index.tsx +8 -1
  84. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  85. package/apps/www/settings/privacy/index.tsx +494 -436
  86. package/apps/www/settings/read-receipts/index.tsx +148 -125
  87. package/apps/www/settings/sharing/index.tsx +17 -14
  88. package/apps/www/tasks/index.tsx +638 -579
  89. package/dist/apps/admin/_404.d.ts +2 -0
  90. package/dist/apps/admin/_500.d.ts +2 -0
  91. package/dist/apps/admin/_layout.d.ts +8 -0
  92. package/dist/apps/admin/audit-log/index.d.ts +11 -0
  93. package/dist/apps/admin/audit-log/index.js +38 -8
  94. package/dist/apps/admin/branding/index.d.ts +3 -0
  95. package/dist/apps/admin/data-requests/index.d.ts +3 -0
  96. package/dist/apps/admin/data-requests/index.js +52 -72
  97. package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
  98. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  99. package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
  100. package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
  101. package/dist/apps/admin/domains/[uid].d.ts +7 -0
  102. package/dist/apps/admin/domains/index.d.ts +3 -0
  103. package/dist/apps/admin/domains/new/index.d.ts +3 -0
  104. package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
  105. package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
  106. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  107. package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
  108. package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
  109. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  110. package/dist/apps/admin/index.d.ts +3 -0
  111. package/dist/apps/admin/index.js +4 -1
  112. package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
  113. package/dist/apps/admin/ingest-queue/index.js +24 -6
  114. package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
  115. package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
  116. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  117. package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
  118. package/dist/apps/admin/plugins/index.d.ts +3 -0
  119. package/dist/apps/admin/quarantine/index.d.ts +6 -0
  120. package/dist/apps/admin/quarantine/index.js +20 -11
  121. package/dist/apps/admin/retention-policy/index.d.ts +3 -0
  122. package/dist/apps/admin/setup/index.d.ts +3 -0
  123. package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
  124. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  125. package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
  126. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  127. package/dist/apps/admin/transport-rules/index.d.ts +3 -0
  128. package/dist/apps/admin/transport-rules/index.js +22 -6
  129. package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
  130. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  131. package/dist/apps/escrow/_layout.d.ts +10 -0
  132. package/dist/apps/escrow/_layout.js +3 -3
  133. package/dist/apps/escrow/audit-log/index.d.ts +9 -0
  134. package/dist/apps/escrow/audit-log/index.js +26 -1
  135. package/dist/apps/escrow/index.d.ts +3 -0
  136. package/dist/apps/escrow/matters/[uid].d.ts +7 -0
  137. package/dist/apps/escrow/matters/[uid].js +77 -42
  138. package/dist/apps/escrow/matters/new/index.d.ts +3 -0
  139. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
  140. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
  141. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  142. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
  143. package/dist/apps/shared/components/admin/layout/AdminShell.js +15 -6
  144. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
  145. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  146. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
  147. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
  148. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  149. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
  150. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  151. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +9 -0
  152. package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
  153. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
  154. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
  155. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +12 -0
  156. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
  157. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
  159. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
  160. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +10 -0
  161. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
  162. package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +4 -0
  163. package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
  164. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +9 -0
  165. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
  166. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
  167. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
  168. package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
  169. package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
  170. package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
  171. package/dist/apps/shared/components/admin/signOut.js +75 -0
  172. package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
  173. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  174. package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
  175. package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
  176. package/dist/apps/shared/components/calendar/EventModal.js +142 -21
  177. package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
  178. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  179. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
  180. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  181. package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
  182. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
  183. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  184. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
  185. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  186. package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
  187. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  188. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
  189. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -3
  190. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
  191. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  192. package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
  193. package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
  194. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
  195. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  196. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
  197. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
  198. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  199. package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
  200. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  201. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
  202. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
  203. package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
  204. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  205. package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
  206. package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
  207. package/dist/apps/shared/components/layout/AppShell.js +97 -26
  208. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
  209. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  210. package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
  211. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
  212. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  214. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  216. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
  217. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
  218. package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
  219. package/dist/apps/shared/components/mail/ConversationList.d.ts +11 -0
  220. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +23 -0
  221. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  222. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +93 -0
  223. package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
  224. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +58 -0
  225. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  226. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
  227. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
  228. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
  229. package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
  230. package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
  231. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +34 -0
  232. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
  233. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
  234. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  235. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +57 -0
  236. package/dist/apps/shared/components/mail/layout/MailShell.js +13 -4
  237. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
  238. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  239. package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
  240. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  241. package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
  242. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  243. package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
  244. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  245. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
  246. package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -9
  247. package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
  248. package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
  249. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
  250. package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
  251. package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
  252. package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
  253. package/dist/apps/shared/mail/listAllPages.js +25 -0
  254. package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
  255. package/dist/apps/shared/plugins/pluginNav.js +33 -0
  256. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
  257. package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
  258. package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
  259. package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
  260. package/dist/apps/shared/search/localIndexBuilder.js +292 -37
  261. package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
  262. package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
  263. package/dist/apps/shared/search/localIndexRpcClient.js +191 -26
  264. package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
  265. package/dist/apps/shared/search/localIndexSchema.js +12 -6
  266. package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
  267. package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
  268. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  269. package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
  270. package/dist/apps/shared/search/localIndexVFS.js +68 -40
  271. package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
  272. package/dist/apps/shared/search/localIndexWorker.js +519 -173
  273. package/dist/apps/shared/search/searchTier2.d.ts +30 -0
  274. package/dist/apps/www/_404.d.ts +2 -0
  275. package/dist/apps/www/_500.d.ts +2 -0
  276. package/dist/apps/www/_layout.d.ts +14 -0
  277. package/dist/apps/www/calendar/index.d.ts +3 -0
  278. package/dist/apps/www/calendar/index.js +28 -10
  279. package/dist/apps/www/contacts/[uid].d.ts +13 -0
  280. package/dist/apps/www/contacts/[uid].js +13 -2
  281. package/dist/apps/www/contacts/index.d.ts +3 -0
  282. package/dist/apps/www/contacts/index.js +80 -10
  283. package/dist/apps/www/index.d.ts +3 -0
  284. package/dist/apps/www/index.js +339 -108
  285. package/dist/apps/www/messages/[uid].d.ts +11 -0
  286. package/dist/apps/www/messages/[uid].js +18 -6
  287. package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
  288. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  289. package/dist/apps/www/settings/encryption/index.d.ts +7 -0
  290. package/dist/apps/www/settings/encryption/index.js +514 -97
  291. package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
  292. package/dist/apps/www/settings/filters/[uid].js +22 -7
  293. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
  294. package/dist/apps/www/settings/filters/index.d.ts +4 -0
  295. package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
  296. package/dist/apps/www/settings/filters/new/index.js +8 -2
  297. package/dist/apps/www/settings/focused-inbox/index.d.ts +4 -0
  298. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  299. package/dist/apps/www/settings/labels/index.d.ts +4 -0
  300. package/dist/apps/www/settings/privacy/index.d.ts +4 -0
  301. package/dist/apps/www/settings/privacy/index.js +57 -22
  302. package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
  303. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  304. package/dist/apps/www/settings/sharing/index.d.ts +4 -0
  305. package/dist/apps/www/settings/sharing/index.js +7 -11
  306. package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
  307. package/dist/apps/www/settings/signatures/index.d.ts +4 -0
  308. package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
  309. package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
  310. package/dist/apps/www/tasks/index.d.ts +3 -0
  311. package/dist/apps/www/tasks/index.js +50 -10
  312. package/package.json +3 -3
  313. package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
  314. package/apps/www/settings/booking-types/[uid].tsx +0 -300
  315. package/apps/www/settings/booking-types/index.tsx +0 -98
  316. package/apps/www/settings/booking-types/new/index.tsx +0 -212
  317. package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
  318. package/dist/apps/www/settings/booking-types/[uid].js +0 -129
  319. package/dist/apps/www/settings/booking-types/index.js +0 -29
  320. package/dist/apps/www/settings/booking-types/new/index.js +0 -86
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { format } from "date-fns";
2
3
  import { WEEKDAY_CODES, WEEKDAY_LABELS, describeRecurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
4
+ import { recurrenceUntilDateKey, recurrenceUntilInstant } from "./allDay.js";
3
5
  const SELECT_CLASS = "text-sm py-2 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
4
6
  const INPUT_CLASS = "text-sm py-2 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
5
7
  const FREQ_LABEL = {
@@ -21,20 +23,30 @@ function endConditionOf(rule) {
21
23
  * live "every ... until/for ..." summary comes from `describeRecurrence()` (built on `rrule`'s own
22
24
  * `.toText()`), so it never drifts out of sync with what will actually be submitted.
23
25
  */
24
- export default function RecurrenceEditor({ value, onChange }) {
26
+ export default function RecurrenceEditor({ value, onChange, allDay = false, startWeekday = "MO" }) {
25
27
  function handleEnable(enabled) {
26
- onChange(enabled ? { freq: "weekly", interval: 1, byDay: ["MO"], exceptions: [] } : null);
28
+ onChange(enabled ? { freq: "weekly", interval: 1, byDay: [startWeekday], exceptions: [] } : null);
27
29
  }
28
30
  // `update`/`toggleDay` are only ever invoked from handlers rendered inside the `{value && (...)}`
29
31
  // block below, so `value` is always non-null here — no defensive null check needed.
30
32
  function update(patch) {
31
33
  onChange({ ...value, ...patch });
32
34
  }
35
+ // A weekly rule always keeps at least one weekday: with none left, rrule would repeat every day instead.
33
36
  function toggleDay(day) {
34
37
  const current = value.byDay ?? [];
38
+ if (current.length === 1 && current[0] === day) {
39
+ return;
40
+ }
35
41
  const next = current.includes(day) ? current.filter((d) => d !== day) : [...current, day];
36
42
  update({ byDay: next });
37
43
  }
44
+ // react-shared passes `byDay` to rrule for every frequency, so a weekday list left over from Weekly would
45
+ // turn "every day" into "every Monday". Leaving Weekly drops it; entering Weekly starts on the event's own
46
+ // start weekday (a select only reports an actual change, so this is never Weekly -> Weekly).
47
+ function handleFrequency(freq) {
48
+ update({ freq, byDay: freq === "weekly" ? [startWeekday] : undefined });
49
+ }
38
50
  function handleEndCondition(condition) {
39
51
  if (condition === "never") {
40
52
  update({ count: undefined, until: undefined });
@@ -43,13 +55,36 @@ export default function RecurrenceEditor({ value, onChange }) {
43
55
  update({ count: 10, until: undefined });
44
56
  }
45
57
  else {
46
- update({ until: new Date().toISOString(), count: undefined });
58
+ update({ until: recurrenceUntilInstant(format(new Date(), "yyyy-MM-dd"), allDay), count: undefined });
47
59
  }
48
60
  }
49
- return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx("input", { type: "checkbox", checked: value !== null, onChange: (e) => handleEnable(e.target.checked) }), "Repeats"] }), value && (_jsxs("div", { className: "flex flex-col gap-3 pl-6 border-l-2 border-border", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { children: "Every" }), _jsx("input", { type: "number", min: 1, className: `${INPUT_CLASS} w-16`, value: value.interval, onChange: (e) => update({ interval: Number(e.target.value) }), "aria-label": "Recurrence interval" }), _jsxs("select", { className: SELECT_CLASS, value: value.freq, onChange: (e) => update({ freq: e.target.value }), "aria-label": "Recurrence frequency", children: [_jsx("option", { value: "daily", children: value.interval === 1 ? FREQ_LABEL.daily.unit : FREQ_LABEL.daily.unitPlural }), _jsx("option", { value: "weekly", children: value.interval === 1 ? FREQ_LABEL.weekly.unit : FREQ_LABEL.weekly.unitPlural }), _jsx("option", { value: "monthly", children: value.interval === 1 ? FREQ_LABEL.monthly.unit : FREQ_LABEL.monthly.unitPlural }), _jsx("option", { value: "yearly", children: value.interval === 1 ? FREQ_LABEL.yearly.unit : FREQ_LABEL.yearly.unitPlural })] })] }), value.freq === "weekly" && (_jsx("div", { className: "flex gap-1", children: WEEKDAY_CODES.map((day) => (_jsx("button", { type: "button", onClick: () => toggleDay(day), "aria-pressed": (value.byDay ?? []).includes(day), "aria-label": WEEKDAY_LABELS[day], className: [
61
+ /** A positive whole number from a number input, or `null` while it's empty (a user clearing the field
62
+ * to type a new value) — the rule keeps its previous value then, rather than becoming 0/NaN. */
63
+ function parsePositive(raw) {
64
+ if (raw.trim() === "")
65
+ return null;
66
+ const n = Math.floor(Number(raw));
67
+ return n >= 1 ? n : 1;
68
+ }
69
+ function handleInterval(raw) {
70
+ const interval = parsePositive(raw);
71
+ if (interval !== null)
72
+ update({ interval });
73
+ }
74
+ function handleCount(raw) {
75
+ const count = parsePositive(raw);
76
+ if (count !== null)
77
+ update({ count });
78
+ }
79
+ function handleUntil(raw) {
80
+ // A cleared (or partially typed) date input reports "" — keep the previous end date.
81
+ if (/^\d{4}-\d{2}-\d{2}$/.test(raw))
82
+ update({ until: recurrenceUntilInstant(raw, allDay) });
83
+ }
84
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx("input", { type: "checkbox", checked: value !== null, onChange: (e) => handleEnable(e.target.checked) }), "Repeats"] }), value && (_jsxs("div", { className: "flex flex-col gap-3 pl-6 border-l-2 border-border", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { children: "Every" }), _jsx("input", { type: "number", min: 1, className: `${INPUT_CLASS} w-16`, value: value.interval, onChange: (e) => handleInterval(e.target.value), "aria-label": "Recurrence interval" }), _jsxs("select", { className: SELECT_CLASS, value: value.freq, onChange: (e) => handleFrequency(e.target.value), "aria-label": "Recurrence frequency", children: [_jsx("option", { value: "daily", children: value.interval === 1 ? FREQ_LABEL.daily.unit : FREQ_LABEL.daily.unitPlural }), _jsx("option", { value: "weekly", children: value.interval === 1 ? FREQ_LABEL.weekly.unit : FREQ_LABEL.weekly.unitPlural }), _jsx("option", { value: "monthly", children: value.interval === 1 ? FREQ_LABEL.monthly.unit : FREQ_LABEL.monthly.unitPlural }), _jsx("option", { value: "yearly", children: value.interval === 1 ? FREQ_LABEL.yearly.unit : FREQ_LABEL.yearly.unitPlural })] })] }), value.freq === "weekly" && (_jsx("div", { className: "flex gap-1", children: WEEKDAY_CODES.map((day) => (_jsx("button", { type: "button", onClick: () => toggleDay(day), "aria-pressed": (value.byDay ?? []).includes(day), "aria-label": WEEKDAY_LABELS[day], className: [
50
85
  "w-9 h-9 rounded-full text-xs font-semibold border",
51
86
  (value.byDay ?? []).includes(day)
52
87
  ? "bg-primary text-white border-primary"
53
88
  : "bg-surface text-text-muted border-border hover:border-primary",
54
- ].join(" "), children: _jsx("span", { "aria-hidden": "true", children: WEEKDAY_LABELS[day][0] }) }, day))) })), _jsxs("fieldset", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("legend", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1", children: "Ends" }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "recurrence-end", checked: endConditionOf(value) === "never", onChange: () => handleEndCondition("never") }), "Never"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "recurrence-end", "aria-label": "After", checked: endConditionOf(value) === "count", onChange: () => handleEndCondition("count") }), "After", _jsx("input", { type: "number", min: 1, className: `${INPUT_CLASS} w-16`, value: value.count ?? 10, disabled: endConditionOf(value) !== "count", onChange: (e) => update({ count: Number(e.target.value) }), "aria-label": "Number of occurrences" }), "occurrences"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "recurrence-end", "aria-label": "On", checked: endConditionOf(value) === "until", onChange: () => handleEndCondition("until") }), "On", _jsx("input", { type: "date", className: INPUT_CLASS, value: value.until ? value.until.slice(0, 10) : "", disabled: endConditionOf(value) !== "until", onChange: (e) => update({ until: new Date(e.target.value).toISOString() }), "aria-label": "End date" })] })] }), ((value.freq !== "weekly" || (value.byDay ?? []).length > 0)) && (_jsxs("p", { className: "text-xs text-text-muted", children: ["Repeats ", describeRecurrence(value), "."] }))] }))] }));
89
+ ].join(" "), children: _jsx("span", { "aria-hidden": "true", children: WEEKDAY_LABELS[day][0] }) }, day))) })), _jsxs("fieldset", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("legend", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1", children: "Ends" }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "recurrence-end", checked: endConditionOf(value) === "never", onChange: () => handleEndCondition("never") }), "Never"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "recurrence-end", "aria-label": "After", checked: endConditionOf(value) === "count", onChange: () => handleEndCondition("count") }), "After", _jsx("input", { type: "number", min: 1, className: `${INPUT_CLASS} w-16`, value: value.count ?? 10, disabled: endConditionOf(value) !== "count", onChange: (e) => handleCount(e.target.value), "aria-label": "Number of occurrences" }), "occurrences"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "recurrence-end", "aria-label": "On", checked: endConditionOf(value) === "until", onChange: () => handleEndCondition("until") }), "On", _jsx("input", { type: "date", className: INPUT_CLASS, value: value.until ? recurrenceUntilDateKey(value.until, allDay) : "", disabled: endConditionOf(value) !== "until", onChange: (e) => handleUntil(e.target.value), "aria-label": "End date" })] })] }), (value.freq !== "weekly" || (value.byDay ?? []).length > 0) && (_jsxs("p", { className: "text-xs text-text-muted", children: ["Repeats ", describeRecurrence(value), "."] }))] }))] }));
55
90
  }
@@ -0,0 +1,19 @@
1
+ import React, { RefObject } from "react";
2
+ import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ export interface ResourcePickerProps {
4
+ anchorRef: RefObject<HTMLElement | null>;
5
+ onClose: () => void;
6
+ onSelect: (mailbox: Mailbox) => void;
7
+ /** Addresses already added as attendees (lowercased by the caller) — excluded from the list so the
8
+ * same room/equipment mailbox can't be picked twice. */
9
+ excludeAddresses: string[];
10
+ }
11
+ /**
12
+ * A searchable dropdown of bookable resource mailboxes (rooms/equipment), for adding one as a `"resource"`
13
+ * attendee in `EventModal` without typing its raw address. Fetches the caller's full visible resource-mailbox
14
+ * list once on open and filters it client-side by name/address as the reader types — the same
15
+ * "fetch-flat-list, filter-client-side" contract every other list endpoint in this codebase already uses
16
+ * (see `calendarApi.ts`'s `listCalendarEvents` doc comment), since there is no dedicated server-side search
17
+ * endpoint for mailboxes and the resource-mailbox count in any one org is expected to be small.
18
+ */
19
+ export default function ResourcePicker({ anchorRef, onClose, onSelect, excludeAddresses }: ResourcePickerProps): React.JSX.Element;
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+ import { CalendarOccurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
3
+ export interface SplitDayColumn {
4
+ folderUid: string;
5
+ name: string;
6
+ color: string;
7
+ }
8
+ export interface SplitDayViewProps {
9
+ day: Date;
10
+ columns: SplitDayColumn[];
11
+ /** Already expanded for `day` — see `recurrence.ts`'s `expandAllOccurrences`. Not pre-filtered by
12
+ * calendar; this component splits them into columns itself by `occurrence.folderUid`. */
13
+ occurrences: CalendarOccurrence[];
14
+ onSelectEvent: (occurrence: CalendarOccurrence) => void;
15
+ /** Click on an empty slot in one calendar's column — the calendar it belongs to is `folderUid`. */
16
+ onSelectSlot: (start: Date, end: Date, folderUid: string) => void;
17
+ }
18
+ /**
19
+ * Outlook's "Split" view: one column per checked calendar, all showing the same single day, so
20
+ * events across calendars can be compared side by side. A deliberately separate component from
21
+ * `TimeGridView` rather than a generalization of it — it shares that component's visual language
22
+ * (same hour rail, same slot height) but is genuinely simpler in one respect: no drag-to-move/resize.
23
+ * Dragging an event between calendar columns would mean reassigning its `folderUid`, a real feature
24
+ * `moveOccurrence`/`resolveDragAction` don't support today; rather than bolt that on, this view is
25
+ * click-only (select an event, or click a slot to create one already targeted at that column's
26
+ * calendar) — a practical scope cut, not an oversight.
27
+ */
28
+ export default function SplitDayView({ day, columns, occurrences, onSelectEvent, onSelectSlot }: SplitDayViewProps): React.JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { addMinutes, format, isSameDay, startOfDay } from "date-fns";
2
+ import { addDays, addMinutes, format, startOfDay } from "date-fns";
3
+ import { occursOnDay } from "./allDay.js";
3
4
  const HOUR_HEIGHT_PX = 48;
4
5
  const SLOT_MINUTES = 30;
5
6
  const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
@@ -15,7 +16,7 @@ const HOURS = Array.from({ length: 24 }, (_, h) => h);
15
16
  * calendar) — a practical scope cut, not an oversight.
16
17
  */
17
18
  export default function SplitDayView({ day, columns, occurrences, onSelectEvent, onSelectSlot }) {
18
- const dayOccurrences = occurrences.filter((occ) => isSameDay(new Date(occ.startDate), day));
19
+ const dayOccurrences = occurrences.filter((occ) => occursOnDay(occ, day));
19
20
  const dayStart = startOfDay(day);
20
21
  return (_jsxs("div", { role: "region", "aria-label": "Split view", className: "flex-1 flex flex-col min-h-0 overflow-y-auto", children: [_jsxs("div", { className: "flex border-b border-border shrink-0", children: [_jsx("div", { className: "w-14 shrink-0" }), columns.map((col) => (_jsxs("div", { className: "flex-1 min-w-0 py-1 flex items-center justify-center gap-1.5 border-l border-border", children: [_jsx("span", { "aria-hidden": "true", className: "w-2 h-2 rounded-full shrink-0", style: { backgroundColor: col.color } }), _jsx("span", { className: "text-xs font-semibold truncate", children: col.name })] }, col.folderUid)))] }), _jsxs("div", { className: "flex flex-1", children: [_jsx("div", { className: "w-14 shrink-0", children: HOURS.map((hour) => (_jsx("div", { style: { height: HOUR_HEIGHT_PX }, className: "text-[10px] text-text-muted text-right pr-1.5 -mt-1.5", children: hour === 0 ? "" : format(new Date(2000, 0, 1, hour), "ha") }, hour))) }), columns.map((col) => (_jsx(SplitColumn, { column: col, dayStart: dayStart, occurrences: dayOccurrences.filter((occ) => occ.folderUid === col.folderUid), onSelectEvent: onSelectEvent, onSelectSlot: onSelectSlot }, col.folderUid)))] })] }));
21
22
  }
@@ -26,8 +27,13 @@ function SplitColumn({ column, dayStart, occurrences, onSelectEvent, onSelectSlo
26
27
  }), occurrences.map((occurrence) => {
27
28
  const start = new Date(occurrence.startDate);
28
29
  const end = new Date(occurrence.endDate);
29
- const top = ((start.getTime() - dayStart.getTime()) / 60000 / 60) * HOUR_HEIGHT_PX;
30
- const height = Math.max(((end.getTime() - start.getTime()) / 60000 / 60) * HOUR_HEIGHT_PX, 16);
30
+ // Clipped to this day, so a multi-day event fills only the part of the day it covers. An
31
+ // all-day event is date-only (see `allDay.ts`), so it always spans the whole local day.
32
+ const dayEnd = addDays(dayStart, 1).getTime();
33
+ const visibleStart = occurrence.allDay ? dayStart.getTime() : Math.max(start.getTime(), dayStart.getTime());
34
+ const visibleEnd = occurrence.allDay ? dayEnd : Math.min(end.getTime(), dayEnd);
35
+ const top = ((visibleStart - dayStart.getTime()) / 60000 / 60) * HOUR_HEIGHT_PX;
36
+ const height = Math.max(((visibleEnd - visibleStart) / 60000 / 60) * HOUR_HEIGHT_PX, 16);
31
37
  const isFree = occurrence.busyStatus === "free";
32
38
  return (_jsxs("div", { onClick: () => onSelectEvent(occurrence), style: {
33
39
  position: "absolute",
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ import { CalendarOccurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
3
+ export interface TimeGridViewProps {
4
+ /** One day for the Day view, seven for the Week view, five for the Work Week view. */
5
+ days: Date[];
6
+ /** Already expanded for the visible range — see `recurrence.ts`'s `expandAllOccurrences`. */
7
+ occurrences: CalendarOccurrence[];
8
+ /** Each occurrence's own calendar color, keyed by `folderUid` — see `calendarColors.ts`. */
9
+ folderColors: Record<string, string>;
10
+ onSelectEvent: (occurrence: CalendarOccurrence) => void;
11
+ /** Click on an empty slot — start/end default to a 30-minute block there, for the "New event" flow. */
12
+ onSelectSlot: (start: Date, end: Date) => void;
13
+ }
14
+ /** An hourly time grid (Week, Work Week, or Day view — they share all their rendering/drag logic and
15
+ * differ only in how many day columns are shown), with a day-header row and all-day events in a
16
+ * header strip above it. */
17
+ export default function TimeGridView({ days, occurrences, folderColors, onSelectEvent, onSelectSlot }: TimeGridViewProps): React.JSX.Element;
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useDraggable, useDroppable } from "@dnd-kit/core";
3
- import { addMinutes, format, isSameDay, isToday, startOfDay } from "date-fns";
3
+ import { addDays, addMinutes, format, isToday, startOfDay } from "date-fns";
4
4
  import { eventDragId, resizeDragId, slotDropId } from "@rapidmx/react-shared/calendar/calendarDragIds.js";
5
+ import { occursOnDay, startsOnDay } from "./allDay.js";
5
6
  const HOUR_HEIGHT_PX = 48;
6
7
  const SLOT_MINUTES = 30;
7
8
  const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
@@ -13,20 +14,37 @@ export default function TimeGridView({ days, occurrences, folderColors, onSelect
13
14
  const allDayEvents = occurrences.filter((occ) => occ.allDay);
14
15
  const timedEvents = occurrences.filter((occ) => !occ.allDay);
15
16
  return (_jsxs("div", { className: "flex-1 flex flex-col min-h-0 overflow-y-auto", children: [_jsxs("div", { className: "flex border-b border-border shrink-0", children: [_jsx("div", { className: "w-14 shrink-0" }), days.map((day) => (_jsx("div", { className: ["flex-1 min-w-0 py-1 text-center text-xs font-semibold border-l border-border", isToday(day) ? "text-primary-dark" : ""].join(" "), children: format(day, "EEE d") }, day.toISOString())))] }), allDayEvents.length > 0 && (_jsxs("div", { className: "flex border-b border-border shrink-0", children: [_jsx("div", { className: "w-14 shrink-0" }), days.map((day) => (_jsx("div", { className: "flex-1 min-w-0 p-1 flex flex-col gap-0.5 border-l border-border", children: allDayEvents
16
- .filter((occ) => isSameDay(new Date(occ.startDate), day))
17
- .map((occ) => (_jsx("button", { type: "button", onClick: () => onSelectEvent(occ), style: { backgroundColor: folderColors[occ.folderUid], color: "#fff" }, className: "text-xs text-left truncate rounded-sm px-1.5 py-0.5", children: occ.title }, occ.occurrenceKey))) }, day.toISOString())))] })), _jsxs("div", { className: "flex flex-1", children: [_jsx("div", { className: "w-14 shrink-0", children: HOURS.map((hour) => (_jsx("div", { style: { height: HOUR_HEIGHT_PX }, className: "text-[10px] text-text-muted text-right pr-1.5 -mt-1.5", children: hour === 0 ? "" : format(new Date(2000, 0, 1, hour), "ha") }, hour))) }), days.map((day) => (_jsx(DayColumn, { day: day, occurrences: timedEvents.filter((occ) => isSameDay(new Date(occ.startDate), day)), folderColors: folderColors, onSelectEvent: onSelectEvent, onSelectSlot: onSelectSlot }, day.toISOString())))] })] }));
17
+ .filter((occ) => occursOnDay(occ, day))
18
+ .map((occ) => (_jsx("button", { type: "button", onClick: () => onSelectEvent(occ), style: { backgroundColor: folderColors[occ.folderUid], color: "#fff" }, className: "text-xs text-left truncate rounded-sm px-1.5 py-0.5", children: occ.title }, occ.occurrenceKey))) }, day.toISOString())))] })), _jsxs("div", { className: "flex flex-1", children: [_jsx("div", { className: "w-14 shrink-0", children: HOURS.map((hour) => (_jsx("div", { style: { height: HOUR_HEIGHT_PX }, className: "text-[10px] text-text-muted text-right pr-1.5 -mt-1.5", children: hour === 0 ? "" : format(new Date(2000, 0, 1, hour), "ha") }, hour))) }), days.map((day) => (_jsx(DayColumn, { day: day, occurrences: timedEvents.filter((occ) => occursOnDay(occ, day)), folderColors: folderColors, onSelectEvent: onSelectEvent, onSelectSlot: onSelectSlot }, day.toISOString())))] })] }));
18
19
  }
19
20
  function DayColumn({ day, occurrences, folderColors, onSelectEvent, onSelectSlot }) {
20
21
  const dayStart = startOfDay(day);
21
22
  return (_jsxs("div", { className: ["flex-1 min-w-0 relative border-l border-border", isToday(day) ? "bg-primary/5" : ""].join(" "), children: [Array.from({ length: SLOTS_PER_DAY }, (_, i) => {
22
23
  const slotStart = addMinutes(dayStart, i * SLOT_MINUTES);
23
24
  return _jsx(TimeSlot, { start: slotStart, onSelectSlot: onSelectSlot }, i);
24
- }), occurrences.map((occurrence) => (_jsx(EventBlock, { occurrence: occurrence, color: folderColors[occurrence.folderUid], dayStart: dayStart, onSelect: onSelectEvent }, occurrence.occurrenceKey)))] }));
25
+ }), occurrences.map((occurrence) => startsOnDay(occurrence, day) ? (_jsx(EventBlock, { occurrence: occurrence, color: folderColors[occurrence.folderUid], dayStart: dayStart, onSelect: onSelectEvent }, occurrence.occurrenceKey)) : (_jsx(ContinuationBlock, { occurrence: occurrence, color: folderColors[occurrence.folderUid], dayStart: dayStart, onSelect: onSelectEvent }, occurrence.occurrenceKey)))] }));
25
26
  }
26
27
  function TimeSlot({ start, onSelectSlot }) {
27
28
  const { setNodeRef, isOver } = useDroppable({ id: slotDropId(start) });
28
29
  return (_jsx("button", { ref: setNodeRef, type: "button", onClick: () => onSelectSlot(start, addMinutes(start, SLOT_MINUTES)), style: { height: HOUR_HEIGHT_PX / 2 }, className: ["block w-full border-b border-border/50 text-left", isOver ? "bg-primary/10" : ""].join(" "), "aria-label": `New event at ${format(start, "h:mm a, MMM d")}` }));
29
30
  }
31
+ /** A block's position within its day column. A multi-day event is drawn in every day column it
32
+ * overlaps, clipped to that day. */
33
+ function blockGeometry(occurrence, dayStart) {
34
+ const visibleStart = Math.max(new Date(occurrence.startDate).getTime(), dayStart.getTime());
35
+ const visibleEnd = Math.min(new Date(occurrence.endDate).getTime(), addDays(dayStart, 1).getTime());
36
+ return {
37
+ top: ((visibleStart - dayStart.getTime()) / 60000 / 60) * HOUR_HEIGHT_PX,
38
+ height: Math.max(((visibleEnd - visibleStart) / 60000 / 60) * HOUR_HEIGHT_PX, 16),
39
+ };
40
+ }
41
+ /** A multi-day event's block in a day column after its first: clickable, but not draggable/resizable
42
+ * (its drag ids are already taken by the start day's block). */
43
+ function ContinuationBlock({ occurrence, color, dayStart, onSelect, }) {
44
+ const isFree = occurrence.busyStatus === "free";
45
+ const { top, height } = blockGeometry(occurrence, dayStart);
46
+ return (_jsx("div", { onClick: () => onSelect(occurrence), style: { position: "absolute", top, height, left: 2, right: 2, ...(isFree ? undefined : { backgroundColor: color, color: "#fff" }) }, className: ["rounded-sm px-1.5 py-0.5 text-xs text-left overflow-hidden cursor-pointer", isFree ? "bg-surface-alt text-text-muted" : ""].join(" "), children: _jsx("div", { className: "font-medium truncate", children: occurrence.title }) }));
47
+ }
30
48
  function EventBlock({ occurrence, color, dayStart, onSelect, }) {
31
49
  const { setNodeRef, listeners, attributes, transform, isDragging } = useDraggable({ id: eventDragId(occurrence) });
32
50
  const { setNodeRef: setResizeRef, listeners: resizeListeners, attributes: resizeAttributes } = useDraggable({
@@ -34,13 +52,11 @@ function EventBlock({ occurrence, color, dayStart, onSelect, }) {
34
52
  });
35
53
  const isFree = occurrence.busyStatus === "free";
36
54
  const start = new Date(occurrence.startDate);
37
- const end = new Date(occurrence.endDate);
38
- const top = ((start.getTime() - dayStart.getTime()) / 60000 / 60) * HOUR_HEIGHT_PX;
55
+ const { top, height } = blockGeometry(occurrence, dayStart);
39
56
  // The resize handle's own drag position has no live pixel preview (it only applies its ns-resize
40
57
  // affordance) — the actual new end time is computed from whichever slot it's dropped onto (see
41
58
  // `resolveDragAction`'s "resize" case), snapped to the grid rather than following the pointer
42
59
  // continuously. Simpler, and avoids a visual preview that could disagree with the snapped result.
43
- const height = Math.max(((end.getTime() - start.getTime()) / 60000 / 60) * HOUR_HEIGHT_PX, 16);
44
60
  return (_jsxs("div", { ref: setNodeRef, onClick: () => onSelect(occurrence), style: {
45
61
  position: "absolute",
46
62
  top,
@@ -0,0 +1,51 @@
1
+ import { WeekdayCode } from "@rapidmx/react-shared/calendar/calendarApi.js";
2
+ import { CalendarOccurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
3
+ /**
4
+ * All-day events are stored date-only: `startDate` is UTC midnight of the first day and `endDate` is UTC
5
+ * midnight of the day *after* the last day (exclusive, like iCalendar's `DTEND;VALUE=DATE`). That keeps
6
+ * an all-day event on the same calendar date for every viewer, whatever their timezone.
7
+ *
8
+ * Older events were saved as the creator's *local* midnight converted to UTC (e.g. `...T22:00Z` for a
9
+ * UTC+2 creator), so reading one rounds the instant to the nearest UTC midnight rather than slicing the
10
+ * ISO string — which gives the intended date for both formats.
11
+ */
12
+ export declare function allDayDateKey(iso: string): string;
13
+ /** `YYYY-MM-DD` -> the stored UTC-midnight instant for that date. */
14
+ export declare function allDayInstant(dateKey: string): string;
15
+ /** Adds `days` to a `YYYY-MM-DD` date key. */
16
+ export declare function addDaysToKey(dateKey: string, days: number): string;
17
+ /** The local calendar date `day` (a local-midnight `Date` from a grid view) as a `YYYY-MM-DD` key. */
18
+ export declare function localDateKey(day: Date): string;
19
+ /**
20
+ * A recurrence rule's inclusive "Ends on" date -> its stored `until` instant: the end of that day in the
21
+ * frame the series expands in. A timed series expands in its own (normally the viewer's) timezone, so that's
22
+ * the end of the local day - `new Date("YYYY-MM-DD")` would be UTC midnight, the previous day west of UTC.
23
+ * An all-day series expands in UTC (react-shared's `expandOccurrences`), so it's the end of the UTC day: the
24
+ * local end of day is already the next UTC day west of UTC and would add one more occurrence.
25
+ */
26
+ export declare function recurrenceUntilInstant(dateKey: string, allDay: boolean): string;
27
+ /**
28
+ * Inverse of `recurrenceUntilInstant()`. An all-day series' value is recognized explicitly in its stored
29
+ * `T23:59:59.999Z` form (and as a bare UTC midnight, the oldest form). Anything else is a legacy value stored
30
+ * as the creator's *local* end of day, which can land anywhere from just before 10:00Z on the chosen date (UTC+14)
31
+ * to just before 12:00Z the day after (UTC-12) - a 26-hour spread no UTC-only rounding can resolve (UTC+14 and UTC-10
32
+ * creators' values are exactly 24h apart for the same date), so it reads back on the local calendar, the
33
+ * frame it was written in.
34
+ */
35
+ export declare function recurrenceUntilDateKey(until: string, allDay: boolean): string;
36
+ /** The weekday an event starts on, in the frame its series expands in: the UTC date for an all-day event
37
+ * (`start`'s date part), the event's own timezone otherwise (`start` being a local `datetime-local` value).
38
+ * `undefined` while the start input is empty/invalid. */
39
+ export declare function startWeekdayCode(start: string, allDay: boolean, timezone: string | undefined): WeekdayCode | undefined;
40
+ /**
41
+ * Whether `occurrence` should be shown on the grid's local calendar day `day`. All-day events cover every
42
+ * date from their start date up to (not including) their exclusive end date — at least their start date.
43
+ * Timed events cover every local day their [start, end) interval overlaps, so a multi-day event shows on
44
+ * each day rather than only on its start day.
45
+ */
46
+ export declare function occursOnDay(occurrence: CalendarOccurrence, day: Date): boolean;
47
+ /**
48
+ * Whether `day` is the first day `occurrence` covers. Grid views only make that day's chip/block
49
+ * draggable — a drag id must be unique, so the continuation copies on later days are display-only.
50
+ */
51
+ export declare function startsOnDay(occurrence: CalendarOccurrence, day: Date): boolean;
@@ -0,0 +1,111 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { format } from "date-fns";
6
+ import { toEventWallClock } from "@rapidmx/react-shared/calendar/recurrence.js";
7
+ const MS_PER_DAY = 86400000;
8
+ /**
9
+ * All-day events are stored date-only: `startDate` is UTC midnight of the first day and `endDate` is UTC
10
+ * midnight of the day *after* the last day (exclusive, like iCalendar's `DTEND;VALUE=DATE`). That keeps
11
+ * an all-day event on the same calendar date for every viewer, whatever their timezone.
12
+ *
13
+ * Older events were saved as the creator's *local* midnight converted to UTC (e.g. `...T22:00Z` for a
14
+ * UTC+2 creator), so reading one rounds the instant to the nearest UTC midnight rather than slicing the
15
+ * ISO string — which gives the intended date for both formats.
16
+ */
17
+ export function allDayDateKey(iso) {
18
+ const rounded = Math.round(new Date(iso).getTime() / MS_PER_DAY) * MS_PER_DAY;
19
+ return new Date(rounded).toISOString().slice(0, 10);
20
+ }
21
+ /** `YYYY-MM-DD` -> the stored UTC-midnight instant for that date. */
22
+ export function allDayInstant(dateKey) {
23
+ return `${dateKey}T00:00:00.000Z`;
24
+ }
25
+ /** Adds `days` to a `YYYY-MM-DD` date key. */
26
+ export function addDaysToKey(dateKey, days) {
27
+ return new Date(new Date(allDayInstant(dateKey)).getTime() + days * MS_PER_DAY).toISOString().slice(0, 10);
28
+ }
29
+ /** The local calendar date `day` (a local-midnight `Date` from a grid view) as a `YYYY-MM-DD` key. */
30
+ export function localDateKey(day) {
31
+ return format(day, "yyyy-MM-dd");
32
+ }
33
+ /**
34
+ * A recurrence rule's inclusive "Ends on" date -> its stored `until` instant: the end of that day in the
35
+ * frame the series expands in. A timed series expands in its own (normally the viewer's) timezone, so that's
36
+ * the end of the local day - `new Date("YYYY-MM-DD")` would be UTC midnight, the previous day west of UTC.
37
+ * An all-day series expands in UTC (react-shared's `expandOccurrences`), so it's the end of the UTC day: the
38
+ * local end of day is already the next UTC day west of UTC and would add one more occurrence.
39
+ */
40
+ export function recurrenceUntilInstant(dateKey, allDay) {
41
+ if (allDay) {
42
+ return `${dateKey}T23:59:59.999Z`;
43
+ }
44
+ const [y, m, d] = dateKey.split("-").map(Number);
45
+ return new Date(y, m - 1, d, 23, 59, 59, 999).toISOString();
46
+ }
47
+ /** The stored form of an all-day series' "Ends on" date - the last millisecond of that UTC day. */
48
+ const ALL_DAY_UNTIL_PATTERN = /^(\d{4}-\d{2}-\d{2})T23:59:59\.999Z$/;
49
+ /** A bare UTC midnight - how the oldest editor (and date-only `UNTIL` values) stored the chosen date itself. */
50
+ const UTC_MIDNIGHT_PATTERN = /^(\d{4}-\d{2}-\d{2})T00:00:00(?:\.000)?Z$/;
51
+ /**
52
+ * Inverse of `recurrenceUntilInstant()`. An all-day series' value is recognized explicitly in its stored
53
+ * `T23:59:59.999Z` form (and as a bare UTC midnight, the oldest form). Anything else is a legacy value stored
54
+ * as the creator's *local* end of day, which can land anywhere from just before 10:00Z on the chosen date (UTC+14)
55
+ * to just before 12:00Z the day after (UTC-12) - a 26-hour spread no UTC-only rounding can resolve (UTC+14 and UTC-10
56
+ * creators' values are exactly 24h apart for the same date), so it reads back on the local calendar, the
57
+ * frame it was written in.
58
+ */
59
+ export function recurrenceUntilDateKey(until, allDay) {
60
+ if (allDay) {
61
+ const stored = ALL_DAY_UNTIL_PATTERN.exec(until) ?? UTC_MIDNIGHT_PATTERN.exec(until);
62
+ if (stored) {
63
+ return stored[1];
64
+ }
65
+ }
66
+ return format(new Date(until), "yyyy-MM-dd");
67
+ }
68
+ /** `Date#getUTCDay()` index -> RFC 5545 weekday code. */
69
+ const JS_WEEKDAY_CODES = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
70
+ /** The weekday an event starts on, in the frame its series expands in: the UTC date for an all-day event
71
+ * (`start`'s date part), the event's own timezone otherwise (`start` being a local `datetime-local` value).
72
+ * `undefined` while the start input is empty/invalid. */
73
+ export function startWeekdayCode(start, allDay, timezone) {
74
+ const ms = allDay ? new Date(allDayInstant(start.slice(0, 10))).getTime() : new Date(start).getTime();
75
+ if (Number.isNaN(ms)) {
76
+ return undefined;
77
+ }
78
+ return JS_WEEKDAY_CODES[new Date(toEventWallClock(ms, timezone, allDay)).getUTCDay()];
79
+ }
80
+ /**
81
+ * Whether `occurrence` should be shown on the grid's local calendar day `day`. All-day events cover every
82
+ * date from their start date up to (not including) their exclusive end date — at least their start date.
83
+ * Timed events cover every local day their [start, end) interval overlaps, so a multi-day event shows on
84
+ * each day rather than only on its start day.
85
+ */
86
+ export function occursOnDay(occurrence, day) {
87
+ if (occurrence.allDay) {
88
+ const key = localDateKey(day);
89
+ const startKey = allDayDateKey(occurrence.startDate);
90
+ const endKey = allDayDateKey(occurrence.endDate);
91
+ return key === startKey || (key > startKey && key < endKey);
92
+ }
93
+ const dayStart = new Date(day.getFullYear(), day.getMonth(), day.getDate());
94
+ const dayEnd = new Date(day.getFullYear(), day.getMonth(), day.getDate() + 1);
95
+ const start = new Date(occurrence.startDate);
96
+ const end = new Date(occurrence.endDate);
97
+ if (start.getTime() === end.getTime()) {
98
+ return start >= dayStart && start < dayEnd;
99
+ }
100
+ return start < dayEnd && end > dayStart;
101
+ }
102
+ /**
103
+ * Whether `day` is the first day `occurrence` covers. Grid views only make that day's chip/block
104
+ * draggable — a drag id must be unique, so the continuation copies on later days are display-only.
105
+ */
106
+ export function startsOnDay(occurrence, day) {
107
+ if (occurrence.allDay) {
108
+ return allDayDateKey(occurrence.startDate) === localDateKey(day);
109
+ }
110
+ return localDateKey(new Date(occurrence.startDate)) === localDateKey(day);
111
+ }
@@ -0,0 +1,40 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ import { Folder, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ import { AppShellProps } from "../../layout/AppShell.js";
4
+ export type CalendarShellProps = Omit<AppShellProps, "active">;
5
+ /** One mailbox's own `calendar`-type folders. `error` is set (and `calendarFolders` left empty) when that
6
+ * mailbox's own `listFolders()` failed - one mailbox's failure never hides every other mailbox's calendars. */
7
+ export interface MailboxCalendars {
8
+ mailbox: Mailbox;
9
+ calendarFolders: Folder[];
10
+ error?: string;
11
+ }
12
+ export interface CalendarShellContextValue {
13
+ /** The default mailbox for "+ New event" (`?mailboxUid=` if accessible, else the caller's own mailbox,
14
+ * else the first accessible one) - every accessible mailbox's calendars are shown regardless. */
15
+ mailboxUid?: string;
16
+ /** `mailboxUid`'s first `calendar`-type folder - the default target for "+ New event". */
17
+ folderUid?: string;
18
+ /** Every accessible mailbox's `calendar`-type folders, flattened - the set of calendars that can be
19
+ * checked/shown (see `CalendarListSidebar`). Each folder's own `mailboxUid` says which mailbox it's in. */
20
+ calendarFolders: Folder[];
21
+ /** The same folders, grouped per mailbox, for the sidebar's per-mailbox sections. */
22
+ mailboxCalendars: MailboxCalendars[];
23
+ mailboxes: Mailbox[];
24
+ /** Re-fetches every mailbox's folders (e.g. after creating a new calendar) without a full reload. */
25
+ reloadFolders: () => void;
26
+ /** The display color for a calendar folder: its own `color`, else the default for the caller's own
27
+ * mailbox, else its mailbox's `accentColorForMailbox()` - so a shared mailbox's calendar is visually
28
+ * distinct from the caller's own even when neither has a color set. */
29
+ colorFor: (folder: Folder) => string;
30
+ }
31
+ /** Reads the calendars the Calendar is currently showing, as resolved by the enclosing `CalendarShell`. */
32
+ export declare function useCalendarShell(): CalendarShellContextValue;
33
+ /**
34
+ * The Calendar app's shell. Fetches every accessible mailbox's calendar folders in parallel so shared
35
+ * mailboxes' calendars show alongside the caller's own, each mailbox color-coded (see `colorFor`) - there
36
+ * is no mailbox switcher. The actual view being looked at (month/week/day, and which one) is *not* shell
37
+ * state — it lives in `apps/www/calendar/index.tsx`'s own local state, since navigating between
38
+ * views/dates must be instant (no full page reload) and this framework has no client-side router.
39
+ */
40
+ export default function CalendarShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<CalendarShellProps>): React.JSX.Element;
@@ -29,7 +29,7 @@ export function useCalendarShell() {
29
29
  * state — it lives in `apps/www/calendar/index.tsx`'s own local state, since navigating between
30
30
  * views/dates must be instant (no full page reload) and this framework has no client-side router.
31
31
  */
32
- export default function CalendarShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, children, }) {
32
+ export default function CalendarShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
33
33
  const [status, setStatus] = useState("checking");
34
34
  const [error, setError] = useState(null);
35
35
  const [mailboxes, setMailboxes] = useState([]);
@@ -62,7 +62,7 @@ export default function CalendarShell({ userUid, authServerUrl, impersonating, i
62
62
  return;
63
63
  }
64
64
  let cancelled = false;
65
- Promise.all(mailboxes.map((mailbox) => listFolders(mailbox.uid)
65
+ void Promise.all(mailboxes.map((mailbox) => listFolders(mailbox.uid)
66
66
  .then((folders) => ({ mailbox, calendarFolders: folders.filter((f) => f.type === "calendar") }))
67
67
  .catch((err) => ({
68
68
  mailbox,
@@ -100,5 +100,5 @@ export default function CalendarShell({ userUid, authServerUrl, impersonating, i
100
100
  else if (userUid && status === "ready") {
101
101
  inner = (_jsx("div", { className: "flex-1 min-w-0 flex flex-col", children: _jsx(CalendarShellContext.Provider, { value: contextValue, children: children }) }));
102
102
  }
103
- return (_jsx(AppShell, { active: "calendar", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, children: inner }));
103
+ return (_jsx(AppShell, { active: "calendar", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner }));
104
104
  }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { Contact } from "@rapidmx/react-shared/contacts/contactsApi.js";
3
+ export interface ContactDetailPaneProps {
4
+ contact: Contact;
5
+ onEdit: () => void;
6
+ onDelete: () => void;
7
+ /** Present only on the mobile detail route — renders a "back to contacts" link above the header. Absent
8
+ * on the desktop inline pane, which never navigates away (selecting a different contact just swaps
9
+ * `contact` in place). */
10
+ backHref?: string;
11
+ /** Called after one of the contact's key changes was accepted or kept, or turned out to be stale - the caller
12
+ * re-reads the contact. */
13
+ onKeysChanged?: () => void;
14
+ /** `false` when the reader is known not to be able to change this contact's keys (the actions are hidden); `undefined`
15
+ * when unknown (they're hidden after a 403). */
16
+ canResolveKeys?: boolean;
17
+ }
18
+ /**
19
+ * A contact's read-only detail view. Shared by the desktop inline pane (`apps/www/contacts/index.tsx`,
20
+ * always visible alongside the contact list) and the mobile detail route
21
+ * (`apps/www/contacts/[uid].tsx`, a full page on its own reached by tapping a contact row).
22
+ */
23
+ export default function ContactDetailPane({ contact, onEdit, onDelete, backHref, onKeysChanged, canResolveKeys }: ContactDetailPaneProps): React.JSX.Element;
@@ -1,20 +1,58 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, 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 { useState } from "react";
2
7
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
3
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
4
8
  import ContactAvatar from "@rapidmx/react-shared/components/avatar/ContactAvatar.js";
9
+ import KeyChangeReview from "./KeyChangeReview.js";
10
+ import { KEY_CHANGE_STALE_MESSAGE, formatDate, keyPinnedSince, revocationLabel } from "./contactKeys.js";
11
+ import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
5
12
  /** Groups a hex fingerprint into 4-character blocks (`ab12 cd34 ...`) for out-of-band verification -
6
13
  * the spec's own use case ("compare this over the phone") is materially easier with a grouped string
7
14
  * than one 64-character run. */
8
15
  function formatFingerprint(fingerprint) {
9
16
  return fingerprint.match(/.{1,4}/g)?.join(" ") ?? fingerprint;
10
17
  }
18
+ /** A revoked key's reason: a superseded key was routinely replaced, a revoked one must not be trusted. */
19
+ function RevocationBadge({ label }) {
20
+ if (!label) {
21
+ return null;
22
+ }
23
+ return _jsxs("span", { className: label === "superseded" ? "text-text-muted" : "text-danger", children: [" (", label, ")"] });
24
+ }
11
25
  /**
12
26
  * A contact's read-only detail view. Shared by the desktop inline pane (`apps/www/contacts/index.tsx`,
13
27
  * always visible alongside the contact list) and the mobile detail route
14
28
  * (`apps/www/contacts/[uid].tsx`, a full page on its own reached by tapping a contact row).
15
29
  */
16
- export default function ContactDetailPane({ contact, onEdit, onDelete, backHref }) {
17
- return (_jsxs("div", { role: "region", "aria-label": "Contact details", className: "max-w-xl flex flex-col gap-5", children: [backHref && (_jsx("a", { href: backHref, className: "text-sm text-primary-dark hover:underline", children: "\u2190 Back to contacts" })), _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(ContactAvatar, { displayName: contact.displayName, size: 48 }), _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold tracking-tight", children: contact.displayName }), contact.jobTitle && contact.company && (_jsxs("p", { className: "text-sm text-text-muted mt-0.5", children: [contact.jobTitle, " at ", contact.company] }))] })] }), _jsxs("div", { className: "flex gap-2 shrink-0", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onEdit, children: "Edit" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", onClick: onDelete, children: "Delete" })] })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6 flex flex-col gap-4 text-sm", children: [contact.emails.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "text-text-muted text-xs font-bold uppercase tracking-wide mb-1", children: "Email" }), contact.emails.map((e, i) => (_jsxs("div", { children: [e.address, " ", _jsxs("span", { className: "text-text-muted", children: ["(", e.type, ")"] })] }, i)))] })), contact.phones.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "text-text-muted text-xs font-bold uppercase tracking-wide mb-1", children: "Phone" }), contact.phones.map((p, i) => (_jsxs("div", { children: [p.phoneNumber, " ", _jsxs("span", { className: "text-text-muted", children: ["(", p.type, ")"] })] }, i)))] })), contact.addresses.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "text-text-muted text-xs font-bold uppercase tracking-wide mb-1", children: "Address" }), contact.addresses.map((a, i) => (_jsx("div", { children: [a.street, a.city, a.state, a.postalCode, a.country].filter(Boolean).join(", ") }, i)))] })), contact.categories && contact.categories.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "text-text-muted text-xs font-bold uppercase tracking-wide mb-1", children: "Categories" }), _jsx("div", { children: contact.categories.join(", ") })] })), contact.notes && (_jsxs("div", { children: [_jsx("div", { className: "text-text-muted text-xs font-bold uppercase tracking-wide mb-1", children: "Notes" }), _jsx("div", { children: contact.notes })] })), (contact.keys && contact.keys.length > 0) || contact.encryptPreference || contact.keyConflict ? (_jsxs("div", { children: [_jsx("div", { className: "text-text-muted text-xs font-bold uppercase tracking-wide mb-1", children: "Encryption" }), contact.keyConflict && (_jsx("div", { className: "mb-2", children: _jsxs(Alert, { children: [_jsxs("p", { className: "mb-2", children: ["A different encryption key was observed for this contact on", " ", new Date(contact.keyConflict.observedAt).toLocaleDateString(), ". The previously verified key below is still the one in use \u2014 this is routine after a device change or reinstall, but it\u2019s also what a real attack looks like, so verify the new fingerprint with ", contact.displayName, " ", "directly (e.g. by phone) before trusting it. There is no automatic way to accept or reject this yet."] }), _jsxs("p", { className: "font-mono text-xs", children: ["Newly observed: ", formatFingerprint(contact.keyConflict.observedFingerprint)] })] }) })), contact.encryptPreference && (_jsx("div", { className: "mb-1", children: contact.encryptPreference.preferEncrypt === "mutual"
30
+ export default function ContactDetailPane({ contact, onEdit, onDelete, backHref, onKeysChanged, canResolveKeys }) {
31
+ // The outcome of the last key decision, kept per contact so selecting another contact doesn't carry it over.
32
+ const [keyNotice, setKeyNotice] = useState(null);
33
+ const notice = keyNotice?.contactUid === contact.uid ? keyNotice.text : null;
34
+ const keyConflicts = contact.keyConflicts ?? [];
35
+ const previousKeys = contact.previousKeys ?? [];
36
+ // Pinned signing keys vouch for signatures, so the pinned-signer cache is dropped along with re-reading the contact.
37
+ function refresh(text) {
38
+ setKeyNotice({ contactUid: contact.uid, text });
39
+ clearPinnedSignerCache();
40
+ onKeysChanged?.();
41
+ }
42
+ return (_jsxs("div", { role: "region", "aria-label": "Contact details", className: "max-w-xl flex flex-col gap-5", children: [backHref && (_jsx("a", { href: backHref, className: "text-sm text-primary-dark hover:underline", children: "\u2190 Back to contacts" })), _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(ContactAvatar, { displayName: contact.displayName, size: 48 }), _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold tracking-tight", children: contact.displayName }), contact.jobTitle && contact.company && (_jsxs("p", { className: "text-sm text-text-muted mt-0.5", children: [contact.jobTitle, " at ", contact.company] }))] })] }), _jsxs("div", { className: "flex gap-2 shrink-0", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onEdit, children: "Edit" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", onClick: onDelete, children: "Delete" })] })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6 flex flex-col gap-4 text-sm", children: [contact.emails.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "text-text-muted text-xs font-bold uppercase tracking-wide mb-1", children: "Email" }), contact.emails.map((e, i) => (_jsxs("div", { children: [e.address, " ", _jsxs("span", { className: "text-text-muted", children: ["(", e.type, ")"] })] }, i)))] })), contact.phones.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "text-text-muted text-xs font-bold uppercase tracking-wide mb-1", children: "Phone" }), contact.phones.map((p, i) => (_jsxs("div", { children: [p.phoneNumber, " ", _jsxs("span", { className: "text-text-muted", children: ["(", p.type, ")"] })] }, i)))] })), contact.addresses.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "text-text-muted text-xs font-bold uppercase tracking-wide mb-1", children: "Address" }), contact.addresses.map((a, i) => (_jsx("div", { children: [a.street, a.city, a.state, a.postalCode, a.country].filter(Boolean).join(", ") }, i)))] })), contact.categories && contact.categories.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "text-text-muted text-xs font-bold uppercase tracking-wide mb-1", children: "Categories" }), _jsx("div", { children: contact.categories.join(", ") })] })), contact.notes && (_jsxs("div", { children: [_jsx("div", { className: "text-text-muted text-xs font-bold uppercase tracking-wide mb-1", children: "Notes" }), _jsx("div", { children: contact.notes })] })), (contact.keys && contact.keys.length > 0) ||
43
+ contact.encryptPreference ||
44
+ keyConflicts.length > 0 ||
45
+ previousKeys.length > 0 ? (_jsxs("div", { children: [_jsx("div", { className: "text-text-muted text-xs font-bold uppercase tracking-wide mb-1", children: "Encryption" }), notice && (_jsx("p", { role: "status", className: "mb-2 py-2 px-3 rounded-sm bg-surface-alt text-text", children: notice })), keyConflicts.map((conflict) => {
46
+ const pinned = contact.keys?.find((key) => key.useType === conflict.useType);
47
+ const kind = conflict.useType === "sign" ? "signing" : "encryption";
48
+ return (_jsxs("section", { "aria-label": `${conflict.useType === "sign" ? "Signing" : "Encryption"} key change`, className: "mb-3 py-3 px-3 rounded-sm bg-warning/15 text-text flex flex-col gap-2", children: [_jsxs("h2", { className: "font-semibold", children: ["A different ", kind, " key was seen for ", contact.displayName] }), _jsx("p", { children: "The current key stays in use until you decide what to do with the new one." }), _jsx(KeyChangeReview, { mailboxUid: contact.mailboxUid, address: contact.emails[0]?.address, useType: conflict.useType, ownerName: contact.displayName, current: pinned && { fingerprint: pinned.fingerprint, since: keyPinnedSince(contact, pinned) }, proposed: {
49
+ fingerprint: conflict.observedKey.fingerprint,
50
+ observedAt: conflict.observedAt,
51
+ source: conflict.source,
52
+ }, canReject: true, canResolve: canResolveKeys, onResolved: (action) => refresh(action === "accept"
53
+ ? `The new ${kind} key is now trusted for ${contact.displayName}.`
54
+ : `You kept the current ${kind} key for ${contact.displayName}.`), onPinnedKeyChanged: () => refresh(KEY_CHANGE_STALE_MESSAGE) })] }, conflict.useType));
55
+ }), contact.encryptPreference && (_jsx("div", { className: "mb-1", children: contact.encryptPreference.preferEncrypt === "mutual"
18
56
  ? "This contact also encrypts to you — messages to them can be encrypted."
19
- : "This contact has not indicated a mutual encryption preference." })), contact.keys?.map((key) => (_jsxs("div", { className: "font-mono text-xs py-0.5", children: [key.useType === "sign" ? "Signing" : "Encryption", " key: ", formatFingerprint(key.fingerprint), key.revokedAt && _jsx("span", { className: "text-danger", children: " (revoked)" })] }, key.fingerprint)))] })) : null] })] }));
57
+ : "This contact has not indicated a mutual encryption preference." })), contact.keys?.map((key) => (_jsxs("div", { className: "font-mono text-xs py-0.5", children: [key.useType === "sign" ? "Signing" : "Encryption", " key: ", formatFingerprint(key.fingerprint), _jsx(RevocationBadge, { label: revocationLabel(key) })] }, key.fingerprint))), previousKeys.length > 0 && (_jsxs("div", { className: "mt-3", children: [_jsx("div", { className: "text-text-muted text-xs font-bold mb-1", children: "Key history" }), _jsx("ul", { "aria-label": "Key history", children: previousKeys.map((key) => (_jsxs("li", { className: "text-xs py-0.5", children: [_jsxs("span", { className: "font-mono", children: [key.useType === "sign" ? "Signing" : "Encryption", " key: ", formatFingerprint(key.fingerprint)] }), _jsx(RevocationBadge, { label: revocationLabel(key) }), _jsxs("span", { className: "text-text-muted", children: [" ", "\u00B7", " ", key.replacement === "automatic" ? "Renewed automatically" : "Replaced by you", " on", " ", formatDate(key.replacedAt)] })] }, `${key.useType}:${key.fingerprint}:${key.replacedAt}`))) })] }))] })) : null] })] }));
20
58
  }