@rapidmx/web-client 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. package/README.md +183 -2
  2. package/apps/admin/_layout.tsx +8 -2
  3. package/apps/admin/index.tsx +7 -1
  4. package/apps/admin/ingest-queue/index.tsx +2 -1
  5. package/apps/admin/mailboxes/[uid].tsx +23 -10
  6. package/apps/admin/mailboxes/new/index.tsx +3 -3
  7. package/apps/admin/quarantine/index.tsx +2 -1
  8. package/apps/admin/signing-certificates/index.tsx +343 -0
  9. package/apps/escrow/_layout.tsx +8 -2
  10. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  11. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  12. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  13. package/apps/shared/appearance/ColorField.tsx +107 -0
  14. package/apps/shared/appearance/appearanceCache.ts +106 -0
  15. package/apps/shared/appearance/appearanceContext.ts +77 -0
  16. package/apps/shared/appearance/bootScript.ts +35 -0
  17. package/apps/shared/appearance/color.ts +169 -0
  18. package/apps/shared/appearance/photo.ts +88 -0
  19. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  20. package/apps/shared/appearance/theme.ts +355 -0
  21. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +4 -4
  22. package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
  23. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
  25. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +60 -14
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +65 -16
  27. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  28. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  29. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  30. package/apps/shared/components/contacts/ContactForm.tsx +11 -5
  31. package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
  32. package/apps/shared/components/contacts/ContactsToolbar.tsx +25 -52
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +10 -5
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +98 -32
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  38. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  39. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  40. package/apps/shared/components/layout/UserMenu.tsx +75 -6
  41. package/apps/shared/components/mail/ConversationList.tsx +4 -2
  42. package/apps/shared/components/mail/ConversationThreadPane.tsx +487 -475
  43. package/apps/shared/components/mail/LazyReadingPane.tsx +14 -16
  44. package/apps/shared/components/mail/MailSelectionBar.tsx +2 -10
  45. package/apps/shared/components/mail/MessageDetailPane.tsx +366 -287
  46. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  47. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  48. package/apps/shared/components/mail/compose/ComposeContext.tsx +35 -2
  49. package/apps/shared/components/mail/compose/ComposeWindow.tsx +239 -437
  50. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  51. package/apps/shared/components/mail/layout/MailShell.tsx +39 -25
  52. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  53. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  54. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  55. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  56. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  57. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  58. package/apps/shared/components/mail/reading/color.ts +245 -0
  59. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  60. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  61. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  62. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  63. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  64. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  65. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  66. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  67. package/apps/shared/components/sharing/PrincipalListField.tsx +82 -0
  68. package/apps/shared/components/sharing/PrincipalPicker.tsx +76 -0
  69. package/apps/shared/components/sharing/PrincipalResolver.tsx +164 -0
  70. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  71. package/apps/shared/components/tasks/layout/TasksShell.tsx +9 -5
  72. package/apps/shared/mail/folderCounts.ts +9 -3
  73. package/apps/shared/mail/folderTree.ts +143 -0
  74. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  75. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  76. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  77. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  78. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  79. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  80. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  81. package/apps/shared/mail/outbox/sendState.ts +35 -0
  82. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  83. package/apps/shared/mail/useMailConnection.ts +81 -28
  84. package/apps/shared/mail/useMailLiveUpdates.ts +55 -10
  85. package/apps/shared/mail/useNewMailNotifications.ts +47 -32
  86. package/apps/shared/navigation/AppRouter.tsx +2 -2
  87. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  88. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  89. package/apps/shared/notifications/apiErrors.ts +98 -0
  90. package/apps/shared/notifications/headerOffset.ts +35 -0
  91. package/apps/shared/notifications/pushStatus.ts +56 -0
  92. package/apps/shared/notifications/store.ts +549 -0
  93. package/apps/shared/notifications/systemErrors.ts +63 -0
  94. package/apps/shared/notifications/useNotifications.ts +41 -0
  95. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  96. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  97. package/apps/shared/signing/enrollmentView.ts +251 -0
  98. package/apps/shared/signing/signingInfo.ts +58 -0
  99. package/apps/shared/signing/useNow.ts +19 -0
  100. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  101. package/apps/shared/styles/app.css +80 -0
  102. package/apps/www/_layout.tsx +8 -2
  103. package/apps/www/_routes.ts +1 -0
  104. package/apps/www/calendar/index.tsx +9 -14
  105. package/apps/www/contacts/[uid].tsx +5 -8
  106. package/apps/www/contacts/index.tsx +587 -584
  107. package/apps/www/index.tsx +38 -27
  108. package/apps/www/messages/[uid].tsx +3 -1
  109. package/apps/www/settings/appearance/index.tsx +21 -0
  110. package/apps/www/settings/auto-reply/index.tsx +3 -7
  111. package/apps/www/settings/encryption/index.tsx +151 -116
  112. package/apps/www/settings/filters/[uid].tsx +3 -1
  113. package/apps/www/settings/filters/new/index.tsx +3 -1
  114. package/apps/www/settings/privacy/index.tsx +3 -8
  115. package/apps/www/settings/read-receipts/index.tsx +3 -7
  116. package/apps/www/settings/sharing/index.tsx +44 -65
  117. package/apps/www/settings/signatures/[uid].tsx +3 -1
  118. package/apps/www/settings/signatures/new/index.tsx +3 -2
  119. package/apps/www/tasks/index.tsx +11 -16
  120. package/dist/apps/admin/_layout.d.ts +5 -1
  121. package/dist/apps/admin/_layout.js +3 -2
  122. package/dist/apps/admin/index.js +3 -2
  123. package/dist/apps/admin/ingest-queue/index.js +2 -1
  124. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  125. package/dist/apps/admin/mailboxes/new/index.js +1 -1
  126. package/dist/apps/admin/quarantine/index.js +2 -1
  127. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  128. package/dist/apps/admin/signing-certificates/index.js +161 -0
  129. package/dist/apps/escrow/_layout.d.ts +5 -1
  130. package/dist/apps/escrow/_layout.js +3 -2
  131. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  132. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  133. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  134. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  135. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  136. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  137. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  138. package/dist/apps/shared/appearance/ColorField.js +40 -0
  139. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  140. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  141. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  142. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  143. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  144. package/dist/apps/shared/appearance/bootScript.js +33 -0
  145. package/dist/apps/shared/appearance/color.d.ts +65 -0
  146. package/dist/apps/shared/appearance/color.js +132 -0
  147. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  148. package/dist/apps/shared/appearance/photo.js +79 -0
  149. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  150. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  151. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  152. package/dist/apps/shared/appearance/theme.js +251 -0
  153. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +3 -2
  154. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  155. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  156. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  157. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  158. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  159. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  160. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +25 -5
  161. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +18 -7
  162. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  163. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  164. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  165. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  166. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  167. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  168. package/dist/apps/shared/components/contacts/ContactsToolbar.js +24 -9
  169. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -1
  170. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  171. package/dist/apps/shared/components/layout/AppShell.d.ts +6 -1
  172. package/dist/apps/shared/components/layout/AppShell.js +59 -13
  173. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  174. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  175. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  176. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  177. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  178. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  179. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  180. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  181. package/dist/apps/shared/components/layout/UserMenu.d.ts +7 -1
  182. package/dist/apps/shared/components/layout/UserMenu.js +41 -7
  183. package/dist/apps/shared/components/mail/ConversationList.js +3 -2
  184. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +2 -1
  185. package/dist/apps/shared/components/mail/ConversationThreadPane.js +42 -20
  186. package/dist/apps/shared/components/mail/LazyReadingPane.js +9 -8
  187. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +3 -4
  188. package/dist/apps/shared/components/mail/MailSelectionBar.js +2 -3
  189. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +18 -7
  190. package/dist/apps/shared/components/mail/MessageDetailPane.js +86 -41
  191. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  192. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  193. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  194. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  195. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +8 -0
  196. package/dist/apps/shared/components/mail/compose/ComposeContext.js +28 -3
  197. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +2 -1
  198. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +229 -328
  199. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  200. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  201. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +2 -0
  202. package/dist/apps/shared/components/mail/layout/MailShell.js +20 -14
  203. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  204. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  205. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  206. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  207. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  208. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  209. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  210. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  211. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  212. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  213. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  214. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  215. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  216. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  217. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  218. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  219. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  220. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  221. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  222. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  223. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  224. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  225. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  226. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  227. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  228. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  229. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  230. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  231. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  232. package/dist/apps/shared/components/sharing/PrincipalListField.d.ts +24 -0
  233. package/dist/apps/shared/components/sharing/PrincipalListField.js +23 -0
  234. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +28 -0
  235. package/dist/apps/shared/components/sharing/PrincipalPicker.js +24 -0
  236. package/dist/apps/shared/components/sharing/PrincipalResolver.d.ts +44 -0
  237. package/dist/apps/shared/components/sharing/PrincipalResolver.js +92 -0
  238. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  239. package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -1
  240. package/dist/apps/shared/mail/folderCounts.d.ts +1 -1
  241. package/dist/apps/shared/mail/folderCounts.js +9 -3
  242. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  243. package/dist/apps/shared/mail/folderTree.js +116 -0
  244. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  245. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  246. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  247. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  248. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  249. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  250. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  251. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  252. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  253. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  254. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  255. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  256. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  257. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  258. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  259. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  260. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  261. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  262. package/dist/apps/shared/mail/useMailConnection.d.ts +9 -8
  263. package/dist/apps/shared/mail/useMailConnection.js +64 -22
  264. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +17 -4
  265. package/dist/apps/shared/mail/useMailLiveUpdates.js +36 -10
  266. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +4 -10
  267. package/dist/apps/shared/mail/useNewMailNotifications.js +43 -23
  268. package/dist/apps/shared/navigation/AppRouter.js +2 -2
  269. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  270. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  271. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  272. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  273. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  274. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  275. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  276. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  277. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  278. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  279. package/dist/apps/shared/notifications/store.d.ts +136 -0
  280. package/dist/apps/shared/notifications/store.js +416 -0
  281. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  282. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  283. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  284. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  285. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  286. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  287. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  288. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  289. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  290. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  291. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  292. package/dist/apps/shared/signing/signingInfo.js +48 -0
  293. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  294. package/dist/apps/shared/signing/useNow.js +18 -0
  295. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  296. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  297. package/dist/apps/shared/styles/app.css +80 -0
  298. package/dist/apps/www/_layout.d.ts +5 -1
  299. package/dist/apps/www/_layout.js +3 -2
  300. package/dist/apps/www/_routes.js +1 -0
  301. package/dist/apps/www/calendar/index.js +3 -4
  302. package/dist/apps/www/contacts/[uid].js +5 -7
  303. package/dist/apps/www/contacts/index.js +14 -21
  304. package/dist/apps/www/index.js +37 -19
  305. package/dist/apps/www/messages/[uid].js +3 -1
  306. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  307. package/dist/apps/www/settings/appearance/index.js +13 -0
  308. package/dist/apps/www/settings/auto-reply/index.js +3 -6
  309. package/dist/apps/www/settings/encryption/index.js +103 -95
  310. package/dist/apps/www/settings/filters/[uid].js +3 -1
  311. package/dist/apps/www/settings/filters/new/index.js +3 -1
  312. package/dist/apps/www/settings/privacy/index.js +5 -8
  313. package/dist/apps/www/settings/read-receipts/index.js +3 -6
  314. package/dist/apps/www/settings/sharing/index.js +19 -33
  315. package/dist/apps/www/settings/signatures/[uid].js +3 -1
  316. package/dist/apps/www/settings/signatures/new/index.js +3 -2
  317. package/dist/apps/www/tasks/index.js +8 -14
  318. package/package.json +2 -2
  319. package/apps/shared/components/mail/NewMailToasts.tsx +0 -141
  320. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  321. package/dist/apps/shared/components/mail/NewMailToasts.d.ts +0 -19
  322. package/dist/apps/shared/components/mail/NewMailToasts.js +0 -55
  323. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  324. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -0,0 +1,251 @@
