@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,62 @@
1
+ import React from "react";
2
+ import type { MailAddressLike } from "@rapidmx/react-shared/mail/mailAddress.js";
3
+ /**
4
+ * The pieces the reading pane is drawn from: a header card that carries the subject, and one card per message. Built from the app's own
5
+ * tokens only (`bg-surface`, `text-text`, `border-border`), so they follow the theme, a branding palette and the Appearance colours; the
6
+ * body area inside a card paints its own opaque surface (`MessageBody`), so a translucent card over a background photo stays legible.
7
+ */
8
+ /** The header-style card at the top of the pane: the conversation's (or message's) subject, and what goes with it. */
9
+ export declare function SubjectCard({ subject, meta, children, }: {
10
+ subject: string;
11
+ /** A line under the subject, such as "3 messages". */
12
+ meta?: React.ReactNode;
13
+ children?: React.ReactNode;
14
+ }): React.JSX.Element;
15
+ /** One message's card: rounded, bordered, lifted a little, its content clipped to the corners. The accent bar of an unread message runs down its left edge. */
16
+ export declare function CardShell({ unread, className, children, }: {
17
+ unread?: boolean;
18
+ className?: string;
19
+ children: React.ReactNode;
20
+ }): React.JSX.Element;
21
+ /**
22
+ * Headings carry the display typeface globally (`app.css`, unlayered - so no utility class can beat it); a sender line that happens to be a
23
+ * heading is body text and keeps the body's, by inline style.
24
+ */
25
+ export declare const BODY_FONT_STYLE: React.CSSProperties;
26
+ /** A sender's avatar in the card header. There are no photos in the data, so this is the initials disc every contact gets. */
27
+ export declare function SenderAvatar({ from }: {
28
+ from: MailAddressLike;
29
+ }): React.JSX.Element;
30
+ /**
31
+ * A collapsed message: its card with the sender, the date and the first line of the body, all of it the one button that expands it.
32
+ * Shares the header's look with an expanded card, so expanding does not change how the message begins.
33
+ */
34
+ export declare function CollapsedCard({ from, date, preview, unread, buttonProps, buttonRef, senderClassName, dateClassName, }: {
35
+ from: MailAddressLike;
36
+ date: string;
37
+ preview: React.ReactNode;
38
+ unread: boolean;
39
+ buttonProps: React.ButtonHTMLAttributes<HTMLButtonElement>;
40
+ buttonRef: (node: HTMLButtonElement | null) => void;
41
+ senderClassName: string;
42
+ dateClassName: string;
43
+ }): React.JSX.Element;
44
+ /** The card's place-holder while its message is still loading: an avatar, two header lines and a few lines of body. */
45
+ export declare function CardSkeleton(): React.JSX.Element;
46
+ /**
47
+ * What the pane shows while it is getting ready - its code loading, a conversation's messages arriving: the subject card as soon as the
48
+ * subject is known (`subject` is `undefined` when it is not, as on the mobile page before its message arrived) and a skeleton card per
49
+ * message, so the frame of the pane is on screen at once and the messages only fill in. One live region for the whole thing.
50
+ */
51
+ export declare function ReadingPaneSkeleton({ subject, messageCount }: {
52
+ subject?: string;
53
+ messageCount?: number;
54
+ }): React.JSX.Element;
55
+ /**
56
+ * The skeleton cards themselves - up to three, one live region ("Loading the message") for all of them - in the place the message cards take,
57
+ * so a pane that already shows its subject card (a thread whose messages are still coming) swaps them for the real ones without the header
58
+ * card being drawn again.
59
+ */
60
+ export declare function SkeletonCards({ messageCount }: {
61
+ messageCount?: number;
62
+ }): React.JSX.Element;
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import ContactAvatar from "@rapidmx/react-shared/components/avatar/ContactAvatar.js";
3
+ import Skeleton from "@rapidmx/react-shared/components/feedback/Skeleton.js";
4
+ import MailAddress from "../MailAddress.js";
5
+ import { UnreadBar, UnreadLabel } from "../unreadStyle.js";
6
+ /**
7
+ * The pieces the reading pane is drawn from: a header card that carries the subject, and one card per message. Built from the app's own
8
+ * tokens only (`bg-surface`, `text-text`, `border-border`), so they follow the theme, a branding palette and the Appearance colours; the
9
+ * body area inside a card paints its own opaque surface (`MessageBody`), so a translucent card over a background photo stays legible.
10
+ */
11
+ /** The header-style card at the top of the pane: the conversation's (or message's) subject, and what goes with it. */
12
+ export function SubjectCard({ subject, meta, children, }) {
13
+ return (_jsxs("header", { className: "shrink-0 mx-2 mt-2 sm:mx-3 sm:mt-3 rounded-lg border border-border bg-surface backdrop-blur-sm shadow-sm px-4 py-2.5", children: [_jsx("h1", { className: "text-base sm:text-lg font-bold tracking-tight line-clamp-2 break-words", children: subject }), meta && _jsx("p", { className: "text-xs sm:text-sm text-text-muted mt-0.5", children: meta }), children] }));
14
+ }
15
+ /** One message's card: rounded, bordered, lifted a little, its content clipped to the corners. The accent bar of an unread message runs down its left edge. */
16
+ export function CardShell({ unread = false, className = "", children, }) {
17
+ return (_jsxs("div", { className: ["relative rounded-lg border border-border bg-surface backdrop-blur-sm shadow-sm overflow-hidden", className].join(" "), children: [_jsx(UnreadBar, { unread: unread }), children] }));
18
+ }
19
+ /**
20
+ * Headings carry the display typeface globally (`app.css`, unlayered - so no utility class can beat it); a sender line that happens to be a
21
+ * heading is body text and keeps the body's, by inline style.
22
+ */
23
+ export const BODY_FONT_STYLE = { fontFamily: "var(--rr-font-family)" };
24
+ /** A sender's avatar in the card header. There are no photos in the data, so this is the initials disc every contact gets. */
25
+ export function SenderAvatar({ from }) {
26
+ return _jsx(ContactAvatar, { displayName: from.displayName || from.name || from.address, size: 40 });
27
+ }
28
+ /**
29
+ * A collapsed message: its card with the sender, the date and the first line of the body, all of it the one button that expands it.
30
+ * Shares the header's look with an expanded card, so expanding does not change how the message begins.
31
+ */
32
+ export function CollapsedCard({ from, date, preview, unread, buttonProps, buttonRef, senderClassName, dateClassName, }) {
33
+ return (_jsx(CardShell, { unread: unread, children: _jsx("h2", { style: BODY_FONT_STYLE, children: _jsxs("button", { type: "button", ref: buttonRef, ...buttonProps, className: [
34
+ "w-full text-left flex items-center gap-3 px-4 py-3",
35
+ unread ? "bg-primary/[0.07] hover:bg-primary/10" : "hover:bg-surface-alt",
36
+ "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary",
37
+ ].join(" "), children: [_jsx(UnreadLabel, { unread: unread }), _jsx(SenderAvatar, { from: from }), _jsxs("span", { className: "flex-1 min-w-0", children: [_jsxs("span", { className: "flex flex-wrap items-baseline justify-between gap-x-2 text-sm", children: [_jsx(MailAddress, { recipient: from, className: `${senderClassName} min-w-[13rem]! flex-1` }), _jsx("span", { className: ["text-xs shrink-0", dateClassName].join(" "), children: date })] }), _jsx("span", { className: "block text-xs text-text-muted truncate font-normal", children: preview })] })] }) }) }));
38
+ }
39
+ /** The card's place-holder while its message is still loading: an avatar, two header lines and a few lines of body. */
40
+ export function CardSkeleton() {
41
+ return (_jsxs("div", { "aria-hidden": "true", className: "rounded-lg border border-border bg-surface shadow-sm overflow-hidden", children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [_jsx(Skeleton, { width: "w-10", height: "h-10", className: "rounded-full shrink-0" }), _jsxs("div", { className: "flex-1 flex flex-col gap-2", children: [_jsx(Skeleton, { width: "w-1/3" }), _jsx(Skeleton, { width: "w-1/2", height: "h-3" })] }), _jsx(Skeleton, { width: "w-16", height: "h-3" })] }), _jsxs("div", { className: "flex flex-col gap-2.5 px-4 pb-4", children: [_jsx(Skeleton, { width: "w-11/12" }), _jsx(Skeleton, { width: "w-4/5" }), _jsx(Skeleton, { width: "w-9/12" })] })] }));
42
+ }
43
+ /**
44
+ * What the pane shows while it is getting ready - its code loading, a conversation's messages arriving: the subject card as soon as the
45
+ * subject is known (`subject` is `undefined` when it is not, as on the mobile page before its message arrived) and a skeleton card per
46
+ * message, so the frame of the pane is on screen at once and the messages only fill in. One live region for the whole thing.
47
+ */
48
+ export function ReadingPaneSkeleton({ subject, messageCount = 1 }) {
49
+ return (_jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex flex-col", children: [subject !== undefined ? (_jsx(SubjectCard, { subject: subject || "(no subject)", meta: messageCount > 1 ? `${messageCount} messages` : undefined })) : (_jsx("div", { "aria-hidden": "true", className: "shrink-0 mx-2 mt-2 sm:mx-3 sm:mt-3 rounded-lg border border-border bg-surface shadow-sm px-4 py-3", children: _jsx(Skeleton, { width: "w-2/3", height: "h-6" }) })), _jsx(SkeletonCards, { messageCount: messageCount })] }));
50
+ }
51
+ /**
52
+ * The skeleton cards themselves - up to three, one live region ("Loading the message") for all of them - in the place the message cards take,
53
+ * so a pane that already shows its subject card (a thread whose messages are still coming) swaps them for the real ones without the header
54
+ * card being drawn again.
55
+ */
56
+ export function SkeletonCards({ messageCount = 1 }) {
57
+ return (_jsxs("div", { role: "status", "aria-busy": "true", className: "flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3", children: [_jsx("span", { className: "sr-only", children: "Loading the message" }), Array.from({ length: Math.min(3, Math.max(1, messageCount)) }, (_, index) => (_jsx(CardSkeleton, {}, index)))] }));
58
+ }
@@ -0,0 +1,38 @@
1
+ import { Attachment } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ import type { MimeAttachment } from "@rapidmx/react-shared/crypto/mime.js";
3
+ /** What a message's body is: server-sanitized HTML, or plain text (a message with no HTML part, or the server's plain-text fallback). */
4
+ export type BodyContent = {
5
+ kind: "html";
6
+ html: string;
7
+ } | {
8
+ kind: "text";
9
+ text: string;
10
+ };
11
+ /** What the pane asks `/content` for. */
12
+ export declare const ACCEPT = "text/html, text/plain;q=0.8";
13
+ /** Forgets every remembered body (sign-out, and the tests). */
14
+ export declare function clearBodyContentCache(): void;
15
+ /** The remembered body for a message at a version, if any. */
16
+ export declare function cachedBodyContent(uid: string, version: number): BodyContent | undefined;
17
+ /**
18
+ * The message's body from the server's `GET /mail/messages/:id/content`: the HTML the server sanitized when it ingested the message
19
+ * (`sanitizedHtmlBlobKey`), or - for a message with none - the plain-text preview. A request that fails throws an `ApiRequestError`.
20
+ * Successful answers are remembered per uid and version.
21
+ */
22
+ export declare function fetchBodyContent(uid: string, version: number, signal?: AbortSignal): Promise<BodyContent>;
23
+ /** The largest inline image embedded into a body from a signed or encrypted message. */
24
+ export declare const MAX_INLINE_IMAGE_BYTES = 5000000;
25
+ /** A server attachment record as it may really arrive: the model carries the part's `Content-ID`, though the client type doesn't list it. */
26
+ export type InlineCapable = Attachment & {
27
+ contentId?: string;
28
+ };
29
+ /** The absolute URL of an attachment - the frame is a document of its own, where a relative URL would not mean what it does here. */
30
+ export declare function absoluteAttachmentUrl(uid: string): string;
31
+ /** The CSP source that allows exactly this server's attachment URLs and nothing else. */
32
+ export declare function attachmentSource(): string;
33
+ /**
34
+ * How an inline `cid:` image is resolved to something the frame may load: for a message shown from the server, the attachment whose
35
+ * `Content-ID` matches (its URL on this server); for a decrypted or verified one, the part inside the signed/encrypted entity, embedded
36
+ * as a `data:` URI (raster and SVG images up to `MAX_INLINE_IMAGE_BYTES`). Anything else has no source, so the image is not shown.
37
+ */
38
+ export declare function makeCidResolver(attachments: InlineCapable[] | undefined, parts: MimeAttachment[] | undefined): (contentId: string) => string | undefined;
@@ -0,0 +1,84 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { ApiRequestError, apiUrl } from "@rapidmx/react-shared/util/api.js";
6
+ import { attachmentContentUrl } from "@rapidmx/react-shared/mail/mailApi.js";
7
+ /** How many bodies are remembered for the session - a thread that is expanded, collapsed and expanded again does not ask the server twice. */
8
+ const CACHE_LIMIT = 40;
9
+ /** What the pane asks `/content` for. */
10
+ export const ACCEPT = "text/html, text/plain;q=0.8";
11
+ const cache = new Map();
12
+ /** Forgets every remembered body (sign-out, and the tests). */
13
+ export function clearBodyContentCache() {
14
+ cache.clear();
15
+ }
16
+ /** The remembered body for a message at a version, if any. */
17
+ export function cachedBodyContent(uid, version) {
18
+ return cache.get(`${uid}:${version}`);
19
+ }
20
+ /**
21
+ * The message's body from the server's `GET /mail/messages/:id/content`: the HTML the server sanitized when it ingested the message
22
+ * (`sanitizedHtmlBlobKey`), or - for a message with none - the plain-text preview. A request that fails throws an `ApiRequestError`.
23
+ * Successful answers are remembered per uid and version.
24
+ */
25
+ export async function fetchBodyContent(uid, version, signal) {
26
+ const cached = cachedBodyContent(uid, version);
27
+ if (cached) {
28
+ return cached;
29
+ }
30
+ // Says what it can show, as a document request would: the server answers with the sanitized HTML, or its plain-text fallback.
31
+ const res = await fetch(apiUrl(`/mail/messages/${encodeURIComponent(uid)}/content`), {
32
+ credentials: "include",
33
+ headers: { Accept: ACCEPT },
34
+ signal,
35
+ });
36
+ if (!res.ok) {
37
+ const contentType = res.headers.get("content-type") ?? "";
38
+ const body = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
39
+ throw new ApiRequestError((body && (body.message || body.error)) || res.statusText || "Could not load this message.", res.status, body?.code, body);
40
+ }
41
+ const text = await res.text();
42
+ const content = (res.headers.get("content-type") ?? "").toLowerCase().includes("text/html")
43
+ ? { kind: "html", html: text }
44
+ : { kind: "text", text };
45
+ if (cache.size >= CACHE_LIMIT) {
46
+ cache.delete(cache.keys().next().value);
47
+ }
48
+ cache.set(`${uid}:${version}`, content);
49
+ return content;
50
+ }
51
+ /** The largest inline image embedded into a body from a signed or encrypted message. */
52
+ export const MAX_INLINE_IMAGE_BYTES = 5000000;
53
+ const INLINE_IMAGE_TYPES = /^image\/(?:png|jpe?g|gif|webp|avif|bmp|svg\+xml)$/;
54
+ function base64(bytes) {
55
+ let binary = "";
56
+ for (let i = 0; i < bytes.length; i += 0x8000) {
57
+ binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
58
+ }
59
+ return btoa(binary);
60
+ }
61
+ /** The absolute URL of an attachment - the frame is a document of its own, where a relative URL would not mean what it does here. */
62
+ export function absoluteAttachmentUrl(uid) {
63
+ return new URL(attachmentContentUrl(uid), window.location.href).href;
64
+ }
65
+ /** The CSP source that allows exactly this server's attachment URLs and nothing else. */
66
+ export function attachmentSource() {
67
+ return new URL(apiUrl("/mail/attachments/"), window.location.href).href;
68
+ }
69
+ /**
70
+ * How an inline `cid:` image is resolved to something the frame may load: for a message shown from the server, the attachment whose
71
+ * `Content-ID` matches (its URL on this server); for a decrypted or verified one, the part inside the signed/encrypted entity, embedded
72
+ * as a `data:` URI (raster and SVG images up to `MAX_INLINE_IMAGE_BYTES`). Anything else has no source, so the image is not shown.
73
+ */
74
+ export function makeCidResolver(attachments, parts) {
75
+ return (contentId) => {
76
+ if (parts) {
77
+ const part = parts.find((candidate) => candidate.contentId === contentId);
78
+ const bytes = part && INLINE_IMAGE_TYPES.test(part.contentType) ? part.decode() : undefined;
79
+ return part && bytes && bytes.length <= MAX_INLINE_IMAGE_BYTES ? `data:${part.contentType};base64,${base64(bytes)}` : undefined;
80
+ }
81
+ const attachment = attachments?.find((candidate) => candidate.contentId === contentId);
82
+ return attachment ? absoluteAttachmentUrl(attachment.uid) : undefined;
83
+ };
84
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Turning a message's HTML - which is hostile until proven otherwise - into the fragment that goes into its display frame.
3
+ *
4
+ * Four independent layers stand between a mail and script execution, and this file is two of them:
5
+ *
6
+ * 1. the server's own allow-list sanitizer (restapi's `ScanPipeline`), before the body is stored;
7
+ * 2. **this file**: DOMPurify (react-shared's `sanitizeMessageBodyHtml()` - scripts, `on*` handlers, `javascript:` URLs, and every
8
+ * remote resource reference removed) followed by a second, structural pass of our own that removes what DOMPurify's defaults
9
+ * allow but a mail body never needs (forms, frames, media, SVG's active elements, every non-`http(s)`/`mailto`/`tel` link);
10
+ * 3. the frame's Content-Security-Policy (`frameDocument.ts`): no script, no object, no frame, no form, nothing fetched;
11
+ * 4. the frame's `sandbox` attribute, which never has `allow-scripts`, so nothing executes even if all of the above failed.
12
+ *
13
+ * Nothing here parses into the app's own document: every parse is a `DOMParser` document, which has no browsing context, runs no
14
+ * script and fetches nothing.
15
+ */
16
+ /** A body longer than this many characters is not sanitized or shown inline - DOMPurify on megabytes of markup would freeze the tab. */
17
+ export declare const MAX_BODY_HTML_LENGTH = 1500000;
18
+ /** A body with more elements than this is not shown inline either: it cannot be laid out and adapted in a reasonable time. */
19
+ export declare const MAX_BODY_ELEMENTS = 20000;
20
+ export interface PreparedBody {
21
+ /** `too_large` means `html` is empty and the caller should offer the message on its own page instead. */
22
+ status: "ok" | "too_large";
23
+ /** The sanitized fragment: one wrapper `div.rr-msg` holding the message's own `<style>` elements and body. */
24
+ html: string;
25
+ /** The mail carries its own dark styles: a `color-scheme` meta or property, or a `prefers-color-scheme: dark` block. */
26
+ declaresDarkSupport: boolean;
27
+ /** How many `<img>`s point at an inline part (`cid:`) - the frame's CSP only needs an image source for the attachments then. */
28
+ inlineImages: number;
29
+ }
30
+ export interface PrepareOptions {
31
+ /** The URL an inline image's `cid:` reference stands for, or `undefined` when there is no such part. Only `data:` URIs of an
32
+ * image type and `http(s)` URLs are accepted; anything else is treated as unresolved. */
33
+ resolveCid?: (contentId: string) => string | undefined;
34
+ }
35
+ /** CSS with the constructs above neutralised. */
36
+ export declare function sanitizeCss(css: string): string;
37
+ /**
38
+ * Whether a mail's own markup declares that it supports a dark scheme: `<meta name="color-scheme">` naming dark,
39
+ * a `color-scheme` property naming dark in a stylesheet or a `style` attribute, or an `@media (prefers-color-scheme: dark)` block. Such a mail
40
+ * brings its own dark styles, which the display honours instead of adapting the mail's colours.
41
+ */
42
+ export declare function declaresDarkSupport(doc: Document): boolean;
43
+ /**
44
+ * The display fragment for a message's HTML - see the file's header. `too_large` (and an empty `html`) for a body over
45
+ * `MAX_BODY_HTML_LENGTH` characters or `MAX_BODY_ELEMENTS` elements; an empty fragment where there is no DOM to parse with.
46
+ */
47
+ export declare function prepareBodyHtml(raw: string, options?: PrepareOptions): PreparedBody;
@@ -0,0 +1,267 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { sanitizeMessageBodyHtml } from "@rapidmx/react-shared/mail/messageBodySanitizer.js";
6
+ import { parseColour } from "./color.js";
7
+ /**
8
+ * Turning a message's HTML - which is hostile until proven otherwise - into the fragment that goes into its display frame.
9
+ *
10
+ * Four independent layers stand between a mail and script execution, and this file is two of them:
11
+ *
12
+ * 1. the server's own allow-list sanitizer (restapi's `ScanPipeline`), before the body is stored;
13
+ * 2. **this file**: DOMPurify (react-shared's `sanitizeMessageBodyHtml()` - scripts, `on*` handlers, `javascript:` URLs, and every
14
+ * remote resource reference removed) followed by a second, structural pass of our own that removes what DOMPurify's defaults
15
+ * allow but a mail body never needs (forms, frames, media, SVG's active elements, every non-`http(s)`/`mailto`/`tel` link);
16
+ * 3. the frame's Content-Security-Policy (`frameDocument.ts`): no script, no object, no frame, no form, nothing fetched;
17
+ * 4. the frame's `sandbox` attribute, which never has `allow-scripts`, so nothing executes even if all of the above failed.
18
+ *
19
+ * Nothing here parses into the app's own document: every parse is a `DOMParser` document, which has no browsing context, runs no
20
+ * script and fetches nothing.
21
+ */
22
+ /** A body longer than this many characters is not sanitized or shown inline - DOMPurify on megabytes of markup would freeze the tab. */
23
+ export const MAX_BODY_HTML_LENGTH = 1500000;
24
+ /** A body with more elements than this is not shown inline either: it cannot be laid out and adapted in a reasonable time. */
25
+ export const MAX_BODY_ELEMENTS = 20000;
26
+ /** What a stylesheet or a `style` attribute may not contain, whatever the sanitizers before it did. */
27
+ const STYLE_HAZARDS = [
28
+ // Fixed and sticky positioning could cover the frame (or, with a viewport-sized box, all of it) with a fake dialog.
29
+ [/position\s*:\s*(?:fixed|sticky)/gi, "position:relative"],
30
+ // A length in viewport-height units is the frame's own height, which is the message's height: `min-height: 100vh` would make the frame grow
31
+ // by what its content adds on every measurement. As plain pixels, nothing in the message depends on the size of the frame it is in.
32
+ [/(\d(?:\.\d+)?)\s*(?:[sld]?v(?:h|min|max|b))\b/gi, "$1px"],
33
+ [/expression\s*\(/gi, "none("],
34
+ [/-moz-binding\s*:/gi, "-x-binding:"],
35
+ [/behavior\s*:/gi, "x-behavior:"],
36
+ [/@import[^;]*;?/gi, ""],
37
+ ];
38
+ /** CSS with the constructs above neutralised. */
39
+ export function sanitizeCss(css) {
40
+ return STYLE_HAZARDS.reduce((text, [pattern, replacement]) => text.replace(pattern, replacement), css);
41
+ }
42
+ /** Elements removed together with everything inside them. */
43
+ const REMOVED_ELEMENTS = new Set([
44
+ "script",
45
+ "noscript",
46
+ "object",
47
+ "embed",
48
+ "applet",
49
+ "iframe",
50
+ "frame",
51
+ "frameset",
52
+ "noembed",
53
+ "noframes",
54
+ "audio",
55
+ "video",
56
+ "source",
57
+ "track",
58
+ "canvas",
59
+ "dialog",
60
+ "template",
61
+ "slot",
62
+ "portal",
63
+ "bgsound",
64
+ "xml",
65
+ "base",
66
+ "meta",
67
+ "link",
68
+ "title",
69
+ "head",
70
+ "input",
71
+ "select",
72
+ "textarea",
73
+ "datalist",
74
+ "option",
75
+ "optgroup",
76
+ "math",
77
+ // SVG's active or reference-following elements: a script, a foreign document, a use of another element, and animation
78
+ // (which can set an `href` or an event handler's target).
79
+ "foreignobject",
80
+ "use",
81
+ "set",
82
+ "animate",
83
+ "animatemotion",
84
+ "animatetransform",
85
+ ]);
86
+ /** Elements dropped but not their content: a message wrapped in an old ASP.NET `<form>` is still a message. */
87
+ const UNWRAPPED_ELEMENTS = new Set(["form", "fieldset", "legend", "label", "button", "output", "marquee", "blink"]);
88
+ /** Attributes no element of a mail may keep, on top of every `on*` handler. */
89
+ const REMOVED_ATTRIBUTES = new Set([
90
+ "name",
91
+ "srcdoc",
92
+ "formaction",
93
+ "form",
94
+ "ping",
95
+ "nonce",
96
+ "is",
97
+ "slot",
98
+ "contenteditable",
99
+ "accesskey",
100
+ "tabindex",
101
+ "autofocus",
102
+ "draggable",
103
+ "srcset",
104
+ "crossorigin",
105
+ "integrity",
106
+ "referrerpolicy",
107
+ "http-equiv",
108
+ "xlink:href",
109
+ ]);
110
+ /** The `href`s of a link a reader can follow: nothing that runs, nothing relative to this app, nothing that reads a local file. */
111
+ const LINK_URL = /^(?:https?:\/\/|mailto:|tel:)/i;
112
+ /** Inline images: raster types and SVG (an image context cannot run script or load anything), embedded as data. */
113
+ const IMAGE_DATA_URI = /^data:image\/(?:png|jpe?g|gif|webp|avif|bmp|svg\+xml)[;,]/i;
114
+ const CSS_ESCAPE = /\\([0-9a-f]{1,6})\s?|\\(.)/gi;
115
+ /** Decodes CSS escapes, so a check for `color-scheme` cannot be dodged with `col\6fr-scheme`. */
116
+ function unescapeCss(css) {
117
+ return css.replace(CSS_ESCAPE, (_match, hex, other) => hex ? String.fromCodePoint(Math.min(parseInt(hex, 16), 0x10ffff)) : other);
118
+ }
119
+ /**
120
+ * Whether a mail's own markup declares that it supports a dark scheme: `<meta name="color-scheme">` naming dark,
121
+ * a `color-scheme` property naming dark in a stylesheet or a `style` attribute, or an `@media (prefers-color-scheme: dark)` block. Such a mail
122
+ * brings its own dark styles, which the display honours instead of adapting the mail's colours.
123
+ */
124
+ export function declaresDarkSupport(doc) {
125
+ const meta = doc.querySelector('meta[name="color-scheme" i]');
126
+ if (/\bdark\b/i.test(meta?.getAttribute("content") ?? "")) {
127
+ return true;
128
+ }
129
+ // An element's text is never null, and a `[style]` element always has the attribute.
130
+ const sheets = Array.from(doc.querySelectorAll("style"), (style) => style.textContent);
131
+ const inline = Array.from(doc.querySelectorAll("[style]"), (element) => element.getAttribute("style"));
132
+ return [...sheets, ...inline].some((css) => {
133
+ const text = unescapeCss(css);
134
+ return /color-scheme\s*:[^;}]*\bdark\b/i.test(text) || /prefers-color-scheme\s*:\s*dark/i.test(text);
135
+ });
136
+ }
137
+ /** A `bgcolor`/`text`/`link` attribute's value as an `rgb()` colour if we can read it - nothing else is ever copied into a style.
138
+ * `bgcolor="ffffff"` (no hash) is what old templates write and what a browser accepts. */
139
+ function attributeColour(value) {
140
+ const text = (value ?? "").trim();
141
+ const colour = parseColour(/^[0-9a-f]{6}$/i.test(text) ? `#${text}` : text);
142
+ return colour && colour.a > 0 ? `rgb(${Math.round(colour.r)}, ${Math.round(colour.g)}, ${Math.round(colour.b)})` : undefined;
143
+ }
144
+ /** The wrapper `div.rr-msg`: the message's head `<style>`s first (DOMPurify drops a leading one as head content), then its body, with
145
+ * what a `<body bgcolor text link>` said carried over as the styles those attributes stand for. */
146
+ function wrapMessage(doc) {
147
+ const wrapper = doc.createElement("div");
148
+ wrapper.className = "rr-msg";
149
+ for (const style of Array.from(doc.head.querySelectorAll("style"))) {
150
+ wrapper.appendChild(style.cloneNode(true));
151
+ }
152
+ const [background, text, link] = ["bgcolor", "text", "link"].map((name) => attributeColour(doc.body.getAttribute(name)));
153
+ // What the `<body>` said about itself moves to the wrapper, which stands in for it: its presentational colours first (a `style` beats
154
+ // them), then its own `style`, its classes (a template's stylesheet may select on them) and its direction.
155
+ const declarations = [background && `background-color:${background}`, text && `color:${text}`, doc.body.getAttribute("style")].filter(Boolean);
156
+ if (declarations.length > 0) {
157
+ wrapper.setAttribute("style", declarations.join(";"));
158
+ }
159
+ const bodyClass = doc.body.getAttribute("class");
160
+ if (bodyClass) {
161
+ wrapper.className += ` ${bodyClass}`;
162
+ }
163
+ const direction = doc.body.getAttribute("dir");
164
+ if (direction) {
165
+ wrapper.setAttribute("dir", direction);
166
+ }
167
+ if (link) {
168
+ const rule = doc.createElement("style");
169
+ rule.textContent = `:where(.rr-msg a:link){color:${link}}`;
170
+ wrapper.appendChild(rule);
171
+ }
172
+ wrapper.append(...Array.from(doc.body.childNodes));
173
+ return wrapper.outerHTML;
174
+ }
175
+ /** Replaces `element` with its children. */
176
+ function unwrap(element) {
177
+ element.replaceWith(...Array.from(element.childNodes));
178
+ }
179
+ function hardenLink(element) {
180
+ const href = element.getAttribute("href")?.trim() ?? "";
181
+ if (LINK_URL.test(href)) {
182
+ element.setAttribute("href", href);
183
+ element.setAttribute("target", "_blank");
184
+ element.setAttribute("rel", "noopener noreferrer nofollow");
185
+ }
186
+ else {
187
+ // A link that goes nowhere a reader could safely follow stays as text, without the address it hid.
188
+ element.removeAttribute("href");
189
+ element.removeAttribute("target");
190
+ }
191
+ }
192
+ function hardenImage(element, options) {
193
+ const src = (element.getAttribute("src") ?? "").trim();
194
+ const inline = /^cid:/i.test(src);
195
+ const target = inline ? options.resolveCid?.(src.slice(4).replace(/^<|>$/g, "")) : src;
196
+ // Remote images are never fetched (they would tell the sender the message was read); an inline image resolves to an embedded
197
+ // image or to this server's own attachment URL, and one that doesn't resolve has nothing to show.
198
+ if (target !== undefined && (IMAGE_DATA_URI.test(target) || (inline && /^https?:\/\//i.test(target)))) {
199
+ element.setAttribute("src", target);
200
+ }
201
+ else {
202
+ element.removeAttribute("src");
203
+ }
204
+ }
205
+ /**
206
+ * The second, structural pass over already-sanitized markup: everything a mail body has no use for is removed or unwrapped, every
207
+ * link is forced to open in a new tab without an opener, and hazards in stylesheets are neutralised. Runs on a detached document.
208
+ */
209
+ function harden(body, options) {
210
+ let inlineImages = 0;
211
+ const all = Array.from(body.querySelectorAll("*"));
212
+ for (const element of all) {
213
+ const tag = element.localName.toLowerCase();
214
+ if (REMOVED_ELEMENTS.has(tag)) {
215
+ element.remove();
216
+ continue;
217
+ }
218
+ if (UNWRAPPED_ELEMENTS.has(tag)) {
219
+ unwrap(element);
220
+ continue;
221
+ }
222
+ for (const attribute of Array.from(element.attributes)) {
223
+ const name = attribute.name.toLowerCase();
224
+ if (name.startsWith("on") || REMOVED_ATTRIBUTES.has(name)) {
225
+ element.removeAttribute(attribute.name);
226
+ }
227
+ else if (name === "style") {
228
+ element.setAttribute("style", sanitizeCss(attribute.value));
229
+ }
230
+ }
231
+ if (tag === "style") {
232
+ element.textContent = sanitizeCss(element.textContent);
233
+ }
234
+ else if (tag === "a" || tag === "area") {
235
+ hardenLink(element);
236
+ }
237
+ else if (tag === "img") {
238
+ if (/^cid:/i.test(element.getAttribute("src") ?? "")) {
239
+ inlineImages++;
240
+ }
241
+ hardenImage(element, options);
242
+ }
243
+ }
244
+ return { elements: all.length, inlineImages };
245
+ }
246
+ /**
247
+ * The display fragment for a message's HTML - see the file's header. `too_large` (and an empty `html`) for a body over
248
+ * `MAX_BODY_HTML_LENGTH` characters or `MAX_BODY_ELEMENTS` elements; an empty fragment where there is no DOM to parse with.
249
+ */
250
+ export function prepareBodyHtml(raw, options = {}) {
251
+ const empty = { html: "", declaresDarkSupport: false, inlineImages: 0 };
252
+ if (raw.length > MAX_BODY_HTML_LENGTH) {
253
+ return { status: "too_large", ...empty };
254
+ }
255
+ if (typeof DOMParser === "undefined") {
256
+ return { status: "ok", ...empty };
257
+ }
258
+ const parsed = new DOMParser().parseFromString(raw, "text/html");
259
+ const darkSupport = declaresDarkSupport(parsed);
260
+ const sanitized = sanitizeMessageBodyHtml(wrapMessage(parsed));
261
+ const cleaned = new DOMParser().parseFromString(`<!doctype html><body>${sanitized}`, "text/html").body;
262
+ const { elements, inlineImages } = harden(cleaned, options);
263
+ if (elements > MAX_BODY_ELEMENTS) {
264
+ return { status: "too_large", ...empty };
265
+ }
266
+ return { status: "ok", html: cleaned.innerHTML, declaresDarkSupport: darkSupport, inlineImages };
267
+ }
@@ -0,0 +1,63 @@
1
+ /** A colour as the browser reports it: channels 0-255 and an alpha 0-1. */
2
+ export interface Rgba {
3
+ r: number;
4
+ g: number;
5
+ b: number;
6
+ a: number;
7
+ }
8
+ /** A colour with no transparency worth speaking of - what a contrast ratio is computed between. */
9
+ export type Rgb = Pick<Rgba, "r" | "g" | "b">;
10
+ /**
11
+ * Reads the colour notations a browser's computed style and an old-fashioned `bgcolor` attribute produce: `#rgb`, `#rgba`,
12
+ * `#rrggbb`, `#rrggbbaa`, `rgb()`/`rgba()` in the comma and the space syntax, `color(srgb ...)`, `transparent` and a few names.
13
+ * `undefined` for anything else (a gradient, `currentcolor`, `oklch()`), which callers treat as "unknown, leave it alone".
14
+ */
15
+ export declare function parseColour(input: string | undefined | null): Rgba | undefined;
16
+ /** `rgb(r, g, b)` for an opaque colour, `rgba(...)` otherwise - what goes into a generated stylesheet. */
17
+ export declare function formatColour(colour: Rgba | Rgb): string;
18
+ /** `top` painted over `bottom`, as the opaque colour that results. */
19
+ export declare function composite(top: Rgba, bottom: Rgb): Rgb;
20
+ /** WCAG relative luminance, 0 (black) to 1 (white). */
21
+ export declare function luminance(colour: Rgb): number;
22
+ /** WCAG contrast ratio between two opaque colours, 1 to 21. */
23
+ export declare function contrastRatio(a: Rgb, b: Rgb): number;
24
+ /** The lowest contrast ratio WCAG accepts for body text. */
25
+ export declare const MIN_TEXT_CONTRAST = 4.5;
26
+ export interface Hsl {
27
+ h: number;
28
+ s: number;
29
+ l: number;
30
+ }
31
+ export declare function rgbToHsl({ r, g, b }: Rgb): Hsl;
32
+ export declare function hslToRgb({ h, s, l }: Hsl): Rgb;
33
+ /** Whether two colours are the same to the eye's tolerance - what "unchanged" means when deciding to emit a rule. */
34
+ export declare function sameColour(a: Rgb, b: Rgb): boolean;
35
+ /**
36
+ * `colour` moved along its own hue and saturation - lighter over a dark background, darker over a light one - until it reaches
37
+ * `min` contrast against `background`. A colour that already has it is returned untouched; one that cannot get there (a mid-grey
38
+ * background) becomes whichever of white and black reads better.
39
+ */
40
+ export declare function ensureContrast(colour: Rgb, background: Rgb, min?: number): Rgb;
41
+ /** The theme a message is shown in: the opaque surface behind the body, its text and link colours, and whether it is a dark one. */
42
+ export interface ThemeSurface {
43
+ background: Rgb;
44
+ text: Rgb;
45
+ link: Rgb;
46
+ dark: boolean;
47
+ }
48
+ /** Whichever of black and white reads better on `background`. */
49
+ export declare function blackOrWhite(background: Rgb): Rgb;
50
+ /**
51
+ * An authored text colour that cannot be read against the theme's background, adapted so it can. A neutral (black, white, a
52
+ * grey) is flipped to the other end of the scale - black becomes the theme's text colour, a dark grey a light one, and in a
53
+ * light theme white becomes dark text - by placing it between the theme's text and background colours according to how far it
54
+ * is from the one it must not be. Any other colour keeps its hue and saturation and only has its lightness moved, as little as
55
+ * it takes to reach 4.5:1 (`ensureContrast()`), so navy stays a navy and a brand red stays red. One that already reads is
56
+ * returned untouched.
57
+ */
58
+ export declare function adaptForeground(colour: Rgb, surface: ThemeSurface): Rgb;
59
+ /**
60
+ * The first colour in a computed `background-image` (a gradient's first stop): the best guess at what text over a gradient sits
61
+ * on. `undefined` for an image (`url(...)`) or anything with no colour in it.
62
+ */
63
+ export declare function firstColourStop(backgroundImage: string): Rgba | undefined;