@rapidmx/web-client 0.10.0 → 0.12.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 (324) hide show
  1. package/README.md +183 -2
  2. package/apps/admin/_layout.tsx +8 -2
  3. package/apps/admin/index.tsx +7 -1
  4. package/apps/admin/ingest-queue/index.tsx +2 -1
  5. package/apps/admin/mailboxes/[uid].tsx +23 -10
  6. package/apps/admin/mailboxes/new/index.tsx +3 -3
  7. package/apps/admin/quarantine/index.tsx +2 -1
  8. package/apps/admin/signing-certificates/index.tsx +343 -0
  9. package/apps/escrow/_layout.tsx +8 -2
  10. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  11. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  12. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  13. package/apps/shared/appearance/ColorField.tsx +107 -0
  14. package/apps/shared/appearance/appearanceCache.ts +106 -0
  15. package/apps/shared/appearance/appearanceContext.ts +77 -0
  16. package/apps/shared/appearance/bootScript.ts +35 -0
  17. package/apps/shared/appearance/color.ts +169 -0
  18. package/apps/shared/appearance/photo.ts +88 -0
  19. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  20. package/apps/shared/appearance/theme.ts +355 -0
  21. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +4 -4
  22. package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
  23. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
  25. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +60 -14
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +65 -16
  27. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  28. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  29. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  30. package/apps/shared/components/contacts/ContactForm.tsx +11 -5
  31. package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
  32. package/apps/shared/components/contacts/ContactsToolbar.tsx +25 -52
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +10 -5
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +98 -32
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  38. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  39. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  40. package/apps/shared/components/layout/UserMenu.tsx +75 -6
  41. package/apps/shared/components/mail/ConversationList.tsx +4 -2
  42. package/apps/shared/components/mail/ConversationThreadPane.tsx +487 -475
  43. package/apps/shared/components/mail/LazyReadingPane.tsx +14 -16
  44. package/apps/shared/components/mail/MailSelectionBar.tsx +2 -10
  45. package/apps/shared/components/mail/MessageDetailPane.tsx +366 -287
  46. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  47. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  48. package/apps/shared/components/mail/compose/ComposeContext.tsx +35 -2
  49. package/apps/shared/components/mail/compose/ComposeWindow.tsx +239 -437
  50. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  51. package/apps/shared/components/mail/layout/MailShell.tsx +39 -25
  52. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  53. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  54. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  55. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  56. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  57. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  58. package/apps/shared/components/mail/reading/color.ts +245 -0
  59. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  60. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  61. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  62. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  63. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  64. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  65. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  66. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  67. package/apps/shared/components/sharing/PrincipalListField.tsx +82 -0
  68. package/apps/shared/components/sharing/PrincipalPicker.tsx +76 -0
  69. package/apps/shared/components/sharing/PrincipalResolver.tsx +164 -0
  70. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  71. package/apps/shared/components/tasks/layout/TasksShell.tsx +9 -5
  72. package/apps/shared/mail/folderCounts.ts +9 -3
  73. package/apps/shared/mail/folderTree.ts +143 -0
  74. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  75. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  76. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  77. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  78. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  79. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  80. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  81. package/apps/shared/mail/outbox/sendState.ts +35 -0
  82. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  83. package/apps/shared/mail/useMailConnection.ts +81 -28
  84. package/apps/shared/mail/useMailLiveUpdates.ts +55 -10
  85. package/apps/shared/mail/useNewMailNotifications.ts +47 -32
  86. package/apps/shared/navigation/AppRouter.tsx +2 -2
  87. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  88. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  89. package/apps/shared/notifications/apiErrors.ts +98 -0
  90. package/apps/shared/notifications/headerOffset.ts +35 -0
  91. package/apps/shared/notifications/pushStatus.ts +56 -0
  92. package/apps/shared/notifications/store.ts +549 -0
  93. package/apps/shared/notifications/systemErrors.ts +63 -0
  94. package/apps/shared/notifications/useNotifications.ts +41 -0
  95. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  96. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  97. package/apps/shared/signing/enrollmentView.ts +251 -0
  98. package/apps/shared/signing/signingInfo.ts +58 -0
  99. package/apps/shared/signing/useNow.ts +19 -0
  100. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  101. package/apps/shared/styles/app.css +80 -0
  102. package/apps/www/_layout.tsx +8 -2
  103. package/apps/www/_routes.ts +1 -0
  104. package/apps/www/calendar/index.tsx +9 -14
  105. package/apps/www/contacts/[uid].tsx +5 -8
  106. package/apps/www/contacts/index.tsx +587 -584
  107. package/apps/www/index.tsx +38 -27
  108. package/apps/www/messages/[uid].tsx +3 -1
  109. package/apps/www/settings/appearance/index.tsx +21 -0
  110. package/apps/www/settings/auto-reply/index.tsx +3 -7
  111. package/apps/www/settings/encryption/index.tsx +151 -116
  112. package/apps/www/settings/filters/[uid].tsx +3 -1
  113. package/apps/www/settings/filters/new/index.tsx +3 -1
  114. package/apps/www/settings/privacy/index.tsx +3 -8
  115. package/apps/www/settings/read-receipts/index.tsx +3 -7
  116. package/apps/www/settings/sharing/index.tsx +44 -65
  117. package/apps/www/settings/signatures/[uid].tsx +3 -1
  118. package/apps/www/settings/signatures/new/index.tsx +3 -2
  119. package/apps/www/tasks/index.tsx +11 -16
  120. package/dist/apps/admin/_layout.d.ts +5 -1
  121. package/dist/apps/admin/_layout.js +3 -2
  122. package/dist/apps/admin/index.js +3 -2
  123. package/dist/apps/admin/ingest-queue/index.js +2 -1
  124. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  125. package/dist/apps/admin/mailboxes/new/index.js +1 -1
  126. package/dist/apps/admin/quarantine/index.js +2 -1
  127. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  128. package/dist/apps/admin/signing-certificates/index.js +161 -0
  129. package/dist/apps/escrow/_layout.d.ts +5 -1
  130. package/dist/apps/escrow/_layout.js +3 -2
  131. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  132. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  133. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  134. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  135. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  136. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  137. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  138. package/dist/apps/shared/appearance/ColorField.js +40 -0
  139. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  140. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  141. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  142. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  143. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  144. package/dist/apps/shared/appearance/bootScript.js +33 -0
  145. package/dist/apps/shared/appearance/color.d.ts +65 -0
  146. package/dist/apps/shared/appearance/color.js +132 -0
  147. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  148. package/dist/apps/shared/appearance/photo.js +79 -0
  149. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  150. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  151. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  152. package/dist/apps/shared/appearance/theme.js +251 -0
  153. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +3 -2
  154. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  155. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  156. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  157. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  158. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  159. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  160. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +25 -5
  161. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +18 -7
  162. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  163. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  164. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  165. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  166. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  167. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  168. package/dist/apps/shared/components/contacts/ContactsToolbar.js +24 -9
  169. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -1
  170. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  171. package/dist/apps/shared/components/layout/AppShell.d.ts +6 -1
  172. package/dist/apps/shared/components/layout/AppShell.js +59 -13
  173. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  174. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  175. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  176. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  177. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  178. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  179. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  180. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  181. package/dist/apps/shared/components/layout/UserMenu.d.ts +7 -1
  182. package/dist/apps/shared/components/layout/UserMenu.js +41 -7
  183. package/dist/apps/shared/components/mail/ConversationList.js +3 -2
  184. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +2 -1
  185. package/dist/apps/shared/components/mail/ConversationThreadPane.js +42 -20
  186. package/dist/apps/shared/components/mail/LazyReadingPane.js +9 -8
  187. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +3 -4
  188. package/dist/apps/shared/components/mail/MailSelectionBar.js +2 -3
  189. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +18 -7
  190. package/dist/apps/shared/components/mail/MessageDetailPane.js +86 -41
  191. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  192. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  193. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  194. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  195. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +8 -0
  196. package/dist/apps/shared/components/mail/compose/ComposeContext.js +28 -3
  197. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +2 -1
  198. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +229 -328
  199. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  200. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  201. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +2 -0
  202. package/dist/apps/shared/components/mail/layout/MailShell.js +20 -14
  203. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  204. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  205. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  206. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  207. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  208. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  209. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  210. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  211. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  212. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  213. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  214. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  215. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  216. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  217. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  218. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  219. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  220. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  221. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  222. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  223. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  224. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  225. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  226. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  227. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  228. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  229. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  230. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  231. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  232. package/dist/apps/shared/components/sharing/PrincipalListField.d.ts +24 -0
  233. package/dist/apps/shared/components/sharing/PrincipalListField.js +23 -0
  234. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +28 -0
  235. package/dist/apps/shared/components/sharing/PrincipalPicker.js +24 -0
  236. package/dist/apps/shared/components/sharing/PrincipalResolver.d.ts +44 -0
  237. package/dist/apps/shared/components/sharing/PrincipalResolver.js +92 -0
  238. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  239. package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -1
  240. package/dist/apps/shared/mail/folderCounts.d.ts +1 -1
  241. package/dist/apps/shared/mail/folderCounts.js +9 -3
  242. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  243. package/dist/apps/shared/mail/folderTree.js +116 -0
  244. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  245. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  246. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  247. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  248. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  249. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  250. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  251. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  252. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  253. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  254. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  255. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  256. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  257. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  258. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  259. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  260. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  261. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  262. package/dist/apps/shared/mail/useMailConnection.d.ts +9 -8
  263. package/dist/apps/shared/mail/useMailConnection.js +64 -22
  264. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +17 -4
  265. package/dist/apps/shared/mail/useMailLiveUpdates.js +36 -10
  266. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +4 -10
  267. package/dist/apps/shared/mail/useNewMailNotifications.js +43 -23
  268. package/dist/apps/shared/navigation/AppRouter.js +2 -2
  269. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  270. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  271. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  272. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  273. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  274. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  275. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  276. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  277. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  278. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  279. package/dist/apps/shared/notifications/store.d.ts +136 -0
  280. package/dist/apps/shared/notifications/store.js +416 -0
  281. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  282. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  283. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  284. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  285. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  286. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  287. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  288. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  289. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  290. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  291. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  292. package/dist/apps/shared/signing/signingInfo.js +48 -0
  293. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  294. package/dist/apps/shared/signing/useNow.js +18 -0
  295. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  296. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  297. package/dist/apps/shared/styles/app.css +80 -0
  298. package/dist/apps/www/_layout.d.ts +5 -1
  299. package/dist/apps/www/_layout.js +3 -2
  300. package/dist/apps/www/_routes.js +1 -0
  301. package/dist/apps/www/calendar/index.js +3 -4
  302. package/dist/apps/www/contacts/[uid].js +5 -7
  303. package/dist/apps/www/contacts/index.js +14 -21
  304. package/dist/apps/www/index.js +37 -19
  305. package/dist/apps/www/messages/[uid].js +3 -1
  306. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  307. package/dist/apps/www/settings/appearance/index.js +13 -0
  308. package/dist/apps/www/settings/auto-reply/index.js +3 -6
  309. package/dist/apps/www/settings/encryption/index.js +103 -95
  310. package/dist/apps/www/settings/filters/[uid].js +3 -1
  311. package/dist/apps/www/settings/filters/new/index.js +3 -1
  312. package/dist/apps/www/settings/privacy/index.js +5 -8
  313. package/dist/apps/www/settings/read-receipts/index.js +3 -6
  314. package/dist/apps/www/settings/sharing/index.js +19 -33
  315. package/dist/apps/www/settings/signatures/[uid].js +3 -1
  316. package/dist/apps/www/settings/signatures/new/index.js +3 -2
  317. package/dist/apps/www/tasks/index.js +8 -14
  318. package/package.json +2 -2
  319. package/apps/shared/components/mail/NewMailToasts.tsx +0 -141
  320. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  321. package/dist/apps/shared/components/mail/NewMailToasts.d.ts +0 -19
  322. package/dist/apps/shared/components/mail/NewMailToasts.js +0 -55
  323. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  324. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -0,0 +1,58 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useState } from "react";
