@rapidmx/web-client 0.9.0 → 0.11.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 (443) hide show
  1. package/README.md +373 -88
  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/quarantine/index.tsx +2 -1
  7. package/apps/admin/signing-certificates/index.tsx +343 -0
  8. package/apps/escrow/_layout.tsx +8 -2
  9. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  10. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  11. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  12. package/apps/shared/appearance/ColorField.tsx +107 -0
  13. package/apps/shared/appearance/appearanceCache.ts +106 -0
  14. package/apps/shared/appearance/appearanceContext.ts +77 -0
  15. package/apps/shared/appearance/bootScript.ts +35 -0
  16. package/apps/shared/appearance/color.ts +169 -0
  17. package/apps/shared/appearance/photo.ts +88 -0
  18. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  19. package/apps/shared/appearance/theme.ts +355 -0
  20. package/apps/shared/auth/accountUrl.ts +9 -0
  21. package/apps/shared/auth/adminAccess.ts +99 -0
  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 +2 -1
  26. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  27. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  28. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  29. package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
  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 +98 -115
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +25 -8
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +477 -288
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
  38. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -164
  39. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  40. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  41. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  42. package/apps/shared/components/layout/UserMenu.tsx +372 -185
  43. package/apps/shared/components/mail/ConversationList.tsx +271 -262
  44. package/apps/shared/components/mail/ConversationThreadPane.tsx +147 -94
  45. package/apps/shared/components/mail/LazyReadingPane.tsx +113 -0
  46. package/apps/shared/components/mail/MailSelectionBar.tsx +227 -222
  47. package/apps/shared/components/mail/MessageDetailPane.tsx +1537 -1387
  48. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  49. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  50. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -160
  51. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
  52. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1644 -1736
  53. package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
  54. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
  55. package/apps/shared/components/mail/compose/composePerf.ts +46 -0
  56. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  57. package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
  58. package/apps/shared/components/mail/layout/MailShell.tsx +490 -472
  59. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  60. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  61. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  62. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  63. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  64. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  65. package/apps/shared/components/mail/reading/color.ts +245 -0
  66. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  67. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  68. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  69. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  70. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  71. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  72. package/apps/shared/components/mail/unreadStyle.tsx +75 -0
  73. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  74. package/apps/shared/components/settings/layout/SettingsShell.tsx +253 -240
  75. package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
  76. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  77. package/apps/shared/components/tasks/layout/TasksShell.tsx +24 -8
  78. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
  79. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
  80. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
  81. package/apps/shared/keyboard/dispatch.ts +124 -0
  82. package/apps/shared/keyboard/format.ts +89 -0
  83. package/apps/shared/keyboard/keymap.ts +114 -0
  84. package/apps/shared/keyboard/match.ts +44 -0
  85. package/apps/shared/keyboard/parse.ts +136 -0
  86. package/apps/shared/keyboard/platform.ts +34 -0
  87. package/apps/shared/keyboard/registry.ts +65 -0
  88. package/apps/shared/keyboard/targets.ts +79 -0
  89. package/apps/shared/keyboard/useShortcut.ts +50 -0
  90. package/apps/shared/keyboard/useShortcutProps.ts +17 -0
  91. package/apps/shared/mail/folderCounts.ts +308 -0
  92. package/apps/shared/mail/folderTree.ts +143 -0
  93. package/apps/shared/mail/listSnapshots.ts +87 -0
  94. package/apps/shared/mail/messageReadState.ts +85 -0
  95. package/apps/shared/mail/newMailNotifications.ts +183 -0
  96. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  97. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  98. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  99. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  100. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  103. package/apps/shared/mail/outbox/sendState.ts +35 -0
  104. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  105. package/apps/shared/mail/useMailConnection.ts +203 -0
  106. package/apps/shared/mail/useMailLiveUpdates.ts +277 -224
  107. package/apps/shared/mail/useMarkMessageRead.ts +47 -0
  108. package/apps/shared/mail/useNewMailNotifications.ts +178 -0
  109. package/apps/shared/mail/useUnreadTitle.ts +42 -0
  110. package/apps/shared/navigation/AppRouter.tsx +300 -0
  111. package/apps/shared/navigation/appHrefs.ts +23 -0
  112. package/apps/shared/navigation/frameContext.tsx +35 -0
  113. package/apps/shared/navigation/idle.ts +45 -0
  114. package/apps/shared/navigation/routerContext.tsx +83 -0
  115. package/apps/shared/navigation/routes.ts +72 -0
  116. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  117. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  118. package/apps/shared/notifications/apiErrors.ts +98 -0
  119. package/apps/shared/notifications/headerOffset.ts +35 -0
  120. package/apps/shared/notifications/pushStatus.ts +56 -0
  121. package/apps/shared/notifications/store.ts +549 -0
  122. package/apps/shared/notifications/systemErrors.ts +63 -0
  123. package/apps/shared/notifications/useNotifications.ts +41 -0
  124. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
  125. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  126. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  127. package/apps/shared/signing/enrollmentView.ts +251 -0
  128. package/apps/shared/signing/signingInfo.ts +58 -0
  129. package/apps/shared/signing/useNow.ts +19 -0
  130. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  131. package/apps/shared/styles/app.css +108 -10
  132. package/apps/www/_layout.tsx +8 -2
  133. package/apps/www/_routedPage.tsx +24 -0
  134. package/apps/www/_routes.ts +36 -0
  135. package/apps/www/calendar/index.tsx +64 -33
  136. package/apps/www/contacts/[uid].tsx +109 -107
  137. package/apps/www/contacts/index.tsx +62 -42
  138. package/apps/www/index.tsx +2291 -1917
  139. package/apps/www/messages/[uid].tsx +108 -101
  140. package/apps/www/settings/appearance/index.tsx +21 -0
  141. package/apps/www/settings/auto-reply/index.tsx +7 -8
  142. package/apps/www/settings/encryption/index.tsx +1287 -1249
  143. package/apps/www/settings/filters/[uid].tsx +7 -2
  144. package/apps/www/settings/filters/index.tsx +102 -99
  145. package/apps/www/settings/filters/new/index.tsx +140 -133
  146. package/apps/www/settings/labels/index.tsx +204 -201
  147. package/apps/www/settings/privacy/index.tsx +7 -9
  148. package/apps/www/settings/read-receipts/index.tsx +7 -8
  149. package/apps/www/settings/sharing/index.tsx +256 -274
  150. package/apps/www/settings/signatures/[uid].tsx +172 -167
  151. package/apps/www/settings/signatures/index.tsx +88 -85
  152. package/apps/www/settings/signatures/new/index.tsx +135 -129
  153. package/apps/www/tasks/index.tsx +29 -18
  154. package/dist/apps/admin/_layout.d.ts +5 -1
  155. package/dist/apps/admin/_layout.js +3 -2
  156. package/dist/apps/admin/index.js +3 -2
  157. package/dist/apps/admin/ingest-queue/index.js +2 -1
  158. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  159. package/dist/apps/admin/quarantine/index.js +2 -1
  160. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  161. package/dist/apps/admin/signing-certificates/index.js +161 -0
  162. package/dist/apps/escrow/_layout.d.ts +5 -1
  163. package/dist/apps/escrow/_layout.js +3 -2
  164. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  165. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  166. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  167. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  168. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  169. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  170. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  171. package/dist/apps/shared/appearance/ColorField.js +40 -0
  172. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  173. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  174. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  175. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  176. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  177. package/dist/apps/shared/appearance/bootScript.js +33 -0
  178. package/dist/apps/shared/appearance/color.d.ts +65 -0
  179. package/dist/apps/shared/appearance/color.js +132 -0
  180. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  181. package/dist/apps/shared/appearance/photo.js +79 -0
  182. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  183. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  184. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  185. package/dist/apps/shared/appearance/theme.js +251 -0
  186. package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
  187. package/dist/apps/shared/auth/accountUrl.js +8 -0
  188. package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
  189. package/dist/apps/shared/auth/adminAccess.js +89 -0
  190. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  191. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  192. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  193. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  194. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  195. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  196. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +1 -1
  197. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  198. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  199. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  200. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
  201. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
  202. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  203. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  204. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +6 -2
  205. package/dist/apps/shared/components/contacts/ContactsToolbar.js +28 -10
  206. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
  207. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +19 -6
  208. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  209. package/dist/apps/shared/components/layout/AppShell.d.ts +34 -4
  210. package/dist/apps/shared/components/layout/AppShell.js +122 -17
  211. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  212. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  213. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +3 -1
  215. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  216. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  217. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  218. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  219. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  220. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  221. package/dist/apps/shared/components/layout/UserMenu.d.ts +25 -3
  222. package/dist/apps/shared/components/layout/UserMenu.js +88 -7
  223. package/dist/apps/shared/components/mail/ConversationList.js +5 -11
  224. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +6 -2
  225. package/dist/apps/shared/components/mail/ConversationThreadPane.js +87 -40
  226. package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
  227. package/dist/apps/shared/components/mail/LazyReadingPane.js +83 -0
  228. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -4
  229. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
  230. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +25 -8
  231. package/dist/apps/shared/components/mail/MessageDetailPane.js +167 -73
  232. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  233. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  234. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  235. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  236. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +38 -0
  237. package/dist/apps/shared/components/mail/compose/ComposeContext.js +97 -6
  238. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
  239. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +3 -2
  240. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +316 -339
  241. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
  242. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
  243. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
  244. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
  245. package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
  246. package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
  247. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  248. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  249. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
  250. package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
  251. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +14 -4
  252. package/dist/apps/shared/components/mail/layout/MailShell.js +90 -104
  253. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  254. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  255. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  256. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  257. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  258. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  259. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  260. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  261. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  262. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  263. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  264. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  265. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  266. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  267. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  268. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  269. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  270. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  271. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  272. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  273. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  274. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  275. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  276. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  277. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  278. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  279. package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
  280. package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
  281. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  282. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  283. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
  284. package/dist/apps/shared/components/settings/layout/SettingsShell.js +16 -5
  285. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
  286. package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
  287. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  288. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
  289. package/dist/apps/shared/components/tasks/layout/TasksShell.js +18 -6
  290. package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
  291. package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
  292. package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
  293. package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
  294. package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
  295. package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
  296. package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
  297. package/dist/apps/shared/keyboard/dispatch.js +109 -0
  298. package/dist/apps/shared/keyboard/format.d.ts +12 -0
  299. package/dist/apps/shared/keyboard/format.js +74 -0
  300. package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
  301. package/dist/apps/shared/keyboard/keymap.js +84 -0
  302. package/dist/apps/shared/keyboard/match.d.ts +20 -0
  303. package/dist/apps/shared/keyboard/match.js +31 -0
  304. package/dist/apps/shared/keyboard/parse.d.ts +31 -0
  305. package/dist/apps/shared/keyboard/parse.js +107 -0
  306. package/dist/apps/shared/keyboard/platform.d.ts +15 -0
  307. package/dist/apps/shared/keyboard/platform.js +22 -0
  308. package/dist/apps/shared/keyboard/registry.d.ts +39 -0
  309. package/dist/apps/shared/keyboard/registry.js +33 -0
  310. package/dist/apps/shared/keyboard/targets.d.ts +14 -0
  311. package/dist/apps/shared/keyboard/targets.js +67 -0
  312. package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
  313. package/dist/apps/shared/keyboard/useShortcut.js +35 -0
  314. package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
  315. package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
  316. package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
  317. package/dist/apps/shared/mail/folderCounts.js +218 -0
  318. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  319. package/dist/apps/shared/mail/folderTree.js +116 -0
  320. package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
  321. package/dist/apps/shared/mail/listSnapshots.js +43 -0
  322. package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
  323. package/dist/apps/shared/mail/messageReadState.js +63 -0
  324. package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
  325. package/dist/apps/shared/mail/newMailNotifications.js +138 -0
  326. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  327. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  328. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  329. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  330. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  331. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  332. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  333. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  334. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  335. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  336. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  337. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  338. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  339. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  340. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  341. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  342. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  343. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  344. package/dist/apps/shared/mail/useMailConnection.d.ts +56 -0
  345. package/dist/apps/shared/mail/useMailConnection.js +138 -0
  346. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +29 -9
  347. package/dist/apps/shared/mail/useMailLiveUpdates.js +60 -32
  348. package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
  349. package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
  350. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +35 -0
  351. package/dist/apps/shared/mail/useNewMailNotifications.js +130 -0
  352. package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
  353. package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
  354. package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
  355. package/dist/apps/shared/navigation/AppRouter.js +242 -0
  356. package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
  357. package/dist/apps/shared/navigation/appHrefs.js +20 -0
  358. package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
  359. package/dist/apps/shared/navigation/frameContext.js +24 -0
  360. package/dist/apps/shared/navigation/idle.d.ts +14 -0
  361. package/dist/apps/shared/navigation/idle.js +43 -0
  362. package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
  363. package/dist/apps/shared/navigation/routerContext.js +56 -0
  364. package/dist/apps/shared/navigation/routes.d.ts +32 -0
  365. package/dist/apps/shared/navigation/routes.js +37 -0
  366. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  367. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  368. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  369. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  370. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  371. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  372. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  373. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  374. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  375. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  376. package/dist/apps/shared/notifications/store.d.ts +136 -0
  377. package/dist/apps/shared/notifications/store.js +416 -0
  378. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  379. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  380. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  381. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  382. package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
  383. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  384. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  385. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  386. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  387. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  388. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  389. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  390. package/dist/apps/shared/signing/signingInfo.js +48 -0
  391. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  392. package/dist/apps/shared/signing/useNow.js +18 -0
  393. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  394. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  395. package/dist/apps/shared/styles/app.css +108 -10
  396. package/dist/apps/www/_layout.d.ts +5 -1
  397. package/dist/apps/www/_layout.js +3 -2
  398. package/dist/apps/www/_routedPage.d.ts +12 -0
  399. package/dist/apps/www/_routedPage.js +19 -0
  400. package/dist/apps/www/_routes.d.ts +11 -0
  401. package/dist/apps/www/_routes.js +30 -0
  402. package/dist/apps/www/calendar/index.d.ts +2 -2
  403. package/dist/apps/www/calendar/index.js +41 -11
  404. package/dist/apps/www/contacts/[uid].d.ts +3 -9
  405. package/dist/apps/www/contacts/[uid].js +11 -9
  406. package/dist/apps/www/contacts/index.d.ts +2 -2
  407. package/dist/apps/www/contacts/index.js +30 -24
  408. package/dist/apps/www/index.d.ts +2 -2
  409. package/dist/apps/www/index.js +332 -48
  410. package/dist/apps/www/messages/[uid].d.ts +3 -7
  411. package/dist/apps/www/messages/[uid].js +10 -5
  412. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  413. package/dist/apps/www/settings/appearance/index.js +13 -0
  414. package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
  415. package/dist/apps/www/settings/auto-reply/index.js +6 -7
  416. package/dist/apps/www/settings/encryption/index.d.ts +2 -2
  417. package/dist/apps/www/settings/encryption/index.js +106 -96
  418. package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
  419. package/dist/apps/www/settings/filters/[uid].js +6 -2
  420. package/dist/apps/www/settings/filters/index.d.ts +2 -2
  421. package/dist/apps/www/settings/filters/index.js +3 -1
  422. package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
  423. package/dist/apps/www/settings/filters/new/index.js +9 -3
  424. package/dist/apps/www/settings/labels/index.d.ts +2 -2
  425. package/dist/apps/www/settings/labels/index.js +3 -1
  426. package/dist/apps/www/settings/privacy/index.d.ts +2 -2
  427. package/dist/apps/www/settings/privacy/index.js +8 -9
  428. package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
  429. package/dist/apps/www/settings/read-receipts/index.js +6 -7
  430. package/dist/apps/www/settings/sharing/index.d.ts +2 -2
  431. package/dist/apps/www/settings/sharing/index.js +22 -34
  432. package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
  433. package/dist/apps/www/settings/signatures/[uid].js +6 -2
  434. package/dist/apps/www/settings/signatures/index.d.ts +2 -2
  435. package/dist/apps/www/settings/signatures/index.js +3 -1
  436. package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
  437. package/dist/apps/www/settings/signatures/new/index.js +9 -4
  438. package/dist/apps/www/tasks/index.d.ts +2 -2
  439. package/dist/apps/www/tasks/index.js +22 -16
  440. package/package.json +2 -2
  441. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  442. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  443. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -0,0 +1,106 @@
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 { AppearanceMode, AppearancePreferences, normalizeAppearance } from "@rapidmx/react-shared/appearance/preferencesApi.js";
18
+ import { rangeFromLevels, rangeToLevels } from "./photo.js";
19
+ import type { PhotoRange } from "./theme.js";
20
+
21
+ export const APPEARANCE_CACHE_KEY = "rapidmx-appearance";
22
+
23
+ export interface AppearanceCache {
24
+ v: 1;
25
+ /** Whose it is - so a browser shared by two accounts never shows one the other's background. Absent when the page didn't know. */
26
+ uid?: string;
27
+ /** `cacheKeyOf(prefs)`: what the stylesheet was made from. */
28
+ key: string;
29
+ /** When these preferences were last changed, in milliseconds - see this module's doc comment. */
30
+ t: number;
31
+ mode: AppearanceMode;
32
+ prefs: AppearancePreferences;
33
+ /** The finished stylesheet text; empty when the preferences change nothing. */
34
+ css: string;
35
+ /** The measured lightness of the image, as two gray levels, with the image version they belong to. */
36
+ measured?: { version: string; levels: [number, number] };
37
+ }
38
+
39
+ /** 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. */
40
+ export function cacheKeyOf(prefs: AppearancePreferences): string {
41
+ return JSON.stringify(normalizeAppearance(prefs) ?? prefs);
42
+ }
43
+
44
+ /** 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. */
45
+ export function timeOf(updatedAt: string | number | undefined): number {
46
+ const time = typeof updatedAt === "number" ? updatedAt : updatedAt === undefined ? 0 : Date.parse(updatedAt);
47
+ return Number.isFinite(time) ? time : 0;
48
+ }
49
+
50
+ /** The stored cache, or `undefined` when there is none, it can't be read, or it isn't well formed. */
51
+ export function readAppearanceCache(): AppearanceCache | undefined {
52
+ try {
53
+ const raw = JSON.parse(localStorage.getItem(APPEARANCE_CACHE_KEY) ?? "null");
54
+ const prefs = normalizeAppearance(raw?.prefs);
55
+ if (raw?.v !== 1 || !prefs || typeof raw.css !== "string") {
56
+ return undefined;
57
+ }
58
+ const cache: AppearanceCache = {
59
+ v: 1,
60
+ key: cacheKeyOf(prefs),
61
+ t: typeof raw.t === "number" && Number.isFinite(raw.t) ? raw.t : 0,
62
+ mode: prefs.mode,
63
+ prefs,
64
+ css: raw.css,
65
+ };
66
+ if (typeof raw.uid === "string") {
67
+ cache.uid = raw.uid;
68
+ }
69
+ if (typeof raw.measured?.version === "string" && rangeFromLevels(raw.measured.levels)) {
70
+ cache.measured = { version: raw.measured.version, levels: raw.measured.levels };
71
+ }
72
+ return cache;
73
+ } catch {
74
+ return undefined;
75
+ }
76
+ }
77
+
78
+ export function writeAppearanceCache(
79
+ prefs: AppearancePreferences,
80
+ css: string,
81
+ time: number,
82
+ uid?: string,
83
+ measured?: { version: string; range: PhotoRange },
84
+ ): void {
85
+ try {
86
+ const cache: AppearanceCache = { v: 1, key: cacheKeyOf(prefs), t: time, mode: prefs.mode, prefs, css };
87
+ if (uid) {
88
+ cache.uid = uid;
89
+ }
90
+ if (measured) {
91
+ cache.measured = { version: measured.version, levels: rangeToLevels(measured.range) };
92
+ }
93
+ localStorage.setItem(APPEARANCE_CACHE_KEY, JSON.stringify(cache));
94
+ } catch {
95
+ // Storage is unavailable or full: the theme still applies, it just isn't remembered.
96
+ }
97
+ }
98
+
99
+ /** Forgets the cache - on sign-out, so the next person to sign in on this browser starts from their own. */
100
+ export function clearAppearanceCache(): void {
101
+ try {
102
+ localStorage.removeItem(APPEARANCE_CACHE_KEY);
103
+ } catch {
104
+ // Nothing to forget.
105
+ }
106
+ }
@@ -0,0 +1,77 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { createContext, useContext } from "react";
6
+ import type {
7
+ AppearanceBackground,
8
+ AppearanceMode,
9
+ AppearancePreferences,
10
+ ColorKey,
11
+ } from "@rapidmx/react-shared/appearance/preferencesApi.js";
12
+ import type { ResolvedTheme } from "./resolvedTheme.js";
13
+ import type { PhotoRange } from "./theme.js";
14
+
15
+ /**
16
+ * A change to the preferences, merged into what is there: `mode` replaces; each `colors` entry replaces that colour, and `null` clears
17
+ * it (back to the app's own); `background` merges into the current one (or a fresh one); `background: null` removes it.
18
+ */
19
+ export interface AppearancePatch {
20
+ mode?: AppearanceMode;
21
+ colors?: Partial<Record<ColorKey, string | null>>;
22
+ background?: Partial<AppearanceBackground> | null;
23
+ }
24
+
25
+ export interface AppearanceApi {
26
+ /** What is showing now - the user's choices including one still being saved. Never `undefined`: a user who chose nothing has the defaults. */
27
+ prefs: AppearancePreferences;
28
+ /** The scheme showing: the chosen mode, or for "System" the operating system's. */
29
+ resolved: ResolvedTheme;
30
+ /** Where the background image is loaded from - the stored image's URL, or a local preview while it uploads. */
31
+ backgroundUrl: string | undefined;
32
+ /** The background image's measured lightness, once known (until then the panels assume the worst case). */
33
+ measured: PhotoRange | undefined;
34
+ /** Nothing customised - `Reset all` has nothing to do. */
35
+ isDefault: boolean;
36
+ /** A save is in flight or waiting for the next quiet moment. */
37
+ saving: boolean;
38
+ /** Why the last save failed, in words for the user; `null` after a success or `clearError()`. The change it belonged to was rolled back. */
39
+ error: string | null;
40
+ clearError: () => void;
41
+ /** Applies the change at once and saves it in the background; a failed save rolls back to what the server has and sets `error`. */
42
+ setPrefs: (patch: AppearancePatch) => void;
43
+ /** Shows `file` as the background at once (already checked with `validateBackgroundFile()`) while it uploads; rolls back on failure. */
44
+ uploadBackground: (file: Blob) => Promise<void>;
45
+ /** Removes the background image (and returns to no background). */
46
+ removeBackground: () => Promise<void>;
47
+ /** Back to the defaults: system scheme, the app's colours, no background. */
48
+ reset: () => Promise<void>;
49
+ }
50
+
51
+ const NOOP_PREFS: AppearancePreferences = { version: 1, mode: "system" };
52
+
53
+ /** What `useAppearance()` returns outside an `AppearanceProvider` (a component tested alone): the defaults, and changes go nowhere. */
54
+ export const INERT_APPEARANCE: AppearanceApi = {
55
+ prefs: NOOP_PREFS,
56
+ resolved: "light",
57
+ backgroundUrl: undefined,
58
+ measured: undefined,
59
+ isDefault: true,
60
+ saving: false,
61
+ error: null,
62
+ clearError: () => undefined,
63
+ setPrefs: () => undefined,
64
+ uploadBackground: () => Promise.resolve(),
65
+ removeBackground: () => Promise.resolve(),
66
+ reset: () => Promise.resolve(),
67
+ };
68
+
69
+ export const AppearanceContext = createContext<AppearanceApi>(INERT_APPEARANCE);
70
+
71
+ /**
72
+ * The user's appearance preferences and how to change them: `{ prefs, setPrefs, ... }` (see `AppearanceApi`). Changes apply to the
73
+ * whole app the instant they are made and are saved in the background. Outside an `AppearanceProvider` it is inert.
74
+ */
75
+ export function useAppearance(): AppearanceApi {
76
+ return useContext(AppearanceContext);
77
+ }
@@ -0,0 +1,35 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { APPEARANCE_STYLE_ID } from "./theme.js";
6
+ import { APPEARANCE_CACHE_KEY } from "./appearanceCache.js";
7
+
8
+ /**
9
+ * The inline script the page layouts put in `<head>`, after the stylesheets and before the body, so the user's theme is in place before
10
+ * the first paint. It runs before any of this app's code and must not depend on it, which is why it is a plain ES5 string:
11
+ *
12
+ * - it reads the cache `AppearanceProvider` keeps in `localStorage` (`appearanceCache.ts`): finished stylesheet text, the mode, the key
13
+ * of the preferences they were made from and the time they were last changed (`t`);
14
+ * - it does nothing when the cache belongs to another account (`<html data-uid>` names the signed-in one, when the page knows it), or
15
+ * when the server already rendered a stylesheet (`<style data-key data-t>`) for *different* preferences that are at least as new: the
16
+ * server's copy can be a minute old, so it only wins when it is not older than the browser's;
17
+ * - otherwise it puts the cached stylesheet in the page (replacing the server's, which for the same preferences lacks the measured
18
+ * lightness of the background image), or removes the server's when the cache says nothing is chosen, and sets `<html data-theme>` for
19
+ * an explicit "Light" or "Dark" (removing it for "System", whose stylesheet follows the operating system by media query).
20
+ *
21
+ * Any failure (storage blocked, bad JSON) leaves the page as it was.
22
+ */
23
+ export const APPEARANCE_BOOT_SCRIPT =
24
+ "(function(){try{" +
25
+ `var c=JSON.parse(localStorage.getItem("${APPEARANCE_CACHE_KEY}")||"null"),d=document,h=d.documentElement;` +
26
+ 'if(!c||c.v!==1||typeof c.css!=="string"||typeof c.key!=="string")return;' +
27
+ 'var u=h.getAttribute("data-uid");if(u&&c.uid&&u!==c.uid)return;' +
28
+ `var s=d.getElementById("${APPEARANCE_STYLE_ID}");` +
29
+ 'if(s&&s.getAttribute("data-key")!==c.key&&Number(s.getAttribute("data-t")||0)>=(c.t||0))return;' +
30
+ "if(c.css){" +
31
+ `if(!s){s=d.createElement("style");s.id="${APPEARANCE_STYLE_ID}";d.head.appendChild(s)}` +
32
+ 's.setAttribute("data-key",c.key);s.textContent=c.css' +
33
+ "}else if(s){s.parentNode.removeChild(s)}" +
34
+ 'if(c.mode==="light"||c.mode==="dark")h.setAttribute("data-theme",c.mode);else h.removeAttribute("data-theme")' +
35
+ "}catch(e){}})();";
@@ -0,0 +1,169 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * The small amount of colour maths theming needs, with no dependency: parsing and formatting `#rrggbb`, mixing, WCAG relative
7
+ * luminance and contrast, and deriving a readable text colour or a whole shade scale from one chosen colour. Pure - nothing here
8
+ * touches the DOM.
9
+ */
10
+
11
+ /** A colour as 0-255 red, green and blue. */
12
+ export interface Rgb {
13
+ r: number;
14
+ g: number;
15
+ b: number;
16
+ }
17
+
18
+ export const BLACK: Rgb = { r: 0, g: 0, b: 0 };
19
+ export const WHITE: Rgb = { r: 255, g: 255, b: 255 };
20
+
21
+ /** WCAG AA for normal-size text. */
22
+ export const AA_CONTRAST = 4.5;
23
+
24
+ const HEX_6 = /^#?([0-9a-f]{6})$/i;
25
+ const HEX_3 = /^#?([0-9a-f])([0-9a-f])([0-9a-f])$/i;
26
+
27
+ /** `#rrggbb` (or, forgivingly, `rrggbb`, `#rgb` and `rgb`, in any case) as a colour; `undefined` for anything else. */
28
+ export function parseHex(input: string): Rgb | undefined {
29
+ const text = input.trim();
30
+ let digits: string;
31
+ const long = HEX_6.exec(text);
32
+ if (long) {
33
+ digits = long[1];
34
+ } else {
35
+ const short = HEX_3.exec(text);
36
+ if (!short) {
37
+ return undefined;
38
+ }
39
+ digits = short[1] + short[1] + short[2] + short[2] + short[3] + short[3];
40
+ }
41
+ const value = parseInt(digits, 16);
42
+ return { r: (value >> 16) & 255, g: (value >> 8) & 255, b: value & 255 };
43
+ }
44
+
45
+ function channel(value: number): number {
46
+ return Math.min(255, Math.max(0, Math.round(value)));
47
+ }
48
+
49
+ /** The colour as lower-case `#rrggbb` (channels are rounded and clamped). */
50
+ export function toHex({ r, g, b }: Rgb): string {
51
+ return "#" + [r, g, b].map((value) => channel(value).toString(16).padStart(2, "0")).join("");
52
+ }
53
+
54
+ /** The lower-case `#rrggbb` form of a hex colour typed any of the forgiving ways `parseHex()` accepts; `undefined` if it isn't one. */
55
+ export function normalizeHex(input: string): string | undefined {
56
+ const rgb = parseHex(input);
57
+ return rgb ? toHex(rgb) : undefined;
58
+ }
59
+
60
+ /** `true` for a strict `#rrggbb` (what the server stores). */
61
+ export function isHexColor(input: string): boolean {
62
+ return /^#[0-9a-f]{6}$/i.test(input);
63
+ }
64
+
65
+ /** The colour `amount` of the way from `from` to `to` (0 is `from`, 1 is `to`), mixed in sRGB. */
66
+ export function mix(from: Rgb, to: Rgb, amount: number): Rgb {
67
+ const t = Math.min(1, Math.max(0, amount));
68
+ return { r: from.r + (to.r - from.r) * t, g: from.g + (to.g - from.g) * t, b: from.b + (to.b - from.b) * t };
69
+ }
70
+
71
+ function linear(value: number): number {
72
+ const s = value / 255;
73
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
74
+ }
75
+
76
+ /** WCAG 2 relative luminance: 0 for black, 1 for white. */
77
+ export function relativeLuminance({ r, g, b }: Rgb): number {
78
+ return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
79
+ }
80
+
81
+ /** The gray (0-255) whose relative luminance is `luminance` - the inverse of `relativeLuminance()` for a gray. */
82
+ export function grayOfLuminance(luminance: number): number {
83
+ const l = Math.min(1, Math.max(0, luminance));
84
+ const s = l <= 0.0031308 ? l * 12.92 : 1.055 * Math.pow(l, 1 / 2.4) - 0.055;
85
+ return channel(s * 255);
86
+ }
87
+
88
+ /** WCAG 2 contrast ratio between two colours: 1 (identical) to 21 (black on white). Symmetric. */
89
+ export function contrastRatio(a: Rgb, b: Rgb): number {
90
+ const la = relativeLuminance(a);
91
+ const lb = relativeLuminance(b);
92
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
93
+ }
94
+
95
+ /** The contrast ratio of two `#rrggbb` colours, `undefined` when either doesn't parse. */
96
+ export function contrastOfHex(a: string, b: string): number | undefined {
97
+ const first = parseHex(a);
98
+ const second = parseHex(b);
99
+ return first && second ? contrastRatio(first, second) : undefined;
100
+ }
101
+
102
+ /** The app's own two text colours, for text that must sit on a colour of unknown lightness. */
103
+ export const DARK_TEXT: Rgb = { r: 0x1c, g: 0x25, b: 0x26 };
104
+ export const LIGHT_TEXT: Rgb = { r: 0xff, g: 0xff, b: 0xff };
105
+
106
+ /**
107
+ * The app's dark or light text colour for text on `background`: `prefer` (light by default, as on the primary colour; the accent
108
+ * colour prefers dark) whenever that reaches AA contrast, else whichever of the two reads better.
109
+ */
110
+ export function readableOn(background: Rgb, prefer: Rgb = LIGHT_TEXT): Rgb {
111
+ if (contrastRatio(prefer, background) >= AA_CONTRAST) {
112
+ return prefer;
113
+ }
114
+ return contrastRatio(DARK_TEXT, background) >= contrastRatio(LIGHT_TEXT, background) ? DARK_TEXT : LIGHT_TEXT;
115
+ }
116
+
117
+ /**
118
+ * `foreground`, moved toward black or white - whichever the background is further from - just far enough to reach `minimum`
119
+ * contrast against `background`; unchanged when it already does. Where even black or white can't (a mid-gray background can't reach
120
+ * 7:1), the best of the two.
121
+ */
122
+ export function ensureContrast(foreground: Rgb, background: Rgb, minimum: number = AA_CONTRAST): Rgb {
123
+ if (contrastRatio(foreground, background) >= minimum) {
124
+ return foreground;
125
+ }
126
+ const target = contrastRatio(BLACK, background) >= contrastRatio(WHITE, background) ? BLACK : WHITE;
127
+ for (let step = 1; step <= 20; step++) {
128
+ const candidate = mix(foreground, target, step / 20);
129
+ if (contrastRatio(candidate, background) >= minimum) {
130
+ return candidate;
131
+ }
132
+ }
133
+ return target;
134
+ }
135
+
136
+ /** The shades the app's tokens come in, derived from one colour. */
137
+ export interface PrimaryScale {
138
+ base: string;
139
+ dark: string;
140
+ darker: string;
141
+ darkest: string;
142
+ light: string;
143
+ lighter: string;
144
+ lightest: string;
145
+ /** Text that reads on `base`: `preferOnBase` (white by default) when that reaches AA contrast, else the better of the app's two. */
146
+ onBase: string;
147
+ }
148
+
149
+ /**
150
+ * The seven-step scale (`base`, three darker shades, three lighter ones) plus the text colour for a button of `base`, from the single
151
+ * `#rrggbb` a user picked. Shades are mixes with black and white, so the hue stays exactly the one chosen. `undefined` if `hex` isn't
152
+ * a colour.
153
+ */
154
+ export function deriveScale(hex: string, preferOnBase: Rgb = LIGHT_TEXT): PrimaryScale | undefined {
155
+ const base = parseHex(hex);
156
+ if (!base) {
157
+ return undefined;
158
+ }
159
+ return {
160
+ base: toHex(base),
161
+ dark: toHex(mix(base, BLACK, 0.2)),
162
+ darker: toHex(mix(base, BLACK, 0.4)),
163
+ darkest: toHex(mix(base, BLACK, 0.55)),
164
+ light: toHex(mix(base, WHITE, 0.2)),
165
+ lighter: toHex(mix(base, WHITE, 0.4)),
166
+ lightest: toHex(mix(base, WHITE, 0.6)),
167
+ onBase: toHex(readableOn(base, preferOnBase)),
168
+ };
169
+ }
@@ -0,0 +1,88 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * How light and how dark a background photo is, measured in the browser, so the panels over it are only as opaque as its busiest
7
+ * places demand (see `panelAlpha()`). Nothing is uploaded: the picture is drawn once at 32 x 32 pixels on a canvas and its lightest and
8
+ * darkest twentieth are taken.
9
+ */
10
+ import { grayOfLuminance, Rgb, relativeLuminance } from "./color.js";
11
+ import type { PhotoRange } from "./theme.js";
12
+
13
+ /** The side, in pixels, of the square the picture is shrunk to for measuring. */
14
+ export const MEASURE_SIZE = 32;
15
+
16
+ /** Pixels more transparent than this don't count - a transparent PNG margin is whatever is behind it, not the picture. */
17
+ const MIN_ALPHA = 128;
18
+
19
+ function gray(level: number): Rgb {
20
+ return { r: level, g: level, b: level };
21
+ }
22
+
23
+ /**
24
+ * The 5th and 95th percentile of relative luminance across RGBA pixel data, as the grays that have them - the darkest and lightest
25
+ * a panel's text may sit over. `undefined` when no pixel is opaque enough to count.
26
+ */
27
+ export function rangeOfPixels(rgba: ArrayLike<number>): PhotoRange | undefined {
28
+ const values: number[] = [];
29
+ for (let i = 0; i + 3 < rgba.length; i += 4) {
30
+ if (rgba[i + 3] >= MIN_ALPHA) {
31
+ values.push(relativeLuminance({ r: rgba[i], g: rgba[i + 1], b: rgba[i + 2] }));
32
+ }
33
+ }
34
+ if (values.length === 0) {
35
+ return undefined;
36
+ }
37
+ values.sort((a, b) => a - b);
38
+ const at = (fraction: number) => values[Math.min(values.length - 1, Math.floor(values.length * fraction))];
39
+ return { lo: gray(grayOfLuminance(at(0.05))), hi: gray(grayOfLuminance(at(0.95))) };
40
+ }
41
+
42
+ /**
43
+ * Loads `url` and measures it. `undefined` where it can't be done - no canvas, the picture doesn't load, or it comes from another
44
+ * origin without CORS and taints the canvas - in which case the caller assumes the worst.
45
+ */
46
+ export async function measureImage(url: string): Promise<PhotoRange | undefined> {
47
+ if (typeof document === "undefined" || typeof Image === "undefined") {
48
+ return undefined;
49
+ }
50
+ try {
51
+ const image = new Image();
52
+ image.decoding = "async";
53
+ const loaded = new Promise<void>((resolve, reject) => {
54
+ image.onload = () => resolve();
55
+ image.onerror = () => reject(new Error("The image did not load."));
56
+ });
57
+ image.src = url;
58
+ await loaded;
59
+ const canvas = document.createElement("canvas");
60
+ canvas.width = MEASURE_SIZE;
61
+ canvas.height = MEASURE_SIZE;
62
+ const context = canvas.getContext("2d", { willReadFrequently: true });
63
+ if (!context) {
64
+ return undefined;
65
+ }
66
+ context.drawImage(image, 0, 0, MEASURE_SIZE, MEASURE_SIZE);
67
+ return rangeOfPixels(context.getImageData(0, 0, MEASURE_SIZE, MEASURE_SIZE).data);
68
+ } catch {
69
+ return undefined;
70
+ }
71
+ }
72
+
73
+ /** A measured range as the two gray levels the cache stores. */
74
+ export function rangeToLevels(range: PhotoRange): [number, number] {
75
+ return [range.lo.r, range.hi.r];
76
+ }
77
+
78
+ /** The range for two stored gray levels; `undefined` unless both are numbers from 0 to 255. */
79
+ export function rangeFromLevels(levels: unknown): PhotoRange | undefined {
80
+ if (
81
+ !Array.isArray(levels) ||
82
+ levels.length !== 2 ||
83
+ !levels.every((level) => typeof level === "number" && Number.isFinite(level) && level >= 0 && level <= 255)
84
+ ) {
85
+ return undefined;
86
+ }
87
+ return { lo: gray(levels[0]), hi: gray(levels[1]) };
88
+ }
@@ -0,0 +1,67 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useSyncExternalStore } from "react";
6
+
7
+ /** The colour scheme the app is showing right now: what the user chose, or - for "System" - what the operating system prefers. */
8
+ export type ResolvedTheme = "light" | "dark";
9
+
10
+ /** The media query that says the operating system prefers a dark scheme. */
11
+ export const DARK_SCHEME_QUERY = "(prefers-color-scheme: dark)";
12
+
13
+ /**
14
+ * The scheme in effect: an explicit `data-theme="light" | "dark"` on `<html>` (set by `AppearanceProvider` for every choice
15
+ * including "System", and by a page's own `<html data-theme>`) wins, else the operating system's preference, else `"light"`.
16
+ * `"light"` where there is no document (server-side rendering).
17
+ */
18
+ export function readResolvedTheme(): ResolvedTheme {
19
+ if (typeof document === "undefined") {
20
+ return "light";
21
+ }
22
+ const override = document.documentElement.getAttribute("data-theme");
23
+ if (override === "light" || override === "dark") {
24
+ return override;
25
+ }
26
+ return typeof window.matchMedia === "function" && window.matchMedia(DARK_SCHEME_QUERY).matches ? "dark" : "light";
27
+ }
28
+
29
+ /** Calls `onChange` when the operating system's scheme or `<html data-theme>` changes; returns the unsubscribe. */
30
+ function subscribe(onChange: () => void): () => void {
31
+ const media = typeof window.matchMedia === "function" ? window.matchMedia(DARK_SCHEME_QUERY) : undefined;
32
+ media?.addEventListener("change", onChange);
33
+ const observer = new MutationObserver(onChange);
34
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
35
+ return () => {
36
+ media?.removeEventListener("change", onChange);
37
+ observer.disconnect();
38
+ };
39
+ }
40
+
41
+ /**
42
+ * `"light"` or `"dark"`: the scheme the app is showing now, kept live - it changes when the user picks another in Settings >
43
+ * Appearance and, while they follow the system, when the operating system's preference flips. For anything that must match the
44
+ * app's scheme without reading CSS (a message body rendered in its own document, a canvas, a chart). Needs no provider: it reads
45
+ * `<html data-theme>` and `matchMedia` itself. Renders `"light"` on the server and for the hydrating render, then the real value.
46
+ */
47
+ export function useResolvedTheme(): ResolvedTheme {
48
+ return useSyncExternalStore(subscribe, readResolvedTheme, () => "light");
49
+ }
50
+
51
+ export { useAppearance } from "./appearanceContext.js";
52
+ export type { AppearanceApi, AppearancePatch } from "./appearanceContext.js";
53
+
54
+ function subscribeSystem(onChange: () => void): () => void {
55
+ const media = typeof window.matchMedia === "function" ? window.matchMedia(DARK_SCHEME_QUERY) : undefined;
56
+ media?.addEventListener("change", onChange);
57
+ return () => media?.removeEventListener("change", onChange);
58
+ }
59
+
60
+ function readSystemDark(): boolean {
61
+ return typeof window.matchMedia === "function" && window.matchMedia(DARK_SCHEME_QUERY).matches;
62
+ }
63
+
64
+ /** `true` while the operating system prefers a dark scheme - only the media query, never `<html data-theme>` (which `AppearanceProvider` sets from this). */
65
+ export function useSystemPrefersDark(): boolean {
66
+ return useSyncExternalStore(subscribeSystem, readSystemDark, () => false);
67
+ }