@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
@@ -2,7 +2,7 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- import React, { FormEvent, useCallback, useEffect, useRef, useState } from "react";
5
+ import React, { FormEvent, useCallback, useEffect, useId, useRef, useState } from "react";
6
6
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
7
  import {
8
8
  addPlugin,
@@ -17,6 +17,8 @@ import {
17
17
  PluginChangePlan,
18
18
  PluginInstanceStatus,
19
19
  PluginNamespace,
20
+ PluginPurgeInfo,
21
+ PluginPurgeState,
20
22
  PluginRegistryLookup,
21
23
  PluginSearchResult,
22
24
  PluginSettingDefinition,
@@ -24,12 +26,16 @@ import {
24
26
  PluginStatus,
25
27
  PluginUpdateInfo,
26
28
  removePlugin,
29
+ RemovePluginResult,
30
+ retryPluginPurge,
27
31
  searchPlugins,
28
32
  updatePlugin,
29
33
  } from "@rapidmx/react-shared/admin/pluginsApi.js";
30
34
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
31
35
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
32
36
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
37
+ import { notify } from "../../../notifications/store.js";
38
+ import { isElevationRequired } from "../elevation.js";
33
39
 
34
40
  const INPUT_CLASS =
35
41
  "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
@@ -57,6 +63,23 @@ async function attempt(action: () => Promise<void>, failure: string): Promise<st
57
63
  }
58
64
  }
59
65
 
66
+ /** The classes of a confirm button that deletes data, the same as the app's other destructive buttons. */
67
+ const DANGER_BUTTON_CLASS = "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger";
68
+
69
+ /** Shown when the server wants the administrator to have confirmed their identity recently (`api-104`). */
70
+ const ELEVATION_MESSAGE = "Deleting data needs you to have recently confirmed your identity. Reload this page, or sign in again, then try once more.";
71
+
72
+ /** What an administrator sees an uninstalled plugin's data deletion as, without the display name of a plugin that's gone. */
73
+ const purgeName = (purge: PluginPurgeInfo): string => purge.displayName ?? purge.name;
74
+
75
+ /** Why a data deletion failed: what the server recorded, else the steps that failed. */
76
+ function purgeFailure(purge: PluginPurgeInfo): string {
77
+ return purge.error ?? (purge.steps.filter((step) => !step.ok).map((step) => step.error ?? step.step).join("; ") || "The reason wasn't recorded.");
78
+ }
79
+
80
+ /** A day, in the administrator's own format. */
81
+ const formatDate = (iso: string): string => new Date(iso).toLocaleDateString(undefined, { dateStyle: "medium" });
82
+
60
83
  /** A change waiting for the administrator to confirm the other plugins it also installs or enables. */
61
84
  interface PendingChange {
62
85
  displayName: string;
@@ -116,6 +139,8 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
116
139
  /** How many times status has been retried while none has ever been read. */
117
140
  const [statusRetries, setStatusRetries] = useState(0);
118
141
  const statusRequest = useRef<Promise<void> | null>(null);
142
+ /** The state each data deletion was last seen in, so a notification is raised when one that was under way ends. */
143
+ const seenPurges = useRef<Map<string, PluginPurgeState>>(new Map());
119
144
 
120
145
  const setBusy = (uid: string, busy: boolean) =>
121
146
  setBusyUids((prev) => {
@@ -171,8 +196,12 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
171
196
  }, [refreshStatus, refreshUpdates]);
172
197
 
173
198
  const pending: boolean = !!status && status.instances.some((instance) => instance.hash !== status.hash);
199
+ // A plugin's data deletion in the status that isn't over yet, for a plugin that's uninstalled (one added again is hidden).
200
+ const installedNames: Set<string> = new Set(plugins.map((plugin) => plugin.name));
201
+ const purges: PluginPurgeInfo[] = (status?.purges ?? []).filter((purge) => !installedNames.has(purge.name));
202
+ const purging: boolean = purges.some((purge) => purge.state === "pending" || purge.state === "running");
174
203
  useEffect(() => {
175
- if (!pending && pollUntil === 0) {
204
+ if (!pending && !purging && pollUntil === 0) {
176
205
  return;
177
206
  }
178
207
  const timer = setInterval(() => void refreshStatus(), PENDING_POLL_MS);
@@ -182,7 +211,29 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
182
211
  clearInterval(timer);
183
212
  clearTimeout(stop);
184
213
  };
185
- }, [pending, pollUntil, refreshStatus]);
214
+ }, [pending, purging, pollUntil, refreshStatus]);
215
+
216
+ // A data deletion seen under way that has now ended is worth a notification: it happens after the servers restart,
217
+ // long after the administrator asked for it.
218
+ useEffect(() => {
219
+ for (const purge of status?.purges ?? []) {
220
+ const before = seenPurges.current.get(purge.uid);
221
+ seenPurges.current.set(purge.uid, purge.state);
222
+ if (before !== "pending" && before !== "running") {
223
+ continue;
224
+ }
225
+ if (purge.state === "done") {
226
+ notify({ kind: "success", title: `${purgeName(purge)}'s data was deleted`, dedupeKey: `plugin-purge-${purge.uid}-done` });
227
+ } else if (purge.state === "failed") {
228
+ notify({
229
+ kind: "error",
230
+ title: `Deleting ${purgeName(purge)}'s data failed`,
231
+ message: purgeFailure(purge),
232
+ dedupeKey: `plugin-purge-${purge.uid}-failed`,
233
+ });
234
+ }
235
+ }
236
+ }, [status]);
186
237
 