6
+ import { SigningEnrollmentInfo, getSigningEnrollmentInfo } from "@rapidmx/react-shared/crypto/signingProviderApi.js";
7
+
8
+ // The type and the request both come from react-shared's `signingProviderApi.js` (R6's) - this module only adds the caching, the
9
+ // never-rejects contract and the hook that Settings > Encryption and its card want. Re-exported so the rest of `signing/` and
10
+ // `SigningCertificateCard` keep importing `SigningEnrollmentInfo` from here rather than reaching into react-shared directly.
11
+ export type { SigningEnrollmentInfo };
12
+
13
+ /** How long an answer is reused: the page asks once when it opens, and again after this if it is opened again. */
14
+ export const SIGNING_INFO_CACHE_MS = 5 * 60_000;
15
+
16
+ let cached: { at: number; promise: Promise<SigningEnrollmentInfo | null> } | undefined;
17
+
18
+ /** The deployment's signing-enrollment description, or `null` when it cannot be had (an older server with no such endpoint, offline,
19
+ * anything) - never rejects. One request at a time, reused for five minutes. */
20
+ export function fetchSigningEnrollmentInfo(now: number = Date.now()): Promise<SigningEnrollmentInfo | null> {
21
+ if (cached && now - cached.at < SIGNING_INFO_CACHE_MS) {
22
+ return cached.promise;
23
+ }
24
+ const promise = getSigningEnrollmentInfo().catch(() => null);
25
+ cached = { at: now, promise };
26
+ // An answer that could not be had is not worth remembering: the next visit asks again.
27
+ void promise.then((info) => {
28
+ if (info === null && cached?.promise === promise) {
29
+ cached = undefined;
30
+ }
31
+ });
32
+ return promise;
33
+ }
34
+
35
+ /** Forgets the remembered answer: for tests, which share the module. */
36
+ export function resetSigningInfo(): void {
37
+ cached = undefined;
38
+ }
39
+
40
+ /** The deployment's description, once it is known: `undefined` while it is being asked for, `null` when there is none. Asks only while `enabled`. */
41
+ export function useSigningEnrollmentInfo(enabled: boolean): SigningEnrollmentInfo | null | undefined {
42
+ const [info, setInfo] = useState<SigningEnrollmentInfo | null | undefined>(undefined);
43
+ useEffect(() => {
44
+ if (!enabled) {
45
+ return;
46
+ }
47
+ let cancelled = false;
48
+ void fetchSigningEnrollmentInfo().then((answer) => {
49
+ if (!cancelled) {
50
+ setInfo(answer);
51
+ }
52
+ });
53
+ return () => {
54
+ cancelled = true;
55
+ };
56
+ }, [enabled]);
57
+ return info;
58
+ }
@@ -0,0 +1,19 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useState } from "react";
6
+
7
+ /** The current time in epoch ms, re-read every `intervalMs` while `enabled` - for "3 min ago" texts and countdowns that keep themselves right. */
8
+ export function useNow(intervalMs: number, enabled = true): number {
9
+ const [now, setNow] = useState(() => Date.now());
10
+ useEffect(() => {
11
+ if (!enabled) {
12
+ return;
13
+ }
14
+ setNow(Date.now());
15
+ const timer = setInterval(() => setNow(Date.now()), intervalMs);
16
+ return () => clearInterval(timer);
17
+ }, [intervalMs, enabled]);
18
+ return now;
19
+ }
@@ -0,0 +1,90 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useRef } from "react";
6
+ import type { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
7
+ import { getCurrentSignEnrollment } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
8
+ import { notify } from "../notifications/store.js";
9
+ import { readStoredSignEnrollment, storeSignEnrollment } from "./enrollmentStorage.js";
10
+ import { EnrollmentSnapshot, onEnrollmentEnded, watchCurrentEnrollment, watchEnrollment } from "./enrollmentTracker.js";
11
+ import { formatDate, isInstalling } from "./enrollmentView.js";
12
+
13
+ /** Where the pop-up's action leads: the page that shows the certificate. */
14
+ export const SIGNING_SETTINGS_HREF = "/settings/encryption";
15
+
16
+ /**
17
+ * Tells the user, wherever they are in the app, when a signing certificate they asked for is issued or has failed. Lives in the persistent app
18
+ * frame (`AppChrome`): for each mailbox the user owns it follows the enrollment this browser has a stored id for and - for one started on another
19
+ * device - the one the server names as the mailbox's current enrollment when it is still pending (`GET .../sign-enrollment`; an older server
20
+ * answers 404 and nothing is followed). Reading, backoff and visibility rules are the tracker's (`enrollmentTracker.ts`), shared with the
21
+ * Settings > Encryption card, so an enrollment is asked about once. The pop-up is raised once per enrollment (`dedupeKey`) and only for one that
22
+ * was seen pending - never for a certificate issued long ago.
23
+ */
24
+ export function useSigningEnrollmentWatcher({ userUid, mailboxes, enabled }: { userUid?: string; mailboxes: Mailbox[]; enabled: boolean }): void {
25
+ const mailboxesRef = useRef(mailboxes);
26
+ mailboxesRef.current = mailboxes;
27
+ // Keyed on the uids of the mailboxes the user owns: a refreshed list of the same mailboxes must not start over.
28
+ const ownedKey = mailboxes
29
+ .filter((mailbox) => mailbox.ownerUserUid === userUid)
30
+ .map((mailbox) => mailbox.uid)
31
+ .join("|");
32
+
33
+ useEffect(() => {
34
+ if (!enabled || !userUid || !ownedKey) {
35
+ return;
36
+ }
37
+ let cancelled = false;
38
+ const releases: (() => void)[] = [];
39
+ for (const mailboxUid of ownedKey.split("|")) {
40
+ const storedId = readStoredSignEnrollment(mailboxUid);
41
+ if (storedId) {
42
+ releases.push(watchEnrollment(mailboxUid, storedId));
43
+ continue;
44
+ }
45
+ // Never throws or rejects: a lookup that cannot be made (an older server, offline, anything) is "nothing is pending", and the frame goes on.
46
+ void (async () => {
47
+ try {
48
+ const current = await getCurrentSignEnrollment(mailboxUid);
49
+ if (!cancelled && current?.status === "pending") {
50
+ storeSignEnrollment(mailboxUid, current.enrollmentId);
51
+ releases.push(watchCurrentEnrollment(mailboxUid, current));
52
+ }
53
+ } catch {
54
+ // Nothing to follow.
55
+ }
56
+ })();
57
+ }
58
+ const stopListening = onEnrollmentEnded((snapshot) => announce(snapshot, mailboxesRef.current));
59
+ return () => {
60
+ cancelled = true;
61
+ stopListening();
62
+ releases.forEach((release) => release());
63
+ };
64
+ }, [enabled, userUid, ownedKey]);
65
+ }
66
+
67
+ /** The pop-up for an enrollment that just ended - issued or failed (the tracker announces nothing else, and always with its answer). */
68
+ function announce(snapshot: EnrollmentSnapshot, mailboxes: Mailbox[]): void {
69
+ const address = mailboxes.find((mailbox) => mailbox.uid === snapshot.mailboxUid)?.primarySmtpAddress;
70
+ const result = snapshot.result!;
71
+ if (result.status === "issued") {
72
+ notify({
73
+ kind: "success",
74
+ title: "Digital signature certificate issued",
75
+ message: isInstalling(result)
76
+ ? `The certificate for ${address ?? "your mailbox"} is issued and is being installed - mail is signed once it is (a few minutes).`
77
+ : `${address ? `Mail from ${address}` : "Your mail"} is now signed${result.notAfter ? ` (valid until ${formatDate(result.notAfter)})` : ""}.`,
78
+ actions: [{ label: "View", href: SIGNING_SETTINGS_HREF }],
79
+ dedupeKey: `sign-enrollment:${snapshot.enrollmentId}`,
80
+ });
81
+ } else {
82
+ notify({
83
+ kind: "error",
84
+ title: "Digital signature certificate could not be issued",
85
+ message: result.error ?? "The certificate authority did not issue a certificate.",
86
+ actions: [{ label: result.retryable === false ? "Details" : "Try again", href: SIGNING_SETTINGS_HREF }],
87
+ dedupeKey: `sign-enrollment:${snapshot.enrollmentId}`,
88
+ });
89
+ }
90
+ }
@@ -304,3 +304,83 @@ h6 {
304
304
  transform: none;
305
305
  }
306
306
  }
