@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,161 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useRef, useState } from "react";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { getSigningEnrollmentInfo, listSigningEnrollments, looksLikePemCertificate, rejectSigningEnrollment, signingEnrollmentCsrUrl, uploadSigningEnrollmentCertificate, } from "@rapidmx/react-shared/crypto/signingProviderApi.js";
9
+ import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
+ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
13
+ import { notify } from "../../shared/notifications/store.js";
14
+ const INPUT_CLASS = "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface";
15
+ const TEXTAREA_CLASS = `${INPUT_CLASS} font-mono text-xs`;
16
+ function errorMessage(err, fallback) {
17
+ return err instanceof ApiRequestError ? err.message : fallback;
18
+ }
19
+ function statusBadgeClass(status) {
20
+ if (status === "failed")
21
+ return "bg-danger-bg text-danger";
22
+ if (status === "pending")
23
+ return "bg-surface-alt text-text-muted";
24
+ return "bg-success text-white";
25
+ }
26
+ function providerLabel(provider) {
27
+ return provider === "rfc8823" ? "Automatic (RFC 8823)" : "Manual";
28
+ }
29
+ export default function SigningCertificatesPage(props) {
30
+ return (_jsx(AdminShell, { ...props, active: "signingCertificates", children: _jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Signing Certificates" }), _jsx("p", { className: "text-sm text-text-muted", children: "Every mailbox's pending signing (digital-signature) certificate request. Requests issued automatically by a certificate authority are listed for visibility only; a manual request needs a certificate uploaded here once the certificate authority has issued one." })] }), _jsx(BackendSummary, {}), _jsx(EnrollmentsTable, {})] }) }));
31
+ }
32
+ /** How signing certificates are issued in this deployment - fetched once, since it doesn't change while the page is open. */
33
+ function BackendSummary() {
34
+ const [info, setInfo] = useState(null);
35
+ const [error, setError] = useState(null);
36
+ useEffect(() => {
37
+ getSigningEnrollmentInfo()
38
+ .then(setInfo)
39
+ .catch((err) => setError(errorMessage(err, "Could not load the signing certificate backend.")));
40
+ }, []);
41
+ if (error) {
42
+ return _jsx(Alert, { children: error });
43
+ }
44
+ if (!info) {
45
+ return null;
46
+ }
47
+ if (info.backend === "none") {
48
+ return _jsx("p", { className: "text-sm text-text-muted", children: "Signing certificates are not enabled in this deployment." });
49
+ }
50
+ return (_jsxs("div", { className: "text-sm border border-border rounded-sm p-3 bg-surface-alt flex flex-col gap-1", children: [_jsxs("span", { children: [_jsx("strong", { children: info.automatic ? "Automatic issuance" : "Manual issuance" }), info.ca && _jsxs(_Fragment, { children: [" through ", info.ca.host] }), info.typicalDurationMinutes !== undefined && _jsxs(_Fragment, { children: [" \u00B7 typically ", info.typicalDurationMinutes, " minutes"] })] }), info.health && !info.health.ok && (_jsxs("span", { className: "text-danger", children: ["The certificate authority could not be reached", info.health.lastError && _jsxs(_Fragment, { children: [": ", info.health.lastError] }), "."] }))] }));
51
+ }
52
+ function EnrollmentsTable() {
53
+ const [items, setItems] = useState(null);
54
+ const [loadError, setLoadError] = useState(null);
55
+ const [uploadTarget, setUploadTarget] = useState(null);
56
+ const [rejectTarget, setRejectTarget] = useState(null);
57
+ async function reload() {
58
+ try {
59
+ setItems(await listSigningEnrollments());
60
+ setLoadError(null);
61
+ }
62
+ catch (err) {
63
+ setLoadError(errorMessage(err, "Could not load the pending signing certificate requests."));
64
+ }
65
+ }
66
+ useEffect(() => {
67
+ void reload();
68
+ }, []);
69
+ if (loadError) {
70
+ return _jsx(Alert, { children: loadError });
71
+ }
72
+ if (items === null) {
73
+ return _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" });
74
+ }
75
+ if (items.length === 0) {
76
+ return _jsx("p", { className: "text-sm text-text-muted", children: "No pending signing certificate requests." });
77
+ }
78
+ return (_jsxs("div", { className: "overflow-x-auto", children: [_jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "text-left text-xs font-bold uppercase tracking-wide text-text-muted border-b border-border", children: [_jsx("th", { className: "py-2 pr-3", children: "Mailbox" }), _jsx("th", { className: "py-2 pr-3", children: "Requested" }), _jsx("th", { className: "py-2 pr-3", children: "Provider" }), _jsx("th", { className: "py-2 pr-3", children: "Status" }), _jsx("th", { className: "py-2 pr-3", children: "Actions" })] }) }), _jsx("tbody", { children: items.map((row) => (_jsxs("tr", { className: "border-b border-border", children: [_jsx("td", { className: "py-2 pr-3 break-all", children: row.identity }), _jsx("td", { className: "py-2 pr-3 whitespace-nowrap", children: new Date(row.requestedAt).toLocaleString() }), _jsx("td", { className: "py-2 pr-3 whitespace-nowrap", children: providerLabel(row.provider) }), _jsxs("td", { className: "py-2 pr-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(row.status)}`, children: row.status }), row.lastError && _jsx("div", { className: "text-xs text-danger mt-1 max-w-xs", children: row.lastError })] }), _jsx("td", { className: "py-2 pr-3", children: _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [row.provider === "manual" && (_jsx("a", { href: signingEnrollmentCsrUrl(row.enrollmentId), className: "text-primary-dark hover:underline font-medium whitespace-nowrap", children: "Download CSR" })), row.canUpload && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setUploadTarget(row), children: "Upload certificate" })), row.status === "pending" && row.provider === "manual" && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setRejectTarget(row), children: "Reject" })), row.uploadBlockedReason && !row.canUpload && (_jsx("span", { className: "text-xs text-text-muted max-w-xs", children: row.uploadBlockedReason }))] }) })] }, row.enrollmentId))) })] }), _jsx(UploadCertificateModal, { target: uploadTarget, onClose: () => setUploadTarget(null), onDone: reload }), _jsx(RejectModal, { target: rejectTarget, onClose: () => setRejectTarget(null), onDone: reload })] }));
79
+ }
80
+ function UploadCertificateModal({ target, onClose, onDone, }) {
81
+ const [certificate, setCertificate] = useState("");
82
+ const [uploading, setUploading] = useState(false);
83
+ const [error, setError] = useState(null);
84
+ const fileInputRef = useRef(null);
85
+ function reset() {
86
+ setCertificate("");
87
+ setError(null);
88
+ }
89
+ function dismiss() {
90
+ if (!uploading) {
91
+ reset();
92
+ onClose();
93
+ }
94
+ }
95
+ function handleFileChange(e) {
96
+ const file = e.target.files?.[0];
97
+ e.target.value = "";
98
+ if (!file) {
99
+ return;
100
+ }
101
+ file.text()
102
+ .then((text) => setCertificate(text))
103
+ .catch(() => setError("Could not read this file."));
104
+ }
105
+ // Only invoked from the button below, which only renders once `target` is set (the modal's body) - same
106
+ // non-null-assertion pattern as `data-requests`' `handleApprove()`/`handleDeny()`.
107
+ async function handleUpload() {
108
+ setError(null);
109
+ setUploading(true);
110
+ try {
111
+ const result = await uploadSigningEnrollmentCertificate(target.enrollmentId, certificate);
112
+ notify({
113
+ kind: "success",
114
+ title: "Certificate accepted",
115
+ message: `${result.identity}: ${result.message}`,
116
+ });
117
+ reset();
118
+ onClose();
119
+ await onDone();
120
+ }
121
+ catch (err) {
122
+ setError(errorMessage(err, "Could not upload this certificate."));
123
+ }
124
+ finally {
125
+ setUploading(false);
126
+ }
127
+ }
128
+ const sane = looksLikePemCertificate(certificate);
129
+ return (_jsxs(Modal, { open: target !== null, onClose: dismiss, title: "Upload signing certificate", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Paste, or choose a file for, the certificate (or certificate chain, leaf first) the certificate authority issued for", " ", _jsx("strong", { className: "break-all", children: target?.identity }), "."] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-3", children: [_jsx("span", { className: "font-semibold", children: "Certificate (PEM)" }), _jsx("textarea", { "aria-label": "Certificate PEM", className: TEXTAREA_CLASS, rows: 8, value: certificate, onChange: (e) => setCertificate(e.target.value), placeholder: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----" })] }), _jsxs("div", { className: "mb-4", children: [_jsx("input", { ref: fileInputRef, type: "file", accept: ".pem,.crt,.cer,.txt", className: "hidden", onChange: handleFileChange }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => fileInputRef.current?.click(), children: "Choose file\u2026" })] }), certificate.trim() !== "" && !sane && (_jsx("p", { role: "status", className: "mb-3 py-2 px-3 rounded-sm text-xs bg-warning/15 text-text", children: "This doesn't look like a PEM certificate (it should start with -----BEGIN CERTIFICATE-----)." })), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-3 justify-end mt-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: uploading, onClick: dismiss, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: uploading, disabled: uploading || !certificate.trim(), onClick: handleUpload, children: "Upload" })] })] }));
130
+ }
131
+ function RejectModal({ target, onClose, onDone }) {
132
+ const [reason, setReason] = useState("");
133
+ const [rejecting, setRejecting] = useState(false);
134
+ const [error, setError] = useState(null);
135
+ function dismiss() {
136
+ if (!rejecting) {
137
+ setReason("");
138
+ setError(null);
139
+ onClose();
140
+ }
141
+ }
142
+ // Only invoked from the button below, which only renders once `target` is set - see `handleUpload()`'s identical note.
143
+ async function handleReject() {
144
+ setError(null);
145
+ setRejecting(true);
146
+ try {
147
+ await rejectSigningEnrollment(target.enrollmentId, reason);
148
+ notify({ kind: "info", title: "Request rejected", message: `${target.identity}'s signing certificate request was rejected.` });
149
+ setReason("");
150
+ onClose();
151
+ await onDone();
152
+ }
153
+ catch (err) {
154
+ setError(errorMessage(err, "Could not reject this request."));
155
+ }
156
+ finally {
157
+ setRejecting(false);
158
+ }
159
+ }
160
+ return (_jsxs(Modal, { open: target !== null, onClose: dismiss, title: "Reject signing certificate request", children: [_jsxs("p", { className: "text-sm mb-3", children: ["The reason below is shown to ", _jsx("strong", { className: "break-all", children: target?.identity }), "'s owner."] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-4", children: [_jsx("span", { className: "font-semibold", children: "Reason" }), _jsx("input", { "aria-label": "Rejection reason", className: INPUT_CLASS, value: reason, onChange: (e) => setReason(e.target.value) })] }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: rejecting, onClick: dismiss, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: rejecting, disabled: rejecting || !reason.trim(), onClick: handleReject, children: "Reject" })] })] }));
161
+ }
@@ -4,7 +4,11 @@ export interface LayoutProps {
4
4
  /** See `apps/www/_layout.tsx`'s `LayoutProps` doc comment — identical mechanism, supplied by
5
5
  * `EscrowConsoleRoute`'s `fetchProps()`. */
6
6
  branding?: Branding;
7
+ /** 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`. */
8
+ appearance?: unknown;
9
+ /** The signed-in user (supplied by the framework), so a cached appearance from another account on this browser is never applied. */
10
+ userUid?: string;
7
11
  }
8
12
  /** Unlike the other apps, never links the admin-configured custom stylesheet: the escrow console handles escrow
9
13
  * key material, so no admin-supplied CSS is loaded on it. */
10
- export default function Layout({ children, branding }: PropsWithChildren<LayoutProps>): React.JSX.Element;
14
+ export default function Layout({ children, branding, appearance, userUid }: PropsWithChildren<LayoutProps>): React.JSX.Element;
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AppearanceHead, appearanceHtmlAttributes } from "../shared/appearance/AppearanceHead.js";
2
3
  /** Unlike the other apps, never links the admin-configured custom stylesheet: the escrow console handles escrow
3
4
  * key material, so no admin-supplied CSS is loaded on it. */
4
- export default function Layout({ children, branding }) {
5
+ export default function Layout({ children, branding, appearance, userUid }) {
5
6
  const title = branding?.title || branding?.companyName
6
7
  ? `${branding?.title || branding?.companyName}: Escrow Console`
7
8
  : "RapidMX: Escrow Console";
8
9
  const iconHref = branding?.iconUrl || branding?.logoUrl || "/images/logo.svg";
9
- return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: title }), _jsx("link", { rel: "icon", type: "image/svg+xml", href: iconHref }), _jsx("link", { rel: "alternate icon", href: "/favicon.ico" }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }), _jsx("link", { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" })] }), _jsx("body", { children: children })] }));
10
+ return (_jsxs("html", { lang: "en", ...appearanceHtmlAttributes(appearance, userUid), children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: title }), _jsx("link", { rel: "icon", type: "image/svg+xml", href: iconHref }), _jsx("link", { rel: "alternate icon", href: "/favicon.ico" }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }), _jsx("link", { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" }), _jsx(AppearanceHead, { appearance: appearance })] }), _jsx("body", { children: children })] }));
10
11
  }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ /**
3
+ * Settings > Appearance: the colour scheme, the theme colours and the background. Every control changes the app itself at once - the
4
+ * page you are on is the preview - and saves in the background (`useAppearance()`); a save that fails puts the change back and says so
5
+ * here. Nothing waits on the server, and nothing is disabled while it works.
6
+ */
7
+ export default function AppearanceForm(): React.JSX.Element;
@@ -0,0 +1,131 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useRef, useState } from "react";
7
+ import { APPEARANCE_MODES, BACKGROUND_BLUR_MAX, BACKGROUND_DIM_MAX, BACKGROUND_FITS, BACKGROUND_IMAGE_TYPES, BACKGROUND_MAX_BYTES, DEFAULT_BACKGROUND, validateBackgroundFile, } from "@rapidmx/react-shared/appearance/preferencesApi.js";
8
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
9
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
10
+ import ColorField from "./ColorField.js";
11
+ import { normalizeHex } from "./color.js";
12
+ import { useAppearance } from "./appearanceContext.js";
13
+ import { DEFAULT_PALETTE, contrastWarnings } from "./theme.js";
14
+ const MODE_LABELS = {
15
+ system: { label: "System", hint: "Follow this device" },
16
+ light: { label: "Light", hint: "Always light" },
17
+ dark: { label: "Dark", hint: "Always dark" },
18
+ };
19
+ const FIT_LABELS = {
20
+ cover: "Cover (fill the window)",
21
+ contain: "Contain (show all of it)",
22
+ tile: "Tile (repeat)",
23
+ };
24
+ const COLOR_FIELDS = [
25
+ { key: "primary", label: "Primary", description: "Links, the selected app and folder, and focus rings.", token: "--rr-color-primary" },
26
+ { key: "accent", label: "Accent", description: "Main buttons and highlights.", token: "--rr-color-accent" },
27
+ { key: "surface", label: "Surface", description: "Panels, lists and menus.", token: "--rr-color-surface" },
28
+ { key: "text", label: "Text", description: "Everything written on a surface.", token: "--rr-color-text" },
29
+ ];
30
+ /** The colour a token has right now on the page (the app's, the branding's, or the user's own), as `#rrggbb`; `fallback` where it can't be read (jsdom, or a value that isn't a colour). Only called from an effect, so there is always a document. */
31
+ function readToken(token, fallback) {
32
+ return normalizeHex(getComputedStyle(document.documentElement).getPropertyValue(token)) ?? fallback;
33
+ }
34
+ const DEFAULT_TOKEN_COLORS = {
35
+ primary: { light: "#0d9488", dark: "#2dd4bf" },
36
+ accent: { light: "#a3690a", dark: "#f2af0d" },
37
+ surface: { light: DEFAULT_PALETTE.light.surface, dark: DEFAULT_PALETTE.dark.surface },
38
+ text: { light: DEFAULT_PALETTE.light.text, dark: DEFAULT_PALETTE.dark.text },
39
+ };
40
+ /** The colour a new "Colour" background starts as. */
41
+ const STARTING_BACKGROUND_COLOR = { light: "#dbe4ee", dark: "#1e293b" };
42
+ function formatBytes(bytes) {
43
+ return `${bytes / (1024 * 1024)} MB`;
44
+ }
45
+ /**
46
+ * Settings > Appearance: the colour scheme, the theme colours and the background. Every control changes the app itself at once - the
47
+ * page you are on is the preview - and saves in the background (`useAppearance()`); a save that fails puts the change back and says so
48
+ * here. Nothing waits on the server, and nothing is disabled while it works.
49
+ */
50
+ export default function AppearanceForm() {
51
+ const appearance = useAppearance();
52
+ const { prefs, resolved, setPrefs, uploadBackground, removeBackground, reset, error, clearError, saving, isDefault, backgroundUrl } = appearance;
53
+ const background = prefs.background;
54
+ const colors = prefs.colors;
55
+ // The kind the radio shows: chosen before it can take effect ("Image" is chosen before there is one).
56
+ const [kind, setKind] = useState(background?.kind ?? "none");
57
+ useEffect(() => setKind(background?.kind ?? "none"), [background?.kind]);
58
+ const [fileError, setFileError] = useState(null);
59
+ const [dragging, setDragging] = useState(false);
60
+ const [uploading, setUploading] = useState(null);
61
+ const inputRef = useRef(null);
62
+ // What the colour fields show while unset: the tokens as they are on the page (a deployment's branding may have its own).
63
+ const [current, setCurrent] = useState(() => ({
64
+ primary: DEFAULT_TOKEN_COLORS.primary[resolved],
65
+ accent: DEFAULT_TOKEN_COLORS.accent[resolved],
66
+ surface: DEFAULT_TOKEN_COLORS.surface[resolved],
67
+ text: DEFAULT_TOKEN_COLORS.text[resolved],
68
+ }));
69
+ useEffect(() => {
70
+ setCurrent(Object.fromEntries(COLOR_FIELDS.map((field) => [field.key, readToken(field.token, DEFAULT_TOKEN_COLORS[field.key][resolved])])));
71
+ }, [resolved, colors]);
72
+ const warnings = contrastWarnings(colors, resolved);
73
+ const hasImage = background?.imageVersion !== undefined;
74
+ function chooseKind(next) {
75
+ setKind(next);
76
+ if (next === "none") {
77
+ setPrefs({ background: { kind: "none" } });
78
+ }
79
+ else if (next === "color") {
80
+ setPrefs({ background: { kind: "color", color: background?.color ?? STARTING_BACKGROUND_COLOR[resolved] } });
81
+ }
82
+ else if (hasImage) {
83
+ setPrefs({ background: { kind: "image" } });
84
+ }
85
+ }
86
+ function handleFile(file) {
87
+ if (!file) {
88
+ return;
89
+ }
90
+ const problem = validateBackgroundFile(file);
91
+ if (problem) {
92
+ setFileError(problem);
93
+ return;
94
+ }
95
+ setFileError(null);
96
+ setUploading(file.name);
97
+ void uploadBackground(file).finally(() => setUploading(null));
98
+ }
99
+ function handleDrop(event) {
100
+ event.preventDefault();
101
+ setDragging(false);
102
+ handleFile(event.dataTransfer.files[0]);
103
+ }
104
+ const showImageControls = kind === "image";
105
+ const dim = background?.dim ?? DEFAULT_BACKGROUND.dim;
106
+ const blur = background?.blur ?? DEFAULT_BACKGROUND.blur;
107
+ const fit = background?.fit ?? DEFAULT_BACKGROUND.fit;
108
+ return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-2xl flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Appearance" }), _jsx("p", { className: "text-sm text-text-muted", children: "Choose how RapidMX looks for you. Every change shows straight away, across the whole app, and is saved to your account for your other devices." }), _jsx("p", { role: "status", "aria-live": "polite", className: "mt-1 h-4 text-xs text-text-muted", children: saving ? "Saving…" : "" })] }), error && (_jsxs(Alert, { children: [_jsx("span", { className: "flex-1", children: error }), _jsx("button", { type: "button", className: "font-semibold underline hover:no-underline", onClick: clearError, children: "Dismiss" })] })), _jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Colour scheme" }), _jsx("div", { className: "flex flex-wrap gap-2", children: APPEARANCE_MODES.map((mode) => (_jsxs("label", { className: [
109
+ "flex cursor-pointer items-center gap-2 rounded-sm border px-3 py-2 text-sm",
110
+ prefs.mode === mode ? "border-primary bg-primary/10" : "border-border hover:bg-surface-alt",
111
+ ].join(" "), children: [_jsx("input", { type: "radio", name: "appearance-mode", value: mode, checked: prefs.mode === mode, onChange: () => setPrefs({ mode }) }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold", children: MODE_LABELS[mode].label }), _jsx("span", { className: "ml-1.5 text-xs text-text-muted", children: MODE_LABELS[mode].hint })] })] }, mode))) })] }), _jsxs("fieldset", { className: "flex flex-col gap-4", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Theme colours" }), _jsx("p", { className: "text-xs text-text-muted -mt-2", children: "These apply in both the light and the dark scheme. A colour you have not chosen stays as the app (or your organization) has it." }), COLOR_FIELDS.map((field) => (_jsx(ColorField, { id: `appearance-${field.key}`, label: field.label, description: field.description, value: colors?.[field.key], current: current[field.key], onChange: (hex) => setPrefs({ colors: { [field.key]: hex } }), onReset: () => setPrefs({ colors: { [field.key]: null } }) }, field.key))), warnings.map((warning) => (_jsx("p", { role: "status", className: "rounded-sm bg-warning px-3 py-2 text-sm font-medium text-warning-contrast", children: warning.message }, warning.id)))] }), _jsxs("fieldset", { className: "flex flex-col gap-4", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Background" }), _jsx("div", { className: "flex flex-wrap gap-2", children: ["none", "color", "image"].map((option) => (_jsxs("label", { className: [
112
+ "flex cursor-pointer items-center gap-2 rounded-sm border px-3 py-2 text-sm",
113
+ kind === option ? "border-primary bg-primary/10" : "border-border hover:bg-surface-alt",
114
+ ].join(" "), children: [_jsx("input", { type: "radio", name: "appearance-background", value: option, checked: kind === option, onChange: () => chooseKind(option) }), _jsx("span", { className: "font-semibold", children: option === "none" ? "None" : option === "color" ? "Colour" : "Image" })] }, option))) }), kind === "color" && background?.color && (_jsx(ColorField, { id: "appearance-background-color", label: "Background colour", description: "Shows behind every panel of the app.", value: background.color, current: background.color, onChange: (hex) => setPrefs({ background: { kind: "color", color: hex } }) })), showImageControls && (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { role: "group", "aria-label": "Background image", onDragEnter: (event) => {
115
+ event.preventDefault();
116
+ setDragging(true);
117
+ }, onDragOver: (event) => {
118
+ event.preventDefault();
119
+ setDragging(true);
120
+ }, onDragLeave: () => setDragging(false), onDrop: handleDrop, className: [
121
+ "flex flex-wrap items-center gap-4 rounded-md border-2 border-dashed p-4",
122
+ dragging ? "border-primary bg-primary/10" : "border-border",
123
+ ].join(" "), children: [hasImage && backgroundUrl && (_jsx("img", { src: backgroundUrl, alt: "Your background image", className: "h-20 w-32 shrink-0 rounded-sm border border-border object-cover" })), _jsxs("div", { className: "min-w-0 flex-1 basis-48 text-sm", children: [_jsx("p", { className: "font-semibold", children: hasImage ? "Replace the image" : "Add an image" }), _jsxs("p", { className: "text-xs text-text-muted", children: ["Drag a picture here, or choose one. PNG, JPEG, WebP or AVIF, up to ", formatBytes(BACKGROUND_MAX_BYTES), "."] }), uploading && (_jsxs("p", { role: "status", className: "mt-1 text-xs text-text-muted", children: ["Uploading ", uploading, "\u2026"] }))] }), _jsx("input", { ref: inputRef, type: "file", accept: BACKGROUND_IMAGE_TYPES.join(","), "aria-label": "Choose a background image", className: "sr-only", onChange: (event) => {
124
+ handleFile(event.target.files?.[0]);
125
+ // The same file can be chosen again.
126
+ event.target.value = "";
127
+ } }), _jsxs("div", { className: "flex shrink-0 gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => inputRef.current?.click(), children: "Choose image" }), hasImage && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => {
128
+ setFileError(null);
129
+ void removeBackground();
130
+ }, children: "Remove" }))] })] }), fileError && (_jsx("p", { role: "alert", className: "text-sm text-danger", children: fileError })), hasImage && (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { children: [_jsxs("label", { htmlFor: "appearance-dim", className: "flex justify-between text-sm font-semibold", children: [_jsx("span", { children: "Dim" }), _jsxs("span", { className: "font-normal text-text-muted", children: [Math.round(dim * 100), "%"] })] }), _jsx("input", { id: "appearance-dim", type: "range", min: 0, max: Math.round(BACKGROUND_DIM_MAX * 100), step: 1, value: Math.round(dim * 100), "aria-valuetext": `${Math.round(dim * 100)} percent`, onChange: (event) => setPrefs({ background: { dim: Number(event.target.value) / 100 } }), className: "w-full accent-primary" }), _jsx("p", { className: "text-xs text-text-muted", children: "Lays the app\u2019s surface colour over the picture so text stays easy to read." })] }), _jsxs("div", { children: [_jsxs("label", { htmlFor: "appearance-blur", className: "flex justify-between text-sm font-semibold", children: [_jsx("span", { children: "Blur" }), _jsxs("span", { className: "font-normal text-text-muted", children: [blur, " px"] })] }), _jsx("input", { id: "appearance-blur", type: "range", min: 0, max: BACKGROUND_BLUR_MAX, step: 1, value: blur, "aria-valuetext": `${blur} pixels`, onChange: (event) => setPrefs({ background: { blur: Number(event.target.value) } }), className: "w-full accent-primary" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "appearance-fit", className: "block text-sm font-semibold mb-1", children: "Fit" }), _jsx("select", { id: "appearance-fit", value: fit, onChange: (event) => setPrefs({ background: { fit: event.target.value } }), className: "w-full max-w-xs rounded-sm border border-border bg-surface px-2 py-1.5 text-sm text-text", children: BACKGROUND_FITS.map((option) => (_jsx("option", { value: option, children: FIT_LABELS[option] }, option))) })] })] }))] }))] }), _jsx("div", { children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: isDefault, onClick: () => void reset(), children: "Reset all" }) })] }) }));
131
+ }
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ /** What a page layout puts on `<html>` for the server-rendered appearance: the explicit scheme (never for "System", which the stylesheet's own media query follows) and the signed-in user. */
3
+ export declare function appearanceHtmlAttributes(appearance: unknown, userUid?: string): {
4
+ "data-theme"?: "light" | "dark";
5
+ "data-uid"?: string;
6
+ };
7
+ /**
8
+ * The `<head>` part of the user's appearance, for every page layout (`_layout.tsx` of www, admin and escrow): the stylesheet the server
9
+ * built from the preferences it was given (`appearance`, a page prop - only the www pages get one), so the first byte of the page is
10
+ * already themed, and the boot script (`APPEARANCE_BOOT_SCRIPT`), which applies the browser's cached copy before the first paint on pages
11
+ * that weren't given any. Nothing user-controlled reaches the CSS but validated colours, numbers and an image version made of safe characters.
12
+ */
13
+ export declare function AppearanceHead({ appearance }: {
14
+ appearance?: unknown;
15
+ }): React.JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { appearanceBackgroundUrl, normalizeAppearance } from "@rapidmx/react-shared/appearance/preferencesApi.js";
3
+ import { APPEARANCE_BOOT_SCRIPT } from "./bootScript.js";
4
+ import { cacheKeyOf, timeOf } from "./appearanceCache.js";
5
+ import { APPEARANCE_STYLE_ID, appearanceCss } from "./theme.js";
6
+ /** What a page layout puts on `<html>` for the server-rendered appearance: the explicit scheme (never for "System", which the stylesheet's own media query follows) and the signed-in user. */
7
+ export function appearanceHtmlAttributes(appearance, userUid) {
8
+ const prefs = normalizeAppearance(appearance);
9
+ const attributes = {};
10
+ if (prefs && prefs.mode !== "system") {
11
+ attributes["data-theme"] = prefs.mode;
12
+ }
13
+ if (userUid) {
14
+ attributes["data-uid"] = userUid;
15
+ }
16
+ return attributes;
17
+ }
18
+ /**
19
+ * The `<head>` part of the user's appearance, for every page layout (`_layout.tsx` of www, admin and escrow): the stylesheet the server
20
+ * built from the preferences it was given (`appearance`, a page prop - only the www pages get one), so the first byte of the page is
21
+ * already themed, and the boot script (`APPEARANCE_BOOT_SCRIPT`), which applies the browser's cached copy before the first paint on pages
22
+ * that weren't given any. Nothing user-controlled reaches the CSS but validated colours, numbers and an image version made of safe characters.
23
+ */
24
+ export function AppearanceHead({ appearance }) {
25
+ const prefs = normalizeAppearance(appearance);
26
+ const version = prefs?.background?.imageVersion;
27
+ const css = appearanceCss(prefs, { imageUrl: version === undefined ? undefined : appearanceBackgroundUrl(version) });
28
+ return (_jsxs(_Fragment, { children: [prefs && (_jsx("style", { id: APPEARANCE_STYLE_ID, "data-key": cacheKeyOf(prefs), "data-t": timeOf(prefs.updatedAt),
29
+ // Present even when it is empty, for its time: the boot script keeps a browser copy that is newer than this page's idea of nothing. Cannot contain a closing tag: nothing in the CSS is free text.
30
+ dangerouslySetInnerHTML: { __html: css.replace(/</g, "\\3c ") } })), _jsx("script", { dangerouslySetInnerHTML: { __html: APPEARANCE_BOOT_SCRIPT } })] }));
31
+ }
@@ -0,0 +1,32 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ import { AppearancePreferences } from "@rapidmx/react-shared/appearance/preferencesApi.js";
3
+ import { AppearancePatch } from "./appearanceContext.js";
4
+ /** How long a change waits for a further one before it is saved - a slider being dragged is one save, not sixty. */
5
+ export declare const APPEARANCE_SAVE_DELAY_MS = 400;
6
+ /** `prefs` with `patch` merged in, normalised (so an out-of-range number or bad colour can't get through). See `AppearancePatch`. */
7
+ export declare function mergeAppearance(prefs: AppearancePreferences, patch: AppearancePatch): AppearancePreferences;
8
+ export interface AppearanceProviderProps {
9
+ /** The signed-in user - what the cache is filed under. */
10
+ userUid?: string;
11
+ /** The user's stored preferences as the server rendered the page (its `appearance` prop), so the first paint is already themed. Absent when the
12
+ * page wasn't given them (or the user has none). The server caches a page for up to a minute, so this can be older than what this browser last
13
+ * applied: the browser's own copy is used instead when it is newer (see `appearanceCache.ts`). */
14
+ initial?: unknown;
15
+ /** Whether to ask the server for the preferences once mounted (the default), which corrects a page that was cached. The admin and escrow
16
+ * consoles pass `false`: they use what the last webmail page cached in this browser. */
17
+ lookUp?: boolean;
18
+ }
19
+ /**
20
+ * Owns the user's appearance for the whole app: keeps the theme applied to the page and everything Settings > Appearance needs.
21
+ *
22
+ * - **Applies it.** One `<style id="rr-appearance">` in `<head>` (`appearanceCss()`) holds the user's colour tokens and the background
23
+ * layer, and `<html data-theme>` says which scheme is showing - the chosen one, or for "System" the operating system's, live.
24
+ * - **Instantly.** `setPrefs()`, `uploadBackground()`, `removeBackground()` and `reset()` change the page in the same frame; the save
25
+ * happens after (`APPEARANCE_SAVE_DELAY_MS` of quiet, coalesced), sending only what changed - the server merges - and a failed one
26
+ * rolls back to what the server has and sets `error`.
27
+ * - **No flash.** It starts from the server's `initial` preferences, or the browser's newer cached copy (which the inline boot script has
28
+ * already put in place before anything painted), and then asks the server what is true now.
29
+ * - **Stays current.** Another tab or device's change arrives as a push event and is applied unless this tab has changes of its own
30
+ * still in flight.
31
+ */
32
+ export default function AppearanceProvider({ userUid, initial, lookUp, children }: PropsWithChildren<AppearanceProviderProps>): React.JSX.Element;