187
238
  // With no status read at all there's nothing pending to poll for, so keep retrying - less often each time.
188
239
  const neverRead: boolean = !status && statusStale;
@@ -298,6 +349,10 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
298
349
  async (plan) => {
299
350
  const result = await addPlugin(name, packageVersion, expectedPlanOf(plan));
300
351
  applied(...result.dependencies, result.plugin);
352
+ // Adding a plugin cancels the deletion of its data that was waiting for the servers to stop running it.
353
+ for (const warning of result.warnings ?? []) {
354
+ notify({ kind: "warning", title: "Data deletion cancelled", message: warning });
355
+ }
301
356
  },
302
357
  `Could not install ${displayName}.`,
303
358
  );
@@ -328,6 +383,22 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
328
383
  return onRow(plugin, () => changeVersion(plugin, packageVersion));
329
384
  }
330
385
 
386
+ /** Runs the steps of a failed data deletion that failed, again. */
387
+ async function retryPurge(purge: PluginPurgeInfo) {
388
+ setBusy(purge.uid, true);
389
+ setError(null);
390
+ setRetryEnableUid(null);
391
+ try {
392
+ await retryPluginPurge(purge.uid);
393
+ // The retry starts on a server shortly; keep reading status until it ends.
394
+ setPollUntil(Date.now() + AFTER_CHANGE_POLL_MS);
395
+ await refreshStatus();
396
+ } catch (err) {
397
+ setError(isElevationRequired(err) ? ELEVATION_MESSAGE : errorMessage(err, `Could not retry deleting ${purgeName(purge)}'s data.`));
398
+ }
399
+ setBusy(purge.uid, false);
400
+ }
401
+
331
402
  // Retried against the plugin as it's listed now (a newer version, or already enabled elsewhere, hides the retry).
332
403
  const retryEnable: Plugin | undefined = plugins.find((plugin) => plugin.uid === retryEnableUid && !plugin.enabled);
333
404
 
@@ -398,7 +469,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
398
469
  </div>
399
470
  {loading ? (
400
471
  <p className="text-sm text-text-muted">Loading&hellip;</p>
401
- ) : plugins.length === 0 ? (
472
+ ) : plugins.length === 0 && purges.length === 0 ? (
402
473
  <p className="text-sm text-text-muted">No plugins installed.</p>
403
474
  ) : (
404
475
  <div className="overflow-x-auto">
@@ -492,6 +563,9 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
492
563
  </tr>
493
564
  );
494
565
  })}
566
+ {purges.map((purge) => (
567
+ <UninstalledPluginRow key={purge.uid} purge={purge} busy={busyUids.has(purge.uid)} onRetry={() => void retryPurge(purge)} />
568
+ ))}
495
569
  </tbody>
496
570
  </table>
497
571
  </div>
