@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
///////////////////////////////////////////////////////////////////////////////
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
@@ -7,14 +7,11 @@ import { useRef } from "react";
|
|
|
7
7
|
import { HiOutlineArrowDownTray, HiOutlineArrowUpTray, HiOutlineEnvelope, HiOutlinePencil, HiOutlineStar, HiOutlineTag, HiOutlineTrash, HiOutlineUserPlus, HiStar, } from "react-icons/hi2";
|
|
8
8
|
import { SHORTCUTS } from "../../keyboard/keymap.js";
|
|
9
9
|
import { useShortcutProps } from "../../keyboard/useShortcutProps.js";
|
|
10
|
-
|
|
11
|
-
return (_jsxs("button", { type: "button", onClick: onClick, disabled: disabled, ...hint, className: "flex flex-col items-center gap-1 text-xs text-text-muted hover:text-text disabled:opacity-40 disabled:cursor-not-allowed px-2.5 py-1.5 rounded-sm hover:not-disabled:bg-surface-alt", children: [_jsx(Icon, { size: 18, "aria-hidden": "true" }), _jsx("span", { children: label })] }));
|
|
12
|
-
}
|
|
13
|
-
function Divider() {
|
|
14
|
-
return _jsx("div", { className: "w-px self-stretch my-1 bg-border", "aria-hidden": "true" });
|
|
15
|
-
}
|
|
10
|
+
import ResponsiveToolbar from "../layout/ResponsiveToolbar.js";
|
|
16
11
|
/**
|
|
17
|
-
* Contacts' Outlook-style ribbon toolbar
|
|
12
|
+
* Contacts' Outlook-style ribbon toolbar, fitted to its column (`ResponsiveToolbar`): captions under the icons while there is room, then icons
|
|
13
|
+
* alone, then the rarely used actions - Import, Export, Add category, and so on up - in a "More" menu; the bar can never spill into the pane
|
|
14
|
+
* beside it. Every button keeps its tooltip (New contact its shortcut's) and accessible name in every layout. Deliberately presentational — every action is a callback the
|
|
18
15
|
* page (`apps/www/contacts/index.tsx`) implements, since it owns the actual contact data, the current
|
|
19
16
|
* selection, and the mailbox/folder context every real action (create/delete/email/favorite/category)
|
|
20
17
|
* needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
|
|
@@ -31,5 +28,23 @@ export default function ContactsToolbar({ selectedCount, allSelectedFavorited, o
|
|
|
31
28
|
onImportFile(file);
|
|
32
29
|
}
|
|
33
30
|
}
|
|
34
|
-
|
|
31
|
+
const actions = [
|
|
32
|
+
{ id: "new", label: "New contact", icon: HiOutlineUserPlus, onClick: onNewContact, hint: shortcuts ? newContactHint : undefined, group: 0, rank: 99, essential: true },
|
|
33
|
+
{ id: "edit", label: "Edit", icon: HiOutlinePencil, onClick: onEdit, disabled: selectedCount !== 1, group: 1, rank: 6 },
|
|
34
|
+
{ id: "delete", label: "Delete", icon: HiOutlineTrash, onClick: onDelete, disabled: !hasSelection, group: 1, rank: 5 },
|
|
35
|
+
{ id: "email", label: "Email", icon: HiOutlineEnvelope, onClick: onEmail, disabled: !hasSelection, group: 2, rank: 4 },
|
|
36
|
+
{
|
|
37
|
+
id: "favorite",
|
|
38
|
+
label: allSelectedFavorited ? "Unfavorite" : "Favorite",
|
|
39
|
+
icon: allSelectedFavorited ? HiStar : HiOutlineStar,
|
|
40
|
+
onClick: onToggleFavorite,
|
|
41
|
+
disabled: !hasSelection,
|
|
42
|
+
group: 2,
|
|
43
|
+
rank: 3,
|
|
44
|
+
},
|
|
45
|
+
{ id: "category", label: "Add category", icon: HiOutlineTag, onClick: onAddCategory, disabled: !hasSelection, group: 2, rank: 2 },
|
|
46
|
+
{ id: "export", label: "Export", icon: HiOutlineArrowUpTray, onClick: onExportVCard, disabled: !hasSelection, group: 3, rank: 1 },
|
|
47
|
+
{ id: "import", label: "Import", icon: HiOutlineArrowDownTray, onClick: () => importInputRef.current?.click(), group: 3, rank: 0 },
|
|
48
|
+
];
|
|
49
|
+
return (_jsx(ResponsiveToolbar, { label: "Contacts actions", actions: actions, children: _jsx("input", { ref: importInputRef, type: "file", accept: ".vcf", className: "sr-only", "aria-label": "Import contacts file", onChange: handleImportFileChosen }) }));
|
|
35
50
|
}
|
|
@@ -97,7 +97,10 @@ export default function ContactsShell({ userUid, authServerUrl, impersonating, i
|
|
|
97
97
|
const sidebarContent = (idPrefix) => (_jsxs(_Fragment, { children: [mailboxes.length > 1 && (_jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold uppercase tracking-wide text-text-muted mb-1", htmlFor: `${idPrefix}-contacts-mailbox-switcher`, children: "Mailbox" }), _jsx("select", { id: `${idPrefix}-contacts-mailbox-switcher`, className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: mailboxUid, onChange: (e) => {
|
|
98
98
|
navigate(`/contacts?mailboxUid=${encodeURIComponent(e.target.value)}`);
|
|
99
99
|
}, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), folderError && _jsx(Alert, { children: folderError })] }));
|
|
100
|
-
|
|
100
|
+
// The mailbox switcher (or an error) is all this column holds: with a single mailbox and no error there is nothing to put in it, and an empty
|
|
101
|
+
// 224 px column beside the contacts' own menu is wasted room.
|
|
102
|
+
const hasSidebar = mailboxes.length > 1 || !!folderError;
|
|
103
|
+
inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") })), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(ContactsShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
101
104
|
}
|
|
102
105
|
return (_jsx(AppShell, { active: "contacts", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
|
|
103
106
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
///////////////////////////////////////////////////////////////////////////////
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
@@ -13,6 +13,8 @@ import { getBranding } from "@rapidmx/react-shared/branding/brandingApi.js";
|
|
|
13
13
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
14
14
|
import BottomTabBar from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
|
|
15
15
|
import UserMenu from "../../layout/UserMenu.js";
|
|
16
|
+
import RailIcon from "../../layout/RailIcon.js";
|
|
17
|
+
import AppearanceProvider from "../../../appearance/AppearanceProvider.js";
|
|
16
18
|
import { signOutOfConsole } from "../../admin/signOut.js";
|
|
17
19
|
const DEFAULT_ICON_SRC = "/images/logo.svg";
|
|
18
20
|
const NAV_ITEMS = [
|
|
@@ -83,12 +85,13 @@ export default function EscrowShell({ active, userUid, authServerUrl, children }
|
|
|
83
85
|
}
|
|
84
86
|
else {
|
|
85
87
|
const activeItem = NAV_ITEMS.find((item) => item.id === active);
|
|
86
|
-
content = (_jsx("div", { className: "min-h-screen flex flex-col
|
|
88
|
+
content = (_jsx("div", { className: "rr-frame-bg min-h-screen flex flex-col", children: _jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsxs("nav", { "aria-label": "Escrow sections", className: "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center pb-3 gap-1", children: [_jsx(RailIcon, { src: iconSrc }), NAV_ITEMS.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
|
|
87
89
|
"w-10 h-10 flex items-center justify-center rounded-sm",
|
|
88
90
|
id === active
|
|
89
91
|
? "bg-primary/10 text-primary-dark"
|
|
90
92
|
: "text-text-muted hover:bg-surface-alt hover:text-text",
|
|
91
|
-
].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: NAV_ITEMS, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeItem?.label }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut })] }), _jsx("div", { className: "flex-1 pb-14 md:pb-0", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
|
|
93
|
+
].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: NAV_ITEMS, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeItem?.label }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut })] }), _jsx("div", { id: "app-content", className: "flex-1 pb-14 md:pb-0", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
|
|
92
94
|
}
|
|
93
|
-
|
|
95
|
+
// The user's own colours and background (`AppearanceProvider`), from what the last webmail page cached in this browser: the console never asks the server.
|
|
96
|
+
return (_jsx(AppearanceProvider, { userUid: userUid, lookUp: false, children: content }));
|
|
94
97
|
}
|
|
@@ -2,6 +2,7 @@ import "../../styles/app.css";
|
|
|
2
2
|
import React, { PropsWithChildren } from "react";
|
|
3
3
|
import type { IconType } from "react-icons";
|
|
4
4
|
import { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
|
|
5
|
+
import type { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
|
|
5
6
|
import { PluginNav, PluginNavProps } from "../../plugins/pluginNav.js";
|
|
6
7
|
/** How long sign-out waits for auth-server's logout before navigating anyway. */
|
|
7
8
|
export declare const LOGOUT_TIMEOUT_MS = 3000;
|
|
@@ -34,6 +35,10 @@ export interface AppShellProps extends PluginNavProps {
|
|
|
34
35
|
/** The role names the server treats as trusted (its `trusted_roles` config, injected via the route's `fetchProps`) -
|
|
35
36
|
* what that lookup looks for. Absent means the server's own default, `["admin"]`. */
|
|
36
37
|
trustedRoles?: string[];
|
|
38
|
+
/** The deployment's branding as the server rendered the page (its `branding` prop). Used until the frame's own fetch answers, so a custom header is there from the first paint - and the frame knows to drop its own title bar - instead of appearing (and moving everything) once the request returns. */
|
|
39
|
+
branding?: Branding;
|
|
40
|
+
/** The signed-in user's stored appearance preferences as the server rendered the page (its `appearance` prop), so the first paint already wears the theme and background - see `AppearanceProvider`. */
|
|
41
|
+
appearance?: unknown;
|
|
37
42
|
}
|
|
38
43
|
export interface AppDef {
|
|
39
44
|
id: AppShellApp;
|
|
@@ -64,7 +69,7 @@ export interface AppChromeProps extends AppShellProps {
|
|
|
64
69
|
* Rendered by `AppRouter`, once, for the life of the page, so that moving between apps replaces only `children` - see
|
|
65
70
|
* `AppShell` below for what a page's own shell renders instead.
|
|
66
71
|
*/
|
|
67
|
-
export declare function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, routeKey, busy, hideChrome, children, }: PropsWithChildren<AppChromeProps>): React.JSX.Element;
|
|
72
|
+
export declare function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, routeKey, busy, hideChrome, children, }: PropsWithChildren<AppChromeProps>): React.JSX.Element;
|
|
68
73
|
/**
|
|
69
74
|
* What a page's own shell (`MailShell`, `CalendarShell`, ...) renders around its content. Outside the client-side router it
|
|
70
75
|
* is the whole `AppChrome`, as it always was. Inside it (`AppRouter`) the chrome is already mounted above the page - and stays
|
|
@@ -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;
|