@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
@@ -1,19 +1,78 @@
1
- import React from "react";
2
- import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
1
+ import React, { ReactNode } from "react";
2
+ import type { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
3
+ /** The attribute that marks the elements React fills in (`{USER_MENU}`, `{APP_TITLE}`). Stripped from the author's own HTML, so only
4
+ * this module's placeholders can carry it. */
5
+ export declare const SLOT_ATTRIBUTE = "data-rr-slot";
6
+ /** The variables a branding header or footer may contain, written in the HTML's text. */
7
+ export declare const USER_MENU_VARIABLE = "{USER_MENU}";
8
+ export declare const APP_TITLE_VARIABLE = "{APP_TITLE}";
3
9
  /**
4
10
  * Sanitizes admin-configured branding HTML before it's rendered. The server sanitizes on save too; this also
5
11
  * covers anything stored before that. Returns `""` where there's no DOM to sanitize with (server-side
6
12
  * rendering), so unsanitized HTML is never emitted.
7
13
  */
8
14
  export declare function sanitizeBrandingHtml(html: string): string;
15
+ /** Branding HTML ready to render: sanitized, with its variables replaced by placeholder elements for React to fill. */
16
+ export interface ParsedBrandingHtml {
17
+ html: string;
18
+ /** It contains a `{USER_MENU}` where the account menu can be rendered. */
19
+ hasUserMenu: boolean;
20
+ }
9
21
  /**
10
- * Renders the admin-configured `Branding.headerHtml`/`footerHtml` (see `useBranding()`), sanitized. The webmail
11
- * chrome (`AppShell`) shows both; the admin console shows only the footer - its header is the console's own - and the
12
- * escrow console deliberately renders no branding HTML at all.
22
+ * Sanitizes `html` (the same rules as `sanitizeBrandingHtml()`) and swaps the variables in its **text** for placeholder elements:
23
+ * `{USER_MENU}` becomes the slot the account menu is rendered into, `{APP_TITLE}` a slot holding the current app's name.
24
+ *
25
+ * - Only text is searched, after sanitizing - a variable in an attribute (`title="{USER_MENU}"`), a comment, or a `<script>` (which
26
+ * is gone) is never touched, and the author cannot write a placeholder themselves (`data-rr-slot` is stripped).
27
+ * - `{USER_MENU}` is replaced **once**, at its first place in the document; a second (or third) one is removed, so there is never
28
+ * more than one account menu (two would give two sets of duplicate ids and focus targets). One inside a link, or in text that
29
+ * can't hold an element (`<title>`, `<textarea>`), is removed too: a menu inside an `<a>` would navigate when clicked.
30
+ * - `{APP_TITLE}` is replaced everywhere it appears. Inside `<title>`-like raw text it stays as written.
31
+ * - Nothing is matched loosely: the names are upper-case, in braces, with no spaces.
32
+ *
33
+ * `null` when there is nothing to show (no DOM to sanitize with - server-side rendering - or the HTML is empty once sanitized).
13
34
  */
14
- export declare function BrandingHeader({ branding }: {
15
- branding: Branding | null;
16
- }): React.JSX.Element;
17
- export declare function BrandingFooter({ branding }: {
35
+ export declare function parseBrandingHtml(html: string): ParsedBrandingHtml | null;
36
+ /**
37
+ * `parseBrandingHtml()` of `html`, done once the component is on the page (never while rendering: the server has no DOM to sanitize
38
+ * with, and rendering different HTML there than the hydrating client would leave the page without it). `null` when there is no HTML,
39
+ * or nothing is left of it; `undefined` while it has not been parsed yet - the frame reserves the header's place meanwhile.
40
+ */
41
+ export declare function useBrandingHtml(html: string | undefined): ParsedBrandingHtml | null | undefined;
42
+ export interface FrameBrandingHeaderProps {
43
+ /** `useBrandingHtml(branding.headerHtml)`. `null` renders nothing. */
44
+ parsed: ParsedBrandingHtml | null | undefined;
45
+ /** The account menu, rendered into the header's `{USER_MENU}` (when it has one). */
46
+ userMenu?: ReactNode;
47
+ /** Rendered at the right end of the header when it has no `{USER_MENU}` and the footer doesn't host the menu either - the account menu is never lost. */
48
+ fallbackMenu?: ReactNode;
49
+ appTitle?: string;
50
+ }
51
+ /**
52
+ * The frame's version of the admin-configured `Branding.headerHtml` (see `useBranding()`), sanitized, in a `<header>` that stays at the top of the window as the
53
+ * page scrolls. When it is there the app has no title bar of its own (`AppChrome`): the header is the top of the app, and the account
54
+ * menu lives in it - where the HTML writes `{USER_MENU}`, else in a small cell at its right end. The webmail chrome (`AppShell`) shows
55
+ * it; the admin console has no custom header (its header is the console's own) and the escrow console renders no branding HTML at all.
56
+ */
57
+ export declare function FrameBrandingHeader({ parsed, userMenu, fallbackMenu, appTitle }: FrameBrandingHeaderProps): React.JSX.Element | null;
58
+ export interface FrameBrandingFooterProps {
59
+ /** `useBrandingHtml(branding.footerHtml)`. */
60
+ parsed: ParsedBrandingHtml | null | undefined;
61
+ /** Rendered into the footer's `{USER_MENU}` - passed only when the header has none. */
62
+ userMenu?: ReactNode;
63
+ appTitle?: string;
64
+ }
65
+ /** The frame's version of the admin-configured `Branding.footerHtml`, sanitized. Its variables work as in the header; a `{USER_MENU}` here only takes the menu when the header has none. */
66
+ export declare function FrameBrandingFooter({ parsed, userMenu, appTitle }: FrameBrandingFooterProps): React.JSX.Element;
67
+ export interface BrandingChromeProps {
18
68
  branding: Branding | null;
19
- }): React.JSX.Element;
69
+ /** What `{APP_TITLE}` is replaced with - the page's own name. Nothing when absent. */
70
+ appTitle?: string;
71
+ }
72
+ /**
73
+ * The admin-configured `Branding.headerHtml`, sanitized, for pages that don't use a shell, such as public pages (the booking pages). There is no
74
+ * account menu there, so a `{USER_MENU}` in the HTML renders nothing. Renders nothing without branding or HTML.
75
+ */
76
+ export declare function BrandingHeader({ branding, appTitle }: BrandingChromeProps): React.JSX.Element;
77
+ /** The admin-configured `Branding.footerHtml`, sanitized, for pages that don't use a shell - see `BrandingHeader`. */
78
+ export declare function BrandingFooter({ branding, appTitle }: BrandingChromeProps): React.JSX.Element;
@@ -1,5 +1,12 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
7
+ import { createPortal } from "react-dom";
2
8
  import DOMPurify from "dompurify";
9
+ import { useHeaderHeightRef } from "../../notifications/headerOffset.js";
3
10
  /** Elements that could run script, restyle the whole page, collect input, or embed another page - stripped on
4
11
  * top of DOMPurify's defaults (which already drop `<script>`, `on*` handlers, and `javascript:` URLs). */
5
12
  const FORBIDDEN_TAGS = [
@@ -23,6 +30,15 @@ const FORBIDDEN_TAGS = [
23
30
  "math",
24
31
  ];
25
32
  const FORBIDDEN_ATTRS = ["action", "formaction", "srcdoc"];
33
+ /** The attribute that marks the elements React fills in (`{USER_MENU}`, `{APP_TITLE}`). Stripped from the author's own HTML, so only
34
+ * this module's placeholders can carry it. */
35
+ export const SLOT_ATTRIBUTE = "data-rr-slot";
36
+ /** The variables a branding header or footer may contain, written in the HTML's text. */
37
+ export const USER_MENU_VARIABLE = "{USER_MENU}";
38
+ export const APP_TITLE_VARIABLE = "{APP_TITLE}";
39
+ const VARIABLE = /\{(USER_MENU|APP_TITLE)\}/g;
40
+ /** Elements whose text is not markup a placeholder element could live in (raw text and RCDATA), and the ones a menu must not sit inside. */
41
+ const RAW_TEXT_PARENTS = new Set(["TITLE", "TEXTAREA", "OPTION", "STYLE", "SCRIPT", "NOSCRIPT", "XMP", "IFRAME", "NOEMBED", "NOFRAMES", "PLAINTEXT"]);
26
42
  /**
27
43
  * Sanitizes admin-configured branding HTML before it's rendered. The server sanitizes on save too; this also
28
44
  * covers anything stored before that. Returns `""` where there's no DOM to sanitize with (server-side
@@ -34,21 +50,133 @@ export function sanitizeBrandingHtml(html) {
34
50
  }
35
51
  return DOMPurify.sanitize(html, { FORBID_TAGS: FORBIDDEN_TAGS, FORBID_ATTR: FORBIDDEN_ATTRS });
36
52
  }
37
- function BrandingHtml({ html }) {
38
- const clean = html ? sanitizeBrandingHtml(html) : "";
39
- if (!clean) {
53
+ function slotElement(name) {
54
+ const span = document.createElement("span");
55
+ span.setAttribute(SLOT_ATTRIBUTE, name);
56
+ return span;
57
+ }
58
+ /**
59
+ * Sanitizes `html` (the same rules as `sanitizeBrandingHtml()`) and swaps the variables in its **text** for placeholder elements:
60
+ * `{USER_MENU}` becomes the slot the account menu is rendered into, `{APP_TITLE}` a slot holding the current app's name.
61
+ *
62
+ * - Only text is searched, after sanitizing - a variable in an attribute (`title="{USER_MENU}"`), a comment, or a `<script>` (which
63
+ * is gone) is never touched, and the author cannot write a placeholder themselves (`data-rr-slot` is stripped).
64
+ * - `{USER_MENU}` is replaced **once**, at its first place in the document; a second (or third) one is removed, so there is never
65
+ * more than one account menu (two would give two sets of duplicate ids and focus targets). One inside a link, or in text that
66
+ * can't hold an element (`<title>`, `<textarea>`), is removed too: a menu inside an `<a>` would navigate when clicked.
67
+ * - `{APP_TITLE}` is replaced everywhere it appears. Inside `<title>`-like raw text it stays as written.
68
+ * - Nothing is matched loosely: the names are upper-case, in braces, with no spaces.
69
+ *
70
+ * `null` when there is nothing to show (no DOM to sanitize with - server-side rendering - or the HTML is empty once sanitized).
71
+ */
72
+ export function parseBrandingHtml(html) {
73
+ if (!DOMPurify.isSupported) {
74
+ return null;
75
+ }
76
+ const fragment = DOMPurify.sanitize(html, {
77
+ FORBID_TAGS: FORBIDDEN_TAGS,
78
+ FORBID_ATTR: [...FORBIDDEN_ATTRS, SLOT_ATTRIBUTE],
79
+ RETURN_DOM_FRAGMENT: true,
80
+ });
81
+ const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_TEXT);
82
+ const texts = [];
83
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
84
+ texts.push(node);
85
+ }
86
+ let hasUserMenu = false;
87
+ for (const text of texts) {
88
+ const parent = text.parentElement;
89
+ if (parent && RAW_TEXT_PARENTS.has(parent.tagName)) {
90
+ continue;
91
+ }
92
+ const inLink = !!parent?.closest("a");
93
+ // `split` with a capturing group alternates text and variable names: [text, name, text, name, ..., text].
94
+ const pieces = text.data.split(VARIABLE);
95
+ if (pieces.length === 1) {
96
+ continue;
97
+ }
98
+ const replacement = document.createDocumentFragment();
99
+ pieces.forEach((piece, index) => {
100
+ if (index % 2 === 0) {
101
+ if (piece) {
102
+ replacement.appendChild(document.createTextNode(piece));
103
+ }
104
+ }
105
+ else if (piece === "APP_TITLE") {
106
+ replacement.appendChild(slotElement("app-title"));
107
+ }
108
+ else if (!hasUserMenu && !inLink) {
109
+ hasUserMenu = true;
110
+ replacement.appendChild(slotElement("user-menu"));
111
+ }
112
+ });
113
+ text.replaceWith(replacement);
114
+ }
115
+ const holder = document.createElement("div");
116
+ holder.appendChild(fragment);
117
+ const result = holder.innerHTML;
118
+ return result.trim() ? { html: result, hasUserMenu } : null;
119
+ }
120
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
121
+ /**
122
+ * `parseBrandingHtml()` of `html`, done once the component is on the page (never while rendering: the server has no DOM to sanitize
123
+ * with, and rendering different HTML there than the hydrating client would leave the page without it). `null` when there is no HTML,
124
+ * or nothing is left of it; `undefined` while it has not been parsed yet - the frame reserves the header's place meanwhile.
125
+ */
126
+ export function useBrandingHtml(html) {
127
+ const source = html?.trim() ? html : undefined;
128
+ const [parsed, setParsed] = useState(undefined);
129
+ useIsomorphicLayoutEffect(() => {
130
+ setParsed(source === undefined ? undefined : { source, value: parseBrandingHtml(source) });
131
+ }, [source]);
132
+ if (source === undefined) {
133
+ return null;
134
+ }
135
+ return parsed?.source === source ? parsed.value : undefined;
136
+ }
137
+ function BrandingHtml({ parsed, userMenu, hostMenu, appTitle, className }) {
138
+ const ref = useRef(null);
139
+ const [targets, setTargets] = useState({ menu: null, titles: [] });
140
+ const html = parsed?.html ?? "";
141
+ // The placeholders exist once React has set the HTML: find them here, before the browser paints, and portal into them.
142
+ useIsomorphicLayoutEffect(() => {
143
+ const root = ref.current;
144
+ setTargets({
145
+ menu: root?.querySelector(`[${SLOT_ATTRIBUTE}="user-menu"]`) ?? null,
146
+ titles: root ? Array.from(root.querySelectorAll(`[${SLOT_ATTRIBUTE}="app-title"]`)) : [],
147
+ });
148
+ }, [html]);
149
+ if (parsed === null) {
150
+ return null;
151
+ }
152
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ref: ref, className: className, dangerouslySetInnerHTML: { __html: html } }), hostMenu && targets.menu && createPortal(userMenu, targets.menu), targets.titles.map((element, index) => createPortal(appTitle, element, `app-title-${index}`))] }));
153
+ }
154
+ /**
155
+ * The frame's version of the admin-configured `Branding.headerHtml` (see `useBranding()`), sanitized, in a `<header>` that stays at the top of the window as the
156
+ * page scrolls. When it is there the app has no title bar of its own (`AppChrome`): the header is the top of the app, and the account
157
+ * menu lives in it - where the HTML writes `{USER_MENU}`, else in a small cell at its right end. The webmail chrome (`AppShell`) shows
158
+ * it; the admin console has no custom header (its header is the console's own) and the escrow console renders no branding HTML at all.
159
+ */
160
+ export function FrameBrandingHeader({ parsed, userMenu, fallbackMenu, appTitle }) {
161
+ // Publishes the header's height (`--rr-header-h`) for the pop-up stack, which sticks just below it.
162
+ const headerRef = useHeaderHeightRef();
163
+ if (parsed === null) {
40
164
  return null;
41
165
  }
42
- return _jsx("div", { dangerouslySetInnerHTML: { __html: clean } });
166
+ return (_jsxs("header", { ref: headerRef, className: "sticky top-0 z-30 shrink-0 flex items-stretch bg-surface-alt", "data-branding-header": "", children: [_jsx(BrandingHtml, { parsed: parsed, userMenu: userMenu, hostMenu: parsed?.hasUserMenu, appTitle: appTitle, className: "flex-1 min-w-0" }), parsed && !parsed.hasUserMenu && fallbackMenu ? (_jsx("div", { className: "shrink-0 flex items-center px-3 bg-surface border-l border-border", "data-branding-menu-cell": "", children: fallbackMenu })) : null] }));
167
+ }
168
+ /** The frame's version of the admin-configured `Branding.footerHtml`, sanitized. Its variables work as in the header; a `{USER_MENU}` here only takes the menu when the header has none. */
169
+ export function FrameBrandingFooter({ parsed, userMenu, appTitle }) {
170
+ return _jsx(BrandingHtml, { parsed: parsed, userMenu: userMenu, hostMenu: userMenu !== undefined, appTitle: appTitle });
43
171
  }
