@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.
- package/README.md +183 -2
- 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/mailboxes/new/index.tsx +3 -3
- 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/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +4 -4
- 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 +60 -14
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +65 -16
- 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/contacts/ContactForm.tsx +11 -5
- package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
- package/apps/shared/components/contacts/ContactsToolbar.tsx +25 -52
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +10 -5
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
- package/apps/shared/components/layout/AppShell.tsx +98 -32
- package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
- 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 +75 -6
- package/apps/shared/components/mail/ConversationList.tsx +4 -2
- package/apps/shared/components/mail/ConversationThreadPane.tsx +487 -475
- package/apps/shared/components/mail/LazyReadingPane.tsx +14 -16
- package/apps/shared/components/mail/MailSelectionBar.tsx +2 -10
- package/apps/shared/components/mail/MessageDetailPane.tsx +366 -287
- 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 +35 -2
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +239 -437
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +39 -25
- 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/settings/SigningCertificateCard.tsx +344 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
- package/apps/shared/components/sharing/PrincipalListField.tsx +82 -0
- package/apps/shared/components/sharing/PrincipalPicker.tsx +76 -0
- package/apps/shared/components/sharing/PrincipalResolver.tsx +164 -0
- package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
- package/apps/shared/components/tasks/layout/TasksShell.tsx +9 -5
- package/apps/shared/mail/folderCounts.ts +9 -3
- package/apps/shared/mail/folderTree.ts +143 -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 +81 -28
- package/apps/shared/mail/useMailLiveUpdates.ts +55 -10
- package/apps/shared/mail/useNewMailNotifications.ts +47 -32
- package/apps/shared/navigation/AppRouter.tsx +2 -2
- 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/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 +80 -0
- package/apps/www/_layout.tsx +8 -2
- package/apps/www/_routes.ts +1 -0
- package/apps/www/calendar/index.tsx +9 -14
- package/apps/www/contacts/[uid].tsx +5 -8
- package/apps/www/contacts/index.tsx +587 -584
- package/apps/www/index.tsx +38 -27
- package/apps/www/messages/[uid].tsx +3 -1
- package/apps/www/settings/appearance/index.tsx +21 -0
- package/apps/www/settings/auto-reply/index.tsx +3 -7
- package/apps/www/settings/encryption/index.tsx +151 -116
- package/apps/www/settings/filters/[uid].tsx +3 -1
- package/apps/www/settings/filters/new/index.tsx +3 -1
- package/apps/www/settings/privacy/index.tsx +3 -8
- package/apps/www/settings/read-receipts/index.tsx +3 -7
- package/apps/www/settings/sharing/index.tsx +44 -65
- package/apps/www/settings/signatures/[uid].tsx +3 -1
- package/apps/www/settings/signatures/new/index.tsx +3 -2
- package/apps/www/tasks/index.tsx +11 -16
- 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/mailboxes/new/index.js +1 -1
- 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/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +3 -2
- 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 +25 -5
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +18 -7
- 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/contacts/ContactForm.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +24 -9
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -1
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
- package/dist/apps/shared/components/layout/AppShell.d.ts +6 -1
- package/dist/apps/shared/components/layout/AppShell.js +59 -13
- 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/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 +7 -1
- package/dist/apps/shared/components/layout/UserMenu.js +41 -7
- package/dist/apps/shared/components/mail/ConversationList.js +3 -2
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +2 -1
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +42 -20
- package/dist/apps/shared/components/mail/LazyReadingPane.js +9 -8
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +3 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +2 -3
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +18 -7
- package/dist/apps/shared/components/mail/MessageDetailPane.js +86 -41
- 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 +8 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +28 -3
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +2 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +229 -328
- 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/layout/MailShell.d.ts +2 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +20 -14
- 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/settings/SigningCertificateCard.d.ts +32 -0
- package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
- package/dist/apps/shared/components/sharing/PrincipalListField.d.ts +24 -0
- package/dist/apps/shared/components/sharing/PrincipalListField.js +23 -0
- package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +28 -0
- package/dist/apps/shared/components/sharing/PrincipalPicker.js +24 -0
- package/dist/apps/shared/components/sharing/PrincipalResolver.d.ts +44 -0
- package/dist/apps/shared/components/sharing/PrincipalResolver.js +92 -0
- package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -1
- package/dist/apps/shared/mail/folderCounts.d.ts +1 -1
- package/dist/apps/shared/mail/folderCounts.js +9 -3
- package/dist/apps/shared/mail/folderTree.d.ts +55 -0
- package/dist/apps/shared/mail/folderTree.js +116 -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 +9 -8
- package/dist/apps/shared/mail/useMailConnection.js +64 -22
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +17 -4
- package/dist/apps/shared/mail/useMailLiveUpdates.js +36 -10
- package/dist/apps/shared/mail/useNewMailNotifications.d.ts +4 -10
- package/dist/apps/shared/mail/useNewMailNotifications.js +43 -23
- package/dist/apps/shared/navigation/AppRouter.js +2 -2
- 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/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 +80 -0
- package/dist/apps/www/_layout.d.ts +5 -1
- package/dist/apps/www/_layout.js +3 -2
- package/dist/apps/www/_routes.js +1 -0
- package/dist/apps/www/calendar/index.js +3 -4
- package/dist/apps/www/contacts/[uid].js +5 -7
- package/dist/apps/www/contacts/index.js +14 -21
- package/dist/apps/www/index.js +37 -19
- package/dist/apps/www/messages/[uid].js +3 -1
- 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.js +3 -6
- package/dist/apps/www/settings/encryption/index.js +103 -95
- package/dist/apps/www/settings/filters/[uid].js +3 -1
- package/dist/apps/www/settings/filters/new/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.js +5 -8
- package/dist/apps/www/settings/read-receipts/index.js +3 -6
- package/dist/apps/www/settings/sharing/index.js +19 -33
- package/dist/apps/www/settings/signatures/[uid].js +3 -1
- package/dist/apps/www/settings/signatures/new/index.js +3 -2
- package/dist/apps/www/tasks/index.js +8 -14
- package/package.json +2 -2
- package/apps/shared/components/mail/NewMailToasts.tsx +0 -141
- package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
- package/dist/apps/shared/components/mail/NewMailToasts.d.ts +0 -19
- package/dist/apps/shared/components/mail/NewMailToasts.js +0 -55
- 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,221 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import {
|
|
6
|
+
MIN_TEXT_CONTRAST,
|
|
7
|
+
Rgb,
|
|
8
|
+
ThemeSurface,
|
|
9
|
+
adaptForeground,
|
|
10
|
+
blackOrWhite,
|
|
11
|
+
composite,
|
|
12
|
+
contrastRatio,
|
|
13
|
+
ensureContrast,
|
|
14
|
+
firstColourStop,
|
|
15
|
+
formatColour,
|
|
16
|
+
luminance,
|
|
17
|
+
parseColour,
|
|
18
|
+
sameColour,
|
|
19
|
+
} from "./color.js";
|
|
20
|
+
import { LINK_SENTINEL, TEXT_SENTINEL } from "./frameDocument.js";
|
|
21
|
+
import { documentElementOf, viewOf } from "./safeDocument.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Makes a message take the app's theme wherever its author left the colours to the reader, and leaves it as authored wherever the
|
|
25
|
+
* author chose them. Runs in the parent against the display frame's document (which is same-origin only because no script can run
|
|
26
|
+
* there - see `frameDocument.ts`); reads with `getComputedStyle` and writes nothing but `color` on individual elements, as inline
|
|
27
|
+
* `!important` declarations built from numbers, never from text of the message.
|
|
28
|
+
*
|
|
29
|
+
* **Authored or inherited, without parsing CSS.** The frame's base stylesheet gives the root a sentinel text colour, and unvisited/visited
|
|
30
|
+
* links another, at zero specificity, so any rule of the message beats them. An element whose computed `color` still equals the sentinel
|
|
31
|
+
* was never coloured by the message (its text is *unauthored*); any other value - black included - was chosen by somebody. A background
|
|
32
|
+
* is authored when its computed colour is not transparent or its image is not `none`.
|
|
33
|
+
*
|
|
34
|
+
* **What each text becomes**, from its effective background (the nearest authored background up the tree, composited over what is beneath
|
|
35
|
+
* it, else the theme's surface):
|
|
36
|
+
* - unauthored text on the theme surface: the theme's text colour; an unauthored link: the theme's link colour;
|
|
37
|
+
* - authored text on the theme surface: kept if it reads (4.5:1), else adapted (`adaptForeground()`: black becomes white, navy a lighter
|
|
38
|
+
* navy) - the symmetric case in a light theme included;
|
|
39
|
+
* - anything on an authored, opaque background (a white cell, a dark banner, a gradient): that region is shown as authored - its own text
|
|
40
|
+
* colours untouched - except unauthored text, which must not inherit the theme's colour (white on white) and gets black or white by
|
|
41
|
+
* contrast with that background (black when the background is a picture or a gradient with no first colour to judge by);
|
|
42
|
+
* - a semi-transparent authored background is composited over what is under it and treated like the theme surface, since how it looks
|
|
43
|
+
* depends on the theme.
|
|
44
|
+
*
|
|
45
|
+
* Every element ends with exactly its target colour: an element is written to only when inheritance would not give it that (which includes
|
|
46
|
+
* one that may carry a declaration of its own). Images, SVG and everything else that is not text are untouched.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
export interface AdaptationResult {
|
|
50
|
+
/** Elements examined. */
|
|
51
|
+
elements: number;
|
|
52
|
+
/** Elements given an inline colour. */
|
|
53
|
+
written: number;
|
|
54
|
+
/** Whether adapting makes a visible difference from the message as authored: a text colour changed a lot, or the canvas is the theme's
|
|
55
|
+
* dark surface rather than the white a message is authored on. False for e.g. a bare message in a light theme. */
|
|
56
|
+
material: boolean;
|
|
57
|
+
/** `<html>` or `<body>` carries an authored background: the whole frame takes it. */
|
|
58
|
+
authoredCanvas: boolean;
|
|
59
|
+
/** Stopped because `isCancelled()` said the frame was gone. */
|
|
60
|
+
cancelled: boolean;
|
|
61
|
+
/** Puts every colour back as the message had it (paper is not a dark surface: the frame does this for printing). */
|
|
62
|
+
revert: () => void;
|
|
63
|
+
/** Puts the adapted colours back after `revert()`. */
|
|
64
|
+
reapply: () => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface AdaptOptions {
|
|
68
|
+
/** How long one slice of work may take before the pass gives the browser a turn. */
|
|
69
|
+
sliceMs?: number;
|
|
70
|
+
isCancelled?: () => boolean;
|
|
71
|
+
/** For tests. */
|
|
72
|
+
now?: () => number;
|
|
73
|
+
yieldToBrowser?: () => Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Not examined, nor are their children. `head` and its contents never render; `svg` is left as drawn. */
|
|
77
|
+
const SKIPPED = new Set(["head", "script", "style", "title", "meta", "link", "template", "svg", "img"]);
|
|
78
|
+
|
|
79
|
+
/** What travels down the tree: the effective background, whether it is an authored opaque one, and the colour the parent ends up with. */
|
|
80
|
+
interface Context {
|
|
81
|
+
background: Rgb;
|
|
82
|
+
authored: boolean;
|
|
83
|
+
unknown: boolean;
|
|
84
|
+
parentColour: Rgb;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Classic link blue - what an unauthored link looks like in an authored region, before contrast. */
|
|
88
|
+
const DEFAULT_LINK: Rgb = { r: 0, g: 0, b: 238 };
|
|
89
|
+
/** How much lighter or darker (in luminance) an adapted text colour must be than the original for the difference to be worth an opt-out. */
|
|
90
|
+
const MATERIAL_LUMINANCE_SHIFT = 0.3;
|
|
91
|
+
/** A surface at least this luminous is close enough to the white a message is authored on that it is not, by itself, an adaptation. */
|
|
92
|
+
const LIGHT_SURFACE = 0.85;
|
|
93
|
+
|
|
94
|
+
/** The elements whose background is the message's canvas: `<html>`, `<body>`, and the wrapper that stands in for the message's own `<body>`. */
|
|
95
|
+
function standsForCanvas(element: HTMLElement, root: HTMLElement): boolean {
|
|
96
|
+
return element === root || element.localName === "body" || element.parentElement?.id === "rr-body";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function noop(): void {
|
|
100
|
+
// Nothing was written, so there is nothing to put back.
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function nextTurn(): Promise<void> {
|
|
104
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The colour a `color` value comes to over a background: opaque, alpha composited. */
|
|
108
|
+
function opaque(css: string, over: Rgb): Rgb | undefined {
|
|
109
|
+
const colour = parseColour(css);
|
|
110
|
+
return colour ? composite(colour, over) : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Adapts `doc` to `surface` - see the file's header. Time-sliced (`sliceMs`, then a turn for the browser) so a very large message never
|
|
115
|
+
* blocks the page; the frame stays hidden until this resolves, so nothing unreadable is ever shown.
|
|
116
|
+
*/
|
|
117
|
+
export async function adaptDocument(doc: Document, surface: ThemeSurface, options: AdaptOptions = {}): Promise<AdaptationResult> {
|
|
118
|
+
const { sliceMs = 12, isCancelled = () => false, now = () => performance.now(), yieldToBrowser = nextTurn } = options;
|
|
119
|
+
const view = viewOf(doc);
|
|
120
|
+
const result: AdaptationResult = { elements: 0, written: 0, material: false, authoredCanvas: false, cancelled: false, revert: noop, reapply: noop };
|
|
121
|
+
if (!view) {
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
const root = documentElementOf(doc);
|
|
125
|
+
// Every read happens before the first write: a write changes what its descendants inherit, which would make every child of an element
|
|
126
|
+
// we coloured look authored.
|
|
127
|
+
const writes: { element: HTMLElement; colour: string }[] = [];
|
|
128
|
+
const stack: { element: HTMLElement; context: Context }[] = [
|
|
129
|
+
{ element: root, context: { background: surface.background, authored: false, unknown: false, parentColour: surface.text } },
|
|
130
|
+
];
|
|
131
|
+
let sliceStart = now();
|
|
132
|
+
while (stack.length > 0) {
|
|
133
|
+
if (now() - sliceStart > sliceMs) {
|
|
134
|
+
await yieldToBrowser();
|
|
135
|
+
if (isCancelled()) {
|
|
136
|
+
result.cancelled = true;
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
sliceStart = now();
|
|
140
|
+
}
|
|
141
|
+
const { element, context } = stack.pop()!;
|
|
142
|
+
if (SKIPPED.has(element.localName.toLowerCase())) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
result.elements++;
|
|
146
|
+
const style = view.getComputedStyle(element);
|
|
147
|
+
let { background, authored, unknown } = context;
|
|
148
|
+
const own = parseColour(style.backgroundColor);
|
|
149
|
+
const image = style.backgroundImage;
|
|
150
|
+
if (image && image !== "none") {
|
|
151
|
+
const stop = firstColourStop(image) ?? (own && own.a > 0 ? own : undefined);
|
|
152
|
+
authored = true;
|
|
153
|
+
unknown = !stop;
|
|
154
|
+
background = stop ? composite(stop, background) : background;
|
|
155
|
+
result.authoredCanvas ||= standsForCanvas(element, root);
|
|
156
|
+
} else if (own && own.a >= 0.98) {
|
|
157
|
+
authored = true;
|
|
158
|
+
unknown = false;
|
|
159
|
+
background = composite(own, background);
|
|
160
|
+
result.authoredCanvas ||= standsForCanvas(element, root);
|
|
161
|
+
} else if (own && own.a > 0) {
|
|
162
|
+
background = composite(own, background);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const declared = style.color;
|
|
166
|
+
const original = opaque(declared, background);
|
|
167
|
+
let target: { rgb: Rgb; css: string } | undefined;
|
|
168
|
+
if (declared === TEXT_SENTINEL) {
|
|
169
|
+
const rgb = authored ? (unknown ? { r: 0, g: 0, b: 0 } : blackOrWhite(background)) : ensureContrast(surface.text, background);
|
|
170
|
+
target = { rgb, css: formatColour(rgb) };
|
|
171
|
+
} else if (declared === LINK_SENTINEL) {
|
|
172
|
+
const rgb = authored ? (unknown ? DEFAULT_LINK : ensureContrast(DEFAULT_LINK, background)) : ensureContrast(surface.link, background);
|
|
173
|
+
target = { rgb, css: formatColour(rgb) };
|
|
174
|
+
} else if (original && !authored) {
|
|
175
|
+
// Authored text on the theme's surface: it stays if it reads, else it is adapted.
|
|
176
|
+
const readable = contrastRatio(original, background) >= MIN_TEXT_CONTRAST;
|
|
177
|
+
const rgb = readable ? original : adaptForeground(original, { ...surface, background });
|
|
178
|
+
target = { rgb, css: readable ? declared : formatColour(rgb) };
|
|
179
|
+
} else if (original) {
|
|
180
|
+
// Authored text on an authored background: as its author had it.
|
|
181
|
+
target = { rgb: original, css: declared };
|
|
182
|
+
}
|
|
183
|
+
let parentColour = context.parentColour;
|
|
184
|
+
if (target) {
|
|
185
|
+
// An element is written to unless inheritance already gives it its target: the parent must end with that colour, and an element
|
|
186
|
+
// that may carry a declaration of its own (an authored colour, a link - the sentinel is a rule on it - and the root) must not
|
|
187
|
+
// be declaring a different one. Text nobody coloured inherits and has nothing of its own to override.
|
|
188
|
+
const ownDeclaration = declared !== TEXT_SENTINEL || element === root;
|
|
189
|
+
if (!sameColour(target.rgb, context.parentColour) || (ownDeclaration && !sameColour(target.rgb, original ?? target.rgb))) {
|
|
190
|
+
writes.push({ element, colour: target.css });
|
|
191
|
+
}
|
|
192
|
+
if (original && Math.abs(luminance(target.rgb) - luminance(original)) > MATERIAL_LUMINANCE_SHIFT) {
|
|
193
|
+
result.material = true;
|
|
194
|
+
}
|
|
195
|
+
parentColour = target.rgb;
|
|
196
|
+
}
|
|
197
|
+
for (const child of Array.from(element.children).reverse()) {
|
|
198
|
+
stack.push({ element: child as HTMLElement, context: { background, authored, unknown, parentColour } });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// What each element had inline before, so the writes can be taken back (a message may carry its own inline colour).
|
|
202
|
+
const before = writes.map(({ element }) => ({ value: element.style.getPropertyValue("color"), priority: element.style.getPropertyPriority("color") }));
|
|
203
|
+
result.reapply = () => {
|
|
204
|
+
for (const { element, colour } of writes) {
|
|
205
|
+
element.style.setProperty("color", colour, "important");
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
result.revert = () => {
|
|
209
|
+
writes.forEach(({ element }, index) => {
|
|
210
|
+
if (before[index].value) {
|
|
211
|
+
element.style.setProperty("color", before[index].value, before[index].priority);
|
|
212
|
+
} else {
|
|
213
|
+
element.style.removeProperty("color");
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
result.reapply();
|
|
218
|
+
result.written = writes.length;
|
|
219
|
+
result.material ||= !result.authoredCanvas && luminance(surface.background) < LIGHT_SURFACE;
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import type { ResolvedTheme } from "../../../appearance/resolvedTheme.js";
|
|
7
|
+
import { Rgb, Rgba, ThemeSurface, composite, formatColour, parseColour } from "./color.js";
|
|
8
|
+
|
|
9
|
+
/** The palette used where the page's own tokens cannot be read (no DOM, or a stylesheet without them): the app's built-in schemes. */
|
|
10
|
+
export const DEFAULT_SURFACES: Record<ResolvedTheme, ThemeSurface> = {
|
|
11
|
+
light: { background: { r: 255, g: 255, b: 255 }, text: { r: 28, g: 37, b: 38 }, link: { r: 20, g: 138, b: 128 }, dark: false },
|
|
12
|
+
dark: { background: { r: 27, g: 32, b: 34 }, text: { r: 238, g: 242, b: 243 }, link: { r: 45, g: 212, b: 191 }, dark: true },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** The page's own CSS variables a message is adapted to. Read from the document, so a branding palette or a user's own Appearance colours are followed. */
|
|
16
|
+
const TOKENS = {
|
|
17
|
+
surface: "--rr-color-surface",
|
|
18
|
+
page: "--rr-color-bg",
|
|
19
|
+
text: "--rr-color-text",
|
|
20
|
+
link: "--rr-color-primary-dark",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
/** The colour a CSS custom property resolves to, by asking the browser what a probe element painted with it comes to (which also
|
|
24
|
+
* resolves `var()` chains and colour functions); `undefined` when it isn't defined or isn't a colour. */
|
|
25
|
+
function resolveToken(probe: HTMLElement, name: string): Rgba | undefined {
|
|
26
|
+
probe.style.color = `var(${name}, transparent)`;
|
|
27
|
+
const colour = parseColour(getComputedStyle(probe).color);
|
|
28
|
+
return colour && colour.a > 0 ? colour : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The theme a message is shown in, read from the page's own tokens: the surface behind the body composited to an opaque colour (a
|
|
33
|
+
* translucent surface over a photo would make text unreadable, so the body area is always painted with this opaque one), the text
|
|
34
|
+
* colour and the link colour. Falls back to the built-in scheme for anything the page doesn't define.
|
|
35
|
+
*/
|
|
36
|
+
export function readThemeSurface(theme: ResolvedTheme): ThemeSurface {
|
|
37
|
+
const fallback = DEFAULT_SURFACES[theme];
|
|
38
|
+
if (typeof document === "undefined" || !document.body) {
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
const probe = document.createElement("span");
|
|
42
|
+
probe.style.display = "none";
|
|
43
|
+
document.body.appendChild(probe);
|
|
44
|
+
try {
|
|
45
|
+
const page = resolveToken(probe, TOKENS.page);
|
|
46
|
+
const surface = resolveToken(probe, TOKENS.surface);
|
|
47
|
+
const underlay: Rgb = page ? composite(page, fallback.background) : fallback.background;
|
|
48
|
+
const text = resolveToken(probe, TOKENS.text);
|
|
49
|
+
const link = resolveToken(probe, TOKENS.link);
|
|
50
|
+
return {
|
|
51
|
+
dark: fallback.dark,
|
|
52
|
+
background: surface ? composite(surface, underlay) : underlay,
|
|
53
|
+
text: text ? composite(text, underlay) : fallback.text,
|
|
54
|
+
link: link ? composite(link, underlay) : fallback.link,
|
|
55
|
+
};
|
|
56
|
+
} finally {
|
|
57
|
+
probe.remove();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A string that changes exactly when the surface does - what a frame is keyed on. */
|
|
62
|
+
export function surfaceKey(surface: ThemeSurface): string {
|
|
63
|
+
return [surface.background, surface.text, surface.link].map(formatColour).join("|") + (surface.dark ? "|dark" : "|light");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The theme surface for `theme`, kept current: re-read when the scheme changes and when the page's own colours are changed at runtime
|
|
68
|
+
* (Appearance sets `data-theme` on `<html>` and writes the user's colours into a `<style>` in the head). The identity only changes when a colour does.
|
|
69
|
+
*/
|
|
70
|
+
export function useThemeSurface(theme: ResolvedTheme): ThemeSurface {
|
|
71
|
+
const [surface, setSurface] = useState(() => readThemeSurface(theme));
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const update = () =>
|
|
74
|
+
setSurface((previous) => {
|
|
75
|
+
const next = readThemeSurface(theme);
|
|
76
|
+
return surfaceKey(previous) === surfaceKey(next) ? previous : next;
|
|
77
|
+
});
|
|
78
|
+
update();
|
|
79
|
+
if (typeof MutationObserver === "undefined") {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const observer = new MutationObserver(update);
|
|
83
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["style", "class", "data-theme"] });
|
|
84
|
+
// The Appearance provider writes the user's own colours into a `<style>` element in the head, whose text is replaced when they change.
|
|
85
|
+
observer.observe(document.head, { childList: true, subtree: true, characterData: true, attributes: true });
|
|
86
|
+
return () => observer.disconnect();
|
|
87
|
+
}, [theme]);
|
|
88
|
+
// The render that first sees a new scheme must not use the previous one's colours (the effect above corrects the state after it).
|
|
89
|
+
return surface.dark === (theme === "dark") ? surface : readThemeSurface(theme);
|
|
90
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useCallback, useState } from "react";
|
|
6
|
+
|
|
7
|
+
/** Messages the reader chose to see exactly as authored rather than adapted to the theme, for the life of the tab. */
|
|
8
|
+
const original = new Set<string>();
|
|
9
|
+
|
|
10
|
+
/** Whether the reader asked to see `uid` as authored. */
|
|
11
|
+
export function isViewedOriginal(uid: string): boolean {
|
|
12
|
+
return original.has(uid);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Forgets every choice (the tests). */
|
|
16
|
+
export function clearViewedOriginal(): void {
|
|
17
|
+
original.clear();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The per-message "View original" choice: `false` (follow the theme) until the reader flips it, then remembered for the session so
|
|
22
|
+
* collapsing and expanding a message, or coming back to it, keeps the choice.
|
|
23
|
+
*/
|
|
24
|
+
export function useViewOriginal(uid: string): [boolean, (value: boolean) => void] {
|
|
25
|
+
const [value, setValue] = useState(() => isViewedOriginal(uid));
|
|
26
|
+
const update = useCallback(
|
|
27
|
+
(next: boolean) => {
|
|
28
|
+
if (next) {
|
|
29
|
+
original.add(uid);
|
|
30
|
+
} else {
|
|
31
|
+
original.delete(uid);
|
|
32
|
+
}
|
|
33
|
+
setValue(next);
|
|
34
|
+
},
|
|
35
|
+
[uid],
|
|
36
|
+
);
|
|
37
|
+
return [value, update];
|
|
38
|
+
}
|