@@ -551,10 +625,23 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
551
625
  <RemoveModal
552
626
  plugin={removing}
553
627
  onClose={() => setRemoving(null)}
554
- onRemoved={() => {
628
+ onRemoved={(result) => {
555
629
  const uid = removing.uid;
630
+ const displayName = removing.manifest.displayName;
556
631
  setRemoving(null);
557
632
  setPlugins((prev) => prev.filter((plugin) => plugin.uid !== uid));
633
+ if (result.purgeScheduled) {
634
+ // Shown at once; the status polling that follows replaces it with what the servers report.
635
+ const scheduled = result.purge;
636
+ setStatus((prev) =>
637
+ prev && scheduled ? { ...prev, purges: [...(prev.purges ?? []).filter((purge) => purge.uid !== scheduled.uid), scheduled] } : prev,
638
+ );
639
+ notify({
640
+ kind: "info",
641
+ title: `${displayName} uninstalled`,
642
+ message: "Its data is deleted once every server has stopped running it. Adding the plugin again before then cancels the deletion.",
643
+ });
644
+ }
558
645
  watchRollout();
559
646
  }}
560
647
  />
@@ -811,6 +898,68 @@ function PluginServers({ plugin, status }: { plugin: Plugin; status: PluginStatu
811
898
  );
812
899
  }
813
900
 
901
+ /** The row of a plugin that was uninstalled but whose data deletion is still listed: what became of its data. */
902
+ function UninstalledPluginRow({ purge, busy, onRetry }: { purge: PluginPurgeInfo; busy: boolean; onRetry: () => void }) {
903
+ const failedSteps = purge.steps.filter((step) => !step.ok);
904
+ return (
905
+ <tr role="row" className={INSTALLED_ROW_CLASS}>
906
+ <td role="cell" className={`${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`}>
907
+ <div className="font-semibold">{purgeName(purge)}</div>
908
+ <div className="text-xs text-text-muted">{purge.name}</div>
909
+ </td>
910
+ <td role="cell" className={`${INSTALLED_TD_CLASS} hidden lg:table-cell`} />
911
+ <td role="cell" className={`${INSTALLED_TD_CLASS} pt-1 pb-3 basis-full lg:basis-auto`}>
912
+ <div className="flex flex-col items-start gap-1 text-xs max-w-sm">
913
+ <span className={`${BADGE_CLASS} bg-surface-alt text-text-muted`}>Uninstalled</span>
914
+ {purge.state === "pending" && (
915
+ <>
916
+ <span>Uninstalled - data will be deleted after servers restart</span>
917
+ {!!purge.serversRunning && (
918
+ <span className="text-text-muted">
919
+ {purge.serversRunning} of {purge.serversTotal} {purge.serversTotal === 1 ? "server" : "servers"} still running it
920
+ </span>
921
+ )}
922
+ </>
923
+ )}
924
+ {purge.state === "running" && <span>Uninstalled - deleting its data now</span>}
925
+ {purge.state === "done" && <span className="text-success font-medium">Data deleted{purge.completedAt ? ` ${formatDate(purge.completedAt)}` : ""}</span>}
926
+ {purge.state === "failed" && (
927
+ <>
928
+ <span className="text-danger break-words">Data deletion failed: {purgeFailure(purge)}</span>
929
+ {failedSteps.length > 0 && (
930
+ <ul className="list-disc pl-4 text-text-muted break-words">
931
+ {failedSteps.map((step) => (
932
+ <li key={step.step}>
933
+ {step.step}: {step.error ?? "failed"}
934
+ </li>
935
+ ))}
936
+ </ul>
937
+ )}
938
+ </>
939
+ )}
940
+ </div>
941
+ </td>
942
+ <td role="cell" className={`${INSTALLED_TD_CLASS} basis-full pt-1 pb-3`}>
943
+ {purge.state === "failed" && (
944
+ <div className="flex lg:justify-end">
945
+ <Button
946
+ type="button"
947
+ variant="secondary"
948
+ className="!w-auto"
949
+ aria-label={`Retry deleting the data of ${purgeName(purge)}`}
950
+ disabled={busy}
951
+ loading={busy}
952
+ onClick={onRetry}
953
+ >
954
+ Retry
955
+ </Button>
956
+ </div>
957
+ )}
958
+ </td>
959
+ </tr>
960
+ );
961
+ }
962
+
814
963
  function AddPluginModal({
815
964
  open,
816
965
  onClose,
@@ -1252,37 +1401,91 @@ function SettingField({
1252
1401
  );
1253
1402
  }
1254
1403
 
1255
- function RemoveModal({ plugin, onClose, onRemoved }: { plugin: Plugin; onClose: () => void; onRemoved: () => void }) {
1404
+ function RemoveModal({ plugin, onClose, onRemoved }: { plugin: Plugin; onClose: () => void; onRemoved: (result: RemovePluginResult) => void }) {
1256
1405
  const [error, setError] = useState<string | null>(null);
1257
1406
  const [busy, setBusy] = useState(false);
1258
-
1259
- async function remove() {
1407
+ const [purgeData, setPurgeData] = useState(false);
1408
+ const [typed, setTyped] = useState("");
1409
+ const helpId = useId();
1410
+ const displayName = plugin.manifest.displayName;
1411
+ // Deleting data is destructive and permanent, so it also takes the plugin's name typed out.
1412
+ const confirmed = !purgeData || typed.trim().toLowerCase() === displayName.trim().toLowerCase();
1413
+
1414
+ async function remove(event: FormEvent) {
1415
+ event.preventDefault();
1260
1416
  setBusy(true);
1261
1417
  setError(null);
1262
1418
  try {
1263
- await removePlugin(plugin.uid);
1264
- onRemoved();
1419
+ onRemoved(await removePlugin(plugin.uid, { purgeData }));
1265
1420
  } catch (err) {
1266
- setError(errorMessage(err, "Could not uninstall the plugin."));
1421
+ setError(isElevationRequired(err) ? ELEVATION_MESSAGE : errorMessage(err, "Could not uninstall the plugin."));
1267
1422
  setBusy(false);
1268
1423
  }
1269
1424
  }
1270
1425
 
1271
1426
  return (
1272
- <Modal open onClose={onClose} title={`Uninstall ${plugin.manifest.displayName}?`}>
1273
- {error && <Alert>{error}</Alert>}
1274
- <p className="text-sm mb-4">
1275
- The servers stop running this plugin after they restart. Data it stored stays in the database, and adding
1276
- the plugin again brings it back.
1277
- </p>
1278
- <div className="flex gap-2 justify-end">
1279
- <Button type="button" variant="secondary" className="!w-auto" onClick={onClose}>
1280
- Cancel
1281
- </Button>
1282
- <Button type="button" className="!w-auto" loading={busy} disabled={busy} onClick={() => void remove()}>
1283
- Uninstall
1284
- </Button>
1285
- </div>
1427
+ <Modal open onClose={onClose} title={`Uninstall ${displayName}?`}>
1428
+ <form onSubmit={(event) => void remove(event)}>
1429
+ {error && <Alert>{error}</Alert>}
1430
+ <p className="text-sm mb-4">
1431
+ {purgeData
1432
+ ? "The servers stop running this plugin after they restart, and then all the data it stored is deleted."
1433
+ : "The servers stop running this plugin after they restart. Data it stored stays in the database, and adding the plugin again brings it back."}
1434
+ </p>
1435
+ <div className="mb-4">
1436
+ <label className="flex items-start gap-2 text-sm font-semibold cursor-pointer">
1437
+ <input
1438
+ type="checkbox"
1439
+ className="mt-0.5 h-4 w-4 shrink-0 accent-danger"
1440
+ checked={purgeData}
1441
+ disabled={busy}
1442
+ aria-describedby={helpId}
1443
+ onChange={(e) => setPurgeData(e.target.checked)}
1444
+ />
1445
+ <span>Also delete all data this plugin stored</span>
1446
+ </label>
1447
+ <div id={helpId} className="mt-2 pl-6 text-xs text-text-muted">
1448
+ <p>What is deleted:</p>
1449
+ <ul className="list-disc pl-5 mt-1 space-y-0.5">
1450
+ <li>the database collections and tables the plugin&apos;s features use, with everything in them</li>
1451
+ <li>the plugin&apos;s saved settings</li>
1452
+ <li>its downloaded package and cached pages on the servers</li>
1453
+ <li>whatever else the plugin cleans up itself, such as files it stored or data kept outside the database</li>
1454
+ </ul>
1455
+ <p className="mt-2 text-danger font-semibold">This can&apos;t be undone.</p>
1456
+ </div>
1457
+ </div>
1458
+ {purgeData && (
1459
+ <>
1460
+ <p className="text-xs text-text-muted mb-3">
1461
+ Nothing is deleted while a server is still running the plugin. Once every server has restarted without it, usually
1462
+ within a few minutes, its data is deleted. Adding the plugin again before then cancels the deletion.
1463
+ </p>
1464
+ <label className="block text-sm mb-4">
1465
+ <span>
1466
+ Type <strong>{displayName}</strong> to confirm
1467
+ </span>
1468
+ <input
1469
+ type="text"
1470
+ className={`${INPUT_CLASS} mt-1.5`}
1471
+ value={typed}
1472
+ autoComplete="off"
1473
+ spellCheck={false}
1474
+ disabled={busy}
1475
+ onChange={(e) => setTyped(e.target.value)}
1476
+ />
1477
+ </label>
1478
+ </>
1479
+ )}
1480
+ <div className="flex gap-2 justify-end">
1481
+ <Button type="button" variant="secondary" className="!w-auto" onClick={onClose}>
1482
+ Cancel
1483
+ </Button>
1484
+ <Button type="submit" className={purgeData ? DANGER_BUTTON_CLASS : "!w-auto"} loading={busy} disabled={busy || !confirmed}>
1485
+ {purgeData ? "Uninstall and delete data" : "Uninstall"}
1486
+ </Button>
1487
+ </div>
1488
+ </form>
1286
1489
  </Modal>
1287
1490
  );
1288
1491
  }
@@ -391,7 +391,8 @@ function MailboxesStep({ userUid, domain }: { userUid: string; domain?: string }
391
391
  const [formKey, setFormKey] = useState(0);
392
392
 
393
393
  useEffect(() => {
394
- listMailboxes({ limit: 100 })
394
+ // The administration scope: every mailbox that exists (administrative metadata), not only the administrator's own.
395
+ listMailboxes({ limit: 100, scope: "admin" })
395
396
  .then((list) =>
396
397
  // Keep any mailbox created here while the list was loading.
397
398
  setMailboxes((prev) => [...list, ...prev.filter((mine) => !list.some((mailbox) => mailbox.uid === mine.uid))]),
@@ -6,6 +6,7 @@ import React, { FormEvent, useState } from "react";
6
6
  import { CALENDAR_COLOR_PALETTE, accentColorForMailbox, colorForFolder } from "@rapidmx/react-shared/calendar/calendarColors.js";
7
7
  import { Folder, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
8
8
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
9
+ import { notifyApiError } from "../../notifications/apiErrors.js";
9
10
 
10
11
  export interface CalendarListSidebarMailbox {
11
12
  mailbox: Mailbox;
@@ -42,7 +43,6 @@ export default function CalendarListSidebar({
42
43
  const [addingFor, setAddingFor] = useState<string | null>(null);
43
44
  const [name, setName] = useState("");
44
45
  const [color, setColor] = useState(CALENDAR_COLOR_PALETTE[0]);
45
- const [error, setError] = useState<string | null>(null);
46
46
  const [saving, setSaving] = useState(false);
47
47
  const multiple = mailboxCalendars.length > 1;
48
48
 
@@ -57,15 +57,15 @@ export default function CalendarListSidebar({
57
57
  if (!name.trim() || !addingFor) {
58
58
  return;
59
59
  }
60
- setError(null);
61
60
  setSaving(true);
62
61
  try {
63
62
  await onAddCalendar(addingFor, name.trim(), color);
64
63
  setName("");
65
64
  setColor(CALENDAR_COLOR_PALETTE[0]);
66
65
  setAddingFor(null);
67
- } catch {
68
- setError("Could not create this calendar.");
66
+ } catch (err) {
67
+ // A pop-up; the form stays open, with what was typed, for another try.
68
+ notifyApiError(err, "Couldn't create the calendar");
69
69
  } finally {
70
70
  setSaving(false);
71
71
  }
@@ -107,7 +107,6 @@ export default function CalendarListSidebar({
107
107
 
108
108
  return (
109
109
  <nav aria-label="My calendars" className="p-3 border-t border-border flex flex-col gap-3">
110
- {error && <Alert>{error}</Alert>}
111
110
  {mailboxCalendars.map(({ mailbox, calendarFolders, error: mailboxError }) => {
112
111
  const title = multiple ? `${mailbox.displayName}${mailbox.ownerUserUid ? "" : " (shared)"}` : "My calendars";
113
112
  return (
@@ -144,7 +144,11 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
144
144
  }
145
145
 
146
146
  return (
147
- <form onSubmit={handleSubmit} className="max-w-xl flex flex-col gap-1">
147
+ // The form fills the pane it is in: the fields scroll in their own area and Save / Cancel stay in view under them (`rr-solid`: opaque over a
148
+ // background image). Outside a bounded pane (the phone's page, which scrolls as a whole) the footer sticks above the bottom tab bar instead.
149
+ <form onSubmit={handleSubmit} className="@container flex min-h-0 flex-1 flex-col">
150
+ <div className="min-h-0 flex-1 overflow-y-auto p-6">
151
+ <div className="flex max-w-xl flex-col gap-1">
148
152
  <h1 className="text-xl font-bold uppercase tracking-wide mb-3">{contact ? "Edit contact" : "New contact"}</h1>
149
153
 
150
154
  {error && <Alert>{error}</Alert>}
@@ -177,7 +181,7 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
177
181
  />
178
182
  </FormField>
179
183
 
180
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
184
+ <div className="grid grid-cols-1 @md:grid-cols-2 gap-3">
181
185
  <FormField label="First name" htmlFor="contact-givenName">
182
186
  <input id="contact-givenName" type="text" className={INPUT_CLASS} value={givenName} onChange={(e) => setGivenName(e.target.value)} />
183
187
  </FormField>
@@ -186,7 +190,7 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
186
190
  </FormField>
187
191
  </div>
188
192
 
189
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
193
+ <div className="grid grid-cols-1 @md:grid-cols-2 gap-3">
190
194
  <FormField label="Company" htmlFor="contact-company">
191
195
  <input id="contact-company" type="text" className={INPUT_CLASS} value={company} onChange={(e) => setCompany(e.target.value)} />
192
196
  </FormField>
@@ -291,7 +295,7 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
291
295
  <div className="flex flex-col gap-3">
292
296
  {addresses.map((address, i) => (
293
297
  <div key={i} className="flex flex-col gap-2">
294
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
298
+ <div className="grid grid-cols-1 @md:grid-cols-2 gap-2">
295
299
  <input
296
300
  type="text"
297
301
  placeholder="Street"
@@ -364,7 +368,9 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
364
368
  <textarea id="contact-notes" rows={3} className={INPUT_CLASS} value={notes} onChange={(e) => setNotes(e.target.value)} />
365
369
  </FormField>
366
370
 
367
- <div className="flex gap-3 mt-2">
371
+ </div>
372
+ </div>
373
+ <div className="rr-solid sticky bottom-14 z-10 flex shrink-0 gap-3 border-t border-border bg-surface px-6 py-3 md:bottom-0">
368
374
  <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
369
375
  Save
370
376
  </Button>
@@ -7,6 +7,7 @@ import { HiOutlineBars3 } from "react-icons/hi2";
7
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
8
  import { Contact, ContactList, createContactList, listContactLists } from "@rapidmx/react-shared/contacts/contactsApi.js";
9
9
  import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
10
+ import { notifyApiError } from "../../notifications/apiErrors.js";
10
11
 
11
12
  export type ContactsView =
12
13
  | { type: "all" }
@@ -144,7 +145,8 @@ export default function ContactsSidebar({ mailboxUid, contacts, active, onSelect
144
145
  setNewListName("");
145
146
  setAddingList(false);
146
147
  } catch (err) {
147
- setListsError(err instanceof ApiRequestError ? err.message : "Could not create this list.");
148
+ // A pop-up: `listsError` is only the lists' own load failure.
149
+ notifyApiError(err, "Couldn't create the list");
148
150
  }
149
151
  }
150
152
 
@@ -229,13 +231,13 @@ export default function ContactsSidebar({ mailboxUid, contacts, active, onSelect
229
231
  <>
230
232
  <button
231
233
  type="button"
232
- className="md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
234
+ className="lg:hidden m-3 mb-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
233
235
  aria-label="Open contacts menu"
234
236
  onClick={() => setDrawerOpen(true)}
235
237
  >
236
238
  <HiOutlineBars3 size={20} aria-hidden="true" />
237
239
  </button>
238
- <nav aria-label="Contacts" className="hidden md:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto">
240
+ <nav aria-label="Contacts" className="hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto">
239
241
  {navContent("desktop")}
240
242
  </nav>
241
243
  <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Contacts">
@@ -3,7 +3,6 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { ChangeEvent, useRef } from "react";
6
- import type { IconType } from "react-icons";
7
6
  import {
8
7
  HiOutlineArrowDownTray,
9
8
  HiOutlineArrowUpTray,
@@ -17,6 +16,7 @@ import {
17
16
  } from "react-icons/hi2";
18
17
  import { SHORTCUTS } from "../../keyboard/keymap.js";
19
18
  import { useShortcutProps } from "../../keyboard/useShortcutProps.js";
19
+ import ResponsiveToolbar, { ToolbarAction } from "../layout/ResponsiveToolbar.js";
20
20
 
21
21
  export interface ContactsToolbarProps {
22
22
  selectedCount: number;
@@ -37,40 +37,10 @@ export interface ContactsToolbarProps {
37
37
  shortcuts?: boolean;
38
38
  }
39
39
 
40
- function ToolbarButton({
41
- label,
42
- icon: Icon,
43
- disabled,
44
- onClick,
45
- hint,
46
- }: {
47
- label: string;
48
- icon: IconType;
49
- disabled?: boolean;
50
- onClick: () => void;
51
- /** `title` and `aria-keyshortcuts` for a button with a shortcut. */
52
- hint?: { title: string; "aria-keyshortcuts"?: string };
53
- }) {
54
- return (
55
- <button
56
- type="button"
57
- onClick={onClick}
58
- disabled={disabled}
59
- {...hint}
60
- className="flex flex-col items-center gap-1 text-xs text-text-muted hover:text-text disabled:opacity-40 disabled:cursor-not-allowed px-2.5 py-1.5 rounded-sm hover:not-disabled:bg-surface-alt"
61
- >
62
- <Icon size={18} aria-hidden="true" />
63
- <span>{label}</span>
64
- </button>
65
- );
66
- }
67
-
68
- function Divider() {
69
- return <div className="w-px self-stretch my-1 bg-border" aria-hidden="true" />;
70
- }
71
-
72
40
  /**
73
- * Contacts' Outlook-style ribbon toolbar. Deliberately presentational — every action is a callback the
41
+ * Contacts' Outlook-style ribbon toolbar, fitted to its column (`ResponsiveToolbar`): captions under the icons while there is room, then icons
42
+ * alone, then the rarely used actions - Import, Export, Add category, and so on up - in a "More" menu; the bar can never spill into the pane
43
+ * beside it. Every button keeps its tooltip (New contact its shortcut's) and accessible name in every layout. Deliberately presentational — every action is a callback the
74
44
  * page (`apps/www/contacts/index.tsx`) implements, since it owns the actual contact data, the current
75
45
  * selection, and the mailbox/folder context every real action (create/delete/email/favorite/category)
76
46
  * needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
@@ -101,25 +71,28 @@ export default function ContactsToolbar({
101
71
  }
102
72
  }
103
73
 
74
+ const actions: ToolbarAction[] = [
75
+ { id: "new", label: "New contact", icon: HiOutlineUserPlus, onClick: onNewContact, hint: shortcuts ? newContactHint : undefined, group: 0, rank: 99, essential: true },
76
+ { id: "edit", label: "Edit", icon: HiOutlinePencil, onClick: onEdit, disabled: selectedCount !== 1, group: 1, rank: 6 },
77
+ { id: "delete", label: "Delete", icon: HiOutlineTrash, onClick: onDelete, disabled: !hasSelection, group: 1, rank: 5 },
78
+ { id: "email", label: "Email", icon: HiOutlineEnvelope, onClick: onEmail, disabled: !hasSelection, group: 2, rank: 4 },
79
+ {
80
+ id: "favorite",
81
+ label: allSelectedFavorited ? "Unfavorite" : "Favorite",
82
+ icon: allSelectedFavorited ? HiStar : HiOutlineStar,
83
+ onClick: onToggleFavorite,
84
+ disabled: !hasSelection,
85
+ group: 2,
86
+ rank: 3,
87
+ },
88
+ { id: "category", label: "Add category", icon: HiOutlineTag, onClick: onAddCategory, disabled: !hasSelection, group: 2, rank: 2 },
89
+ { id: "export", label: "Export", icon: HiOutlineArrowUpTray, onClick: onExportVCard, disabled: !hasSelection, group: 3, rank: 1 },
90
+ { id: "import", label: "Import", icon: HiOutlineArrowDownTray, onClick: () => importInputRef.current?.click(), group: 3, rank: 0 },
91
+ ];
92
+
104
93
  return (
105
- <div role="toolbar" aria-label="Contacts actions" className="border-b border-border bg-surface-alt flex items-center gap-0.5 px-2 py-1">
106
- <ToolbarButton label="New contact" icon={HiOutlineUserPlus} onClick={onNewContact} hint={shortcuts ? newContactHint : undefined} />
107
- <Divider />
108
- <ToolbarButton label="Edit" icon={HiOutlinePencil} disabled={selectedCount !== 1} onClick={onEdit} />
109
- <ToolbarButton label="Delete" icon={HiOutlineTrash} disabled={!hasSelection} onClick={onDelete} />
110
- <Divider />
111
- <ToolbarButton label="Email" icon={HiOutlineEnvelope} disabled={!hasSelection} onClick={onEmail} />
112
- <ToolbarButton
113
- label={allSelectedFavorited ? "Unfavorite" : "Favorite"}
114
- icon={allSelectedFavorited ? HiStar : HiOutlineStar}
115
- disabled={!hasSelection}
116
- onClick={onToggleFavorite}
117
- />
118
- <ToolbarButton label="Add category" icon={HiOutlineTag} disabled={!hasSelection} onClick={onAddCategory} />
119
- <Divider />
120
- <ToolbarButton label="Export" icon={HiOutlineArrowUpTray} disabled={!hasSelection} onClick={onExportVCard} />
121
- <ToolbarButton label="Import" icon={HiOutlineArrowDownTray} onClick={() => importInputRef.current?.click()} />
94
+ <ResponsiveToolbar label="Contacts actions" actions={actions}>
122
95
  <input ref={importInputRef} type="file" accept=".vcf" className="sr-only" aria-label="Import contacts file" onChange={handleImportFileChosen} />
123
- </div>
96
+ </ResponsiveToolbar>
124
97
  );
125
98
  }
@@ -169,19 +169,24 @@ export default function ContactsShell({
169
169
  </>
170
170
  );
171
171
 
172
+ // The mailbox switcher (or an error) is all this column holds: with a single mailbox and no error there is nothing to put in it, and an empty
173
+ // 224 px column beside the contacts' own menu is wasted room.
174
+ const hasSidebar = mailboxes.length > 1 || !!folderError;
172
175
  inner = (
173
176
  <>
174
- <aside className="hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3">
175
- {sidebarContent("desktop")}
176
- </aside>
177
+ {hasSidebar && (
178
+ <aside className="hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3">
179
+ {sidebarContent("desktop")}
180
+ </aside>
181
+ )}
177
182
  <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Mailbox">
178
183
  <div className="flex flex-col gap-3">{sidebarContent("mobile")}</div>
179
184
  </Drawer>
180
185
  <main className="flex-1 min-w-0 flex flex-col">
181
- {(mailboxes.length > 1 || folderError) && (
186
+ {hasSidebar && (
182
187
  <button
183
188
  type="button"
184
- className="md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
189
+ className="lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
185
190
  aria-label="Open mailbox switcher"
186
191
  onClick={() => setDrawerOpen(true)}
187
192
  >