@rapidmx/web-client 0.10.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 +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/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/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/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/PrincipalPicker.tsx +145 -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/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/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/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/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.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
|
@@ -14,11 +14,14 @@ import { useIdleKeyTimeout } from "@rapidmx/react-shared/crypto/useIdleKeyTimeou
|
|
|
14
14
|
import ComposeProvider from "../mail/compose/ComposeContext.js";
|
|
15
15
|
import { flushComposeDrafts, markSigningOut } from "../mail/compose/composeFlushRegistry.js";
|
|
16
16
|
import BottomTabBar from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
|
|
17
|
-
import {
|
|
17
|
+
import { FrameBrandingFooter, FrameBrandingHeader, useBrandingHtml } from "./BrandingChrome.js";
|
|
18
|
+
import RailIcon from "./RailIcon.js";
|
|
19
|
+
import AppearanceProvider from "../../appearance/AppearanceProvider.js";
|
|
20
|
+
import { clearAppearanceCache } from "../../appearance/appearanceCache.js";
|
|
18
21
|
import UserMenu from "./UserMenu.js";
|
|
19
22
|
import { UnlockPromptProvider } from "./UnlockPromptProvider.js";
|
|
20
23
|
import { SIGN_OUT_CHANNEL, destroyAllLocalIndexes } from "../../search/localIndexRpcClient.js";
|
|
21
|
-
import { authApiFetch } from "@rapidmx/react-shared/util/api.js";
|
|
24
|
+
import { authApiFetch, setApiUnauthorizedObserver } from "@rapidmx/react-shared/util/api.js";
|
|
22
25
|
import { destroyUnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
|
|
23
26
|
import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
|
|
24
27
|
import { mergePluginNavItems } from "../../plugins/pluginNav.js";
|
|
@@ -31,7 +34,13 @@ import ShortcutsDialog from "../../keyboard/ShortcutsDialog.js";
|
|
|
31
34
|
import { inboxUnreadTotal } from "../../mail/folderCounts.js";
|
|
32
35
|
import { MailConnectionContext, useMailConnection } from "../../mail/useMailConnection.js";
|
|
33
36
|
import { useUnreadTitle } from "../../mail/useUnreadTitle.js";
|
|
34
|
-
import
|
|
37
|
+
import UnlockBridge from "../../mail/outbox/UnlockBridge.js";
|
|
38
|
+
import NotificationCenter from "../../notifications/NotificationCenter.js";
|
|
39
|
+
import { useHeaderHeightRef } from "../../notifications/headerOffset.js";
|
|
40
|
+
import NotificationHistoryDialog from "../../notifications/NotificationHistoryDialog.js";
|
|
41
|
+
import { notifySessionExpired, setSignInUrl } from "../../notifications/apiErrors.js";
|
|
42
|
+
import { useUnseenErrorCount } from "../../notifications/useNotifications.js";
|
|
43
|
+
import { useSigningEnrollmentWatcher } from "../../signing/useSigningEnrollmentWatcher.js";
|
|
35
44
|
/** How long sign-out waits for auth-server's logout before navigating anyway. */
|
|
36
45
|
export const LOGOUT_TIMEOUT_MS = 3000;
|
|
37
46
|
/**
|
|
@@ -77,12 +86,24 @@ export function appRailItems(pluginNav) {
|
|
|
77
86
|
* Rendered by `AppRouter`, once, for the life of the page, so that moving between apps replaces only `children` - see
|
|
78
87
|
* `AppShell` below for what a page's own shell renders instead.
|
|
79
88
|
*/
|
|
80
|
-
export function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, routeKey, busy, hideChrome, children, }) {
|
|
89
|
+
export function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, routeKey, busy, hideChrome, children, }) {
|
|
81
90
|
const [stoppingImpersonation, setStoppingImpersonation] = useState(false);
|
|
82
91
|
// The keyboard shortcuts dialog: opened by `?`/Ctrl+/ (`GlobalShortcuts`) and by the account menu's item.
|
|
83
92
|
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
|
93
|
+
// The pop-up history ("Recent notifications" in the account menu). The pop-ups themselves are drawn by `NotificationCenter`, below.
|
|
94
|
+
const [historyOpen, setHistoryOpen] = useState(false);
|
|
95
|
+
// Only the count is read, so the whole frame does not render again for every pop-up that comes and goes.
|
|
96
|
+
const unseenErrors = useUnseenErrorCount();
|
|
97
|
+
// The title bar's height for the pop-up stack, which sticks just below the header (a branding header publishes its own - `FrameBrandingHeader`).
|
|
98
|
+
const headerRef = useHeaderHeightRef();
|
|
84
99
|
const signingOutRef = useRef(false);
|
|
85
|
-
const { branding, iconSrc } = useBranding();
|
|
100
|
+
const { branding: fetchedBranding, iconSrc: fetchedIconSrc } = useBranding();
|
|
101
|
+
// The server's copy until the fetch answers, so the frame's shape (a custom header replaces the title bar) never changes after the first paint.
|
|
102
|
+
const branding = fetchedBranding ?? initialBranding ?? null;
|
|
103
|
+
const iconSrc = fetchedBranding ? fetchedIconSrc : initialBranding?.iconUrl || initialBranding?.logoUrl || fetchedIconSrc;
|
|
104
|
+
// The custom header and footer, sanitized and with their `{USER_MENU}` / `{APP_TITLE}` placeholders (`undefined` until parsed, `null` when there is none).
|
|
105
|
+
const header = useBrandingHtml(branding?.headerHtml);
|
|
106
|
+
const footer = useBrandingHtml(branding?.footerHtml);
|
|
86
107
|
// The mailboxes, their folders and the one push connection live here, in the frame that stays mounted as the router swaps pages - so
|
|
87
108
|
// new-mail pop-ups, the folder counters and the tab title's unread count work in Calendar, Contacts, Tasks and Settings too, and moving
|
|
88
109
|
// between apps never opens a second socket (the server allows ten per user). `MailShell` reads them from `MailConnectionContext`. Outside
|
|
@@ -90,7 +111,19 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
|
|
|
90
111
|
const inFrame = useInAppFrame();
|
|
91
112
|
const navigate = useNavigate();
|
|
92
113
|
const mail = useMailConnection({ userUid, enabled: inFrame, open: navigate });
|
|
114
|
+
// A signing certificate the user asked for is announced when it is issued (or fails), on whichever page they are - see the hook.
|
|
115
|
+
useSigningEnrollmentWatcher({ userUid, mailboxes: mail.mailboxes, enabled: inFrame });
|
|
93
116
|
useRedirectIfUnauthenticated(userUid, authServerUrl);
|
|
117
|
+
// Any request this app makes that the server answers with a 401 - the session ended - raises one "Your session expired" pop-up with a
|
|
118
|
+
// Sign in action (see `notifySessionExpired()`), whichever request noticed first, a background refresh included.
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
setSignInUrl(authServerUrl);
|
|
121
|
+
if (!userUid) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
setApiUnauthorizedObserver(() => void notifySessionExpired());
|
|
125
|
+
return () => setApiUnauthorizedObserver(undefined);
|
|
126
|
+
}, [userUid, authServerUrl]);
|
|
94
127
|
// Mounted here, not scoped to Mail/Settings (the only shells that actually read unlocked keys),
|
|
95
128
|
// specifically so activity in *any* app resets the idle clock - see that hook's own doc comment.
|
|
96
129
|
useIdleKeyTimeout();
|
|
@@ -130,6 +163,7 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
|
|
|
130
163
|
markSigningOut();
|
|
131
164
|
destroyUnlockedKeys();
|
|
132
165
|
clearPinnedSignerCache();
|
|
166
|
+
clearAppearanceCache();
|
|
133
167
|
// Bounded by its own timeout and never rejects - awaited so navigating doesn't kill the Worker mid-delete.
|
|
134
168
|
void destroyAllLocalIndexes().then(() => {
|
|
135
169
|
window.location.href = authServerUrl ?? "/";
|
|
@@ -146,6 +180,7 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
|
|
|
146
180
|
destroyUnlockedKeys();
|
|
147
181
|
// Trusted signer pins read from contacts don't outlive the session either.
|
|
148
182
|
clearPinnedSignerCache();
|
|
183
|
+
clearAppearanceCache();
|
|
149
184
|
// Open compose windows save edits still waiting on their autosave debounce while the session is still
|
|
150
185
|
// valid - logout invalidates it. Bounded the same way as logout itself, and never rejects.
|
|
151
186
|
await flushComposeDrafts(LOGOUT_TIMEOUT_MS);
|
|
@@ -204,14 +239,25 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
|
|
|
204
239
|
const apps = appRailItems(pluginNav);
|
|
205
240
|
// The header title: "settings" has no rail item, everything else is labelled by its own rail item.
|
|
206
241
|
const activeLabel = active === "settings" ? "Settings" : apps.find((app) => app.id === active)?.label;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
242
|
+
// A custom header (`Branding.headerHtml`) replaces the app's own title bar and the icon at the top of the rail: it is the top of the app, and the
|
|
243
|
+
// account menu moves into it. While it is still being parsed it already counts, so the frame's shape doesn't change under the user.
|
|
244
|
+
const customHeader = header !== null;
|
|
245
|
+
// Where the one account menu lives: the header's `{USER_MENU}`, else the footer's, else a small cell at the header's right end - never lost.
|
|
246
|
+
const menuInHeader = !!header?.hasUserMenu;
|
|
247
|
+
const menuInFooter = customHeader && !!header && !menuInHeader && !!footer?.hasUserMenu;
|
|
248
|
+
const renderUserMenu = (placement) => (_jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut,
|
|
249
|
+
// An impersonating administrator acts as the impersonated user, so the console link is hidden then.
|
|
250
|
+
showAdminLink: !!trusted && !impersonating, detectAdmin: !trusted && !impersonating, trustedRoles: trustedRoles, showSettingsLink: true, showNotificationSettings: true, onShowShortcuts: () => setShortcutsOpen(true), onShowNotifications: () => setHistoryOpen(true), unseenErrors: unseenErrors, placement: placement }));
|
|
251
|
+
return (_jsx(AppearanceProvider, { userUid: userUid, initial: appearance, lookUp: inFrame, children: _jsx(ShortcutProvider, { children: _jsxs(MailConnectionContext.Provider, { value: inFrame ? mail : null, children: [_jsx(GlobalShortcuts, { authServerUrl: authServerUrl, onToggleHelp: () => setShortcutsOpen((open) => !open) }), _jsx(ShortcutsDialog, { open: shortcutsOpen, onClose: () => setShortcutsOpen(false) }), _jsxs(UnlockPromptProvider, { children: [_jsx(UnlockBridge, {}), _jsx(ComposeProvider, { userUid: userUid, trusted: !!trusted && !impersonating, children: _jsxs("div", { className: "rr-frame-bg min-h-screen flex flex-col", children: [!hideChrome && customHeader && (_jsx(FrameBrandingHeader, { parsed: header, userMenu: menuInHeader ? renderUserMenu("down") : undefined, fallbackMenu: header && !menuInHeader && !menuInFooter ? renderUserMenu("down") : undefined, appTitle: activeLabel })), impersonating && !hideChrome && (_jsxs("div", { className: "h-10 shrink-0 bg-warning text-warning-contrast flex items-center justify-center gap-3 text-sm font-medium px-4", children: [_jsxs("span", { children: ["You are viewing as ", _jsx("strong", { children: userUid }), "."] }), _jsx("button", { type: "button", onClick: handleStopImpersonating, disabled: stoppingImpersonation, className: "underline hover:no-underline disabled:opacity-60", children: stoppingImpersonation ? "Returning to admin…" : "Return to admin" })] })), _jsxs("div", { className: "flex-1 flex min-h-0", children: [!hideChrome && (_jsxs("nav", { "aria-label": "Apps", className: [
|
|
252
|
+
"hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center gap-1",
|
|
253
|
+
// Under a custom header the rail starts with the app icons; without one its own icon is flush with the top of the window.
|
|
254
|
+
customHeader ? "py-3" : "pb-3",
|
|
255
|
+
].join(" "), children: [!customHeader && (_jsx(RailIcon, { src: iconSrc })), apps.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
|
|
256
|
+
"w-10 h-10 flex items-center justify-center rounded-sm",
|
|
257
|
+
id === active
|
|
258
|
+
? "bg-primary/10 text-primary-dark"
|
|
259
|
+
: "text-text-muted hover:bg-surface-alt hover:text-text",
|
|
260
|
+
].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] })), !hideChrome && _jsx(BottomTabBar, { apps: apps, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [!hideChrome && !customHeader && (_jsxs("header", { ref: headerRef, className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeLabel }), renderUserMenu("down")] })), _jsx(NotificationCenter, {}), _jsx("div", { ref: contentRef, id: "app-content", tabIndex: -1, "aria-busy": busy || undefined, className: ["flex-1 flex min-h-0 outline-none", hideChrome ? "" : "pb-14 md:pb-0"].join(" "), children: children })] })] }), routeKey !== undefined && (_jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement })), !hideChrome && _jsx(FrameBrandingFooter, { parsed: footer, userMenu: menuInFooter ? renderUserMenu("up") : undefined, appTitle: activeLabel })] }) })] }), _jsx(NotificationHistoryDialog, { open: historyOpen, onClose: () => setHistoryOpen(false) })] }) }) }));
|
|
215
261
|
}
|
|
216
262
|
/**
|
|
217
263
|
* What a page's own shell (`MailShell`, `CalendarShell`, ...) renders around its content. Outside the client-side router it
|
|
@@ -1,19 +1,78 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import type { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
|
|
3
|
+
/** The attribute that marks the elements React fills in (`{USER_MENU}`, `{APP_TITLE}`). Stripped from the author's own HTML, so only
|
|
4
|
+
* this module's placeholders can carry it. */
|
|
5
|
+
export declare const SLOT_ATTRIBUTE = "data-rr-slot";
|
|
6
|
+
/** The variables a branding header or footer may contain, written in the HTML's text. */
|
|
7
|
+
export declare const USER_MENU_VARIABLE = "{USER_MENU}";
|
|
8
|
+
export declare const APP_TITLE_VARIABLE = "{APP_TITLE}";
|
|
3
9
|
/**
|
|
4
10
|
* Sanitizes admin-configured branding HTML before it's rendered. The server sanitizes on save too; this also
|
|
5
11
|
* covers anything stored before that. Returns `""` where there's no DOM to sanitize with (server-side
|
|
6
12
|
* rendering), so unsanitized HTML is never emitted.
|
|
7
13
|
*/
|
|
8
14
|
export declare function sanitizeBrandingHtml(html: string): string;
|
|
15
|
+
/** Branding HTML ready to render: sanitized, with its variables replaced by placeholder elements for React to fill. */
|
|
16
|
+
export interface ParsedBrandingHtml {
|
|
17
|
+
html: string;
|
|
18
|
+
/** It contains a `{USER_MENU}` where the account menu can be rendered. */
|
|
19
|
+
hasUserMenu: boolean;
|
|
20
|
+
}
|
|
9
21
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
22
|
+
* Sanitizes `html` (the same rules as `sanitizeBrandingHtml()`) and swaps the variables in its **text** for placeholder elements:
|
|
23
|
+
* `{USER_MENU}` becomes the slot the account menu is rendered into, `{APP_TITLE}` a slot holding the current app's name.
|
|
24
|
+
*
|
|
25
|
+
* - Only text is searched, after sanitizing - a variable in an attribute (`title="{USER_MENU}"`), a comment, or a `<script>` (which
|
|
26
|
+
* is gone) is never touched, and the author cannot write a placeholder themselves (`data-rr-slot` is stripped).
|
|
27
|
+
* - `{USER_MENU}` is replaced **once**, at its first place in the document; a second (or third) one is removed, so there is never
|
|
28
|
+
* more than one account menu (two would give two sets of duplicate ids and focus targets). One inside a link, or in text that
|
|
29
|
+
* can't hold an element (`<title>`, `<textarea>`), is removed too: a menu inside an `<a>` would navigate when clicked.
|
|
30
|
+
* - `{APP_TITLE}` is replaced everywhere it appears. Inside `<title>`-like raw text it stays as written.
|
|
31
|
+
* - Nothing is matched loosely: the names are upper-case, in braces, with no spaces.
|
|
32
|
+
*
|
|
33
|
+
* `null` when there is nothing to show (no DOM to sanitize with - server-side rendering - or the HTML is empty once sanitized).
|
|
13
34
|
*/
|
|
14
|
-
export declare function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
35
|
+
export declare function parseBrandingHtml(html: string): ParsedBrandingHtml | null;
|
|
36
|
+
/**
|
|
37
|
+
* `parseBrandingHtml()` of `html`, done once the component is on the page (never while rendering: the server has no DOM to sanitize
|
|
38
|
+
* with, and rendering different HTML there than the hydrating client would leave the page without it). `null` when there is no HTML,
|
|
39
|
+
* or nothing is left of it; `undefined` while it has not been parsed yet - the frame reserves the header's place meanwhile.
|
|
40
|
+
*/
|
|
41
|
+
export declare function useBrandingHtml(html: string | undefined): ParsedBrandingHtml | null | undefined;
|
|
42
|
+
export interface FrameBrandingHeaderProps {
|
|
43
|
+
/** `useBrandingHtml(branding.headerHtml)`. `null` renders nothing. */
|
|
44
|
+
parsed: ParsedBrandingHtml | null | undefined;
|
|
45
|
+
/** The account menu, rendered into the header's `{USER_MENU}` (when it has one). */
|
|
46
|
+
userMenu?: ReactNode;
|
|
47
|
+
/** Rendered at the right end of the header when it has no `{USER_MENU}` and the footer doesn't host the menu either - the account menu is never lost. */
|
|
48
|
+
fallbackMenu?: ReactNode;
|
|
49
|
+
appTitle?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The frame's version of the admin-configured `Branding.headerHtml` (see `useBranding()`), sanitized, in a `<header>` that stays at the top of the window as the
|
|
53
|
+
* page scrolls. When it is there the app has no title bar of its own (`AppChrome`): the header is the top of the app, and the account
|
|
54
|
+
* menu lives in it - where the HTML writes `{USER_MENU}`, else in a small cell at its right end. The webmail chrome (`AppShell`) shows
|
|
55
|
+
* it; the admin console has no custom header (its header is the console's own) and the escrow console renders no branding HTML at all.
|
|
56
|
+
*/
|
|
57
|
+
export declare function FrameBrandingHeader({ parsed, userMenu, fallbackMenu, appTitle }: FrameBrandingHeaderProps): React.JSX.Element | null;
|
|
58
|
+
export interface FrameBrandingFooterProps {
|
|
59
|
+
/** `useBrandingHtml(branding.footerHtml)`. */
|
|
60
|
+
parsed: ParsedBrandingHtml | null | undefined;
|
|
61
|
+
/** Rendered into the footer's `{USER_MENU}` - passed only when the header has none. */
|
|
62
|
+
userMenu?: ReactNode;
|
|
63
|
+
appTitle?: string;
|
|
64
|
+
}
|
|
65
|
+
/** The frame's version of the admin-configured `Branding.footerHtml`, sanitized. Its variables work as in the header; a `{USER_MENU}` here only takes the menu when the header has none. */
|
|
66
|
+
export declare function FrameBrandingFooter({ parsed, userMenu, appTitle }: FrameBrandingFooterProps): React.JSX.Element;
|
|
67
|
+
export interface BrandingChromeProps {
|
|
18
68
|
branding: Branding | null;
|
|
19
|
-
}
|
|
69
|
+
/** What `{APP_TITLE}` is replaced with - the page's own name. Nothing when absent. */
|
|
70
|
+
appTitle?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The admin-configured `Branding.headerHtml`, sanitized, for pages that don't use a shell, such as public pages (the booking pages). There is no
|
|
74
|
+
* account menu there, so a `{USER_MENU}` in the HTML renders nothing. Renders nothing without branding or HTML.
|
|
75
|
+
*/
|
|
76
|
+
export declare function BrandingHeader({ branding, appTitle }: BrandingChromeProps): React.JSX.Element;
|
|
77
|
+
/** The admin-configured `Branding.footerHtml`, sanitized, for pages that don't use a shell - see `BrandingHeader`. */
|
|
78
|
+
export declare function BrandingFooter({ branding, appTitle }: BrandingChromeProps): React.JSX.Element;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
7
|
+
import { createPortal } from "react-dom";
|
|
2
8
|
import DOMPurify from "dompurify";
|
|
9
|
+
import { useHeaderHeightRef } from "../../notifications/headerOffset.js";
|
|
3
10
|
/** Elements that could run script, restyle the whole page, collect input, or embed another page - stripped on
|
|
4
11
|
* top of DOMPurify's defaults (which already drop `<script>`, `on*` handlers, and `javascript:` URLs). */
|
|
5
12
|
const FORBIDDEN_TAGS = [
|
|
@@ -23,6 +30,15 @@ const FORBIDDEN_TAGS = [
|
|
|
23
30
|
"math",
|
|
24
31
|
];
|
|
25
32
|
const FORBIDDEN_ATTRS = ["action", "formaction", "srcdoc"];
|
|
33
|
+
/** The attribute that marks the elements React fills in (`{USER_MENU}`, `{APP_TITLE}`). Stripped from the author's own HTML, so only
|
|
34
|
+
* this module's placeholders can carry it. */
|
|
35
|
+
export const SLOT_ATTRIBUTE = "data-rr-slot";
|
|
36
|
+
/** The variables a branding header or footer may contain, written in the HTML's text. */
|
|
37
|
+
export const USER_MENU_VARIABLE = "{USER_MENU}";
|
|
38
|
+
export const APP_TITLE_VARIABLE = "{APP_TITLE}";
|
|
39
|
+
const VARIABLE = /\{(USER_MENU|APP_TITLE)\}/g;
|
|
40
|
+
/** Elements whose text is not markup a placeholder element could live in (raw text and RCDATA), and the ones a menu must not sit inside. */
|
|
41
|
+
const RAW_TEXT_PARENTS = new Set(["TITLE", "TEXTAREA", "OPTION", "STYLE", "SCRIPT", "NOSCRIPT", "XMP", "IFRAME", "NOEMBED", "NOFRAMES", "PLAINTEXT"]);
|
|
26
42
|
/**
|
|
27
43
|
* Sanitizes admin-configured branding HTML before it's rendered. The server sanitizes on save too; this also
|
|
28
44
|
* covers anything stored before that. Returns `""` where there's no DOM to sanitize with (server-side
|
|
@@ -34,21 +50,133 @@ export function sanitizeBrandingHtml(html) {
|
|
|
34
50
|
}
|
|
35
51
|
return DOMPurify.sanitize(html, { FORBID_TAGS: FORBIDDEN_TAGS, FORBID_ATTR: FORBIDDEN_ATTRS });
|
|
36
52
|
}
|
|
37
|
-
function
|
|
38
|
-
const
|
|
39
|
-
|
|
53
|
+
function slotElement(name) {
|
|
54
|
+
const span = document.createElement("span");
|
|
55
|
+
span.setAttribute(SLOT_ATTRIBUTE, name);
|
|
56
|
+
return span;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Sanitizes `html` (the same rules as `sanitizeBrandingHtml()`) and swaps the variables in its **text** for placeholder elements:
|
|
60
|
+
* `{USER_MENU}` becomes the slot the account menu is rendered into, `{APP_TITLE}` a slot holding the current app's name.
|
|
61
|
+
*
|
|
62
|
+
* - Only text is searched, after sanitizing - a variable in an attribute (`title="{USER_MENU}"`), a comment, or a `<script>` (which
|
|
63
|
+
* is gone) is never touched, and the author cannot write a placeholder themselves (`data-rr-slot` is stripped).
|
|
64
|
+
* - `{USER_MENU}` is replaced **once**, at its first place in the document; a second (or third) one is removed, so there is never
|
|
65
|
+
* more than one account menu (two would give two sets of duplicate ids and focus targets). One inside a link, or in text that
|
|
66
|
+
* can't hold an element (`<title>`, `<textarea>`), is removed too: a menu inside an `<a>` would navigate when clicked.
|
|
67
|
+
* - `{APP_TITLE}` is replaced everywhere it appears. Inside `<title>`-like raw text it stays as written.
|
|
68
|
+
* - Nothing is matched loosely: the names are upper-case, in braces, with no spaces.
|
|
69
|
+
*
|
|
70
|
+
* `null` when there is nothing to show (no DOM to sanitize with - server-side rendering - or the HTML is empty once sanitized).
|
|
71
|
+
*/
|
|
72
|
+
export function parseBrandingHtml(html) {
|
|
73
|
+
if (!DOMPurify.isSupported) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const fragment = DOMPurify.sanitize(html, {
|
|
77
|
+
FORBID_TAGS: FORBIDDEN_TAGS,
|
|
78
|
+
FORBID_ATTR: [...FORBIDDEN_ATTRS, SLOT_ATTRIBUTE],
|
|
79
|
+
RETURN_DOM_FRAGMENT: true,
|
|
80
|
+
});
|
|
81
|
+
const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_TEXT);
|
|
82
|
+
const texts = [];
|
|
83
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
84
|
+
texts.push(node);
|
|
85
|
+
}
|
|
86
|
+
let hasUserMenu = false;
|
|
87
|
+
for (const text of texts) {
|
|
88
|
+
const parent = text.parentElement;
|
|
89
|
+
if (parent && RAW_TEXT_PARENTS.has(parent.tagName)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const inLink = !!parent?.closest("a");
|
|
93
|
+
// `split` with a capturing group alternates text and variable names: [text, name, text, name, ..., text].
|
|
94
|
+
const pieces = text.data.split(VARIABLE);
|
|
95
|
+
if (pieces.length === 1) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const replacement = document.createDocumentFragment();
|
|
99
|
+
pieces.forEach((piece, index) => {
|
|
100
|
+
if (index % 2 === 0) {
|
|
101
|
+
if (piece) {
|
|
102
|
+
replacement.appendChild(document.createTextNode(piece));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (piece === "APP_TITLE") {
|
|
106
|
+
replacement.appendChild(slotElement("app-title"));
|
|
107
|
+
}
|
|
108
|
+
else if (!hasUserMenu && !inLink) {
|
|
109
|
+
hasUserMenu = true;
|
|
110
|
+
replacement.appendChild(slotElement("user-menu"));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
text.replaceWith(replacement);
|
|
114
|
+
}
|
|
115
|
+
const holder = document.createElement("div");
|
|
116
|
+
holder.appendChild(fragment);
|
|
117
|
+
const result = holder.innerHTML;
|
|
118
|
+
return result.trim() ? { html: result, hasUserMenu } : null;
|
|
119
|
+
}
|
|
120
|
+
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
121
|
+
/**
|
|
122
|
+
* `parseBrandingHtml()` of `html`, done once the component is on the page (never while rendering: the server has no DOM to sanitize
|
|
123
|
+
* with, and rendering different HTML there than the hydrating client would leave the page without it). `null` when there is no HTML,
|
|
124
|
+
* or nothing is left of it; `undefined` while it has not been parsed yet - the frame reserves the header's place meanwhile.
|
|
125
|
+
*/
|
|
126
|
+
export function useBrandingHtml(html) {
|
|
127
|
+
const source = html?.trim() ? html : undefined;
|
|
128
|
+
const [parsed, setParsed] = useState(undefined);
|
|
129
|
+
useIsomorphicLayoutEffect(() => {
|
|
130
|
+
setParsed(source === undefined ? undefined : { source, value: parseBrandingHtml(source) });
|
|
131
|
+
}, [source]);
|
|
132
|
+
if (source === undefined) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return parsed?.source === source ? parsed.value : undefined;
|
|
136
|
+
}
|
|
137
|
+
function BrandingHtml({ parsed, userMenu, hostMenu, appTitle, className }) {
|
|
138
|
+
const ref = useRef(null);
|
|
139
|
+
const [targets, setTargets] = useState({ menu: null, titles: [] });
|
|
140
|
+
const html = parsed?.html ?? "";
|
|
141
|
+
// The placeholders exist once React has set the HTML: find them here, before the browser paints, and portal into them.
|
|
142
|
+
useIsomorphicLayoutEffect(() => {
|
|
143
|
+
const root = ref.current;
|
|
144
|
+
setTargets({
|
|
145
|
+
menu: root?.querySelector(`[${SLOT_ATTRIBUTE}="user-menu"]`) ?? null,
|
|
146
|
+
titles: root ? Array.from(root.querySelectorAll(`[${SLOT_ATTRIBUTE}="app-title"]`)) : [],
|
|
147
|
+
});
|
|
148
|
+
}, [html]);
|
|
149
|
+
if (parsed === null) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { ref: ref, className: className, dangerouslySetInnerHTML: { __html: html } }), hostMenu && targets.menu && createPortal(userMenu, targets.menu), targets.titles.map((element, index) => createPortal(appTitle, element, `app-title-${index}`))] }));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The frame's version of the admin-configured `Branding.headerHtml` (see `useBranding()`), sanitized, in a `<header>` that stays at the top of the window as the
|
|
156
|
+
* page scrolls. When it is there the app has no title bar of its own (`AppChrome`): the header is the top of the app, and the account
|
|
157
|
+
* menu lives in it - where the HTML writes `{USER_MENU}`, else in a small cell at its right end. The webmail chrome (`AppShell`) shows
|
|
158
|
+
* it; the admin console has no custom header (its header is the console's own) and the escrow console renders no branding HTML at all.
|
|
159
|
+
*/
|
|
160
|
+
export function FrameBrandingHeader({ parsed, userMenu, fallbackMenu, appTitle }) {
|
|
161
|
+
// Publishes the header's height (`--rr-header-h`) for the pop-up stack, which sticks just below it.
|
|
162
|
+
const headerRef = useHeaderHeightRef();
|
|
163
|
+
if (parsed === null) {
|
|
40
164
|
return null;
|
|
41
165
|
}
|
|
42
|
-
return _jsx("div", {
|
|
166
|
+
return (_jsxs("header", { ref: headerRef, className: "sticky top-0 z-30 shrink-0 flex items-stretch bg-surface-alt", "data-branding-header": "", children: [_jsx(BrandingHtml, { parsed: parsed, userMenu: userMenu, hostMenu: parsed?.hasUserMenu, appTitle: appTitle, className: "flex-1 min-w-0" }), parsed && !parsed.hasUserMenu && fallbackMenu ? (_jsx("div", { className: "shrink-0 flex items-center px-3 bg-surface border-l border-border", "data-branding-menu-cell": "", children: fallbackMenu })) : null] }));
|
|
167
|
+
}
|
|
168
|
+
/** The frame's version of the admin-configured `Branding.footerHtml`, sanitized. Its variables work as in the header; a `{USER_MENU}` here only takes the menu when the header has none. */
|
|
169
|
+
export function FrameBrandingFooter({ parsed, userMenu, appTitle }) {
|
|
170
|
+
return _jsx(BrandingHtml, { parsed: parsed, userMenu: userMenu, hostMenu: userMenu !== undefined, appTitle: appTitle });
|
|
43
171
|
}
|
|
44
172
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* escrow console deliberately renders no branding HTML at all.
|
|
173
|
+
* The admin-configured `Branding.headerHtml`, sanitized, for pages that don't use a shell, such as public pages (the booking pages). There is no
|
|
174
|
+
* account menu there, so a `{USER_MENU}` in the HTML renders nothing. Renders nothing without branding or HTML.
|
|
48
175
|
*/
|
|
49
|
-
export function BrandingHeader({ branding }) {
|
|
50
|
-
return _jsx(BrandingHtml, {
|
|
176
|
+
export function BrandingHeader({ branding, appTitle }) {
|
|
177
|
+
return _jsx(BrandingHtml, { parsed: useBrandingHtml(branding?.headerHtml), appTitle: appTitle });
|
|
51
178
|
}
|
|
52
|
-
|
|
53
|
-
|
|
179
|
+
/** The admin-configured `Branding.footerHtml`, sanitized, for pages that don't use a shell - see `BrandingHeader`. */
|
|
180
|
+
export function BrandingFooter({ branding, appTitle }) {
|
|
181
|
+
return _jsx(BrandingHtml, { parsed: useBrandingHtml(branding?.footerHtml), appTitle: appTitle });
|
|
54
182
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/** The number of rows at the top of `size` x `size` RGBA pixel data that are empty - the icon's own margin above its first visible pixel. */
|
|
3
|
+
export declare function emptyRowsAtTop(rgba: ArrayLike<number>, size: number): number;
|
|
4
|
+
/** Whether this environment can decode an image and read it back - not a server, and not jsdom, where there is nothing to measure. */
|
|
5
|
+
export declare function canMeasureImages(): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* How many pixels of empty margin an icon has above its first visible pixel when fitted (`object-fit: contain`) into a `SIZE` square.
|
|
8
|
+
* Drawn on a canvas and read back, so it works for any picture the browser can decode from the same origin (or with CORS); `0` where
|
|
9
|
+
* that isn't possible (no canvas, a picture that won't load, one from another origin that taints the canvas).
|
|
10
|
+
*/
|
|
11
|
+
export declare function measureTopMargin(src: string): Promise<number>;
|
|
12
|
+
/**
|
|
13
|
+
* The deployment's icon at the top of an app rail (`AppShell`, `AdminShell`, `EscrowShell`): a block as wide as the rail and as tall as the
|
|
14
|
+
* title bar beside it (4rem), flush with the top and left edges of the window - no padding above it - with the bar's own bottom border
|
|
15
|
+
* continued under it, so the two read as one block. The picture is fitted inside the block, never cropped, and raised by whatever empty
|
|
16
|
+
* margin the file itself has above its artwork, so the first visible pixel is at the very top of the window (a logo exported with 6% of
|
|
17
|
+
* air around it would otherwise still leave a gap). The icon is shown once measured (a few milliseconds after it loads).
|
|
18
|
+
*/
|
|
19
|
+
export default function RailIcon({ src }: {
|
|
20
|
+
src: string;
|
|
21
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
/** The side, in CSS pixels, of the square the icon is fitted into and measured in: the rail is 4rem wide and the title bar 4rem tall, each less its 1px border. */
|
|
8
|
+
const SIZE = 63;
|
|
9
|
+
/** Pixels this transparent or less count as empty. */
|
|
10
|
+
const EMPTY_ALPHA = 24;
|
|
11
|
+
/** The number of rows at the top of `size` x `size` RGBA pixel data that are empty - the icon's own margin above its first visible pixel. */
|
|
12
|
+
export function emptyRowsAtTop(rgba, size) {
|
|
13
|
+
for (let y = 0; y < size; y++) {
|
|
14
|
+
for (let x = 0; x < size; x++) {
|
|
15
|
+
if (rgba[(y * size + x) * 4 + 3] > EMPTY_ALPHA) {
|
|
16
|
+
return y;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
/** Whether this environment can decode an image and read it back - not a server, and not jsdom, where there is nothing to measure. */
|
|
23
|
+
export function canMeasureImages() {
|
|
24
|
+
try {
|
|
25
|
+
return typeof new Image().decode === "function";
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* How many pixels of empty margin an icon has above its first visible pixel when fitted (`object-fit: contain`) into a `SIZE` square.
|
|
33
|
+
* Drawn on a canvas and read back, so it works for any picture the browser can decode from the same origin (or with CORS); `0` where
|
|
34
|
+
* that isn't possible (no canvas, a picture that won't load, one from another origin that taints the canvas).
|
|
35
|
+
*/
|
|
36
|
+
export async function measureTopMargin(src) {
|
|
37
|
+
try {
|
|
38
|
+
const image = new Image();
|
|
39
|
+
image.src = src;
|
|
40
|
+
await image.decode();
|
|
41
|
+
const canvas = document.createElement("canvas");
|
|
42
|
+
canvas.width = SIZE;
|
|
43
|
+
canvas.height = SIZE;
|
|
44
|
+
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
45
|
+
if (!context || !image.naturalWidth || !image.naturalHeight) {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
const scale = Math.min(SIZE / image.naturalWidth, SIZE / image.naturalHeight);
|
|
49
|
+
const width = image.naturalWidth * scale;
|
|
50
|
+
const height = image.naturalHeight * scale;
|
|
51
|
+
context.drawImage(image, (SIZE - width) / 2, (SIZE - height) / 2, width, height);
|
|
52
|
+
return emptyRowsAtTop(context.getImageData(0, 0, SIZE, SIZE).data, SIZE);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The deployment's icon at the top of an app rail (`AppShell`, `AdminShell`, `EscrowShell`): a block as wide as the rail and as tall as the
|
|
60
|
+
* title bar beside it (4rem), flush with the top and left edges of the window - no padding above it - with the bar's own bottom border
|
|
61
|
+
* continued under it, so the two read as one block. The picture is fitted inside the block, never cropped, and raised by whatever empty
|
|
62
|
+
* margin the file itself has above its artwork, so the first visible pixel is at the very top of the window (a logo exported with 6% of
|
|
63
|
+
* air around it would otherwise still leave a gap). The icon is shown once measured (a few milliseconds after it loads).
|
|
64
|
+
*/
|
|
65
|
+
export default function RailIcon({ src }) {
|
|
66
|
+
const [margin, setMargin] = useState(null);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
let cancelled = false;
|
|
69
|
+
if (!canMeasureImages()) {
|
|
70
|
+
// Nothing to measure with: show the icon as it is (in the same effect pass, so nothing waits and nothing updates later).
|
|
71
|
+
setMargin(0);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setMargin(null);
|
|
75
|
+
void measureTopMargin(src).then((measured) => {
|
|
76
|
+
if (!cancelled) {
|
|
77
|
+
setMargin(measured);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return () => {
|
|
81
|
+
cancelled = true;
|
|
82
|
+
};
|
|
83
|
+
}, [src]);
|
|
84
|
+
return (_jsx("div", { className: "h-16 w-full shrink-0 overflow-hidden border-b border-border mb-2", "data-rail-icon": "", children: _jsx("img", { src: src, width: "64", height: "64", alt: "", className: "block h-full w-full object-contain", style: { transform: margin ? `translateY(-${margin}px)` : undefined, opacity: margin === null ? 0 : 1 } }) }));
|
|
85
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import type { IconType } from "react-icons";
|
|
3
|
+
/** One button of a `ResponsiveToolbar`. */
|
|
4
|
+
export interface ToolbarAction {
|
|
5
|
+
id: string;
|
|
6
|
+
/** The visible caption while there is room, else the tooltip and the accessible name. */
|
|
7
|
+
label: string;
|
|
8
|
+
icon: IconType;
|
|
9
|
+
onClick: () => void;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** A toggle: the button's pressed state. */
|
|
12
|
+
pressed?: boolean;
|
|
13
|
+
/** `title` and `aria-keyshortcuts` for a button with a shortcut - kept as given, whatever the layout. */
|
|
14
|
+
hint?: {
|
|
15
|
+
title: string;
|
|
16
|
+
"aria-keyshortcuts"?: string;
|
|
17
|
+
};
|
|
18
|
+
/** Actions with the same `group` sit together; a divider goes between groups. */
|
|
19
|
+
group: number;
|
|
20
|
+
/** Which goes into the "More" menu first when there is not room: the lowest number first. Never used for `essential`. */
|
|
21
|
+
rank: number;
|
|
22
|
+
/** Never moves into the "More" menu (the primary action). */
|
|
23
|
+
essential?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Room (px) each kind of button takes, and what the bar spends on its padding, a divider and the "More" button - the layout is computed from them
|
|
26
|
+
* rather than measured, so the first paint on the client is already right and nothing jumps. */
|
|
27
|
+
export declare const LABELLED_BUTTON_WIDTH = 64;
|
|
28
|
+
export declare const ICON_BUTTON_WIDTH = 36;
|
|
29
|
+
export interface ToolbarLayout {
|
|
30
|
+
/** Captions under the icons. */
|
|
31
|
+
labels: boolean;
|
|
32
|
+
/** The ids of the actions shown as buttons, in order. The rest are in the "More" menu. */
|
|
33
|
+
visible: string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Which buttons a bar of `width` px shows, and how: every one with its caption when that fits; else every one as an icon (the captions go first);
|
|
37
|
+
* else as many icons as fit, the least important (`rank`) going into a "More" menu one at a time - the essential ones stay whatever the width.
|
|
38
|
+
* Pure, so it is the same on the server and the client for a given width; `width === undefined` (not measured yet, no ResizeObserver) shows
|
|
39
|
+
* everything.
|
|
40
|
+
*/
|
|
41
|
+
export declare function layoutToolbar(actions: ToolbarAction[], width: number | undefined): ToolbarLayout;
|
|
42
|
+
/** The width of `ref`'s element, kept current with a ResizeObserver; `undefined` until measured (and where there is no ResizeObserver). */
|
|
43
|
+
export declare function useElementWidth(ref: React.RefObject<HTMLElement | null>): number | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* A toolbar that fits the column it is in - never wider, never over another pane. It measures its own width (`ResizeObserver`) and shows every
|
|
46
|
+
* action with a caption when there is room; when there is not, the captions go first (each button keeps its tooltip and accessible name), then the
|
|
47
|
+
* least important actions move into a "More" menu (`layoutToolbar()`). Arrow keys, Home and End move between the buttons; the menu is a real
|
|
48
|
+
* `role="menu"`. `children` (a hidden file input, say) are rendered inside the toolbar's element but take no room.
|
|
49
|
+
*/
|
|
50
|
+
export default function ResponsiveToolbar({ label, actions, leading, children }: {
|
|
51
|
+
label: string;
|
|
52
|
+
actions: ToolbarAction[];
|
|
53
|
+
leading?: ReactNode;
|
|
54
|
+
children?: ReactNode;
|
|
55
|
+
}): React.JSX.Element;
|