307
+
308
+ /* The frame's own background: the page's `surface-alt`, unless the user has chosen a background (`appearance/theme.ts` then sets `--rr-frame-bg`
309
+ * to `transparent` so the fixed background layer - `html::before`/`html::after` - shows through instead). */
310
+ .rr-frame-bg {
311
+ background-color: var(--rr-frame-bg, var(--rr-color-surface-alt));
312
+ }
313
+
314
+ /* With a background set, `--color-surface` and `--color-surface-alt` (what `bg-surface` and `bg-surface-alt` read) are translucent panels
315
+ * (`appearance/theme.ts`). Anything that must stay readable on its own - menus, dialogs, drawers, list pop-ups and the pop-ups - takes the
316
+ * opaque colours back for itself and everything inside it, and so does anything that stays put while a page scrolls under it (`.rr-solid`: the
317
+ * sticky title bar, the branding header, the phone's bottom tab bar), or the text beneath would show through it. Without a background these two declarations change nothing. */
318
+ [role="menu"],
319
+ [role="dialog"],
320
+ [role="listbox"],
321
+ .rr-toast-in,
322
+ .rr-solid,
323
+ [data-branding-header],
324
+ nav[aria-label="Mobile navigation"] {
325
+ --color-surface: var(--rr-color-surface);
326
+ --color-surface-alt: var(--rr-color-surface-alt);
327
+ }
328
+
329
+ /* The slot a branding header or footer's `{USER_MENU}` becomes (`BrandingChrome.tsx`): the account menu's button sits in the line of text
330
+ * the variable was written in. */
331
+ [data-rr-slot="user-menu"] {
332
+ display: inline-flex;
333
+ align-items: center;
334
+ vertical-align: middle;
335
+ }
336
+
337
+ /* The Outbox pill's "something is on its way" dot (`OutboxBadge`). Still when the user asks for less motion. */
338
+ @media (prefers-reduced-motion: no-preference) {
339
+ .rr-sending-dot {
340
+ animation: rr-sending-dot 1.1s ease-in-out infinite;
341
+ }
342
+ }
343
+ @keyframes rr-sending-dot {
344
+ 0%,
345
+ 100% {
346
+ opacity: 0.35;
347
+ transform: scale(0.8);
348
+ }
349
+ 50% {
350
+ opacity: 1;
351
+ transform: scale(1.15);
352
+ }
353
+ }
354
+
355
+ /* The signing-certificate card's progress bar (`SigningCertificateCard`): a soft highlight sweeping across the filled part while a step is active, and
356
+ * an endless slide for a bar with no number. Both stand still when the user asks for less motion (the indeterminate bar then shows as a faded full bar). */
357
+ .rr-progress-shimmer {
358
+ position: relative;
359
+ overflow: hidden;
360
+ }
361
+ @media (prefers-reduced-motion: no-preference) {
362
+ .rr-progress-shimmer::after {
363
+ content: "";
364
+ position: absolute;
365
+ inset: 0;
366
+ background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.45), transparent);
367
+ transform: translateX(-100%);
368
+ animation: rr-progress-shimmer 1.6s ease-in-out infinite;
369
+ }
370
+ .rr-progress-indeterminate {
371
+ animation: rr-progress-slide 1.4s ease-in-out infinite;
372
+ }
373
+ }
374
+ @keyframes rr-progress-shimmer {
375
+ to {
376
+ transform: translateX(100%);
377
+ }
378
+ }
379
+ @keyframes rr-progress-slide {
380
+ 0% {
381
+ transform: translateX(-100%);
382
+ }
383
+ 100% {
384
+ transform: translateX(300%);
385
+ }
386
+ }
@@ -4,6 +4,7 @@
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { PropsWithChildren } from "react";
6
6
  import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
