@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,348 +1,377 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { FormEvent, useState } from "react";
6
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
- import {
8
- Contact,
9
- ContactAddressKind,
10
- ContactEmail,
11
- ContactPhone,
12
- ContactPostalAddress,
13
- createContact,
14
- updateContact,
15
- } from "@rapidmx/react-shared/contacts/contactsApi.js";
16
- import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
17
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
18
- import Button from "@rapidmx/react-shared/components/buttons/Button.js";
19
- import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
20
- import { findWellKnownFolderUid } from "../../mail/findWellKnownFolderUid.js";
21
-
22
- const INPUT_CLASS =
23
- "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
24
- const SELECT_CLASS =
25
- "text-sm py-2 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
26
-
27
- export interface ContactFormProps {
28
- contact?: Contact;
29
- /** Create mode: the default mailbox (and its Contacts folder) to create in. */
30
- mailboxUid?: string;
31
- folderUid?: string;
32
- /** Create mode: every mailbox the contact could be created in - with more than one, a Mailbox selector
33
- * is shown. Choosing a mailbox other than `mailboxUid` looks up that mailbox's own Contacts folder. */
34
- mailboxes?: Mailbox[];
35
- onSaved: (contact: Contact) => void;
36
- onCancel: () => void;
37
- }
38
-
39
- const ADDRESS_KINDS: ContactAddressKind[] = ["home", "work", "other"];
40
-
41
- /**
42
- * A contact's create/edit form. Shared by the desktop inline pane (`apps/www/contacts/index.tsx`) and the
43
- * mobile detail route's own edit mode (`apps/www/contacts/[uid].tsx`) — creation itself stays
44
- * desktop-and-mobile-inline (see that route's own doc comment on why "new" never gets a dedicated route).
45
- */
46
- export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes, onSaved, onCancel }: ContactFormProps) {
47
- const [targetMailboxUid, setTargetMailboxUid] = useState(mailboxUid);
48
- const [displayName, setDisplayName] = useState(contact?.displayName ?? "");
49
- const [givenName, setGivenName] = useState(contact?.givenName ?? "");
50
- const [surname, setSurname] = useState(contact?.surname ?? "");
51
- const [company, setCompany] = useState(contact?.company ?? "");
52
- const [jobTitle, setJobTitle] = useState(contact?.jobTitle ?? "");
53
- const [notes, setNotes] = useState(contact?.notes ?? "");
54
- const [favorite, setFavorite] = useState(contact?.favorite ?? false);
55
- const [categoriesText, setCategoriesText] = useState((contact?.categories ?? []).join(", "));
56
- const [emails, setEmails] = useState<ContactEmail[]>(contact?.emails ?? []);
57
- const [phones, setPhones] = useState<ContactPhone[]>(contact?.phones ?? []);
58
- const [address, setAddress] = useState<ContactPostalAddress | null>(contact?.addresses[0] ?? null);
59
- const [error, setError] = useState<string | null>(null);
60
- const [saving, setSaving] = useState(false);
61
-
62
- function updateEmail(index: number, patch: Partial<ContactEmail>) {
63
- setEmails((prev) => prev.map((e, i) => (i === index ? { ...e, ...patch } : e)));
64
- }
65
- function removeEmail(index: number) {
66
- setEmails((prev) => prev.filter((_, i) => i !== index));
67
- }
68
- function updatePhone(index: number, patch: Partial<ContactPhone>) {
69
- setPhones((prev) => prev.map((p, i) => (i === index ? { ...p, ...patch } : p)));
70
- }
71
- function removePhone(index: number) {
72
- setPhones((prev) => prev.filter((_, i) => i !== index));
73
- }
74
-
75
- async function handleSubmit(e: FormEvent) {
76
- e.preventDefault();
77
- setError(null);
78
-
79
- if (!displayName.trim()) {
80
- setError("A display name is required.");
81
- return;
82
- }
83
-
84
- setSaving(true);
85
- try {
86
- const categories = categoriesText
87
- .split(",")
88
- .map((c) => c.trim())
89
- .filter(Boolean);
90
- const input = {
91
- displayName: displayName.trim(),
92
- givenName: givenName.trim() || undefined,
93
- surname: surname.trim() || undefined,
94
- company: company.trim() || undefined,
95
- jobTitle: jobTitle.trim() || undefined,
96
- notes: notes.trim() || undefined,
97
- favorite,
98
- categories,
99
- emails,
100
- phones,
101
- addresses: address ? [address] : [],
102
- };
103
- let saved: Contact;
104
- if (contact) {
105
- saved = await updateContact({ uid: contact.uid, version: contact.version, mailboxUid: contact.mailboxUid, folderUid: contact.folderUid, ...input });
106
- } else {
107
- const targetFolderUid =
108
- targetMailboxUid === mailboxUid ? folderUid : await findWellKnownFolderUid(targetMailboxUid as string, "contacts");
109
- if (!targetFolderUid) {
110
- setError("That mailbox has no Contacts folder.");
111
- return;
112
- }
113
- saved = await createContact({ mailboxUid: targetMailboxUid as string, folderUid: targetFolderUid, ...input });
114
- }
115
- onSaved(saved);
116
- } catch (err) {
117
- setError(err instanceof ApiRequestError ? err.message : "Could not save this contact.");
118
- } finally {
119
- setSaving(false);
120
- }
121
- }
122
-
123
- return (
124
- <form onSubmit={handleSubmit} className="max-w-xl flex flex-col gap-1">
125
- <h1 className="text-xl font-bold uppercase tracking-wide mb-3">{contact ? "Edit contact" : "New contact"}</h1>
126
-
127
- {error && <Alert>{error}</Alert>}
128
-
129
- {!contact && mailboxes && mailboxes.length > 1 && (
130
- <FormField label="Mailbox" htmlFor="contact-mailbox">
131
- <select
132
- id="contact-mailbox"
133
- className={`${SELECT_CLASS} w-full`}
134
- value={targetMailboxUid}
135
- onChange={(e) => setTargetMailboxUid(e.target.value)}
136
- >
137
- {mailboxes.map((mb) => (
138
- <option key={mb.uid} value={mb.uid}>
139
- {mb.displayName}
140
- {mb.ownerUserUid ? "" : " (shared)"}
141
- </option>
142
- ))}
143
- </select>
144
- </FormField>
145
- )}
146
-
147
- <FormField label="Display name" htmlFor="contact-displayName">
148
- <input
149
- id="contact-displayName"
150
- type="text"
151
- className={INPUT_CLASS}
152
- value={displayName}
153
- onChange={(e) => setDisplayName(e.target.value)}
154
- />
155
- </FormField>
156
-
157
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
158
- <FormField label="First name" htmlFor="contact-givenName">
159
- <input id="contact-givenName" type="text" className={INPUT_CLASS} value={givenName} onChange={(e) => setGivenName(e.target.value)} />
160
- </FormField>
161
- <FormField label="Last name" htmlFor="contact-surname">
162
- <input id="contact-surname" type="text" className={INPUT_CLASS} value={surname} onChange={(e) => setSurname(e.target.value)} />
163
- </FormField>
164
- </div>
165
-
166
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
167
- <FormField label="Company" htmlFor="contact-company">
168
- <input id="contact-company" type="text" className={INPUT_CLASS} value={company} onChange={(e) => setCompany(e.target.value)} />
169
- </FormField>
170
- <FormField label="Job title" htmlFor="contact-jobTitle">
171
- <input id="contact-jobTitle" type="text" className={INPUT_CLASS} value={jobTitle} onChange={(e) => setJobTitle(e.target.value)} />
172
- </FormField>
173
- </div>
174
-
175
- <label className="flex items-center gap-2 text-sm mb-4">
176
- <input type="checkbox" checked={favorite} onChange={(e) => setFavorite(e.target.checked)} />
177
- Favorite
178
- </label>
179
-
180
- <FormField label="Categories (comma-separated)" htmlFor="contact-categories">
181
- <input
182
- id="contact-categories"
183
- type="text"
184
- className={INPUT_CLASS}
185
- value={categoriesText}
186
- onChange={(e) => setCategoriesText(e.target.value)}
187
- placeholder="VIP, Work"
188
- />
189
- </FormField>
190
-
191
- <FormField label="Email" htmlFor="contact-emails">
192
- <div id="contact-emails" className="flex flex-col gap-2">
193
- {emails.map((email, i) => (
194
- <div key={i} className="flex gap-2">
195
- <input
196
- type="email"
197
- className={`${INPUT_CLASS} flex-1`}
198
- value={email.address}
199
- onChange={(e) => updateEmail(i, { address: e.target.value })}
200
- aria-label={`Email address ${i + 1}`}
201
- />
202
- <select
203
- className={SELECT_CLASS}
204
- value={email.type}
205
- onChange={(e) => updateEmail(i, { type: e.target.value as ContactAddressKind })}
206
- aria-label={`Email type ${i + 1}`}
207
- >
208
- {ADDRESS_KINDS.map((kind) => (
209
- <option key={kind} value={kind}>
210
- {kind}
211
- </option>
212
- ))}
213
- </select>
214
- <button type="button" onClick={() => removeEmail(i)} className="text-sm text-danger px-2" aria-label={`Remove email ${i + 1}`}>
215
- &times;
216
- </button>
217
- </div>
218
- ))}
219
- <button
220
- type="button"
221
- onClick={() => setEmails((prev) => [...prev, { address: "", type: "work" }])}
222
- className="self-start text-xs font-medium text-primary-dark hover:underline"
223
- >
224
- + Add email
225
- </button>
226
- </div>
227
- </FormField>
228
-
229
- <FormField label="Phone" htmlFor="contact-phones">
230
- <div id="contact-phones" className="flex flex-col gap-2">
231
- {phones.map((phone, i) => (
232
- <div key={i} className="flex gap-2">
233
- <input
234
- type="tel"
235
- className={`${INPUT_CLASS} flex-1`}
236
- value={phone.phoneNumber}
237
- onChange={(e) => updatePhone(i, { phoneNumber: e.target.value })}
238
- aria-label={`Phone number ${i + 1}`}
239
- />
240
- <select
241
- className={SELECT_CLASS}
242
- value={phone.type}
243
- onChange={(e) => updatePhone(i, { type: e.target.value as ContactAddressKind })}
244
- aria-label={`Phone type ${i + 1}`}
245
- >
246
- {ADDRESS_KINDS.map((kind) => (
247
- <option key={kind} value={kind}>
248
- {kind}
249
- </option>
250
- ))}
251
- </select>
252
- <button type="button" onClick={() => removePhone(i)} className="text-sm text-danger px-2" aria-label={`Remove phone ${i + 1}`}>
253
- &times;
254
- </button>
255
- </div>
256
- ))}
257
- <button
258
- type="button"
259
- onClick={() => setPhones((prev) => [...prev, { phoneNumber: "", type: "work" }])}
260
- className="self-start text-xs font-medium text-primary-dark hover:underline"
261
- >
262
- + Add phone
263
- </button>
264
- </div>
265
- </FormField>
266
-
267
- <FormField label="Address" htmlFor="contact-address">
268
- {address ? (
269
- <div className="flex flex-col gap-2">
270
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
271
- <input
272
- type="text"
273
- placeholder="Street"
274
- className={INPUT_CLASS}
275
- value={address.street ?? ""}
276
- onChange={(e) => setAddress({ ...address, street: e.target.value })}
277
- />
278
- <input
279
- type="text"
280
- placeholder="City"
281
- className={INPUT_CLASS}
282
- value={address.city ?? ""}
283
- onChange={(e) => setAddress({ ...address, city: e.target.value })}
284
- />
285
- <input
286
- type="text"
287
- placeholder="State/Province"
288
- className={INPUT_CLASS}
289
- value={address.state ?? ""}
290
- onChange={(e) => setAddress({ ...address, state: e.target.value })}
291
- />
292
- <input
293
- type="text"
294
- placeholder="Postal code"
295
- className={INPUT_CLASS}
296
- value={address.postalCode ?? ""}
297
- onChange={(e) => setAddress({ ...address, postalCode: e.target.value })}
298
- />
299
- <input
300
- type="text"
301
- placeholder="Country"
302
- className={INPUT_CLASS}
303
- value={address.country ?? ""}
304
- onChange={(e) => setAddress({ ...address, country: e.target.value })}
305
- />
306
- <select
307
- className={SELECT_CLASS}
308
- value={address.type}
309
- onChange={(e) => setAddress({ ...address, type: e.target.value as ContactAddressKind })}
310
- aria-label="Address type"
311
- >
312
- {ADDRESS_KINDS.map((kind) => (
313
- <option key={kind} value={kind}>
314
- {kind}
315
- </option>
316
- ))}
317
- </select>
318
- </div>
319
- <button type="button" onClick={() => setAddress(null)} className="self-start text-xs font-medium text-danger hover:underline">
320
- Remove address
321
- </button>
322
- </div>
323
- ) : (
324
- <button
325
- type="button"
326
- onClick={() => setAddress({ type: "home" })}
327
- className="self-start text-xs font-medium text-primary-dark hover:underline"
328
- >
329
- + Add address
330
- </button>
331
- )}
332
- </FormField>
333
-
334
- <FormField label="Notes" htmlFor="contact-notes">
335
- <textarea id="contact-notes" rows={3} className={INPUT_CLASS} value={notes} onChange={(e) => setNotes(e.target.value)} />
336
- </FormField>
337
-
338
- <div className="flex gap-3 mt-2">
339
- <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
340
- Save
341
- </Button>
342
- <Button type="button" variant="secondary" className="!w-auto" onClick={onCancel}>
343
- Cancel
344
- </Button>
345
- </div>
346
- </form>
347
- );
348
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { FormEvent, useState } from "react";
6
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
+ import {
8
+ Contact,
9
+ ContactAddressKind,
10
+ ContactEmail,
11
+ ContactPatch,
12
+ ContactPhone,
13
+ ContactPostalAddress,
14
+ createContact,
15
+ updateContact,
16
+ } from "@rapidmx/react-shared/contacts/contactsApi.js";
17
+ import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
18
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
19
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
20
+ import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
21
+ import { findWellKnownFolderUid } from "../../mail/findWellKnownFolderUid.js";
22
+ import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
23
+
24
+ const INPUT_CLASS =
25
+ "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
26
+ const SELECT_CLASS =
27
+ "text-sm py-2 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
28
+
29
+ export interface ContactFormProps {
30
+ contact?: Contact;
31
+ /** Create mode: the default mailbox (and its Contacts folder) to create in. */
32
+ mailboxUid?: string;
33
+ folderUid?: string;
34
+ /** Create mode: every mailbox the contact could be created in - with more than one, a Mailbox selector
35
+ * is shown. Choosing a mailbox other than `mailboxUid` looks up that mailbox's own Contacts folder. */
36
+ mailboxes?: Mailbox[];
37
+ onSaved: (contact: Contact) => void;
38
+ onCancel: () => void;
39
+ }
40
+
41
+ const ADDRESS_KINDS: ContactAddressKind[] = ["home", "work", "other"];
42
+
43
+ /**
44
+ * A contact's create/edit form. Shared by the desktop inline pane (`apps/www/contacts/index.tsx`) and the
45
+ * mobile detail route's own edit mode (`apps/www/contacts/[uid].tsx`) — creation itself stays
46
+ * desktop-and-mobile-inline (see that route's own doc comment on why "new" never gets a dedicated route).
47
+ */
48
+ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes, onSaved, onCancel }: ContactFormProps) {
49
+ const [targetMailboxUid, setTargetMailboxUid] = useState(mailboxUid);
50
+ const [displayName, setDisplayName] = useState(contact?.displayName ?? "");
51
+ const [givenName, setGivenName] = useState(contact?.givenName ?? "");
52
+ const [surname, setSurname] = useState(contact?.surname ?? "");
53
+ const [company, setCompany] = useState(contact?.company ?? "");
54
+ const [jobTitle, setJobTitle] = useState(contact?.jobTitle ?? "");
55
+ const [notes, setNotes] = useState(contact?.notes ?? "");
56
+ const [favorite, setFavorite] = useState(contact?.favorite ?? false);
57
+ const [categoriesText, setCategoriesText] = useState((contact?.categories ?? []).join(", "));
58
+ const [emails, setEmails] = useState<ContactEmail[]>(contact?.emails ?? []);
59
+ const [phones, setPhones] = useState<ContactPhone[]>(contact?.phones ?? []);
60
+ // Every stored address is kept and editable - editing a contact must never silently drop addresses
61
+ // beyond the first.
62
+ const [addresses, setAddresses] = useState<ContactPostalAddress[]>(contact?.addresses ?? []);
63
+ const [error, setError] = useState<string | null>(null);
64
+ const [saving, setSaving] = useState(false);
65
+
66
+ function updateEmail(index: number, patch: Partial<ContactEmail>) {
67
+ setEmails((prev) => prev.map((e, i) => (i === index ? { ...e, ...patch } : e)));
68
+ }
69
+ function removeEmail(index: number) {
70
+ setEmails((prev) => prev.filter((_, i) => i !== index));
71
+ }
72
+ function updatePhone(index: number, patch: Partial<ContactPhone>) {
73
+ setPhones((prev) => prev.map((p, i) => (i === index ? { ...p, ...patch } : p)));
74
+ }
75
+ function removePhone(index: number) {
76
+ setPhones((prev) => prev.filter((_, i) => i !== index));
77
+ }
78
+ function updateAddress(index: number, patch: Partial<ContactPostalAddress>) {
79
+ setAddresses((prev) => prev.map((a, i) => (i === index ? { ...a, ...patch } : a)));
80
+ }
81
+ function removeAddress(index: number) {
82
+ setAddresses((prev) => prev.filter((_, i) => i !== index));
83
+ }
84
+
85
+ async function handleSubmit(e: FormEvent) {
86
+ e.preventDefault();
87
+ setError(null);
88
+
89
+ if (!displayName.trim()) {
90
+ setError("A display name is required.");
91
+ return;
92
+ }
93
+
94
+ setSaving(true);
95
+ try {
96
+ const categories = categoriesText
97
+ .split(",")
98
+ .map((c) => c.trim())
99
+ .filter(Boolean);
100
+ // An update merges only the fields in its body, so a field the user cleared is sent as an
101
+ // explicit `null` (an omitted/undefined one would keep its stored value); a create just omits it.
102
+ const cleared = contact ? null : undefined;
103
+ const input = {
104
+ displayName: displayName.trim(),
105
+ givenName: givenName.trim() || cleared,
106
+ surname: surname.trim() || cleared,
107
+ company: company.trim() || cleared,
108
+ jobTitle: jobTitle.trim() || cleared,
109
+ notes: notes.trim() || cleared,
110
+ favorite,
111
+ categories,
112
+ emails,
113
+ phones,
114
+ addresses,
115
+ };
116
+ let saved: Contact;
117
+ if (contact) {
118
+ saved = await updateContact({
119
+ uid: contact.uid,
120
+ version: contact.version,
121
+ ...(input as Omit<ContactPatch, "uid" | "version">),
122
+ });
123
+ } else {
124
+ const targetFolderUid =
125
+ targetMailboxUid === mailboxUid ? folderUid : await findWellKnownFolderUid(targetMailboxUid as string, "contacts");
126
+ if (!targetFolderUid) {
127
+ setError("That mailbox has no Contacts folder.");
128
+ return;
129
+ }
130
+ saved = await createContact({
131
+ mailboxUid: targetMailboxUid as string,
132
+ folderUid: targetFolderUid,
133
+ ...(input as Omit<Parameters<typeof createContact>[0], "mailboxUid" | "folderUid">),
134
+ });
135
+ }
136
+ // A message opened later in this page load must see the changed contact's signing keys.
137
+ clearPinnedSignerCache();
138
+ onSaved(saved);
139
+ } catch (err) {
140
+ setError(err instanceof ApiRequestError ? err.message : "Could not save this contact.");
141
+ } finally {
142
+ setSaving(false);
143
+ }
144
+ }
145
+
146
+ return (
147
+ <form onSubmit={handleSubmit} className="max-w-xl flex flex-col gap-1">
148
+ <h1 className="text-xl font-bold uppercase tracking-wide mb-3">{contact ? "Edit contact" : "New contact"}</h1>
149
+
150
+ {error && <Alert>{error}</Alert>}
151
+
152
+ {!contact && mailboxes && mailboxes.length > 1 && (
153
+ <FormField label="Mailbox" htmlFor="contact-mailbox">
154
+ <select
155
+ id="contact-mailbox"
156
+ className={`${SELECT_CLASS} w-full`}
157
+ value={targetMailboxUid}
158
+ onChange={(e) => setTargetMailboxUid(e.target.value)}
159
+ >
160
+ {mailboxes.map((mb) => (
161
+ <option key={mb.uid} value={mb.uid}>
162
+ {mb.displayName}
163
+ {mb.ownerUserUid ? "" : " (shared)"}
164
+ </option>
165
+ ))}
166
+ </select>
167
+ </FormField>
168
+ )}
169
+
170
+ <FormField label="Display name" htmlFor="contact-displayName">
171
+ <input
172
+ id="contact-displayName"
173
+ type="text"
174
+ className={INPUT_CLASS}
175
+ value={displayName}
176
+ onChange={(e) => setDisplayName(e.target.value)}
177
+ />
178
+ </FormField>
179
+
180
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
181
+ <FormField label="First name" htmlFor="contact-givenName">
182
+ <input id="contact-givenName" type="text" className={INPUT_CLASS} value={givenName} onChange={(e) => setGivenName(e.target.value)} />
183
+ </FormField>
184
+ <FormField label="Last name" htmlFor="contact-surname">
185
+ <input id="contact-surname" type="text" className={INPUT_CLASS} value={surname} onChange={(e) => setSurname(e.target.value)} />
186
+ </FormField>
187
+ </div>
188
+
189
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
190
+ <FormField label="Company" htmlFor="contact-company">
191
+ <input id="contact-company" type="text" className={INPUT_CLASS} value={company} onChange={(e) => setCompany(e.target.value)} />
192
+ </FormField>
193
+ <FormField label="Job title" htmlFor="contact-jobTitle">
194
+ <input id="contact-jobTitle" type="text" className={INPUT_CLASS} value={jobTitle} onChange={(e) => setJobTitle(e.target.value)} />
195
+ </FormField>
196
+ </div>
197
+
198
+ <label className="flex items-center gap-2 text-sm mb-4">
199
+ <input type="checkbox" checked={favorite} onChange={(e) => setFavorite(e.target.checked)} />
200
+ Favorite
201
+ </label>
202
+
203
+ <FormField label="Categories (comma-separated)" htmlFor="contact-categories">
204
+ <input
205
+ id="contact-categories"
206
+ type="text"
207
+ className={INPUT_CLASS}
208
+ value={categoriesText}
209
+ onChange={(e) => setCategoriesText(e.target.value)}
210
+ placeholder="VIP, Work"
211
+ />
212
+ </FormField>
213
+
214
+ <FormField label="Email" htmlFor="contact-emails">
215
+ <div id="contact-emails" className="flex flex-col gap-2">
216
+ {emails.map((email, i) => (
217
+ <div key={i} className="flex gap-2">
218
+ <input
219
+ type="email"
220
+ className={`${INPUT_CLASS} flex-1`}
221
+ value={email.address}
222
+ onChange={(e) => updateEmail(i, { address: e.target.value })}
223
+ aria-label={`Email address ${i + 1}`}
224
+ />
225
+ <select
226
+ className={SELECT_CLASS}
227
+ value={email.type}
228
+ onChange={(e) => updateEmail(i, { type: e.target.value as ContactAddressKind })}
229
+ aria-label={`Email type ${i + 1}`}
230
+ >
231
+ {ADDRESS_KINDS.map((kind) => (
232
+ <option key={kind} value={kind}>
233
+ {kind}
234
+ </option>
235
+ ))}
236
+ </select>
237
+ <button type="button" onClick={() => removeEmail(i)} className="text-sm text-danger px-2" aria-label={`Remove email ${i + 1}`}>
238
+ &times;
239
+ </button>
240
+ </div>
241
+ ))}
242
+ <button
243
+ type="button"
244
+ onClick={() => setEmails((prev) => [...prev, { address: "", type: "work" }])}
245
+ className="self-start text-xs font-medium text-primary-dark hover:underline"
246
+ >
247
+ + Add email
248
+ </button>
249
+ </div>
250
+ </FormField>
251
+
252
+ <FormField label="Phone" htmlFor="contact-phones">
253
+ <div id="contact-phones" className="flex flex-col gap-2">
254
+ {phones.map((phone, i) => (
255
+ <div key={i} className="flex gap-2">
256
+ <input
257
+ type="tel"
258
+ className={`${INPUT_CLASS} flex-1`}
259
+ value={phone.phoneNumber}
260
+ onChange={(e) => updatePhone(i, { phoneNumber: e.target.value })}
261
+ aria-label={`Phone number ${i + 1}`}
262
+ />
263
+ <select
264
+ className={SELECT_CLASS}
265
+ value={phone.type}
266
+ onChange={(e) => updatePhone(i, { type: e.target.value as ContactAddressKind })}
267
+ aria-label={`Phone type ${i + 1}`}
268
+ >
269
+ {ADDRESS_KINDS.map((kind) => (
270
+ <option key={kind} value={kind}>
271
+ {kind}
272
+ </option>
273
+ ))}
274
+ </select>
275
+ <button type="button" onClick={() => removePhone(i)} className="text-sm text-danger px-2" aria-label={`Remove phone ${i + 1}`}>
276
+ &times;
277
+ </button>
278
+ </div>
279
+ ))}
280
+ <button
281
+ type="button"
282
+ onClick={() => setPhones((prev) => [...prev, { phoneNumber: "", type: "work" }])}
283
+ className="self-start text-xs font-medium text-primary-dark hover:underline"
284
+ >
285
+ + Add phone
286
+ </button>
287
+ </div>
288
+ </FormField>
289
+
290
+ <FormField label="Address" htmlFor="contact-address">
291
+ <div className="flex flex-col gap-3">
292
+ {addresses.map((address, i) => (
293
+ <div key={i} className="flex flex-col gap-2">
294
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
295
+ <input
296
+ type="text"
297
+ placeholder="Street"
298
+ className={INPUT_CLASS}
299
+ value={address.street ?? ""}
300
+ onChange={(e) => updateAddress(i, { street: e.target.value })}
301
+ />
302
+ <input
303
+ type="text"
304
+ placeholder="City"
305
+ className={INPUT_CLASS}
306
+ value={address.city ?? ""}
307
+ onChange={(e) => updateAddress(i, { city: e.target.value })}
308
+ />
309
+ <input
310
+ type="text"
311
+ placeholder="State/Province"
312
+ className={INPUT_CLASS}
313
+ value={address.state ?? ""}
314
+ onChange={(e) => updateAddress(i, { state: e.target.value })}
315
+ />
316
+ <input
317
+ type="text"
318
+ placeholder="Postal code"
319
+ className={INPUT_CLASS}
320
+ value={address.postalCode ?? ""}
321
+ onChange={(e) => updateAddress(i, { postalCode: e.target.value })}
322
+ />
323
+ <input
324
+ type="text"
325
+ placeholder="Country"
326
+ className={INPUT_CLASS}
327
+ value={address.country ?? ""}
328
+ onChange={(e) => updateAddress(i, { country: e.target.value })}
329
+ />
330
+ <select
331
+ className={SELECT_CLASS}
332
+ value={address.type}
333
+ onChange={(e) => updateAddress(i, { type: e.target.value as ContactAddressKind })}
334
+ aria-label={i === 0 ? "Address type" : `Address type ${i + 1}`}
335
+ >
336
+ {ADDRESS_KINDS.map((kind) => (
337
+ <option key={kind} value={kind}>
338
+ {kind}
339
+ </option>
340
+ ))}
341
+ </select>
342
+ </div>
343
+ <button
344
+ type="button"
345
+ onClick={() => removeAddress(i)}
346
+ className="self-start text-xs font-medium text-danger hover:underline"
347
+ aria-label={i === 0 ? "Remove address" : `Remove address ${i + 1}`}
348
+ >
349
+ Remove address
350
+ </button>
351
+ </div>
352
+ ))}
353
+ <button
354
+ type="button"
355
+ onClick={() => setAddresses((prev) => [...prev, { type: "home" }])}
356
+ className="self-start text-xs font-medium text-primary-dark hover:underline"
357
+ >
358
+ + Add address
359
+ </button>
360
+ </div>
361
+ </FormField>
362
+
363
+ <FormField label="Notes" htmlFor="contact-notes">
364
+ <textarea id="contact-notes" rows={3} className={INPUT_CLASS} value={notes} onChange={(e) => setNotes(e.target.value)} />
365
+ </FormField>
366
+
367
+ <div className="flex gap-3 mt-2">
368
+ <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
369
+ Save
370
+ </Button>
371
+ <Button type="button" variant="secondary" className="!w-auto" onClick={onCancel}>
372
+ Cancel
373
+ </Button>
374
+ </div>
375
+ </form>
376
+ );
377
+ }