1
+ import { AA_CONTRAST, BLACK, DARK_TEXT, WHITE, contrastRatio, deriveScale, ensureContrast, mix, parseHex, readableOn, relativeLuminance, toHex, } from "./color.js";
2
+ export const DEFAULT_PALETTE = {
3
+ light: { surface: "#ffffff", text: "#1c2526", muted: "#5a6b6e" },
4
+ dark: { surface: "#1b2022", text: "#eef2f3", muted: "#9fb0b3" },
5
+ };
6
+ /** How far the text colour is mixed toward the surface to get the muted text colour. */
7
+ const MUTED_MIX = { light: 0.3, dark: 0.35 };
8
+ /** How far the surface is mixed toward the text colour to get `surface-alt`. */
9
+ const ALT_MIX = { light: 0.04, dark: 0.03 };
10
+ const BORDER_MIX = 0.12;
11
+ /** How far the text colour is mixed toward the surface to get the muted text colour while a background is set - less than usual, so the panels can be more see-through. */
12
+ const BACKGROUND_MUTED_MIX = 0.18;
13
+ /** `#rrggbb` known valid (`normalizeAppearance()` and the colour inputs guarantee it). */
14
+ function rgbOf(hex) {
15
+ return parseHex(hex);
16
+ }
17
+ export function effectivePalette(colors, scheme) {
18
+ const defaults = DEFAULT_PALETTE[scheme];
19
+ const surface = rgbOf(colors?.surface ?? defaults.surface);
20
+ let text = rgbOf(colors?.text ?? defaults.text);
21
+ let textAdjusted = colors?.text !== undefined;
22
+ if (!textAdjusted && colors?.surface !== undefined && contrastRatio(text, surface) < AA_CONTRAST) {
23
+ // A surface the app's own text can't be read on: the text follows it, rather than leaving the page unreadable.
24
+ text = readableOn(surface, DARK_TEXT);
25
+ textAdjusted = true;
26
+ }
27
+ const customised = colors?.surface !== undefined || textAdjusted;
28
+ const muted = customised
29
+ ? ensureContrast(mix(text, surface, MUTED_MIX[scheme]), surface, AA_CONTRAST)
30
+ : rgbOf(defaults.muted);
31
+ return { surface, text, muted, surfaceChosen: colors?.surface !== undefined, textAdjusted };
32
+ }
33
+ /**
34
+ * The custom properties a user's colours set for one scheme - only those they changed (and what derives from them): the primary
35
+ * colour's seven-step scale and the text on it, the accent and its shades, and the surface / text family. Empty when they chose
36
+ * no colours.
37
+ */
38
+ export function colorVariables(colors, scheme) {
39
+ const vars = {};
40
+ if (!colors) {
41
+ return vars;
42
+ }
43
+ const primary = colors.primary ? deriveScale(colors.primary) : undefined;
44
+ if (primary) {
45
+ vars["--rr-color-primary"] = primary.base;
46
+ vars["--rr-color-primary-dark"] = primary.dark;
47
+ vars["--rr-color-primary-darker"] = primary.darker;
48
+ vars["--rr-color-primary-darkest"] = primary.darkest;
49
+ vars["--rr-color-primary-light"] = primary.light;
50
+ vars["--rr-color-primary-lighter"] = primary.lighter;
51
+ vars["--rr-color-primary-lightest"] = primary.lightest;
52
+ vars["--rr-color-text-on-primary"] = primary.onBase;
53
+ }
54
+ const accent = colors.accent ? parseHex(colors.accent) : undefined;
55
+ if (accent) {
56
+ vars["--rr-color-accent"] = toHex(accent);
57
+ vars["--rr-color-accent-dark"] = toHex(mix(accent, BLACK, 0.25));
58
+ vars["--rr-color-accent-soft"] = toHex(mix(accent, WHITE, 0.2));
59
+ vars["--rr-color-accent-pale"] = toHex(mix(accent, WHITE, 0.8));
60
+ vars["--rr-color-text-on-accent"] = toHex(readableOn(accent, DARK_TEXT));
61
+ }
62
+ const palette = effectivePalette(colors, scheme);
63
+ if (palette.surfaceChosen || palette.textAdjusted) {
64
+ vars["--rr-color-border"] = toHex(mix(palette.surface, palette.text, BORDER_MIX));
65
+ vars["--rr-color-text-muted"] = toHex(palette.muted);
66
+ }
67
+ if (palette.surfaceChosen) {
68
+ vars["--rr-color-surface"] = toHex(palette.surface);
69
+ vars["--rr-color-surface-alt"] = toHex(mix(palette.surface, palette.text, ALT_MIX[scheme]));
70
+ // The page behind the panels: the surface itself, except that a dark surface sits on a slightly deeper page, as the defaults do.
71
+ vars["--rr-color-bg"] = toHex(relativeLuminance(palette.surface) < 0.18 ? mix(palette.surface, BLACK, 0.25) : palette.surface);
72
+ }
73
+ if (palette.textAdjusted) {
74
+ vars["--rr-color-text"] = toHex(palette.text);
75
+ }
76
+ return vars;
77
+ }
78
+ /** What is assumed of a photo nobody has measured: black in one corner and white in another. */
79
+ export const WORST_CASE_PHOTO = { lo: BLACK, hi: WHITE };
80
+ /** The panels are never more see-through than this ... */
81
+ export const PANEL_ALPHA_MIN = 0.5;
82
+ /** ... nor less than this (still a hint of the picture). */
83
+ export const PANEL_ALPHA_MAX = 0.96;
84
+ /** Contrast the panels are made to keep, a little over AA to leave room for the compression of a photo. */
85
+ const PANEL_CONTRAST = 4.6;
86
+ /**
87
+ * The opacity (0.5 to 0.96) of the surface panels over a background, the lowest for which text and muted text keep AA contrast
88
+ * against the worst place the picture can be. What is behind a panel is the picture under the dim (`dim` of the surface colour laid
89
+ * over it); the panel is `alpha` of the surface over that, so the picture's share of what the text sits on is `(1 - alpha) *
90
+ * (1 - dim)`. A wide-ranging (busy) photo therefore gets more opaque panels than a plain one, and more dim allows less opaque
91
+ * panels.
92
+ */
93
+ export function panelAlpha(palette, dim, photo) {
94
+ for (let alpha = PANEL_ALPHA_MIN; alpha < PANEL_ALPHA_MAX; alpha = Math.round((alpha + 0.01) * 100) / 100) {
95
+ const share = (1 - alpha) * (1 - dim);
96
+ const readable = [photo.lo, photo.hi].every((behind) => {
97
+ const under = mix(palette.surface, behind, share);
98
+ return contrastRatio(palette.text, under) >= PANEL_CONTRAST && contrastRatio(palette.muted, under) >= PANEL_CONTRAST;
99
+ });
100
+ if (readable) {
101
+ return alpha;
102
+ }
103
+ }
104
+ return PANEL_ALPHA_MAX;
105
+ }
106
+ /** What a background shows behind the panels - `undefined` for none: a colour, or an image of unknown or measured lightness. */
107
+ export function backgroundPhoto(background, measured) {
108
+ if (!background || background.kind === "none") {
109
+ return undefined;
110
+ }
111
+ if (background.kind === "color") {
112
+ const colour = rgbOf(background.color);
113
+ return { lo: colour, hi: colour };
114
+ }
115
+ return measured ?? WORST_CASE_PHOTO;
116
+ }
117
+ /** The dim actually laid over the background: a colour has no picture to dim. */
118
+ export function effectiveDim(background) {
119
+ return background.kind === "image" ? background.dim : 0;
120
+ }
121
+ /**
122
+ * The custom properties for one scheme: the user's colours, plus - when a background is set - the translucent panel colours and the
123
+ * transparent frame. `measured` is the image's measured lightness range, when known (`photo.ts`).
124
+ */
125
+ export function themeVariables(prefs, scheme, measured) {
126
+ const vars = colorVariables(prefs.colors, scheme);
127
+ const photo = backgroundPhoto(prefs.background, measured);
128
+ if (photo && prefs.background) {
129
+ const palette = effectivePalette(prefs.colors, scheme);
130
+ // Muted text is what limits how see-through the panels can be, so over a background it is a shade stronger.
131
+ const muted = ensureContrast(mix(palette.text, palette.surface, BACKGROUND_MUTED_MIX), palette.surface, AA_CONTRAST);
132
+ const alpha = panelAlpha({ surface: palette.surface, text: palette.text, muted }, effectiveDim(prefs.background), photo);
133
+ const percent = `${Math.round(alpha * 100)}%`;
134
+ vars["--rr-panel"] = `color-mix(in srgb, var(--rr-color-surface) ${percent}, transparent)`;
135
+ vars["--color-surface"] = "var(--rr-panel)";
136
+ vars["--color-surface-alt"] = `color-mix(in srgb, var(--rr-color-surface-alt) ${percent}, transparent)`;
137
+ vars["--rr-color-text-muted"] = toHex(muted);
138
+ vars["--rr-panel-alpha"] = String(alpha);
139
+ vars["--rr-frame-bg"] = "transparent";
140
+ }
141
+ return vars;
142
+ }
143
+ function declarations(vars) {
144
+ return Object.entries(vars)
145
+ .map(([name, value]) => `${name}:${value} !important;`)
146
+ .join("");
147
+ }
148
+ /** The element `appearanceCss()` writes into `<head>`, so an update replaces rather than duplicates it. */
149
+ export const APPEARANCE_STYLE_ID = "rr-appearance";
150
+ /** A string as a CSS `url()` argument: quoted, with what could end the string or the declaration escaped. */
151
+ export function cssUrl(url) {
152
+ const escaped = url.replace(/[\\"\p{Cc}]/gu, (char) => "\\" + char.charCodeAt(0).toString(16) + " ");
153
+ return `url("${escaped}")`;
154
+ }
155
+ /**
156
+ * The rules for the background layer, which is two pseudo-elements of `<html>` rather than any markup - so it exists from the
157
+ * first byte of a server-rendered page (no flash), follows a live preview by rewriting one stylesheet, and needs no component.
158
+ * `::before` is the picture (or colour): `position: fixed`, behind everything (`z-index: -1` puts it above the page's own canvas
159
+ * colour and below every in-flow box), its box grown by twice the blur so the blurred edge falls outside the window. `::after` is the
160
+ * dim: the scheme's surface colour at the chosen opacity. Neither takes a click, and neither prints.
161
+ */
162
+ export function backgroundCss(background, imageUrl) {
163
+ if (!background || background.kind === "none") {
164
+ return "";
165
+ }
166
+ const layer = "content:\"\";position:fixed;z-index:-1;pointer-events:none;";
167
+ if (background.kind === "color") {
168
+ return [`html::before{${layer}inset:0;background-color:${background.color};}`, "@media print{html::before{display:none}}"].join("\n");
169
+ }
170
+ const grow = -Math.round(background.blur * 2);
171
+ const size = background.fit === "cover"
172
+ ? "background-size:cover;background-repeat:no-repeat;background-position:center;"
173
+ : background.fit === "contain"
174
+ ? "background-size:contain;background-repeat:no-repeat;background-position:center;"
175
+ : "background-size:auto;background-repeat:repeat;background-position:center;";
176
+ const image = imageUrl ? `background-image:${cssUrl(imageUrl)};` : "";
177
+ const color = background.color ?? "var(--rr-color-surface-alt)";
178
+ const blur = background.blur > 0 ? `filter:blur(${background.blur}px);` : "";
179
+ return [
180
+ `html::before{${layer}inset:${grow}px;background-color:${color};${image}${size}${blur}}`,
181
+ `html::after{${layer}inset:0;background-color:var(--rr-color-surface);opacity:${background.dim};}`,
182
+ "@media print{html::before,html::after{display:none}}",
183
+ ].join("\n");
184
+ }
185
+ /**
186
+ * The stylesheet text for the user's preferences, or `""` when they change nothing. Three rules per set of properties so each
187
+ * scheme is right in every case: the light set for everything except an explicit dark (`data-theme="dark"`) - which also covers
188
+ * a page with no `data-theme` at all; the dark set for a dark operating system on a page with no `data-theme` yet (before the
189
+ * provider ran, or a server-rendered page following "System"); and the dark set for an explicit `data-theme="dark"`. All three are
190
+ * `html:root...` (0,2,1) so the later dark rules win the equal-specificity tie against the light one. Then the background layer
191
+ * (`backgroundCss()`).
192
+ */
193
+ export function appearanceCss(prefs, options = {}) {
194
+ if (!prefs) {
195
+ return "";
196
+ }
197
+ const light = declarations(themeVariables(prefs, "light", options.measured));
198
+ const dark = declarations(themeVariables(prefs, "dark", options.measured));
199
+ const rules = [];
200
+ if (light || dark) {
201
+ rules.push(`html:root:not([data-theme="dark"]){${light}}`, `@media (prefers-color-scheme:dark){html:root:not([data-theme]){${dark}}}`, `html:root[data-theme="dark"]{${dark}}`);
202
+ }
203
+ if (prefs.colors?.primary) {
204
+ // Several components write `bg-primary text-white`; on a primary colour the user picked, white may be unreadable.
205
+ rules.push(".bg-primary.text-white{color:var(--rr-color-text-on-primary)}");
206
+ }
207
+ const background = backgroundCss(prefs.background, options.imageUrl);
208
+ if (background) {
209
+ // The content region is one translucent panel; what is inside it (`bg-surface`) shows it instead of adding a second layer.
210
+ rules.push("#app-content{background-color:var(--rr-panel);--color-surface:transparent;}",
211
+ // Fields keep a solid surface: a box that is see-through has no edge to find.
212
+ "#app-content :is(input,select,textarea){--color-surface:var(--rr-color-surface);}",
213
+ // Someone who asked their system for less transparency gets solid panels.
214
+ "@media (prefers-reduced-transparency:reduce){html:root{--rr-panel:var(--rr-color-surface) !important;--color-surface-alt:var(--rr-color-surface-alt) !important;}}", background);
215
+ }
216
+ return rules.join("\n");
217
+ }
218
+ /**
219
+ * The colour pairs the user's choices make hard to read (contrast below 4.5:1, WCAG AA for normal text) in `scheme`: the text on the
220
+ * surface, and the text on an accent-coloured button. A warning, never a block - it is their app. Nothing for a pair they haven't
221
+ * touched (the app's own pairs are the app's business).
222
+ */
223
+ export function contrastWarnings(colors, scheme) {
224
+ const warnings = [];
225
+ if (!colors) {
226
+ return warnings;
227
+ }
228
+ if (colors.surface !== undefined || colors.text !== undefined) {
229
+ const palette = effectivePalette(colors, scheme);
230
+ const ratio = contrastRatio(palette.text, palette.surface);
231
+ if (ratio < AA_CONTRAST) {
232
+ warnings.push({
233
+ id: "text-surface",
234
+ ratio,
235
+ message: `Text on the surface colour has a contrast of ${ratio.toFixed(1)}:1; 4.5:1 or more is easier to read.`,
236
+ });
237
+ }
238
+ }
239
+ if (colors.accent !== undefined) {
240
+ const accent = rgbOf(colors.accent);
241
+ const ratio = contrastRatio(readableOn(accent, DARK_TEXT), accent);
242
+ if (ratio < AA_CONTRAST) {
243
+ warnings.push({
244
+ id: "accent-text",
245
+ ratio,
246
+ message: `Text on buttons in the accent colour has a contrast of ${ratio.toFixed(1)}:1; 4.5:1 or more is easier to read.`,
247
+ });
248
+ }
249
+ }
250
+ return warnings;
251
+ }
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
3
+ import { resolveEscrowScopeHolder } from "@rapidmx/react-shared/admin/escrowScopesApi.js";
3
4
  import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
4
- import StringListField from "../../forms/StringListField.js";
5
+ import PrincipalListField from "../../sharing/PrincipalListField.js";
5
6
  const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
6
7
  /** Builds an all-zeroes starting point for the "new" page — `notBefore`/`notAfter` default to "now" so an
7
8
  * admin pasting in a cert's own dates only has to touch what actually differs. */
@@ -31,5 +32,5 @@ export default function EscrowScopeKeyAndHoldersFields({ value, onChange, disabl
31
32
  function set(key, next) {
32
33
  onChange({ ...value, [key]: next });
33
34
  }
34
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Public key" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "The fields of an already-issued certificate \u2014 this server never generates or holds the corresponding private key." }), _jsx(FormField, { label: "Public key (base64)", htmlFor: "publicKey", children: _jsx("textarea", { id: "publicKey", className: `${INPUT_CLASS} font-mono text-xs`, rows: 4, value: value.publicKey, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("publicKey", e.target.value) }) }), _jsx(FormField, { label: "Key type", htmlFor: "keyType", children: _jsx("input", { id: "keyType", type: "text", className: INPUT_CLASS, value: value.keyType, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("keyType", e.target.value), placeholder: "x509" }) }), _jsx(FormField, { label: "Fingerprint (hex SHA-256)", htmlFor: "fingerprint", children: _jsx("input", { id: "fingerprint", type: "text", className: `${INPUT_CLASS} font-mono`, value: value.fingerprint, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("fingerprint", e.target.value) }) }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-4", children: [_jsx(FormField, { label: "Not before", htmlFor: "notBefore", children: _jsx("input", { id: "notBefore", type: "datetime-local", className: INPUT_CLASS, value: value.notBefore, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("notBefore", e.target.value) }) }), _jsx(FormField, { label: "Not after", htmlFor: "notAfter", children: _jsx("input", { id: "notAfter", type: "datetime-local", className: INPUT_CLASS, value: value.notAfter, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("notAfter", e.target.value) }) })] })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Holders & dual control" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Every uid below is granted the eDiscovery/compliance holder role for this scope \u2014 a deliberately separate role from server administration. At least one holder is required." }), _jsx(StringListField, { label: "Holder user uids", id: "holderUserUids", values: value.holderUserUids, onChange: (holderUserUids) => set("holderUserUids", holderUserUids), placeholder: "User uid to add", emptyMessage: "No holders added yet.", disabled: disabled }), _jsx(FormField, { label: "Required holders (M-of-N dual control)", htmlFor: "requiredHolders", children: _jsx("input", { id: "requiredHolders", type: "number", className: INPUT_CLASS, value: value.requiredHolders, disabled: disabled, onChange: (e) => set("requiredHolders", Number(e.target.value) || 1) }) }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.notifySubjectOnAccess, disabled: disabled, onChange: (e) => set("notifySubjectOnAccess", e.target.checked) }), "Notify subject on access"] })] })] }));
35
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Public key" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "The fields of an already-issued certificate \u2014 this server never generates or holds the corresponding private key." }), _jsx(FormField, { label: "Public key (base64)", htmlFor: "publicKey", children: _jsx("textarea", { id: "publicKey", className: `${INPUT_CLASS} font-mono text-xs`, rows: 4, value: value.publicKey, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("publicKey", e.target.value) }) }), _jsx(FormField, { label: "Key type", htmlFor: "keyType", children: _jsx("input", { id: "keyType", type: "text", className: INPUT_CLASS, value: value.keyType, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("keyType", e.target.value), placeholder: "x509" }) }), _jsx(FormField, { label: "Fingerprint (hex SHA-256)", htmlFor: "fingerprint", children: _jsx("input", { id: "fingerprint", type: "text", className: `${INPUT_CLASS} font-mono`, value: value.fingerprint, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("fingerprint", e.target.value) }) }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-4", children: [_jsx(FormField, { label: "Not before", htmlFor: "notBefore", children: _jsx("input", { id: "notBefore", type: "datetime-local", className: INPUT_CLASS, value: value.notBefore, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("notBefore", e.target.value) }) }), _jsx(FormField, { label: "Not after", htmlFor: "notAfter", children: _jsx("input", { id: "notAfter", type: "datetime-local", className: INPUT_CLASS, value: value.notAfter, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("notAfter", e.target.value) }) })] })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Holders & dual control" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Every uid below is granted the eDiscovery/compliance holder role for this scope \u2014 a deliberately separate role from server administration. At least one holder is required." }), _jsx(PrincipalListField, { label: "Holder user uids", values: value.holderUserUids, onChange: (holderUserUids) => set("holderUserUids", holderUserUids), resolve: resolveEscrowScopeHolder, emptyMessage: "No holders added yet.", disabled: disabled }), _jsx(FormField, { label: "Required holders (M-of-N dual control)", htmlFor: "requiredHolders", children: _jsx("input", { id: "requiredHolders", type: "number", className: INPUT_CLASS, value: value.requiredHolders, disabled: disabled, onChange: (e) => set("requiredHolders", Number(e.target.value) || 1) }) }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.notifySubjectOnAccess, disabled: disabled, onChange: (e) => set("notifySubjectOnAccess", e.target.checked) }), "Notify subject on access"] })] })] }));
35
36
  }