44
172
  /**
45
- * Renders the admin-configured `Branding.headerHtml`/`footerHtml` (see `useBranding()`), sanitized. The webmail
46
- * chrome (`AppShell`) shows both; the admin console shows only the footer - its header is the console's own - and the
47
- * escrow console deliberately renders no branding HTML at all.
173
+ * The admin-configured `Branding.headerHtml`, sanitized, for pages that don't use a shell, such as public pages (the booking pages). There is no
174
+ * account menu there, so a `{USER_MENU}` in the HTML renders nothing. Renders nothing without branding or HTML.
48
175
  */
49
- export function BrandingHeader({ branding }) {
50
- return _jsx(BrandingHtml, { html: branding?.headerHtml });
176
+ export function BrandingHeader({ branding, appTitle }) {
177
+ return _jsx(BrandingHtml, { parsed: useBrandingHtml(branding?.headerHtml), appTitle: appTitle });
51
178
  }
52
- export function BrandingFooter({ branding }) {
53
- return _jsx(BrandingHtml, { html: branding?.footerHtml });
179
+ /** The admin-configured `Branding.footerHtml`, sanitized, for pages that don't use a shell - see `BrandingHeader`. */
180
+ export function BrandingFooter({ branding, appTitle }) {
181
+ return _jsx(BrandingHtml, { parsed: useBrandingHtml(branding?.footerHtml), appTitle: appTitle });
54
182
  }
@@ -10,6 +10,7 @@ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
10
10
  import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
11
11
  import { enrollKey, getKeyVault, VaultAlreadyInitializedError } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
12
12
  import { UnopenableKeysNotice, unlockErrorMessage } from "./UnlockPromptProvider.js";
13
+ import { FrameTakeover } from "../../navigation/frameContext.js";
13
14
  import { RecoveryFollowUpModal, UnlockModeToggle, startRecoveryUnlock } from "./RecoveryCodeUnlock.js";
14
15
  import { ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE, getUnlockedKeys, unlockWithPassword, } from "@rapidmx/react-shared/crypto/keySession.js";
15
16
  import { buildAad, generateMasterKey, sealWithKey } from "@rapidmx/react-shared/crypto/masterKey.js";
@@ -170,22 +171,22 @@ export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxK
170
171
  }
171
172
  if ((status === "unlock" || status === "unlocking") && blocking) {
172
173
  const unlocking = status === "unlocking";
173
- return (_jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Unlock your mailbox" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: unlockMode === "password"
174
- ? "Enter your encryption password to unlock signing and reading protected mail this session."
175
- : "Enter one of your recovery codes to unlock signing and reading protected mail this session. Each code works once." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleUnlock, children: [unlockMode === "password" ? (_jsx(FormField, { label: "Encryption password", htmlFor: "key-unlock-password", children: _jsx("input", { id: "key-unlock-password", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: password, onChange: (e) => setPassword(e.target.value), disabled: unlocking, autoComplete: "current-password" }) })) : (_jsx(FormField, { label: "Recovery code", htmlFor: "key-unlock-recovery-code", children: _jsx("input", { id: "key-unlock-recovery-code", type: "text", className: "w-full text-sm font-mono border border-border rounded-sm py-1.5 px-2 bg-surface", value: recoveryCode, onChange: (e) => setRecoveryCode(e.target.value), disabled: unlocking, autoComplete: "off", autoCapitalize: "characters", spellCheck: false }) })), _jsx(Button, { type: "submit", loading: unlocking, disabled: unlocking, children: "Unlock" })] }), _jsx("div", { className: "mt-3", children: _jsx(UnlockModeToggle, { mode: unlockMode, disabled: unlocking, onChange: (next) => {
176
- setUnlockMode(next);
177
- setError(null);
178
- } }) })] }) }));
174
+ return (_jsx(FrameTakeover, { children: _jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Unlock your mailbox" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: unlockMode === "password"
175
+ ? "Enter your encryption password to unlock signing and reading protected mail this session."
176
+ : "Enter one of your recovery codes to unlock signing and reading protected mail this session. Each code works once." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleUnlock, children: [unlockMode === "password" ? (_jsx(FormField, { label: "Encryption password", htmlFor: "key-unlock-password", children: _jsx("input", { id: "key-unlock-password", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: password, onChange: (e) => setPassword(e.target.value), disabled: unlocking, autoComplete: "current-password" }) })) : (_jsx(FormField, { label: "Recovery code", htmlFor: "key-unlock-recovery-code", children: _jsx("input", { id: "key-unlock-recovery-code", type: "text", className: "w-full text-sm font-mono border border-border rounded-sm py-1.5 px-2 bg-surface", value: recoveryCode, onChange: (e) => setRecoveryCode(e.target.value), disabled: unlocking, autoComplete: "off", autoCapitalize: "characters", spellCheck: false }) })), _jsx(Button, { type: "submit", loading: unlocking, disabled: unlocking, children: "Unlock" })] }), _jsx("div", { className: "mt-3", children: _jsx(UnlockModeToggle, { mode: unlockMode, disabled: unlocking, onChange: (next) => {
177
+ setUnlockMode(next);
178
+ setError(null);
179
+ } }) })] }) }) }));
179
180
  }