7
+ import { AppearanceHead, appearanceHtmlAttributes } from "../shared/appearance/AppearanceHead.js";
7
8
  import { CUSTOM_STYLESHEET_LINK_ID } from "@rapidmx/react-shared/branding/useBranding.js";
8
9
 
9
10
  export interface LayoutProps {
@@ -16,15 +17,19 @@ export interface LayoutProps {
16
17
  * which doesn't receive props at all - falling back to the stock defaults there is acceptable.
17
18
  */
18
19
  branding?: Branding;
20
+ /** The signed-in user's stored appearance preferences (a page prop from the same `fetchProps()`, present only on the www pages and only when the user has any): rendered as a stylesheet in the `<head>` so the very first paint is themed - see `AppearanceHead`. */
21
+ appearance?: unknown;
22
+ /** The signed-in user (supplied by the framework), so a cached appearance from another account on this browser is never applied. */
23
+ userUid?: string;
19
24
  }
20
25
 
21
- export default function Layout({ children, branding }: PropsWithChildren<LayoutProps>) {
26
+ export default function Layout({ children, branding, appearance, userUid }: PropsWithChildren<LayoutProps>) {
22
27
  const title = branding?.title || branding?.companyName ? `${branding?.title || branding?.companyName}: Mail` : "RapidMX: Mail";
23
28
  const iconHref = branding?.iconUrl || branding?.logoUrl || "/images/logo.svg";
24
29
  const stylesheetHref = branding?.stylesheetUrl;
25
30
 
26
31
  return (
27
- <html lang="en">
32
+ <html lang="en" {...appearanceHtmlAttributes(appearance, userUid)}>
28
33
  <head>
29
34
  <meta charSet="utf-8" />
30
35
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -38,6 +43,7 @@ export default function Layout({ children, branding }: PropsWithChildren<LayoutP
38
43
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
39
44
  />
40
45
  {stylesheetHref && <link rel="stylesheet" href={stylesheetHref} id={CUSTOM_STYLESHEET_LINK_ID} />}
46
+ <AppearanceHead appearance={appearance} />
41
47
  </head>
42
48
  <body>{children}</body>
43
49
  </html>
@@ -20,6 +20,7 @@ export const wwwRoutes: readonly RouteDefinition[] = [
20
20
  { path: "/contacts", active: "contacts", load: () => import("./contacts/index.js"), idlePrefetch: true },
21
21
  { path: "/contacts/:uid", active: "contacts", load: () => import("./contacts/[uid].js") },
22
22
  { path: "/tasks", active: "tasks", load: () => import("./tasks/index.js"), idlePrefetch: true },
23
+ { path: "/settings/appearance", active: "settings", load: () => import("./settings/appearance/index.js") },
23
24
  { path: "/settings/auto-reply", active: "settings", load: () => import("./settings/auto-reply/index.js") },
24
25
  { path: "/settings/encryption", active: "settings", load: () => import("./settings/encryption/index.js") },
25
26
  { path: "/settings/filters", active: "settings", load: () => import("./settings/filters/index.js") },
@@ -40,6 +40,7 @@ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
40
40
  import { SHORTCUTS, ShortcutDef } from "../../shared/keyboard/keymap.js";
41
41
  import { useShortcut } from "../../shared/keyboard/useShortcut.js";
42
42
  import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
43
+ import { notifyApiError } from "../../shared/notifications/apiErrors.js";
43
44
 
44
45
  function CalendarPage(props: CalendarShellProps) {
45
46
  return (
@@ -109,7 +110,6 @@ function CalendarContent({ userUid }: { userUid?: string }) {
109
110
  const [events, setEvents] = useState<CalendarEvent[]>([]);
110
111
  const [loading, setLoading] = useState(true);
111
112
  const [error, setError] = useState<string | null>(null);
112
- const [actionError, setActionError] = useState<string | null>(null);
113
113
  const [modal, setModal] = useState<ModalState | null>(null);
114
114
  // `null` means "not yet customized by the user" — every calendar is treated as checked without
115
115
  // needing to be seeded from `calendarFolders` the moment it loads (see `checkedFolderUids` below).
@@ -317,7 +317,6 @@ function CalendarContent({ userUid }: { userUid?: string }) {
317
317
  if (!action) {
318
318
  return;
319
319
  }
320
- setActionError(null);
321
320
  try {
322
321
  if (action.type === "move") {
323
322
  await moveOccurrence(action.occurrence, action.deltaMs);
@@ -326,7 +325,7 @@ function CalendarContent({ userUid }: { userUid?: string }) {
326
325
  }
327
326
  reload();
328
327
  } catch (err) {
329
- setActionError(err instanceof ApiRequestError ? err.message : "Could not update this event.");
328
+ notifyApiError(err, action.type === "move" ? "Couldn't move the event" : "Couldn't resize the event");
330
329
  }
331
330
  }
332
331
 
@@ -365,16 +364,17 @@ function CalendarContent({ userUid }: { userUid?: string }) {
365
364
  );
366
365
 
367
366
  return (
368
- <div className="flex-1 flex min-h-0">
369
- <div className="hidden md:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto">{sidebarContent}</div>
367
+ <div className="flex-1 min-w-0 flex min-h-0">
368
+ <div className="hidden lg:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto">{sidebarContent}</div>
370
369
  <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Calendars">
371
370
  <div className="flex flex-col">{sidebarContent}</div>
372
371
  </Drawer>
373
- <div className="flex-1 flex flex-col min-h-0">
374
- <div className="flex items-center gap-3 p-3 border-b border-border shrink-0">
372
+ {/* `min-w-0`: without it the column is as wide as its widest row (the toolbar, the month grid) and runs off the window instead of the row wrapping. */}
373
+ <div className="flex-1 min-w-0 flex flex-col min-h-0">
374
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-2 p-3 border-b border-border shrink-0">
375
375
  <button
376
376
  type="button"
377
- className="md:hidden w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text shrink-0"
377
+ className="lg:hidden w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text shrink-0"
378
378
  aria-label="Open calendars"
379
379
  onClick={() => setDrawerOpen(true)}
380
380
  >
@@ -395,7 +395,7 @@ function CalendarContent({ userUid }: { userUid?: string }) {
395
395
  </button>
396
396
  </div>
397
397
  <h1 className="hidden md:block text-lg font-bold tracking-tight">{title}</h1>
398
- <div className="ml-auto flex gap-1 overflow-x-auto">
398
+ <div className="ml-auto flex flex-wrap gap-1">
399
399
  {VIEW_TYPES.map((v) => (
400
400
  <ViewButton key={v} view={v} current={view} onSelect={setView} />
401
401
  ))}
@@ -407,11 +407,6 @@ function CalendarContent({ userUid }: { userUid?: string }) {
407
407
  <Alert>{error}</Alert>
408
408
  </div>
409
409
  )}
410
- {actionError && (
411
- <div className="p-3">
412
- <Alert>{actionError}</Alert>
413
- </div>
414
- )}
415
410
 
416
411
  {loading ? (
417
412
  <p className="p-4 text-sm text-text-muted">Loading&hellip;</p>
@@ -12,6 +12,7 @@ import ContactDetailPane from "../../shared/components/contacts/ContactDetailPan
12
12
  import ContactForm from "../../shared/components/contacts/ContactForm.js";
13
13
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
14
14
  import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigners.js";
15
+ import { notifyApiError } from "../../shared/notifications/apiErrors.js";
15
16
 
16
17
  /**
17
18
  * Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/contacts/index.tsx` keeps its
@@ -34,9 +35,6 @@ function ContactDetailContent({ uid }: { uid: string }) {
34
35
  const [contact, setContact] = useState<Contact | null>(null);
35
36
  const [loading, setLoading] = useState(true);
36
37
  const [error, setError] = useState<string | null>(null);
37
- // Kept apart from the load `error` - a failed delete must leave the contact on screen (with the
38
- // reason) rather than replacing the whole page with an error as if it had never loaded.
39
- const [deleteError, setDeleteError] = useState<string | null>(null);
40
38
  const [mode, setMode] = useState<Mode>("view");
41
39
 
42
40
  useEffect(() => {
@@ -53,14 +51,14 @@ function ContactDetailContent({ uid }: { uid: string }) {
53
51
  // rendered once `contact` is already resolved (see the `error || !contact` guard above it), so this
54
52
  // never runs with a stale/absent contact — no redundant null check needed here.
55
53
  async function handleDelete(contact: Contact) {
56
- setDeleteError(null);
57
54
  try {
58
55
  await deleteContact(contact.uid, contact.version);
59
56
  // A deleted contact's pinned signing keys must stop vouching for signatures.
60
57
  clearPinnedSignerCache();
61
58
  navigate("/contacts");
62
59
  } catch (err) {
63
- setDeleteError(err instanceof ApiRequestError ? err.message : "Could not delete this contact.");
60
+ // A pop-up: a failed delete leaves the contact on screen rather than replacing the page with an error.
61
+ notifyApiError(err, "Couldn't delete the contact");
64
62
  }
65
63
  }
66
64
 
@@ -79,8 +77,8 @@ function ContactDetailContent({ uid }: { uid: string }) {
79
77
  const backHref = "/contacts";
80
78
  if (mode === "edit") {
81
79
  return (
82
- <div className="p-6">
83
- <a href={backHref} className="text-sm text-primary-dark hover:underline block mb-4">
80
+ <div>
81
+ <a href={backHref} className="text-sm text-primary-dark hover:underline block px-6 pt-6">
84
82
  &larr; Back to contacts
85
83
  </a>
86
84
  <ContactForm
@@ -97,7 +95,6 @@ function ContactDetailContent({ uid }: { uid: string }) {
97
95
 
98
96
  return (
99
97
  <div className="p-6">
100
- {deleteError && <Alert>{deleteError}</Alert>}
101
98
  <ContactDetailPane
102
99
  contact={contact}
103
100
  onEdit={() => setMode("edit")}