@@ -2,7 +2,7 @@ import "../../../styles/app.css";
2
2
  import React, { PropsWithChildren } from "react";
3
3
  import { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
4
4
  import { PluginNav, PluginNavProps } from "../../../plugins/pluginNav.js";
5
- export type AdminSection = "mailboxes" | "quarantine" | "ingestQueue" | "domains" | "auditLog" | "distributionLists" | "transportRules" | "escrowScopes" | "retentionPolicy" | "encryptionPolicy" | "mailboxPolicy" | "setup" | "dataRequests" | "plugins" | "branding";
5
+ export type AdminSection = "mailboxes" | "quarantine" | "ingestQueue" | "domains" | "auditLog" | "distributionLists" | "transportRules" | "escrowScopes" | "retentionPolicy" | "encryptionPolicy" | "mailboxPolicy" | "setup" | "dataRequests" | "plugins" | "branding" | "signingCertificates";
6
6
  /** A core `AdminSection`, or a plugin's `adminNav` item id (see `PluginNav`). */
7
7
  export type AdminShellActive = AdminSection | (string & {});
8
8
  export interface AdminShellProps extends PluginNavProps {
@@ -1,11 +1,11 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  ///////////////////////////////////////////////////////////////////////////////
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import "../../../styles/app.css";
7
7
  import { useEffect, useState } from "react";
8
- import { HiOutlineClipboardDocumentList, HiOutlineClock, HiOutlineDocumentArrowDown, HiOutlineGlobeAlt, HiOutlineInboxStack, HiOutlineKey, HiOutlineLockClosed, HiOutlinePaintBrush, HiOutlinePuzzlePiece, HiOutlineQueueList, HiOutlineRocketLaunch, HiOutlineShieldCheck, HiOutlineShieldExclamation, HiOutlineUserGroup, HiOutlineWrenchScrewdriver, } from "react-icons/hi2";
8
+ import { HiOutlineClipboardDocumentList, HiOutlineClock, HiOutlineDocumentArrowDown, HiOutlineDocumentCheck, HiOutlineGlobeAlt, HiOutlineInboxStack, HiOutlineKey, HiOutlineLockClosed, HiOutlinePaintBrush, HiOutlinePuzzlePiece, HiOutlineQueueList, HiOutlineRocketLaunch, HiOutlineShieldCheck, HiOutlineShieldExclamation, HiOutlineUserGroup, HiOutlineWrenchScrewdriver, } from "react-icons/hi2";
9
9
  import { apiFetch, ApiRequestError } from "@rapidmx/react-shared/util/api.js";
10
10
  import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
11
11
  import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
@@ -13,7 +13,9 @@ import useBranding from "@rapidmx/react-shared/branding/useBranding.js";
13
13
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
14
14
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
15
15
  import BottomTabBar from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
16
- import { BrandingFooter } from "../../layout/BrandingChrome.js";
16
+ import { FrameBrandingFooter, useBrandingHtml } from "../../layout/BrandingChrome.js";
17
+ import RailIcon from "../../layout/RailIcon.js";
18
+ import AppearanceProvider from "../../../appearance/AppearanceProvider.js";
17
19
  import UserMenu from "../../layout/UserMenu.js";
18
20
  import { clearElevationAttempt, elevationAttemptedRecently, elevationUrl, isElevationRequired, recordElevationAttempt, } from "../elevation.js";
19
21
  import { signOutOfConsole } from "../signOut.js";
@@ -68,6 +70,12 @@ const NAV_ITEMS = [
68
70
  label: "Data Requests",
69
71
  icon: HiOutlineDocumentArrowDown,
70
72
  },
73
+ {
74
+ id: "signingCertificates",
75
+ href: "/admin/signing-certificates",
76
+ label: "Signing Certificates",
77
+ icon: HiOutlineDocumentCheck,
78
+ },
71
79
  { id: "plugins", href: "/admin/plugins", label: "Plugins", icon: HiOutlinePuzzlePiece },
72
80
  { id: "branding", href: "/admin/branding", label: "Branding", icon: HiOutlinePaintBrush },
73
81
  ];
@@ -105,6 +113,7 @@ export default function AdminShell({ active, userUid, authServerUrl, pluginNav,
105
113
  // `branding` only feeds the footer below: the admin-configured header is for the webmail and public pages, not the
106
114
  // console. `useBranding()` is still what injects the custom stylesheet and supplies the rail's icon.
107
115
  const { branding, iconSrc } = useBranding();
116
+ const footer = useBrandingHtml(branding?.footerHtml);
108
117
  useRedirectIfUnauthenticated(userUid, authServerUrl);
109
118
  useEffect(() => {
110
119
  if (!userUid) {
@@ -184,12 +193,13 @@ export default function AdminShell({ active, userUid, authServerUrl, pluginNav,
184
193
  else {
185
194
  const navItems = adminNavItems(pluginNav);
186
195
  const activeItem = [...navItems, ...OFF_RAIL_ITEMS].find((item) => item.id === active);
187
- content = (_jsx("div", { className: "min-h-screen flex flex-col bg-surface-alt", children: _jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsxs("nav", { "aria-label": "Admin sections", className: "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1", children: [_jsx("img", { src: iconSrc, width: "96", height: "96", alt: "", className: "mb-3" }), navItems.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
196
+ content = (_jsx("div", { className: "rr-frame-bg min-h-screen flex flex-col", children: _jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsxs("nav", { "aria-label": "Admin sections", className: "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center pb-3 gap-1", children: [_jsx(RailIcon, { src: iconSrc }), navItems.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
188
197
  "w-10 h-10 flex items-center justify-center rounded-sm",
189
198
  id === active
190
199
  ? "bg-primary/10 text-primary-dark"
191
200
  : "text-text-muted hover:bg-surface-alt hover:text-text",
192
- ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: navItems, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeItem?.label }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut })] }), _jsx("div", { className: "flex-1 pb-14 md:pb-0", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
201
+ ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: navItems, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeItem?.label }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut })] }), _jsx("div", { id: "app-content", className: "flex-1 pb-14 md:pb-0", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
193
202
  }
194
- return (_jsxs(_Fragment, { children: [content, _jsx(BrandingFooter, { branding: branding })] }));
203
+ // The user's own colours and background (`AppearanceProvider`), from what the last webmail page cached in this browser: the console never asks the server.
204
+ return (_jsxs(AppearanceProvider, { userUid: userUid, lookUp: false, children: [content, _jsx(FrameBrandingFooter, { parsed: footer, appTitle: [...adminNavItems(pluginNav), ...OFF_RAIL_ITEMS].find((item) => item.id === active)?.label })] }));
195
205
  }
@@ -1,12 +1,17 @@
1
1
  import React from "react";
2
2
  export interface ShareAccessCardProps {
3
3
  mailboxUid: string;
4
- /** The mailbox's owner - their own grant is shown but can't be revoked from here. */
4
+ /** The mailbox's owner: with one, only the owner can grant access to it. Without, the mailbox is shared and an
5
+ * administrator grants access here - to themselves included. */
5
6
  ownerUserUid?: string;
7
+ /** The signed-in administrator, for "Add me" on a shared mailbox. */
8
+ currentUserUid?: string;
6
9
  }
7
10
  /**
8
- * Lets an admin (or, eventually, the mailbox owner) view and edit a mailbox's delegate access — the
9
- * mechanism behind Exchange-style shared mailboxes. `records` on a mailbox's own ACL doubles as both the
10
- * owner's grant (if any) and every delegate's — see `BaseMailboxRoute`'s doc comment in `@rapidmx/restapi`.
11
+ * A mailbox's members - the mechanism behind Exchange-style shared mailboxes - through the Sharing endpoints
12
+ * (`/mail/mailboxes/:id/access`), the audited way an administrator reaches a mailbox they hold no grant on. An
13
+ * administrator can review any mailbox's members and revoke any of them; they can grant access only on a shared
14
+ * (ownerless) mailbox - themselves included, which is how it appears in their own mail client. A personal mailbox is
15
+ * shared by its owner (or by an administrator impersonating them).
11
16
  */
12
- export default function ShareAccessCard({ mailboxUid, ownerUserUid }: ShareAccessCardProps): React.JSX.Element;
17
+ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserUid }: ShareAccessCardProps): React.JSX.Element;
@@ -1,59 +1,52 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  ///////////////////////////////////////////////////////////////////////////////
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
- import { getMailboxAcl, grantMailboxAccess, revokeMailboxAccess } from "@rapidmx/react-shared/mail/mailApi.js";
8
+ import { listMailboxAccess, removeMailboxAccess, setMailboxAccess, } from "@rapidmx/react-shared/mail/mailboxAccessApi.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
11
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
12
- const DEFAULT_DELEGATE_ACTIONS = ["read", "list", "count", "exists"];
12
+ import PrincipalPicker from "../../sharing/PrincipalPicker.js";
13
+ const ROLE_LABELS = { viewer: "Read only", manager: "Full access" };
14
+ function roleLabel(member) {
15
+ if (member.role === "viewer")
16
+ return ROLE_LABELS.viewer;
17
+ if (member.role === "manager")
18
+ return ROLE_LABELS.manager;
19
+ return (member.actions ?? []).join(", ");
20
+ }
13
21
  /**
14
- * Lets an admin (or, eventually, the mailbox owner) view and edit a mailbox's delegate access — the
15
- * mechanism behind Exchange-style shared mailboxes. `records` on a mailbox's own ACL doubles as both the
16
- * owner's grant (if any) and every delegate's — see `BaseMailboxRoute`'s doc comment in `@rapidmx/restapi`.
22
+ * A mailbox's members - the mechanism behind Exchange-style shared mailboxes - through the Sharing endpoints
23
+ * (`/mail/mailboxes/:id/access`), the audited way an administrator reaches a mailbox they hold no grant on. An
24
+ * administrator can review any mailbox's members and revoke any of them; they can grant access only on a shared
25
+ * (ownerless) mailbox - themselves included, which is how it appears in their own mail client. A personal mailbox is
26
+ * shared by its owner (or by an administrator impersonating them).
17
27
  */
18
- export default function ShareAccessCard({ mailboxUid, ownerUserUid }) {
19
- const [acl, setAcl] = useState(null);
28
+ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserUid }) {
29
+ const [members, setMembers] = useState([]);
20
30
  const [loading, setLoading] = useState(true);
21
31
  const [error, setError] = useState(null);
22
- const [notice, setNotice] = useState(null);
23
- const [newUserUid, setNewUserUid] = useState("");
24
32
  const [saving, setSaving] = useState(false);
25
33
  const [revokeTarget, setRevokeTarget] = useState(null);
34
+ // An entry that is not a user uid, being replaced with the user it was meant for.
35
+ const [replacing, setReplacing] = useState(null);
26
36
  function reload() {
27
37
  setLoading(true);
28
38
  setError(null);
29
- getMailboxAcl(mailboxUid)
30
- .then(setAcl)
39
+ listMailboxAccess(mailboxUid)
40
+ .then(setMembers)
31
41
  .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load share settings."))
32
42
  .finally(() => setLoading(false));
33
43
  }
34
44
  useEffect(reload, [mailboxUid]);
35
- async function handleGrant(e) {
36
- e.preventDefault();
37
- const userUid = newUserUid.trim();
38
- if (!userUid) {
39
- return;
40
- }
45
+ async function addMe(userUid) {
41
46
  setSaving(true);
42
47
  setError(null);
43
- setNotice(null);
44
48
  try {
45
- // `grantMailboxAccess()` replaces any existing record for this uid, so granting read access to someone
46
- // who already has more (e.g. the owner) would silently downgrade them. Merge with their current actions.
47
- const current = await getMailboxAcl(mailboxUid);
48
- const existing = current.records.find((record) => record.userOrRoleId === userUid)?.actions ?? [];
49
- const missing = DEFAULT_DELEGATE_ACTIONS.filter((action) => !existing.includes(action));
50
- if (missing.length === 0) {
51
- setNotice(`${userUid} already has this access.`);
52
- setNewUserUid("");
53
- return;
54
- }
55
- await grantMailboxAccess(mailboxUid, userUid, [...existing, ...missing]);
56
- setNewUserUid("");
49
+ await setMailboxAccess(mailboxUid, userUid, "manager");
57
50
  reload();
58
51
  }
59
52
  catch (err) {
@@ -63,15 +56,33 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid }) {
63
56
  setSaving(false);
64
57
  }
65
58
  }
59
+ // Once the person is granted, the entry they replace (if any) is removed: the string it named never matched anyone.
60
+ async function handleGranted() {
61
+ const stale = replacing;
62
+ setReplacing(null);
63
+ let failure = null;
64
+ if (stale) {
65
+ try {
66
+ await removeMailboxAccess(mailboxUid, stale.userOrRoleId);
67
+ }
68
+ catch (err) {
69
+ failure = err instanceof ApiRequestError ? err.message : "Could not remove the old entry.";
70
+ }
71
+ }
72
+ reload();
73
+ // After the reload, which clears the error it started with.
74
+ if (failure) {
75
+ setError(failure);
76
+ }
77
+ }
66
78
  // Only ever invoked from the revoke-confirmation modal below, which only renders once `revokeTarget` is set.
67
79
  async function handleRevoke() {
68
80
  const userOrRoleId = revokeTarget;
69
81
  setRevokeTarget(null);
70
82
  setSaving(true);
71
83
  setError(null);
72
- setNotice(null);
73
84
  try {
74
- await revokeMailboxAccess(mailboxUid, userOrRoleId);
85
+ await removeMailboxAccess(mailboxUid, userOrRoleId);
75
86
  reload();
76
87
  }
77
88
  catch (err) {
@@ -81,5 +92,9 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid }) {
81
92
  setSaving(false);
82
93
  }
83
94
  }
84
- return (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Shared access" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Grants read access to this mailbox for another user, independent of ownership." }), error && _jsx(Alert, { children: error }), notice && !error && _jsx("div", { className: "mb-4 text-sm text-text-muted", children: notice }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("ul", { className: "flex flex-col gap-2 mb-4", children: [(acl?.records ?? []).length === 0 && (_jsx("li", { className: "text-sm text-text-muted", children: "No grants on this mailbox yet." })), acl?.records.map((record) => (_jsxs("li", { className: "flex items-center justify-between gap-3 py-2 px-3 border border-border rounded-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: record.userOrRoleId }), _jsx("div", { className: "text-xs text-text-muted", children: record.actions.join(", ") })] }), record.userOrRoleId === ownerUserUid ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: "Owner" })) : (_jsx("button", { type: "button", onClick: () => setRevokeTarget(record.userOrRoleId), disabled: saving, className: "text-sm text-danger hover:underline disabled:opacity-55", children: "Revoke" }))] }, record.userOrRoleId)))] })), _jsxs("form", { onSubmit: handleGrant, className: "flex gap-2", children: [_jsx("input", { type: "text", value: newUserUid, onChange: (e) => setNewUserUid(e.target.value), placeholder: "User uid to grant access to", className: "flex-1 text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary" }), _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Grant" })] }), _jsxs(Modal, { open: revokeTarget !== null, onClose: () => setRevokeTarget(null), title: "Revoke access", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Revoke ", _jsx("strong", { className: "break-all", children: revokeTarget }), "\u2019s access to this mailbox? They lose it immediately."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setRevokeTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void handleRevoke(), children: "Revoke" })] })] })] }));
95
+ const shared = !ownerUserUid;
96
+ const isMember = members.some((member) => member.userOrRoleId === currentUserUid);
97
+ return (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Shared access" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: shared
98
+ ? "Who has access to this shared mailbox. Adding yourself is how it appears in your own mail client; every change is recorded in the audit log."
99
+ : "Who the owner has shared this mailbox with. You can review and revoke access; only the owner can grant it (impersonate them to do so)." }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("ul", { className: "flex flex-col gap-2 mb-4", children: [members.length === 0 && _jsx("li", { className: "text-sm text-text-muted", children: "No grants on this mailbox yet." }), members.map((member) => (_jsxs("li", { className: "flex items-center justify-between gap-3 py-2 px-3 border border-border rounded-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: member.userOrRoleId }), _jsx("div", { className: "text-xs text-text-muted", children: roleLabel(member) }), member.noEffect && (_jsxs("div", { className: "text-xs text-danger mt-0.5", children: ["Not a user - this entry has no effect", shared ? " - replace it with the user it was meant for or remove it." : " - remove it."] }))] }), _jsxs("span", { className: "flex items-center gap-3 shrink-0", children: [member.noEffect && shared && (_jsx("button", { type: "button", onClick: () => setReplacing(member), disabled: saving, className: "text-sm text-primary-dark hover:underline disabled:opacity-55", children: "Replace with a user" })), _jsx("button", { type: "button", onClick: () => setRevokeTarget(member.userOrRoleId), disabled: saving, className: "text-sm text-danger hover:underline disabled:opacity-55", children: "Revoke" })] })] }, member.userOrRoleId)))] })), shared && (_jsxs(_Fragment, { children: [currentUserUid && !isMember && !loading && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mb-3", disabled: saving, onClick: () => void addMe(currentUserUid), children: "Add me" })), _jsx(PrincipalPicker, { mailboxUid: mailboxUid, roleLabels: ROLE_LABELS, initialPrincipal: replacing?.userOrRoleId, defaultRole: replacing?.role === "manager" ? "manager" : "viewer", onGranted: handleGranted, onCancel: replacing ? () => setReplacing(null) : undefined }, replacing?.userOrRoleId ?? "new")] })), _jsxs(Modal, { open: revokeTarget !== null, onClose: () => setRevokeTarget(null), title: "Revoke access", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Revoke ", _jsx("strong", { className: "break-all", children: revokeTarget }), "\u2019s access to this mailbox? They lose it immediately."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setRevokeTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void handleRevoke(), children: "Revoke" })] })] })] }));
85
100
  }
@@ -1,5 +1,7 @@
1
1
  import React from "react";
2
2
  import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
3
+ /** A header that uses both variables - the snippet the Branding page offers to copy. */
4
+ export declare const HEADER_HTML_EXAMPLE = "<div style=\"display:flex; align-items:center; justify-content:space-between; padding:0.5rem 1rem\">\n <span>{APP_TITLE}</span>\n <span>{USER_MENU}</span>\n</div>";
3
5
  /** SVG logo/icon uploads are refused (the server refuses them too): an SVG served from this origin can carry script. */
4
6
  export declare function isSvgFile(file: File): boolean;
5
7
  export interface BrandingFormProps {
@@ -8,8 +8,14 @@ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
8
  import { deleteBrandingIcon, deleteBrandingLogo, deleteBrandingStylesheet, updateBranding, uploadBrandingIcon, uploadBrandingLogo, uploadBrandingStylesheet, } from "@rapidmx/react-shared/branding/brandingApi.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 CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
11
12
  const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
12
13
  const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
14
+ /** A header that uses both variables - the snippet the Branding page offers to copy. */
15
+ export const HEADER_HTML_EXAMPLE = `<div style="display:flex; align-items:center; justify-content:space-between; padding:0.5rem 1rem">
16
+ <span>{APP_TITLE}</span>
17
+ <span>{USER_MENU}</span>
18
+ </div>`;
13
19
  /** SVG logo/icon uploads are refused (the server refuses them too): an SVG served from this origin can carry script. */
14
20
  export function isSvgFile(file) {
15
21
  return file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg");
@@ -113,5 +119,5 @@ export default function BrandingForm({ branding, onChange, embedded = false }) {
113
119
  }), children: "Remove icon" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Icon URL", className: INPUT_CLASS, value: iconUrlInput, onChange: (e) => setIconUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || iconUrlInput === (branding.iconUrl ?? ""), loading: assetBusy === "icon-url", onClick: () => void runAsset("icon-url", () => updateBranding({ iconUrl: iconUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Custom stylesheet" }), _jsx("div", { className: "flex items-center gap-3", children: _jsx("span", { className: "text-sm text-text-muted truncate", children: branding.stylesheetUrl || "None configured" }) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "stylesheet-upload", disabled: assetBusy !== null, onClick: () => stylesheetFileRef.current?.click(), children: "Upload CSS file" }), _jsx("input", { ref: stylesheetFileRef, type: "file", accept: "text/css,.css", "aria-label": "Upload stylesheet", className: "hidden", onChange: handleStylesheetFileChange }), branding.stylesheetUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "stylesheet-delete", onClick: () => void runAsset("stylesheet-delete", async () => {
114
120
  await deleteBrandingStylesheet();
115
121
  return { ...branding, stylesheetUrl: undefined };
116
- }), children: "Remove stylesheet" }))] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external stylesheet URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Stylesheet URL", className: INPUT_CLASS, value: stylesheetUrlInput, onChange: (e) => setStylesheetUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? ""), loading: assetBusy === "stylesheet-url", onClick: () => void runAsset("stylesheet-url", () => updateBranding({ stylesheetUrl: stylesheetUrlInput })), children: "Set" })] })] })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4 pt-2 border-t border-border", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Company name" }), _jsx("input", { "aria-label": "Company name", className: INPUT_CLASS, value: companyName, onChange: (e) => setCompanyName(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Product title" }), _jsx("input", { "aria-label": "Product title", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "Shown as the browser tab title." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Header HTML (optional)" }), _jsx("textarea", { "aria-label": "Header HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: headerHtml, onChange: (e) => setHeaderHtml(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Footer HTML (optional)" }), _jsx("textarea", { "aria-label": "Footer HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: footerHtml, onChange: (e) => setFooterHtml(e.target.value) })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] })] }));
122
+ }), children: "Remove stylesheet" }))] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external stylesheet URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Stylesheet URL", className: INPUT_CLASS, value: stylesheetUrlInput, onChange: (e) => setStylesheetUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? ""), loading: assetBusy === "stylesheet-url", onClick: () => void runAsset("stylesheet-url", () => updateBranding({ stylesheetUrl: stylesheetUrlInput })), children: "Set" })] })] })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4 pt-2 border-t border-border", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Company name" }), _jsx("input", { "aria-label": "Company name", className: INPUT_CLASS, value: companyName, onChange: (e) => setCompanyName(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Product title" }), _jsx("input", { "aria-label": "Product title", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "Shown as the browser tab title." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Header HTML (optional)" }), _jsx("textarea", { "aria-label": "Header HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: headerHtml, onChange: (e) => setHeaderHtml(e.target.value) }), _jsxs("span", { className: "text-xs text-text-muted", children: ["A header replaces the app's own title bar and the icon at the top of the icon rail, and stays at the top of the window. Write", " ", _jsx("code", { children: "{USER_MENU}" }), " where the account menu (avatar, Settings, Sign Out) should go, and ", _jsx("code", { children: "{APP_TITLE}" }), " ", "where the name of the app on screen should show. Both work in the footer too. ", _jsx("code", { children: "{USER_MENU}" }), " is replaced once (the header's, else the footer's); without it the menu sits in a small cell at the right end of the header, so it is never lost. Put it where your styles don't hide it on a phone."] })] }), _jsxs("div", { className: "flex flex-col gap-1.5 text-sm", role: "group", "aria-label": "Header example", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "font-semibold", children: "Example header" }), _jsx(CopyButton, { value: HEADER_HTML_EXAMPLE, label: "Copy the example header HTML" })] }), _jsx("pre", { className: "overflow-x-auto rounded-sm border border-border bg-surface-alt p-3 font-mono text-xs text-text", children: HEADER_HTML_EXAMPLE })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Footer HTML (optional)" }), _jsx("textarea", { "aria-label": "Footer HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: footerHtml, onChange: (e) => setFooterHtml(e.target.value) }), _jsxs("span", { className: "text-xs text-text-muted", children: [_jsx("code", { children: "{APP_TITLE}" }), " works here as well, and ", _jsx("code", { children: "{USER_MENU}" }), " takes the account menu (opening upward) when the header has none."] })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] })] }));
117
123
  }