180
181
  if (status === "setup_password" || status === "enrolling") {
181
182
  const enrolling = status === "enrolling";
182
- return (_jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Protect your mailbox" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Choose a password to protect your encryption keys. This is separate from your sign-in password and is never sent to the server." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSetPassword, children: [_jsx(FormField, { label: "Encryption password", htmlFor: "key-enrollment-password", children: _jsx("input", { id: "key-enrollment-password", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: password, onChange: (e) => setPassword(e.target.value), disabled: enrolling, autoComplete: "new-password" }) }), _jsx(FormField, { label: "Confirm password", htmlFor: "key-enrollment-password-confirm", children: _jsx("input", { id: "key-enrollment-password-confirm", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), disabled: enrolling, autoComplete: "new-password" }) }), _jsx(Button, { type: "submit", loading: enrolling, disabled: enrolling, children: "Continue" })] })] }) }));
183
+ return (_jsx(FrameTakeover, { children: _jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Protect your mailbox" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Choose a password to protect your encryption keys. This is separate from your sign-in password and is never sent to the server." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSetPassword, children: [_jsx(FormField, { label: "Encryption password", htmlFor: "key-enrollment-password", children: _jsx("input", { id: "key-enrollment-password", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: password, onChange: (e) => setPassword(e.target.value), disabled: enrolling, autoComplete: "new-password" }) }), _jsx(FormField, { label: "Confirm password", htmlFor: "key-enrollment-password-confirm", children: _jsx("input", { id: "key-enrollment-password-confirm", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), disabled: enrolling, autoComplete: "new-password" }) }), _jsx(Button, { type: "submit", loading: enrolling, disabled: enrolling, children: "Continue" })] })] }) }) }));
183
184
  }
