@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,390 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
7
+ import { DEFAULT_BACKGROUND, appearanceBackgroundUrl, deleteAppearanceBackground, diffAppearance, getAppearance, isDefaultAppearance, normalizeAppearance, parseAppearanceEvent, saveAppearance, uploadAppearanceBackground, } from "@rapidmx/react-shared/appearance/preferencesApi.js";
8
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
9
+ import { getPushClient } from "@rapidmx/react-shared/mail/pushClient.js";
10
+ import { AppearanceContext } from "./appearanceContext.js";
11
+ import { cacheKeyOf, readAppearanceCache, timeOf, writeAppearanceCache } from "./appearanceCache.js";
12
+ import { measureImage, rangeFromLevels } from "./photo.js";
13
+ import { useSystemPrefersDark } from "./resolvedTheme.js";
14
+ import { APPEARANCE_STYLE_ID, appearanceCss } from "./theme.js";
15
+ /** How long a change waits for a further one before it is saved - a slider being dragged is one save, not sixty. */
16
+ export const APPEARANCE_SAVE_DELAY_MS = 400;
17
+ /** The `imageVersion` the preview of a just-chosen file has until the server names the stored one. */
18
+ const LOCAL_VERSION = "local";
19
+ const DEFAULTS = { version: 1, mode: "system" };
20
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
21
+ /** `prefs` with `patch` merged in, normalised (so an out-of-range number or bad colour can't get through). See `AppearancePatch`. */
22
+ export function mergeAppearance(prefs, patch) {
23
+ const next = { version: 1, mode: patch.mode ?? prefs.mode };
24
+ const colors = { ...prefs.colors };
25
+ for (const [key, value] of Object.entries(patch.colors ?? {})) {
26
+ if (value === null || value === undefined) {
27
+ delete colors[key];
28
+ }
29
+ else {
30
+ colors[key] = value;
31
+ }
32
+ }
33
+ if (Object.keys(colors).length > 0) {
34
+ next.colors = colors;
35
+ }
36
+ if (patch.background !== null) {
37
+ const background = patch.background ? { ...(prefs.background ?? DEFAULT_BACKGROUND), ...patch.background } : prefs.background;
38
+ if (background) {
39
+ next.background = background;
40
+ }
41
+ }
42
+ if (prefs.updatedAt !== undefined) {
43
+ next.updatedAt = prefs.updatedAt;
44
+ }
45
+ return normalizeAppearance(next);
46
+ }
47
+ function toInput(prefs) {
48
+ const { updatedAt: _updatedAt, ...input } = prefs;
49
+ return input;
50
+ }
51
+ /** Whether two preference objects say the same thing, ignoring the server's timestamp. */
52
+ function sameContent(a, b) {
53
+ return JSON.stringify(a && toInput(a)) === JSON.stringify(b && toInput(b));
54
+ }
55
+ function messageOf(err, fallback) {
56
+ return err instanceof ApiRequestError && err.message ? err.message : fallback;
57
+ }
58
+ /**
59
+ * Owns the user's appearance for the whole app: keeps the theme applied to the page and everything Settings > Appearance needs.
60
+ *
61
+ * - **Applies it.** One `<style id="rr-appearance">` in `<head>` (`appearanceCss()`) holds the user's colour tokens and the background
62
+ * layer, and `<html data-theme>` says which scheme is showing - the chosen one, or for "System" the operating system's, live.
63
+ * - **Instantly.** `setPrefs()`, `uploadBackground()`, `removeBackground()` and `reset()` change the page in the same frame; the save
64
+ * happens after (`APPEARANCE_SAVE_DELAY_MS` of quiet, coalesced), sending only what changed - the server merges - and a failed one
65
+ * rolls back to what the server has and sets `error`.
66
+ * - **No flash.** It starts from the server's `initial` preferences, or the browser's newer cached copy (which the inline boot script has
67
+ * already put in place before anything painted), and then asks the server what is true now.
68
+ * - **Stays current.** Another tab or device's change arrives as a push event and is applied unless this tab has changes of its own
69
+ * still in flight.
70
+ */
71
+ export default function AppearanceProvider({ userUid, initial, lookUp = true, children }) {
72
+ const initialPrefs = useMemo(() => normalizeAppearance(initial), [initial]);
73
+ const [prefs, setPrefsState] = useState(initialPrefs);
74
+ // The server (or the user) has spoken: until then the page shows whatever the boot script applied from the cache.
75
+ const [authoritative, setAuthoritative] = useState(initialPrefs !== undefined);
76
+ const [local, setLocal] = useState(undefined);
77
+ const [measured, setMeasured] = useState(() => {
78
+ // Read here, not in an effect, so the first stylesheet the provider writes already has the measured panel opacity the boot script used.
79
+ const cache = typeof window === "undefined" ? undefined : readAppearanceCache();
80
+ const range = rangeFromLevels(cache?.measured?.levels);
81
+ return cache?.measured && range ? { version: cache.measured.version, range } : undefined;
82
+ });
83
+ const [saving, setSaving] = useState(false);
84
+ const [error, setError] = useState(null);
85
+ const systemDark = useSystemPrefersDark();
86
+ const prefsRef = useRef(prefs);
87
+ prefsRef.current = prefs;
88
+ /** What the server last acknowledged - what a failed save rolls back to, and what a save is a difference from. */
89
+ const serverRef = useRef(initialPrefs);
90
+ /** When this tab last changed the preferences itself (milliseconds), for the cache's time - see `appearanceCache.ts`. */
91
+ const changedAtRef = useRef(0);
92
+ const pendingRef = useRef(undefined);
93
+ const timerRef = useRef(undefined);
94
+ const flightRef = useRef(undefined);
95
+ const localRef = useRef(local);
96
+ localRef.current = local;
97
+ const resolved = (prefs?.mode ?? "system") === "system" ? (systemDark ? "dark" : "light") : prefs?.mode;
98
+ const apply = useCallback((next) => {
99
+ prefsRef.current = next;
100
+ setPrefsState(next);
101
+ setAuthoritative(true);
102
+ }, []);
103
+ /** `apply()` for a change the user made here: it is newer than anything the server has said so far. */
104
+ const applyLocal = useCallback((next) => {
105
+ changedAtRef.current = Date.now();
106
+ apply(next);
107
+ }, [apply]);
108
+ /** Puts the server's last acknowledged preferences back and says why. */
109
+ const rollback = useCallback((err, fallback) => {
110
+ pendingRef.current = undefined;
111
+ apply(serverRef.current);
112
+ setError(messageOf(err, fallback));
113
+ }, [apply]);
114
+ /** Saves whatever is pending, and whatever arrives while that is in flight, one request at a time. */
115
+ const flush = useCallback(() => {
116
+ clearTimeout(timerRef.current);
117
+ timerRef.current = undefined;
118
+ if (flightRef.current) {
119
+ return flightRef.current;
120
+ }
121
+ if (!pendingRef.current) {
122
+ return Promise.resolve();
123
+ }
124
+ const flight = (async () => {
125
+ try {
126
+ while (pendingRef.current) {
127
+ const toSave = pendingRef.current;
128
+ pendingRef.current = undefined;
129
+ // Only what differs from what the server has: it merges, and refuses a body that names the image or a kind it can't yet show.
130
+ const update = diffAppearance(serverRef.current, toInput(toSave));
131
+ if (!update) {
132
+ continue;
133
+ }
134
+ const stored = await saveAppearance(update);
135
+ serverRef.current = stored;
136
+ if (!pendingRef.current && prefsRef.current) {
137
+ // Nothing newer waiting: take the server's timestamp (the values are the ones sent).
138
+ apply({ ...prefsRef.current, updatedAt: stored.updatedAt });
139
+ }
140
+ }
141
+ }
142
+ catch (err) {
143
+ rollback(err, "Your appearance could not be saved. It has been put back.");
144
+ }
145
+ finally {
146
+ flightRef.current = undefined;
147
+ setSaving(pendingRef.current !== undefined);
148
+ }
149
+ })();
150
+ flightRef.current = flight;
151
+ return flight;
152
+ }, [apply, rollback]);
153
+ /** Queues `next` to be saved after a quiet moment. */
154
+ const scheduleSave = useCallback((next) => {
155
+ pendingRef.current = next;
156
+ setSaving(true);
157
+ clearTimeout(timerRef.current);
158
+ timerRef.current = setTimeout(() => void flush(), APPEARANCE_SAVE_DELAY_MS);
159
+ }, [flush]);
160
+ // A change still waiting when the provider goes away (the page is closing) is sent now rather than lost.
161
+ useEffect(() => {
162
+ function flushWhenHidden() {
163
+ if (document.visibilityState === "hidden") {
164
+ void flush();
165
+ }
166
+ }
167
+ document.addEventListener("visibilitychange", flushWhenHidden);
168
+ return () => {
169
+ document.removeEventListener("visibilitychange", flushWhenHidden);
170
+ void flush();
171
+ };
172
+ }, [flush]);
173
+ const setPrefs = useCallback((patch) => {
174
+ setError(null);
175
+ const next = mergeAppearance(prefsRef.current ?? DEFAULTS, patch);
176
+ applyLocal(next);
177
+ scheduleSave(next);
178
+ }, [applyLocal, scheduleSave]);
179
+ const releaseLocal = useCallback(() => {
180
+ if (localRef.current) {
181
+ URL.revokeObjectURL(localRef.current);
182
+ localRef.current = undefined;
183
+ }
184
+ setLocal(undefined);
185
+ }, []);
186
+ const uploadBackground = useCallback(async (file) => {
187
+ setError(null);
188
+ releaseLocal();
189
+ const url = URL.createObjectURL(file);
190
+ localRef.current = url;
191
+ setLocal(url);
192
+ applyLocal(mergeAppearance(prefsRef.current ?? DEFAULTS, { background: { kind: "image", imageVersion: LOCAL_VERSION } }));
193
+ try {
194
+ // Whatever was changed a moment ago is stored first, so its (older) save can't land after the picture.
195
+ await flush();
196
+ const stored = await uploadAppearanceBackground(file);
197
+ if (stored.background?.imageVersion === undefined) {
198
+ throw new ApiRequestError("The server did not keep the image.", 502);
199
+ }
200
+ serverRef.current = stored;
201
+ const version = String(stored.background.imageVersion);
202
+ const merged = mergeAppearance(prefsRef.current ?? DEFAULTS, {
203
+ background: { kind: "image", imageVersion: stored.background.imageVersion },
204
+ });
205
+ apply({ ...merged, updatedAt: stored.updatedAt });
206
+ if (!sameContent(merged, stored)) {
207
+ // The user moved a slider while the image was uploading: the server has the older values.
208
+ scheduleSave(merged);
209
+ }
210
+ // The local preview stays until the stored image has loaded, so swapping to it doesn't blink.
211
+ const preload = new Image();
212
+ const swap = () => {
213
+ setMeasured((current) => (current?.version === LOCAL_VERSION ? { ...current, version } : current));
214
+ if (localRef.current === url) {
215
+ releaseLocal();
216
+ }
217
+ };
218
+ preload.onload = swap;
219
+ preload.onerror = swap;
220
+ preload.src = appearanceBackgroundUrl(version);
221
+ }
222
+ catch (err) {
223
+ releaseLocal();
224
+ rollback(err, "That image could not be uploaded.");
225
+ }
226
+ }, [apply, applyLocal, flush, releaseLocal, rollback, scheduleSave]);
227
+ const removeBackground = useCallback(async () => {
228
+ setError(null);
229
+ const next = mergeAppearance(prefsRef.current ?? DEFAULTS, { background: { kind: "none", imageVersion: undefined } });
230
+ applyLocal(next);
231
+ releaseLocal();
232
+ try {
233
+ await flush();
234
+ serverRef.current = (await deleteAppearanceBackground()) ?? serverRef.current;
235
+ scheduleSave(next);
236
+ }
237
+ catch (err) {
238
+ rollback(err, "The background could not be removed.");
239
+ }
240
+ }, [applyLocal, flush, releaseLocal, rollback, scheduleSave]);
241
+ const reset = useCallback(async () => {
242
+ setError(null);
243
+ const hadImage = prefsRef.current?.background?.imageVersion !== undefined;
244
+ applyLocal(DEFAULTS);
245
+ releaseLocal();
246
+ pendingRef.current = undefined;
247
+ try {
248
+ await flush();
249
+ if (hadImage) {
250
+ serverRef.current = (await deleteAppearanceBackground()) ?? serverRef.current;
251
+ }
252
+ const update = diffAppearance(serverRef.current, toInput(DEFAULTS));
253
+ if (update) {
254
+ serverRef.current = await saveAppearance(update);
255
+ }
256
+ }
257
+ catch (err) {
258
+ rollback(err, "Your appearance could not be reset.");
259
+ }
260
+ }, [applyLocal, flush, releaseLocal, rollback]);
261
+ // The page's copy can be a minute old (the server caches a rendered page), and this browser may have changed the preferences since: the
262
+ // browser's own copy, when it is newer, is what to show - before the first paint, so nothing flips.
263
+ useIsomorphicLayoutEffect(() => {
264
+ if (initialPrefs === undefined) {
265
+ return;
266
+ }
267
+ const cache = readAppearanceCache();
268
+ if (cache && (!cache.uid || cache.uid === userUid) && cache.t > timeOf(initialPrefs.updatedAt)) {
269
+ apply(cache.prefs);
270
+ }
271
+ // Once, for the page as it was rendered.
272
+ }, []);
273
+ // Without the page's copy, the cache is what to show until the server answers; then the server's answer is, unless the user changed something
274
+ // meanwhile. With it, the server is asked too, to correct a page that was cached.
275
+ useEffect(() => {
276
+ if (!userUid) {
277
+ return;
278
+ }
279
+ if (initialPrefs === undefined) {
280
+ const cache = readAppearanceCache();
281
+ if (cache && (!cache.uid || cache.uid === userUid)) {
282
+ setPrefsState((current) => current ?? cache.prefs);
283
+ }
284
+ }
285
+ if (!lookUp) {
286
+ return;
287
+ }
288
+ let cancelled = false;
289
+ getAppearance().then((stored) => {
290
+ if (cancelled || pendingRef.current || flightRef.current || timerRef.current) {
291
+ return;
292
+ }
293
+ serverRef.current = stored;
294
+ if (!sameContent(stored, prefsRef.current)) {
295
+ apply(stored);
296
+ }
297
+ }, () => undefined);
298
+ return () => {
299
+ cancelled = true;
300
+ };
301
+ }, [initialPrefs, userUid, lookUp, apply]);
302
+ // Another tab or device changed it.
303
+ useEffect(() => {
304
+ if (!userUid) {
305
+ return;
306
+ }
307
+ return getPushClient().onEvent((event) => {
308
+ const pushed = parseAppearanceEvent(event);
309
+ if (pushed === undefined) {
310
+ return;
311
+ }
312
+ // Our own change still travelling (or its echo, which says what is already on screen) must not undo what is on screen.
313
+ if (pendingRef.current || flightRef.current || timerRef.current || sameContent(pushed ?? undefined, prefsRef.current)) {
314
+ return;
315
+ }
316
+ serverRef.current = pushed ?? undefined;
317
+ apply(pushed ?? undefined);
318
+ });
319
+ }, [userUid, apply]);
320
+ const version = prefs?.background?.imageVersion;
321
+ const imageUrl = local ?? (version === undefined || version === LOCAL_VERSION ? undefined : appearanceBackgroundUrl(version));
322
+ const measureKey = local ? LOCAL_VERSION : version === undefined ? undefined : String(version);
323
+ const isImage = prefs?.background?.kind === "image";
324
+ // Measure the picture (once per image) so the panels over it are only as opaque as it needs.
325
+ useEffect(() => {
326
+ if (!isImage || !imageUrl || !measureKey || measured?.version === measureKey) {
327
+ return;
328
+ }
329
+ let cancelled = false;
330
+ void measureImage(imageUrl).then((range) => {
331
+ if (!cancelled && range) {
332
+ setMeasured({ version: measureKey, range });
333
+ }
334
+ });
335
+ return () => {
336
+ cancelled = true;
337
+ };
338
+ }, [isImage, imageUrl, measureKey, measured?.version]);
339
+ const range = measured && measured.version === measureKey ? measured.range : undefined;
340
+ // Apply: the stylesheet, then the scheme.
341
+ useIsomorphicLayoutEffect(() => {
342
+ if (!authoritative && !prefs) {
343
+ // Nothing known yet: leave what the boot script applied from the cache.
344
+ return;
345
+ }
346
+ const css = appearanceCss(prefs, { measured: range, imageUrl });
347
+ let element = document.getElementById(APPEARANCE_STYLE_ID);
348
+ if (!css) {
349
+ element?.remove();
350
+ }
351
+ else {
352
+ if (!element) {
353
+ element = document.createElement("style");
354
+ element.id = APPEARANCE_STYLE_ID;
355
+ document.head.appendChild(element);
356
+ }
357
+ element.textContent = css;
358
+ element.setAttribute("data-key", cacheKeyOf(prefs));
359
+ }
360
+ // What the next page load starts from: never a blob: preview, and "nothing chosen" is remembered too (an empty stylesheet), so a page
361
+ // cached by the server from before a reset doesn't bring the old background back.
362
+ if (!local) {
363
+ writeAppearanceCache(prefs ?? DEFAULTS, css, Math.max(timeOf(prefs?.updatedAt), changedAtRef.current), userUid, range && measureKey ? { version: measureKey, range } : undefined);
364
+ }
365
+ }, [authoritative, prefs, range, imageUrl, local, userUid, measureKey]);
366
+ useIsomorphicLayoutEffect(() => {
367
+ document.documentElement.setAttribute("data-theme", resolved);
368
+ }, [resolved]);
369
+ // A blob: URL that outlives the provider leaks the picture's memory.
370
+ useEffect(() => () => {
371
+ if (localRef.current) {
372
+ URL.revokeObjectURL(localRef.current);
373
+ }
374
+ }, []);
375
+ const value = useMemo(() => ({
376
+ prefs: prefs ?? DEFAULTS,
377
+ resolved,
378
+ backgroundUrl: imageUrl,
379
+ measured: range,
380
+ isDefault: isDefaultAppearance(prefs),
381
+ saving,
382
+ error,
383
+ clearError: () => setError(null),
384
+ setPrefs,
385
+ uploadBackground,
386
+ removeBackground,
387
+ reset,
388
+ }), [prefs, resolved, imageUrl, range, saving, error, setPrefs, uploadBackground, removeBackground, reset]);
389
+ return _jsx(AppearanceContext.Provider, { value: value, children: children });
390
+ }
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ export interface ColorFieldProps {
3
+ /** Unique in the page: the ids of the two inputs derive from it. */
4
+ id: string;
5
+ label: string;
6
+ /** One line under the label saying what the colour is used for. */
7
+ description: string;
8
+ /** The colour the user chose, `#rrggbb`; `undefined` while they haven't. */
9
+ value: string | undefined;
10
+ /** What shows while unset: the colour the app has now. */
11
+ current: string;
12
+ /** Called with a new `#rrggbb` as the person picks or types one - on every change, so the app follows a drag on the picker. */
13
+ onChange: (hex: string) => void;
14
+ /** Called for "Reset": back to the app's own colour. Absent for a field with nothing to reset to. */
15
+ onReset?: () => void;
16
+ }
17
+ /**
18
+ * A colour to choose: the browser's colour picker beside a hex field (`#rrggbb`, and the short `#rgb` and a missing `#` are understood),
19
+ * and a "Reset" that goes back to the app's own colour. A full `#rrggbb` is applied as it is typed, a short one when the field is left, and
20
+ * something that isn't a colour changes nothing - the field says so and goes back to the current colour when it is left.
21
+ */
22
+ export default function ColorField({ id, label, description, value, current, onChange, onReset }: ColorFieldProps): React.JSX.Element;
@@ -0,0 +1,40 @@
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 { useState } from "react";
7
+ import { isHexColor, normalizeHex } from "./color.js";
8
+ /**
9
+ * A colour to choose: the browser's colour picker beside a hex field (`#rrggbb`, and the short `#rgb` and a missing `#` are understood),
10
+ * and a "Reset" that goes back to the app's own colour. A full `#rrggbb` is applied as it is typed, a short one when the field is left, and
11
+ * something that isn't a colour changes nothing - the field says so and goes back to the current colour when it is left.
12
+ */
13
+ export default function ColorField({ id, label, description, value, current, onChange, onReset }) {
14
+ // What is being typed, while it is: `null` shows the colour itself (after a pick, or once the field is left).
15
+ const [draft, setDraft] = useState(null);
16
+ const shown = value ?? current;
17
+ const invalid = draft !== null && normalizeHex(draft) === undefined;
18
+ return (_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [_jsxs("div", { className: "min-w-0 flex-1 basis-40", children: [_jsx("label", { htmlFor: `${id}-hex`, className: "block text-sm font-semibold text-text", children: label }), _jsx("p", { id: `${id}-description`, className: "text-xs text-text-muted", children: description })] }), _jsx("input", { type: "color", "aria-label": `${label} picker`, value: shown, onChange: (event) => {
19
+ setDraft(null);
20
+ // A colour input only ever holds a `#rrggbb` (the browser sanitises anything else to black).
21
+ onChange(normalizeHex(event.target.value));
22
+ }, className: "h-9 w-12 shrink-0 cursor-pointer rounded-sm border border-border bg-surface p-0.5" }), _jsx("div", { className: "w-28 shrink-0", children: _jsx("input", { id: `${id}-hex`, type: "text", inputMode: "text", autoComplete: "off", spellCheck: false, maxLength: 7, value: draft ?? shown, "aria-invalid": invalid || undefined, "aria-describedby": invalid ? `${id}-error ${id}-description` : `${id}-description`, onChange: (event) => {
23
+ setDraft(event.target.value);
24
+ // Only a complete `#rrggbb` (the hash is optional) is applied while typing: `#1a2` on the way to `#1a2b3c` would flash `#11aa22`.
25
+ const typed = event.target.value.trim();
26
+ if (isHexColor(typed.startsWith("#") ? typed : "#" + typed)) {
27
+ onChange(normalizeHex(typed));
28
+ }
29
+ }, onBlur: () => {
30
+ // Leaving the field applies a short form (`#abc`) that was typed, and drops anything that isn't a colour.
31
+ const hex = draft === null ? undefined : normalizeHex(draft);
32
+ if (hex && hex !== shown) {
33
+ onChange(hex);
34
+ }
35
+ setDraft(null);
36
+ }, className: "w-full rounded-sm border border-border bg-surface px-2 py-1.5 font-mono text-sm text-text" }) }), onReset && (_jsx("button", { type: "button", onClick: () => {
37
+ setDraft(null);
38
+ onReset();
39
+ }, disabled: value === undefined, "aria-label": `Reset ${label.toLowerCase()} to the default`, className: "shrink-0 rounded-sm px-2 py-1.5 text-sm font-semibold text-accent-dark hover:not-disabled:underline disabled:cursor-not-allowed disabled:opacity-50", children: "Reset" })), invalid && (_jsx("p", { id: `${id}-error`, role: "alert", className: "basis-full text-xs text-danger", children: "Use a colour like #1a2b3c." }))] }));
40
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The last appearance this browser applied, kept in `localStorage` so a page load (or a page the server didn't send the preferences
3
+ * to) paints with the user's theme from the first frame instead of flashing the default. It holds the finished stylesheet text, so
4
+ * the tiny inline script in `<head>` (`APPEARANCE_BOOT_SCRIPT`) applies it without any of this app's code having loaded.
5
+ *
6
+ * **Which copy wins.** The server renders a page's preferences into it, but the server caches a rendered page for up to a minute, so
7
+ * that copy can be *older* than what this browser last applied (a change made a few seconds ago). Each copy therefore carries a time -
8
+ * the server's `updatedAt`, or for an edit this browser has made and not yet heard back about, the moment it was made - and the newer
9
+ * one is applied (`t`). After a reset the cache keeps an empty stylesheet ("nothing chosen") rather than being removed, for the same
10
+ * reason: it is newer than the stale page's idea of a background. Every access is in try/catch: storage can be blocked, full or
11
+ * absent (private windows, server-side rendering).
12
+ */
13
+ import { AppearanceMode, AppearancePreferences } from "@rapidmx/react-shared/appearance/preferencesApi.js";
14
+ import type { PhotoRange } from "./theme.js";
15
+ export declare const APPEARANCE_CACHE_KEY = "rapidmx-appearance";
16
+ export interface AppearanceCache {
17
+ v: 1;
18
+ /** Whose it is - so a browser shared by two accounts never shows one the other's background. Absent when the page didn't know. */
19
+ uid?: string;
20
+ /** `cacheKeyOf(prefs)`: what the stylesheet was made from. */
21
+ key: string;
22
+ /** When these preferences were last changed, in milliseconds - see this module's doc comment. */
23
+ t: number;
24
+ mode: AppearanceMode;
25
+ prefs: AppearancePreferences;
26
+ /** The finished stylesheet text; empty when the preferences change nothing. */
27
+ css: string;
28
+ /** The measured lightness of the image, as two gray levels, with the image version they belong to. */
29
+ measured?: {
30
+ version: string;
31
+ levels: [number, number];
32
+ };
33
+ }
34
+ /** What identifies a set of preferences (the server-rendered stylesheet carries the same in `data-key`): their normalised JSON, whose key order is always the same. */
35
+ export declare function cacheKeyOf(prefs: AppearancePreferences): string;
36
+ /** A server `updatedAt` (an ISO date or milliseconds, both opaque to the rest of the client) as milliseconds; `0` for none or one that isn't a date. */
37
+ export declare function timeOf(updatedAt: string | number | undefined): number;
38
+ /** The stored cache, or `undefined` when there is none, it can't be read, or it isn't well formed. */
39
+ export declare function readAppearanceCache(): AppearanceCache | undefined;
40
+ export declare function writeAppearanceCache(prefs: AppearancePreferences, css: string, time: number, uid?: string, measured?: {
41
+ version: string;
42
+ range: PhotoRange;
43
+ }): void;
44
+ /** Forgets the cache - on sign-out, so the next person to sign in on this browser starts from their own. */
45
+ export declare function clearAppearanceCache(): void;
@@ -0,0 +1,80 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * The last appearance this browser applied, kept in `localStorage` so a page load (or a page the server didn't send the preferences
7
+ * to) paints with the user's theme from the first frame instead of flashing the default. It holds the finished stylesheet text, so
8
+ * the tiny inline script in `<head>` (`APPEARANCE_BOOT_SCRIPT`) applies it without any of this app's code having loaded.
9
+ *
10
+ * **Which copy wins.** The server renders a page's preferences into it, but the server caches a rendered page for up to a minute, so
11
+ * that copy can be *older* than what this browser last applied (a change made a few seconds ago). Each copy therefore carries a time -
12
+ * the server's `updatedAt`, or for an edit this browser has made and not yet heard back about, the moment it was made - and the newer
13
+ * one is applied (`t`). After a reset the cache keeps an empty stylesheet ("nothing chosen") rather than being removed, for the same
14
+ * reason: it is newer than the stale page's idea of a background. Every access is in try/catch: storage can be blocked, full or
15
+ * absent (private windows, server-side rendering).
16
+ */
17
+ import { normalizeAppearance } from "@rapidmx/react-shared/appearance/preferencesApi.js";
18
+ import { rangeFromLevels, rangeToLevels } from "./photo.js";
19
+ export const APPEARANCE_CACHE_KEY = "rapidmx-appearance";
20
+ /** What identifies a set of preferences (the server-rendered stylesheet carries the same in `data-key`): their normalised JSON, whose key order is always the same. */
21
+ export function cacheKeyOf(prefs) {
22
+ return JSON.stringify(normalizeAppearance(prefs) ?? prefs);
23
+ }
24
+ /** A server `updatedAt` (an ISO date or milliseconds, both opaque to the rest of the client) as milliseconds; `0` for none or one that isn't a date. */
25
+ export function timeOf(updatedAt) {
26
+ const time = typeof updatedAt === "number" ? updatedAt : updatedAt === undefined ? 0 : Date.parse(updatedAt);
27
+ return Number.isFinite(time) ? time : 0;
28
+ }
29
+ /** The stored cache, or `undefined` when there is none, it can't be read, or it isn't well formed. */
30
+ export function readAppearanceCache() {
31
+ try {
32
+ const raw = JSON.parse(localStorage.getItem(APPEARANCE_CACHE_KEY) ?? "null");
33
+ const prefs = normalizeAppearance(raw?.prefs);
34
+ if (raw?.v !== 1 || !prefs || typeof raw.css !== "string") {
35
+ return undefined;
36
+ }
37
+ const cache = {
38
+ v: 1,
39
+ key: cacheKeyOf(prefs),
40
+ t: typeof raw.t === "number" && Number.isFinite(raw.t) ? raw.t : 0,
41
+ mode: prefs.mode,
42
+ prefs,
43
+ css: raw.css,
44
+ };
45
+ if (typeof raw.uid === "string") {
46
+ cache.uid = raw.uid;
47
+ }
48
+ if (typeof raw.measured?.version === "string" && rangeFromLevels(raw.measured.levels)) {
49
+ cache.measured = { version: raw.measured.version, levels: raw.measured.levels };
50
+ }
51
+ return cache;
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ export function writeAppearanceCache(prefs, css, time, uid, measured) {
58
+ try {
59
+ const cache = { v: 1, key: cacheKeyOf(prefs), t: time, mode: prefs.mode, prefs, css };
60
+ if (uid) {
61
+ cache.uid = uid;
62
+ }
63
+ if (measured) {
64
+ cache.measured = { version: measured.version, levels: rangeToLevels(measured.range) };
65
+ }
66
+ localStorage.setItem(APPEARANCE_CACHE_KEY, JSON.stringify(cache));
67
+ }
68
+ catch {
69
+ // Storage is unavailable or full: the theme still applies, it just isn't remembered.
70
+ }
71
+ }
72
+ /** Forgets the cache - on sign-out, so the next person to sign in on this browser starts from their own. */
73
+ export function clearAppearanceCache() {
74
+ try {
75
+ localStorage.removeItem(APPEARANCE_CACHE_KEY);
76
+ }
77
+ catch {
78
+ // Nothing to forget.
79
+ }
80
+ }
@@ -0,0 +1,45 @@
1
+ import type { AppearanceBackground, AppearanceMode, AppearancePreferences, ColorKey } from "@rapidmx/react-shared/appearance/preferencesApi.js";
2
+ import type { ResolvedTheme } from "./resolvedTheme.js";
3
+ import type { PhotoRange } from "./theme.js";
4
+ /**
5
+ * A change to the preferences, merged into what is there: `mode` replaces; each `colors` entry replaces that colour, and `null` clears
6
+ * it (back to the app's own); `background` merges into the current one (or a fresh one); `background: null` removes it.
7
+ */
8
+ export interface AppearancePatch {
9
+ mode?: AppearanceMode;
10
+ colors?: Partial<Record<ColorKey, string | null>>;
11
+ background?: Partial<AppearanceBackground> | null;
12
+ }
13
+ export interface AppearanceApi {
14
+ /** What is showing now - the user's choices including one still being saved. Never `undefined`: a user who chose nothing has the defaults. */
15
+ prefs: AppearancePreferences;
16
+ /** The scheme showing: the chosen mode, or for "System" the operating system's. */
17
+ resolved: ResolvedTheme;
18
+ /** Where the background image is loaded from - the stored image's URL, or a local preview while it uploads. */
19
+ backgroundUrl: string | undefined;
20
+ /** The background image's measured lightness, once known (until then the panels assume the worst case). */
21
+ measured: PhotoRange | undefined;
22
+ /** Nothing customised - `Reset all` has nothing to do. */
23
+ isDefault: boolean;
24
+ /** A save is in flight or waiting for the next quiet moment. */
25
+ saving: boolean;
26
+ /** Why the last save failed, in words for the user; `null` after a success or `clearError()`. The change it belonged to was rolled back. */
27
+ error: string | null;
28
+ clearError: () => void;
29
+ /** Applies the change at once and saves it in the background; a failed save rolls back to what the server has and sets `error`. */
30
+ setPrefs: (patch: AppearancePatch) => void;
31
+ /** Shows `file` as the background at once (already checked with `validateBackgroundFile()`) while it uploads; rolls back on failure. */
32
+ uploadBackground: (file: Blob) => Promise<void>;
33
+ /** Removes the background image (and returns to no background). */
34
+ removeBackground: () => Promise<void>;
35
+ /** Back to the defaults: system scheme, the app's colours, no background. */
36
+ reset: () => Promise<void>;
37
+ }
38
+ /** What `useAppearance()` returns outside an `AppearanceProvider` (a component tested alone): the defaults, and changes go nowhere. */
39
+ export declare const INERT_APPEARANCE: AppearanceApi;
40
+ export declare const AppearanceContext: import("react").Context<AppearanceApi>;
41
+ /**
42
+ * The user's appearance preferences and how to change them: `{ prefs, setPrefs, ... }` (see `AppearanceApi`). Changes apply to the
43
+ * whole app the instant they are made and are saved in the background. Outside an `AppearanceProvider` it is inert.
44
+ */
45
+ export declare function useAppearance(): AppearanceApi;
@@ -0,0 +1,29 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { createContext, useContext } from "react";
6
+ const NOOP_PREFS = { version: 1, mode: "system" };
7
+ /** What `useAppearance()` returns outside an `AppearanceProvider` (a component tested alone): the defaults, and changes go nowhere. */
8
+ export const INERT_APPEARANCE = {
9
+ prefs: NOOP_PREFS,
10
+ resolved: "light",
11
+ backgroundUrl: undefined,
12
+ measured: undefined,
13
+ isDefault: true,
14
+ saving: false,
15
+ error: null,
16
+ clearError: () => undefined,
17
+ setPrefs: () => undefined,
18
+ uploadBackground: () => Promise.resolve(),
19
+ removeBackground: () => Promise.resolve(),
20
+ reset: () => Promise.resolve(),
21
+ };
22
+ export const AppearanceContext = createContext(INERT_APPEARANCE);
23
+ /**
24
+ * The user's appearance preferences and how to change them: `{ prefs, setPrefs, ... }` (see `AppearanceApi`). Changes apply to the
25
+ * whole app the instant they are made and are saved in the background. Outside an `AppearanceProvider` it is inert.
26
+ */
27
+ export function useAppearance() {
28
+ return useContext(AppearanceContext);
29
+ }