@ds-mo/ui 14.28.0 → 14.29.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 +27 -17
- package/dist/agent.json +148 -25
- package/dist/angular/ds-button-filled.d.ts +1 -1
- package/dist/angular/ds-button-filled.js +3 -3
- package/dist/angular/ds-button-unfilled.d.ts +1 -1
- package/dist/angular/ds-button-unfilled.js +3 -3
- package/dist/angular/ds-panel-nav.d.ts +1 -1
- package/dist/angular/ds-panel-nav.js +3 -3
- package/dist/angular/ds-panel-tool-header.d.ts +1 -1
- package/dist/angular/ds-panel-tool-header.js +3 -3
- package/dist/angular/ds-panel-tool-search.d.ts +1 -1
- package/dist/angular/ds-panel-tool-search.js +3 -3
- package/dist/angular/ds-table.d.ts +1 -1
- package/dist/angular/ds-table.js +3 -3
- package/dist/angular/ds-tag.d.ts +1 -1
- package/dist/angular/ds-tag.js +3 -3
- 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-title.js +1 -1
- package/dist/components/ds-bar-workflow.js +1 -1
- package/dist/components/ds-button-filled.js +1 -1
- package/dist/components/ds-button-unfilled.js +1 -1
- package/dist/components/ds-card-chart.js +1 -1
- package/dist/components/ds-card-navigation.js +1 -1
- package/dist/components/ds-card-navigation.js.map +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-legend.js +1 -1
- package/dist/components/ds-chart-legend.js.map +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-chip.js +1 -1
- package/dist/components/ds-code-block.js +1 -1
- package/dist/components/ds-conversation-list-item.js +1 -1
- package/dist/components/ds-conversation-list-item.js.map +1 -1
- package/dist/components/ds-filter-menu.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-sub-nav.js +1 -1
- package/dist/components/ds-panel-sub-nav.js.map +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-tool-search.js.map +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-radio.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-tools.js +1 -1
- package/dist/components/ds-tab-group.js +1 -1
- package/dist/components/ds-table-filter.js +1 -1
- package/dist/components/ds-table-group.js +1 -1
- package/dist/components/ds-table-group.js.map +1 -1
- package/dist/components/ds-table-saved-views.js +1 -1
- package/dist/components/ds-table-saved-views.js.map +1 -1
- package/dist/components/ds-table-search.js +1 -1
- package/dist/components/ds-table-search.js.map +1 -1
- package/dist/components/ds-table-sort.js +1 -1
- package/dist/components/ds-table-sort.js.map +1 -1
- package/dist/components/ds-table.js +1 -1
- package/dist/components/ds-table.js.map +1 -1
- package/dist/components/ds-tag.js +1 -1
- package/dist/components/ds-textarea.js +1 -1
- package/dist/components/ds-textarea.js.map +1 -1
- package/dist/components/ds-toast.js +1 -1
- package/dist/components/p--yvQzZoO.js +2 -0
- package/dist/components/p--yvQzZoO.js.map +1 -0
- package/dist/components/p-B5AZd41L.js +2 -0
- package/dist/components/p-B5AZd41L.js.map +1 -0
- package/dist/components/p-B5eMVBlz.js +2 -0
- package/dist/components/p-B5eMVBlz.js.map +1 -0
- package/dist/components/p-BGnRFtBa.js +2 -0
- package/dist/components/p-BGnRFtBa.js.map +1 -0
- package/dist/components/p-BIDGQ4tc.js +2 -0
- package/dist/components/p-BIDGQ4tc.js.map +1 -0
- package/dist/components/p-BZDQLjE0.js +2 -0
- package/dist/components/p-BZDQLjE0.js.map +1 -0
- package/dist/components/{p-DmYZLH_L.js → p-BdC7wxhu.js} +2 -2
- package/dist/components/{p-DmYZLH_L.js.map → p-BdC7wxhu.js.map} +1 -1
- package/dist/components/p-BfVlFinS.js +2 -0
- package/dist/components/{p-WM1GTEun.js.map → p-BfVlFinS.js.map} +1 -1
- package/dist/components/p-BmRHJ_jo.js +2 -0
- package/dist/components/p-BmRHJ_jo.js.map +1 -0
- package/dist/components/{p-CxbGZJBU.js → p-BwAWZAHk.js} +2 -2
- package/dist/components/{p-8atLxerP.js → p-By5TDnbF.js} +2 -2
- package/dist/components/{p-BdXUd-tK.js → p-CDXcINTO.js} +2 -2
- package/dist/components/{p-BdXUd-tK.js.map → p-CDXcINTO.js.map} +1 -1
- package/dist/components/{p-DGHeKNfG.js → p-CE8gsv7V.js} +2 -2
- package/dist/components/p-CLcwh1kq.js +2 -0
- package/dist/components/p-CLcwh1kq.js.map +1 -0
- package/dist/components/p-CcK3UiTu.js +2 -0
- package/dist/components/{p-NB-SoVpo.js.map → p-CcK3UiTu.js.map} +1 -1
- package/dist/components/p-Cr5vn5JO.js +2 -0
- package/dist/components/{p-CsW_2w-s.js.map → p-Cr5vn5JO.js.map} +1 -1
- package/dist/components/{p-CW7JhWlS.js → p-Crd3PEUx.js} +2 -2
- package/dist/components/{p-CW7JhWlS.js.map → p-Crd3PEUx.js.map} +1 -1
- package/dist/components/{p-Kp8ifB1g.js → p-Ct90k-nz.js} +2 -2
- package/dist/components/{p-C82vesh_.js → p-Cx2dWjaW.js} +2 -2
- package/dist/components/p-CyoLh8su.js +2 -0
- package/dist/components/p-CyoLh8su.js.map +1 -0
- package/dist/components/p-CysCSinT.js +2 -0
- package/dist/components/{p-BdU6FPu4.js.map → p-CysCSinT.js.map} +1 -1
- package/dist/components/p-CzbsTZt7.js.map +1 -1
- package/dist/components/p-D1pPq-y0.js +2 -0
- package/dist/components/{p-BWDbER5T.js.map → p-D1pPq-y0.js.map} +1 -1
- package/dist/components/{p-B3iy9Rl_.js → p-DF8W85Tz.js} +2 -2
- package/dist/components/p-GbtafFds.js +2 -0
- package/dist/components/p-GbtafFds.js.map +1 -0
- package/dist/components/{p-B_NN7k8H.js → p-K39w53Bw.js} +2 -2
- package/dist/components/p-K39w53Bw.js.map +1 -0
- package/dist/components/p-frKsmPQn.js +2 -0
- package/dist/components/{p-CvB76TJn.js.map → p-frKsmPQn.js.map} +1 -1
- package/dist/docs/components.json +188 -6
- package/dist/lib/components/ButtonFilled/ButtonFilled.d.ts +11 -3
- package/dist/lib/components/PanelTools/panel-tools-types.d.ts +2 -0
- package/dist/lib/components/ShellApp/shell-app-types.d.ts +2 -0
- package/dist/lib/shell/index.js.map +1 -1
- package/dist/lib/utils/choice-list.d.ts +2 -0
- package/dist/mcp-data/registry/bar-nav.json +7 -2
- package/dist/mcp-data/registry/bar-title.json +8 -3
- package/dist/mcp-data/registry/button-filled.json +26 -5
- package/dist/mcp-data/registry/button-unfilled.json +27 -6
- package/dist/mcp-data/registry/filter-menu.json +7 -2
- package/dist/mcp-data/registry/mobile-section-switcher.json +8 -3
- package/dist/mcp-data/registry/panel-nav.json +27 -6
- package/dist/mcp-data/registry/panel-tool-header.json +23 -2
- package/dist/mcp-data/registry/panel-tool-search.json +23 -2
- package/dist/mcp-data/registry/panel-tools.json +7 -2
- package/dist/mcp-data/registry/registry.json +147 -24
- package/dist/mcp-data/registry/select.json +8 -3
- package/dist/mcp-data/registry/shell-app.json +8 -3
- package/dist/mcp-data/registry/shell-tools.json +7 -2
- package/dist/mcp-data/registry/table-group.json +7 -2
- package/dist/mcp-data/registry/table-saved-views.json +7 -2
- package/dist/mcp-data/registry/table-search.json +7 -2
- package/dist/mcp-data/registry/table-sort.json +7 -2
- package/dist/mcp-data/registry/table.json +24 -3
- package/dist/mcp-data/registry/tag.json +24 -3
- package/dist/styles/table.css +1 -0
- package/dist/types/components/BarTitle/BarTitle.d.ts +3 -0
- package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +11 -3
- package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +11 -5
- package/dist/types/components/MobileSectionSwitcher/MobileSectionSwitcher.d.ts +2 -0
- package/dist/types/components/PanelNav/PanelNav.d.ts +4 -1
- package/dist/types/components/PanelToolHeader/PanelToolHeader.d.ts +2 -0
- package/dist/types/components/PanelToolSearch/PanelToolSearch.d.ts +2 -0
- package/dist/types/components/PanelTools/panel-tools-types.d.ts +2 -0
- package/dist/types/components/ShellApp/ShellApp.d.ts +1 -0
- package/dist/types/components/ShellApp/shell-app-types.d.ts +2 -0
- package/dist/types/components/Table/Table.d.ts +10 -0
- package/dist/types/components/Table/table-row-view.d.ts +6 -1
- package/dist/types/components/TableSort/TableSort.d.ts +1 -0
- package/dist/types/components/Tag/Tag.d.ts +3 -0
- package/dist/types/components.d.ts +69 -6
- package/dist/types/utils/choice-list.d.ts +2 -0
- package/dist/vue/ds-button-filled.js +1 -0
- package/dist/vue/ds-button-unfilled.js +1 -0
- package/dist/vue/ds-panel-nav.js +1 -0
- package/dist/vue/ds-panel-tool-header.js +1 -0
- package/dist/vue/ds-panel-tool-search.js +1 -0
- package/dist/vue/ds-table.js +1 -0
- package/dist/vue/ds-tag.js +1 -0
- package/package.json +1 -1
- package/dist/components/p-6iF6gdoc.js +0 -2
- package/dist/components/p-6iF6gdoc.js.map +0 -1
- package/dist/components/p-BWDbER5T.js +0 -2
- package/dist/components/p-B_NN7k8H.js.map +0 -1
- package/dist/components/p-BdU6FPu4.js +0 -2
- package/dist/components/p-Bj018KMC.js +0 -2
- package/dist/components/p-Bj018KMC.js.map +0 -1
- package/dist/components/p-BxLF8b8a.js +0 -2
- package/dist/components/p-BxLF8b8a.js.map +0 -1
- package/dist/components/p-C1BJgnHY.js +0 -2
- package/dist/components/p-C1BJgnHY.js.map +0 -1
- package/dist/components/p-C78uTz-n.js +0 -2
- package/dist/components/p-C78uTz-n.js.map +0 -1
- package/dist/components/p-COn7l__5.js +0 -2
- package/dist/components/p-COn7l__5.js.map +0 -1
- package/dist/components/p-CsW_2w-s.js +0 -2
- package/dist/components/p-CvB76TJn.js +0 -2
- package/dist/components/p-D4-RkuER.js +0 -2
- package/dist/components/p-D4-RkuER.js.map +0 -1
- package/dist/components/p-DB_tlJ2d.js +0 -2
- package/dist/components/p-DB_tlJ2d.js.map +0 -1
- package/dist/components/p-NB-SoVpo.js +0 -2
- package/dist/components/p-P9My22Z1.js +0 -2
- package/dist/components/p-P9My22Z1.js.map +0 -1
- package/dist/components/p-WM1GTEun.js +0 -2
- package/dist/components/p-oAbq-DLk.js +0 -2
- package/dist/components/p-oAbq-DLk.js.map +0 -1
- /package/dist/components/{p-CxbGZJBU.js.map → p-BwAWZAHk.js.map} +0 -0
- /package/dist/components/{p-8atLxerP.js.map → p-By5TDnbF.js.map} +0 -0
- /package/dist/components/{p-DGHeKNfG.js.map → p-CE8gsv7V.js.map} +0 -0
- /package/dist/components/{p-Kp8ifB1g.js.map → p-Ct90k-nz.js.map} +0 -0
- /package/dist/components/{p-C82vesh_.js.map → p-Cx2dWjaW.js.map} +0 -0
- /package/dist/components/{p-B3iy9Rl_.js.map → p-DF8W85Tz.js.map} +0 -0
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"Create sections only for categories that are meaningful to customers, not merely to break a long list into arbitrary chunks.",
|
|
70
70
|
"When any option supplies subtext, every row uses the same subtext structure and missing values render an em dash. Subtext should disambiguate or add decision-helpful information.",
|
|
71
71
|
"Option labels and the selected trigger label stay on one line and truncate when constrained. Option subtext may wrap to at most two lines.",
|
|
72
|
-
"Use an option action only for a contextual menu about that user-managed option. The fixed ellipsis action appears on hover, keyboard activity, focus, and touch;
|
|
72
|
+
"Use an option action only for a contextual menu about that user-managed option. The fixed ellipsis action appears on hover, keyboard activity, focus, and touch; surfaceOpen keeps it visible while its controlled menu is rendered. Activating it emits dsOptionAction without selecting the option or closing the Select popup.",
|
|
73
73
|
"Use option subtextActions for a compact set of immediate actions that belong to one option. They render beneath only that option's label using the standard supporting-text inset and rhythm, use action or negative semantic color, and emit dsOptionSubtextAction without selecting the option or closing the popup. Unlike descriptive subtext, they do not reserve a blank supporting line on neighboring options.",
|
|
74
74
|
"The optional prefix icon and trailing indicator always use the parent surface's secondary foreground color, including when a selected value promotes the label to primary. When collapseLabel compact icon-only chrome is active, a selected value promotes the prefix icon to primary instead. The default down indicator communicates disclosure; up-down is reserved for compact value steppers such as pagination page size.",
|
|
75
75
|
"While the popup is open, the trigger holds the pressed interaction wash without changing its resting foreground or selected-value treatment.",
|
|
@@ -83,7 +83,8 @@
|
|
|
83
83
|
"The error trigger uses a 1.5px bold-negative inset stroke so invalid state remains distinguishable from the normal 1px border without changing control geometry. Set hasBorder to false for chrome where the trigger must remain borderless at rest, when selected or focused, and when invalid; error semantics and supporting copy remain available.",
|
|
84
84
|
"An initially empty option set is not an expected Select use case. The no-results state describes a search with no matches, not a source with no choices.",
|
|
85
85
|
"Use options for a flat list or sections for grouped choices; when sections are present they take precedence.",
|
|
86
|
-
"The background prop describes the actual parent surface and uses the same surface set as ButtonUnfilled."
|
|
86
|
+
"The background prop describes the actual parent surface and uses the same surface set as ButtonUnfilled.",
|
|
87
|
+
"Select owns its associated popup and automatically retains the shared surface-open pressed wash. Consumers do not synchronize a second flag for the built-in picker. Contextual option actions are externally owned: their surfaceOpen state keeps the action visible through the associated menu exit, independently of the selected option."
|
|
87
88
|
],
|
|
88
89
|
"responsiveBehavior": [
|
|
89
90
|
"Use width fill for form fields and width hug for compact filter bars. Override those defaults only when the owning layout has a clear constraint.",
|
|
@@ -114,6 +115,10 @@
|
|
|
114
115
|
{
|
|
115
116
|
"label": "Selection patterns",
|
|
116
117
|
"path": "src/docs/SelectionPatterns.mdx"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"label": "Surface ownership and trigger state",
|
|
121
|
+
"path": "src/docs/SurfaceOwnership.mdx"
|
|
117
122
|
}
|
|
118
123
|
]
|
|
119
124
|
},
|
|
@@ -972,7 +977,7 @@
|
|
|
972
977
|
},
|
|
973
978
|
{
|
|
974
979
|
"path": "src/wc/components/Select/Select.tsx",
|
|
975
|
-
"content": "import {\n AttachInternals,\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n type VNode,\n Watch,\n} from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_SUPPORTING_TEXT_VARIANT,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringArrayFormState,\n restoreStringFormState,\n setFormControlValue,\n setRepeatedFormControlValue,\n setRequiredValidity,\n type ControlWidth,\n} from '../../utils';\nimport {\n ChoiceActionFooter,\n ChoiceFooter,\n ChoiceOptionRow,\n ChoiceSearch,\n} from '../../utils/choice-list-parts';\nimport {\n choiceBackgroundClassMap,\n choiceListUsesIcons,\n choiceListUsesSubtext,\n enabledChoiceIndexes,\n filterChoiceSections,\n flattenChoiceSections,\n resolveChoiceSections,\n type ChoiceBackground,\n type ChoiceOption,\n type ChoiceSection,\n} from '../../utils/choice-list';\nimport { SelectController } from '../../utils/select-controller';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\n\nexport type SelectOption = ChoiceOption;\nexport type SelectSection = ChoiceSection;\nexport type SelectBackground = ChoiceBackground;\nexport type SelectSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type SelectWidth = ControlWidth;\nexport type SelectPopupAlign = 'start' | 'end';\nexport type SelectValue = string | string[];\nexport type SelectIndicator = 'down' | 'up-down';\n\nexport interface SelectOptionActionDetail {\n value: string;\n anchorId: string;\n originalEvent: MouseEvent;\n}\n\nexport interface SelectOptionSubtextActionDetail {\n value: string;\n actionValue: string;\n originalEvent: MouseEvent;\n}\n\nconst ICON_SIZE: Record<SelectSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst OPTION_ACTION_SIZE: Record<SelectSize, 'md' | 'sm' | 'xs'> = {\n lg: 'md',\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\n};\n\nlet selectId = 0;\n\n@Component({\n tag: 'ds-select',\n styleUrl: 'Select.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Select {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Flat choices. Assign arrays through the JavaScript property. */\n @Prop() options: SelectOption[] = [];\n /** Grouped choices; takes precedence over options. Assign through JavaScript. */\n @Prop() sections: SelectSection[] = [];\n /** Enable independent multi-value selection while keeping the popup open. */\n @Prop({ reflect: true }) multiple: boolean = false;\n /** Selected scalar or array value according to `multiple`. */\n @Prop({ mutable: true }) value: SelectValue = '';\n /** Controlled popup visibility. */\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n /** Native form field name. */\n @Prop({ reflect: true }) name: string | undefined;\n /** Native disabled state. */\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Require one valid selected value. */\n @Prop({ reflect: true }) required: boolean = false;\n /** Validation message used when required is missing. */\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n /** Trigger text shown when no valid value is selected. */\n @Prop() placeholder: string = 'Select';\n /** Optional scalar-mode trigger text that does not change the selected option label. */\n @Prop() triggerLabel: string | undefined;\n /** Present triggerLabel with placeholder emphasis while preserving the selected value. */\n @Prop() triggerLabelPlaceholder: boolean = false;\n /** Show a supplemental notification dot beside the trigger label. */\n @Prop() dot: boolean = false;\n /** Control density. */\n @Prop() size: SelectSize = 'md';\n /** Width fit — hug content (default) or fill the parent. */\n @Prop() width: SelectWidth = 'hug';\n /** Align the popup's choice edge to the trigger start or end edge. */\n @Prop() popupAlign: SelectPopupAlign = 'start';\n /** Explicit collision owner; otherwise the nearest data-ds-overlay-boundary ancestor is used. */\n @Prop() boundary: HTMLElement | undefined;\n /** Shared inactive treatment; removes interaction and form submission. */\n @Prop() isInactive: boolean = false;\n /** Replace the prefix with a loader and disable option interaction. */\n @Prop() isLoading: boolean = false;\n /** Show the selected interaction fill when a valid value exists. */\n @Prop() activeFill: boolean = true;\n /**\n * Opt into table-caption icon-only chrome below 900px. The trigger omits its\n * visible label and chevron; keep an accessible name via aria-label.\n */\n @Prop({ reflect: true }) collapseLabel: boolean = false;\n /** Show the surface-aware inset border, including focused and invalid strokes. */\n @Prop() hasBorder: boolean = true;\n /** Optional trigger prefix icon name. */\n @Prop() icon: string | undefined;\n /** Trailing choice indicator. Use up-down for compact value steppers such as page size. */\n @Prop() indicator: SelectIndicator = 'down';\n /** Show the clear footer action when a value exists. */\n @Prop() allowClear: boolean = true;\n /** Localized clear action label. */\n @Prop() clearLabel: string = 'Clear';\n /** Localized noun displayed after the selected count in multiple mode. */\n @Prop() selectedLabel: string = 'selected';\n /** Optional text action shown in the popup footer instead of the clear action. */\n @Prop() footerActionLabel: string | undefined;\n /** Show immediate local filtering over option labels, subtext, and section headings. */\n @Prop() searchable: boolean = false;\n /** Localized search-field placeholder and accessible name. */\n @Prop() searchPlaceholder: string = 'Search';\n /** Localized empty-filter result text. */\n @Prop() noResultsText: string = 'No results found';\n /** Accessible loading status label. */\n @Prop() loadingLabel: string = 'Loading';\n /** Actual parent surface context; omit on primary and secondary surfaces. */\n @Prop() background: SelectBackground | undefined;\n /** Show invalid visual state. */\n @Prop() error: boolean = false;\n /** Error text rendered below the trigger when error is true. */\n @Prop() errorMessage: string | undefined;\n /** ID applied to the internal combobox trigger for external labels. */\n @Prop() inputId: string | undefined;\n /** Direct accessible name when no external label is available. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** IDs of elements that label the combobox. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n /** Additional IDs that describe the combobox. */\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n /** Emitted after selection or clearing with the next scalar or array value. */\n @Event() dsChange!: EventEmitter<SelectValue>;\n /** Emitted after the footer clear action. */\n @Event() dsClear!: EventEmitter<void>;\n /** Emitted whenever popup visibility changes. */\n @Event() dsOpenChange!: EventEmitter<boolean>;\n /** Emitted when the optional popup footer action is activated. */\n @Event() dsFooterAction!: EventEmitter<void>;\n /** Emitted when an option's contextual ellipsis action is activated. */\n @Event() dsOptionAction!: EventEmitter<SelectOptionActionDetail>;\n /** Emitted when an option's supporting text action is activated. */\n @Event() dsOptionSubtextAction!: EventEmitter<SelectOptionSubtextActionDetail>;\n\n @State() private activeIndex = -1;\n @State() private searchTerm = '';\n @State() private formDisabled = false;\n @State() private position = { x: 0, y: 0 };\n @State() private positionReady = false;\n @State() private focusRingVisible = false;\n @State() private compactFooterSummary = false;\n @State() private captionCompact = false;\n\n private readonly generatedId = `ds-select-${++selectId}`;\n private readonly listboxId = `${this.generatedId}-listbox`;\n private readonly errorId = `${this.generatedId}-error`;\n private initialValue: SelectValue = '';\n private readonly controller = this.createController();\n private footerContentElement?: HTMLDivElement;\n private footerSummaryMeasureElement?: HTMLElement;\n private footerClearElement?: HTMLButtonElement;\n private footerResizeObserver?: ResizeObserver;\n private observedFooterContentElement?: HTMLDivElement;\n private footerMeasurementFrame?: number;\n private captionCompactDisconnect: (() => void) | undefined;\n private hasLoaded = false;\n\n private createController() {\n // eslint-disable-next-line @typescript-eslint/no-this-alias -- adapter getters preserve reactive component ownership without inheritance.\n const owner = this;\n return new SelectController<SelectOption>({\n get host() {\n return owner.el;\n },\n generatedId: owner.generatedId,\n get options() {\n return owner.visibleOptions;\n },\n get searchable() {\n return owner.searchable;\n },\n get isLoading() {\n return owner.isLoading;\n },\n get isDisabled() {\n return owner.isDisabled;\n },\n get preferredIndex() {\n if (owner.multiple) return -1;\n return owner.visibleOptions.findIndex(\n option => option.value === owner.scalarValue && !option.isInactive\n );\n },\n get popupAlign() {\n return owner.popupAlign;\n },\n get boundary() {\n return owner.boundary;\n },\n get open() {\n return owner.open;\n },\n set open(value) {\n owner.open = value;\n },\n get activeIndex() {\n return owner.activeIndex;\n },\n set activeIndex(value) {\n owner.activeIndex = value;\n },\n get searchTerm() {\n return owner.searchTerm;\n },\n set searchTerm(value) {\n owner.searchTerm = value;\n },\n get focusRingVisible() {\n return owner.focusRingVisible;\n },\n set focusRingVisible(value) {\n owner.focusRingVisible = value;\n },\n get position() {\n return owner.position;\n },\n set position(value) {\n owner.position = value;\n },\n get positionReady() {\n return owner.positionReady;\n },\n set positionReady(value) {\n owner.positionReady = value;\n },\n selectOption: option => owner.selectOption(option),\n });\n }\n\n componentWillLoad() {\n this.value = this.normalizeValueForMode(this.value);\n this.initialValue = Array.isArray(this.value) ? [...this.value] : this.value;\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.controller.connect();\n this.observeFooterContent();\n this.syncCaptionCompactObserver();\n }\n\n connectedCallback() {\n if (!this.hasLoaded) return;\n this.controller.connect();\n this.observeFooterContent();\n this.syncCaptionCompactObserver();\n }\n\n componentDidRender() {\n this.observeFooterContent();\n this.scheduleFooterSummaryMeasurement();\n }\n\n disconnectedCallback() {\n this.controller.disconnect();\n this.footerResizeObserver?.disconnect();\n this.observedFooterContentElement = undefined;\n if (this.footerMeasurementFrame !== undefined) {\n cancelAnimationFrame(this.footerMeasurementFrame);\n this.footerMeasurementFrame = undefined;\n }\n this.disconnectCaptionCompactObserver();\n }\n\n @Watch('collapseLabel')\n onCollapseLabelChange() {\n this.syncCaptionCompactObserver();\n }\n\n @Watch('value')\n @Watch('multiple')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n @Watch('name')\n @Watch('options')\n @Watch('sections')\n syncFormValue() {\n if (\n (this.multiple && !Array.isArray(this.value)) ||\n (!this.multiple && Array.isArray(this.value))\n ) {\n this.value = this.normalizeValueForMode(this.value);\n }\n const inactive = this.isDisabled;\n if (this.multiple) {\n const values = this.resolvedValues;\n setRepeatedFormControlValue(this.internals, this.name, values, { inactive });\n } else {\n const value = this.hasSelection ? this.scalarValue : '';\n setFormControlValue(this.internals, value, { inactive });\n }\n const missing = this.required && !inactive && !this.hasSelection;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n this.controller.optionsChanged();\n }\n\n @Watch('isLoading')\n onLoadingChange() {\n this.controller.loadingChanged();\n }\n\n @Watch('popupAlign')\n @Watch('boundary')\n onPopupAlignChange() {\n this.controller.positionChanged();\n }\n\n @Watch('open')\n onOpenChange(open: boolean) {\n this.dsOpenChange.emit(open);\n this.controller.openChanged(open);\n }\n\n @Watch('searchTerm')\n onSearchTermChange() {\n this.controller.searchChanged();\n }\n\n @Watch('activeIndex')\n onActiveIndexChange() {\n this.controller.activeIndexChanged();\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.normalizeValueForMode(this.initialValue);\n this.closePopup();\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n if (!this.multiple) {\n this.value = restoreStringFormState(state);\n return;\n }\n this.value = restoreStringArrayFormState(state);\n }\n\n @Method()\n async setFocus() {\n this.controller.setFocus();\n }\n\n private get isDisabled(): boolean {\n return this.disabled || this.isInactive || this.formDisabled;\n }\n\n private normalizeValueForMode(value: SelectValue): SelectValue {\n if (this.multiple) {\n return Array.isArray(value) ? [...value] : value ? [value] : [];\n }\n return Array.isArray(value) ? (value[0] ?? '') : value;\n }\n\n private get allSections(): SelectSection[] {\n return resolveChoiceSections(this.options, this.sections);\n }\n\n private get allOptions(): SelectOption[] {\n return flattenChoiceSections(this.allSections);\n }\n\n private get visibleSections(): SelectSection[] {\n return this.searchable\n ? filterChoiceSections(this.allSections, this.searchTerm)\n : this.allSections;\n }\n\n private get visibleOptions(): SelectOption[] {\n return flattenChoiceSections(this.visibleSections);\n }\n\n private get scalarValue(): string {\n return typeof this.value === 'string' ? this.value : '';\n }\n\n private get resolvedValues(): string[] {\n if (!this.multiple || !Array.isArray(this.value)) return [];\n const valid = new Set(this.allOptions.map(option => option.value));\n return [...new Set(this.value.filter(value => valid.has(value)))];\n }\n\n private get selectedOption(): SelectOption | undefined {\n if (this.multiple) return undefined;\n return this.allOptions.find(option => option.value === this.scalarValue);\n }\n\n private get hasSelection(): boolean {\n return this.multiple ? this.resolvedValues.length > 0 : Boolean(this.selectedOption);\n }\n\n private get activeOptionId(): string | undefined {\n return this.controller.activeOptionId;\n }\n\n private get popupRole(): 'listbox' | 'grid' {\n return this.allOptions.some(option => Boolean(option.action)) ? 'grid' : 'listbox';\n }\n\n private observeFooterContent() {\n const content =\n this.open && this.multiple && this.hasSelection ? this.footerContentElement : undefined;\n if (!content?.isConnected) {\n this.footerResizeObserver?.disconnect();\n this.observedFooterContentElement = undefined;\n return;\n }\n if (this.observedFooterContentElement === content) return;\n\n this.footerResizeObserver?.disconnect();\n if (typeof ResizeObserver !== 'undefined') {\n this.footerResizeObserver = new ResizeObserver(() => this.scheduleFooterSummaryMeasurement());\n this.footerResizeObserver.observe(content);\n }\n this.observedFooterContentElement = content;\n }\n\n private scheduleFooterSummaryMeasurement() {\n if (\n !this.open ||\n !this.multiple ||\n !this.hasSelection ||\n this.footerMeasurementFrame !== undefined\n ) {\n return;\n }\n this.footerMeasurementFrame = requestAnimationFrame(() => {\n this.footerMeasurementFrame = undefined;\n this.measureFooterSummary();\n });\n }\n\n private measureFooterSummary() {\n const content = this.footerContentElement;\n const summary = this.footerSummaryMeasureElement;\n const clear = this.footerClearElement;\n if (!content?.isConnected || !summary?.isConnected || !clear?.isConnected) return;\n\n const styles = getComputedStyle(content);\n const innerWidth =\n content.clientWidth -\n Number.parseFloat(styles.paddingInlineStart || '0') -\n Number.parseFloat(styles.paddingInlineEnd || '0');\n const requiredWidth =\n summary.getBoundingClientRect().width +\n clear.getBoundingClientRect().width +\n Number.parseFloat(styles.columnGap || '0');\n const nextCompact = requiredWidth > innerWidth + 0.5;\n if (nextCompact !== this.compactFooterSummary) {\n this.compactFooterSummary = nextCompact;\n }\n }\n\n private closePopup(restoreFocus = false) {\n this.controller.closePopup(restoreFocus);\n }\n\n private openPopup(focusVisible: boolean, edge?: 'first' | 'last') {\n this.controller.openPopup(focusVisible, edge);\n }\n\n private selectOption(option: SelectOption) {\n if (option.isInactive || this.isLoading) return;\n if (this.multiple) {\n const current = this.resolvedValues;\n const next = current.includes(option.value)\n ? current.filter(value => value !== option.value)\n : [...current, option.value];\n this.value = next;\n this.dsChange.emit([...next]);\n return;\n }\n this.value = option.value;\n this.dsChange.emit(option.value);\n this.controller.closePopup(true);\n }\n\n private clearSelection = (event: Event) => {\n event.preventDefault();\n event.stopPropagation();\n this.value = this.multiple ? [] : '';\n this.dsChange.emit(this.multiple ? [] : '');\n this.dsClear.emit();\n if (!this.multiple) {\n const enabled = enabledChoiceIndexes(this.visibleOptions);\n this.activeIndex = enabled[0] ?? -1;\n }\n this.controller.focusSearchOrTrigger();\n };\n\n private activateFooterAction = (event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n this.dsFooterAction.emit();\n this.closePopup();\n };\n\n private readonly handleListKeyDown = this.controller.handleListKeyDown;\n private readonly handleTriggerKeyDown = this.controller.handleTriggerKeyDown;\n\n private renderOption(\n option: SelectOption,\n index: number,\n usesIcons: boolean,\n usesSubtext: boolean\n ) {\n const selected = this.multiple\n ? this.resolvedValues.includes(option.value)\n : option.value === this.scalarValue;\n const active = index === this.activeIndex;\n const actionId = `${this.generatedId}-option-action-${index}`;\n return (\n <ChoiceOptionRow\n size={this.size}\n id={`${this.generatedId}-option-${index}`}\n option={option}\n selected={selected}\n active={active}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={usesSubtext}\n actionOpen={option.action?.expanded}\n popupRole={this.popupRole}\n leading={\n this.multiple ? (\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-checkbox\n class=\"select-option__checkbox\"\n label=\"\"\n size={this.size}\n checked={selected}\n presentation\n />\n </span>\n ) : usesIcons && option.icon ? (\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name={option.icon} size={this.size} color=\"inherit\" />\n </span>\n ) : undefined\n }\n supporting={\n option.subtextActions?.length ? (\n <span class=\"ds-choice-item__subtext ds-control-label-box select-option__subtext-actions\">\n {option.subtextActions.map((action, actionIndex) => [\n actionIndex > 0 ? (\n <ds-text\n class=\"select-option__subtext-action-separator\"\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"inherit\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\n ) : null,\n <button\n type=\"button\"\n class={{\n 'select-option__subtext-action': true,\n 'select-option__subtext-action--negative': action.tone === 'negative',\n 'ds-text-action': true,\n 'ds-focus-ring': true,\n }}\n onKeyDown={(event: KeyboardEvent) => event.stopPropagation()}\n onClick={(event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n this.dsOptionSubtextAction.emit({\n value: option.value,\n actionValue: action.value,\n originalEvent: event,\n });\n }}\n >\n <ds-text\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"inherit\"\n >\n {action.label}\n </ds-text>\n </button>,\n ])}\n </span>\n ) : undefined\n }\n action={\n option.action ? (\n <ds-tooltip label={option.action.label} side=\"left\" size=\"sm\">\n <ds-button-unfilled\n id={actionId}\n variant=\"icon\"\n size={OPTION_ACTION_SIZE[this.size]}\n icon=\"Ellipses\"\n rounded\n hasBorder={false}\n activeFill={false}\n ariaLabel={option.action.label}\n haspopup=\"menu\"\n controls={option.action.controls}\n expanded={option.action.expanded}\n onKeyDown={event => {\n event.stopPropagation();\n if (event.key === 'Escape' || event.key === 'ArrowLeft') {\n event.preventDefault();\n this.controller.focusSearchOrTrigger();\n }\n }}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n event.stopPropagation();\n this.dsOptionAction.emit({\n value: option.value,\n anchorId: actionId,\n originalEvent: event.detail,\n });\n }}\n />\n </ds-tooltip>\n ) : undefined\n }\n onHover={() => {\n this.focusRingVisible = false;\n this.activeIndex = index;\n }}\n onSelect={() => this.selectOption(option)}\n />\n );\n }\n\n private get captionIconOnly(): boolean {\n return this.collapseLabel && this.captionCompact && Boolean(this.icon || this.isLoading);\n }\n\n private syncCaptionCompactObserver(): void {\n this.disconnectCaptionCompactObserver();\n if (!this.collapseLabel) {\n if (this.captionCompact) this.captionCompact = false;\n return;\n }\n this.captionCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.captionCompact !== compact) this.captionCompact = compact;\n });\n }\n\n private disconnectCaptionCompactObserver(): void {\n this.captionCompactDisconnect?.();\n this.captionCompactDisconnect = undefined;\n }\n\n private renderCaptionTooltip(trigger: VNode, label: string): VNode {\n if (!this.collapseLabel) return trigger;\n return (\n <ds-tooltip label={label} side=\"top\" size=\"sm\">\n {trigger}\n </ds-tooltip>\n );\n }\n\n render() {\n const inactive = this.isDisabled;\n const showPlaceholder = !this.hasSelection;\n const count = this.resolvedValues.length;\n const label = this.multiple\n ? `${this.placeholder}${count > 0 ? ` · ${count}` : ''}`\n : showPlaceholder\n ? this.placeholder\n : (this.triggerLabel ?? this.selectedOption?.label);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n const usesOptionIcons = !this.multiple && choiceListUsesIcons(this.allOptions);\n const usesOptionSubtext = choiceListUsesSubtext(this.allOptions);\n const showError = this.error && Boolean(this.errorMessage);\n const describedBy =\n [this.ariaDescribedby, showError ? this.errorId : undefined].filter(Boolean).join(' ') ||\n undefined;\n const popupStyle = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.position.x)}px, ${Math.round(this.position.y)}px)`,\n zIndex: 'var(--dimension-z-index-floating)',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n let flatIndex = 0;\n\n return (\n <Host\n class={{\n 'select-host': true,\n 'ds-select-trigger-host': true,\n 'select-host--multiple': this.multiple,\n 'ds-field-stack': true,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n 'ds-table-caption-control': this.collapseLabel,\n 'ds-table-caption-control--compact': this.captionIconOnly,\n ...controlWidthClass(this.width),\n [`select-host--background-${this.background}`]: !!this.background,\n [`ds-select-trigger-host--background-${this.background}`]: !!this.background,\n }}\n >\n {this.renderCaptionTooltip(\n <button\n ref={element => {\n this.controller.setTriggerElement((element as HTMLButtonElement) ?? null);\n }}\n id={this.inputId ?? this.generatedId}\n type=\"button\"\n class={{\n trigger: true,\n 'ds-control-frame': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': !inactive && this.activeFill && this.hasSelection,\n 'trigger--expanded': !inactive && this.open,\n 'trigger--bordered': this.hasBorder,\n 'trigger--placeholder': showPlaceholder && !this.multiple,\n 'trigger--has-value': this.hasSelection,\n 'trigger--label-placeholder':\n !this.multiple && Boolean(this.triggerLabel) && this.triggerLabelPlaceholder,\n 'wrapper--error': this.hasBorder && this.error,\n [`ds-control--${this.size}`]: true,\n ...choiceBackgroundClassMap(this.background),\n }}\n disabled={inactive}\n role=\"combobox\"\n aria-haspopup={this.popupRole}\n aria-expanded={String(this.open)}\n aria-controls={this.open ? this.listboxId : undefined}\n aria-activedescendant={this.open && !this.searchable ? this.activeOptionId : undefined}\n aria-label={this.ariaLabel ?? (this.captionIconOnly ? label : undefined)}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.error ? 'true' : undefined}\n aria-required={this.required || undefined}\n aria-busy={this.isLoading ? 'true' : undefined}\n onClick={() => (this.open ? this.closePopup() : this.openPopup(false))}\n onKeyDown={event => {\n const action = this.visibleOptions[this.activeIndex]?.action;\n if (\n this.open &&\n action &&\n (event.key === 'ArrowRight' || (event.key === 'F10' && event.shiftKey))\n ) {\n event.preventDefault();\n document\n .getElementById(`${this.generatedId}-option-action-${this.activeIndex}`)\n ?.querySelector<HTMLElement>('button')\n ?.focus();\n return;\n }\n this.handleTriggerKeyDown(event);\n }}\n >\n {(this.icon || this.isLoading) && (\n <span\n class=\"trigger__prefix ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n {this.isLoading ? (\n <ds-loader size={iconSize} color=\"inherit\" />\n ) : (\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n )}\n </span>\n )}\n {this.captionIconOnly ? null : (\n <span\n class={{\n 'trigger__label-box': true,\n 'ds-interaction-fill__content': true,\n }}\n >\n <span\n class={{\n 'trigger__label-content': true,\n 'ds-control-label-box': true,\n 'ds-control-label-dot': this.dot,\n 'trigger__label-content--dot': this.dot,\n }}\n >\n <ds-text\n class=\"trigger__label\"\n as=\"span\"\n variant={textVariant}\n color=\"inherit\"\n lineTruncation={1}\n >\n {label}\n </ds-text>\n {this.dot && (\n <ds-badge\n class=\"trigger__dot ds-control-label-dot__badge\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n aria-hidden=\"true\"\n />\n )}\n </span>\n </span>\n )}\n {this.captionIconOnly ? null : (\n <span\n class=\"trigger__chevron ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon\n name={this.indicator === 'up-down' ? 'ChevronUpDown' : 'ChevronDown'}\n size={iconSize}\n color=\"inherit\"\n />\n </span>\n )}\n </button>,\n this.captionIconOnly ? this.ariaLabel?.trim() || this.placeholder : ''\n )}\n\n {this.open && (\n <div\n popover=\"manual\"\n ref={element => {\n this.controller.setPopupElement((element as HTMLDivElement) ?? null);\n }}\n class=\"select-popup ds-choice-popup\"\n style={popupStyle}\n >\n {this.searchable && (\n <ChoiceSearch\n size={this.size}\n value={this.searchTerm}\n placeholder={this.searchPlaceholder}\n controls={this.listboxId}\n activeDescendant={this.activeOptionId}\n inputRef={this.controller.setSearchElement}\n clearLabel={this.clearLabel}\n onValueChange={value => {\n this.searchTerm = value;\n }}\n onKeyDown={event => this.handleListKeyDown(event)}\n />\n )}\n <div\n id={this.listboxId}\n class=\"ds-choice-list\"\n role={this.popupRole}\n aria-multiselectable={this.multiple ? 'true' : undefined}\n aria-label={this.ariaLabel ?? this.placeholder}\n aria-busy={this.isLoading ? 'true' : undefined}\n >\n {this.isLoading ? (\n <div\n class=\"ds-choice-loading ds-empty-region\"\n role={this.popupRole === 'grid' ? 'row' : 'option'}\n aria-selected=\"false\"\n aria-disabled=\"true\"\n aria-label={this.loadingLabel}\n aria-live=\"polite\"\n >\n {this.popupRole === 'grid' ? (\n <span role=\"gridcell\">\n <ds-loader size={this.size} color=\"inherit\" />\n </span>\n ) : (\n <ds-loader size={this.size} color=\"inherit\" />\n )}\n </div>\n ) : this.visibleOptions.length === 0 ? (\n <div\n class=\"ds-choice-empty ds-empty-region\"\n role={this.popupRole === 'grid' ? 'row' : 'option'}\n aria-selected=\"false\"\n aria-disabled=\"true\"\n aria-label={this.noResultsText}\n aria-live=\"polite\"\n >\n {this.popupRole === 'grid' ? (\n <span role=\"gridcell\">\n <ds-empty-state body={this.noResultsText} />\n </span>\n ) : (\n <ds-empty-state body={this.noResultsText} />\n )}\n </div>\n ) : (\n this.visibleSections.map((section, sectionIndex) => (\n <div\n class={{\n 'ds-choice-section': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--sm': true,\n 'ds-choice-section--divided': !!section.divider,\n 'ds-choice-section--headed-after-first':\n sectionIndex > 0 && Boolean(section.header),\n }}\n role={\n this.popupRole === 'grid' ? 'rowgroup' : section.header ? 'group' : undefined\n }\n aria-label={section.header}\n >\n {section.header && this.popupRole === 'grid' ? (\n <ds-text\n class={`ds-choice-section__header ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n emphasis\n color=\"primary\"\n role=\"row\"\n >\n <span class=\"ds-choice-section__header-label\" role=\"gridcell\">\n {section.header}\n </span>\n </ds-text>\n ) : section.header ? (\n <ds-text\n class={`ds-choice-section__header ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n <span class=\"ds-choice-section__header-label\">{section.header}</span>\n </ds-text>\n ) : null}\n {section.options.map(option =>\n this.renderOption(option, flatIndex++, usesOptionIcons, usesOptionSubtext)\n )}\n </div>\n ))\n )}\n </div>\n {this.footerActionLabel && !this.isLoading ? (\n <ChoiceActionFooter\n size={this.size}\n label={this.footerActionLabel}\n onAction={this.activateFooterAction}\n />\n ) : this.allowClear && this.hasSelection && !this.isLoading ? (\n <ChoiceFooter\n size={this.size}\n summary={this.multiple ? `${count} ${this.selectedLabel}` : undefined}\n compactSummary={this.multiple ? String(count) : undefined}\n useCompactSummary={this.compactFooterSummary}\n contentRef={element => {\n this.footerContentElement = element ?? undefined;\n }}\n summaryMeasureRef={element => {\n this.footerSummaryMeasureElement = element ?? undefined;\n }}\n clearRef={element => {\n this.footerClearElement = element ?? undefined;\n }}\n clearLabel={this.clearLabel}\n onClear={this.clearSelection}\n />\n ) : null}\n </div>\n )}\n\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
|
|
980
|
+
"content": "import {\n AttachInternals,\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n type VNode,\n Watch,\n} from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_SUPPORTING_TEXT_VARIANT,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringArrayFormState,\n restoreStringFormState,\n setFormControlValue,\n setRepeatedFormControlValue,\n setRequiredValidity,\n type ControlWidth,\n} from '../../utils';\nimport {\n ChoiceActionFooter,\n ChoiceFooter,\n ChoiceOptionRow,\n ChoiceSearch,\n} from '../../utils/choice-list-parts';\nimport {\n choiceBackgroundClassMap,\n choiceListUsesIcons,\n choiceListUsesSubtext,\n enabledChoiceIndexes,\n filterChoiceSections,\n flattenChoiceSections,\n resolveChoiceSections,\n type ChoiceBackground,\n type ChoiceOption,\n type ChoiceSection,\n} from '../../utils/choice-list';\nimport { SelectController } from '../../utils/select-controller';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\n\nexport type SelectOption = ChoiceOption;\nexport type SelectSection = ChoiceSection;\nexport type SelectBackground = ChoiceBackground;\nexport type SelectSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type SelectWidth = ControlWidth;\nexport type SelectPopupAlign = 'start' | 'end';\nexport type SelectValue = string | string[];\nexport type SelectIndicator = 'down' | 'up-down';\n\nexport interface SelectOptionActionDetail {\n value: string;\n anchorId: string;\n originalEvent: MouseEvent;\n}\n\nexport interface SelectOptionSubtextActionDetail {\n value: string;\n actionValue: string;\n originalEvent: MouseEvent;\n}\n\nconst ICON_SIZE: Record<SelectSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst OPTION_ACTION_SIZE: Record<SelectSize, 'md' | 'sm' | 'xs'> = {\n lg: 'md',\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\n};\n\nlet selectId = 0;\n\n@Component({\n tag: 'ds-select',\n styleUrl: 'Select.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Select {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Flat choices. Assign arrays through the JavaScript property. */\n @Prop() options: SelectOption[] = [];\n /** Grouped choices; takes precedence over options. Assign through JavaScript. */\n @Prop() sections: SelectSection[] = [];\n /** Enable independent multi-value selection while keeping the popup open. */\n @Prop({ reflect: true }) multiple: boolean = false;\n /** Selected scalar or array value according to `multiple`. */\n @Prop({ mutable: true }) value: SelectValue = '';\n /** Controlled popup visibility. */\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n /** Native form field name. */\n @Prop({ reflect: true }) name: string | undefined;\n /** Native disabled state. */\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Require one valid selected value. */\n @Prop({ reflect: true }) required: boolean = false;\n /** Validation message used when required is missing. */\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n /** Trigger text shown when no valid value is selected. */\n @Prop() placeholder: string = 'Select';\n /** Optional scalar-mode trigger text that does not change the selected option label. */\n @Prop() triggerLabel: string | undefined;\n /** Present triggerLabel with placeholder emphasis while preserving the selected value. */\n @Prop() triggerLabelPlaceholder: boolean = false;\n /** Show a supplemental notification dot beside the trigger label. */\n @Prop() dot: boolean = false;\n /** Control density. */\n @Prop() size: SelectSize = 'md';\n /** Width fit — hug content (default) or fill the parent. */\n @Prop() width: SelectWidth = 'hug';\n /** Align the popup's choice edge to the trigger start or end edge. */\n @Prop() popupAlign: SelectPopupAlign = 'start';\n /** Explicit collision owner; otherwise the nearest data-ds-overlay-boundary ancestor is used. */\n @Prop() boundary: HTMLElement | undefined;\n /** Shared inactive treatment; removes interaction and form submission. */\n @Prop() isInactive: boolean = false;\n /** Replace the prefix with a loader and disable option interaction. */\n @Prop() isLoading: boolean = false;\n /** Show the selected interaction fill when a valid value exists. */\n @Prop() activeFill: boolean = true;\n /**\n * Opt into table-caption icon-only chrome below 900px. The trigger omits its\n * visible label and chevron; keep an accessible name via aria-label.\n */\n @Prop({ reflect: true }) collapseLabel: boolean = false;\n /** Show the surface-aware inset border, including focused and invalid strokes. */\n @Prop() hasBorder: boolean = true;\n /** Optional trigger prefix icon name. */\n @Prop() icon: string | undefined;\n /** Trailing choice indicator. Use up-down for compact value steppers such as page size. */\n @Prop() indicator: SelectIndicator = 'down';\n /** Show the clear footer action when a value exists. */\n @Prop() allowClear: boolean = true;\n /** Localized clear action label. */\n @Prop() clearLabel: string = 'Clear';\n /** Localized noun displayed after the selected count in multiple mode. */\n @Prop() selectedLabel: string = 'selected';\n /** Optional text action shown in the popup footer instead of the clear action. */\n @Prop() footerActionLabel: string | undefined;\n /** Show immediate local filtering over option labels, subtext, and section headings. */\n @Prop() searchable: boolean = false;\n /** Localized search-field placeholder and accessible name. */\n @Prop() searchPlaceholder: string = 'Search';\n /** Localized empty-filter result text. */\n @Prop() noResultsText: string = 'No results found';\n /** Accessible loading status label. */\n @Prop() loadingLabel: string = 'Loading';\n /** Actual parent surface context; omit on primary and secondary surfaces. */\n @Prop() background: SelectBackground | undefined;\n /** Show invalid visual state. */\n @Prop() error: boolean = false;\n /** Error text rendered below the trigger when error is true. */\n @Prop() errorMessage: string | undefined;\n /** ID applied to the internal combobox trigger for external labels. */\n @Prop() inputId: string | undefined;\n /** Direct accessible name when no external label is available. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** IDs of elements that label the combobox. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n /** Additional IDs that describe the combobox. */\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n /** Emitted after selection or clearing with the next scalar or array value. */\n @Event() dsChange!: EventEmitter<SelectValue>;\n /** Emitted after the footer clear action. */\n @Event() dsClear!: EventEmitter<void>;\n /** Emitted whenever popup visibility changes. */\n @Event() dsOpenChange!: EventEmitter<boolean>;\n /** Emitted when the optional popup footer action is activated. */\n @Event() dsFooterAction!: EventEmitter<void>;\n /** Emitted when an option's contextual ellipsis action is activated. */\n @Event() dsOptionAction!: EventEmitter<SelectOptionActionDetail>;\n /** Emitted when an option's supporting text action is activated. */\n @Event() dsOptionSubtextAction!: EventEmitter<SelectOptionSubtextActionDetail>;\n\n @State() private activeIndex = -1;\n @State() private searchTerm = '';\n @State() private formDisabled = false;\n @State() private position = { x: 0, y: 0 };\n @State() private positionReady = false;\n @State() private focusRingVisible = false;\n @State() private compactFooterSummary = false;\n @State() private captionCompact = false;\n\n private readonly generatedId = `ds-select-${++selectId}`;\n private readonly listboxId = `${this.generatedId}-listbox`;\n private readonly errorId = `${this.generatedId}-error`;\n private initialValue: SelectValue = '';\n private readonly controller = this.createController();\n private footerContentElement?: HTMLDivElement;\n private footerSummaryMeasureElement?: HTMLElement;\n private footerClearElement?: HTMLButtonElement;\n private footerResizeObserver?: ResizeObserver;\n private observedFooterContentElement?: HTMLDivElement;\n private footerMeasurementFrame?: number;\n private captionCompactDisconnect: (() => void) | undefined;\n private hasLoaded = false;\n\n private createController() {\n // eslint-disable-next-line @typescript-eslint/no-this-alias -- adapter getters preserve reactive component ownership without inheritance.\n const owner = this;\n return new SelectController<SelectOption>({\n get host() {\n return owner.el;\n },\n generatedId: owner.generatedId,\n get options() {\n return owner.visibleOptions;\n },\n get searchable() {\n return owner.searchable;\n },\n get isLoading() {\n return owner.isLoading;\n },\n get isDisabled() {\n return owner.isDisabled;\n },\n get preferredIndex() {\n if (owner.multiple) return -1;\n return owner.visibleOptions.findIndex(\n option => option.value === owner.scalarValue && !option.isInactive\n );\n },\n get popupAlign() {\n return owner.popupAlign;\n },\n get boundary() {\n return owner.boundary;\n },\n get open() {\n return owner.open;\n },\n set open(value) {\n owner.open = value;\n },\n get activeIndex() {\n return owner.activeIndex;\n },\n set activeIndex(value) {\n owner.activeIndex = value;\n },\n get searchTerm() {\n return owner.searchTerm;\n },\n set searchTerm(value) {\n owner.searchTerm = value;\n },\n get focusRingVisible() {\n return owner.focusRingVisible;\n },\n set focusRingVisible(value) {\n owner.focusRingVisible = value;\n },\n get position() {\n return owner.position;\n },\n set position(value) {\n owner.position = value;\n },\n get positionReady() {\n return owner.positionReady;\n },\n set positionReady(value) {\n owner.positionReady = value;\n },\n selectOption: option => owner.selectOption(option),\n });\n }\n\n componentWillLoad() {\n this.value = this.normalizeValueForMode(this.value);\n this.initialValue = Array.isArray(this.value) ? [...this.value] : this.value;\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.controller.connect();\n this.observeFooterContent();\n this.syncCaptionCompactObserver();\n }\n\n connectedCallback() {\n if (!this.hasLoaded) return;\n this.controller.connect();\n this.observeFooterContent();\n this.syncCaptionCompactObserver();\n }\n\n componentDidRender() {\n this.observeFooterContent();\n this.scheduleFooterSummaryMeasurement();\n }\n\n disconnectedCallback() {\n this.controller.disconnect();\n this.footerResizeObserver?.disconnect();\n this.observedFooterContentElement = undefined;\n if (this.footerMeasurementFrame !== undefined) {\n cancelAnimationFrame(this.footerMeasurementFrame);\n this.footerMeasurementFrame = undefined;\n }\n this.disconnectCaptionCompactObserver();\n }\n\n @Watch('collapseLabel')\n onCollapseLabelChange() {\n this.syncCaptionCompactObserver();\n }\n\n @Watch('value')\n @Watch('multiple')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n @Watch('name')\n @Watch('options')\n @Watch('sections')\n syncFormValue() {\n if (\n (this.multiple && !Array.isArray(this.value)) ||\n (!this.multiple && Array.isArray(this.value))\n ) {\n this.value = this.normalizeValueForMode(this.value);\n }\n const inactive = this.isDisabled;\n if (this.multiple) {\n const values = this.resolvedValues;\n setRepeatedFormControlValue(this.internals, this.name, values, { inactive });\n } else {\n const value = this.hasSelection ? this.scalarValue : '';\n setFormControlValue(this.internals, value, { inactive });\n }\n const missing = this.required && !inactive && !this.hasSelection;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n this.controller.optionsChanged();\n }\n\n @Watch('isLoading')\n onLoadingChange() {\n this.controller.loadingChanged();\n }\n\n @Watch('popupAlign')\n @Watch('boundary')\n onPopupAlignChange() {\n this.controller.positionChanged();\n }\n\n @Watch('open')\n onOpenChange(open: boolean) {\n this.dsOpenChange.emit(open);\n this.controller.openChanged(open);\n }\n\n @Watch('searchTerm')\n onSearchTermChange() {\n this.controller.searchChanged();\n }\n\n @Watch('activeIndex')\n onActiveIndexChange() {\n this.controller.activeIndexChanged();\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.normalizeValueForMode(this.initialValue);\n this.closePopup();\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n if (!this.multiple) {\n this.value = restoreStringFormState(state);\n return;\n }\n this.value = restoreStringArrayFormState(state);\n }\n\n @Method()\n async setFocus() {\n this.controller.setFocus();\n }\n\n private get isDisabled(): boolean {\n return this.disabled || this.isInactive || this.formDisabled;\n }\n\n private normalizeValueForMode(value: SelectValue): SelectValue {\n if (this.multiple) {\n return Array.isArray(value) ? [...value] : value ? [value] : [];\n }\n return Array.isArray(value) ? (value[0] ?? '') : value;\n }\n\n private get allSections(): SelectSection[] {\n return resolveChoiceSections(this.options, this.sections);\n }\n\n private get allOptions(): SelectOption[] {\n return flattenChoiceSections(this.allSections);\n }\n\n private get visibleSections(): SelectSection[] {\n return this.searchable\n ? filterChoiceSections(this.allSections, this.searchTerm)\n : this.allSections;\n }\n\n private get visibleOptions(): SelectOption[] {\n return flattenChoiceSections(this.visibleSections);\n }\n\n private get scalarValue(): string {\n return typeof this.value === 'string' ? this.value : '';\n }\n\n private get resolvedValues(): string[] {\n if (!this.multiple || !Array.isArray(this.value)) return [];\n const valid = new Set(this.allOptions.map(option => option.value));\n return [...new Set(this.value.filter(value => valid.has(value)))];\n }\n\n private get selectedOption(): SelectOption | undefined {\n if (this.multiple) return undefined;\n return this.allOptions.find(option => option.value === this.scalarValue);\n }\n\n private get hasSelection(): boolean {\n return this.multiple ? this.resolvedValues.length > 0 : Boolean(this.selectedOption);\n }\n\n private get activeOptionId(): string | undefined {\n return this.controller.activeOptionId;\n }\n\n private get popupRole(): 'listbox' | 'grid' {\n return this.allOptions.some(option => Boolean(option.action)) ? 'grid' : 'listbox';\n }\n\n private observeFooterContent() {\n const content =\n this.open && this.multiple && this.hasSelection ? this.footerContentElement : undefined;\n if (!content?.isConnected) {\n this.footerResizeObserver?.disconnect();\n this.observedFooterContentElement = undefined;\n return;\n }\n if (this.observedFooterContentElement === content) return;\n\n this.footerResizeObserver?.disconnect();\n if (typeof ResizeObserver !== 'undefined') {\n this.footerResizeObserver = new ResizeObserver(() => this.scheduleFooterSummaryMeasurement());\n this.footerResizeObserver.observe(content);\n }\n this.observedFooterContentElement = content;\n }\n\n private scheduleFooterSummaryMeasurement() {\n if (\n !this.open ||\n !this.multiple ||\n !this.hasSelection ||\n this.footerMeasurementFrame !== undefined\n ) {\n return;\n }\n this.footerMeasurementFrame = requestAnimationFrame(() => {\n this.footerMeasurementFrame = undefined;\n this.measureFooterSummary();\n });\n }\n\n private measureFooterSummary() {\n const content = this.footerContentElement;\n const summary = this.footerSummaryMeasureElement;\n const clear = this.footerClearElement;\n if (!content?.isConnected || !summary?.isConnected || !clear?.isConnected) return;\n\n const styles = getComputedStyle(content);\n const innerWidth =\n content.clientWidth -\n Number.parseFloat(styles.paddingInlineStart || '0') -\n Number.parseFloat(styles.paddingInlineEnd || '0');\n const requiredWidth =\n summary.getBoundingClientRect().width +\n clear.getBoundingClientRect().width +\n Number.parseFloat(styles.columnGap || '0');\n const nextCompact = requiredWidth > innerWidth + 0.5;\n if (nextCompact !== this.compactFooterSummary) {\n this.compactFooterSummary = nextCompact;\n }\n }\n\n private closePopup(restoreFocus = false) {\n this.controller.closePopup(restoreFocus);\n }\n\n private openPopup(focusVisible: boolean, edge?: 'first' | 'last') {\n this.controller.openPopup(focusVisible, edge);\n }\n\n private selectOption(option: SelectOption) {\n if (option.isInactive || this.isLoading) return;\n if (this.multiple) {\n const current = this.resolvedValues;\n const next = current.includes(option.value)\n ? current.filter(value => value !== option.value)\n : [...current, option.value];\n this.value = next;\n this.dsChange.emit([...next]);\n return;\n }\n this.value = option.value;\n this.dsChange.emit(option.value);\n this.controller.closePopup(true);\n }\n\n private clearSelection = (event: Event) => {\n event.preventDefault();\n event.stopPropagation();\n this.value = this.multiple ? [] : '';\n this.dsChange.emit(this.multiple ? [] : '');\n this.dsClear.emit();\n if (!this.multiple) {\n const enabled = enabledChoiceIndexes(this.visibleOptions);\n this.activeIndex = enabled[0] ?? -1;\n }\n this.controller.focusSearchOrTrigger();\n };\n\n private activateFooterAction = (event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n this.dsFooterAction.emit();\n this.closePopup();\n };\n\n private readonly handleListKeyDown = this.controller.handleListKeyDown;\n private readonly handleTriggerKeyDown = this.controller.handleTriggerKeyDown;\n\n private renderOption(\n option: SelectOption,\n index: number,\n usesIcons: boolean,\n usesSubtext: boolean\n ) {\n const selected = this.multiple\n ? this.resolvedValues.includes(option.value)\n : option.value === this.scalarValue;\n const active = index === this.activeIndex;\n const actionId = `${this.generatedId}-option-action-${index}`;\n return (\n <ChoiceOptionRow\n size={this.size}\n id={`${this.generatedId}-option-${index}`}\n option={option}\n selected={selected}\n active={active}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={usesSubtext}\n actionOpen={option.action?.surfaceOpen ?? option.action?.expanded}\n popupRole={this.popupRole}\n leading={\n this.multiple ? (\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-checkbox\n class=\"select-option__checkbox\"\n label=\"\"\n size={this.size}\n checked={selected}\n presentation\n />\n </span>\n ) : usesIcons && option.icon ? (\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name={option.icon} size={this.size} color=\"inherit\" />\n </span>\n ) : undefined\n }\n supporting={\n option.subtextActions?.length ? (\n <span class=\"ds-choice-item__subtext ds-control-label-box select-option__subtext-actions\">\n {option.subtextActions.map((action, actionIndex) => [\n actionIndex > 0 ? (\n <ds-text\n class=\"select-option__subtext-action-separator\"\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"inherit\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\n ) : null,\n <button\n type=\"button\"\n class={{\n 'select-option__subtext-action': true,\n 'select-option__subtext-action--negative': action.tone === 'negative',\n 'ds-text-action': true,\n 'ds-focus-ring': true,\n }}\n onKeyDown={(event: KeyboardEvent) => event.stopPropagation()}\n onClick={(event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n this.dsOptionSubtextAction.emit({\n value: option.value,\n actionValue: action.value,\n originalEvent: event,\n });\n }}\n >\n <ds-text\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"inherit\"\n >\n {action.label}\n </ds-text>\n </button>,\n ])}\n </span>\n ) : undefined\n }\n action={\n option.action ? (\n <ds-tooltip label={option.action.label} side=\"left\" size=\"sm\">\n <ds-button-unfilled\n id={actionId}\n variant=\"icon\"\n size={OPTION_ACTION_SIZE[this.size]}\n icon=\"Ellipses\"\n rounded\n hasBorder={false}\n activeFill={false}\n ariaLabel={option.action.label}\n haspopup=\"menu\"\n controls={option.action.controls}\n expanded={option.action.expanded}\n surfaceOpen={option.action.surfaceOpen}\n onKeyDown={event => {\n event.stopPropagation();\n if (event.key === 'Escape' || event.key === 'ArrowLeft') {\n event.preventDefault();\n this.controller.focusSearchOrTrigger();\n }\n }}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n event.stopPropagation();\n this.dsOptionAction.emit({\n value: option.value,\n anchorId: actionId,\n originalEvent: event.detail,\n });\n }}\n />\n </ds-tooltip>\n ) : undefined\n }\n onHover={() => {\n this.focusRingVisible = false;\n this.activeIndex = index;\n }}\n onSelect={() => this.selectOption(option)}\n />\n );\n }\n\n private get captionIconOnly(): boolean {\n return this.collapseLabel && this.captionCompact && Boolean(this.icon || this.isLoading);\n }\n\n private syncCaptionCompactObserver(): void {\n this.disconnectCaptionCompactObserver();\n if (!this.collapseLabel) {\n if (this.captionCompact) this.captionCompact = false;\n return;\n }\n this.captionCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.captionCompact !== compact) this.captionCompact = compact;\n });\n }\n\n private disconnectCaptionCompactObserver(): void {\n this.captionCompactDisconnect?.();\n this.captionCompactDisconnect = undefined;\n }\n\n private renderCaptionTooltip(trigger: VNode, label: string): VNode {\n if (!this.collapseLabel) return trigger;\n return (\n <ds-tooltip label={label} side=\"top\" size=\"sm\">\n {trigger}\n </ds-tooltip>\n );\n }\n\n render() {\n const inactive = this.isDisabled;\n const showPlaceholder = !this.hasSelection;\n const count = this.resolvedValues.length;\n const label = this.multiple\n ? `${this.placeholder}${count > 0 ? ` · ${count}` : ''}`\n : showPlaceholder\n ? this.placeholder\n : (this.triggerLabel ?? this.selectedOption?.label);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n const usesOptionIcons = !this.multiple && choiceListUsesIcons(this.allOptions);\n const usesOptionSubtext = choiceListUsesSubtext(this.allOptions);\n const showError = this.error && Boolean(this.errorMessage);\n const describedBy =\n [this.ariaDescribedby, showError ? this.errorId : undefined].filter(Boolean).join(' ') ||\n undefined;\n const popupStyle = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.position.x)}px, ${Math.round(this.position.y)}px)`,\n zIndex: 'var(--dimension-z-index-floating)',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n let flatIndex = 0;\n\n return (\n <Host\n class={{\n 'select-host': true,\n 'ds-select-trigger-host': true,\n 'select-host--multiple': this.multiple,\n 'ds-field-stack': true,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n 'ds-table-caption-control': this.collapseLabel,\n 'ds-table-caption-control--compact': this.captionIconOnly,\n ...controlWidthClass(this.width),\n [`select-host--background-${this.background}`]: !!this.background,\n [`ds-select-trigger-host--background-${this.background}`]: !!this.background,\n }}\n >\n {this.renderCaptionTooltip(\n <button\n ref={element => {\n this.controller.setTriggerElement((element as HTMLButtonElement) ?? null);\n }}\n id={this.inputId ?? this.generatedId}\n type=\"button\"\n class={{\n trigger: true,\n 'ds-control-frame': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': !inactive && this.activeFill && this.hasSelection,\n 'trigger--expanded': !inactive && this.open,\n 'ds-interaction-fill--surface-open': !inactive && this.open,\n 'trigger--bordered': this.hasBorder,\n 'trigger--placeholder': showPlaceholder && !this.multiple,\n 'trigger--has-value': this.hasSelection,\n 'trigger--label-placeholder':\n !this.multiple && Boolean(this.triggerLabel) && this.triggerLabelPlaceholder,\n 'wrapper--error': this.hasBorder && this.error,\n [`ds-control--${this.size}`]: true,\n ...choiceBackgroundClassMap(this.background),\n }}\n disabled={inactive}\n role=\"combobox\"\n aria-haspopup={this.popupRole}\n aria-expanded={String(this.open)}\n aria-controls={this.open ? this.listboxId : undefined}\n aria-activedescendant={this.open && !this.searchable ? this.activeOptionId : undefined}\n aria-label={this.ariaLabel ?? (this.captionIconOnly ? label : undefined)}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.error ? 'true' : undefined}\n aria-required={this.required || undefined}\n aria-busy={this.isLoading ? 'true' : undefined}\n onClick={() => (this.open ? this.closePopup() : this.openPopup(false))}\n onKeyDown={event => {\n const action = this.visibleOptions[this.activeIndex]?.action;\n if (\n this.open &&\n action &&\n (event.key === 'ArrowRight' || (event.key === 'F10' && event.shiftKey))\n ) {\n event.preventDefault();\n document\n .getElementById(`${this.generatedId}-option-action-${this.activeIndex}`)\n ?.querySelector<HTMLElement>('button')\n ?.focus();\n return;\n }\n this.handleTriggerKeyDown(event);\n }}\n >\n {(this.icon || this.isLoading) && (\n <span\n class=\"trigger__prefix ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n {this.isLoading ? (\n <ds-loader size={iconSize} color=\"inherit\" />\n ) : (\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n )}\n </span>\n )}\n {this.captionIconOnly ? null : (\n <span\n class={{\n 'trigger__label-box': true,\n 'ds-interaction-fill__content': true,\n }}\n >\n <span\n class={{\n 'trigger__label-content': true,\n 'ds-control-label-box': true,\n 'ds-control-label-dot': this.dot,\n 'trigger__label-content--dot': this.dot,\n }}\n >\n <ds-text\n class=\"trigger__label\"\n as=\"span\"\n variant={textVariant}\n color=\"inherit\"\n lineTruncation={1}\n >\n {label}\n </ds-text>\n {this.dot && (\n <ds-badge\n class=\"trigger__dot ds-control-label-dot__badge\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n aria-hidden=\"true\"\n />\n )}\n </span>\n </span>\n )}\n {this.captionIconOnly ? null : (\n <span\n class=\"trigger__chevron ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon\n name={this.indicator === 'up-down' ? 'ChevronUpDown' : 'ChevronDown'}\n size={iconSize}\n color=\"inherit\"\n />\n </span>\n )}\n </button>,\n this.captionIconOnly ? this.ariaLabel?.trim() || this.placeholder : ''\n )}\n\n {this.open && (\n <div\n popover=\"manual\"\n ref={element => {\n this.controller.setPopupElement((element as HTMLDivElement) ?? null);\n }}\n class=\"select-popup ds-choice-popup\"\n style={popupStyle}\n >\n {this.searchable && (\n <ChoiceSearch\n size={this.size}\n value={this.searchTerm}\n placeholder={this.searchPlaceholder}\n controls={this.listboxId}\n activeDescendant={this.activeOptionId}\n inputRef={this.controller.setSearchElement}\n clearLabel={this.clearLabel}\n onValueChange={value => {\n this.searchTerm = value;\n }}\n onKeyDown={event => this.handleListKeyDown(event)}\n />\n )}\n <div\n id={this.listboxId}\n class=\"ds-choice-list\"\n role={this.popupRole}\n aria-multiselectable={this.multiple ? 'true' : undefined}\n aria-label={this.ariaLabel ?? this.placeholder}\n aria-busy={this.isLoading ? 'true' : undefined}\n >\n {this.isLoading ? (\n <div\n class=\"ds-choice-loading ds-empty-region\"\n role={this.popupRole === 'grid' ? 'row' : 'option'}\n aria-selected=\"false\"\n aria-disabled=\"true\"\n aria-label={this.loadingLabel}\n aria-live=\"polite\"\n >\n {this.popupRole === 'grid' ? (\n <span role=\"gridcell\">\n <ds-loader size={this.size} color=\"inherit\" />\n </span>\n ) : (\n <ds-loader size={this.size} color=\"inherit\" />\n )}\n </div>\n ) : this.visibleOptions.length === 0 ? (\n <div\n class=\"ds-choice-empty ds-empty-region\"\n role={this.popupRole === 'grid' ? 'row' : 'option'}\n aria-selected=\"false\"\n aria-disabled=\"true\"\n aria-label={this.noResultsText}\n aria-live=\"polite\"\n >\n {this.popupRole === 'grid' ? (\n <span role=\"gridcell\">\n <ds-empty-state body={this.noResultsText} />\n </span>\n ) : (\n <ds-empty-state body={this.noResultsText} />\n )}\n </div>\n ) : (\n this.visibleSections.map((section, sectionIndex) => (\n <div\n class={{\n 'ds-choice-section': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--sm': true,\n 'ds-choice-section--divided': !!section.divider,\n 'ds-choice-section--headed-after-first':\n sectionIndex > 0 && Boolean(section.header),\n }}\n role={\n this.popupRole === 'grid' ? 'rowgroup' : section.header ? 'group' : undefined\n }\n aria-label={section.header}\n >\n {section.header && this.popupRole === 'grid' ? (\n <ds-text\n class={`ds-choice-section__header ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n emphasis\n color=\"primary\"\n role=\"row\"\n >\n <span class=\"ds-choice-section__header-label\" role=\"gridcell\">\n {section.header}\n </span>\n </ds-text>\n ) : section.header ? (\n <ds-text\n class={`ds-choice-section__header ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n <span class=\"ds-choice-section__header-label\">{section.header}</span>\n </ds-text>\n ) : null}\n {section.options.map(option =>\n this.renderOption(option, flatIndex++, usesOptionIcons, usesOptionSubtext)\n )}\n </div>\n ))\n )}\n </div>\n {this.footerActionLabel && !this.isLoading ? (\n <ChoiceActionFooter\n size={this.size}\n label={this.footerActionLabel}\n onAction={this.activateFooterAction}\n />\n ) : this.allowClear && this.hasSelection && !this.isLoading ? (\n <ChoiceFooter\n size={this.size}\n summary={this.multiple ? `${count} ${this.selectedLabel}` : undefined}\n compactSummary={this.multiple ? String(count) : undefined}\n useCompactSummary={this.compactFooterSummary}\n contentRef={element => {\n this.footerContentElement = element ?? undefined;\n }}\n summaryMeasureRef={element => {\n this.footerSummaryMeasureElement = element ?? undefined;\n }}\n clearRef={element => {\n this.footerClearElement = element ?? undefined;\n }}\n clearLabel={this.clearLabel}\n onClear={this.clearSelection}\n />\n ) : null}\n </div>\n )}\n\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
|
|
976
981
|
"type": "registry:ui"
|
|
977
982
|
}
|
|
978
983
|
]
|
|
@@ -77,7 +77,8 @@
|
|
|
77
77
|
"Managed mobile Back and page-action controls use the same large touch density as the mobile navigation header without increasing the shared header height.",
|
|
78
78
|
"Managed ShellApp keeps the selected desktop section-navigation presentation and ShellPage present and owns page header stickiness and the content scroller.",
|
|
79
79
|
"Route-owned pageChrome independently selects ShellPage contentInset, page-header seams, scroll compaction, expanded and compact header-divider visibility, and contentSurface. ShellApp forwards them while resolving whether desktop title geometry is page-local or shell-bar-owned.",
|
|
80
|
-
"The application filters unauthorized destinations, sections, and tools before passing them into shell components."
|
|
80
|
+
"The application filters unauthorized destinations, sections, and tools before passing them into shell components.",
|
|
81
|
+
"Application-owned header actions and the account trigger retain surface ownership across shell presentations. Supply surfaceOpen on header actions and accountMenuSurfaceOpen on navigation for rendered surface lifetimes; keep disclosure and selected-route state separate."
|
|
81
82
|
],
|
|
82
83
|
"responsiveBehavior": [
|
|
83
84
|
"ShellApp owns the fixed system breakpoints and resolves desktop at 1200px and wider, tablet from 768px through 1199px, and mobile below 768px.",
|
|
@@ -136,6 +137,10 @@
|
|
|
136
137
|
{
|
|
137
138
|
"label": "Application shell contract",
|
|
138
139
|
"path": "agent/patterns/application-shell/pattern.agent.json"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"label": "Surface ownership and trigger state",
|
|
143
|
+
"path": "src/docs/SurfaceOwnership.mdx"
|
|
139
144
|
}
|
|
140
145
|
]
|
|
141
146
|
},
|
|
@@ -691,12 +696,12 @@
|
|
|
691
696
|
},
|
|
692
697
|
{
|
|
693
698
|
"path": "src/wc/components/ShellApp/ShellApp.tsx",
|
|
694
|
-
"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 { 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 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 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 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 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 }\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 componentDidLoad() {\n this.hasLoaded = true;\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 requestAnimationFrame(() => {\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.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.disconnectViewportListeners();\n this.disconnectPageLifecycleListeners();\n this.cancelForegroundRefresh();\n this.chromeSyncCoalescer.cancel();\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) this.mobileActionMenuOpen = false;\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 requestAnimationFrame(() => {\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 (typeof window === 'undefined') return;\n const next = resolveShellResponsiveMode(window.innerWidth);\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 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 = 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 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 const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;\n const bar = this.el.querySelector(\n 'ds-bar-nav, 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 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.renderManagedBarTitle('shell-bar')\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 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 onDsClick={(event: CustomEvent<MouseEvent>) => {\n this.mobileActionMenuInitialFocusVisible = event.detail.detail === 0;\n this.mobileActionMenuOpen = !this.mobileActionMenuOpen;\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 requestAnimationFrame(() => 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 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 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 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 class={shellCls} responsive-mode={this.resolvedMode}>\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\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 class={shellCls} responsive-mode={this.resolvedMode}>\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\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",
|
|
699
|
+
"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 { 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 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 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 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 }\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 componentDidLoad() {\n this.hasLoaded = true;\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 requestAnimationFrame(() => {\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.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.disconnectViewportListeners();\n this.disconnectPageLifecycleListeners();\n this.cancelForegroundRefresh();\n this.chromeSyncCoalescer.cancel();\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 requestAnimationFrame(() => {\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 (typeof window === 'undefined') return;\n const next = resolveShellResponsiveMode(window.innerWidth);\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 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 = 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 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 const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;\n const bar = this.el.querySelector(\n 'ds-bar-nav, 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.renderManagedBarTitle('shell-bar')\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 requestAnimationFrame(() => 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 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 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 class={shellCls} responsive-mode={this.resolvedMode}>\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\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 class={shellCls} responsive-mode={this.resolvedMode}>\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\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",
|
|
695
700
|
"type": "registry:ui"
|
|
696
701
|
},
|
|
697
702
|
{
|
|
698
703
|
"path": "src/wc/components/ShellApp/shell-app-types.ts",
|
|
699
|
-
"content": "import type { BarNavTab } from '../BarNav/bar-nav-types';\nimport type {\n BarTitleActionConfigItem,\n BarTitleActionItem,\n BarTitlePrimaryAction,\n} from '../BarTitle/bar-title-types';\nimport type { BreadcrumbItem } from '../Breadcrumb/breadcrumb-types';\nimport type { MobileHeaderTone } from '../MobileHeader/mobile-header-types';\nimport type { PanelNavGroup, PanelNavItem, PanelNavRouterMode } from '../PanelNav/panel-nav-types';\nimport type {\n PanelToolsHeaders,\n PanelToolsHeaderAction,\n PanelToolsItem,\n PanelToolsRailAccessory,\n} from '../PanelTools/panel-tools-types';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport type { ShellPageContentInset, ShellPageContentSurface } from '../ShellPage/shell-page-types';\n\nexport type ShellAppComposition = 'managed' | 'slotted';\nexport type ShellSectionNavigation = 'bar' | 'panel';\n\n/**\n * Router-owned application navigation supplied to managed ShellApp.\n * ShellApp renders the responsive chrome but never performs route changes.\n */\nexport interface ShellNavigationConfig {\n /** Groups shown in the current desktop/tablet PanelNav context. */\n groups?: PanelNavGroup[];\n /** Complete Dashboard collection used by the mobile navigation sheet. */\n dashboardGroups?: PanelNavGroup[];\n /** Complete Settings collection used by the mobile navigation sheet. */\n settingsGroups?: PanelNavGroup[];\n /** Current application URL used for active-item matching. */\n currentUrl?: string;\n /** Optional explicit active destination when URL matching is unavailable. */\n activeId?: string;\n /** Current Dashboard or Settings browsing context. */\n browseContext?: NavChromeStyle;\n /** Managed shells default to event routing so the application router stays authoritative. */\n routerMode?: PanelNavRouterMode;\n /** Persisted desktop PanelNav collapse preference key. */\n storageKey?: string;\n userName?: string;\n userInitial?: string;\n accountMenuExpanded?: boolean;\n dashboardLabel?: string;\n settingsLabel?: string;\n accountLabel?: string;\n /** Hide the mobile-sheet Account shortcut when Settings already owns that destination. */\n showMobileAccount?: boolean;\n navigationLabel?: string;\n dashboardNavigationLabel?: string;\n settingsNavigationLabel?: string;\n}\n\n/**\n * Route-owned page chrome. The same data drives desktop BarNav/BarTitle and\n * the intentionally different mobile MobileHeader presentation.\n */\nexport interface ShellPageChromeConfig {\n heading?: string;\n /** Desktop/tablet route-navigation heading when it differs from the page h1. */\n routeHeading?: string;\n description?: string;\n headingLevel?: 'h1' | 'h2';\n tabs?: BarNavTab[];\n value?: string;\n basePath?: string;\n currentUrl?: string;\n sectionsAriaLabel?: string;\n subsections?: BarNavTab[];\n subvalue?: string;\n subsectionsAriaLabel?: string;\n showBack?: boolean;\n backAriaLabel?: string;\n backIcon?: string;\n backLabel?: string;\n breadcrumbs?: BreadcrumbItem[];\n breadcrumbAriaLabel?: string;\n primaryAction?: BarTitlePrimaryAction | null;\n actions?: BarTitleActionItem[];\n /** Ordered desktop, tablet, and mobile page-action model. */\n actionItems?: BarTitleActionConfigItem[];\n /** Icon actions rendered in the mobile header's trailing lane. */\n mobileActions?: PanelToolsHeaderAction[];\n actionsAriaLabel?: string;\n tone?: MobileHeaderTone;\n contentInset?: ShellPageContentInset;\n /** Remove only the content gutter adjacent to the page header. */\n contentInsetBlockStart?: ShellPageContentInset;\n /** Exact roomy block-start content inset when the responsive default is not appropriate. */\n contentInsetBlockStartSize?: string;\n /** Exact block-start content inset while page-title capacity is compact or constrained. */\n compactContentInsetBlockStartSize?: string;\n /** Allow a roomy page title to compact in response to page scrolling. */\n scrollCompaction?: boolean;\n /** Draw the divider beneath the desktop/tablet page title. */\n showHeaderDivider?: boolean;\n /** Override divider visibility while the page title is compact or constrained. */\n showCompactHeaderDivider?: boolean;\n /** Canvas surface painted by ShellPage around and beneath routed content. */\n contentSurface?: ShellPageContentSurface;\n}\n\n/**\n * Product-owned global tools supplied to managed ShellApp. Tool view content\n * remains mounted in the corresponding named `*-view` slots.\n */\nexport interface ShellToolsConfig {\n items?: PanelToolsItem[];\n /** Desktop/tablet-only direct rail intents and decorative group boundaries. */\n accessories?: PanelToolsRailAccessory[];\n headers?: PanelToolsHeaders;\n storageKey?: string;\n fullscreenHeaderMode?: 'shared' | 'split';\n toolsLabel?: string;\n toolShortcutsLabel?: string;\n inboxLabel?: string;\n inboxNavigationLabel?: string;\n /** Temporarily conceal the mobile bar while a tool-owned keyboard interaction needs the space. */\n mobileBarHidden?: boolean;\n}\n\nexport interface ShellNavigationSelectDetail {\n id: string;\n item?: PanelNavItem;\n}\n",
|
|
704
|
+
"content": "import type { BarNavTab } from '../BarNav/bar-nav-types';\nimport type {\n BarTitleActionConfigItem,\n BarTitleActionItem,\n BarTitlePrimaryAction,\n} from '../BarTitle/bar-title-types';\nimport type { BreadcrumbItem } from '../Breadcrumb/breadcrumb-types';\nimport type { MobileHeaderTone } from '../MobileHeader/mobile-header-types';\nimport type { PanelNavGroup, PanelNavItem, PanelNavRouterMode } from '../PanelNav/panel-nav-types';\nimport type {\n PanelToolsHeaders,\n PanelToolsHeaderAction,\n PanelToolsItem,\n PanelToolsRailAccessory,\n} from '../PanelTools/panel-tools-types';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport type { ShellPageContentInset, ShellPageContentSurface } from '../ShellPage/shell-page-types';\n\nexport type ShellAppComposition = 'managed' | 'slotted';\nexport type ShellSectionNavigation = 'bar' | 'panel';\n\n/**\n * Router-owned application navigation supplied to managed ShellApp.\n * ShellApp renders the responsive chrome but never performs route changes.\n */\nexport interface ShellNavigationConfig {\n /** Groups shown in the current desktop/tablet PanelNav context. */\n groups?: PanelNavGroup[];\n /** Complete Dashboard collection used by the mobile navigation sheet. */\n dashboardGroups?: PanelNavGroup[];\n /** Complete Settings collection used by the mobile navigation sheet. */\n settingsGroups?: PanelNavGroup[];\n /** Current application URL used for active-item matching. */\n currentUrl?: string;\n /** Optional explicit active destination when URL matching is unavailable. */\n activeId?: string;\n /** Current Dashboard or Settings browsing context. */\n browseContext?: NavChromeStyle;\n /** Managed shells default to event routing so the application router stays authoritative. */\n routerMode?: PanelNavRouterMode;\n /** Persisted desktop PanelNav collapse preference key. */\n storageKey?: string;\n userName?: string;\n userInitial?: string;\n accountMenuExpanded?: boolean;\n /** Associated account surface remains visible, including exit motion. */\n accountMenuSurfaceOpen?: boolean;\n dashboardLabel?: string;\n settingsLabel?: string;\n accountLabel?: string;\n /** Hide the mobile-sheet Account shortcut when Settings already owns that destination. */\n showMobileAccount?: boolean;\n navigationLabel?: string;\n dashboardNavigationLabel?: string;\n settingsNavigationLabel?: string;\n}\n\n/**\n * Route-owned page chrome. The same data drives desktop BarNav/BarTitle and\n * the intentionally different mobile MobileHeader presentation.\n */\nexport interface ShellPageChromeConfig {\n heading?: string;\n /** Desktop/tablet route-navigation heading when it differs from the page h1. */\n routeHeading?: string;\n description?: string;\n headingLevel?: 'h1' | 'h2';\n tabs?: BarNavTab[];\n value?: string;\n basePath?: string;\n currentUrl?: string;\n sectionsAriaLabel?: string;\n subsections?: BarNavTab[];\n subvalue?: string;\n subsectionsAriaLabel?: string;\n showBack?: boolean;\n backAriaLabel?: string;\n backIcon?: string;\n backLabel?: string;\n breadcrumbs?: BreadcrumbItem[];\n breadcrumbAriaLabel?: string;\n primaryAction?: BarTitlePrimaryAction | null;\n actions?: BarTitleActionItem[];\n /** Ordered desktop, tablet, and mobile page-action model. */\n actionItems?: BarTitleActionConfigItem[];\n /** Icon actions rendered in the mobile header's trailing lane. */\n mobileActions?: PanelToolsHeaderAction[];\n actionsAriaLabel?: string;\n tone?: MobileHeaderTone;\n contentInset?: ShellPageContentInset;\n /** Remove only the content gutter adjacent to the page header. */\n contentInsetBlockStart?: ShellPageContentInset;\n /** Exact roomy block-start content inset when the responsive default is not appropriate. */\n contentInsetBlockStartSize?: string;\n /** Exact block-start content inset while page-title capacity is compact or constrained. */\n compactContentInsetBlockStartSize?: string;\n /** Allow a roomy page title to compact in response to page scrolling. */\n scrollCompaction?: boolean;\n /** Draw the divider beneath the desktop/tablet page title. */\n showHeaderDivider?: boolean;\n /** Override divider visibility while the page title is compact or constrained. */\n showCompactHeaderDivider?: boolean;\n /** Canvas surface painted by ShellPage around and beneath routed content. */\n contentSurface?: ShellPageContentSurface;\n}\n\n/**\n * Product-owned global tools supplied to managed ShellApp. Tool view content\n * remains mounted in the corresponding named `*-view` slots.\n */\nexport interface ShellToolsConfig {\n items?: PanelToolsItem[];\n /** Desktop/tablet-only direct rail intents and decorative group boundaries. */\n accessories?: PanelToolsRailAccessory[];\n headers?: PanelToolsHeaders;\n storageKey?: string;\n fullscreenHeaderMode?: 'shared' | 'split';\n toolsLabel?: string;\n toolShortcutsLabel?: string;\n inboxLabel?: string;\n inboxNavigationLabel?: string;\n /** Temporarily conceal the mobile bar while a tool-owned keyboard interaction needs the space. */\n mobileBarHidden?: boolean;\n}\n\nexport interface ShellNavigationSelectDetail {\n id: string;\n item?: PanelNavItem;\n}\n",
|
|
700
705
|
"type": "registry:ui"
|
|
701
706
|
}
|
|
702
707
|
]
|