184
185
  if (status === "already_set_up") {
185
- return (_jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Encryption is already set up" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "This mailbox\u2019s encryption was set up in another tab or on another device while this page was open, so nothing was changed here. Reload the page and unlock with the password chosen there." }), _jsx(Button, { type: "button", onClick: () => window.location.reload(), children: "Reload" })] }) }));
186
+ return (_jsx(FrameTakeover, { children: _jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Encryption is already set up" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "This mailbox\u2019s encryption was set up in another tab or on another device while this page was open, so nothing was changed here. Reload the page and unlock with the password chosen there." }), _jsx(Button, { type: "button", onClick: () => window.location.reload(), children: "Reload" })] }) }) }));
186
187
  }
187
188
  if (status === "show_recovery_codes") {
188
- return (_jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Save your recovery codes" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "If you lose your password, these codes are the only way to recover your encrypted mail. Each code can be used once. Store them somewhere safe \u2014 they will not be shown again." }), _jsx("ul", { className: "grid grid-cols-2 gap-2 mb-3 font-mono text-sm", children: recoveryCodes.map((code) => (_jsx("li", { className: "bg-surface-alt rounded-sm py-1.5 px-2 text-center", children: code }, code))) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mb-5", onClick: handleCopyCodes, children: codesCopied ? "Copied" : "Copy codes to clipboard" }), _jsxs("label", { className: "flex items-center gap-2 text-sm mb-4", children: [_jsx("input", { type: "checkbox", checked: codesSaved, onChange: (e) => setCodesSaved(e.target.checked) }), "I have saved these recovery codes in a safe place."] }), _jsx(Button, { type: "button", disabled: !codesSaved, onClick: () => setStatus("ready"), children: "Continue" })] }) }));
189
+ return (_jsx(FrameTakeover, { children: _jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Save your recovery codes" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "If you lose your password, these codes are the only way to recover your encrypted mail. Each code can be used once. Store them somewhere safe \u2014 they will not be shown again." }), _jsx("ul", { className: "grid grid-cols-2 gap-2 mb-3 font-mono text-sm", children: recoveryCodes.map((code) => (_jsx("li", { className: "bg-surface-alt rounded-sm py-1.5 px-2 text-center", children: code }, code))) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mb-5", onClick: handleCopyCodes, children: codesCopied ? "Copied" : "Copy codes to clipboard" }), _jsxs("label", { className: "flex items-center gap-2 text-sm mb-4", children: [_jsx("input", { type: "checkbox", checked: codesSaved, onChange: (e) => setCodesSaved(e.target.checked) }), "I have saved these recovery codes in a safe place."] }), _jsx(Button, { type: "button", disabled: !codesSaved, onClick: () => setStatus("ready"), children: "Continue" })] }) }) }));
189
190
  }
