@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.
- package/README.md +373 -88
- package/apps/admin/_layout.tsx +8 -2
- package/apps/admin/index.tsx +7 -1
- package/apps/admin/ingest-queue/index.tsx +2 -1
- package/apps/admin/mailboxes/[uid].tsx +23 -10
- package/apps/admin/quarantine/index.tsx +2 -1
- package/apps/admin/signing-certificates/index.tsx +343 -0
- package/apps/escrow/_layout.tsx +8 -2
- package/apps/shared/appearance/AppearanceForm.tsx +392 -0
- package/apps/shared/appearance/AppearanceHead.tsx +49 -0
- package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
- package/apps/shared/appearance/ColorField.tsx +107 -0
- package/apps/shared/appearance/appearanceCache.ts +106 -0
- package/apps/shared/appearance/appearanceContext.ts +77 -0
- package/apps/shared/appearance/bootScript.ts +35 -0
- package/apps/shared/appearance/color.ts +169 -0
- package/apps/shared/appearance/photo.ts +88 -0
- package/apps/shared/appearance/resolvedTheme.ts +67 -0
- package/apps/shared/appearance/theme.ts +355 -0
- package/apps/shared/auth/accountUrl.ts +9 -0
- package/apps/shared/auth/adminAccess.ts +99 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
- package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +2 -1
- package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
- package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
- package/apps/shared/components/contacts/ContactForm.tsx +11 -5
- package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
- package/apps/shared/components/contacts/ContactsToolbar.tsx +98 -115
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +25 -8
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
- package/apps/shared/components/layout/AppShell.tsx +477 -288
- package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -164
- package/apps/shared/components/layout/RailIcon.tsx +100 -0
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
- package/apps/shared/components/layout/UserMenu.tsx +372 -185
- package/apps/shared/components/mail/ConversationList.tsx +271 -262
- package/apps/shared/components/mail/ConversationThreadPane.tsx +147 -94
- package/apps/shared/components/mail/LazyReadingPane.tsx +113 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +227 -222
- package/apps/shared/components/mail/MessageDetailPane.tsx +1537 -1387
- package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
- package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -160
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1644 -1736
- package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
- package/apps/shared/components/mail/compose/composePerf.ts +46 -0
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
- package/apps/shared/components/mail/layout/MailShell.tsx +490 -472
- package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
- package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
- package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
- package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
- package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
- package/apps/shared/components/mail/reading/color.ts +245 -0
- package/apps/shared/components/mail/reading/frameControl.ts +142 -0
- package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
- package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
- package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
- package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
- package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
- package/apps/shared/components/mail/unreadStyle.tsx +75 -0
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +253 -240
- package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
- package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
- package/apps/shared/components/tasks/layout/TasksShell.tsx +24 -8
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
- package/apps/shared/keyboard/dispatch.ts +124 -0
- package/apps/shared/keyboard/format.ts +89 -0
- package/apps/shared/keyboard/keymap.ts +114 -0
- package/apps/shared/keyboard/match.ts +44 -0
- package/apps/shared/keyboard/parse.ts +136 -0
- package/apps/shared/keyboard/platform.ts +34 -0
- package/apps/shared/keyboard/registry.ts +65 -0
- package/apps/shared/keyboard/targets.ts +79 -0
- package/apps/shared/keyboard/useShortcut.ts +50 -0
- package/apps/shared/keyboard/useShortcutProps.ts +17 -0
- package/apps/shared/mail/folderCounts.ts +308 -0
- package/apps/shared/mail/folderTree.ts +143 -0
- package/apps/shared/mail/listSnapshots.ts +87 -0
- package/apps/shared/mail/messageReadState.ts +85 -0
- package/apps/shared/mail/newMailNotifications.ts +183 -0
- package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
- package/apps/shared/mail/outbox/composeBridge.ts +69 -0
- package/apps/shared/mail/outbox/outboxState.ts +88 -0
- package/apps/shared/mail/outbox/pendingSends.ts +146 -0
- package/apps/shared/mail/outbox/sendDecision.ts +157 -0
- package/apps/shared/mail/outbox/sendJob.ts +427 -0
- package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
- package/apps/shared/mail/outbox/sendState.ts +35 -0
- package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
- package/apps/shared/mail/useMailConnection.ts +203 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -224
- package/apps/shared/mail/useMarkMessageRead.ts +47 -0
- package/apps/shared/mail/useNewMailNotifications.ts +178 -0
- package/apps/shared/mail/useUnreadTitle.ts +42 -0
- package/apps/shared/navigation/AppRouter.tsx +300 -0
- package/apps/shared/navigation/appHrefs.ts +23 -0
- package/apps/shared/navigation/frameContext.tsx +35 -0
- package/apps/shared/navigation/idle.ts +45 -0
- package/apps/shared/navigation/routerContext.tsx +83 -0
- package/apps/shared/navigation/routes.ts +72 -0
- package/apps/shared/notifications/NotificationCenter.tsx +215 -0
- package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
- package/apps/shared/notifications/apiErrors.ts +98 -0
- package/apps/shared/notifications/headerOffset.ts +35 -0
- package/apps/shared/notifications/pushStatus.ts +56 -0
- package/apps/shared/notifications/store.ts +549 -0
- package/apps/shared/notifications/systemErrors.ts +63 -0
- package/apps/shared/notifications/useNotifications.ts +41 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
- package/apps/shared/signing/enrollmentStorage.ts +33 -0
- package/apps/shared/signing/enrollmentTracker.ts +385 -0
- package/apps/shared/signing/enrollmentView.ts +251 -0
- package/apps/shared/signing/signingInfo.ts +58 -0
- package/apps/shared/signing/useNow.ts +19 -0
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
- package/apps/shared/styles/app.css +108 -10
- package/apps/www/_layout.tsx +8 -2
- package/apps/www/_routedPage.tsx +24 -0
- package/apps/www/_routes.ts +36 -0
- package/apps/www/calendar/index.tsx +64 -33
- package/apps/www/contacts/[uid].tsx +109 -107
- package/apps/www/contacts/index.tsx +62 -42
- package/apps/www/index.tsx +2291 -1917
- package/apps/www/messages/[uid].tsx +108 -101
- package/apps/www/settings/appearance/index.tsx +21 -0
- package/apps/www/settings/auto-reply/index.tsx +7 -8
- package/apps/www/settings/encryption/index.tsx +1287 -1249
- package/apps/www/settings/filters/[uid].tsx +7 -2
- package/apps/www/settings/filters/index.tsx +102 -99
- package/apps/www/settings/filters/new/index.tsx +140 -133
- package/apps/www/settings/labels/index.tsx +204 -201
- package/apps/www/settings/privacy/index.tsx +7 -9
- package/apps/www/settings/read-receipts/index.tsx +7 -8
- package/apps/www/settings/sharing/index.tsx +256 -274
- package/apps/www/settings/signatures/[uid].tsx +172 -167
- package/apps/www/settings/signatures/index.tsx +88 -85
- package/apps/www/settings/signatures/new/index.tsx +135 -129
- package/apps/www/tasks/index.tsx +29 -18
- package/dist/apps/admin/_layout.d.ts +5 -1
- package/dist/apps/admin/_layout.js +3 -2
- package/dist/apps/admin/index.js +3 -2
- package/dist/apps/admin/ingest-queue/index.js +2 -1
- package/dist/apps/admin/mailboxes/[uid].js +9 -6
- package/dist/apps/admin/quarantine/index.js +2 -1
- package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
- package/dist/apps/admin/signing-certificates/index.js +161 -0
- package/dist/apps/escrow/_layout.d.ts +5 -1
- package/dist/apps/escrow/_layout.js +3 -2
- package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
- package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
- package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
- package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
- package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
- package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
- package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
- package/dist/apps/shared/appearance/ColorField.js +40 -0
- package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
- package/dist/apps/shared/appearance/appearanceCache.js +80 -0
- package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
- package/dist/apps/shared/appearance/appearanceContext.js +29 -0
- package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
- package/dist/apps/shared/appearance/bootScript.js +33 -0
- package/dist/apps/shared/appearance/color.d.ts +65 -0
- package/dist/apps/shared/appearance/color.js +132 -0
- package/dist/apps/shared/appearance/photo.d.ts +17 -0
- package/dist/apps/shared/appearance/photo.js +79 -0
- package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
- package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
- package/dist/apps/shared/appearance/theme.d.ts +111 -0
- package/dist/apps/shared/appearance/theme.js +251 -0
- package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
- package/dist/apps/shared/auth/accountUrl.js +8 -0
- package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
- package/dist/apps/shared/auth/adminAccess.js +89 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
- package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +1 -1
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
- package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +6 -2
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +28 -10
- package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +19 -6
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
- package/dist/apps/shared/components/layout/AppShell.d.ts +34 -4
- package/dist/apps/shared/components/layout/AppShell.js +122 -17
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
- package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +3 -1
- package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
- package/dist/apps/shared/components/layout/RailIcon.js +85 -0
- package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
- package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
- package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
- package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +25 -3
- package/dist/apps/shared/components/layout/UserMenu.js +88 -7
- package/dist/apps/shared/components/mail/ConversationList.js +5 -11
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +6 -2
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +87 -40
- package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
- package/dist/apps/shared/components/mail/LazyReadingPane.js +83 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +25 -8
- package/dist/apps/shared/components/mail/MessageDetailPane.js +167 -73
- package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
- package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
- package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
- package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +38 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +97 -6
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +3 -2
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +316 -339
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
- package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
- package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +14 -4
- package/dist/apps/shared/components/mail/layout/MailShell.js +90 -104
- package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
- package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
- package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
- package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
- package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
- package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
- package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
- package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
- package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
- package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
- package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
- package/dist/apps/shared/components/mail/reading/color.js +201 -0
- package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
- package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
- package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
- package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
- package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
- package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
- package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
- package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
- package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
- package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
- package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
- package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
- package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
- package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
- package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +16 -5
- package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
- package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
- package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
- package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +18 -6
- package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
- package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
- package/dist/apps/shared/keyboard/dispatch.js +109 -0
- package/dist/apps/shared/keyboard/format.d.ts +12 -0
- package/dist/apps/shared/keyboard/format.js +74 -0
- package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
- package/dist/apps/shared/keyboard/keymap.js +84 -0
- package/dist/apps/shared/keyboard/match.d.ts +20 -0
- package/dist/apps/shared/keyboard/match.js +31 -0
- package/dist/apps/shared/keyboard/parse.d.ts +31 -0
- package/dist/apps/shared/keyboard/parse.js +107 -0
- package/dist/apps/shared/keyboard/platform.d.ts +15 -0
- package/dist/apps/shared/keyboard/platform.js +22 -0
- package/dist/apps/shared/keyboard/registry.d.ts +39 -0
- package/dist/apps/shared/keyboard/registry.js +33 -0
- package/dist/apps/shared/keyboard/targets.d.ts +14 -0
- package/dist/apps/shared/keyboard/targets.js +67 -0
- package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
- package/dist/apps/shared/keyboard/useShortcut.js +35 -0
- package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
- package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
- package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
- package/dist/apps/shared/mail/folderCounts.js +218 -0
- package/dist/apps/shared/mail/folderTree.d.ts +55 -0
- package/dist/apps/shared/mail/folderTree.js +116 -0
- package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
- package/dist/apps/shared/mail/listSnapshots.js +43 -0
- package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
- package/dist/apps/shared/mail/messageReadState.js +63 -0
- package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
- package/dist/apps/shared/mail/newMailNotifications.js +138 -0
- package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
- package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
- package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
- package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
- package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
- package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
- package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
- package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
- package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
- package/dist/apps/shared/mail/outbox/sendState.js +8 -0
- package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
- package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
- package/dist/apps/shared/mail/useMailConnection.d.ts +56 -0
- package/dist/apps/shared/mail/useMailConnection.js +138 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +29 -9
- package/dist/apps/shared/mail/useMailLiveUpdates.js +60 -32
- package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
- package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
- package/dist/apps/shared/mail/useNewMailNotifications.d.ts +35 -0
- package/dist/apps/shared/mail/useNewMailNotifications.js +130 -0
- package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
- package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
- package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
- package/dist/apps/shared/navigation/AppRouter.js +242 -0
- package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
- package/dist/apps/shared/navigation/appHrefs.js +20 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
- package/dist/apps/shared/navigation/frameContext.js +24 -0
- package/dist/apps/shared/navigation/idle.d.ts +14 -0
- package/dist/apps/shared/navigation/idle.js +43 -0
- package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
- package/dist/apps/shared/navigation/routerContext.js +56 -0
- package/dist/apps/shared/navigation/routes.d.ts +32 -0
- package/dist/apps/shared/navigation/routes.js +37 -0
- package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
- package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
- package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
- package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
- package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
- package/dist/apps/shared/notifications/apiErrors.js +86 -0
- package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
- package/dist/apps/shared/notifications/headerOffset.js +33 -0
- package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
- package/dist/apps/shared/notifications/pushStatus.js +54 -0
- package/dist/apps/shared/notifications/store.d.ts +136 -0
- package/dist/apps/shared/notifications/store.js +416 -0
- package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
- package/dist/apps/shared/notifications/systemErrors.js +58 -0
- package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
- package/dist/apps/shared/notifications/useNotifications.js +18 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
- package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
- package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
- package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
- package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
- package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
- package/dist/apps/shared/signing/enrollmentView.js +214 -0
- package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
- package/dist/apps/shared/signing/signingInfo.js +48 -0
- package/dist/apps/shared/signing/useNow.d.ts +2 -0
- package/dist/apps/shared/signing/useNow.js +18 -0
- package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
- package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
- package/dist/apps/shared/styles/app.css +108 -10
- package/dist/apps/www/_layout.d.ts +5 -1
- package/dist/apps/www/_layout.js +3 -2
- package/dist/apps/www/_routedPage.d.ts +12 -0
- package/dist/apps/www/_routedPage.js +19 -0
- package/dist/apps/www/_routes.d.ts +11 -0
- package/dist/apps/www/_routes.js +30 -0
- package/dist/apps/www/calendar/index.d.ts +2 -2
- package/dist/apps/www/calendar/index.js +41 -11
- package/dist/apps/www/contacts/[uid].d.ts +3 -9
- package/dist/apps/www/contacts/[uid].js +11 -9
- package/dist/apps/www/contacts/index.d.ts +2 -2
- package/dist/apps/www/contacts/index.js +30 -24
- package/dist/apps/www/index.d.ts +2 -2
- package/dist/apps/www/index.js +332 -48
- package/dist/apps/www/messages/[uid].d.ts +3 -7
- package/dist/apps/www/messages/[uid].js +10 -5
- package/dist/apps/www/settings/appearance/index.d.ts +4 -0
- package/dist/apps/www/settings/appearance/index.js +13 -0
- package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
- package/dist/apps/www/settings/auto-reply/index.js +6 -7
- package/dist/apps/www/settings/encryption/index.d.ts +2 -2
- package/dist/apps/www/settings/encryption/index.js +106 -96
- package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
- package/dist/apps/www/settings/filters/[uid].js +6 -2
- package/dist/apps/www/settings/filters/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/index.js +3 -1
- package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/new/index.js +9 -3
- package/dist/apps/www/settings/labels/index.d.ts +2 -2
- package/dist/apps/www/settings/labels/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.d.ts +2 -2
- package/dist/apps/www/settings/privacy/index.js +8 -9
- package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
- package/dist/apps/www/settings/read-receipts/index.js +6 -7
- package/dist/apps/www/settings/sharing/index.d.ts +2 -2
- package/dist/apps/www/settings/sharing/index.js +22 -34
- package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
- package/dist/apps/www/settings/signatures/[uid].js +6 -2
- package/dist/apps/www/settings/signatures/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/index.js +3 -1
- package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/new/index.js +9 -4
- package/dist/apps/www/tasks/index.d.ts +2 -2
- package/dist/apps/www/tasks/index.js +22 -16
- package/package.json +2 -2
- package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* Turns a user's `AppearancePreferences` into CSS - pure functions, no DOM (`AppearanceProvider` applies the result).
|
|
7
|
+
*
|
|
8
|
+
* **How the theme reaches the app.** The app's colours are custom properties (`--rr-color-*`, mapped to Tailwind's colours in
|
|
9
|
+
* `app.css`'s `@theme`), with a light set on `:root` and a dark set for `prefers-color-scheme: dark` and `[data-theme="dark"]`. A
|
|
10
|
+
* deployment's branding stylesheet re-points the same properties. The user's choices are emitted as one more set of those
|
|
11
|
+
* properties, `!important` and for both schemes (`appearanceCss()`), so precedence is: built-in defaults < the branding stylesheet <
|
|
12
|
+
* the user. A custom property declared `!important` on `<html>` cannot be beaten by any ordinary rule, however specific - but a
|
|
13
|
+
* branding stylesheet's own *rules* (say `nav { background: ... }`) are untouched and still decide their element, exactly as they
|
|
14
|
+
* always did; only the tokens are the user's.
|
|
15
|
+
*
|
|
16
|
+
* **Background.** With a background set, `--color-surface` and `--color-surface-alt` (what `bg-surface` and `bg-surface-alt` read)
|
|
17
|
+
* become the scheme's surface colour at an alpha, so the rail and header are translucent panels over the fixed background layer,
|
|
18
|
+
* and the content region (`#app-content`) is one such panel itself, its own `bg-surface` children transparent so panels never stack
|
|
19
|
+
* into an opaque slab; menus, dialogs and pop-ups reset both tokens to the opaque colour again (see `app.css`). The alpha is chosen
|
|
20
|
+
* so text stays at AA contrast over the photo (`panelAlpha()`).
|
|
21
|
+
*/
|
|
22
|
+
import type {
|
|
23
|
+
AppearanceBackground,
|
|
24
|
+
AppearanceColors,
|
|
25
|
+
AppearancePreferences,
|
|
26
|
+
} from "@rapidmx/react-shared/appearance/preferencesApi.js";
|
|
27
|
+
import {
|
|
28
|
+
AA_CONTRAST,
|
|
29
|
+
BLACK,
|
|
30
|
+
DARK_TEXT,
|
|
31
|
+
Rgb,
|
|
32
|
+
WHITE,
|
|
33
|
+
contrastRatio,
|
|
34
|
+
deriveScale,
|
|
35
|
+
ensureContrast,
|
|
36
|
+
mix,
|
|
37
|
+
parseHex,
|
|
38
|
+
readableOn,
|
|
39
|
+
relativeLuminance,
|
|
40
|
+
toHex,
|
|
41
|
+
} from "./color.js";
|
|
42
|
+
import type { ResolvedTheme } from "./resolvedTheme.js";
|
|
43
|
+
|
|
44
|
+
/** What each scheme's tokens are without any customisation - `app.css`'s own values. */
|
|
45
|
+
export interface SchemePalette {
|
|
46
|
+
surface: string;
|
|
47
|
+
text: string;
|
|
48
|
+
muted: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const DEFAULT_PALETTE: Record<ResolvedTheme, SchemePalette> = {
|
|
52
|
+
light: { surface: "#ffffff", text: "#1c2526", muted: "#5a6b6e" },
|
|
53
|
+
dark: { surface: "#1b2022", text: "#eef2f3", muted: "#9fb0b3" },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** The colours in effect for a scheme: the user's surface and text where chosen, else the defaults. */
|
|
57
|
+
export interface EffectivePalette {
|
|
58
|
+
surface: Rgb;
|
|
59
|
+
text: Rgb;
|
|
60
|
+
muted: Rgb;
|
|
61
|
+
/** The user chose a surface. */
|
|
62
|
+
surfaceChosen: boolean;
|
|
63
|
+
/** The user chose a text colour, or the default one could not be read on their surface and was replaced. */
|
|
64
|
+
textAdjusted: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** How far the text colour is mixed toward the surface to get the muted text colour. */
|
|
68
|
+
const MUTED_MIX: Record<ResolvedTheme, number> = { light: 0.3, dark: 0.35 };
|
|
69
|
+
/** How far the surface is mixed toward the text colour to get `surface-alt`. */
|
|
70
|
+
const ALT_MIX: Record<ResolvedTheme, number> = { light: 0.04, dark: 0.03 };
|
|
71
|
+
const BORDER_MIX = 0.12;
|
|
72
|
+
/** 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. */
|
|
73
|
+
const BACKGROUND_MUTED_MIX = 0.18;
|
|
74
|
+
|
|
75
|
+
/** `#rrggbb` known valid (`normalizeAppearance()` and the colour inputs guarantee it). */
|
|
76
|
+
function rgbOf(hex: string): Rgb {
|
|
77
|
+
return parseHex(hex) as Rgb;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function effectivePalette(colors: AppearanceColors | undefined, scheme: ResolvedTheme): EffectivePalette {
|
|
81
|
+
const defaults = DEFAULT_PALETTE[scheme];
|
|
82
|
+
const surface = rgbOf(colors?.surface ?? defaults.surface);
|
|
83
|
+
let text = rgbOf(colors?.text ?? defaults.text);
|
|
84
|
+
let textAdjusted = colors?.text !== undefined;
|
|
85
|
+
if (!textAdjusted && colors?.surface !== undefined && contrastRatio(text, surface) < AA_CONTRAST) {
|
|
86
|
+
// A surface the app's own text can't be read on: the text follows it, rather than leaving the page unreadable.
|
|
87
|
+
text = readableOn(surface, DARK_TEXT);
|
|
88
|
+
textAdjusted = true;
|
|
89
|
+
}
|
|
90
|
+
const customised = colors?.surface !== undefined || textAdjusted;
|
|
91
|
+
const muted = customised
|
|
92
|
+
? ensureContrast(mix(text, surface, MUTED_MIX[scheme]), surface, AA_CONTRAST)
|
|
93
|
+
: rgbOf(defaults.muted);
|
|
94
|
+
return { surface, text, muted, surfaceChosen: colors?.surface !== undefined, textAdjusted };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The custom properties a user's colours set for one scheme - only those they changed (and what derives from them): the primary
|
|
99
|
+
* colour's seven-step scale and the text on it, the accent and its shades, and the surface / text family. Empty when they chose
|
|
100
|
+
* no colours.
|
|
101
|
+
*/
|
|
102
|
+
export function colorVariables(colors: AppearanceColors | undefined, scheme: ResolvedTheme): Record<string, string> {
|
|
103
|
+
const vars: Record<string, string> = {};
|
|
104
|
+
if (!colors) {
|
|
105
|
+
return vars;
|
|
106
|
+
}
|
|
107
|
+
const primary = colors.primary ? deriveScale(colors.primary) : undefined;
|
|
108
|
+
if (primary) {
|
|
109
|
+
vars["--rr-color-primary"] = primary.base;
|
|
110
|
+
vars["--rr-color-primary-dark"] = primary.dark;
|
|
111
|
+
vars["--rr-color-primary-darker"] = primary.darker;
|
|
112
|
+
vars["--rr-color-primary-darkest"] = primary.darkest;
|
|
113
|
+
vars["--rr-color-primary-light"] = primary.light;
|
|
114
|
+
vars["--rr-color-primary-lighter"] = primary.lighter;
|
|
115
|
+
vars["--rr-color-primary-lightest"] = primary.lightest;
|
|
116
|
+
vars["--rr-color-text-on-primary"] = primary.onBase;
|
|
117
|
+
}
|
|
118
|
+
const accent = colors.accent ? parseHex(colors.accent) : undefined;
|
|
119
|
+
if (accent) {
|
|
120
|
+
vars["--rr-color-accent"] = toHex(accent);
|
|
121
|
+
vars["--rr-color-accent-dark"] = toHex(mix(accent, BLACK, 0.25));
|
|
122
|
+
vars["--rr-color-accent-soft"] = toHex(mix(accent, WHITE, 0.2));
|
|
123
|
+
vars["--rr-color-accent-pale"] = toHex(mix(accent, WHITE, 0.8));
|
|
124
|
+
vars["--rr-color-text-on-accent"] = toHex(readableOn(accent, DARK_TEXT));
|
|
125
|
+
}
|
|
126
|
+
const palette = effectivePalette(colors, scheme);
|
|
127
|
+
if (palette.surfaceChosen || palette.textAdjusted) {
|
|
128
|
+
vars["--rr-color-border"] = toHex(mix(palette.surface, palette.text, BORDER_MIX));
|
|
129
|
+
vars["--rr-color-text-muted"] = toHex(palette.muted);
|
|
130
|
+
}
|
|
131
|
+
if (palette.surfaceChosen) {
|
|
132
|
+
vars["--rr-color-surface"] = toHex(palette.surface);
|
|
133
|
+
vars["--rr-color-surface-alt"] = toHex(mix(palette.surface, palette.text, ALT_MIX[scheme]));
|
|
134
|
+
// The page behind the panels: the surface itself, except that a dark surface sits on a slightly deeper page, as the defaults do.
|
|
135
|
+
vars["--rr-color-bg"] = toHex(relativeLuminance(palette.surface) < 0.18 ? mix(palette.surface, BLACK, 0.25) : palette.surface);
|
|
136
|
+
}
|
|
137
|
+
if (palette.textAdjusted) {
|
|
138
|
+
vars["--rr-color-text"] = toHex(palette.text);
|
|
139
|
+
}
|
|
140
|
+
return vars;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The lightest and darkest a background can be behind a panel, as colours. */
|
|
144
|
+
export interface PhotoRange {
|
|
145
|
+
lo: Rgb;
|
|
146
|
+
hi: Rgb;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** What is assumed of a photo nobody has measured: black in one corner and white in another. */
|
|
150
|
+
export const WORST_CASE_PHOTO: PhotoRange = { lo: BLACK, hi: WHITE };
|
|
151
|
+
|
|
152
|
+
/** The panels are never more see-through than this ... */
|
|
153
|
+
export const PANEL_ALPHA_MIN = 0.5;
|
|
154
|
+
/** ... nor less than this (still a hint of the picture). */
|
|
155
|
+
export const PANEL_ALPHA_MAX = 0.96;
|
|
156
|
+
/** Contrast the panels are made to keep, a little over AA to leave room for the compression of a photo. */
|
|
157
|
+
const PANEL_CONTRAST = 4.6;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 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
|
|
161
|
+
* 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
|
|
162
|
+
* 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) *
|
|
163
|
+
* (1 - dim)`. A wide-ranging (busy) photo therefore gets more opaque panels than a plain one, and more dim allows less opaque
|
|
164
|
+
* panels.
|
|
165
|
+
*/
|
|
166
|
+
export function panelAlpha(palette: Pick<EffectivePalette, "surface" | "text" | "muted">, dim: number, photo: PhotoRange): number {
|
|
167
|
+
for (let alpha = PANEL_ALPHA_MIN; alpha < PANEL_ALPHA_MAX; alpha = Math.round((alpha + 0.01) * 100) / 100) {
|
|
168
|
+
const share = (1 - alpha) * (1 - dim);
|
|
169
|
+
const readable = [photo.lo, photo.hi].every((behind) => {
|
|
170
|
+
const under = mix(palette.surface, behind, share);
|
|
171
|
+
return contrastRatio(palette.text, under) >= PANEL_CONTRAST && contrastRatio(palette.muted, under) >= PANEL_CONTRAST;
|
|
172
|
+
});
|
|
173
|
+
if (readable) {
|
|
174
|
+
return alpha;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return PANEL_ALPHA_MAX;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** What a background shows behind the panels - `undefined` for none: a colour, or an image of unknown or measured lightness. */
|
|
181
|
+
export function backgroundPhoto(background: AppearanceBackground | undefined, measured?: PhotoRange): PhotoRange | undefined {
|
|
182
|
+
if (!background || background.kind === "none") {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
if (background.kind === "color") {
|
|
186
|
+
const colour = rgbOf(background.color as string);
|
|
187
|
+
return { lo: colour, hi: colour };
|
|
188
|
+
}
|
|
189
|
+
return measured ?? WORST_CASE_PHOTO;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The dim actually laid over the background: a colour has no picture to dim. */
|
|
193
|
+
export function effectiveDim(background: AppearanceBackground): number {
|
|
194
|
+
return background.kind === "image" ? background.dim : 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The custom properties for one scheme: the user's colours, plus - when a background is set - the translucent panel colours and the
|
|
199
|
+
* transparent frame. `measured` is the image's measured lightness range, when known (`photo.ts`).
|
|
200
|
+
*/
|
|
201
|
+
export function themeVariables(prefs: AppearancePreferences, scheme: ResolvedTheme, measured?: PhotoRange): Record<string, string> {
|
|
202
|
+
const vars = colorVariables(prefs.colors, scheme);
|
|
203
|
+
const photo = backgroundPhoto(prefs.background, measured);
|
|
204
|
+
if (photo && prefs.background) {
|
|
205
|
+
const palette = effectivePalette(prefs.colors, scheme);
|
|
206
|
+
// Muted text is what limits how see-through the panels can be, so over a background it is a shade stronger.
|
|
207
|
+
const muted = ensureContrast(mix(palette.text, palette.surface, BACKGROUND_MUTED_MIX), palette.surface, AA_CONTRAST);
|
|
208
|
+
const alpha = panelAlpha({ surface: palette.surface, text: palette.text, muted }, effectiveDim(prefs.background), photo);
|
|
209
|
+
const percent = `${Math.round(alpha * 100)}%`;
|
|
210
|
+
vars["--rr-panel"] = `color-mix(in srgb, var(--rr-color-surface) ${percent}, transparent)`;
|
|
211
|
+
vars["--color-surface"] = "var(--rr-panel)";
|
|
212
|
+
vars["--color-surface-alt"] = `color-mix(in srgb, var(--rr-color-surface-alt) ${percent}, transparent)`;
|
|
213
|
+
vars["--rr-color-text-muted"] = toHex(muted);
|
|
214
|
+
vars["--rr-panel-alpha"] = String(alpha);
|
|
215
|
+
vars["--rr-frame-bg"] = "transparent";
|
|
216
|
+
}
|
|
217
|
+
return vars;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function declarations(vars: Record<string, string>): string {
|
|
221
|
+
return Object.entries(vars)
|
|
222
|
+
.map(([name, value]) => `${name}:${value} !important;`)
|
|
223
|
+
.join("");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The element `appearanceCss()` writes into `<head>`, so an update replaces rather than duplicates it. */
|
|
227
|
+
export const APPEARANCE_STYLE_ID = "rr-appearance";
|
|
228
|
+
|
|
229
|
+
/** A string as a CSS `url()` argument: quoted, with what could end the string or the declaration escaped. */
|
|
230
|
+
export function cssUrl(url: string): string {
|
|
231
|
+
const escaped = url.replace(/[\\"\p{Cc}]/gu, (char) => "\\" + char.charCodeAt(0).toString(16) + " ");
|
|
232
|
+
return `url("${escaped}")`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The rules for the background layer, which is two pseudo-elements of `<html>` rather than any markup - so it exists from the
|
|
237
|
+
* first byte of a server-rendered page (no flash), follows a live preview by rewriting one stylesheet, and needs no component.
|
|
238
|
+
* `::before` is the picture (or colour): `position: fixed`, behind everything (`z-index: -1` puts it above the page's own canvas
|
|
239
|
+
* 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
|
|
240
|
+
* dim: the scheme's surface colour at the chosen opacity. Neither takes a click, and neither prints.
|
|
241
|
+
*/
|
|
242
|
+
export function backgroundCss(background: AppearanceBackground | undefined, imageUrl?: string): string {
|
|
243
|
+
if (!background || background.kind === "none") {
|
|
244
|
+
return "";
|
|
245
|
+
}
|
|
246
|
+
const layer = "content:\"\";position:fixed;z-index:-1;pointer-events:none;";
|
|
247
|
+
if (background.kind === "color") {
|
|
248
|
+
return [`html::before{${layer}inset:0;background-color:${background.color};}`, "@media print{html::before{display:none}}"].join("\n");
|
|
249
|
+
}
|
|
250
|
+
const grow = -Math.round(background.blur * 2);
|
|
251
|
+
const size =
|
|
252
|
+
background.fit === "cover"
|
|
253
|
+
? "background-size:cover;background-repeat:no-repeat;background-position:center;"
|
|
254
|
+
: background.fit === "contain"
|
|
255
|
+
? "background-size:contain;background-repeat:no-repeat;background-position:center;"
|
|
256
|
+
: "background-size:auto;background-repeat:repeat;background-position:center;";
|
|
257
|
+
const image = imageUrl ? `background-image:${cssUrl(imageUrl)};` : "";
|
|
258
|
+
const color = background.color ?? "var(--rr-color-surface-alt)";
|
|
259
|
+
const blur = background.blur > 0 ? `filter:blur(${background.blur}px);` : "";
|
|
260
|
+
return [
|
|
261
|
+
`html::before{${layer}inset:${grow}px;background-color:${color};${image}${size}${blur}}`,
|
|
262
|
+
`html::after{${layer}inset:0;background-color:var(--rr-color-surface);opacity:${background.dim};}`,
|
|
263
|
+
"@media print{html::before,html::after{display:none}}",
|
|
264
|
+
].join("\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface AppearanceCssOptions {
|
|
268
|
+
/** The image's measured lightness range, when known (`photo.ts`); an unmeasured photo is assumed the worst case. */
|
|
269
|
+
measured?: PhotoRange;
|
|
270
|
+
/** Where the background image comes from - the server's URL for the stored version, or a `blob:` URL while an upload is in flight. */
|
|
271
|
+
imageUrl?: string;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The stylesheet text for the user's preferences, or `""` when they change nothing. Three rules per set of properties so each
|
|
276
|
+
* scheme is right in every case: the light set for everything except an explicit dark (`data-theme="dark"`) - which also covers
|
|
277
|
+
* 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
|
|
278
|
+
* provider ran, or a server-rendered page following "System"); and the dark set for an explicit `data-theme="dark"`. All three are
|
|
279
|
+
* `html:root...` (0,2,1) so the later dark rules win the equal-specificity tie against the light one. Then the background layer
|
|
280
|
+
* (`backgroundCss()`).
|
|
281
|
+
*/
|
|
282
|
+
export function appearanceCss(prefs: AppearancePreferences | undefined, options: AppearanceCssOptions = {}): string {
|
|
283
|
+
if (!prefs) {
|
|
284
|
+
return "";
|
|
285
|
+
}
|
|
286
|
+
const light = declarations(themeVariables(prefs, "light", options.measured));
|
|
287
|
+
const dark = declarations(themeVariables(prefs, "dark", options.measured));
|
|
288
|
+
const rules: string[] = [];
|
|
289
|
+
if (light || dark) {
|
|
290
|
+
rules.push(
|
|
291
|
+
`html:root:not([data-theme="dark"]){${light}}`,
|
|
292
|
+
`@media (prefers-color-scheme:dark){html:root:not([data-theme]){${dark}}}`,
|
|
293
|
+
`html:root[data-theme="dark"]{${dark}}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (prefs.colors?.primary) {
|
|
297
|
+
// Several components write `bg-primary text-white`; on a primary colour the user picked, white may be unreadable.
|
|
298
|
+
rules.push(".bg-primary.text-white{color:var(--rr-color-text-on-primary)}");
|
|
299
|
+
}
|
|
300
|
+
const background = backgroundCss(prefs.background, options.imageUrl);
|
|
301
|
+
if (background) {
|
|
302
|
+
// The content region is one translucent panel; what is inside it (`bg-surface`) shows it instead of adding a second layer.
|
|
303
|
+
rules.push(
|
|
304
|
+
"#app-content{background-color:var(--rr-panel);--color-surface:transparent;}",
|
|
305
|
+
// Fields keep a solid surface: a box that is see-through has no edge to find.
|
|
306
|
+
"#app-content :is(input,select,textarea){--color-surface:var(--rr-color-surface);}",
|
|
307
|
+
// Someone who asked their system for less transparency gets solid panels.
|
|
308
|
+
"@media (prefers-reduced-transparency:reduce){html:root{--rr-panel:var(--rr-color-surface) !important;--color-surface-alt:var(--rr-color-surface-alt) !important;}}",
|
|
309
|
+
background,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return rules.join("\n");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** A pair of colours that is hard to read, as `ContrastWarning`s describe them. */
|
|
316
|
+
export interface ContrastWarning {
|
|
317
|
+
id: "text-surface" | "accent-text";
|
|
318
|
+
ratio: number;
|
|
319
|
+
message: string;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 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
|
|
324
|
+
* 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
|
|
325
|
+
* touched (the app's own pairs are the app's business).
|
|
326
|
+
*/
|
|
327
|
+
export function contrastWarnings(colors: AppearanceColors | undefined, scheme: ResolvedTheme): ContrastWarning[] {
|
|
328
|
+
const warnings: ContrastWarning[] = [];
|
|
329
|
+
if (!colors) {
|
|
330
|
+
return warnings;
|
|
331
|
+
}
|
|
332
|
+
if (colors.surface !== undefined || colors.text !== undefined) {
|
|
333
|
+
const palette = effectivePalette(colors, scheme);
|
|
334
|
+
const ratio = contrastRatio(palette.text, palette.surface);
|
|
335
|
+
if (ratio < AA_CONTRAST) {
|
|
336
|
+
warnings.push({
|
|
337
|
+
id: "text-surface",
|
|
338
|
+
ratio,
|
|
339
|
+
message: `Text on the surface colour has a contrast of ${ratio.toFixed(1)}:1; 4.5:1 or more is easier to read.`,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (colors.accent !== undefined) {
|
|
344
|
+
const accent = rgbOf(colors.accent);
|
|
345
|
+
const ratio = contrastRatio(readableOn(accent, DARK_TEXT), accent);
|
|
346
|
+
if (ratio < AA_CONTRAST) {
|
|
347
|
+
warnings.push({
|
|
348
|
+
id: "accent-text",
|
|
349
|
+
ratio,
|
|
350
|
+
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.`,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return warnings;
|
|
355
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
|
|
6
|
+
/** auth-server's account page, or `undefined` when no auth-server is configured. Tolerates a configured URL with a trailing slash. */
|
|
7
|
+
export function accountUrlOf(authServerUrl: string | undefined): string | undefined {
|
|
8
|
+
return authServerUrl ? `${authServerUrl.replace(/\/+$/, "")}/account` : undefined;
|
|
9
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { authApiFetch } from "@rapidmx/react-shared/util/api.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether the signed-in user holds an administrator role, learned without elevating.
|
|
9
|
+
*
|
|
10
|
+
* The JWT the browser carries can't say: auth-server strips every trusted role from a token that isn't elevated
|
|
11
|
+
* (`TokenUtils.resolveTokenUser()`), so `trusted` (from the JWT) is false for a real administrator until they confirm
|
|
12
|
+
* their identity on auth-server's elevation page. The role itself lives on the caller's own `User` record, which they may
|
|
13
|
+
* always read - `GET {authServerUrl}/api/users/me` (the same call auth-server's own UI makes "to check for admin access") -
|
|
14
|
+
* so the answer is read from there, with the same cross-origin credentials the profile lookups use.
|
|
15
|
+
*
|
|
16
|
+
* This only decides whether a menu item is shown. The `/admin` pages and API stay authoritative: they check the role and
|
|
17
|
+
* the elevation themselves, and the link only navigates (`/admin` then runs the elevation redirect), so a wrong answer
|
|
18
|
+
* - a stale cache, a tampered `sessionStorage` - can show a link that leads to "no administrator access", never more.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The roles the server treats as trusted when it isn't configured otherwise (`trusted_roles`, default `["admin"]`). */
|
|
22
|
+
export const DEFAULT_TRUSTED_ROLES: readonly string[] = ["admin"];
|
|
23
|
+
|
|
24
|
+
/** `sessionStorage` key prefix of a remembered answer; the user's uid follows, so another account signing in in the same
|
|
25
|
+
* tab never inherits it. */
|
|
26
|
+
export const ADMIN_ACCESS_KEY_PREFIX = "rapidmx-admin-access:";
|
|
27
|
+
|
|
28
|
+
/** How long a remembered answer is trusted - long enough that navigating between the apps costs no request, short enough
|
|
29
|
+
* that a role granted or removed shows up within the sitting. */
|
|
30
|
+
export const ADMIN_ACCESS_TTL_MS = 30 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
/** Lookups in flight, by key, so two components asking at once make one request. */
|
|
33
|
+
const lookups = new Map<string, Promise<boolean | undefined>>();
|
|
34
|
+
|
|
35
|
+
/** Forgets the lookups in flight (tests). */
|
|
36
|
+
export function resetAdminAccessLookups(): void {
|
|
37
|
+
lookups.clear();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readRemembered(key: string, now: number): boolean | undefined {
|
|
41
|
+
try {
|
|
42
|
+
const stored = JSON.parse(sessionStorage.getItem(key) ?? "null") as { admin?: unknown; at?: unknown } | null;
|
|
43
|
+
if (typeof stored?.admin === "boolean" && typeof stored.at === "number" && now - stored.at >= 0 && now - stored.at < ADMIN_ACCESS_TTL_MS) {
|
|
44
|
+
return stored.admin;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Unreadable, or storage unavailable - ask again.
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function remember(key: string, admin: boolean, now: number): void {
|
|
53
|
+
try {
|
|
54
|
+
sessionStorage.setItem(key, JSON.stringify({ admin, at: now }));
|
|
55
|
+
} catch {
|
|
56
|
+
// Storage unavailable - the next page load asks again.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolves `true` when `userUid`'s own `User` record on auth-server carries one of `trustedRoles`, `false` when it
|
|
62
|
+
* doesn't, and `undefined` when that couldn't be established (unreachable, blocked by CORS, not signed in, an unexpected
|
|
63
|
+
* body, or a record that isn't this user's). Only a real answer is remembered; a failure is asked again on the next page.
|
|
64
|
+
* Never rejects.
|
|
65
|
+
*/
|
|
66
|
+
export function lookUpAdminAccess(
|
|
67
|
+
authServerUrl: string,
|
|
68
|
+
userUid: string,
|
|
69
|
+
trustedRoles: readonly string[] = DEFAULT_TRUSTED_ROLES,
|
|
70
|
+
now: number = Date.now(),
|
|
71
|
+
): Promise<boolean | undefined> {
|
|
72
|
+
const key = `${ADMIN_ACCESS_KEY_PREFIX}${userUid}`;
|
|
73
|
+
const remembered = readRemembered(key, now);
|
|
74
|
+
if (remembered !== undefined) {
|
|
75
|
+
return Promise.resolve(remembered);
|
|
76
|
+
}
|
|
77
|
+
const inFlight = lookups.get(key);
|
|
78
|
+
if (inFlight) {
|
|
79
|
+
return inFlight;
|
|
80
|
+
}
|
|
81
|
+
const lookup = (async (): Promise<boolean | undefined> => {
|
|
82
|
+
try {
|
|
83
|
+
const user = await authApiFetch<{ uid?: unknown; roles?: unknown } | null>(authServerUrl, "/users/me");
|
|
84
|
+
if (!user || user.uid !== userUid || !Array.isArray(user.roles)) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const admin = user.roles.some((role) => typeof role === "string" && trustedRoles.includes(role));
|
|
88
|
+
remember(key, admin, now);
|
|
89
|
+
return admin;
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
} finally {
|
|
93
|
+
// Answered: what was learned is in storage; a failure may be asked again.
|
|
94
|
+
lookups.delete(key);
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
lookups.set(key, lookup);
|
|
98
|
+
return lookup;
|
|
99
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
HiOutlineClipboardDocumentList,
|
|
9
9
|
HiOutlineClock,
|
|
10
10
|
HiOutlineDocumentArrowDown,
|
|
11
|
+
HiOutlineDocumentCheck,
|
|
11
12
|
HiOutlineGlobeAlt,
|
|
12
13
|
HiOutlineInboxStack,
|
|
13
14
|
HiOutlineKey,
|
|
@@ -28,7 +29,9 @@ import useBranding from "@rapidmx/react-shared/branding/useBranding.js";
|
|
|
28
29
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
29
30
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
30
31
|
import BottomTabBar, { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
|
|
31
|
-
import {
|
|
32
|
+
import { FrameBrandingFooter, useBrandingHtml } from "../../layout/BrandingChrome.js";
|
|
33
|
+
import RailIcon from "../../layout/RailIcon.js";
|
|
34
|
+
import AppearanceProvider from "../../../appearance/AppearanceProvider.js";
|
|
32
35
|
import UserMenu from "../../layout/UserMenu.js";
|
|
33
36
|
import {
|
|
34
37
|
clearElevationAttempt,
|
|
@@ -55,7 +58,8 @@ export type AdminSection =
|
|
|
55
58
|
| "setup"
|
|
56
59
|
| "dataRequests"
|
|
57
60
|
| "plugins"
|
|
58
|
-
| "branding"
|
|
61
|
+
| "branding"
|
|
62
|
+
| "signingCertificates";
|
|
59
63
|
|
|
60
64
|
/** A core `AdminSection`, or a plugin's `adminNav` item id (see `PluginNav`). */
|
|
61
65
|
export type AdminShellActive = AdminSection | (string & {});
|
|
@@ -130,6 +134,12 @@ const NAV_ITEMS: NavItem[] = [
|
|
|
130
134
|
label: "Data Requests",
|
|
131
135
|
icon: HiOutlineDocumentArrowDown,
|
|
132
136
|
},
|
|
137
|
+
{
|
|
138
|
+
id: "signingCertificates",
|
|
139
|
+
href: "/admin/signing-certificates",
|
|
140
|
+
label: "Signing Certificates",
|
|
141
|
+
icon: HiOutlineDocumentCheck,
|
|
142
|
+
},
|
|
133
143
|
{ id: "plugins", href: "/admin/plugins", label: "Plugins", icon: HiOutlinePuzzlePiece },
|
|
134
144
|
{ id: "branding", href: "/admin/branding", label: "Branding", icon: HiOutlinePaintBrush },
|
|
135
145
|
];
|
|
@@ -177,6 +187,7 @@ export default function AdminShell({ active, userUid, authServerUrl, pluginNav,
|
|
|
177
187
|
// `branding` only feeds the footer below: the admin-configured header is for the webmail and public pages, not the
|
|
178
188
|
// console. `useBranding()` is still what injects the custom stylesheet and supplies the rail's icon.
|
|
179
189
|
const { branding, iconSrc } = useBranding();
|
|
190
|
+
const footer = useBrandingHtml(branding?.footerHtml);
|
|
180
191
|
|
|
181
192
|
useRedirectIfUnauthenticated(userUid, authServerUrl);
|
|
182
193
|
|
|
@@ -285,13 +296,13 @@ export default function AdminShell({ active, userUid, authServerUrl, pluginNav,
|
|
|
285
296
|
const navItems = adminNavItems(pluginNav);
|
|
286
297
|
const activeItem = [...navItems, ...OFF_RAIL_ITEMS].find((item) => item.id === active);
|
|
287
298
|
content = (
|
|
288
|
-
<div className="min-h-screen flex flex-col
|
|
299
|
+
<div className="rr-frame-bg min-h-screen flex flex-col">
|
|
289
300
|
<div className="flex-1 flex min-h-0">
|
|
290
301
|
<nav
|
|
291
302
|
aria-label="Admin sections"
|
|
292
|
-
className="hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center
|
|
303
|
+
className="hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center pb-3 gap-1"
|
|
293
304
|
>
|
|
294
|
-
<
|
|
305
|
+
<RailIcon src={iconSrc} />
|
|
295
306
|
{navItems.map(({ id, href, label, icon: Icon }) => (
|
|
296
307
|
<a
|
|
297
308
|
key={id}
|
|
@@ -312,11 +323,11 @@ export default function AdminShell({ active, userUid, authServerUrl, pluginNav,
|
|
|
312
323
|
</nav>
|
|
313
324
|
<BottomTabBar apps={navItems} active={active} />
|
|
314
325
|
<div className="flex-1 flex flex-col min-w-0">
|
|
315
|
-
<header className="h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6">
|
|
326
|
+
<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">
|
|
316
327
|
<span className="font-display font-bold text-lg uppercase tracking-wide">{activeItem?.label}</span>
|
|
317
328
|
<UserMenu userUid={userUid} authServerUrl={authServerUrl} onSignOut={handleSignOut} />
|
|
318
329
|
</header>
|
|
319
|
-
<div className="flex-1 pb-14 md:pb-0">
|
|
330
|
+
<div id="app-content" className="flex-1 pb-14 md:pb-0">
|
|
320
331
|
<main className="max-w-6xl mx-auto px-6 py-8">{children}</main>
|
|
321
332
|
</div>
|
|
322
333
|
</div>
|
|
@@ -325,10 +336,11 @@ export default function AdminShell({ active, userUid, authServerUrl, pluginNav,
|
|
|
325
336
|
);
|
|
326
337
|
}
|
|
327
338
|
|
|
339
|
+
// The user's own colours and background (`AppearanceProvider`), from what the last webmail page cached in this browser: the console never asks the server.
|
|
328
340
|
return (
|
|
329
|
-
|
|
341
|
+
<AppearanceProvider userUid={userUid} lookUp={false}>
|
|
330
342
|
{content}
|
|
331
|
-
<
|
|
332
|
-
|
|
343
|
+
<FrameBrandingFooter parsed={footer} appTitle={[...adminNavItems(pluginNav), ...OFF_RAIL_ITEMS].find((item) => item.id === active)?.label} />
|
|
344
|
+
</AppearanceProvider>
|
|
333
345
|
);
|
|
334
346
|
}
|