@ds-mo/ui 15.10.1 → 15.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/dist/.package-ready.json +1 -1
- package/dist/agent-patterns.json +1 -1
- package/dist/agent.json +13 -11
- package/dist/angular/ds-shell-app.d.ts +3 -8
- package/dist/angular/ds-shell-tools.d.ts +3 -8
- package/dist/components/ds-agent-questionnaire.js +1 -1
- package/dist/components/ds-agent-response.js +1 -1
- package/dist/components/ds-banner.js +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-page-title.js +1 -1
- package/dist/components/ds-bar-title.js +1 -1
- package/dist/components/ds-bar-workflow.js +1 -1
- package/dist/components/ds-button-unfilled.js +1 -1
- package/dist/components/ds-calendar.js +1 -1
- package/dist/components/ds-card-chart.js +1 -1
- package/dist/components/ds-card-overview.js +1 -1
- package/dist/components/ds-card-overview.js.map +1 -1
- package/dist/components/ds-card-setting.js +1 -1
- package/dist/components/ds-chart.js +1 -1
- package/dist/components/ds-chart.js.map +1 -1
- package/dist/components/ds-code-block.js +1 -1
- package/dist/components/ds-data-customize.js +1 -1
- package/dist/components/ds-data-filter.js +1 -1
- package/dist/components/ds-data-group.js +1 -1
- package/dist/components/ds-data-preferences.js +1 -1
- package/dist/components/ds-data-saved-views.js +1 -1
- package/dist/components/ds-data-search.js +1 -1
- package/dist/components/ds-data-sort.js +1 -1
- package/dist/components/ds-filter-menu.js +1 -1
- package/dist/components/ds-input-date.js +1 -1
- package/dist/components/ds-input-time.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-markdown.js +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-message-actions.js +1 -1
- package/dist/components/ds-message-composer.js +1 -1
- package/dist/components/ds-message-scroller.js +1 -1
- package/dist/components/ds-mobile-bar-nav.js +1 -1
- package/dist/components/ds-mobile-header.js +1 -1
- package/dist/components/ds-mobile-section-switcher.js +1 -1
- package/dist/components/ds-mobile-sheet-nav.js +1 -1
- package/dist/components/ds-modal.js +1 -1
- package/dist/components/ds-pagination.js +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-tool-header.js +1 -1
- package/dist/components/ds-panel-tool-search.js +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-scroll-overlay.js +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-shell-app.js +1 -1
- package/dist/components/ds-shell-app.js.map +1 -1
- package/dist/components/ds-shell-page.js +1 -1
- package/dist/components/ds-shell-tools.js +1 -1
- package/dist/components/ds-table.js +1 -1
- package/dist/components/ds-table.js.map +1 -1
- package/dist/components/ds-toast.js +1 -1
- package/dist/components/ds-toast.js.map +1 -1
- package/dist/components/ds-tooltip.js +1 -1
- package/dist/components/{p-PNY7auMh.js → p-B5rN3slq.js} +2 -2
- package/dist/components/{p-kQ6wvk-r.js → p-B9Q7fXxW.js} +2 -2
- package/dist/components/p-BNnw_O4e.js +2 -0
- package/dist/components/p-BNnw_O4e.js.map +1 -0
- package/dist/components/{p-BgpzSEWv.js → p-BT6xQogY.js} +2 -2
- package/dist/components/{p-YmJYwnhW.js → p-Ba-dSshd.js} +2 -2
- package/dist/components/{p-YmJYwnhW.js.map → p-Ba-dSshd.js.map} +1 -1
- package/dist/components/p-BkajUPiA.js +2 -0
- package/dist/components/p-BkajUPiA.js.map +1 -0
- package/dist/components/{p-lJeAJKa5.js → p-BqIRExej.js} +2 -2
- package/dist/components/{p-BtSEQ-FW.js → p-C8D853vs.js} +2 -2
- package/dist/components/{p-CHYYRdRJ.js → p-CEB8RFih.js} +2 -2
- package/dist/components/{p-DuCCuJKI.js → p-CGKjjJZm.js} +2 -2
- package/dist/components/{p-2ZV8JA88.js → p-CMrHRjjH.js} +2 -2
- package/dist/components/{p-DwuqJhR5.js → p-CPNA8raE.js} +2 -2
- package/dist/components/{p-ioGH5SoA.js → p-CVYjLzYI.js} +2 -2
- package/dist/components/p-CVYjLzYI.js.map +1 -0
- package/dist/components/p-CW-7vLUB.js +2 -0
- package/dist/components/p-CW-7vLUB.js.map +1 -0
- package/dist/components/{p-BrB05_X8.js → p-CaNMVLhm.js} +2 -2
- package/dist/components/{p-hmvu1lmP.js → p-Cxk1A7Sx.js} +2 -2
- package/dist/components/p-Cxk1A7Sx.js.map +1 -0
- package/dist/components/{p-BjLGayCj.js → p-D-cZ6cRI.js} +2 -2
- package/dist/components/{p-DQeUT9o2.js → p-D4r6oWyQ.js} +2 -2
- package/dist/components/{p-DoRgzvup.js → p-DGDF4NMV.js} +2 -2
- package/dist/components/{p-DSpKpoSG.js → p-DKG26pp5.js} +2 -2
- package/dist/components/p-DfphSI9S.js +2 -0
- package/dist/components/p-DfphSI9S.js.map +1 -0
- package/dist/components/p-DfwvHxYi.js +2 -0
- package/dist/components/p-DfwvHxYi.js.map +1 -0
- package/dist/components/{p-BQE2Uj4p.js → p-DfzXyVoM.js} +2 -2
- package/dist/components/{p-BQE2Uj4p.js.map → p-DfzXyVoM.js.map} +1 -1
- package/dist/components/{p-z_8aZHHo.js → p-DixX2_Bd.js} +2 -2
- package/dist/components/{p-CipTqNIr.js → p-Djkk338n.js} +2 -2
- package/dist/components/{p-Dx7wJaLv.js → p-DvMCOJwc.js} +2 -2
- package/dist/components/{p-ChsBscqZ.js → p-DwFnt0W0.js} +2 -2
- package/dist/components/{p-uAtciSyr.js → p-OFJpWJcN.js} +2 -2
- package/dist/components/{p-Bx_72ylJ.js → p-S5QCVAdH.js} +2 -2
- package/dist/docs/components.json +13 -13
- package/dist/lib/utils/index.js +3 -1
- package/dist/lib/utils/index.js.map +2 -2
- package/dist/lib/utils/resolve-css-length-px.d.ts +1 -1
- package/dist/mcp-data/registry/card-overview.json +1 -1
- package/dist/mcp-data/registry/chart.json +3 -3
- package/dist/mcp-data/registry/code-block.json +3 -2
- package/dist/mcp-data/registry/mobile-bar-nav.json +3 -3
- package/dist/mcp-data/registry/mobile-header.json +3 -3
- package/dist/mcp-data/registry/mobile-section-switcher.json +3 -2
- package/dist/mcp-data/registry/mobile-sheet-nav.json +6 -6
- package/dist/mcp-data/registry/modal.json +1 -1
- package/dist/mcp-data/registry/panel-tools.json +1 -1
- package/dist/mcp-data/registry/registry.json +12 -10
- package/dist/mcp-data/registry/shell-app.json +3 -3
- package/dist/mcp-data/registry/shell-tools.json +4 -4
- package/dist/mcp-data/registry/table.json +2 -2
- package/dist/mcp-data/registry/toast.json +1 -1
- package/dist/react/ds-shell-app.d.ts +2 -5
- package/dist/react/ds-shell-tools.d.ts +2 -5
- package/dist/styles/responsive.css +293 -0
- package/dist/types/components/CodeBlock/CodeBlock.d.ts +8 -0
- package/dist/types/components/ShellApp/ShellApp.d.ts +2 -5
- package/dist/types/components/ShellTools/ShellTools.d.ts +2 -5
- package/dist/types/components.d.ts +4 -10
- package/dist/types/utils/resolve-css-length-px.d.ts +1 -1
- package/package.json +2 -1
- package/dist/components/p-BE9ladwu.js +0 -2
- package/dist/components/p-BE9ladwu.js.map +0 -1
- package/dist/components/p-BfdYb8L4.js +0 -2
- package/dist/components/p-BfdYb8L4.js.map +0 -1
- package/dist/components/p-BuwYiPCW.js +0 -2
- package/dist/components/p-BuwYiPCW.js.map +0 -1
- package/dist/components/p-cktJHlYv.js +0 -2
- package/dist/components/p-cktJHlYv.js.map +0 -1
- package/dist/components/p-hmvu1lmP.js.map +0 -1
- package/dist/components/p-iEhBF5Oi.js +0 -2
- package/dist/components/p-iEhBF5Oi.js.map +0 -1
- package/dist/components/p-ioGH5SoA.js.map +0 -1
- /package/dist/components/{p-PNY7auMh.js.map → p-B5rN3slq.js.map} +0 -0
- /package/dist/components/{p-kQ6wvk-r.js.map → p-B9Q7fXxW.js.map} +0 -0
- /package/dist/components/{p-BgpzSEWv.js.map → p-BT6xQogY.js.map} +0 -0
- /package/dist/components/{p-lJeAJKa5.js.map → p-BqIRExej.js.map} +0 -0
- /package/dist/components/{p-BtSEQ-FW.js.map → p-C8D853vs.js.map} +0 -0
- /package/dist/components/{p-CHYYRdRJ.js.map → p-CEB8RFih.js.map} +0 -0
- /package/dist/components/{p-DuCCuJKI.js.map → p-CGKjjJZm.js.map} +0 -0
- /package/dist/components/{p-2ZV8JA88.js.map → p-CMrHRjjH.js.map} +0 -0
- /package/dist/components/{p-DwuqJhR5.js.map → p-CPNA8raE.js.map} +0 -0
- /package/dist/components/{p-BrB05_X8.js.map → p-CaNMVLhm.js.map} +0 -0
- /package/dist/components/{p-BjLGayCj.js.map → p-D-cZ6cRI.js.map} +0 -0
- /package/dist/components/{p-DQeUT9o2.js.map → p-D4r6oWyQ.js.map} +0 -0
- /package/dist/components/{p-DoRgzvup.js.map → p-DGDF4NMV.js.map} +0 -0
- /package/dist/components/{p-DSpKpoSG.js.map → p-DKG26pp5.js.map} +0 -0
- /package/dist/components/{p-z_8aZHHo.js.map → p-DixX2_Bd.js.map} +0 -0
- /package/dist/components/{p-CipTqNIr.js.map → p-Djkk338n.js.map} +0 -0
- /package/dist/components/{p-Dx7wJaLv.js.map → p-DvMCOJwc.js.map} +0 -0
- /package/dist/components/{p-ChsBscqZ.js.map → p-DwFnt0W0.js.map} +0 -0
- /package/dist/components/{p-uAtciSyr.js.map → p-OFJpWJcN.js.map} +0 -0
- /package/dist/components/{p-Bx_72ylJ.js.map → p-S5QCVAdH.js.map} +0 -0
|
@@ -289,7 +289,7 @@
|
|
|
289
289
|
},
|
|
290
290
|
{
|
|
291
291
|
"name": "dsHeaderAction",
|
|
292
|
-
"detail": "
|
|
292
|
+
"detail": "PanelToolsHeaderActionDetail",
|
|
293
293
|
"bubbles": true,
|
|
294
294
|
"cancelable": true,
|
|
295
295
|
"composed": true,
|
|
@@ -534,7 +534,7 @@
|
|
|
534
534
|
},
|
|
535
535
|
{
|
|
536
536
|
"name": "dsHeaderAction",
|
|
537
|
-
"detail": "
|
|
537
|
+
"detail": "PanelToolsHeaderActionDetail",
|
|
538
538
|
"bubbles": true,
|
|
539
539
|
"cancelable": true,
|
|
540
540
|
"composed": true,
|
|
@@ -726,7 +726,7 @@
|
|
|
726
726
|
},
|
|
727
727
|
{
|
|
728
728
|
"path": "src/wc/components/ShellApp/ShellApp.tsx",
|
|
729
|
-
"content": "import {\n Component,\n Prop,\n Element,\n Event,\n EventEmitter,\n State,\n Watch,\n Listen,\n Method,\n h,\n Host,\n} from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport { ConnectionTasks } from '../../utils/connection-tasks';\nimport { isEditableShortcutTarget, resolveShellShortcut } from '../../shell/shell-shortcuts';\nimport type {\n PanelToolsHeaderAction,\n PanelToolsRailAccessoryActionDetail,\n PanelToolsToolId,\n} from '../PanelTools/panel-tools-types';\nimport { PANEL_TOOLS_DEFAULT_ITEMS } from '../PanelTools/panel-tools-types';\nimport type { BarTitleActionConfigItem, BarTitleSectionItem } from '../BarTitle/bar-title-types';\nimport type { BarTitlePlacement } from '../BarTitle/bar-title-types';\nimport {\n findBarTitleAction,\n overflowBarTitleActionSections,\n resolveBarTitleActionItems,\n visibleBarTitleActions,\n} from '../BarTitle/bar-title-actions';\nimport type { BarNavTab } from '../BarNav/bar-nav-types';\nimport type { BreadcrumbSelectDetail } from '../Breadcrumb/breadcrumb-types';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport {\n DEFAULT_SHELL_GRADIENT_PRESET,\n normalizeShellGradientPreset,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\nimport {\n CHROME_TRANSITION_END,\n CHROME_TRANSITION_START,\n ChromeTransitionDepth,\n createRafCoalescer,\n readChromeTransitionSource,\n} from '../../shell/chrome-transition';\nimport {\n isPanelNavCollapsed,\n panelWidthPxFromTokens,\n readPanelNavWidthTokens,\n type PanelNavWidthTokens,\n} from '../../shell/shell-chrome-metrics';\nimport {\n SHELL_GRADIENT_IMAGE_VAR,\n SHELL_GRADIENT_OPACITY_VAR,\n SHELL_GRADIENT_POSITION_BAR_VAR,\n SHELL_GRADIENT_POSITION_PANEL_VAR,\n SHELL_GRADIENT_SIZE_VAR,\n buildShellRadialGradient,\n readShellViewportDimensions,\n shellGradientPositionBar,\n shellGradientPositionPanel,\n shellGradientPresetOpacity,\n shellGradientSize,\n} from '../../shell/shell-gradient';\nimport {\n itemUsesShellInbox,\n resolveAvailableInboxTool,\n resolveManagedShellPageCapacity,\n resolveShellResponsiveMode,\n SHELL_DESKTOP_BREAKPOINT,\n SHELL_WIDE_DESKTOP_BREAKPOINT,\n shellMobileDestinationForTool,\n type MobileDestination,\n type ShellInboxToolId,\n type ShellResponsiveMode,\n} from '../../shell/shell-responsive';\nimport { deriveActiveIdFromUrl } from '../PanelNav/panel-nav-utils';\nimport type { PanelNavChildSelectDetail } from '../PanelNav/panel-nav-types';\nimport type { PanelNavItem } from '../PanelNav/panel-nav-types';\nimport type { PanelNavUserActionDetail } from '../PanelNav/panel-nav-types';\nimport type { MobileBarNavDestinationDetail } from '../MobileBarNav/mobile-bar-nav-types';\nimport type {\n ShellAppComposition,\n ShellNavigationConfig,\n ShellPageChromeConfig,\n ShellSectionNavigation,\n ShellToolsConfig,\n} from './shell-app-types';\nimport type { PaperTextureConfig } from '../PaperTexture/paper-texture-types';\n\nlet nextShellAppId = 0;\ntype FocusableButton = HTMLElement & { setFocus?: () => Promise<void> };\n\n@Component({\n tag: 'ds-shell-app',\n styleUrl: 'ShellApp.css',\n scoped: true,\n})\nexport class ShellApp {\n private static readonly FOREGROUND_REFRESH_MS = 50;\n\n /** Managed renders the complete responsive chrome; slotted exposes the advanced composition. */\n @Prop({ reflect: true }) composition: ShellAppComposition = 'managed';\n\n /** Router-owned navigation data used by managed composition. */\n @Prop() navigation: ShellNavigationConfig = {};\n\n /** Route sections in bar/header pickers, or nested beneath desktop and mobile menu parents. */\n @Prop({ attribute: 'section-navigation', reflect: true })\n sectionNavigation: ShellSectionNavigation = 'bar';\n\n /** Route-owned page title and section data used by managed composition. */\n @Prop({ attribute: 'page-chrome' }) pageChrome: ShellPageChromeConfig = {};\n\n /** Product-owned global tool definitions used by managed composition. */\n @Prop() tools: ShellToolsConfig = {};\n\n /** Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n /**\n * Shell chrome wash preset. `none` renders solid chrome; the remaining\n * presets use token-based washes that adapt to the active color theme.\n */\n @Prop({ attribute: 'gradient-preset', reflect: true }) gradientPreset: ShellGradientPreset =\n DEFAULT_SHELL_GRADIENT_PRESET;\n\n /** Optional decorative Paper Design texture layered above the shell wash. */\n @Prop({ attribute: 'paper-texture' }) paperTexture?: PaperTextureConfig;\n\n /** When `true` (default), registers global shell keyboard shortcuts. `[` toggles panel nav; `]` closes tools; K, A, S, M, N, and / toggle tool drawers. Modifiers are ignored so browser chords like ⌘N stay native. */\n @Prop({ attribute: 'shortcuts-enabled' }) shortcutsEnabled: boolean = true;\n\n /** Controlled mobile surface shown above the persistent bottom bar. */\n @Prop({ attribute: 'mobile-destination' })\n mobileDestination: MobileDestination = 'area';\n\n /** Controlled full-stage Mobile Sheet Nav state. */\n @Prop({ attribute: 'mobile-sheet-nav-open' })\n mobileSheetNavOpen: boolean = false;\n\n /** Emitted after crossing the fixed 768px or 1200px shell boundaries. */\n @Event() dsResponsiveModeChange!: EventEmitter<{ mode: ShellResponsiveMode }>;\n\n /** Managed primary-navigation intent; the application router performs navigation. */\n @Event() dsNavSelect!: EventEmitter<string>;\n\n /** Managed desktop BarNav or mobile section-selection intent. */\n @Event() dsTabChange!: EventEmitter<string>;\n\n /** Managed nested PanelNav child-route intent. */\n @Event() dsNavChildSelect!: EventEmitter<PanelNavChildSelectDetail>;\n\n /** Managed page-subsection intent from BarTitle or MobileHeader. */\n @Event() dsSubsectionChange!: EventEmitter<string>;\n\n /** Managed page-level Back intent. */\n @Event() dsPageBack!: EventEmitter<MouseEvent>;\n\n /** Managed breadcrumb intent from BarTitle. */\n @Event({ cancelable: true })\n dsBreadcrumbSelect!: EventEmitter<BreadcrumbSelectDetail>;\n\n /** Managed page action intent from BarTitle. */\n @Event() dsPageAction!: EventEmitter<string>;\n\n /** Managed tools selection state. */\n @Event() dsToolChange!: EventEmitter<{ id: PanelToolsToolId; selected: boolean }>;\n\n /** Managed tool-header Back intent. */\n @Event() dsHeaderBack!: EventEmitter<{ tool: PanelToolsToolId }>;\n\n /** Managed tool-header action intent. */\n @Event() dsHeaderAction!: EventEmitter<{ tool: PanelToolsToolId; id: string }>;\n\n /** Managed desktop/tablet rail accessory intent. */\n @Event() dsRailAccessoryAction!: EventEmitter<PanelToolsRailAccessoryActionDetail>;\n\n /** Managed drawer/fullscreen presentation state. */\n @Event() dsPresentationChange!: EventEmitter<{ presentation: 'drawer' | 'fullscreen' }>;\n\n /** Managed mobile Dashboard/Settings browsing intent. */\n @Event() dsBrowseContextChange!: EventEmitter<NavChromeStyle>;\n\n /** Managed account-footer action intent. */\n @Event() dsNavFooterAction!: EventEmitter<void>;\n\n /** Managed user/account trigger intent. */\n @Event() dsNavUserAction!: EventEmitter<PanelNavUserActionDetail>;\n\n @Element() el!: HTMLElement;\n @State() private toolsFullscreen = false;\n @State() private resolvedMode: ShellResponsiveMode = 'desktop';\n @State() private wideViewport = false;\n @State() private managedToolsOpen = false;\n @State() private managedActiveTool: PanelToolsToolId | '' = '';\n @State() private managedToolPresentation: 'drawer' | 'fullscreen' = 'drawer';\n @State() private managedMobileDestination: MobileDestination = 'area';\n @State() private managedMobilePrimaryDestination: 'area' | 'help' = 'area';\n @State() private managedMobileSheetNavOpen = false;\n @State() private managedInboxTool: ShellInboxToolId | '' = '';\n @State() private managedBrowseContext: NavChromeStyle = 'dashboard';\n @State() private mobileActionMenuOpen = false;\n @State() private mobileActionSurfaceOpen = false;\n @State() private mobileActionMenuInitialFocusVisible = false;\n\n private readonly instanceId = nextShellAppId++;\n private readonly mobileActionMenuTriggerId = `shell-app-mobile-action-trigger-${this.instanceId}`;\n private readonly mobileActionMenuId = `shell-app-mobile-action-menu-${this.instanceId}`;\n private mobileActionTriggerEl: FocusableButton | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private hasLoaded = false;\n private readonly connectionTasks = new ConnectionTasks(() => this.el.isConnected);\n private foregroundRefreshTimer: number | null = null;\n private readonly panelNavTransition = new ChromeTransitionDepth();\n private readonly chromeSyncCoalescer = createRafCoalescer(() => this.syncChrome());\n private panelWidthTokens: PanelNavWidthTokens = { expandedPx: 0, collapsedPx: 0 };\n private cachedViewportWidth = 0;\n private cachedViewportHeight = 0;\n private onWindowResize = () => {\n this.updateResponsiveMode();\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n };\n private onVisualViewportChange = () => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n };\n\n private get managed(): boolean {\n return this.composition === 'managed';\n }\n\n private get effectiveMobileDestination(): MobileDestination {\n return this.managed ? this.managedMobileDestination : this.mobileDestination;\n }\n\n private get effectiveMobileSheetNavOpen(): boolean {\n return this.managed ? this.managedMobileSheetNavOpen : this.mobileSheetNavOpen;\n }\n\n private get resolvedNavigationGroups() {\n if (this.navigation.groups?.length) return this.navigation.groups;\n return this.managedBrowseContext === 'settings'\n ? (this.navigation.settingsGroups ?? [])\n : (this.navigation.dashboardGroups ?? []);\n }\n\n private get resolvedToolItems() {\n return this.tools.items ?? PANEL_TOOLS_DEFAULT_ITEMS;\n }\n\n private get resolvedPageTabs(): BarNavTab[] {\n const children = this.currentArea.children;\n if (!children?.length) return this.pageChrome.tabs ?? [];\n return children.map(child => ({\n id: child.id,\n label: child.label,\n dot: child.dot,\n isInactive: child.isInactive,\n }));\n }\n\n private get availableInboxTools(): ShellInboxToolId[] {\n return this.resolvedToolItems\n .filter(item => !item.isInactive && itemUsesShellInbox(item))\n .map(item => item.id as ShellInboxToolId);\n }\n\n componentWillLoad() {\n if (typeof window !== 'undefined') {\n this.resolvedMode = resolveShellResponsiveMode(window.innerWidth);\n this.wideViewport = window.innerWidth >= SHELL_WIDE_DESKTOP_BREAKPOINT;\n }\n this.managedBrowseContext = this.navigation.browseContext ?? this.navStyle;\n this.managedMobileDestination = this.mobileDestination;\n this.managedMobileSheetNavOpen = this.mobileSheetNavOpen;\n this.managedInboxTool = resolveAvailableInboxTool('', this.availableInboxTools);\n }\n\n connectedCallback() {\n if (this.hasLoaded) this.connectResources();\n }\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.connectResources();\n }\n\n private connectResources() {\n if (!this.el.isConnected) return;\n this.updateResponsiveMode();\n this.syncSlottedNavStyle();\n this.syncSlottedMobileState();\n this.connectMetricsObserver();\n this.connectViewportListeners();\n this.connectPageLifecycleListeners();\n this.el.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n this.el.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n this.connectionTasks.frame(() => {\n this.cachePanelWidthTokens();\n this.scheduleChromeSync();\n });\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools');\n this.toolsFullscreen = tools?.getAttribute('presentation') === 'fullscreen';\n }\n\n @Listen('dsPresentationChange')\n handleToolsPresentation(event: CustomEvent<{ presentation?: string }>) {\n this.toolsFullscreen = event.detail?.presentation === 'fullscreen';\n }\n\n disconnectedCallback() {\n this.connectionTasks.cancel();\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.disconnectViewportListeners();\n this.disconnectPageLifecycleListeners();\n this.cancelForegroundRefresh();\n this.chromeSyncCoalescer.cancel();\n this.panelNavTransition.reset();\n this.el.removeEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n this.el.removeEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n }\n\n @Watch('navStyle')\n @Watch('gradientPreset')\n @Watch('paperTexture')\n @Watch('sectionNavigation')\n onShellPropsChange() {\n this.syncSlottedNavStyle();\n this.scheduleChromeSync();\n }\n\n @Watch('navigation')\n handleNavigationChange(next: ShellNavigationConfig, previous: ShellNavigationConfig) {\n this.managedBrowseContext = this.navigation.browseContext ?? this.navStyle;\n if (next.currentUrl !== previous.currentUrl || next.activeId !== previous.activeId) {\n this.managedMobilePrimaryDestination = 'area';\n }\n }\n\n @Watch('pageChrome')\n handlePageChromeChange() {\n if (this.mobileActionMenuSections.length === 0) {\n this.mobileActionMenuOpen = false;\n this.mobileActionSurfaceOpen = false;\n }\n }\n\n @Watch('tools')\n handleToolsChange() {\n if (!this.resolvedToolItems.some(item => item.id === 'help' && !item.isInactive)) {\n this.managedMobilePrimaryDestination = 'area';\n }\n if (\n this.managedActiveTool &&\n !this.resolvedToolItems.some(item => item.id === this.managedActiveTool)\n ) {\n this.managedToolsOpen = false;\n this.managedActiveTool = '';\n this.managedMobileDestination = 'area';\n }\n this.managedInboxTool = resolveAvailableInboxTool(\n this.managedInboxTool,\n this.availableInboxTools\n );\n }\n\n @Watch('mobileDestination')\n @Watch('mobileSheetNavOpen')\n onMobileStateChange(_next: unknown, previous: unknown) {\n if (this.managed) {\n this.managedMobileDestination = this.mobileDestination;\n this.managedMobileSheetNavOpen = this.mobileSheetNavOpen;\n }\n this.syncSlottedMobileState();\n if (previous === true && !this.effectiveMobileSheetNavOpen && this.resolvedMode === 'mobile') {\n this.connectionTasks.frame(() => {\n const bar = this.el.querySelector('ds-mobile-bar-nav') as\n | (HTMLElement & {\n focusDestination?: (destination: MobileDestination | 'sheet-nav') => Promise<void>;\n })\n | null;\n const activeElement = document.activeElement;\n if (activeElement instanceof HTMLElement && bar?.contains(activeElement)) return;\n void bar?.focusDestination?.('sheet-nav');\n });\n }\n }\n\n private updateResponsiveMode() {\n if (!this.el.isConnected || typeof window === 'undefined') return;\n const next = resolveShellResponsiveMode(window.innerWidth);\n this.wideViewport = window.innerWidth >= SHELL_WIDE_DESKTOP_BREAKPOINT;\n if (next === this.resolvedMode) return;\n this.resolvedMode = next;\n this.syncSlottedMobileState();\n this.scheduleChromeSync();\n if (this.hasLoaded) this.dsResponsiveModeChange.emit({ mode: next });\n }\n\n private onChromeTransitionStart = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source !== 'panel-nav') return;\n\n this.panelNavTransition.enter();\n const viewport = readShellViewportDimensions();\n this.cachedViewportWidth = viewport.width;\n this.cachedViewportHeight = viewport.height;\n this.syncChrome();\n };\n\n private onChromeTransitionEnd = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source !== 'panel-nav') return;\n\n this.panelNavTransition.exit();\n if (!this.panelNavTransition.isActive) {\n this.scheduleChromeSync();\n }\n };\n\n /** Coalesce ResizeObserver bursts to one layout read per frame. */\n private scheduleChromeSync() {\n if (!this.el.isConnected) return;\n this.chromeSyncCoalescer.schedule();\n }\n\n private syncSlottedNavStyle() {\n const panel = this.el.querySelector('ds-panel-nav') as\n | (HTMLElement & { navStyle: NavChromeStyle })\n | null;\n const bar = this.el.querySelector('ds-bar-nav') as\n | (HTMLElement & { navStyle: NavChromeStyle })\n | null;\n if (panel) {\n panel.setAttribute('nav-style', this.navStyle);\n panel.navStyle = this.navStyle;\n }\n if (bar) {\n bar.setAttribute('nav-style', this.navStyle);\n bar.navStyle = this.navStyle;\n }\n }\n\n private syncSlottedMobileState() {\n const tools = this.el.querySelector('ds-shell-tools') as\n | (HTMLElement & { responsiveMode: ShellResponsiveMode })\n | null;\n const bar = this.el.querySelector('ds-mobile-bar-nav') as\n | (HTMLElement & {\n activeDestination: MobileDestination;\n sheetNavExpanded: boolean;\n })\n | null;\n const sheetNav = this.el.querySelector('ds-mobile-sheet-nav') as\n | (HTMLElement & { open: boolean })\n | null;\n\n if (tools) {\n tools.setAttribute('responsive-mode', this.resolvedMode);\n tools.responsiveMode = this.resolvedMode;\n }\n if (bar) {\n bar.activeDestination = this.effectiveMobileDestination;\n bar.sheetNavExpanded = this.effectiveMobileSheetNavOpen;\n }\n if (sheetNav) {\n sheetNav.open = this.effectiveMobileSheetNavOpen;\n }\n }\n\n private connectMetricsObserver() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = new ResizeObserver(() => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n });\n this.resizeObserver.observe(this.el);\n const panelWrap = this.el.querySelector('.shell-app__panel');\n if (panelWrap) this.resizeObserver.observe(panelWrap);\n }\n\n private connectViewportListeners() {\n if (typeof window === 'undefined') return;\n\n window.addEventListener('resize', this.onWindowResize, { passive: true });\n\n const visual = window.visualViewport;\n if (visual) {\n visual.addEventListener('resize', this.onVisualViewportChange, { passive: true });\n visual.addEventListener('scroll', this.onVisualViewportChange, { passive: true });\n }\n }\n\n private disconnectViewportListeners() {\n if (typeof window === 'undefined') return;\n\n window.removeEventListener('resize', this.onWindowResize);\n\n const visual = window.visualViewport;\n if (visual) {\n visual.removeEventListener('resize', this.onVisualViewportChange);\n visual.removeEventListener('scroll', this.onVisualViewportChange);\n }\n }\n\n private connectPageLifecycleListeners() {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n document.addEventListener('visibilitychange', this.onDocumentVisibilityChange);\n window.addEventListener('pageshow', this.onPageShow);\n }\n\n private disconnectPageLifecycleListeners() {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n document.removeEventListener('visibilitychange', this.onDocumentVisibilityChange);\n window.removeEventListener('pageshow', this.onPageShow);\n }\n\n private onDocumentVisibilityChange = () => {\n if (!document.hidden) this.refreshAfterForegroundRestore();\n };\n\n private onPageShow = (event: PageTransitionEvent) => {\n if (event.persisted) this.refreshAfterForegroundRestore();\n };\n\n private cancelForegroundRefresh() {\n if (this.foregroundRefreshTimer !== null) {\n window.clearTimeout(this.foregroundRefreshTimer);\n this.foregroundRefreshTimer = null;\n }\n this.el.classList.remove('shell-app--foreground-refresh');\n }\n\n private refreshAfterForegroundRestore() {\n if (!this.el.isConnected) return;\n this.updateResponsiveMode();\n this.syncSlottedMobileState();\n this.scheduleChromeSync();\n\n if (this.resolvedMode !== 'mobile') return;\n\n /*\n * Mobile WebKit can restore a live, correctly sized DOM without repainting\n * its suspended shell layer. Re-promote the complete mobile stage for one\n * painted frame; routed and tool owners remain mounted and keep their state.\n */\n this.cancelForegroundRefresh();\n this.el.classList.add('shell-app--foreground-refresh');\n void this.el.offsetHeight;\n // A short timer spans a real compositor commit; resumed WebKit may coalesce\n // nested animation frames before presenting either one.\n this.foregroundRefreshTimer = window.setTimeout(() => {\n this.foregroundRefreshTimer = null;\n this.el.classList.remove('shell-app--foreground-refresh');\n // WebKit otherwise may keep presenting the retired composite layer.\n void this.el.querySelector<HTMLElement>('.shell-app__main')?.offsetHeight;\n }, ShellApp.FOREGROUND_REFRESH_MS);\n }\n\n private cachePanelWidthTokens() {\n const navRoot = this.el.querySelector('ds-panel-nav .panel-nav') as HTMLElement | null;\n if (!navRoot) return;\n this.panelWidthTokens = readPanelNavWidthTokens(navRoot);\n }\n\n private applyGradientVars(target: HTMLElement, vars: Record<string, string | null>) {\n const style = target.style;\n for (const [name, value] of Object.entries(vars)) {\n if (value === null) style.removeProperty(name);\n else style.setProperty(name, value);\n }\n }\n\n private clearGradientPaintVars(targets: HTMLElement[]) {\n const keys = [SHELL_GRADIENT_IMAGE_VAR, SHELL_GRADIENT_SIZE_VAR, SHELL_GRADIENT_OPACITY_VAR];\n for (const target of targets) {\n for (const key of keys) target.style.removeProperty(key);\n }\n }\n\n /** Panel/bar wash offsets for scroll fades and badge rings. */\n private syncChromeLayoutVars(\n panel: HTMLElement | null,\n bar: HTMLElement | null,\n panelPosition: string,\n barPosition: string\n ) {\n if (panel) {\n panel.style.setProperty(SHELL_GRADIENT_POSITION_PANEL_VAR, panelPosition);\n }\n if (bar) {\n bar.style.setProperty(SHELL_GRADIENT_POSITION_BAR_VAR, barPosition);\n }\n }\n\n private resolvePanelWidthPx(panelNav: HTMLElement | null): number {\n const navRoot = panelNav?.querySelector('.panel-nav') as HTMLElement | null;\n\n if (this.panelNavTransition.isActive && this.panelWidthTokens.expandedPx > 0) {\n const collapsed = isPanelNavCollapsed(panelNav, navRoot);\n return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);\n }\n\n const panelWrap = this.el.querySelector('.shell-app__panel') as HTMLElement | null;\n const measured = panelWrap?.getBoundingClientRect().width ?? 0;\n if (measured > 0) return measured;\n\n if (this.panelWidthTokens.expandedPx > 0) {\n const collapsed = isPanelNavCollapsed(panelNav, navRoot);\n return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);\n }\n\n return 0;\n }\n\n /** Fixed-attachment wash/grid size — always the viewport, never the shell element box. */\n private resolveViewportDimensions(): { width: number; height: number } {\n if (this.panelNavTransition.isActive && this.cachedViewportWidth > 0) {\n return {\n width: this.cachedViewportWidth,\n height: this.cachedViewportHeight,\n };\n }\n\n return readShellViewportDimensions();\n }\n\n private chromeLayerActive(): boolean {\n return this.gradientLayerActive() || this.paperTextureLayerActive();\n }\n\n private gradientLayerActive(): boolean {\n return (\n this.resolvedMode !== 'mobile' && normalizeShellGradientPreset(this.gradientPreset) !== 'none'\n );\n }\n\n private paperTextureLayerActive(): boolean {\n return this.resolvedMode !== 'mobile' && this.paperTexture != null;\n }\n\n @Listen('keydown', { target: 'window', capture: true })\n handleWindowKeyDown(e: KeyboardEvent) {\n if (!this.shortcutsEnabled) return;\n if (isEditableShortcutTarget(e.target)) return;\n\n const action = resolveShellShortcut(e);\n if (!action) return;\n\n e.preventDefault();\n\n const panel = this.el.querySelector('ds-panel-nav') as HTMLDsPanelNavElement | null;\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & {\n closeDrawer?: () => Promise<void>;\n activateTool?: (id: PanelToolsToolId) => Promise<void>;\n })\n | null;\n\n if (action === 'toggle-panel-nav') {\n void panel?.toggleCollapsed();\n return;\n }\n\n if (action === 'close-panel-tools') {\n void tools?.closeDrawer?.();\n return;\n }\n\n if (action.startsWith('open-tool:') && tools) {\n const toolId = action.slice('open-tool:'.length) as PanelToolsToolId;\n void tools.activateTool?.(toolId);\n }\n }\n\n /** Open or toggle a managed global tool from application-owned UI. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & { activateTool?: (tool: PanelToolsToolId) => Promise<void> })\n | null;\n await tools?.activateTool?.(id);\n }\n\n /** Close whichever managed global surface is currently visible. */\n @Method()\n async closeGlobalSurface() {\n if (this.managedMobileSheetNavOpen) {\n this.managedMobileSheetNavOpen = false;\n return;\n }\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & { closeDrawer?: () => Promise<void> })\n | null;\n await tools?.closeDrawer?.();\n }\n\n /** Request drawer or fullscreen presentation for the active managed tool. */\n @Method()\n async setToolPresentation(presentation: 'drawer' | 'fullscreen') {\n this.managedToolPresentation = presentation;\n this.toolsFullscreen = presentation === 'fullscreen';\n }\n\n private handleManagedNavSelect = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsNavSelect.emit(event.detail);\n };\n\n private handleManagedNavChildSelect = (event: CustomEvent<PanelNavChildSelectDetail>) => {\n event.stopPropagation();\n this.dsNavChildSelect.emit(event.detail);\n };\n\n private handleManagedTabChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsTabChange.emit(event.detail);\n };\n\n private handleManagedPageBack = (event: CustomEvent<MouseEvent>) => {\n event.stopPropagation();\n this.dsPageBack.emit(event.detail);\n };\n\n private handleManagedBreadcrumbSelect = (event: CustomEvent<BreadcrumbSelectDetail>) => {\n event.stopPropagation();\n const forwarded = this.dsBreadcrumbSelect.emit(event.detail);\n if (forwarded.defaultPrevented) event.preventDefault();\n };\n\n private handleManagedSubsectionChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsSubsectionChange.emit(event.detail);\n };\n\n private handleManagedPageAction = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsPageAction.emit(event.detail);\n };\n\n private handleManagedToolChange = (\n event: CustomEvent<{ id: PanelToolsToolId; selected: boolean }>\n ) => {\n event.stopPropagation();\n const { id, selected } = event.detail;\n this.managedActiveTool = id;\n this.managedToolsOpen = selected;\n const item = this.resolvedToolItems.find(candidate => candidate.id === id);\n if (item && itemUsesShellInbox(item)) {\n this.managedInboxTool = id;\n }\n this.managedMobileDestination = selected\n ? (item?.mobileDestination ?? shellMobileDestinationForTool(true, id))\n : 'area';\n this.managedMobileSheetNavOpen = false;\n this.dsToolChange.emit(event.detail);\n };\n\n private handleManagedPresentationChange = (\n event: CustomEvent<{ presentation: 'drawer' | 'fullscreen' }>\n ) => {\n event.stopPropagation();\n this.managedToolPresentation = event.detail.presentation;\n this.toolsFullscreen = event.detail.presentation === 'fullscreen';\n this.dsPresentationChange.emit(event.detail);\n };\n\n private handleManagedHeaderBack = (event: CustomEvent<{ tool: PanelToolsToolId }>) => {\n event.stopPropagation();\n this.dsHeaderBack.emit(event.detail);\n };\n\n private handleManagedHeaderAction = (\n event: CustomEvent<{ tool: PanelToolsToolId; id: string }>\n ) => {\n event.stopPropagation();\n this.dsHeaderAction.emit(event.detail);\n };\n\n private handleManagedRailAccessoryAction = (\n event: CustomEvent<PanelToolsRailAccessoryActionDetail>\n ) => {\n event.stopPropagation();\n this.dsRailAccessoryAction.emit(event.detail);\n };\n\n private handleManagedSheetToggle = (event: CustomEvent<boolean>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = event.detail;\n };\n\n private activateManagedMobileTool(destination: Exclude<MobileDestination, 'area'>): boolean {\n const canonicalId =\n destination === 'inbox'\n ? resolveAvailableInboxTool(this.managedInboxTool, this.availableInboxTools)\n : destination;\n if (!canonicalId) return false;\n\n const item = this.resolvedToolItems.find(\n candidate =>\n candidate.id === canonicalId ||\n (destination !== 'inbox' && candidate.mobileDestination === destination)\n );\n if (!item || item.isInactive) return false;\n\n const id = item.id;\n this.managedActiveTool = id;\n this.managedToolsOpen = true;\n if (itemUsesShellInbox(item)) this.managedInboxTool = id;\n if (destination === 'help') this.managedMobilePrimaryDestination = 'help';\n this.dsToolChange.emit({ id: id as PanelToolsToolId, selected: true });\n return true;\n }\n\n private handleManagedMobileDestination = (event: CustomEvent<MobileBarNavDestinationDetail>) => {\n event.stopPropagation();\n const destination = event.detail.destination;\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = destination;\n\n if (destination === 'area') {\n this.managedToolsOpen = false;\n return;\n }\n\n this.activateManagedMobileTool(destination);\n };\n\n private handleManagedAreaSelect = (event: CustomEvent<string>) => {\n event.stopPropagation();\n if (event.detail === 'help') {\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'help';\n if (this.activateManagedMobileTool('help')) return;\n }\n\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'area';\n this.managedMobilePrimaryDestination = 'area';\n this.managedToolsOpen = false;\n this.dsNavSelect.emit(event.detail);\n };\n\n private handleManagedMobileChildSelect = (event: CustomEvent<PanelNavChildSelectDetail>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'area';\n this.managedMobilePrimaryDestination = 'area';\n this.managedToolsOpen = false;\n this.dsNavChildSelect.emit(event.detail);\n };\n\n private handleManagedBrowseContext = (event: CustomEvent<NavChromeStyle>) => {\n event.stopPropagation();\n this.managedBrowseContext = event.detail;\n this.dsBrowseContextChange.emit(event.detail);\n };\n\n private handleManagedSheetClose = (event: CustomEvent<void>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = false;\n };\n\n private handleManagedNavFooterAction = (event: CustomEvent<void>) => {\n event.stopPropagation();\n this.dsNavFooterAction.emit();\n };\n\n private handleManagedNavUserAction = (event: CustomEvent<PanelNavUserActionDetail>) => {\n event.stopPropagation();\n this.dsNavUserAction.emit(event.detail);\n };\n\n private get currentArea(): PanelNavItem {\n const groups = [\n ...(this.navigation.dashboardGroups ?? []),\n ...(this.navigation.settingsGroups ?? []),\n ...this.resolvedNavigationGroups,\n ];\n const items = groups.flatMap(group => group.items);\n const activeId =\n this.navigation.activeId ||\n deriveActiveIdFromUrl(this.navigation.currentUrl ?? this.pageChrome.currentUrl ?? '', items);\n return (\n items.find(item => item.id === activeId) ??\n items[0] ?? {\n id: 'area',\n icon: 'MapPage',\n label: this.pageChrome.heading || 'Area',\n }\n );\n }\n\n private toolDot(id: PanelToolsToolId): boolean {\n return this.resolvedToolItems.find(item => item.id === id)?.dot ?? false;\n }\n\n private syncChrome() {\n if (!this.el.isConnected) return;\n const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;\n const bar = this.el.querySelector(\n 'ds-bar-nav, ds-bar-page-title, ds-bar-title[placement=\"shell-bar\"]'\n ) as HTMLElement | null;\n const targets = [this.el, panelNav, bar].filter((el): el is HTMLElement => el !== null);\n\n const clearLayoutVars = () => {\n if (panelNav) panelNav.style.removeProperty(SHELL_GRADIENT_POSITION_PANEL_VAR);\n if (bar) bar.style.removeProperty(SHELL_GRADIENT_POSITION_BAR_VAR);\n };\n\n const preset = normalizeShellGradientPreset(this.gradientPreset);\n if (this.resolvedMode === 'mobile') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n return;\n }\n\n const viewport = this.resolveViewportDimensions();\n if (preset === 'none') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n this.syncPaperTextureLayout(viewport);\n return;\n }\n\n const panelWidth = this.resolvePanelWidthPx(panelNav);\n\n const panelPosition = shellGradientPositionPanel();\n const barPosition = shellGradientPositionBar(panelWidth);\n\n this.syncChromeLayoutVars(panelNav, bar, panelPosition, barPosition);\n\n const image = buildShellRadialGradient(preset);\n\n const size = shellGradientSize({\n width: viewport.width,\n height: viewport.height,\n });\n const opacity = shellGradientPresetOpacity(preset);\n\n for (const target of targets) {\n if (target === this.el) {\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: barPosition,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n });\n continue;\n }\n\n const isBar = target === bar;\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: isBar ? null : panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: isBar ? barPosition : null,\n });\n }\n\n this.syncPaperTextureLayout(viewport);\n }\n\n private syncPaperTextureLayout(viewport: { width: number; height: number }) {\n const chrome = this.el.querySelector<HTMLElement>('.shell-app__chrome');\n const paperTexture = this.el.querySelector<HTMLElement>(\n '.shell-app__chrome > ds-paper-texture'\n );\n if (!chrome || !paperTexture) return;\n\n const chromeRect = chrome.getBoundingClientRect();\n paperTexture.style.setProperty('width', `${Math.round(viewport.width)}px`);\n paperTexture.style.setProperty('height', `${Math.round(viewport.height)}px`);\n paperTexture.style.setProperty('left', `${-Math.round(chromeRect.left)}px`);\n paperTexture.style.setProperty('top', `${-Math.round(chromeRect.top)}px`);\n }\n\n private renderPaperTexture() {\n if (!this.paperTextureLayerActive()) return null;\n return <ds-paper-texture class=\"shell-app__paper-texture\" config={this.paperTexture} />;\n }\n\n private renderManagedPanelNav() {\n const navigation = this.navigation;\n return (\n <ds-panel-nav\n navStyle={this.navStyle}\n presentation={this.sectionNavigation === 'panel' ? 'nested' : 'flat'}\n groups={this.resolvedNavigationGroups}\n breakpoint={SHELL_DESKTOP_BREAKPOINT}\n routerMode={navigation.routerMode ?? 'event'}\n activeId={navigation.activeId ?? ''}\n activeChildId={this.pageChrome.value ?? ''}\n currentUrl={navigation.currentUrl ?? this.pageChrome.currentUrl ?? ''}\n storageKey={navigation.storageKey ?? ''}\n userName={navigation.userName ?? ''}\n userInitial={navigation.userInitial ?? ''}\n accountMenuExpanded={navigation.accountMenuExpanded ?? false}\n accountMenuSurfaceOpen={navigation.accountMenuSurfaceOpen}\n dashboardLabel={navigation.dashboardLabel ?? 'Dashboard'}\n settingsLabel={navigation.settingsLabel ?? 'Settings'}\n accountLabel={navigation.accountLabel ?? 'Account'}\n dashboardNavigationLabel={navigation.dashboardNavigationLabel ?? 'Dashboard navigation'}\n settingsNavigationLabel={navigation.settingsNavigationLabel ?? 'Settings navigation'}\n navigationItemsLabel={navigation.navigationLabel ?? 'Navigation items'}\n onDsNavSelect={this.handleManagedNavSelect}\n onDsNavChildSelect={this.handleManagedNavChildSelect}\n onDsNavFooterAction={this.handleManagedNavFooterAction}\n onDsNavUserAction={this.handleManagedNavUserAction}\n />\n );\n }\n\n private renderManagedBarNav() {\n return (\n <ds-bar-nav\n navStyle={this.navStyle}\n tabs={this.resolvedPageTabs}\n value={this.pageChrome.value ?? ''}\n heading={this.pageChrome.routeHeading ?? this.pageChrome.heading}\n basePath={this.pageChrome.basePath ?? ''}\n currentUrl={this.pageChrome.currentUrl ?? this.navigation.currentUrl ?? ''}\n onDsTabChange={this.handleManagedTabChange}\n />\n );\n }\n\n private renderManagedTopBar() {\n return this.sectionNavigation === 'panel'\n ? this.renderManagedBarPageTitle()\n : this.renderManagedBarNav();\n }\n\n private renderManagedToolSlots() {\n const ids = [...new Set(this.resolvedToolItems.map(item => item.id))].sort();\n return ids.flatMap(id => [\n <div key={`tool:${id}`} class=\"shell-app__tool-slot-proxy\" slot={id}>\n <slot name={id} />\n </div>,\n <div key={`tool-view:${id}`} class=\"shell-app__tool-slot-proxy\" slot={`${id}-view`}>\n <slot name={`${id}-view`} />\n </div>,\n ]);\n }\n\n private renderManagedTools() {\n const tools = this.tools;\n return (\n <ds-shell-tools\n responsiveMode={this.resolvedMode}\n open={this.managedToolsOpen}\n activeTool={this.managedActiveTool}\n presentation={this.managedToolPresentation}\n fullscreenHeaderMode={tools.fullscreenHeaderMode ?? 'shared'}\n items={this.resolvedToolItems}\n accessories={tools.accessories ?? []}\n headers={tools.headers ?? {}}\n storageKey={tools.storageKey ?? ''}\n toolsLabel={tools.toolsLabel ?? 'Tools'}\n toolShortcutsLabel={tools.toolShortcutsLabel ?? 'Tool shortcuts'}\n inboxLabel={tools.inboxLabel ?? 'Inbox'}\n inboxNavigationLabel={tools.inboxNavigationLabel ?? 'Inbox sections'}\n onDsToolChange={this.handleManagedToolChange}\n onDsPresentationChange={this.handleManagedPresentationChange}\n onDsHeaderBack={this.handleManagedHeaderBack}\n onDsHeaderAction={this.handleManagedHeaderAction}\n onDsRailAccessoryAction={this.handleManagedRailAccessoryAction}\n >\n {this.renderManagedToolSlots()}\n </ds-shell-tools>\n );\n }\n\n private renderManagedMobileSheetNav() {\n const navigation = this.navigation;\n return (\n <ds-mobile-sheet-nav\n open={this.managedMobileSheetNavOpen}\n presentation={this.sectionNavigation === 'panel' ? 'nested' : 'flat'}\n browseContext={this.managedBrowseContext}\n dashboardGroups={navigation.dashboardGroups ?? navigation.groups ?? []}\n settingsGroups={navigation.settingsGroups ?? []}\n currentUrl={navigation.currentUrl ?? this.pageChrome.currentUrl ?? ''}\n routeSelectionActive={this.managedMobileDestination === 'area'}\n navigationLabel={navigation.navigationLabel ?? 'Application navigation'}\n dashboardLabel={navigation.dashboardLabel ?? 'Dashboard'}\n settingsLabel={navigation.settingsLabel ?? 'Settings'}\n accountLabel={navigation.accountLabel ?? 'Account'}\n showAccount={navigation.showMobileAccount ?? true}\n onDsAreaSelect={this.handleManagedAreaSelect}\n onDsNavChildSelect={this.handleManagedMobileChildSelect}\n onDsBrowseContextChange={this.handleManagedBrowseContext}\n onDsClose={this.handleManagedSheetClose}\n />\n );\n }\n\n private renderManagedMobileBarNav() {\n const item = (id: PanelToolsToolId) =>\n this.resolvedToolItems.find(candidate => candidate.id === id);\n const label = (id: PanelToolsToolId, fallback: string) =>\n item(id)?.ariaLabel ?? item(id)?.label ?? fallback;\n const directActivity = this.resolvedToolItems.some(\n candidate => !candidate.isInactive && candidate.mobileDestination === 'activity'\n );\n\n return (\n <ds-mobile-bar-nav\n hidden={this.tools.mobileBarHidden ?? false}\n activeDestination={this.managedMobileDestination}\n primaryDestination={this.managedMobilePrimaryDestination}\n currentArea={this.currentArea}\n sheetNavExpanded={this.managedMobileSheetNavOpen}\n searchLabel={label('search', 'Search')}\n activityMode={directActivity ? 'direct' : 'inbox'}\n activityLabel={label('activity', 'Activity')}\n inboxLabel={this.tools.inboxLabel ?? 'Inbox'}\n messagesLabel={label('messages', 'Messages')}\n agentsLabel={label('agents', 'Agents')}\n helpLabel={label('help', 'Help & Support')}\n searchDot={this.toolDot('search')}\n activityDot={this.toolDot('activity')}\n inboxDot={this.toolDot('stacks') || this.toolDot('activity')}\n messagesDot={this.toolDot('messages')}\n agentsDot={this.toolDot('agents')}\n onDsSheetNavToggle={this.handleManagedSheetToggle}\n onDsDestinationChange={this.handleManagedMobileDestination}\n />\n );\n }\n\n private renderManagedMobileLeading() {\n if (!this.pageChrome.showBack) {\n return <slot name=\"page-header-leading\" slot=\"leading\" />;\n }\n return [\n <ds-tooltip\n slot=\"leading\"\n label={this.pageChrome.backAriaLabel ?? 'Back'}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n icon={this.pageChrome.backIcon ?? 'ChevronLeft'}\n size=\"lg\"\n aria-label={this.pageChrome.backAriaLabel ?? 'Back'}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.dsPageBack.emit(event.detail)}\n />\n </ds-tooltip>,\n <slot name=\"page-header-leading\" slot=\"leading\" />,\n ];\n }\n\n private renderManagedMobileActions() {\n const legacyActions = this.pageChrome.mobileActions ?? [];\n const visibleActions = visibleBarTitleActions(this.resolvedHeaderActions, 'mobile');\n return [\n ...legacyActions.map((action: PanelToolsHeaderAction) => (\n <ds-tooltip\n slot=\"trailing\"\n key={action.id}\n label={action.ariaLabel}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n id={action.triggerId || undefined}\n variant=\"icon\"\n icon={action.icon}\n size=\"lg\"\n aria-label={action.ariaLabel}\n haspopup={action.haspopup}\n controls={action.controls}\n expanded={action.expanded}\n surfaceOpen={action.surfaceOpen}\n pressed={action.pressed}\n isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n </ds-tooltip>\n )),\n ...visibleActions.map(action => {\n if (action.type !== 'icon') return null;\n const button =\n (action.appearance ?? 'unfilled') === 'filled' ? (\n <ds-button-filled\n variant=\"icon\"\n icon={action.icon}\n aria-label={action.ariaLabel}\n size=\"lg\"\n intent={action.intent ?? 'brand'}\n contrast={action.contrast ?? 'bold'}\n isInactive={action.isInactive}\n isLoading={action.isLoading}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n ) : (\n <ds-button-unfilled\n variant=\"icon\"\n icon={action.icon}\n aria-label={action.ariaLabel}\n size=\"lg\"\n isInactive={action.isInactive}\n isLoading={action.isLoading}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n );\n return (\n <ds-tooltip slot=\"trailing\" key={action.id} label={action.label} side=\"bottom\" size=\"sm\">\n {button}\n </ds-tooltip>\n );\n }),\n ...(this.mobileActionMenuSections.length > 0\n ? [\n <ds-tooltip slot=\"trailing\" label=\"Page options\" side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n ref={el => {\n this.mobileActionTriggerEl = el ?? null;\n }}\n id={this.mobileActionMenuTriggerId}\n variant=\"icon\"\n icon=\"Ellipses\"\n aria-label={this.pageChrome.actionsAriaLabel ?? 'More page actions'}\n size=\"lg\"\n activeFill={false}\n hasBorder={false}\n haspopup=\"menu\"\n controls={this.mobileActionMenuId}\n expanded={this.mobileActionMenuOpen}\n surfaceOpen={this.mobileActionSurfaceOpen}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n this.mobileActionMenuInitialFocusVisible = event.detail.detail === 0;\n this.mobileActionMenuOpen = !this.mobileActionMenuOpen;\n if (this.mobileActionMenuOpen) this.mobileActionSurfaceOpen = true;\n }}\n />\n </ds-tooltip>,\n ]\n : []),\n <slot name=\"page-header-trailing\" slot=\"trailing\" />,\n ];\n }\n\n private get resolvedHeaderActions(): BarTitleActionConfigItem[] {\n return resolveBarTitleActionItems(\n this.pageChrome.actionItems,\n this.pageChrome.primaryAction ?? null,\n this.pageChrome.actions ?? []\n );\n }\n\n private get mobileActionMenuSections() {\n return overflowBarTitleActionSections(this.resolvedHeaderActions, 'mobile');\n }\n\n private closeMobileActionMenu = () => {\n this.mobileActionMenuOpen = false;\n };\n\n private handleMobileActionSelect = (event: CustomEvent<MenuItemData>) => {\n const id = String(event.detail.value ?? '');\n const action = findBarTitleAction(this.resolvedHeaderActions, id);\n if (!action || action.isInactive || ('isLoading' in action && action.isLoading)) return;\n this.mobileActionMenuOpen = false;\n this.dsPageAction.emit(id);\n this.connectionTasks.frame(() => void this.mobileActionTriggerEl?.setFocus?.());\n };\n\n private renderManagedMobileActionMenu() {\n if (this.mobileActionMenuSections.length === 0) return null;\n return (\n <ds-menu\n id={this.mobileActionMenuId}\n anchorId={this.mobileActionMenuTriggerId}\n align=\"end\"\n menuLabel={this.pageChrome.actionsAriaLabel ?? 'More page actions'}\n open={this.mobileActionMenuOpen}\n initialFocusVisible={this.mobileActionMenuInitialFocusVisible}\n sections={this.mobileActionMenuSections}\n onDsClose={this.closeMobileActionMenu}\n onDsAfterClose={() => {\n if (!this.mobileActionMenuOpen) this.mobileActionSurfaceOpen = false;\n }}\n onDsSelect={this.handleMobileActionSelect}\n />\n );\n }\n\n private get barTitleSections(): BarTitleSectionItem[] {\n return (this.pageChrome.subsections ?? []).map(item =>\n 'type' in item\n ? { type: 'divider' }\n : {\n id: item.id,\n label: item.label,\n isInactive: item.isInactive,\n }\n );\n }\n\n private renderManagedBarPageTitle() {\n const page = this.pageChrome;\n return (\n <ds-bar-page-title\n data-shell-bar=\"\"\n ref={el => {\n if (el) {\n (el as HTMLElement & { actionItems?: BarTitleActionConfigItem[] }).actionItems =\n page.actionItems;\n }\n }}\n heading={page.heading ?? ''}\n showBack={page.showBack ?? false}\n backAriaLabel={page.backAriaLabel ?? 'Back'}\n sections={this.barTitleSections}\n value={page.subvalue ?? ''}\n sectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n primaryAction={page.primaryAction ?? null}\n actions={page.actions ?? []}\n actionsAriaLabel={page.actionsAriaLabel ?? 'More page actions'}\n showDivider={page.showHeaderDivider ?? true}\n showCompactDivider={page.showCompactHeaderDivider}\n onDsBack={this.handleManagedPageBack}\n onDsSectionChange={this.handleManagedSubsectionChange}\n onDsAction={this.handleManagedPageAction}\n />\n );\n }\n\n private renderManagedBarTitle(placement: BarTitlePlacement) {\n const page = this.pageChrome;\n return (\n <ds-bar-title\n ref={el => {\n if (el) {\n (el as HTMLElement & { actionItems?: BarTitleActionConfigItem[] }).actionItems =\n page.actionItems;\n }\n }}\n slot={placement === 'page' ? 'header' : undefined}\n placement={placement}\n variant={placement === 'shell-bar' ? 'compact' : 'expanded'}\n heading={page.heading ?? ''}\n description={page.description ?? ''}\n showBack={page.showBack ?? false}\n backAriaLabel={page.backAriaLabel ?? 'Back'}\n backLabel={page.backLabel ?? 'Back'}\n breadcrumbs={page.breadcrumbs ?? []}\n breadcrumbAriaLabel={page.breadcrumbAriaLabel ?? 'Breadcrumb'}\n sections={this.barTitleSections}\n value={page.subvalue ?? ''}\n sectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n primaryAction={page.primaryAction ?? null}\n actions={page.actions ?? []}\n actionsAriaLabel={page.actionsAriaLabel ?? 'More page actions'}\n showDivider={page.showHeaderDivider ?? true}\n showCompactDivider={page.showCompactHeaderDivider}\n onDsBack={this.handleManagedPageBack}\n onDsBreadcrumbSelect={this.handleManagedBreadcrumbSelect}\n onDsSectionChange={this.handleManagedSubsectionChange}\n onDsAction={this.handleManagedPageAction}\n />\n );\n }\n\n private renderManagedPage() {\n const page = this.pageChrome;\n const nestedNavigation = this.sectionNavigation === 'panel';\n const mobileSections = page.showBack || nestedNavigation ? [] : this.resolvedPageTabs;\n return [\n <ds-shell-page\n responsiveMode={this.resolvedMode}\n headerCapacity={resolveManagedShellPageCapacity(this.resolvedMode)}\n headerPresentation={page.headerPresentation ?? 'auto'}\n desktopHeaderPlacement={this.sectionNavigation === 'panel' ? 'shell-bar' : 'page'}\n contentInset={page.contentInset ?? 'default'}\n contentInsetBlockStart={page.contentInsetBlockStart ?? 'default'}\n contentInsetBlockStartSize={page.contentInsetBlockStartSize}\n compactContentInsetBlockStartSize={page.compactContentInsetBlockStartSize}\n scrollCompaction={page.scrollCompaction ?? true}\n contentSurface={page.contentSurface ?? 'primary'}\n >\n {this.sectionNavigation === 'bar' ? this.renderManagedBarTitle('page') : null}\n <ds-mobile-header\n slot=\"mobile-header\"\n heading={page.heading ?? ''}\n headingLevel={page.headingLevel ?? 'h1'}\n sections={mobileSections}\n value={page.showBack ? '' : (page.value ?? '')}\n sectionsAriaLabel={page.sectionsAriaLabel ?? 'Change page section'}\n subsections={page.subsections ?? []}\n subsectionsPlacement={page.showBack || nestedNavigation ? 'combined' : 'below'}\n subvalue={page.subvalue ?? ''}\n subsectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n tone={page.tone ?? 'default'}\n onDsSectionChange={this.handleManagedTabChange}\n onDsSubsectionChange={this.handleManagedSubsectionChange}\n >\n {this.renderManagedMobileLeading()}\n {this.renderManagedMobileActions()}\n </ds-mobile-header>\n <slot />\n </ds-shell-page>,\n this.renderManagedMobileActionMenu(),\n ];\n }\n\n private renderSlottedShell(\n shellCls: Record<string, boolean>,\n mobile: boolean,\n mobileToolActive: boolean,\n mobileStageBlocked: boolean,\n fullscreen: boolean\n ) {\n return (\n <Host\n class={shellCls}\n responsive-mode={this.resolvedMode}\n data-wide-viewport={String(this.wideViewport)}\n >\n <div class=\"shell-app__banner\">\n <slot name=\"banner\" />\n </div>\n <div class=\"shell-app__row\">\n <div class=\"shell-app__chrome\" aria-hidden=\"true\">\n {this.renderPaperTexture()}\n </div>\n <div\n class=\"shell-app__panel\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n <slot name=\"panel\" />\n </div>\n <div class=\"shell-app__main\">\n <div\n class=\"shell-app__bar\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n <slot name=\"bar\" />\n </div>\n <div\n class=\"shell-app__tools\"\n data-ds-overlay-boundary={fullscreen ? undefined : true}\n aria-hidden={\n mobile && (!mobileToolActive || this.mobileSheetNavOpen) ? 'true' : undefined\n }\n inert={mobile && (!mobileToolActive || this.mobileSheetNavOpen) ? true : undefined}\n hidden={mobile && !mobileToolActive}\n >\n <slot name=\"tools\" />\n </div>\n <div\n class=\"shell-app__content\"\n data-ds-overlay-boundary\n aria-hidden={fullscreen || mobileStageBlocked ? 'true' : undefined}\n inert={fullscreen || mobileStageBlocked ? true : undefined}\n tabIndex={0}\n >\n <slot />\n </div>\n <div\n class=\"shell-app__mobile-sheet-nav\"\n hidden={!mobile || !this.mobileSheetNavOpen}\n aria-hidden={mobile && this.mobileSheetNavOpen ? undefined : 'true'}\n inert={mobile && this.mobileSheetNavOpen ? undefined : true}\n >\n <slot name=\"mobile-sheet-nav\" />\n </div>\n <div class=\"shell-app__mobile-bar-nav\" hidden={!mobile}>\n <slot name=\"mobile-bar-nav\" />\n </div>\n </div>\n </div>\n </Host>\n );\n }\n\n render() {\n const chromeActive = this.chromeLayerActive();\n const gradientActive = this.gradientLayerActive();\n const paperTextureActive = this.paperTextureLayerActive();\n const mobile = this.resolvedMode === 'mobile';\n const mobileToolActive = mobile && this.effectiveMobileDestination !== 'area';\n const mobileStageBlocked = mobile && (mobileToolActive || this.effectiveMobileSheetNavOpen);\n const fullscreen = !mobile && this.toolsFullscreen;\n const shellCls: Record<string, boolean> = {\n 'shell-app': true,\n 'shell-app--chrome': chromeActive,\n 'shell-app--gradient': gradientActive,\n 'shell-app--paper-texture': paperTextureActive,\n [`shell-app--${this.navStyle}`]: true,\n [`shell-app--${this.resolvedMode}`]: true,\n 'shell-app--tools-fullscreen': fullscreen,\n 'shell-app--mobile-tool-active': mobileToolActive,\n 'shell-app--mobile-sheet-nav-open': mobile && this.effectiveMobileSheetNavOpen,\n 'shell-app--managed': this.managed,\n 'shell-app--slotted': !this.managed,\n };\n\n if (!this.managed) {\n return this.renderSlottedShell(\n shellCls,\n mobile,\n mobileToolActive,\n mobileStageBlocked,\n fullscreen\n );\n }\n\n return (\n <Host\n class={shellCls}\n responsive-mode={this.resolvedMode}\n data-wide-viewport={String(this.wideViewport)}\n >\n <div class=\"shell-app__banner\">\n <slot name=\"banner\" />\n </div>\n <div class=\"shell-app__row\">\n <div class=\"shell-app__chrome\" aria-hidden=\"true\">\n {this.renderPaperTexture()}\n </div>\n <div\n class=\"shell-app__panel\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n {this.renderManagedPanelNav()}\n </div>\n <div class=\"shell-app__main\">\n <div\n class=\"shell-app__bar\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n {this.renderManagedTopBar()}\n </div>\n <div\n class=\"shell-app__tools\"\n data-ds-overlay-boundary={fullscreen ? undefined : true}\n aria-hidden={\n mobile && (!mobileToolActive || this.effectiveMobileSheetNavOpen)\n ? 'true'\n : undefined\n }\n inert={\n mobile && (!mobileToolActive || this.effectiveMobileSheetNavOpen) ? true : undefined\n }\n hidden={mobile && !mobileToolActive}\n >\n {this.renderManagedTools()}\n </div>\n <div\n class=\"shell-app__content\"\n data-ds-overlay-boundary\n aria-hidden={fullscreen || mobileStageBlocked ? 'true' : undefined}\n inert={fullscreen || mobileStageBlocked ? true : undefined}\n tabIndex={0}\n >\n {this.renderManagedPage()}\n </div>\n <div\n class=\"shell-app__mobile-sheet-nav\"\n hidden={!mobile || !this.effectiveMobileSheetNavOpen}\n aria-hidden={mobile && this.effectiveMobileSheetNavOpen ? undefined : 'true'}\n inert={mobile && this.effectiveMobileSheetNavOpen ? undefined : true}\n >\n {this.renderManagedMobileSheetNav()}\n </div>\n <div class=\"shell-app__mobile-bar-nav\" hidden={!mobile}>\n {this.renderManagedMobileBarNav()}\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
729
|
+
"content": "import {\n Component,\n Prop,\n Element,\n Event,\n EventEmitter,\n State,\n Watch,\n Listen,\n Method,\n h,\n Host,\n} from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport { ConnectionTasks } from '../../utils/connection-tasks';\nimport { isEditableShortcutTarget, resolveShellShortcut } from '../../shell/shell-shortcuts';\nimport type {\n PanelToolsHeaderAction,\n PanelToolsHeaderActionDetail,\n PanelToolsRailAccessoryActionDetail,\n PanelToolsToolId,\n} from '../PanelTools/panel-tools-types';\nimport { PANEL_TOOLS_DEFAULT_ITEMS } from '../PanelTools/panel-tools-types';\nimport type { BarTitleActionConfigItem, BarTitleSectionItem } from '../BarTitle/bar-title-types';\nimport type { BarTitlePlacement } from '../BarTitle/bar-title-types';\nimport {\n findBarTitleAction,\n overflowBarTitleActionSections,\n resolveBarTitleActionItems,\n visibleBarTitleActions,\n} from '../BarTitle/bar-title-actions';\nimport type { BarNavTab } from '../BarNav/bar-nav-types';\nimport type { BreadcrumbSelectDetail } from '../Breadcrumb/breadcrumb-types';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport {\n DEFAULT_SHELL_GRADIENT_PRESET,\n normalizeShellGradientPreset,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\nimport {\n CHROME_TRANSITION_END,\n CHROME_TRANSITION_START,\n ChromeTransitionDepth,\n createRafCoalescer,\n readChromeTransitionSource,\n} from '../../shell/chrome-transition';\nimport {\n isPanelNavCollapsed,\n panelWidthPxFromTokens,\n readPanelNavWidthTokens,\n type PanelNavWidthTokens,\n} from '../../shell/shell-chrome-metrics';\nimport {\n SHELL_GRADIENT_IMAGE_VAR,\n SHELL_GRADIENT_OPACITY_VAR,\n SHELL_GRADIENT_POSITION_BAR_VAR,\n SHELL_GRADIENT_POSITION_PANEL_VAR,\n SHELL_GRADIENT_SIZE_VAR,\n buildShellRadialGradient,\n readShellViewportDimensions,\n shellGradientPositionBar,\n shellGradientPositionPanel,\n shellGradientPresetOpacity,\n shellGradientSize,\n} from '../../shell/shell-gradient';\nimport {\n itemUsesShellInbox,\n resolveAvailableInboxTool,\n resolveManagedShellPageCapacity,\n resolveShellResponsiveMode,\n SHELL_DESKTOP_BREAKPOINT,\n SHELL_WIDE_DESKTOP_BREAKPOINT,\n shellMobileDestinationForTool,\n type MobileDestination,\n type ShellInboxToolId,\n type ShellResponsiveMode,\n} from '../../shell/shell-responsive';\nimport { deriveActiveIdFromUrl } from '../PanelNav/panel-nav-utils';\nimport type { PanelNavChildSelectDetail } from '../PanelNav/panel-nav-types';\nimport type { PanelNavItem } from '../PanelNav/panel-nav-types';\nimport type { PanelNavUserActionDetail } from '../PanelNav/panel-nav-types';\nimport type { MobileBarNavDestinationDetail } from '../MobileBarNav/mobile-bar-nav-types';\nimport type {\n ShellAppComposition,\n ShellNavigationConfig,\n ShellPageChromeConfig,\n ShellSectionNavigation,\n ShellToolsConfig,\n} from './shell-app-types';\nimport type { PaperTextureConfig } from '../PaperTexture/paper-texture-types';\n\nlet nextShellAppId = 0;\ntype FocusableButton = HTMLElement & { setFocus?: () => Promise<void> };\n\n@Component({\n tag: 'ds-shell-app',\n styleUrl: 'ShellApp.css',\n scoped: true,\n})\nexport class ShellApp {\n private static readonly FOREGROUND_REFRESH_MS = 50;\n\n /** Managed renders the complete responsive chrome; slotted exposes the advanced composition. */\n @Prop({ reflect: true }) composition: ShellAppComposition = 'managed';\n\n /** Router-owned navigation data used by managed composition. */\n @Prop() navigation: ShellNavigationConfig = {};\n\n /** Route sections in bar/header pickers, or nested beneath desktop and mobile menu parents. */\n @Prop({ attribute: 'section-navigation', reflect: true })\n sectionNavigation: ShellSectionNavigation = 'bar';\n\n /** Route-owned page title and section data used by managed composition. */\n @Prop({ attribute: 'page-chrome' }) pageChrome: ShellPageChromeConfig = {};\n\n /** Product-owned global tool definitions used by managed composition. */\n @Prop() tools: ShellToolsConfig = {};\n\n /** Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n /**\n * Shell chrome wash preset. `none` renders solid chrome; the remaining\n * presets use token-based washes that adapt to the active color theme.\n */\n @Prop({ attribute: 'gradient-preset', reflect: true }) gradientPreset: ShellGradientPreset =\n DEFAULT_SHELL_GRADIENT_PRESET;\n\n /** Optional decorative Paper Design texture layered above the shell wash. */\n @Prop({ attribute: 'paper-texture' }) paperTexture?: PaperTextureConfig;\n\n /** When `true` (default), registers global shell keyboard shortcuts. `[` toggles panel nav; `]` closes tools; K, A, S, M, N, and / toggle tool drawers. Modifiers are ignored so browser chords like ⌘N stay native. */\n @Prop({ attribute: 'shortcuts-enabled' }) shortcutsEnabled: boolean = true;\n\n /** Controlled mobile surface shown above the persistent bottom bar. */\n @Prop({ attribute: 'mobile-destination' })\n mobileDestination: MobileDestination = 'area';\n\n /** Controlled full-stage Mobile Sheet Nav state. */\n @Prop({ attribute: 'mobile-sheet-nav-open' })\n mobileSheetNavOpen: boolean = false;\n\n /** Emitted after crossing the fixed 768px or 1200px shell boundaries. */\n @Event() dsResponsiveModeChange!: EventEmitter<{ mode: ShellResponsiveMode }>;\n\n /** Managed primary-navigation intent; the application router performs navigation. */\n @Event() dsNavSelect!: EventEmitter<string>;\n\n /** Managed desktop BarNav or mobile section-selection intent. */\n @Event() dsTabChange!: EventEmitter<string>;\n\n /** Managed nested PanelNav child-route intent. */\n @Event() dsNavChildSelect!: EventEmitter<PanelNavChildSelectDetail>;\n\n /** Managed page-subsection intent from BarTitle or MobileHeader. */\n @Event() dsSubsectionChange!: EventEmitter<string>;\n\n /** Managed page-level Back intent. */\n @Event() dsPageBack!: EventEmitter<MouseEvent>;\n\n /** Managed breadcrumb intent from BarTitle. */\n @Event({ cancelable: true })\n dsBreadcrumbSelect!: EventEmitter<BreadcrumbSelectDetail>;\n\n /** Managed page action intent from BarTitle. */\n @Event() dsPageAction!: EventEmitter<string>;\n\n /** Managed tools selection state. */\n @Event() dsToolChange!: EventEmitter<{ id: PanelToolsToolId; selected: boolean }>;\n\n /** Managed tool-header Back intent. */\n @Event() dsHeaderBack!: EventEmitter<{ tool: PanelToolsToolId }>;\n\n /** Managed tool-header action intent. */\n @Event() dsHeaderAction!: EventEmitter<PanelToolsHeaderActionDetail>;\n\n /** Managed desktop/tablet rail accessory intent. */\n @Event() dsRailAccessoryAction!: EventEmitter<PanelToolsRailAccessoryActionDetail>;\n\n /** Managed drawer/fullscreen presentation state. */\n @Event() dsPresentationChange!: EventEmitter<{ presentation: 'drawer' | 'fullscreen' }>;\n\n /** Managed mobile Dashboard/Settings browsing intent. */\n @Event() dsBrowseContextChange!: EventEmitter<NavChromeStyle>;\n\n /** Managed account-footer action intent. */\n @Event() dsNavFooterAction!: EventEmitter<void>;\n\n /** Managed user/account trigger intent. */\n @Event() dsNavUserAction!: EventEmitter<PanelNavUserActionDetail>;\n\n @Element() el!: HTMLElement;\n @State() private toolsFullscreen = false;\n @State() private resolvedMode: ShellResponsiveMode = 'desktop';\n @State() private wideViewport = false;\n @State() private managedToolsOpen = false;\n @State() private managedActiveTool: PanelToolsToolId | '' = '';\n @State() private managedToolPresentation: 'drawer' | 'fullscreen' = 'drawer';\n @State() private managedMobileDestination: MobileDestination = 'area';\n @State() private managedMobilePrimaryDestination: 'area' | 'help' = 'area';\n @State() private managedMobileSheetNavOpen = false;\n @State() private managedInboxTool: ShellInboxToolId | '' = '';\n @State() private managedBrowseContext: NavChromeStyle = 'dashboard';\n @State() private mobileActionMenuOpen = false;\n @State() private mobileActionSurfaceOpen = false;\n @State() private mobileActionMenuInitialFocusVisible = false;\n\n private readonly instanceId = nextShellAppId++;\n private readonly mobileActionMenuTriggerId = `shell-app-mobile-action-trigger-${this.instanceId}`;\n private readonly mobileActionMenuId = `shell-app-mobile-action-menu-${this.instanceId}`;\n private mobileActionTriggerEl: FocusableButton | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private hasLoaded = false;\n private readonly connectionTasks = new ConnectionTasks(() => this.el.isConnected);\n private foregroundRefreshTimer: number | null = null;\n private readonly panelNavTransition = new ChromeTransitionDepth();\n private readonly chromeSyncCoalescer = createRafCoalescer(() => this.syncChrome());\n private panelWidthTokens: PanelNavWidthTokens = { expandedPx: 0, collapsedPx: 0 };\n private cachedViewportWidth = 0;\n private cachedViewportHeight = 0;\n private onWindowResize = () => {\n this.updateResponsiveMode();\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n };\n private onVisualViewportChange = () => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n };\n\n private get managed(): boolean {\n return this.composition === 'managed';\n }\n\n private get effectiveMobileDestination(): MobileDestination {\n return this.managed ? this.managedMobileDestination : this.mobileDestination;\n }\n\n private get effectiveMobileSheetNavOpen(): boolean {\n return this.managed ? this.managedMobileSheetNavOpen : this.mobileSheetNavOpen;\n }\n\n private get resolvedNavigationGroups() {\n if (this.navigation.groups?.length) return this.navigation.groups;\n return this.managedBrowseContext === 'settings'\n ? (this.navigation.settingsGroups ?? [])\n : (this.navigation.dashboardGroups ?? []);\n }\n\n private get resolvedToolItems() {\n return this.tools.items ?? PANEL_TOOLS_DEFAULT_ITEMS;\n }\n\n private get resolvedPageTabs(): BarNavTab[] {\n const children = this.currentArea.children;\n if (!children?.length) return this.pageChrome.tabs ?? [];\n return children.map(child => ({\n id: child.id,\n label: child.label,\n dot: child.dot,\n isInactive: child.isInactive,\n }));\n }\n\n private get availableInboxTools(): ShellInboxToolId[] {\n return this.resolvedToolItems\n .filter(item => !item.isInactive && itemUsesShellInbox(item))\n .map(item => item.id as ShellInboxToolId);\n }\n\n componentWillLoad() {\n if (typeof window !== 'undefined') {\n this.resolvedMode = resolveShellResponsiveMode(window.innerWidth);\n this.wideViewport = window.innerWidth >= SHELL_WIDE_DESKTOP_BREAKPOINT;\n }\n this.managedBrowseContext = this.navigation.browseContext ?? this.navStyle;\n this.managedMobileDestination = this.mobileDestination;\n this.managedMobileSheetNavOpen = this.mobileSheetNavOpen;\n this.managedInboxTool = resolveAvailableInboxTool('', this.availableInboxTools);\n }\n\n connectedCallback() {\n if (this.hasLoaded) this.connectResources();\n }\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.connectResources();\n }\n\n private connectResources() {\n if (!this.el.isConnected) return;\n this.updateResponsiveMode();\n this.syncSlottedNavStyle();\n this.syncSlottedMobileState();\n this.connectMetricsObserver();\n this.connectViewportListeners();\n this.connectPageLifecycleListeners();\n this.el.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n this.el.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n this.connectionTasks.frame(() => {\n this.cachePanelWidthTokens();\n this.scheduleChromeSync();\n });\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools');\n this.toolsFullscreen = tools?.getAttribute('presentation') === 'fullscreen';\n }\n\n @Listen('dsPresentationChange')\n handleToolsPresentation(event: CustomEvent<{ presentation?: string }>) {\n this.toolsFullscreen = event.detail?.presentation === 'fullscreen';\n }\n\n disconnectedCallback() {\n this.connectionTasks.cancel();\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.disconnectViewportListeners();\n this.disconnectPageLifecycleListeners();\n this.cancelForegroundRefresh();\n this.chromeSyncCoalescer.cancel();\n this.panelNavTransition.reset();\n this.el.removeEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n this.el.removeEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n }\n\n @Watch('navStyle')\n @Watch('gradientPreset')\n @Watch('paperTexture')\n @Watch('sectionNavigation')\n onShellPropsChange() {\n this.syncSlottedNavStyle();\n this.scheduleChromeSync();\n }\n\n @Watch('navigation')\n handleNavigationChange(next: ShellNavigationConfig, previous: ShellNavigationConfig) {\n this.managedBrowseContext = this.navigation.browseContext ?? this.navStyle;\n if (next.currentUrl !== previous.currentUrl || next.activeId !== previous.activeId) {\n this.managedMobilePrimaryDestination = 'area';\n }\n }\n\n @Watch('pageChrome')\n handlePageChromeChange() {\n if (this.mobileActionMenuSections.length === 0) {\n this.mobileActionMenuOpen = false;\n this.mobileActionSurfaceOpen = false;\n }\n }\n\n @Watch('tools')\n handleToolsChange() {\n if (!this.resolvedToolItems.some(item => item.id === 'help' && !item.isInactive)) {\n this.managedMobilePrimaryDestination = 'area';\n }\n if (\n this.managedActiveTool &&\n !this.resolvedToolItems.some(item => item.id === this.managedActiveTool)\n ) {\n this.managedToolsOpen = false;\n this.managedActiveTool = '';\n this.managedMobileDestination = 'area';\n }\n this.managedInboxTool = resolveAvailableInboxTool(\n this.managedInboxTool,\n this.availableInboxTools\n );\n }\n\n @Watch('mobileDestination')\n @Watch('mobileSheetNavOpen')\n onMobileStateChange(_next: unknown, previous: unknown) {\n if (this.managed) {\n this.managedMobileDestination = this.mobileDestination;\n this.managedMobileSheetNavOpen = this.mobileSheetNavOpen;\n }\n this.syncSlottedMobileState();\n if (previous === true && !this.effectiveMobileSheetNavOpen && this.resolvedMode === 'mobile') {\n this.connectionTasks.frame(() => {\n const bar = this.el.querySelector('ds-mobile-bar-nav') as\n | (HTMLElement & {\n focusDestination?: (destination: MobileDestination | 'sheet-nav') => Promise<void>;\n })\n | null;\n const activeElement = document.activeElement;\n if (activeElement instanceof HTMLElement && bar?.contains(activeElement)) return;\n void bar?.focusDestination?.('sheet-nav');\n });\n }\n }\n\n private updateResponsiveMode() {\n if (!this.el.isConnected || typeof window === 'undefined') return;\n const next = resolveShellResponsiveMode(window.innerWidth);\n this.wideViewport = window.innerWidth >= SHELL_WIDE_DESKTOP_BREAKPOINT;\n if (next === this.resolvedMode) return;\n this.resolvedMode = next;\n this.syncSlottedMobileState();\n this.scheduleChromeSync();\n if (this.hasLoaded) this.dsResponsiveModeChange.emit({ mode: next });\n }\n\n private onChromeTransitionStart = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source !== 'panel-nav') return;\n\n this.panelNavTransition.enter();\n const viewport = readShellViewportDimensions();\n this.cachedViewportWidth = viewport.width;\n this.cachedViewportHeight = viewport.height;\n this.syncChrome();\n };\n\n private onChromeTransitionEnd = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source !== 'panel-nav') return;\n\n this.panelNavTransition.exit();\n if (!this.panelNavTransition.isActive) {\n this.scheduleChromeSync();\n }\n };\n\n /** Coalesce ResizeObserver bursts to one layout read per frame. */\n private scheduleChromeSync() {\n if (!this.el.isConnected) return;\n this.chromeSyncCoalescer.schedule();\n }\n\n private syncSlottedNavStyle() {\n const panel = this.el.querySelector('ds-panel-nav') as\n | (HTMLElement & { navStyle: NavChromeStyle })\n | null;\n const bar = this.el.querySelector('ds-bar-nav') as\n | (HTMLElement & { navStyle: NavChromeStyle })\n | null;\n if (panel) {\n panel.setAttribute('nav-style', this.navStyle);\n panel.navStyle = this.navStyle;\n }\n if (bar) {\n bar.setAttribute('nav-style', this.navStyle);\n bar.navStyle = this.navStyle;\n }\n }\n\n private syncSlottedMobileState() {\n const tools = this.el.querySelector('ds-shell-tools') as\n | (HTMLElement & { responsiveMode: ShellResponsiveMode })\n | null;\n const bar = this.el.querySelector('ds-mobile-bar-nav') as\n | (HTMLElement & {\n activeDestination: MobileDestination;\n sheetNavExpanded: boolean;\n })\n | null;\n const sheetNav = this.el.querySelector('ds-mobile-sheet-nav') as\n | (HTMLElement & { open: boolean })\n | null;\n\n if (tools) {\n tools.setAttribute('responsive-mode', this.resolvedMode);\n tools.responsiveMode = this.resolvedMode;\n }\n if (bar) {\n bar.activeDestination = this.effectiveMobileDestination;\n bar.sheetNavExpanded = this.effectiveMobileSheetNavOpen;\n }\n if (sheetNav) {\n sheetNav.open = this.effectiveMobileSheetNavOpen;\n }\n }\n\n private connectMetricsObserver() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = new ResizeObserver(() => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n });\n this.resizeObserver.observe(this.el);\n const panelWrap = this.el.querySelector('.shell-app__panel');\n if (panelWrap) this.resizeObserver.observe(panelWrap);\n }\n\n private connectViewportListeners() {\n if (typeof window === 'undefined') return;\n\n window.addEventListener('resize', this.onWindowResize, { passive: true });\n\n const visual = window.visualViewport;\n if (visual) {\n visual.addEventListener('resize', this.onVisualViewportChange, { passive: true });\n visual.addEventListener('scroll', this.onVisualViewportChange, { passive: true });\n }\n }\n\n private disconnectViewportListeners() {\n if (typeof window === 'undefined') return;\n\n window.removeEventListener('resize', this.onWindowResize);\n\n const visual = window.visualViewport;\n if (visual) {\n visual.removeEventListener('resize', this.onVisualViewportChange);\n visual.removeEventListener('scroll', this.onVisualViewportChange);\n }\n }\n\n private connectPageLifecycleListeners() {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n document.addEventListener('visibilitychange', this.onDocumentVisibilityChange);\n window.addEventListener('pageshow', this.onPageShow);\n }\n\n private disconnectPageLifecycleListeners() {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n document.removeEventListener('visibilitychange', this.onDocumentVisibilityChange);\n window.removeEventListener('pageshow', this.onPageShow);\n }\n\n private onDocumentVisibilityChange = () => {\n if (!document.hidden) this.refreshAfterForegroundRestore();\n };\n\n private onPageShow = (event: PageTransitionEvent) => {\n if (event.persisted) this.refreshAfterForegroundRestore();\n };\n\n private cancelForegroundRefresh() {\n if (this.foregroundRefreshTimer !== null) {\n window.clearTimeout(this.foregroundRefreshTimer);\n this.foregroundRefreshTimer = null;\n }\n this.el.classList.remove('shell-app--foreground-refresh');\n }\n\n private refreshAfterForegroundRestore() {\n if (!this.el.isConnected) return;\n this.updateResponsiveMode();\n this.syncSlottedMobileState();\n this.scheduleChromeSync();\n\n if (this.resolvedMode !== 'mobile') return;\n\n /*\n * Mobile WebKit can restore a live, correctly sized DOM without repainting\n * its suspended shell layer. Re-promote the complete mobile stage for one\n * painted frame; routed and tool owners remain mounted and keep their state.\n */\n this.cancelForegroundRefresh();\n this.el.classList.add('shell-app--foreground-refresh');\n void this.el.offsetHeight;\n // A short timer spans a real compositor commit; resumed WebKit may coalesce\n // nested animation frames before presenting either one.\n this.foregroundRefreshTimer = window.setTimeout(() => {\n this.foregroundRefreshTimer = null;\n this.el.classList.remove('shell-app--foreground-refresh');\n // WebKit otherwise may keep presenting the retired composite layer.\n void this.el.querySelector<HTMLElement>('.shell-app__main')?.offsetHeight;\n }, ShellApp.FOREGROUND_REFRESH_MS);\n }\n\n private cachePanelWidthTokens() {\n const navRoot = this.el.querySelector('ds-panel-nav .panel-nav') as HTMLElement | null;\n if (!navRoot) return;\n this.panelWidthTokens = readPanelNavWidthTokens(navRoot);\n }\n\n private applyGradientVars(target: HTMLElement, vars: Record<string, string | null>) {\n const style = target.style;\n for (const [name, value] of Object.entries(vars)) {\n if (value === null) style.removeProperty(name);\n else style.setProperty(name, value);\n }\n }\n\n private clearGradientPaintVars(targets: HTMLElement[]) {\n const keys = [SHELL_GRADIENT_IMAGE_VAR, SHELL_GRADIENT_SIZE_VAR, SHELL_GRADIENT_OPACITY_VAR];\n for (const target of targets) {\n for (const key of keys) target.style.removeProperty(key);\n }\n }\n\n /** Panel/bar wash offsets for scroll fades and badge rings. */\n private syncChromeLayoutVars(\n panel: HTMLElement | null,\n bar: HTMLElement | null,\n panelPosition: string,\n barPosition: string\n ) {\n if (panel) {\n panel.style.setProperty(SHELL_GRADIENT_POSITION_PANEL_VAR, panelPosition);\n }\n if (bar) {\n bar.style.setProperty(SHELL_GRADIENT_POSITION_BAR_VAR, barPosition);\n }\n }\n\n private resolvePanelWidthPx(panelNav: HTMLElement | null): number {\n const navRoot = panelNav?.querySelector('.panel-nav') as HTMLElement | null;\n\n if (this.panelNavTransition.isActive && this.panelWidthTokens.expandedPx > 0) {\n const collapsed = isPanelNavCollapsed(panelNav, navRoot);\n return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);\n }\n\n const panelWrap = this.el.querySelector('.shell-app__panel') as HTMLElement | null;\n const measured = panelWrap?.getBoundingClientRect().width ?? 0;\n if (measured > 0) return measured;\n\n if (this.panelWidthTokens.expandedPx > 0) {\n const collapsed = isPanelNavCollapsed(panelNav, navRoot);\n return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);\n }\n\n return 0;\n }\n\n /** Fixed-attachment wash/grid size — always the viewport, never the shell element box. */\n private resolveViewportDimensions(): { width: number; height: number } {\n if (this.panelNavTransition.isActive && this.cachedViewportWidth > 0) {\n return {\n width: this.cachedViewportWidth,\n height: this.cachedViewportHeight,\n };\n }\n\n return readShellViewportDimensions();\n }\n\n private chromeLayerActive(): boolean {\n return this.gradientLayerActive() || this.paperTextureLayerActive();\n }\n\n private gradientLayerActive(): boolean {\n return (\n this.resolvedMode !== 'mobile' && normalizeShellGradientPreset(this.gradientPreset) !== 'none'\n );\n }\n\n private paperTextureLayerActive(): boolean {\n return this.resolvedMode !== 'mobile' && this.paperTexture != null;\n }\n\n @Listen('keydown', { target: 'window', capture: true })\n handleWindowKeyDown(e: KeyboardEvent) {\n if (!this.shortcutsEnabled) return;\n if (isEditableShortcutTarget(e.target)) return;\n\n const action = resolveShellShortcut(e);\n if (!action) return;\n\n e.preventDefault();\n\n const panel = this.el.querySelector('ds-panel-nav') as HTMLDsPanelNavElement | null;\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & {\n closeDrawer?: () => Promise<void>;\n activateTool?: (id: PanelToolsToolId) => Promise<void>;\n })\n | null;\n\n if (action === 'toggle-panel-nav') {\n void panel?.toggleCollapsed();\n return;\n }\n\n if (action === 'close-panel-tools') {\n void tools?.closeDrawer?.();\n return;\n }\n\n if (action.startsWith('open-tool:') && tools) {\n const toolId = action.slice('open-tool:'.length) as PanelToolsToolId;\n void tools.activateTool?.(toolId);\n }\n }\n\n /** Open or toggle a managed global tool from application-owned UI. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & { activateTool?: (tool: PanelToolsToolId) => Promise<void> })\n | null;\n await tools?.activateTool?.(id);\n }\n\n /** Close whichever managed global surface is currently visible. */\n @Method()\n async closeGlobalSurface() {\n if (this.managedMobileSheetNavOpen) {\n this.managedMobileSheetNavOpen = false;\n return;\n }\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & { closeDrawer?: () => Promise<void> })\n | null;\n await tools?.closeDrawer?.();\n }\n\n /** Request drawer or fullscreen presentation for the active managed tool. */\n @Method()\n async setToolPresentation(presentation: 'drawer' | 'fullscreen') {\n this.managedToolPresentation = presentation;\n this.toolsFullscreen = presentation === 'fullscreen';\n }\n\n private handleManagedNavSelect = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsNavSelect.emit(event.detail);\n };\n\n private handleManagedNavChildSelect = (event: CustomEvent<PanelNavChildSelectDetail>) => {\n event.stopPropagation();\n this.dsNavChildSelect.emit(event.detail);\n };\n\n private handleManagedTabChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsTabChange.emit(event.detail);\n };\n\n private handleManagedPageBack = (event: CustomEvent<MouseEvent>) => {\n event.stopPropagation();\n this.dsPageBack.emit(event.detail);\n };\n\n private handleManagedBreadcrumbSelect = (event: CustomEvent<BreadcrumbSelectDetail>) => {\n event.stopPropagation();\n const forwarded = this.dsBreadcrumbSelect.emit(event.detail);\n if (forwarded.defaultPrevented) event.preventDefault();\n };\n\n private handleManagedSubsectionChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsSubsectionChange.emit(event.detail);\n };\n\n private handleManagedPageAction = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsPageAction.emit(event.detail);\n };\n\n private handleManagedToolChange = (\n event: CustomEvent<{ id: PanelToolsToolId; selected: boolean }>\n ) => {\n event.stopPropagation();\n const { id, selected } = event.detail;\n this.managedActiveTool = id;\n this.managedToolsOpen = selected;\n const item = this.resolvedToolItems.find(candidate => candidate.id === id);\n if (item && itemUsesShellInbox(item)) {\n this.managedInboxTool = id;\n }\n this.managedMobileDestination = selected\n ? (item?.mobileDestination ?? shellMobileDestinationForTool(true, id))\n : 'area';\n this.managedMobileSheetNavOpen = false;\n this.dsToolChange.emit(event.detail);\n };\n\n private handleManagedPresentationChange = (\n event: CustomEvent<{ presentation: 'drawer' | 'fullscreen' }>\n ) => {\n event.stopPropagation();\n this.managedToolPresentation = event.detail.presentation;\n this.toolsFullscreen = event.detail.presentation === 'fullscreen';\n this.dsPresentationChange.emit(event.detail);\n };\n\n private handleManagedHeaderBack = (event: CustomEvent<{ tool: PanelToolsToolId }>) => {\n event.stopPropagation();\n this.dsHeaderBack.emit(event.detail);\n };\n\n private handleManagedHeaderAction = (event: CustomEvent<PanelToolsHeaderActionDetail>) => {\n event.stopPropagation();\n this.dsHeaderAction.emit(event.detail);\n };\n\n private handleManagedRailAccessoryAction = (\n event: CustomEvent<PanelToolsRailAccessoryActionDetail>\n ) => {\n event.stopPropagation();\n this.dsRailAccessoryAction.emit(event.detail);\n };\n\n private handleManagedSheetToggle = (event: CustomEvent<boolean>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = event.detail;\n };\n\n private activateManagedMobileTool(destination: Exclude<MobileDestination, 'area'>): boolean {\n const canonicalId =\n destination === 'inbox'\n ? resolveAvailableInboxTool(this.managedInboxTool, this.availableInboxTools)\n : destination;\n if (!canonicalId) return false;\n\n const item = this.resolvedToolItems.find(\n candidate =>\n candidate.id === canonicalId ||\n (destination !== 'inbox' && candidate.mobileDestination === destination)\n );\n if (!item || item.isInactive) return false;\n\n const id = item.id;\n this.managedActiveTool = id;\n this.managedToolsOpen = true;\n if (itemUsesShellInbox(item)) this.managedInboxTool = id;\n if (destination === 'help') this.managedMobilePrimaryDestination = 'help';\n this.dsToolChange.emit({ id: id as PanelToolsToolId, selected: true });\n return true;\n }\n\n private handleManagedMobileDestination = (event: CustomEvent<MobileBarNavDestinationDetail>) => {\n event.stopPropagation();\n const destination = event.detail.destination;\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = destination;\n\n if (destination === 'area') {\n this.managedToolsOpen = false;\n return;\n }\n\n this.activateManagedMobileTool(destination);\n };\n\n private handleManagedAreaSelect = (event: CustomEvent<string>) => {\n event.stopPropagation();\n if (event.detail === 'help') {\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'help';\n if (this.activateManagedMobileTool('help')) return;\n }\n\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'area';\n this.managedMobilePrimaryDestination = 'area';\n this.managedToolsOpen = false;\n this.dsNavSelect.emit(event.detail);\n };\n\n private handleManagedMobileChildSelect = (event: CustomEvent<PanelNavChildSelectDetail>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'area';\n this.managedMobilePrimaryDestination = 'area';\n this.managedToolsOpen = false;\n this.dsNavChildSelect.emit(event.detail);\n };\n\n private handleManagedBrowseContext = (event: CustomEvent<NavChromeStyle>) => {\n event.stopPropagation();\n this.managedBrowseContext = event.detail;\n this.dsBrowseContextChange.emit(event.detail);\n };\n\n private handleManagedSheetClose = (event: CustomEvent<void>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = false;\n };\n\n private handleManagedNavFooterAction = (event: CustomEvent<void>) => {\n event.stopPropagation();\n this.dsNavFooterAction.emit();\n };\n\n private handleManagedNavUserAction = (event: CustomEvent<PanelNavUserActionDetail>) => {\n event.stopPropagation();\n this.dsNavUserAction.emit(event.detail);\n };\n\n private get currentArea(): PanelNavItem {\n const groups = [\n ...(this.navigation.dashboardGroups ?? []),\n ...(this.navigation.settingsGroups ?? []),\n ...this.resolvedNavigationGroups,\n ];\n const items = groups.flatMap(group => group.items);\n const activeId =\n this.navigation.activeId ||\n deriveActiveIdFromUrl(this.navigation.currentUrl ?? this.pageChrome.currentUrl ?? '', items);\n return (\n items.find(item => item.id === activeId) ??\n items[0] ?? {\n id: 'area',\n icon: 'MapPage',\n label: this.pageChrome.heading || 'Area',\n }\n );\n }\n\n private toolDot(id: PanelToolsToolId): boolean {\n return this.resolvedToolItems.find(item => item.id === id)?.dot ?? false;\n }\n\n private syncChrome() {\n if (!this.el.isConnected) return;\n const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;\n const bar = this.el.querySelector(\n 'ds-bar-nav, ds-bar-page-title, ds-bar-title[placement=\"shell-bar\"]'\n ) as HTMLElement | null;\n const targets = [this.el, panelNav, bar].filter((el): el is HTMLElement => el !== null);\n\n const clearLayoutVars = () => {\n if (panelNav) panelNav.style.removeProperty(SHELL_GRADIENT_POSITION_PANEL_VAR);\n if (bar) bar.style.removeProperty(SHELL_GRADIENT_POSITION_BAR_VAR);\n };\n\n const preset = normalizeShellGradientPreset(this.gradientPreset);\n if (this.resolvedMode === 'mobile') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n return;\n }\n\n const viewport = this.resolveViewportDimensions();\n if (preset === 'none') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n this.syncPaperTextureLayout(viewport);\n return;\n }\n\n const panelWidth = this.resolvePanelWidthPx(panelNav);\n\n const panelPosition = shellGradientPositionPanel();\n const barPosition = shellGradientPositionBar(panelWidth);\n\n this.syncChromeLayoutVars(panelNav, bar, panelPosition, barPosition);\n\n const image = buildShellRadialGradient(preset);\n\n const size = shellGradientSize({\n width: viewport.width,\n height: viewport.height,\n });\n const opacity = shellGradientPresetOpacity(preset);\n\n for (const target of targets) {\n if (target === this.el) {\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: barPosition,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n });\n continue;\n }\n\n const isBar = target === bar;\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: isBar ? null : panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: isBar ? barPosition : null,\n });\n }\n\n this.syncPaperTextureLayout(viewport);\n }\n\n private syncPaperTextureLayout(viewport: { width: number; height: number }) {\n const chrome = this.el.querySelector<HTMLElement>('.shell-app__chrome');\n const paperTexture = this.el.querySelector<HTMLElement>(\n '.shell-app__chrome > ds-paper-texture'\n );\n if (!chrome || !paperTexture) return;\n\n const chromeRect = chrome.getBoundingClientRect();\n paperTexture.style.setProperty('width', `${Math.round(viewport.width)}px`);\n paperTexture.style.setProperty('height', `${Math.round(viewport.height)}px`);\n paperTexture.style.setProperty('left', `${-Math.round(chromeRect.left)}px`);\n paperTexture.style.setProperty('top', `${-Math.round(chromeRect.top)}px`);\n }\n\n private renderPaperTexture() {\n if (!this.paperTextureLayerActive()) return null;\n return <ds-paper-texture class=\"shell-app__paper-texture\" config={this.paperTexture} />;\n }\n\n private renderManagedPanelNav() {\n const navigation = this.navigation;\n return (\n <ds-panel-nav\n navStyle={this.navStyle}\n presentation={this.sectionNavigation === 'panel' ? 'nested' : 'flat'}\n groups={this.resolvedNavigationGroups}\n breakpoint={SHELL_DESKTOP_BREAKPOINT}\n routerMode={navigation.routerMode ?? 'event'}\n activeId={navigation.activeId ?? ''}\n activeChildId={this.pageChrome.value ?? ''}\n currentUrl={navigation.currentUrl ?? this.pageChrome.currentUrl ?? ''}\n storageKey={navigation.storageKey ?? ''}\n userName={navigation.userName ?? ''}\n userInitial={navigation.userInitial ?? ''}\n accountMenuExpanded={navigation.accountMenuExpanded ?? false}\n accountMenuSurfaceOpen={navigation.accountMenuSurfaceOpen}\n dashboardLabel={navigation.dashboardLabel ?? 'Dashboard'}\n settingsLabel={navigation.settingsLabel ?? 'Settings'}\n accountLabel={navigation.accountLabel ?? 'Account'}\n dashboardNavigationLabel={navigation.dashboardNavigationLabel ?? 'Dashboard navigation'}\n settingsNavigationLabel={navigation.settingsNavigationLabel ?? 'Settings navigation'}\n navigationItemsLabel={navigation.navigationLabel ?? 'Navigation items'}\n onDsNavSelect={this.handleManagedNavSelect}\n onDsNavChildSelect={this.handleManagedNavChildSelect}\n onDsNavFooterAction={this.handleManagedNavFooterAction}\n onDsNavUserAction={this.handleManagedNavUserAction}\n />\n );\n }\n\n private renderManagedBarNav() {\n return (\n <ds-bar-nav\n navStyle={this.navStyle}\n tabs={this.resolvedPageTabs}\n value={this.pageChrome.value ?? ''}\n heading={this.pageChrome.routeHeading ?? this.pageChrome.heading}\n basePath={this.pageChrome.basePath ?? ''}\n currentUrl={this.pageChrome.currentUrl ?? this.navigation.currentUrl ?? ''}\n onDsTabChange={this.handleManagedTabChange}\n />\n );\n }\n\n private renderManagedTopBar() {\n return this.sectionNavigation === 'panel'\n ? this.renderManagedBarPageTitle()\n : this.renderManagedBarNav();\n }\n\n private renderManagedToolSlots() {\n const ids = [...new Set(this.resolvedToolItems.map(item => item.id))].sort();\n return ids.flatMap(id => [\n <div key={`tool:${id}`} class=\"shell-app__tool-slot-proxy\" slot={id}>\n <slot name={id} />\n </div>,\n <div key={`tool-view:${id}`} class=\"shell-app__tool-slot-proxy\" slot={`${id}-view`}>\n <slot name={`${id}-view`} />\n </div>,\n ]);\n }\n\n private renderManagedTools() {\n const tools = this.tools;\n return (\n <ds-shell-tools\n responsiveMode={this.resolvedMode}\n open={this.managedToolsOpen}\n activeTool={this.managedActiveTool}\n presentation={this.managedToolPresentation}\n fullscreenHeaderMode={tools.fullscreenHeaderMode ?? 'shared'}\n items={this.resolvedToolItems}\n accessories={tools.accessories ?? []}\n headers={tools.headers ?? {}}\n storageKey={tools.storageKey ?? ''}\n toolsLabel={tools.toolsLabel ?? 'Tools'}\n toolShortcutsLabel={tools.toolShortcutsLabel ?? 'Tool shortcuts'}\n inboxLabel={tools.inboxLabel ?? 'Inbox'}\n inboxNavigationLabel={tools.inboxNavigationLabel ?? 'Inbox sections'}\n onDsToolChange={this.handleManagedToolChange}\n onDsPresentationChange={this.handleManagedPresentationChange}\n onDsHeaderBack={this.handleManagedHeaderBack}\n onDsHeaderAction={this.handleManagedHeaderAction}\n onDsRailAccessoryAction={this.handleManagedRailAccessoryAction}\n >\n {this.renderManagedToolSlots()}\n </ds-shell-tools>\n );\n }\n\n private renderManagedMobileSheetNav() {\n const navigation = this.navigation;\n return (\n <ds-mobile-sheet-nav\n open={this.managedMobileSheetNavOpen}\n presentation={this.sectionNavigation === 'panel' ? 'nested' : 'flat'}\n browseContext={this.managedBrowseContext}\n dashboardGroups={navigation.dashboardGroups ?? navigation.groups ?? []}\n settingsGroups={navigation.settingsGroups ?? []}\n currentUrl={navigation.currentUrl ?? this.pageChrome.currentUrl ?? ''}\n routeSelectionActive={this.managedMobileDestination === 'area'}\n navigationLabel={navigation.navigationLabel ?? 'Application navigation'}\n dashboardLabel={navigation.dashboardLabel ?? 'Dashboard'}\n settingsLabel={navigation.settingsLabel ?? 'Settings'}\n accountLabel={navigation.accountLabel ?? 'Account'}\n showAccount={navigation.showMobileAccount ?? true}\n onDsAreaSelect={this.handleManagedAreaSelect}\n onDsNavChildSelect={this.handleManagedMobileChildSelect}\n onDsBrowseContextChange={this.handleManagedBrowseContext}\n onDsClose={this.handleManagedSheetClose}\n />\n );\n }\n\n private renderManagedMobileBarNav() {\n const item = (id: PanelToolsToolId) =>\n this.resolvedToolItems.find(candidate => candidate.id === id);\n const label = (id: PanelToolsToolId, fallback: string) =>\n item(id)?.ariaLabel ?? item(id)?.label ?? fallback;\n const directActivity = this.resolvedToolItems.some(\n candidate => !candidate.isInactive && candidate.mobileDestination === 'activity'\n );\n\n return (\n <ds-mobile-bar-nav\n hidden={this.tools.mobileBarHidden ?? false}\n activeDestination={this.managedMobileDestination}\n primaryDestination={this.managedMobilePrimaryDestination}\n currentArea={this.currentArea}\n sheetNavExpanded={this.managedMobileSheetNavOpen}\n searchLabel={label('search', 'Search')}\n activityMode={directActivity ? 'direct' : 'inbox'}\n activityLabel={label('activity', 'Activity')}\n inboxLabel={this.tools.inboxLabel ?? 'Inbox'}\n messagesLabel={label('messages', 'Messages')}\n agentsLabel={label('agents', 'Agents')}\n helpLabel={label('help', 'Help & Support')}\n searchDot={this.toolDot('search')}\n activityDot={this.toolDot('activity')}\n inboxDot={this.toolDot('stacks') || this.toolDot('activity')}\n messagesDot={this.toolDot('messages')}\n agentsDot={this.toolDot('agents')}\n onDsSheetNavToggle={this.handleManagedSheetToggle}\n onDsDestinationChange={this.handleManagedMobileDestination}\n />\n );\n }\n\n private renderManagedMobileLeading() {\n if (!this.pageChrome.showBack) {\n return <slot name=\"page-header-leading\" slot=\"leading\" />;\n }\n return [\n <ds-tooltip\n slot=\"leading\"\n label={this.pageChrome.backAriaLabel ?? 'Back'}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n icon={this.pageChrome.backIcon ?? 'ChevronLeft'}\n size=\"md\"\n aria-label={this.pageChrome.backAriaLabel ?? 'Back'}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.dsPageBack.emit(event.detail)}\n />\n </ds-tooltip>,\n <slot name=\"page-header-leading\" slot=\"leading\" />,\n ];\n }\n\n private renderManagedMobileActions() {\n const legacyActions = this.pageChrome.mobileActions ?? [];\n const visibleActions = visibleBarTitleActions(this.resolvedHeaderActions, 'mobile');\n return [\n ...legacyActions.map((action: PanelToolsHeaderAction) => (\n <ds-tooltip\n slot=\"trailing\"\n key={action.id}\n label={action.ariaLabel}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n id={action.triggerId || undefined}\n variant=\"icon\"\n icon={action.icon}\n size=\"md\"\n aria-label={action.ariaLabel}\n haspopup={action.haspopup}\n controls={action.controls}\n expanded={action.expanded}\n surfaceOpen={action.surfaceOpen}\n pressed={action.pressed}\n isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n </ds-tooltip>\n )),\n ...visibleActions.map(action => {\n if (action.type !== 'icon') return null;\n const button =\n (action.appearance ?? 'unfilled') === 'filled' ? (\n <ds-button-filled\n variant=\"icon\"\n icon={action.icon}\n aria-label={action.ariaLabel}\n size=\"md\"\n intent={action.intent ?? 'brand'}\n contrast={action.contrast ?? 'bold'}\n isInactive={action.isInactive}\n isLoading={action.isLoading}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n ) : (\n <ds-button-unfilled\n variant=\"icon\"\n icon={action.icon}\n aria-label={action.ariaLabel}\n size=\"md\"\n isInactive={action.isInactive}\n isLoading={action.isLoading}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n );\n return (\n <ds-tooltip slot=\"trailing\" key={action.id} label={action.label} side=\"bottom\" size=\"sm\">\n {button}\n </ds-tooltip>\n );\n }),\n ...(this.mobileActionMenuSections.length > 0\n ? [\n <ds-tooltip slot=\"trailing\" label=\"Page options\" side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n ref={el => {\n this.mobileActionTriggerEl = el ?? null;\n }}\n id={this.mobileActionMenuTriggerId}\n variant=\"icon\"\n icon=\"Ellipses\"\n aria-label={this.pageChrome.actionsAriaLabel ?? 'More page actions'}\n size=\"md\"\n activeFill={false}\n hasBorder={false}\n haspopup=\"menu\"\n controls={this.mobileActionMenuId}\n expanded={this.mobileActionMenuOpen}\n surfaceOpen={this.mobileActionSurfaceOpen}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n this.mobileActionMenuInitialFocusVisible = event.detail.detail === 0;\n this.mobileActionMenuOpen = !this.mobileActionMenuOpen;\n if (this.mobileActionMenuOpen) this.mobileActionSurfaceOpen = true;\n }}\n />\n </ds-tooltip>,\n ]\n : []),\n <slot name=\"page-header-trailing\" slot=\"trailing\" />,\n ];\n }\n\n private get resolvedHeaderActions(): BarTitleActionConfigItem[] {\n return resolveBarTitleActionItems(\n this.pageChrome.actionItems,\n this.pageChrome.primaryAction ?? null,\n this.pageChrome.actions ?? []\n );\n }\n\n private get mobileActionMenuSections() {\n return overflowBarTitleActionSections(this.resolvedHeaderActions, 'mobile');\n }\n\n private closeMobileActionMenu = () => {\n this.mobileActionMenuOpen = false;\n };\n\n private handleMobileActionSelect = (event: CustomEvent<MenuItemData>) => {\n const id = String(event.detail.value ?? '');\n const action = findBarTitleAction(this.resolvedHeaderActions, id);\n if (!action || action.isInactive || ('isLoading' in action && action.isLoading)) return;\n this.mobileActionMenuOpen = false;\n this.dsPageAction.emit(id);\n this.connectionTasks.frame(() => void this.mobileActionTriggerEl?.setFocus?.());\n };\n\n private renderManagedMobileActionMenu() {\n if (this.mobileActionMenuSections.length === 0) return null;\n return (\n <ds-menu\n id={this.mobileActionMenuId}\n anchorId={this.mobileActionMenuTriggerId}\n align=\"end\"\n menuLabel={this.pageChrome.actionsAriaLabel ?? 'More page actions'}\n open={this.mobileActionMenuOpen}\n initialFocusVisible={this.mobileActionMenuInitialFocusVisible}\n sections={this.mobileActionMenuSections}\n onDsClose={this.closeMobileActionMenu}\n onDsAfterClose={() => {\n if (!this.mobileActionMenuOpen) this.mobileActionSurfaceOpen = false;\n }}\n onDsSelect={this.handleMobileActionSelect}\n />\n );\n }\n\n private get barTitleSections(): BarTitleSectionItem[] {\n return (this.pageChrome.subsections ?? []).map(item =>\n 'type' in item\n ? { type: 'divider' }\n : {\n id: item.id,\n label: item.label,\n isInactive: item.isInactive,\n }\n );\n }\n\n private renderManagedBarPageTitle() {\n const page = this.pageChrome;\n return (\n <ds-bar-page-title\n data-shell-bar=\"\"\n ref={el => {\n if (el) {\n (el as HTMLElement & { actionItems?: BarTitleActionConfigItem[] }).actionItems =\n page.actionItems;\n }\n }}\n heading={page.heading ?? ''}\n showBack={page.showBack ?? false}\n backAriaLabel={page.backAriaLabel ?? 'Back'}\n sections={this.barTitleSections}\n value={page.subvalue ?? ''}\n sectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n primaryAction={page.primaryAction ?? null}\n actions={page.actions ?? []}\n actionsAriaLabel={page.actionsAriaLabel ?? 'More page actions'}\n showDivider={page.showHeaderDivider ?? true}\n showCompactDivider={page.showCompactHeaderDivider}\n onDsBack={this.handleManagedPageBack}\n onDsSectionChange={this.handleManagedSubsectionChange}\n onDsAction={this.handleManagedPageAction}\n />\n );\n }\n\n private renderManagedBarTitle(placement: BarTitlePlacement) {\n const page = this.pageChrome;\n return (\n <ds-bar-title\n ref={el => {\n if (el) {\n (el as HTMLElement & { actionItems?: BarTitleActionConfigItem[] }).actionItems =\n page.actionItems;\n }\n }}\n slot={placement === 'page' ? 'header' : undefined}\n placement={placement}\n variant={placement === 'shell-bar' ? 'compact' : 'expanded'}\n heading={page.heading ?? ''}\n description={page.description ?? ''}\n showBack={page.showBack ?? false}\n backAriaLabel={page.backAriaLabel ?? 'Back'}\n backLabel={page.backLabel ?? 'Back'}\n breadcrumbs={page.breadcrumbs ?? []}\n breadcrumbAriaLabel={page.breadcrumbAriaLabel ?? 'Breadcrumb'}\n sections={this.barTitleSections}\n value={page.subvalue ?? ''}\n sectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n primaryAction={page.primaryAction ?? null}\n actions={page.actions ?? []}\n actionsAriaLabel={page.actionsAriaLabel ?? 'More page actions'}\n showDivider={page.showHeaderDivider ?? true}\n showCompactDivider={page.showCompactHeaderDivider}\n onDsBack={this.handleManagedPageBack}\n onDsBreadcrumbSelect={this.handleManagedBreadcrumbSelect}\n onDsSectionChange={this.handleManagedSubsectionChange}\n onDsAction={this.handleManagedPageAction}\n />\n );\n }\n\n private renderManagedPage() {\n const page = this.pageChrome;\n const nestedNavigation = this.sectionNavigation === 'panel';\n const mobileSections = page.showBack || nestedNavigation ? [] : this.resolvedPageTabs;\n return [\n <ds-shell-page\n responsiveMode={this.resolvedMode}\n headerCapacity={resolveManagedShellPageCapacity(this.resolvedMode)}\n headerPresentation={page.headerPresentation ?? 'auto'}\n desktopHeaderPlacement={this.sectionNavigation === 'panel' ? 'shell-bar' : 'page'}\n contentInset={page.contentInset ?? 'default'}\n contentInsetBlockStart={page.contentInsetBlockStart ?? 'default'}\n contentInsetBlockStartSize={page.contentInsetBlockStartSize}\n compactContentInsetBlockStartSize={page.compactContentInsetBlockStartSize}\n scrollCompaction={page.scrollCompaction ?? true}\n contentSurface={page.contentSurface ?? 'primary'}\n >\n {this.sectionNavigation === 'bar' ? this.renderManagedBarTitle('page') : null}\n <ds-mobile-header\n slot=\"mobile-header\"\n heading={page.heading ?? ''}\n headingLevel={page.headingLevel ?? 'h1'}\n sections={mobileSections}\n value={page.showBack ? '' : (page.value ?? '')}\n sectionsAriaLabel={page.sectionsAriaLabel ?? 'Change page section'}\n subsections={page.subsections ?? []}\n subsectionsPlacement={page.showBack || nestedNavigation ? 'combined' : 'below'}\n subvalue={page.subvalue ?? ''}\n subsectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n tone={page.tone ?? 'default'}\n onDsSectionChange={this.handleManagedTabChange}\n onDsSubsectionChange={this.handleManagedSubsectionChange}\n >\n {this.renderManagedMobileLeading()}\n {this.renderManagedMobileActions()}\n </ds-mobile-header>\n <slot />\n </ds-shell-page>,\n this.renderManagedMobileActionMenu(),\n ];\n }\n\n private renderSlottedShell(\n shellCls: Record<string, boolean>,\n mobile: boolean,\n mobileToolActive: boolean,\n mobileStageBlocked: boolean,\n fullscreen: boolean\n ) {\n return (\n <Host\n class={shellCls}\n responsive-mode={this.resolvedMode}\n data-wide-viewport={String(this.wideViewport)}\n >\n <div class=\"shell-app__banner\">\n <slot name=\"banner\" />\n </div>\n <div class=\"shell-app__row\">\n <div class=\"shell-app__chrome\" aria-hidden=\"true\">\n {this.renderPaperTexture()}\n </div>\n <div\n class=\"shell-app__panel\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n <slot name=\"panel\" />\n </div>\n <div class=\"shell-app__main\">\n <div\n class=\"shell-app__bar\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n <slot name=\"bar\" />\n </div>\n <div\n class=\"shell-app__tools\"\n data-ds-overlay-boundary={fullscreen ? undefined : true}\n aria-hidden={\n mobile && (!mobileToolActive || this.mobileSheetNavOpen) ? 'true' : undefined\n }\n inert={mobile && (!mobileToolActive || this.mobileSheetNavOpen) ? true : undefined}\n hidden={mobile && !mobileToolActive}\n >\n <slot name=\"tools\" />\n </div>\n <div\n class=\"shell-app__content\"\n data-ds-overlay-boundary\n aria-hidden={fullscreen || mobileStageBlocked ? 'true' : undefined}\n inert={fullscreen || mobileStageBlocked ? true : undefined}\n tabIndex={0}\n >\n <slot />\n </div>\n <div\n class=\"shell-app__mobile-sheet-nav\"\n hidden={!mobile || !this.mobileSheetNavOpen}\n aria-hidden={mobile && this.mobileSheetNavOpen ? undefined : 'true'}\n inert={mobile && this.mobileSheetNavOpen ? undefined : true}\n >\n <slot name=\"mobile-sheet-nav\" />\n </div>\n <div class=\"shell-app__mobile-bar-nav\" hidden={!mobile}>\n <slot name=\"mobile-bar-nav\" />\n </div>\n </div>\n </div>\n </Host>\n );\n }\n\n render() {\n const chromeActive = this.chromeLayerActive();\n const gradientActive = this.gradientLayerActive();\n const paperTextureActive = this.paperTextureLayerActive();\n const mobile = this.resolvedMode === 'mobile';\n const mobileToolActive = mobile && this.effectiveMobileDestination !== 'area';\n const mobileStageBlocked = mobile && (mobileToolActive || this.effectiveMobileSheetNavOpen);\n const fullscreen = !mobile && this.toolsFullscreen;\n const shellCls: Record<string, boolean> = {\n 'shell-app': true,\n 'shell-app--chrome': chromeActive,\n 'shell-app--gradient': gradientActive,\n 'shell-app--paper-texture': paperTextureActive,\n [`shell-app--${this.navStyle}`]: true,\n [`shell-app--${this.resolvedMode}`]: true,\n 'shell-app--tools-fullscreen': fullscreen,\n 'shell-app--mobile-tool-active': mobileToolActive,\n 'shell-app--mobile-sheet-nav-open': mobile && this.effectiveMobileSheetNavOpen,\n 'shell-app--managed': this.managed,\n 'shell-app--slotted': !this.managed,\n };\n\n if (!this.managed) {\n return this.renderSlottedShell(\n shellCls,\n mobile,\n mobileToolActive,\n mobileStageBlocked,\n fullscreen\n );\n }\n\n return (\n <Host\n class={shellCls}\n responsive-mode={this.resolvedMode}\n data-wide-viewport={String(this.wideViewport)}\n >\n <div class=\"shell-app__banner\">\n <slot name=\"banner\" />\n </div>\n <div class=\"shell-app__row\">\n <div class=\"shell-app__chrome\" aria-hidden=\"true\">\n {this.renderPaperTexture()}\n </div>\n <div\n class=\"shell-app__panel\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n {this.renderManagedPanelNav()}\n </div>\n <div class=\"shell-app__main\">\n <div\n class=\"shell-app__bar\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n {this.renderManagedTopBar()}\n </div>\n <div\n class=\"shell-app__tools\"\n data-ds-overlay-boundary={fullscreen ? undefined : true}\n aria-hidden={\n mobile && (!mobileToolActive || this.effectiveMobileSheetNavOpen)\n ? 'true'\n : undefined\n }\n inert={\n mobile && (!mobileToolActive || this.effectiveMobileSheetNavOpen) ? true : undefined\n }\n hidden={mobile && !mobileToolActive}\n >\n {this.renderManagedTools()}\n </div>\n <div\n class=\"shell-app__content\"\n data-ds-overlay-boundary\n aria-hidden={fullscreen || mobileStageBlocked ? 'true' : undefined}\n inert={fullscreen || mobileStageBlocked ? true : undefined}\n tabIndex={0}\n >\n {this.renderManagedPage()}\n </div>\n <div\n class=\"shell-app__mobile-sheet-nav\"\n hidden={!mobile || !this.effectiveMobileSheetNavOpen}\n aria-hidden={mobile && this.effectiveMobileSheetNavOpen ? undefined : 'true'}\n inert={mobile && this.effectiveMobileSheetNavOpen ? undefined : true}\n >\n {this.renderManagedMobileSheetNav()}\n </div>\n <div class=\"shell-app__mobile-bar-nav\" hidden={!mobile}>\n {this.renderManagedMobileBarNav()}\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
730
730
|
"type": "registry:ui"
|
|
731
731
|
},
|
|
732
732
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
],
|
|
55
55
|
"responsiveBehavior": [
|
|
56
56
|
"Desktop and tablet compose PanelTools-compatible rail and drawer chrome.",
|
|
57
|
-
"Mobile renders one centered MobileHeader for every direct tool; explicitly grouped Inbox tools use the
|
|
57
|
+
"Mobile renders one centered MobileHeader for every direct tool; explicitly grouped Inbox tools use the medium segmented section control and share the global responsive scale.",
|
|
58
58
|
"Mobile intentionally omits desktop and tablet rail accessories instead of promoting them into tool views. Applications provide an equivalent persistent mobile status surface through ShellApp's top banner or status region."
|
|
59
59
|
],
|
|
60
60
|
"frameworkCaveats": {
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
"events": [
|
|
217
217
|
{
|
|
218
218
|
"name": "dsHeaderAction",
|
|
219
|
-
"detail": "
|
|
219
|
+
"detail": "PanelToolsHeaderActionDetail",
|
|
220
220
|
"bubbles": true,
|
|
221
221
|
"cancelable": true,
|
|
222
222
|
"composed": true
|
|
@@ -377,7 +377,7 @@
|
|
|
377
377
|
"events": [
|
|
378
378
|
{
|
|
379
379
|
"name": "dsHeaderAction",
|
|
380
|
-
"detail": "
|
|
380
|
+
"detail": "PanelToolsHeaderActionDetail",
|
|
381
381
|
"bubbles": true,
|
|
382
382
|
"cancelable": true,
|
|
383
383
|
"composed": true
|
|
@@ -482,7 +482,7 @@
|
|
|
482
482
|
},
|
|
483
483
|
{
|
|
484
484
|
"path": "src/wc/components/ShellTools/ShellTools.tsx",
|
|
485
|
-
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n Watch,\n} from '@stencil/core';\nimport {\n PANEL_TOOLS_LABELS,\n type PanelToolsHeaderConfig,\n type PanelToolsHeaders,\n type PanelToolsItem,\n type PanelToolsRailAccessory,\n type PanelToolsRailAccessoryActionDetail,\n type PanelToolsToolId,\n} from '../PanelTools/panel-tools-types';\nimport {\n orderPanelToolsItems,\n reconcilePanelToolsAvailability,\n resolvePanelToolActivation,\n} from '../PanelTools/panel-tools-utils';\nimport {\n itemUsesShellInbox,\n isShellInboxTool,\n resolveAvailableInboxTool,\n type ShellInboxToolId,\n type ShellResponsiveMode,\n} from '../../shell/shell-responsive';\n\n@Component({\n tag: 'ds-shell-tools',\n styleUrl: 'ShellTools.css',\n shadow: true,\n})\nexport class ShellTools {\n @Element() el!: HTMLElement;\n\n /** Resolved by ShellApp; mobile uses a full-stage tool presentation. */\n @Prop({ attribute: 'responsive-mode', reflect: true })\n responsiveMode: ShellResponsiveMode = 'desktop';\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n @Prop({ mutable: true, attribute: 'active-tool', reflect: true })\n activeTool: PanelToolsToolId | '' = '';\n @Prop({ mutable: true, reflect: true }) presentation: 'drawer' | 'fullscreen' = 'drawer';\n @Prop({ attribute: 'fullscreen-header-mode', reflect: true })\n fullscreenHeaderMode: 'shared' | 'split' = 'shared';\n @Prop() items: PanelToolsItem[] = [];\n /** Desktop/tablet rail accessories. Intentionally omitted from the mobile tool stage. */\n @Prop() accessories: PanelToolsRailAccessory[] = [];\n @Prop() headers: PanelToolsHeaders = {};\n @Prop({ attribute: 'storage-key' }) storageKey: string = '';\n @Prop() toolsLabel: string = 'Tools';\n @Prop() toolShortcutsLabel: string = 'Tool shortcuts';\n @Prop() inboxLabel: string = 'Inbox';\n @Prop() inboxNavigationLabel: string = 'Inbox sections';\n\n @Watch('presentation')\n presentationChanged(next: 'drawer' | 'fullscreen', previous: 'drawer' | 'fullscreen') {\n if (next === previous) return;\n\n /*\n * Consumers update ShellTools and their slotted master/detail layout in the\n * same render. Forward the request immediately so PanelTools can conceal\n * its surface until its matching render class commits on the next frame.\n */\n if (this.panelToolsEl && this.panelToolsEl.getAttribute('presentation') !== next) {\n this.panelToolsEl.setAttribute('presentation', next);\n }\n }\n\n @Event({ bubbles: true, composed: true }) dsToolChange!: EventEmitter<{\n id: PanelToolsToolId;\n selected: boolean;\n }>;\n @Event({ bubbles: true, composed: true }) dsPresentationChange!: EventEmitter<{\n presentation: 'drawer' | 'fullscreen';\n }>;\n @Event({ bubbles: true, composed: true }) dsHeaderBack!: EventEmitter<{\n tool: PanelToolsToolId;\n }>;\n @Event({ bubbles: true, composed: true }) dsHeaderAction!: EventEmitter<{\n tool: PanelToolsToolId;\n id: string;\n }>;\n @Event({ bubbles: true, composed: true })\n dsRailAccessoryAction!: EventEmitter<PanelToolsRailAccessoryActionDetail>;\n\n private panelToolsEl: HTMLDsPanelToolsElement | null = null;\n\n private get resolvedItems(): PanelToolsItem[] {\n return this.items ?? [];\n }\n\n private get availableInboxTools(): ShellInboxToolId[] {\n return this.resolvedItems\n .filter(item => !item.isInactive && itemUsesShellInbox(item))\n .map(item => item.id as ShellInboxToolId);\n }\n\n private get mobileViewOrder(): PanelToolsToolId[] {\n return orderPanelToolsItems(this.resolvedItems).map(item => item.id);\n }\n\n private toolLabel(id: PanelToolsToolId): string {\n const item = this.resolvedItems.find(candidate => candidate.id === id);\n return (\n item?.label ??\n item?.ariaLabel ??\n PANEL_TOOLS_LABELS[id as keyof typeof PANEL_TOOLS_LABELS] ??\n id\n );\n }\n\n private get mobileActiveTool(): PanelToolsToolId | '' {\n const item = this.resolvedItems.find(candidate => candidate.id === this.activeTool);\n const inbox = item ? itemUsesShellInbox(item) : isShellInboxTool(this.activeTool);\n if (!inbox) return this.activeTool;\n return resolveAvailableInboxTool(this.activeTool, this.availableInboxTools);\n }\n\n componentWillLoad() {\n this.reconcileAvailability();\n }\n\n @Watch('items')\n handleItemsChange() {\n this.reconcileAvailability();\n }\n\n private reconcileAvailability() {\n const next = reconcilePanelToolsAvailability(this.resolvedItems, this.open, this.activeTool);\n this.open = next.open;\n this.activeTool = next.activeTool;\n }\n\n private resolvedHeaders(): PanelToolsHeaders {\n return this.headers ?? {};\n }\n\n private activeHeader(tool: PanelToolsToolId): PanelToolsHeaderConfig {\n return this.resolvedHeaders()[tool] ?? {};\n }\n\n private activateMobileTool(id: PanelToolsToolId) {\n const item = this.resolvedItems.find(entry => entry.id === id);\n if (!item || item.isInactive) return;\n const next = resolvePanelToolActivation(this.open, this.activeTool, id);\n this.open = next.open;\n this.activeTool = next.activeTool;\n this.dsToolChange.emit({ id, selected: next.selected });\n }\n\n private selectInboxTool = (event: CustomEvent<string>) => {\n const id = event.detail;\n if (!this.availableInboxTools.includes(id) || id === this.mobileActiveTool) return;\n this.open = true;\n this.activeTool = id;\n this.dsToolChange.emit({ id, selected: true });\n };\n\n private handlePanelToolChange = (\n event: CustomEvent<{ id: PanelToolsToolId; selected: boolean }>\n ) => {\n this.activeTool = event.detail.id;\n this.open = event.detail.selected;\n };\n\n private handlePanelPresentationChange = (\n event: CustomEvent<{ presentation: 'drawer' | 'fullscreen' }>\n ) => {\n this.presentation = event.detail.presentation;\n };\n\n private handlePanelRailAccessoryAction = (\n event: CustomEvent<PanelToolsRailAccessoryActionDetail>\n ) => {\n event.stopPropagation();\n this.dsRailAccessoryAction.emit(event.detail);\n };\n\n private handleMobileHeaderAction = (\n tool: PanelToolsToolId,\n id: string,\n event: CustomEvent<MouseEvent>\n ) => {\n event.stopPropagation();\n this.dsHeaderAction.emit({ tool, id });\n };\n\n /** Match PanelTools' imperative activation contract in every responsive mode. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n if (this.responsiveMode !== 'mobile') {\n await this.panelToolsEl?.activateTool(id);\n return;\n }\n this.activateMobileTool(id);\n }\n\n /** Close the active global tool surface. */\n @Method()\n async closeDrawer() {\n if (this.responsiveMode !== 'mobile') {\n await this.panelToolsEl?.closeDrawer();\n return;\n }\n if (!this.open) return;\n const id = this.activeTool;\n this.open = false;\n if (id) this.dsToolChange.emit({ id, selected: false });\n }\n\n /** Focus an application-owned action in the visible shared tool header. */\n @Method()\n async focusHeaderAction(id: string) {\n if (this.responsiveMode !== 'mobile') {\n await this.panelToolsEl?.focusHeaderAction(id);\n return;\n }\n const action = (this.el.shadowRoot ?? this.el).querySelector(\n `[data-header-action-id=\"${CSS.escape(id)}\"]`\n ) as (HTMLElement & { setFocus?: () => Promise<void> }) | null;\n await action?.setFocus?.();\n }\n\n private renderForwardedSlots() {\n return this.mobileViewOrder.flatMap(id => [\n <div key={`tool:${id}`} class=\"shell-tools__slot-proxy\" slot={id}>\n <slot name={id} />\n </div>,\n <div key={`tool-view:${id}`} class=\"shell-tools__slot-proxy\" slot={`${id}-view`}>\n <slot name={`${id}-view`} />\n </div>,\n ]);\n }\n\n private renderDesktop() {\n return (\n <div class=\"shell-tools__desktop\">\n <ds-panel-tools\n open={this.open}\n activeTool={this.activeTool}\n presentation={this.presentation}\n fullscreenHeaderMode={this.fullscreenHeaderMode}\n items={this.resolvedItems}\n accessories={this.accessories ?? []}\n headers={this.resolvedHeaders()}\n storageKey={this.storageKey}\n toolsLabel={this.toolsLabel}\n toolShortcutsLabel={this.toolShortcutsLabel}\n ref={element => {\n this.panelToolsEl = element as HTMLDsPanelToolsElement | null;\n }}\n onDsToolChange={this.handlePanelToolChange}\n onDsPresentationChange={this.handlePanelPresentationChange}\n onDsRailAccessoryAction={this.handlePanelRailAccessoryAction}\n >\n {this.renderForwardedSlots()}\n </ds-panel-tools>\n </div>\n );\n }\n\n private renderMobileHeader(tool: PanelToolsToolId) {\n const header = this.activeHeader(tool);\n const configuredTitle = header.title?.trim();\n const title = configuredTitle || this.toolLabel(tool);\n const actions = (header.actions ?? []).filter(action => action.id !== 'fullscreen');\n const item = this.resolvedItems.find(candidate => candidate.id === tool);\n const inboxRoot = Boolean(item && itemUsesShellInbox(item) && !header.showBack);\n const sections = inboxRoot\n ? this.availableInboxTools.map(id => {\n const item = this.resolvedItems.find(candidate => candidate.id === id);\n return {\n id,\n label: this.toolLabel(id),\n variant: 'label' as const,\n dot: item?.dot,\n };\n })\n : [];\n\n return (\n <ds-mobile-header\n class=\"shell-tools__mobile-header\"\n heading={title}\n headingLevel=\"h2\"\n sections={sections}\n value={tool}\n sectionsAriaLabel={this.inboxNavigationLabel}\n sectionsPresentation={inboxRoot ? 'segmented' : 'switcher'}\n sectionsSize=\"lg\"\n onDsSectionChange={this.selectInboxTool}\n >\n {header.showBack ? (\n <ds-tooltip slot=\"leading\" label={header.backAriaLabel || 'Back'} side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon={header.backIcon || 'ChevronLeft'}\n size=\"md\"\n aria-label={header.backAriaLabel || 'Back'}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsHeaderBack.emit({ tool })}\n />\n </ds-tooltip>\n ) : null}\n {actions.map(action => (\n <ds-tooltip\n slot=\"trailing\"\n key={action.id}\n label={action.ariaLabel}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n id={action.triggerId || undefined}\n data-header-action-id={action.id}\n variant=\"icon\"\n icon={action.icon}\n size=\"md\"\n aria-label={action.ariaLabel}\n haspopup={action.haspopup}\n controls={action.controls}\n expanded={action.expanded}\n surfaceOpen={action.surfaceOpen}\n pressed={action.pressed}\n isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.handleMobileHeaderAction(tool, action.id, event)\n }\n />\n </ds-tooltip>\n ))}\n </ds-mobile-header>\n );\n }\n\n private renderMobile() {\n const tool = this.mobileActiveTool;\n return (\n <div\n class=\"shell-tools__mobile\"\n aria-hidden={this.open && tool ? undefined : 'true'}\n inert={this.open && tool ? undefined : true}\n >\n {tool ? this.renderMobileHeader(tool) : null}\n <div class=\"shell-tools__mobile-body\">\n {this.mobileViewOrder.map(id => {\n const active = this.open && id === tool;\n return (\n <div\n key={`mobile-tool-view:${id}`}\n class={{\n 'shell-tools__view': true,\n 'shell-tools__view--active': active,\n }}\n hidden={!active}\n aria-hidden={active ? undefined : 'true'}\n inert={active ? undefined : true}\n >\n <slot name={`${id}-view`}>\n <slot name={id} />\n </slot>\n </div>\n );\n })}\n </div>\n </div>\n );\n }\n\n render() {\n const mobile = this.responsiveMode === 'mobile';\n return (\n <Host\n role={\n mobile ? (this.presentation === 'fullscreen' ? 'dialog' : 'complementary') : undefined\n }\n aria-modal={mobile && this.presentation === 'fullscreen' ? 'true' : undefined}\n aria-label={mobile ? this.toolsLabel : undefined}\n >\n {mobile ? this.renderMobile() : this.renderDesktop()}\n </Host>\n );\n }\n}\n",
|
|
485
|
+
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n Watch,\n} from '@stencil/core';\nimport {\n PANEL_TOOLS_LABELS,\n type PanelToolsHeaderActionDetail,\n type PanelToolsHeaderConfig,\n type PanelToolsHeaders,\n type PanelToolsItem,\n type PanelToolsRailAccessory,\n type PanelToolsRailAccessoryActionDetail,\n type PanelToolsToolId,\n} from '../PanelTools/panel-tools-types';\nimport {\n orderPanelToolsItems,\n reconcilePanelToolsAvailability,\n resolvePanelToolActivation,\n} from '../PanelTools/panel-tools-utils';\nimport {\n itemUsesShellInbox,\n isShellInboxTool,\n resolveAvailableInboxTool,\n type ShellInboxToolId,\n type ShellResponsiveMode,\n} from '../../shell/shell-responsive';\n\n@Component({\n tag: 'ds-shell-tools',\n styleUrl: 'ShellTools.css',\n shadow: true,\n})\nexport class ShellTools {\n @Element() el!: HTMLElement;\n\n /** Resolved by ShellApp; mobile uses a full-stage tool presentation. */\n @Prop({ attribute: 'responsive-mode', reflect: true })\n responsiveMode: ShellResponsiveMode = 'desktop';\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n @Prop({ mutable: true, attribute: 'active-tool', reflect: true })\n activeTool: PanelToolsToolId | '' = '';\n @Prop({ mutable: true, reflect: true }) presentation: 'drawer' | 'fullscreen' = 'drawer';\n @Prop({ attribute: 'fullscreen-header-mode', reflect: true })\n fullscreenHeaderMode: 'shared' | 'split' = 'shared';\n @Prop() items: PanelToolsItem[] = [];\n /** Desktop/tablet rail accessories. Intentionally omitted from the mobile tool stage. */\n @Prop() accessories: PanelToolsRailAccessory[] = [];\n @Prop() headers: PanelToolsHeaders = {};\n @Prop({ attribute: 'storage-key' }) storageKey: string = '';\n @Prop() toolsLabel: string = 'Tools';\n @Prop() toolShortcutsLabel: string = 'Tool shortcuts';\n @Prop() inboxLabel: string = 'Inbox';\n @Prop() inboxNavigationLabel: string = 'Inbox sections';\n\n @Watch('presentation')\n presentationChanged(next: 'drawer' | 'fullscreen', previous: 'drawer' | 'fullscreen') {\n if (next === previous) return;\n\n /*\n * Consumers update ShellTools and their slotted master/detail layout in the\n * same render. Forward the request immediately so PanelTools can conceal\n * its surface until its matching render class commits on the next frame.\n */\n if (this.panelToolsEl && this.panelToolsEl.getAttribute('presentation') !== next) {\n this.panelToolsEl.setAttribute('presentation', next);\n }\n }\n\n @Event({ bubbles: true, composed: true }) dsToolChange!: EventEmitter<{\n id: PanelToolsToolId;\n selected: boolean;\n }>;\n @Event({ bubbles: true, composed: true }) dsPresentationChange!: EventEmitter<{\n presentation: 'drawer' | 'fullscreen';\n }>;\n @Event({ bubbles: true, composed: true }) dsHeaderBack!: EventEmitter<{\n tool: PanelToolsToolId;\n }>;\n @Event({ bubbles: true, composed: true })\n dsHeaderAction!: EventEmitter<PanelToolsHeaderActionDetail>;\n @Event({ bubbles: true, composed: true })\n dsRailAccessoryAction!: EventEmitter<PanelToolsRailAccessoryActionDetail>;\n\n private panelToolsEl: HTMLDsPanelToolsElement | null = null;\n\n private get resolvedItems(): PanelToolsItem[] {\n return this.items ?? [];\n }\n\n private get availableInboxTools(): ShellInboxToolId[] {\n return this.resolvedItems\n .filter(item => !item.isInactive && itemUsesShellInbox(item))\n .map(item => item.id as ShellInboxToolId);\n }\n\n private get mobileViewOrder(): PanelToolsToolId[] {\n return orderPanelToolsItems(this.resolvedItems).map(item => item.id);\n }\n\n private toolLabel(id: PanelToolsToolId): string {\n const item = this.resolvedItems.find(candidate => candidate.id === id);\n return (\n item?.label ??\n item?.ariaLabel ??\n PANEL_TOOLS_LABELS[id as keyof typeof PANEL_TOOLS_LABELS] ??\n id\n );\n }\n\n private get mobileActiveTool(): PanelToolsToolId | '' {\n const item = this.resolvedItems.find(candidate => candidate.id === this.activeTool);\n const inbox = item ? itemUsesShellInbox(item) : isShellInboxTool(this.activeTool);\n if (!inbox) return this.activeTool;\n return resolveAvailableInboxTool(this.activeTool, this.availableInboxTools);\n }\n\n componentWillLoad() {\n this.reconcileAvailability();\n }\n\n @Watch('items')\n handleItemsChange() {\n this.reconcileAvailability();\n }\n\n private reconcileAvailability() {\n const next = reconcilePanelToolsAvailability(this.resolvedItems, this.open, this.activeTool);\n this.open = next.open;\n this.activeTool = next.activeTool;\n }\n\n private resolvedHeaders(): PanelToolsHeaders {\n return this.headers ?? {};\n }\n\n private activeHeader(tool: PanelToolsToolId): PanelToolsHeaderConfig {\n return this.resolvedHeaders()[tool] ?? {};\n }\n\n private activateMobileTool(id: PanelToolsToolId) {\n const item = this.resolvedItems.find(entry => entry.id === id);\n if (!item || item.isInactive) return;\n const next = resolvePanelToolActivation(this.open, this.activeTool, id);\n this.open = next.open;\n this.activeTool = next.activeTool;\n this.dsToolChange.emit({ id, selected: next.selected });\n }\n\n private selectInboxTool = (event: CustomEvent<string>) => {\n const id = event.detail;\n if (!this.availableInboxTools.includes(id) || id === this.mobileActiveTool) return;\n this.open = true;\n this.activeTool = id;\n this.dsToolChange.emit({ id, selected: true });\n };\n\n private handlePanelToolChange = (\n event: CustomEvent<{ id: PanelToolsToolId; selected: boolean }>\n ) => {\n this.activeTool = event.detail.id;\n this.open = event.detail.selected;\n };\n\n private handlePanelPresentationChange = (\n event: CustomEvent<{ presentation: 'drawer' | 'fullscreen' }>\n ) => {\n this.presentation = event.detail.presentation;\n };\n\n private handlePanelRailAccessoryAction = (\n event: CustomEvent<PanelToolsRailAccessoryActionDetail>\n ) => {\n event.stopPropagation();\n this.dsRailAccessoryAction.emit(event.detail);\n };\n\n private handleMobileHeaderAction = (\n tool: PanelToolsToolId,\n id: string,\n event: CustomEvent<MouseEvent>\n ) => {\n event.stopPropagation();\n this.dsHeaderAction.emit({ tool, id, anchor: event.currentTarget as HTMLElement });\n };\n\n /** Match PanelTools' imperative activation contract in every responsive mode. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n if (this.responsiveMode !== 'mobile') {\n await this.panelToolsEl?.activateTool(id);\n return;\n }\n this.activateMobileTool(id);\n }\n\n /** Close the active global tool surface. */\n @Method()\n async closeDrawer() {\n if (this.responsiveMode !== 'mobile') {\n await this.panelToolsEl?.closeDrawer();\n return;\n }\n if (!this.open) return;\n const id = this.activeTool;\n this.open = false;\n if (id) this.dsToolChange.emit({ id, selected: false });\n }\n\n /** Focus an application-owned action in the visible shared tool header. */\n @Method()\n async focusHeaderAction(id: string) {\n if (this.responsiveMode !== 'mobile') {\n await this.panelToolsEl?.focusHeaderAction(id);\n return;\n }\n const action = (this.el.shadowRoot ?? this.el).querySelector(\n `[data-header-action-id=\"${CSS.escape(id)}\"]`\n ) as (HTMLElement & { setFocus?: () => Promise<void> }) | null;\n await action?.setFocus?.();\n }\n\n private renderForwardedSlots() {\n return this.mobileViewOrder.flatMap(id => [\n <div key={`tool:${id}`} class=\"shell-tools__slot-proxy\" slot={id}>\n <slot name={id} />\n </div>,\n <div key={`tool-view:${id}`} class=\"shell-tools__slot-proxy\" slot={`${id}-view`}>\n <slot name={`${id}-view`} />\n </div>,\n ]);\n }\n\n private renderDesktop() {\n return (\n <div class=\"shell-tools__desktop\">\n <ds-panel-tools\n open={this.open}\n activeTool={this.activeTool}\n presentation={this.presentation}\n fullscreenHeaderMode={this.fullscreenHeaderMode}\n items={this.resolvedItems}\n accessories={this.accessories ?? []}\n headers={this.resolvedHeaders()}\n storageKey={this.storageKey}\n toolsLabel={this.toolsLabel}\n toolShortcutsLabel={this.toolShortcutsLabel}\n ref={element => {\n this.panelToolsEl = element as HTMLDsPanelToolsElement | null;\n }}\n onDsToolChange={this.handlePanelToolChange}\n onDsPresentationChange={this.handlePanelPresentationChange}\n onDsRailAccessoryAction={this.handlePanelRailAccessoryAction}\n >\n {this.renderForwardedSlots()}\n </ds-panel-tools>\n </div>\n );\n }\n\n private renderMobileHeader(tool: PanelToolsToolId) {\n const header = this.activeHeader(tool);\n const configuredTitle = header.title?.trim();\n const title = configuredTitle || this.toolLabel(tool);\n const actions = (header.actions ?? []).filter(action => action.id !== 'fullscreen');\n const item = this.resolvedItems.find(candidate => candidate.id === tool);\n const inboxRoot = Boolean(item && itemUsesShellInbox(item) && !header.showBack);\n const sections = inboxRoot\n ? this.availableInboxTools.map(id => {\n const item = this.resolvedItems.find(candidate => candidate.id === id);\n return {\n id,\n label: this.toolLabel(id),\n variant: 'label' as const,\n dot: item?.dot,\n };\n })\n : [];\n\n return (\n <ds-mobile-header\n class=\"shell-tools__mobile-header\"\n heading={title}\n headingLevel=\"h2\"\n sections={sections}\n value={tool}\n sectionsAriaLabel={this.inboxNavigationLabel}\n sectionsPresentation={inboxRoot ? 'segmented' : 'switcher'}\n sectionsSize=\"md\"\n onDsSectionChange={this.selectInboxTool}\n >\n {header.showBack ? (\n <ds-tooltip slot=\"leading\" label={header.backAriaLabel || 'Back'} side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon={header.backIcon || 'ChevronLeft'}\n size=\"md\"\n aria-label={header.backAriaLabel || 'Back'}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsHeaderBack.emit({ tool })}\n />\n </ds-tooltip>\n ) : null}\n {actions.map(action => (\n <ds-tooltip\n slot=\"trailing\"\n key={action.id}\n label={action.ariaLabel}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n id={action.triggerId || undefined}\n data-header-action-id={action.id}\n variant=\"icon\"\n icon={action.icon}\n size=\"md\"\n aria-label={action.ariaLabel}\n haspopup={action.haspopup}\n controls={action.controls}\n expanded={action.expanded}\n surfaceOpen={action.surfaceOpen}\n pressed={action.pressed}\n isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.handleMobileHeaderAction(tool, action.id, event)\n }\n />\n </ds-tooltip>\n ))}\n </ds-mobile-header>\n );\n }\n\n private renderMobile() {\n const tool = this.mobileActiveTool;\n return (\n <div\n class=\"shell-tools__mobile\"\n aria-hidden={this.open && tool ? undefined : 'true'}\n inert={this.open && tool ? undefined : true}\n >\n {tool ? this.renderMobileHeader(tool) : null}\n <div class=\"shell-tools__mobile-body\">\n {this.mobileViewOrder.map(id => {\n const active = this.open && id === tool;\n return (\n <div\n key={`mobile-tool-view:${id}`}\n class={{\n 'shell-tools__view': true,\n 'shell-tools__view--active': active,\n }}\n hidden={!active}\n aria-hidden={active ? undefined : 'true'}\n inert={active ? undefined : true}\n >\n <slot name={`${id}-view`}>\n <slot name={id} />\n </slot>\n </div>\n );\n })}\n </div>\n </div>\n );\n }\n\n render() {\n const mobile = this.responsiveMode === 'mobile';\n return (\n <Host\n role={\n mobile ? (this.presentation === 'fullscreen' ? 'dialog' : 'complementary') : undefined\n }\n aria-modal={mobile && this.presentation === 'fullscreen' ? 'true' : undefined}\n aria-label={mobile ? this.toolsLabel : undefined}\n >\n {mobile ? this.renderMobile() : this.renderDesktop()}\n </Host>\n );\n }\n}\n",
|
|
486
486
|
"type": "registry:ui"
|
|
487
487
|
}
|
|
488
488
|
]
|