190
191
  return (_jsxs(_Fragment, { children: [children, unopenableKeys && _jsx(UnopenableKeysNotice, { fingerprints: unopenableKeys, onDismiss: () => setUnopenableKeys(null) }), followUp && _jsx(RecoveryFollowUpModal, { followUp: followUp, onDone: () => setFollowUp(null) })] }));
191
192
  }
@@ -8,6 +8,7 @@ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
8
  import { autoProvisionMailbox } from "@rapidmx/react-shared/mail/mailApi.js";
9
9
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
10
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
11
+ import { FrameTakeover } from "../../navigation/frameContext.js";
11
12
  /** What the "Retry" screen says: the server couldn't read its provisioning policy (503) ... */
12
13
  const RETRY_POLICY_TEXT = "We couldn\u2019t check whether a mailbox can be set up for you. Please try again.";
13
14
  /** ... or couldn't reach the identity service (auth-server) it looks the caller's username up in (502). */
@@ -114,5 +115,6 @@ export default function MailboxProvisioning() {
114
115
  else {
115
116
  content = (_jsxs(_Fragment, { children: [_jsx("h1", { className: "text-lg font-bold uppercase tracking-wide", children: "No mailbox available" }), reason && _jsx("p", { className: "text-sm", children: reason }), _jsx("p", { className: "text-sm text-text-muted", children: "Ask an administrator to create one for you." })] }));
116
117
  }
117
- return (_jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8 flex flex-col items-center gap-4 text-center", children: [_jsx("img", { src: "/images/wordmark.png", height: "128", alt: "" }), content] }) }));
118
+ // Inside the client-side router's persistent frame this hides the frame's rail and header, as replacing the whole shell used to.
119
+ return (_jsx(FrameTakeover, { children: _jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8 flex flex-col items-center gap-4 text-center", children: [_jsx("img", { src: "/images/wordmark.png", height: "128", alt: "" }), content] }) }) }));
118
120
  }
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ /** The number of rows at the top of `size` x `size` RGBA pixel data that are empty - the icon's own margin above its first visible pixel. */
3
+ export declare function emptyRowsAtTop(rgba: ArrayLike<number>, size: number): number;
4
+ /** Whether this environment can decode an image and read it back - not a server, and not jsdom, where there is nothing to measure. */
5
+ export declare function canMeasureImages(): boolean;
6
+ /**
7
+ * How many pixels of empty margin an icon has above its first visible pixel when fitted (`object-fit: contain`) into a `SIZE` square.
8
+ * Drawn on a canvas and read back, so it works for any picture the browser can decode from the same origin (or with CORS); `0` where
9
+ * that isn't possible (no canvas, a picture that won't load, one from another origin that taints the canvas).
10
+ */
11
+ export declare function measureTopMargin(src: string): Promise<number>;
12
+ /**
13
+ * The deployment's icon at the top of an app rail (`AppShell`, `AdminShell`, `EscrowShell`): a block as wide as the rail and as tall as the
14
+ * title bar beside it (4rem), flush with the top and left edges of the window - no padding above it - with the bar's own bottom border
15
+ * continued under it, so the two read as one block. The picture is fitted inside the block, never cropped, and raised by whatever empty
16
+ * margin the file itself has above its artwork, so the first visible pixel is at the very top of the window (a logo exported with 6% of
17
+ * air around it would otherwise still leave a gap). The icon is shown once measured (a few milliseconds after it loads).
18
+ */
19
+ export default function RailIcon({ src }: {
20
+ src: string;
21
+ }): React.JSX.Element;
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useState } from "react";
7
+ /** The side, in CSS pixels, of the square the icon is fitted into and measured in: the rail is 4rem wide and the title bar 4rem tall, each less its 1px border. */
8
+ const SIZE = 63;
9
+ /** Pixels this transparent or less count as empty. */
10
+ const EMPTY_ALPHA = 24;
11
+ /** The number of rows at the top of `size` x `size` RGBA pixel data that are empty - the icon's own margin above its first visible pixel. */
12
+ export function emptyRowsAtTop(rgba, size) {
13
+ for (let y = 0; y < size; y++) {
14
+ for (let x = 0; x < size; x++) {
15
+ if (rgba[(y * size + x) * 4 + 3] > EMPTY_ALPHA) {
16
+ return y;
17
+ }
18
+ }
19
+ }
20
+ return 0;
21
+ }
22
+ /** Whether this environment can decode an image and read it back - not a server, and not jsdom, where there is nothing to measure. */
23
+ export function canMeasureImages() {
24
+ try {
25
+ return typeof new Image().decode === "function";
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ /**
32
+ * How many pixels of empty margin an icon has above its first visible pixel when fitted (`object-fit: contain`) into a `SIZE` square.
33
+ * Drawn on a canvas and read back, so it works for any picture the browser can decode from the same origin (or with CORS); `0` where
34
+ * that isn't possible (no canvas, a picture that won't load, one from another origin that taints the canvas).
35
+ */
36
+ export async function measureTopMargin(src) {
37
+ try {
38
+ const image = new Image();
39
+ image.src = src;
40
+ await image.decode();
41
+ const canvas = document.createElement("canvas");
42
+ canvas.width = SIZE;
43
+ canvas.height = SIZE;
44
+ const context = canvas.getContext("2d", { willReadFrequently: true });
45
+ if (!context || !image.naturalWidth || !image.naturalHeight) {
46
+ return 0;
47
+ }
48
+ const scale = Math.min(SIZE / image.naturalWidth, SIZE / image.naturalHeight);
49
+ const width = image.naturalWidth * scale;
50
+ const height = image.naturalHeight * scale;
51
+ context.drawImage(image, (SIZE - width) / 2, (SIZE - height) / 2, width, height);
52
+ return emptyRowsAtTop(context.getImageData(0, 0, SIZE, SIZE).data, SIZE);
53
+ }
54
+ catch {
55
+ return 0;
56
+ }
57
+ }
58
+ /**
59
+ * The deployment's icon at the top of an app rail (`AppShell`, `AdminShell`, `EscrowShell`): a block as wide as the rail and as tall as the
60
+ * title bar beside it (4rem), flush with the top and left edges of the window - no padding above it - with the bar's own bottom border
61
+ * continued under it, so the two read as one block. The picture is fitted inside the block, never cropped, and raised by whatever empty
62
+ * margin the file itself has above its artwork, so the first visible pixel is at the very top of the window (a logo exported with 6% of
63
+ * air around it would otherwise still leave a gap). The icon is shown once measured (a few milliseconds after it loads).
64
+ */
65
+ export default function RailIcon({ src }) {
66
+ const [margin, setMargin] = useState(null);
67
+ useEffect(() => {
68
+ let cancelled = false;
69
+ if (!canMeasureImages()) {
70
+ // Nothing to measure with: show the icon as it is (in the same effect pass, so nothing waits and nothing updates later).
71
+ setMargin(0);
72
+ return;
73
+ }
74
+ setMargin(null);
75
+ void measureTopMargin(src).then((measured) => {
76
+ if (!cancelled) {
77
+ setMargin(measured);
78
+ }
79
+ });
80
+ return () => {
81
+ cancelled = true;
82
+ };
83
+ }, [src]);
84
+ return (_jsx("div", { className: "h-16 w-full shrink-0 overflow-hidden border-b border-border mb-2", "data-rail-icon": "", children: _jsx("img", { src: src, width: "64", height: "64", alt: "", className: "block h-full w-full object-contain", style: { transform: margin ? `translateY(-${margin}px)` : undefined, opacity: margin === null ? 0 : 1 } }) }));
85
+ }
@@ -0,0 +1,55 @@
1
+ import React, { ReactNode } from "react";
2
+ import type { IconType } from "react-icons";
3
+ /** One button of a `ResponsiveToolbar`. */
4
+ export interface ToolbarAction {
5
+ id: string;
6
+ /** The visible caption while there is room, else the tooltip and the accessible name. */
7
+ label: string;
8
+ icon: IconType;
9
+ onClick: () => void;
10
+ disabled?: boolean;
11
+ /** A toggle: the button's pressed state. */
12
+ pressed?: boolean;
13
+ /** `title` and `aria-keyshortcuts` for a button with a shortcut - kept as given, whatever the layout. */
14
+ hint?: {
15
+ title: string;
16
+ "aria-keyshortcuts"?: string;
17
+ };
18
+ /** Actions with the same `group` sit together; a divider goes between groups. */
19
+ group: number;
20
+ /** Which goes into the "More" menu first when there is not room: the lowest number first. Never used for `essential`. */
21
+ rank: number;
22
+ /** Never moves into the "More" menu (the primary action). */
23
+ essential?: boolean;
24
+ }
25
+ /** Room (px) each kind of button takes, and what the bar spends on its padding, a divider and the "More" button - the layout is computed from them
26
+ * rather than measured, so the first paint on the client is already right and nothing jumps. */
27
+ export declare const LABELLED_BUTTON_WIDTH = 64;
28
+ export declare const ICON_BUTTON_WIDTH = 36;
29
+ export interface ToolbarLayout {
30
+ /** Captions under the icons. */
31
+ labels: boolean;
32
+ /** The ids of the actions shown as buttons, in order. The rest are in the "More" menu. */
33
+ visible: string[];
34
+ }
35
+ /**
36
+ * Which buttons a bar of `width` px shows, and how: every one with its caption when that fits; else every one as an icon (the captions go first);
37
+ * else as many icons as fit, the least important (`rank`) going into a "More" menu one at a time - the essential ones stay whatever the width.
38
+ * Pure, so it is the same on the server and the client for a given width; `width === undefined` (not measured yet, no ResizeObserver) shows
39
+ * everything.
40
+ */
41
+ export declare function layoutToolbar(actions: ToolbarAction[], width: number | undefined): ToolbarLayout;
42
+ /** The width of `ref`'s element, kept current with a ResizeObserver; `undefined` until measured (and where there is no ResizeObserver). */
43
+ export declare function useElementWidth(ref: React.RefObject<HTMLElement | null>): number | undefined;
44
+ /**
45
+ * A toolbar that fits the column it is in - never wider, never over another pane. It measures its own width (`ResizeObserver`) and shows every
46
+ * action with a caption when there is room; when there is not, the captions go first (each button keeps its tooltip and accessible name), then the
47
+ * least important actions move into a "More" menu (`layoutToolbar()`). Arrow keys, Home and End move between the buttons; the menu is a real
48
+ * `role="menu"`. `children` (a hidden file input, say) are rendered inside the toolbar's element but take no room.
49
+ */
50
+ export default function ResponsiveToolbar({ label, actions, leading, children }: {
51
+ label: string;
52
+ actions: ToolbarAction[];
53
+ leading?: ReactNode;
54
+ children?: ReactNode;
55
+ }): React.JSX.Element;