@ds-mo/ui 15.4.0 → 15.5.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/NOTICE +7 -1
- package/dist/.package-ready.json +1 -1
- package/dist/agent-patterns.json +1 -1
- package/dist/agent.json +64 -8
- package/dist/angular/boolean-value-accessor.js +3 -3
- package/dist/angular/ds-agent-activity.js +3 -3
- package/dist/angular/ds-agent-questionnaire.js +3 -3
- package/dist/angular/ds-agent-response.js +3 -3
- package/dist/angular/ds-agent-source-list.js +3 -3
- package/dist/angular/ds-agent-tool-call.js +3 -3
- package/dist/angular/ds-attachment-list.js +3 -3
- package/dist/angular/ds-avatar.js +3 -3
- package/dist/angular/ds-badge.js +3 -3
- package/dist/angular/ds-banner.js +3 -3
- package/dist/angular/ds-bar-action.js +3 -3
- package/dist/angular/ds-bar-nav.js +3 -3
- package/dist/angular/ds-bar-page-title.js +3 -3
- package/dist/angular/ds-bar-title.js +3 -3
- package/dist/angular/ds-bar-workflow.js +3 -3
- package/dist/angular/ds-breadcrumb.js +3 -3
- package/dist/angular/ds-button-filled.js +3 -3
- package/dist/angular/ds-button-inverted.js +3 -3
- package/dist/angular/ds-button-unfilled.js +3 -3
- package/dist/angular/ds-calendar.js +3 -3
- package/dist/angular/ds-card-action-center.js +3 -3
- package/dist/angular/ds-card-chart.js +3 -3
- package/dist/angular/ds-card-navigation.js +3 -3
- package/dist/angular/ds-card-overview.js +3 -3
- package/dist/angular/ds-card-setting.js +3 -3
- package/dist/angular/ds-card-settings-scope.js +3 -3
- package/dist/angular/ds-chart-legend.js +3 -3
- package/dist/angular/ds-chart.js +3 -3
- package/dist/angular/ds-checkbox-group.js +3 -3
- package/dist/angular/ds-checkbox.d.ts +1 -1
- package/dist/angular/ds-checkbox.js +5 -5
- package/dist/angular/ds-chip.js +3 -3
- package/dist/angular/ds-code-block.js +3 -3
- package/dist/angular/ds-conversation-list-item.js +3 -3
- package/dist/angular/ds-conversation-list-section.js +3 -3
- package/dist/angular/ds-conversation-list.js +3 -3
- package/dist/angular/ds-data-customize.js +3 -3
- package/dist/angular/ds-data-filter.js +3 -3
- package/dist/angular/ds-data-group.js +3 -3
- package/dist/angular/ds-data-preferences.js +3 -3
- package/dist/angular/ds-data-saved-views.js +3 -3
- package/dist/angular/ds-data-search.js +3 -3
- package/dist/angular/ds-data-sort.js +3 -3
- package/dist/angular/ds-data-toolbar.js +3 -3
- package/dist/angular/ds-divider.js +3 -3
- package/dist/angular/ds-empty-state.js +3 -3
- package/dist/angular/ds-field.js +3 -3
- package/dist/angular/ds-filter-menu.js +3 -3
- package/dist/angular/ds-icon.js +3 -3
- package/dist/angular/ds-inline-banner-settings.js +3 -3
- package/dist/angular/ds-input-date.js +3 -3
- package/dist/angular/ds-input-time.js +3 -3
- package/dist/angular/ds-input.js +3 -3
- package/dist/angular/ds-loader.js +3 -3
- package/dist/angular/ds-map-cluster-marker.js +3 -3
- package/dist/angular/ds-map-entity-marker.js +3 -3
- package/dist/angular/ds-markdown.js +3 -3
- package/dist/angular/ds-menu.js +3 -3
- package/dist/angular/ds-message-actions.js +3 -3
- package/dist/angular/ds-message-bubble.js +3 -3
- package/dist/angular/ds-message-composer.js +3 -3
- package/dist/angular/ds-message-scroller.js +3 -3
- package/dist/angular/ds-message.js +3 -3
- package/dist/angular/ds-mobile-bar-nav.js +3 -3
- package/dist/angular/ds-mobile-header.js +3 -3
- package/dist/angular/ds-mobile-section-switcher.js +3 -3
- package/dist/angular/ds-mobile-sheet-nav.js +3 -3
- package/dist/angular/ds-modal.js +3 -3
- package/dist/angular/ds-pagination.js +3 -3
- package/dist/angular/ds-panel-nav.js +3 -3
- package/dist/angular/ds-panel-sub-nav.js +3 -3
- package/dist/angular/ds-panel-tool-header.js +3 -3
- package/dist/angular/ds-panel-tool-search.js +3 -3
- package/dist/angular/ds-panel-tools.js +3 -3
- package/dist/angular/ds-paper-texture.js +3 -3
- package/dist/angular/ds-radio-tile.js +3 -3
- package/dist/angular/ds-radio.js +3 -3
- package/dist/angular/ds-score.js +3 -3
- package/dist/angular/ds-scroll-overlay.js +3 -3
- package/dist/angular/ds-select.d.ts +1 -1
- package/dist/angular/ds-select.js +5 -5
- package/dist/angular/ds-setting-row-checkbox.js +3 -3
- package/dist/angular/ds-setting-row-radio.js +3 -3
- package/dist/angular/ds-setting-row-toggle.js +3 -3
- package/dist/angular/ds-shell-app.js +3 -3
- package/dist/angular/ds-shell-page.js +3 -3
- package/dist/angular/ds-shell-tools.js +3 -3
- package/dist/angular/ds-skeleton.js +3 -3
- package/dist/angular/ds-slider.js +3 -3
- package/dist/angular/ds-swatch-picker.js +3 -3
- package/dist/angular/ds-switch.js +3 -3
- package/dist/angular/ds-tab-group.js +3 -3
- package/dist/angular/ds-table.d.ts +7 -1
- package/dist/angular/ds-table.js +13 -7
- package/dist/angular/ds-tag.js +3 -3
- package/dist/angular/ds-text.js +3 -3
- package/dist/angular/ds-textarea.js +3 -3
- package/dist/angular/ds-time-picker.js +3 -3
- package/dist/angular/ds-toast.js +3 -3
- package/dist/angular/ds-tooltip-chart.js +3 -3
- package/dist/angular/ds-tooltip.js +3 -3
- package/dist/angular/ds-typing-indicator.js +3 -3
- package/dist/angular/number-value-accessor.js +3 -3
- package/dist/angular/radio-value-accessor.js +3 -3
- package/dist/angular/select-value-accessor.js +3 -3
- package/dist/angular/text-value-accessor.js +3 -3
- package/dist/angular/value-accessor.js +3 -3
- package/dist/components/THIRD-PARTY-NOTICES +1791 -0
- package/dist/components/bundled-dependencies.json +478 -0
- package/dist/components/ds-agent-activity.js +1 -1
- package/dist/components/ds-agent-questionnaire.js +1 -1
- package/dist/components/ds-agent-response.js +1 -1
- package/dist/components/ds-agent-source-list.js +1 -1
- package/dist/components/ds-agent-tool-call.js +1 -1
- package/dist/components/ds-attachment-list.js +1 -1
- package/dist/components/ds-avatar.js +1 -1
- package/dist/components/ds-banner.js +1 -1
- package/dist/components/ds-banner.js.map +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-page-title.js +1 -1
- package/dist/components/ds-bar-title.js +1 -1
- package/dist/components/ds-bar-workflow.js +1 -1
- package/dist/components/ds-breadcrumb.js +1 -1
- package/dist/components/ds-button-filled.js +1 -1
- package/dist/components/ds-button-inverted.js +1 -1
- package/dist/components/ds-button-unfilled.js +1 -1
- package/dist/components/ds-calendar.js +1 -1
- package/dist/components/ds-card-action-center.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-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-chart.js +1 -1
- package/dist/components/ds-chart.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.js +1 -1
- package/dist/components/ds-conversation-list.js.map +1 -1
- package/dist/components/ds-data-customize.js +1 -1
- package/dist/components/ds-data-filter.js +1 -1
- package/dist/components/ds-data-group.js +1 -1
- package/dist/components/ds-data-preferences.js +1 -1
- package/dist/components/ds-data-preferences.js.map +1 -1
- package/dist/components/ds-data-saved-views.js +1 -1
- package/dist/components/ds-data-search.js +1 -1
- package/dist/components/ds-data-sort.js +1 -1
- package/dist/components/ds-data-toolbar.js +1 -1
- package/dist/components/ds-divider.js +1 -1
- package/dist/components/ds-empty-state.js +1 -1
- package/dist/components/ds-filter-menu.js +1 -1
- package/dist/components/ds-icon.js +1 -1
- package/dist/components/ds-input-date.js +1 -1
- package/dist/components/ds-input-date.js.map +1 -1
- package/dist/components/ds-input-time.js +1 -1
- package/dist/components/ds-input-time.js.map +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-map-entity-marker.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-bubble.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-message.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-tool-header.js +1 -1
- package/dist/components/ds-panel-tool-search.js +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-radio-tile.js +1 -1
- package/dist/components/ds-radio-tile.js.map +1 -1
- package/dist/components/ds-radio.js +1 -1
- package/dist/components/ds-scroll-overlay.js +1 -1
- package/dist/components/ds-scroll-overlay.js.map +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-setting-row-toggle.js +1 -1
- package/dist/components/ds-shell-app.js +1 -1
- package/dist/components/ds-shell-app.js.map +1 -1
- package/dist/components/ds-shell-page.js +1 -1
- package/dist/components/ds-shell-tools.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-slider.js.map +1 -1
- package/dist/components/ds-switch.js +1 -1
- package/dist/components/ds-tab-group.js +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-time-picker.js +1 -1
- package/dist/components/ds-toast.js +1 -1
- package/dist/components/ds-tooltip-chart.js +1 -1
- package/dist/components/ds-tooltip.js +1 -1
- package/dist/components/p--STJbKuN.js +2 -0
- package/dist/components/p--STJbKuN.js.map +1 -0
- package/dist/components/{p-Co1rUdOR.js → p--pJyciTI.js} +2 -2
- package/dist/components/p--pJyciTI.js.map +1 -0
- package/dist/components/p-02k7d7BH.js +2 -0
- package/dist/components/p-02k7d7BH.js.map +1 -0
- package/dist/components/{p-CZNZTcYS.js → p-5-gQbIIz.js} +2 -2
- package/dist/components/{p-DRdoGSGX.js → p-7MwoWtTJ.js} +2 -2
- package/dist/components/p-B0uO3-Dp.js +2 -0
- package/dist/components/p-B0uO3-Dp.js.map +1 -0
- package/dist/components/p-B6TX55c5.js +2 -0
- package/dist/components/p-B6TX55c5.js.map +1 -0
- package/dist/components/p-B95UP66y.js +2 -0
- package/dist/components/p-B95UP66y.js.map +1 -0
- package/dist/components/p-BGSG6xIS.js +2 -0
- package/dist/components/p-BGSG6xIS.js.map +1 -0
- package/dist/components/p-BNarCFJZ.js +2 -0
- package/dist/components/p-BNarCFJZ.js.map +1 -0
- package/dist/components/{p-DJDbskZf.js → p-BO5URdZF.js} +2 -2
- package/dist/components/{p-DmSnHVW6.js → p-BVxK8ce0.js} +2 -2
- package/dist/components/{p-B0EpvBDs.js → p-BYeG0b5N.js} +2 -2
- package/dist/components/p-BcpIK9S6.js +2 -0
- package/dist/components/p-BcpIK9S6.js.map +1 -0
- package/dist/components/p-Bf-BphXP.js +2 -0
- package/dist/components/p-Bf-BphXP.js.map +1 -0
- package/dist/components/{p-Bk3poU91.js → p-BmIllmq2.js} +2 -2
- package/dist/components/p-BmIllmq2.js.map +1 -0
- package/dist/components/p-Bq_ypMSy.js +2 -0
- package/dist/components/p-Bq_ypMSy.js.map +1 -0
- package/dist/components/p-ByLH5RAD.js +2 -0
- package/dist/components/p-ByLH5RAD.js.map +1 -0
- package/dist/components/{p-DzvTC27M.js → p-C1ZfBbJ_.js} +2 -2
- package/dist/components/p-C1ZfBbJ_.js.map +1 -0
- package/dist/components/p-C1oczmAc.js +2 -0
- package/dist/components/p-C1oczmAc.js.map +1 -0
- package/dist/components/p-C5BBIDVm.js +2 -0
- package/dist/components/p-C5BBIDVm.js.map +1 -0
- package/dist/components/{p-CJ1NnOEW.js → p-CA3iQ1ME.js} +2 -2
- package/dist/components/p-CE6MaXBg.js +2 -0
- package/dist/components/p-CE6MaXBg.js.map +1 -0
- package/dist/components/p-CXGjCWSh.js +2 -0
- package/dist/components/p-CXGjCWSh.js.map +1 -0
- package/dist/components/{p-DYQa1DVF.js → p-C_9VGgPf.js} +2 -2
- package/dist/components/p-CbrDF2gA.js +2 -0
- package/dist/components/p-CbrDF2gA.js.map +1 -0
- package/dist/components/{p-7TVdPhGR.js → p-CgQB8Ct5.js} +2 -2
- package/dist/components/{p-D6FRqNhN.js → p-D-J7GgjY.js} +2 -2
- package/dist/components/{p-DSZPO06T.js → p-D9_iDlkT.js} +2 -2
- package/dist/components/{p-BwdnVqAY.js → p-DEYCF-oJ.js} +2 -2
- package/dist/components/p-DGVZhrMd.js +2 -0
- package/dist/components/p-DGVZhrMd.js.map +1 -0
- package/dist/components/{p-C8V4lkVy.js → p-DKGW3gRa.js} +2 -2
- package/dist/components/p-DPFg5d4_.js +2 -0
- package/dist/components/p-DPFg5d4_.js.map +1 -0
- package/dist/components/{p-C4huuwed.js → p-DR_8IPn9.js} +2 -2
- package/dist/components/p-DXL5D30P.js +2 -0
- package/dist/components/p-DXL5D30P.js.map +1 -0
- package/dist/components/{p-B0BQCVkq.js → p-D_iZPukF.js} +2 -2
- package/dist/components/{p-JBki_jyY.js → p-DfMTDFcE.js} +2 -2
- package/dist/components/{p-MOJmB6vX.js → p-Dg6hnCc2.js} +2 -2
- package/dist/components/{p-DXk1eml1.js → p-DjbT9kKm.js} +2 -2
- package/dist/components/p-Dka0lUI_.js +2 -0
- package/dist/components/p-Dka0lUI_.js.map +1 -0
- package/dist/components/p-DlsjHRU3.js +2 -0
- package/dist/components/p-DlsjHRU3.js.map +1 -0
- package/dist/components/{p-_IqSPt5V.js → p-Dr5rkAhe.js} +2 -2
- package/dist/components/p-DuCCuJKI.js +2 -0
- package/dist/components/p-DuCCuJKI.js.map +1 -0
- package/dist/components/{p-DIQiHX0q.js → p-DvGrsLgt.js} +2 -2
- package/dist/components/{p-Doa2blZT.js → p-PtxCwf5S.js} +2 -2
- package/dist/components/p-PzYJqN8B.js +2 -0
- package/dist/components/p-PzYJqN8B.js.map +1 -0
- package/dist/components/p-S1Ct3FJV.js +2 -0
- package/dist/components/p-S1Ct3FJV.js.map +1 -0
- package/dist/components/p-YC7UM4jH.js +2 -0
- package/dist/components/p-YC7UM4jH.js.map +1 -0
- package/dist/components/{p-B9TM6FqC.js → p-d76Ry5nh.js} +2 -2
- package/dist/components/p-ioGH5SoA.js +2 -0
- package/dist/components/p-ioGH5SoA.js.map +1 -0
- package/dist/components/p-jp7hwRtC.js +2 -0
- package/dist/components/p-jp7hwRtC.js.map +1 -0
- package/dist/components/{p-BFHpU-uU.js → p-kwS-vkau.js} +2 -2
- package/dist/components/{p-DjkUFlDW.js → p-pPtjNRqC.js} +3 -2
- package/dist/components/p-pPtjNRqC.js.map +1 -0
- package/dist/components/{p-B39iJO8F.js → p-rmPIcWED.js} +2 -2
- package/dist/components/p-t2Ebn8si.js +2 -0
- package/dist/components/p-t2Ebn8si.js.map +1 -0
- package/dist/components/p-wRhYxngW.js +2 -0
- package/dist/components/p-wRhYxngW.js.map +1 -0
- package/dist/docs/components.json +477 -82
- package/dist/lib/components/Icon/icon-cache.d.ts +6 -0
- package/dist/lib/utils/clock-time.d.ts +1 -0
- package/dist/lib/utils/date-filter-value.d.ts +2 -0
- package/dist/lib/utils/form-association.d.ts +2 -1
- package/dist/lib/utils/format-percentage.d.ts +1 -1
- package/dist/lib/utils/index.js +113 -93
- package/dist/lib/utils/index.js.map +4 -4
- package/dist/lib/utils/intl-formatters.d.ts +3 -0
- package/dist/lib/utils/resolve-css-length-px.d.ts +4 -3
- package/dist/lint/THIRD-PARTY-NOTICES +97 -40
- package/dist/lint/bundled-dependencies.json +162 -0
- package/dist/lint/index.js +1341 -1333
- package/dist/mcp/THIRD-PARTY-NOTICES +470 -0
- package/dist/mcp/bundled-dependencies.json +29 -0
- package/dist/mcp-data/registry/agent-tool-call.json +1 -1
- package/dist/mcp-data/registry/banner.json +1 -1
- package/dist/mcp-data/registry/bar-nav.json +1 -1
- package/dist/mcp-data/registry/bar-page-title.json +1 -1
- package/dist/mcp-data/registry/breadcrumb.json +1 -1
- package/dist/mcp-data/registry/calendar.json +4 -4
- package/dist/mcp-data/registry/card-overview.json +1 -1
- package/dist/mcp-data/registry/chart.json +3 -3
- package/dist/mcp-data/registry/checkbox.json +17 -1
- package/dist/mcp-data/registry/conversation-list.json +1 -1
- package/dist/mcp-data/registry/data-group.json +1 -1
- package/dist/mcp-data/registry/data-preferences.json +1 -1
- package/dist/mcp-data/registry/filter-menu.json +1 -1
- package/dist/mcp-data/registry/icon.json +6 -5
- package/dist/mcp-data/registry/input-date.json +1 -1
- package/dist/mcp-data/registry/input-time.json +1 -1
- package/dist/mcp-data/registry/markdown.json +1 -1
- package/dist/mcp-data/registry/menu.json +1 -1
- package/dist/mcp-data/registry/message.json +4 -4
- package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
- package/dist/mcp-data/registry/modal.json +1 -1
- package/dist/mcp-data/registry/panel-nav.json +1 -1
- package/dist/mcp-data/registry/radio-tile.json +1 -1
- package/dist/mcp-data/registry/radio.json +1 -1
- package/dist/mcp-data/registry/registry.json +63 -7
- package/dist/mcp-data/registry/scroll-overlay.json +1 -1
- package/dist/mcp-data/registry/select.json +70 -3
- package/dist/mcp-data/registry/shell-app.json +1 -1
- package/dist/mcp-data/registry/slider.json +3 -3
- package/dist/mcp-data/registry/switch.json +1 -1
- package/dist/mcp-data/registry/table.json +140 -14
- package/dist/mcp-data/registry/tooltip-chart.json +1 -1
- package/dist/react/ds-table.d.ts +3 -1
- package/dist/react/ds-table.js +2 -0
- package/dist/styles/table.css +405 -8
- package/dist/types/components/AgentToolCall/AgentToolCall.d.ts +3 -0
- package/dist/types/components/Banner/Banner.d.ts +1 -0
- package/dist/types/components/BarNav/BarNav.d.ts +4 -0
- package/dist/types/components/BarPageTitle/BarPageTitle.d.ts +4 -0
- package/dist/types/components/Breadcrumb/Breadcrumb.d.ts +5 -0
- package/dist/types/components/Calendar/Calendar.d.ts +2 -0
- package/dist/types/components/CardOverview/CardOverview.d.ts +3 -0
- package/dist/types/components/Chart/Chart.d.ts +7 -1
- package/dist/types/components/Checkbox/Checkbox.d.ts +3 -0
- package/dist/types/components/ConversationList/ConversationList.d.ts +3 -0
- package/dist/types/components/DataGroup/DataGroup.d.ts +3 -0
- package/dist/types/components/DataPreferences/DataPreferences.d.ts +1 -0
- package/dist/types/components/FilterMenu/FilterMenu.d.ts +1 -0
- package/dist/types/components/Icon/icon-cache.d.ts +6 -0
- package/dist/types/components/Icon/icon-svg.d.ts +10 -24
- package/dist/types/components/Markdown/Markdown.d.ts +4 -0
- package/dist/types/components/Menu/Menu.d.ts +3 -0
- package/dist/types/components/Message/Message.d.ts +1 -1
- package/dist/types/components/MobileSectionSwitcher/MobileSectionSwitcher.d.ts +5 -0
- package/dist/types/components/Modal/Modal.d.ts +3 -0
- package/dist/types/components/PanelNav/PanelNav.d.ts +8 -1
- package/dist/types/components/Radio/Radio.d.ts +1 -0
- package/dist/types/components/ScrollOverlay/ScrollOverlay.d.ts +1 -0
- package/dist/types/components/Select/Select.d.ts +8 -0
- package/dist/types/components/ShellApp/ShellApp.d.ts +3 -0
- package/dist/types/components/Slider/Slider.d.ts +1 -1
- package/dist/types/components/Switch/Switch.d.ts +2 -0
- package/dist/types/components/Table/Table.d.ts +35 -0
- package/dist/types/components/Table/index.d.ts +1 -1
- package/dist/types/components/Table/table-body-renderer.d.ts +1 -0
- package/dist/types/components/Table/table-cell-editor.d.ts +4 -0
- package/dist/types/components/Table/table-grid-model.d.ts +23 -0
- package/dist/types/components/Table/table-layout-controller.d.ts +1 -0
- package/dist/types/components/Table/table-model.d.ts +1 -1
- package/dist/types/components/Table/table-review-fixtures.d.ts +10 -0
- package/dist/types/components/Table/table-row-view.d.ts +11 -2
- package/dist/types/components/Table/table-skeleton-view.d.ts +3 -1
- package/dist/types/components/Table/table-structure.d.ts +38 -0
- package/dist/types/components/Table/table-types.d.ts +71 -1
- package/dist/types/components/Table/table-virtual-controller.d.ts +1 -0
- package/dist/types/components/TooltipChart/TooltipChart.d.ts +4 -0
- package/dist/types/components.d.ts +98 -12
- package/dist/types/shell/chrome-transition.d.ts +1 -0
- package/dist/types/utils/anchored-position-controller.d.ts +2 -0
- package/dist/types/utils/clock-time.d.ts +1 -0
- package/dist/types/utils/connection-tasks.d.ts +13 -0
- package/dist/types/utils/date-filter-value.d.ts +2 -0
- package/dist/types/utils/form-association.d.ts +2 -1
- package/dist/types/utils/format-percentage.d.ts +1 -1
- package/dist/types/utils/intl-formatters.d.ts +4 -0
- package/dist/types/utils/resolve-css-length-px.d.ts +4 -3
- package/dist/types/utils/select-controller.d.ts +1 -0
- package/dist/vue/THIRD-PARTY-NOTICES +14 -0
- package/dist/vue/bundled-dependencies.json +13 -0
- package/dist/vue/ds-checkbox.js +1 -0
- package/dist/vue/ds-select.js +4 -0
- package/dist/vue/ds-table.js +8 -0
- package/package.json +16 -11
- package/dist/components/p-B8ZonS3S.js +0 -2
- package/dist/components/p-B8ZonS3S.js.map +0 -1
- package/dist/components/p-BDdlJZyL.js +0 -2
- package/dist/components/p-BDdlJZyL.js.map +0 -1
- package/dist/components/p-BERXvOCf.js +0 -2
- package/dist/components/p-BERXvOCf.js.map +0 -1
- package/dist/components/p-BQspV5U7.js +0 -2
- package/dist/components/p-BQspV5U7.js.map +0 -1
- package/dist/components/p-BhjqYTiY.js +0 -2
- package/dist/components/p-BhjqYTiY.js.map +0 -1
- package/dist/components/p-BjtXYFhw.js +0 -2
- package/dist/components/p-BjtXYFhw.js.map +0 -1
- package/dist/components/p-Bk3poU91.js.map +0 -1
- package/dist/components/p-BxduVc5g.js +0 -2
- package/dist/components/p-BxduVc5g.js.map +0 -1
- package/dist/components/p-C0qN3PSb.js +0 -2
- package/dist/components/p-C0qN3PSb.js.map +0 -1
- package/dist/components/p-CPamUzaj.js +0 -2
- package/dist/components/p-CPamUzaj.js.map +0 -1
- package/dist/components/p-CUDocY23.js +0 -2
- package/dist/components/p-CUDocY23.js.map +0 -1
- package/dist/components/p-CX6ZxUQT.js +0 -2
- package/dist/components/p-CX6ZxUQT.js.map +0 -1
- package/dist/components/p-Cgn-BORA.js +0 -2
- package/dist/components/p-Cgn-BORA.js.map +0 -1
- package/dist/components/p-Ck1hgD6g.js +0 -2
- package/dist/components/p-Ck1hgD6g.js.map +0 -1
- package/dist/components/p-Co1rUdOR.js.map +0 -1
- package/dist/components/p-CochmPyg.js +0 -2
- package/dist/components/p-CochmPyg.js.map +0 -1
- package/dist/components/p-DCyJ3_PL.js +0 -2
- package/dist/components/p-DCyJ3_PL.js.map +0 -1
- package/dist/components/p-DVEx-Xg9.js +0 -2
- package/dist/components/p-DVEx-Xg9.js.map +0 -1
- package/dist/components/p-DYsE6HLk.js +0 -2
- package/dist/components/p-DYsE6HLk.js.map +0 -1
- package/dist/components/p-DebLYW7A.js +0 -2
- package/dist/components/p-DebLYW7A.js.map +0 -1
- package/dist/components/p-DjkUFlDW.js.map +0 -1
- package/dist/components/p-Dnt5xUBD.js +0 -2
- package/dist/components/p-Dnt5xUBD.js.map +0 -1
- package/dist/components/p-Dyqd-cub.js +0 -2
- package/dist/components/p-Dyqd-cub.js.map +0 -1
- package/dist/components/p-DzvTC27M.js.map +0 -1
- package/dist/components/p-g0jsfEXz.js +0 -2
- package/dist/components/p-g0jsfEXz.js.map +0 -1
- package/dist/components/p-pp1IQN4Z.js +0 -2
- package/dist/components/p-pp1IQN4Z.js.map +0 -1
- package/dist/components/p-rCeztgVn.js +0 -2
- package/dist/components/p-rCeztgVn.js.map +0 -1
- /package/dist/components/{p-CZNZTcYS.js.map → p-5-gQbIIz.js.map} +0 -0
- /package/dist/components/{p-DRdoGSGX.js.map → p-7MwoWtTJ.js.map} +0 -0
- /package/dist/components/{p-DJDbskZf.js.map → p-BO5URdZF.js.map} +0 -0
- /package/dist/components/{p-DmSnHVW6.js.map → p-BVxK8ce0.js.map} +0 -0
- /package/dist/components/{p-B0EpvBDs.js.map → p-BYeG0b5N.js.map} +0 -0
- /package/dist/components/{p-CJ1NnOEW.js.map → p-CA3iQ1ME.js.map} +0 -0
- /package/dist/components/{p-DYQa1DVF.js.map → p-C_9VGgPf.js.map} +0 -0
- /package/dist/components/{p-7TVdPhGR.js.map → p-CgQB8Ct5.js.map} +0 -0
- /package/dist/components/{p-D6FRqNhN.js.map → p-D-J7GgjY.js.map} +0 -0
- /package/dist/components/{p-DSZPO06T.js.map → p-D9_iDlkT.js.map} +0 -0
- /package/dist/components/{p-BwdnVqAY.js.map → p-DEYCF-oJ.js.map} +0 -0
- /package/dist/components/{p-C8V4lkVy.js.map → p-DKGW3gRa.js.map} +0 -0
- /package/dist/components/{p-C4huuwed.js.map → p-DR_8IPn9.js.map} +0 -0
- /package/dist/components/{p-B0BQCVkq.js.map → p-D_iZPukF.js.map} +0 -0
- /package/dist/components/{p-JBki_jyY.js.map → p-DfMTDFcE.js.map} +0 -0
- /package/dist/components/{p-MOJmB6vX.js.map → p-Dg6hnCc2.js.map} +0 -0
- /package/dist/components/{p-DXk1eml1.js.map → p-DjbT9kKm.js.map} +0 -0
- /package/dist/components/{p-_IqSPt5V.js.map → p-Dr5rkAhe.js.map} +0 -0
- /package/dist/components/{p-DIQiHX0q.js.map → p-DvGrsLgt.js.map} +0 -0
- /package/dist/components/{p-Doa2blZT.js.map → p-PtxCwf5S.js.map} +0 -0
- /package/dist/components/{p-B9TM6FqC.js.map → p-d76Ry5nh.js.map} +0 -0
- /package/dist/components/{p-BFHpU-uU.js.map → p-kwS-vkau.js.map} +0 -0
- /package/dist/components/{p-B39iJO8F.js.map → p-rmPIcWED.js.map} +0 -0
|
@@ -725,7 +725,7 @@
|
|
|
725
725
|
},
|
|
726
726
|
{
|
|
727
727
|
"path": "src/wc/components/ShellApp/ShellApp.tsx",
|
|
728
|
-
"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-page-title, ds-bar-title[placement=\"shell-bar\"]'\n ) as HTMLElement | null;\n const targets = [this.el, panelNav, bar].filter((el): el is HTMLElement => el !== null);\n\n const clearLayoutVars = () => {\n if (panelNav) panelNav.style.removeProperty(SHELL_GRADIENT_POSITION_PANEL_VAR);\n if (bar) bar.style.removeProperty(SHELL_GRADIENT_POSITION_BAR_VAR);\n };\n\n const preset = normalizeShellGradientPreset(this.gradientPreset);\n if (this.resolvedMode === 'mobile') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n return;\n }\n\n const viewport = this.resolveViewportDimensions();\n if (preset === 'none') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n this.syncPaperTextureLayout(viewport);\n return;\n }\n\n const panelWidth = this.resolvePanelWidthPx(panelNav);\n\n const panelPosition = shellGradientPositionPanel();\n const barPosition = shellGradientPositionBar(panelWidth);\n\n this.syncChromeLayoutVars(panelNav, bar, panelPosition, barPosition);\n\n const image = buildShellRadialGradient(preset);\n\n const size = shellGradientSize({\n width: viewport.width,\n height: viewport.height,\n });\n const opacity = shellGradientPresetOpacity(preset);\n\n for (const target of targets) {\n if (target === this.el) {\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: barPosition,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n });\n continue;\n }\n\n const isBar = target === bar;\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: isBar ? null : panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: isBar ? barPosition : null,\n });\n }\n\n this.syncPaperTextureLayout(viewport);\n }\n\n private syncPaperTextureLayout(viewport: { width: number; height: number }) {\n const chrome = this.el.querySelector<HTMLElement>('.shell-app__chrome');\n const paperTexture = this.el.querySelector<HTMLElement>(\n '.shell-app__chrome > ds-paper-texture'\n );\n if (!chrome || !paperTexture) return;\n\n const chromeRect = chrome.getBoundingClientRect();\n paperTexture.style.setProperty('width', `${Math.round(viewport.width)}px`);\n paperTexture.style.setProperty('height', `${Math.round(viewport.height)}px`);\n paperTexture.style.setProperty('left', `${-Math.round(chromeRect.left)}px`);\n paperTexture.style.setProperty('top', `${-Math.round(chromeRect.top)}px`);\n }\n\n private renderPaperTexture() {\n if (!this.paperTextureLayerActive()) return null;\n return <ds-paper-texture class=\"shell-app__paper-texture\" config={this.paperTexture} />;\n }\n\n private renderManagedPanelNav() {\n const navigation = this.navigation;\n return (\n <ds-panel-nav\n navStyle={this.navStyle}\n presentation={this.sectionNavigation === 'panel' ? 'nested' : 'flat'}\n groups={this.resolvedNavigationGroups}\n breakpoint={SHELL_DESKTOP_BREAKPOINT}\n routerMode={navigation.routerMode ?? 'event'}\n activeId={navigation.activeId ?? ''}\n activeChildId={this.pageChrome.value ?? ''}\n currentUrl={navigation.currentUrl ?? this.pageChrome.currentUrl ?? ''}\n storageKey={navigation.storageKey ?? ''}\n userName={navigation.userName ?? ''}\n userInitial={navigation.userInitial ?? ''}\n accountMenuExpanded={navigation.accountMenuExpanded ?? false}\n accountMenuSurfaceOpen={navigation.accountMenuSurfaceOpen}\n dashboardLabel={navigation.dashboardLabel ?? 'Dashboard'}\n settingsLabel={navigation.settingsLabel ?? 'Settings'}\n accountLabel={navigation.accountLabel ?? 'Account'}\n dashboardNavigationLabel={navigation.dashboardNavigationLabel ?? 'Dashboard navigation'}\n settingsNavigationLabel={navigation.settingsNavigationLabel ?? 'Settings navigation'}\n navigationItemsLabel={navigation.navigationLabel ?? 'Navigation items'}\n onDsNavSelect={this.handleManagedNavSelect}\n onDsNavChildSelect={this.handleManagedNavChildSelect}\n onDsNavFooterAction={this.handleManagedNavFooterAction}\n onDsNavUserAction={this.handleManagedNavUserAction}\n />\n );\n }\n\n private renderManagedBarNav() {\n return (\n <ds-bar-nav\n navStyle={this.navStyle}\n tabs={this.resolvedPageTabs}\n value={this.pageChrome.value ?? ''}\n heading={this.pageChrome.routeHeading ?? this.pageChrome.heading}\n basePath={this.pageChrome.basePath ?? ''}\n currentUrl={this.pageChrome.currentUrl ?? this.navigation.currentUrl ?? ''}\n onDsTabChange={this.handleManagedTabChange}\n />\n );\n }\n\n private renderManagedTopBar() {\n return this.sectionNavigation === 'panel'\n ? this.renderManagedBarPageTitle()\n : this.renderManagedBarNav();\n }\n\n private renderManagedToolSlots() {\n const ids = [...new Set(this.resolvedToolItems.map(item => item.id))].sort();\n return ids.flatMap(id => [\n <div key={`tool:${id}`} class=\"shell-app__tool-slot-proxy\" slot={id}>\n <slot name={id} />\n </div>,\n <div key={`tool-view:${id}`} class=\"shell-app__tool-slot-proxy\" slot={`${id}-view`}>\n <slot name={`${id}-view`} />\n </div>,\n ]);\n }\n\n private renderManagedTools() {\n const tools = this.tools;\n return (\n <ds-shell-tools\n responsiveMode={this.resolvedMode}\n open={this.managedToolsOpen}\n activeTool={this.managedActiveTool}\n presentation={this.managedToolPresentation}\n fullscreenHeaderMode={tools.fullscreenHeaderMode ?? 'shared'}\n items={this.resolvedToolItems}\n accessories={tools.accessories ?? []}\n headers={tools.headers ?? {}}\n storageKey={tools.storageKey ?? ''}\n toolsLabel={tools.toolsLabel ?? 'Tools'}\n toolShortcutsLabel={tools.toolShortcutsLabel ?? 'Tool shortcuts'}\n inboxLabel={tools.inboxLabel ?? 'Inbox'}\n inboxNavigationLabel={tools.inboxNavigationLabel ?? 'Inbox sections'}\n onDsToolChange={this.handleManagedToolChange}\n onDsPresentationChange={this.handleManagedPresentationChange}\n onDsHeaderBack={this.handleManagedHeaderBack}\n onDsHeaderAction={this.handleManagedHeaderAction}\n onDsRailAccessoryAction={this.handleManagedRailAccessoryAction}\n >\n {this.renderManagedToolSlots()}\n </ds-shell-tools>\n );\n }\n\n private renderManagedMobileSheetNav() {\n const navigation = this.navigation;\n return (\n <ds-mobile-sheet-nav\n open={this.managedMobileSheetNavOpen}\n presentation={this.sectionNavigation === 'panel' ? 'nested' : 'flat'}\n browseContext={this.managedBrowseContext}\n dashboardGroups={navigation.dashboardGroups ?? navigation.groups ?? []}\n settingsGroups={navigation.settingsGroups ?? []}\n currentUrl={navigation.currentUrl ?? this.pageChrome.currentUrl ?? ''}\n routeSelectionActive={this.managedMobileDestination === 'area'}\n navigationLabel={navigation.navigationLabel ?? 'Application navigation'}\n dashboardLabel={navigation.dashboardLabel ?? 'Dashboard'}\n settingsLabel={navigation.settingsLabel ?? 'Settings'}\n accountLabel={navigation.accountLabel ?? 'Account'}\n showAccount={navigation.showMobileAccount ?? true}\n onDsAreaSelect={this.handleManagedAreaSelect}\n onDsNavChildSelect={this.handleManagedMobileChildSelect}\n onDsBrowseContextChange={this.handleManagedBrowseContext}\n onDsClose={this.handleManagedSheetClose}\n />\n );\n }\n\n private renderManagedMobileBarNav() {\n const item = (id: PanelToolsToolId) =>\n this.resolvedToolItems.find(candidate => candidate.id === id);\n const label = (id: PanelToolsToolId, fallback: string) =>\n item(id)?.ariaLabel ?? item(id)?.label ?? fallback;\n const directActivity = this.resolvedToolItems.some(\n candidate => !candidate.isInactive && candidate.mobileDestination === 'activity'\n );\n\n return (\n <ds-mobile-bar-nav\n hidden={this.tools.mobileBarHidden ?? false}\n activeDestination={this.managedMobileDestination}\n primaryDestination={this.managedMobilePrimaryDestination}\n currentArea={this.currentArea}\n sheetNavExpanded={this.managedMobileSheetNavOpen}\n searchLabel={label('search', 'Search')}\n activityMode={directActivity ? 'direct' : 'inbox'}\n activityLabel={label('activity', 'Activity')}\n inboxLabel={this.tools.inboxLabel ?? 'Inbox'}\n messagesLabel={label('messages', 'Messages')}\n agentsLabel={label('agents', 'Agents')}\n helpLabel={label('help', 'Help & Support')}\n searchDot={this.toolDot('search')}\n activityDot={this.toolDot('activity')}\n inboxDot={this.toolDot('stacks') || this.toolDot('activity')}\n messagesDot={this.toolDot('messages')}\n agentsDot={this.toolDot('agents')}\n onDsSheetNavToggle={this.handleManagedSheetToggle}\n onDsDestinationChange={this.handleManagedMobileDestination}\n />\n );\n }\n\n private renderManagedMobileLeading() {\n if (!this.pageChrome.showBack) {\n return <slot name=\"page-header-leading\" slot=\"leading\" />;\n }\n return [\n <ds-tooltip\n slot=\"leading\"\n label={this.pageChrome.backAriaLabel ?? 'Back'}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n icon={this.pageChrome.backIcon ?? 'ChevronLeft'}\n size=\"lg\"\n aria-label={this.pageChrome.backAriaLabel ?? 'Back'}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.dsPageBack.emit(event.detail)}\n />\n </ds-tooltip>,\n <slot name=\"page-header-leading\" slot=\"leading\" />,\n ];\n }\n\n private renderManagedMobileActions() {\n const legacyActions = this.pageChrome.mobileActions ?? [];\n const visibleActions = visibleBarTitleActions(this.resolvedHeaderActions, 'mobile');\n return [\n ...legacyActions.map((action: PanelToolsHeaderAction) => (\n <ds-tooltip\n slot=\"trailing\"\n key={action.id}\n label={action.ariaLabel}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n id={action.triggerId || undefined}\n variant=\"icon\"\n icon={action.icon}\n size=\"lg\"\n aria-label={action.ariaLabel}\n haspopup={action.haspopup}\n controls={action.controls}\n expanded={action.expanded}\n surfaceOpen={action.surfaceOpen}\n pressed={action.pressed}\n isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n </ds-tooltip>\n )),\n ...visibleActions.map(action => {\n if (action.type !== 'icon') return null;\n const button =\n (action.appearance ?? 'unfilled') === 'filled' ? (\n <ds-button-filled\n variant=\"icon\"\n icon={action.icon}\n aria-label={action.ariaLabel}\n size=\"lg\"\n intent={action.intent ?? 'brand'}\n contrast={action.contrast ?? 'bold'}\n isInactive={action.isInactive}\n isLoading={action.isLoading}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n ) : (\n <ds-button-unfilled\n variant=\"icon\"\n icon={action.icon}\n aria-label={action.ariaLabel}\n size=\"lg\"\n isInactive={action.isInactive}\n isLoading={action.isLoading}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n );\n return (\n <ds-tooltip slot=\"trailing\" key={action.id} label={action.label} side=\"bottom\" size=\"sm\">\n {button}\n </ds-tooltip>\n );\n }),\n ...(this.mobileActionMenuSections.length > 0\n ? [\n <ds-tooltip slot=\"trailing\" label=\"Page options\" side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n ref={el => {\n this.mobileActionTriggerEl = el ?? null;\n }}\n id={this.mobileActionMenuTriggerId}\n variant=\"icon\"\n icon=\"Ellipses\"\n aria-label={this.pageChrome.actionsAriaLabel ?? 'More page actions'}\n size=\"lg\"\n activeFill={false}\n hasBorder={false}\n haspopup=\"menu\"\n controls={this.mobileActionMenuId}\n expanded={this.mobileActionMenuOpen}\n surfaceOpen={this.mobileActionSurfaceOpen}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n this.mobileActionMenuInitialFocusVisible = event.detail.detail === 0;\n this.mobileActionMenuOpen = !this.mobileActionMenuOpen;\n if (this.mobileActionMenuOpen) this.mobileActionSurfaceOpen = true;\n }}\n />\n </ds-tooltip>,\n ]\n : []),\n <slot name=\"page-header-trailing\" slot=\"trailing\" />,\n ];\n }\n\n private get resolvedHeaderActions(): BarTitleActionConfigItem[] {\n return resolveBarTitleActionItems(\n this.pageChrome.actionItems,\n this.pageChrome.primaryAction ?? null,\n this.pageChrome.actions ?? []\n );\n }\n\n private get mobileActionMenuSections() {\n return overflowBarTitleActionSections(this.resolvedHeaderActions, 'mobile');\n }\n\n private closeMobileActionMenu = () => {\n this.mobileActionMenuOpen = false;\n };\n\n private handleMobileActionSelect = (event: CustomEvent<MenuItemData>) => {\n const id = String(event.detail.value ?? '');\n const action = findBarTitleAction(this.resolvedHeaderActions, id);\n if (!action || action.isInactive || ('isLoading' in action && action.isLoading)) return;\n this.mobileActionMenuOpen = false;\n this.dsPageAction.emit(id);\n 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 renderManagedBarPageTitle() {\n const page = this.pageChrome;\n return (\n <ds-bar-page-title\n data-shell-bar=\"\"\n ref={el => {\n if (el) {\n (el as HTMLElement & { actionItems?: BarTitleActionConfigItem[] }).actionItems =\n page.actionItems;\n }\n }}\n heading={page.heading ?? ''}\n showBack={page.showBack ?? false}\n backAriaLabel={page.backAriaLabel ?? 'Back'}\n sections={this.barTitleSections}\n value={page.subvalue ?? ''}\n sectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n primaryAction={page.primaryAction ?? null}\n actions={page.actions ?? []}\n actionsAriaLabel={page.actionsAriaLabel ?? 'More page actions'}\n showDivider={page.showHeaderDivider ?? true}\n showCompactDivider={page.showCompactHeaderDivider}\n onDsBack={this.handleManagedPageBack}\n onDsSectionChange={this.handleManagedSubsectionChange}\n onDsAction={this.handleManagedPageAction}\n />\n );\n }\n\n private renderManagedBarTitle(placement: BarTitlePlacement) {\n const page = this.pageChrome;\n return (\n <ds-bar-title\n ref={el => {\n if (el) {\n (el as HTMLElement & { actionItems?: BarTitleActionConfigItem[] }).actionItems =\n page.actionItems;\n }\n }}\n slot={placement === 'page' ? 'header' : undefined}\n placement={placement}\n variant={placement === 'shell-bar' ? 'compact' : 'expanded'}\n heading={page.heading ?? ''}\n description={page.description ?? ''}\n showBack={page.showBack ?? false}\n backAriaLabel={page.backAriaLabel ?? 'Back'}\n backLabel={page.backLabel ?? 'Back'}\n breadcrumbs={page.breadcrumbs ?? []}\n breadcrumbAriaLabel={page.breadcrumbAriaLabel ?? 'Breadcrumb'}\n sections={this.barTitleSections}\n value={page.subvalue ?? ''}\n sectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n primaryAction={page.primaryAction ?? null}\n actions={page.actions ?? []}\n actionsAriaLabel={page.actionsAriaLabel ?? 'More page actions'}\n showDivider={page.showHeaderDivider ?? true}\n showCompactDivider={page.showCompactHeaderDivider}\n onDsBack={this.handleManagedPageBack}\n onDsBreadcrumbSelect={this.handleManagedBreadcrumbSelect}\n onDsSectionChange={this.handleManagedSubsectionChange}\n onDsAction={this.handleManagedPageAction}\n />\n );\n }\n\n private renderManagedPage() {\n const page = this.pageChrome;\n const nestedNavigation = this.sectionNavigation === 'panel';\n const mobileSections = page.showBack || nestedNavigation ? [] : this.resolvedPageTabs;\n return [\n <ds-shell-page\n responsiveMode={this.resolvedMode}\n headerCapacity={resolveManagedShellPageCapacity(this.resolvedMode)}\n headerPresentation={page.headerPresentation ?? 'auto'}\n desktopHeaderPlacement={this.sectionNavigation === 'panel' ? 'shell-bar' : 'page'}\n contentInset={page.contentInset ?? 'default'}\n contentInsetBlockStart={page.contentInsetBlockStart ?? 'default'}\n contentInsetBlockStartSize={page.contentInsetBlockStartSize}\n compactContentInsetBlockStartSize={page.compactContentInsetBlockStartSize}\n scrollCompaction={page.scrollCompaction ?? true}\n contentSurface={page.contentSurface ?? 'primary'}\n >\n {this.sectionNavigation === 'bar' ? this.renderManagedBarTitle('page') : null}\n <ds-mobile-header\n slot=\"mobile-header\"\n heading={page.heading ?? ''}\n headingLevel={page.headingLevel ?? 'h1'}\n sections={mobileSections}\n value={page.showBack ? '' : (page.value ?? '')}\n sectionsAriaLabel={page.sectionsAriaLabel ?? 'Change page section'}\n subsections={page.subsections ?? []}\n subsectionsPlacement={page.showBack || nestedNavigation ? 'combined' : 'below'}\n subvalue={page.subvalue ?? ''}\n subsectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n tone={page.tone ?? 'default'}\n onDsSectionChange={this.handleManagedTabChange}\n onDsSubsectionChange={this.handleManagedSubsectionChange}\n >\n {this.renderManagedMobileLeading()}\n {this.renderManagedMobileActions()}\n </ds-mobile-header>\n <slot />\n </ds-shell-page>,\n this.renderManagedMobileActionMenu(),\n ];\n }\n\n private renderSlottedShell(\n shellCls: Record<string, boolean>,\n mobile: boolean,\n mobileToolActive: boolean,\n mobileStageBlocked: boolean,\n fullscreen: boolean\n ) {\n return (\n <Host 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={fullscreen ? undefined : true}\n aria-hidden={\n mobile && (!mobileToolActive || this.mobileSheetNavOpen) ? 'true' : undefined\n }\n inert={mobile && (!mobileToolActive || this.mobileSheetNavOpen) ? true : undefined}\n hidden={mobile && !mobileToolActive}\n >\n <slot name=\"tools\" />\n </div>\n <div\n class=\"shell-app__content\"\n data-ds-overlay-boundary\n aria-hidden={fullscreen || mobileStageBlocked ? 'true' : undefined}\n inert={fullscreen || mobileStageBlocked ? true : undefined}\n tabIndex={0}\n >\n <slot />\n </div>\n <div\n class=\"shell-app__mobile-sheet-nav\"\n hidden={!mobile || !this.mobileSheetNavOpen}\n aria-hidden={mobile && this.mobileSheetNavOpen ? undefined : 'true'}\n inert={mobile && this.mobileSheetNavOpen ? undefined : true}\n >\n <slot name=\"mobile-sheet-nav\" />\n </div>\n <div class=\"shell-app__mobile-bar-nav\" hidden={!mobile}>\n <slot name=\"mobile-bar-nav\" />\n </div>\n </div>\n </div>\n </Host>\n );\n }\n\n render() {\n const chromeActive = this.chromeLayerActive();\n const gradientActive = this.gradientLayerActive();\n const paperTextureActive = this.paperTextureLayerActive();\n const mobile = this.resolvedMode === 'mobile';\n const mobileToolActive = mobile && this.effectiveMobileDestination !== 'area';\n const mobileStageBlocked = mobile && (mobileToolActive || this.effectiveMobileSheetNavOpen);\n const fullscreen = !mobile && this.toolsFullscreen;\n const shellCls: Record<string, boolean> = {\n 'shell-app': true,\n 'shell-app--chrome': chromeActive,\n 'shell-app--gradient': gradientActive,\n 'shell-app--paper-texture': paperTextureActive,\n [`shell-app--${this.navStyle}`]: true,\n [`shell-app--${this.resolvedMode}`]: true,\n 'shell-app--tools-fullscreen': fullscreen,\n 'shell-app--mobile-tool-active': mobileToolActive,\n 'shell-app--mobile-sheet-nav-open': mobile && this.effectiveMobileSheetNavOpen,\n 'shell-app--managed': this.managed,\n 'shell-app--slotted': !this.managed,\n };\n\n if (!this.managed) {\n return this.renderSlottedShell(\n shellCls,\n mobile,\n mobileToolActive,\n mobileStageBlocked,\n fullscreen\n );\n }\n\n return (\n <Host 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={fullscreen ? undefined : true}\n aria-hidden={\n mobile && (!mobileToolActive || this.effectiveMobileSheetNavOpen)\n ? 'true'\n : undefined\n }\n inert={\n mobile && (!mobileToolActive || this.effectiveMobileSheetNavOpen) ? true : undefined\n }\n hidden={mobile && !mobileToolActive}\n >\n {this.renderManagedTools()}\n </div>\n <div\n class=\"shell-app__content\"\n data-ds-overlay-boundary\n aria-hidden={fullscreen || mobileStageBlocked ? 'true' : undefined}\n inert={fullscreen || mobileStageBlocked ? true : undefined}\n tabIndex={0}\n >\n {this.renderManagedPage()}\n </div>\n <div\n class=\"shell-app__mobile-sheet-nav\"\n hidden={!mobile || !this.effectiveMobileSheetNavOpen}\n aria-hidden={mobile && this.effectiveMobileSheetNavOpen ? undefined : 'true'}\n inert={mobile && this.effectiveMobileSheetNavOpen ? undefined : true}\n >\n {this.renderManagedMobileSheetNav()}\n </div>\n <div class=\"shell-app__mobile-bar-nav\" hidden={!mobile}>\n {this.renderManagedMobileBarNav()}\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
728
|
+
"content": "import {\n Component,\n Prop,\n Element,\n Event,\n EventEmitter,\n State,\n Watch,\n Listen,\n Method,\n h,\n Host,\n} from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport { ConnectionTasks } from '../../utils/connection-tasks';\nimport { isEditableShortcutTarget, resolveShellShortcut } from '../../shell/shell-shortcuts';\nimport type {\n PanelToolsHeaderAction,\n PanelToolsRailAccessoryActionDetail,\n PanelToolsToolId,\n} from '../PanelTools/panel-tools-types';\nimport { PANEL_TOOLS_DEFAULT_ITEMS } from '../PanelTools/panel-tools-types';\nimport type { BarTitleActionConfigItem, BarTitleSectionItem } from '../BarTitle/bar-title-types';\nimport type { BarTitlePlacement } from '../BarTitle/bar-title-types';\nimport {\n findBarTitleAction,\n overflowBarTitleActionSections,\n resolveBarTitleActionItems,\n visibleBarTitleActions,\n} from '../BarTitle/bar-title-actions';\nimport type { BarNavTab } from '../BarNav/bar-nav-types';\nimport type { BreadcrumbSelectDetail } from '../Breadcrumb/breadcrumb-types';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport {\n DEFAULT_SHELL_GRADIENT_PRESET,\n normalizeShellGradientPreset,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\nimport {\n CHROME_TRANSITION_END,\n CHROME_TRANSITION_START,\n ChromeTransitionDepth,\n createRafCoalescer,\n readChromeTransitionSource,\n} from '../../shell/chrome-transition';\nimport {\n isPanelNavCollapsed,\n panelWidthPxFromTokens,\n readPanelNavWidthTokens,\n type PanelNavWidthTokens,\n} from '../../shell/shell-chrome-metrics';\nimport {\n SHELL_GRADIENT_IMAGE_VAR,\n SHELL_GRADIENT_OPACITY_VAR,\n SHELL_GRADIENT_POSITION_BAR_VAR,\n SHELL_GRADIENT_POSITION_PANEL_VAR,\n SHELL_GRADIENT_SIZE_VAR,\n buildShellRadialGradient,\n readShellViewportDimensions,\n shellGradientPositionBar,\n shellGradientPositionPanel,\n shellGradientPresetOpacity,\n shellGradientSize,\n} from '../../shell/shell-gradient';\nimport {\n itemUsesShellInbox,\n resolveAvailableInboxTool,\n resolveManagedShellPageCapacity,\n resolveShellResponsiveMode,\n SHELL_DESKTOP_BREAKPOINT,\n 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 readonly connectionTasks = new ConnectionTasks(() => this.el.isConnected);\n private foregroundRefreshTimer: number | null = null;\n private readonly panelNavTransition = new ChromeTransitionDepth();\n private readonly chromeSyncCoalescer = createRafCoalescer(() => this.syncChrome());\n private panelWidthTokens: PanelNavWidthTokens = { expandedPx: 0, collapsedPx: 0 };\n private cachedViewportWidth = 0;\n private cachedViewportHeight = 0;\n private onWindowResize = () => {\n this.updateResponsiveMode();\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n };\n private onVisualViewportChange = () => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n };\n\n private get managed(): boolean {\n return this.composition === 'managed';\n }\n\n private get effectiveMobileDestination(): MobileDestination {\n return this.managed ? this.managedMobileDestination : this.mobileDestination;\n }\n\n private get effectiveMobileSheetNavOpen(): boolean {\n return this.managed ? this.managedMobileSheetNavOpen : this.mobileSheetNavOpen;\n }\n\n private get resolvedNavigationGroups() {\n if (this.navigation.groups?.length) return this.navigation.groups;\n return this.managedBrowseContext === 'settings'\n ? (this.navigation.settingsGroups ?? [])\n : (this.navigation.dashboardGroups ?? []);\n }\n\n private get resolvedToolItems() {\n return this.tools.items ?? PANEL_TOOLS_DEFAULT_ITEMS;\n }\n\n private get resolvedPageTabs(): BarNavTab[] {\n const children = this.currentArea.children;\n if (!children?.length) return this.pageChrome.tabs ?? [];\n return children.map(child => ({\n id: child.id,\n label: child.label,\n dot: child.dot,\n isInactive: child.isInactive,\n }));\n }\n\n private get availableInboxTools(): ShellInboxToolId[] {\n return this.resolvedToolItems\n .filter(item => !item.isInactive && itemUsesShellInbox(item))\n .map(item => item.id as ShellInboxToolId);\n }\n\n componentWillLoad() {\n if (typeof window !== 'undefined') {\n this.resolvedMode = resolveShellResponsiveMode(window.innerWidth);\n }\n this.managedBrowseContext = this.navigation.browseContext ?? this.navStyle;\n this.managedMobileDestination = this.mobileDestination;\n this.managedMobileSheetNavOpen = this.mobileSheetNavOpen;\n this.managedInboxTool = resolveAvailableInboxTool('', this.availableInboxTools);\n }\n\n connectedCallback() {\n if (this.hasLoaded) this.connectResources();\n }\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.connectResources();\n }\n\n private connectResources() {\n if (!this.el.isConnected) return;\n this.updateResponsiveMode();\n this.syncSlottedNavStyle();\n this.syncSlottedMobileState();\n this.connectMetricsObserver();\n this.connectViewportListeners();\n this.connectPageLifecycleListeners();\n this.el.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n this.el.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n this.connectionTasks.frame(() => {\n this.cachePanelWidthTokens();\n this.scheduleChromeSync();\n });\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools');\n this.toolsFullscreen = tools?.getAttribute('presentation') === 'fullscreen';\n }\n\n @Listen('dsPresentationChange')\n handleToolsPresentation(event: CustomEvent<{ presentation?: string }>) {\n this.toolsFullscreen = event.detail?.presentation === 'fullscreen';\n }\n\n disconnectedCallback() {\n this.connectionTasks.cancel();\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.disconnectViewportListeners();\n this.disconnectPageLifecycleListeners();\n this.cancelForegroundRefresh();\n this.chromeSyncCoalescer.cancel();\n this.panelNavTransition.reset();\n this.el.removeEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n this.el.removeEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n }\n\n @Watch('navStyle')\n @Watch('gradientPreset')\n @Watch('paperTexture')\n @Watch('sectionNavigation')\n onShellPropsChange() {\n this.syncSlottedNavStyle();\n this.scheduleChromeSync();\n }\n\n @Watch('navigation')\n handleNavigationChange(next: ShellNavigationConfig, previous: ShellNavigationConfig) {\n this.managedBrowseContext = this.navigation.browseContext ?? this.navStyle;\n if (next.currentUrl !== previous.currentUrl || next.activeId !== previous.activeId) {\n this.managedMobilePrimaryDestination = 'area';\n }\n }\n\n @Watch('pageChrome')\n handlePageChromeChange() {\n if (this.mobileActionMenuSections.length === 0) {\n this.mobileActionMenuOpen = false;\n this.mobileActionSurfaceOpen = false;\n }\n }\n\n @Watch('tools')\n handleToolsChange() {\n if (!this.resolvedToolItems.some(item => item.id === 'help' && !item.isInactive)) {\n this.managedMobilePrimaryDestination = 'area';\n }\n if (\n this.managedActiveTool &&\n !this.resolvedToolItems.some(item => item.id === this.managedActiveTool)\n ) {\n this.managedToolsOpen = false;\n this.managedActiveTool = '';\n this.managedMobileDestination = 'area';\n }\n this.managedInboxTool = resolveAvailableInboxTool(\n this.managedInboxTool,\n this.availableInboxTools\n );\n }\n\n @Watch('mobileDestination')\n @Watch('mobileSheetNavOpen')\n onMobileStateChange(_next: unknown, previous: unknown) {\n if (this.managed) {\n this.managedMobileDestination = this.mobileDestination;\n this.managedMobileSheetNavOpen = this.mobileSheetNavOpen;\n }\n this.syncSlottedMobileState();\n if (previous === true && !this.effectiveMobileSheetNavOpen && this.resolvedMode === 'mobile') {\n this.connectionTasks.frame(() => {\n const bar = this.el.querySelector('ds-mobile-bar-nav') as\n | (HTMLElement & {\n focusDestination?: (destination: MobileDestination | 'sheet-nav') => Promise<void>;\n })\n | null;\n const activeElement = document.activeElement;\n if (activeElement instanceof HTMLElement && bar?.contains(activeElement)) return;\n void bar?.focusDestination?.('sheet-nav');\n });\n }\n }\n\n private updateResponsiveMode() {\n if (!this.el.isConnected || typeof window === 'undefined') return;\n const next = resolveShellResponsiveMode(window.innerWidth);\n if (next === this.resolvedMode) return;\n this.resolvedMode = next;\n this.syncSlottedMobileState();\n this.scheduleChromeSync();\n if (this.hasLoaded) this.dsResponsiveModeChange.emit({ mode: next });\n }\n\n private onChromeTransitionStart = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source !== 'panel-nav') return;\n\n this.panelNavTransition.enter();\n const viewport = readShellViewportDimensions();\n this.cachedViewportWidth = viewport.width;\n this.cachedViewportHeight = viewport.height;\n this.syncChrome();\n };\n\n private onChromeTransitionEnd = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source !== 'panel-nav') return;\n\n this.panelNavTransition.exit();\n if (!this.panelNavTransition.isActive) {\n this.scheduleChromeSync();\n }\n };\n\n /** Coalesce ResizeObserver bursts to one layout read per frame. */\n private scheduleChromeSync() {\n if (!this.el.isConnected) return;\n this.chromeSyncCoalescer.schedule();\n }\n\n private syncSlottedNavStyle() {\n const panel = this.el.querySelector('ds-panel-nav') as\n | (HTMLElement & { navStyle: NavChromeStyle })\n | null;\n const bar = this.el.querySelector('ds-bar-nav') as\n | (HTMLElement & { navStyle: NavChromeStyle })\n | null;\n if (panel) {\n panel.setAttribute('nav-style', this.navStyle);\n panel.navStyle = this.navStyle;\n }\n if (bar) {\n bar.setAttribute('nav-style', this.navStyle);\n bar.navStyle = this.navStyle;\n }\n }\n\n private syncSlottedMobileState() {\n const tools = this.el.querySelector('ds-shell-tools') as\n | (HTMLElement & { responsiveMode: ShellResponsiveMode })\n | null;\n const bar = this.el.querySelector('ds-mobile-bar-nav') as\n | (HTMLElement & {\n activeDestination: MobileDestination;\n sheetNavExpanded: boolean;\n })\n | null;\n const sheetNav = this.el.querySelector('ds-mobile-sheet-nav') as\n | (HTMLElement & { open: boolean })\n | null;\n\n if (tools) {\n tools.setAttribute('responsive-mode', this.resolvedMode);\n tools.responsiveMode = this.resolvedMode;\n }\n if (bar) {\n bar.activeDestination = this.effectiveMobileDestination;\n bar.sheetNavExpanded = this.effectiveMobileSheetNavOpen;\n }\n if (sheetNav) {\n sheetNav.open = this.effectiveMobileSheetNavOpen;\n }\n }\n\n private connectMetricsObserver() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = new ResizeObserver(() => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n });\n this.resizeObserver.observe(this.el);\n const panelWrap = this.el.querySelector('.shell-app__panel');\n if (panelWrap) this.resizeObserver.observe(panelWrap);\n }\n\n private connectViewportListeners() {\n if (typeof window === 'undefined') return;\n\n window.addEventListener('resize', this.onWindowResize, { passive: true });\n\n const visual = window.visualViewport;\n if (visual) {\n visual.addEventListener('resize', this.onVisualViewportChange, { passive: true });\n visual.addEventListener('scroll', this.onVisualViewportChange, { passive: true });\n }\n }\n\n private disconnectViewportListeners() {\n if (typeof window === 'undefined') return;\n\n window.removeEventListener('resize', this.onWindowResize);\n\n const visual = window.visualViewport;\n if (visual) {\n visual.removeEventListener('resize', this.onVisualViewportChange);\n visual.removeEventListener('scroll', this.onVisualViewportChange);\n }\n }\n\n private connectPageLifecycleListeners() {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n document.addEventListener('visibilitychange', this.onDocumentVisibilityChange);\n window.addEventListener('pageshow', this.onPageShow);\n }\n\n private disconnectPageLifecycleListeners() {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n document.removeEventListener('visibilitychange', this.onDocumentVisibilityChange);\n window.removeEventListener('pageshow', this.onPageShow);\n }\n\n private onDocumentVisibilityChange = () => {\n if (!document.hidden) this.refreshAfterForegroundRestore();\n };\n\n private onPageShow = (event: PageTransitionEvent) => {\n if (event.persisted) this.refreshAfterForegroundRestore();\n };\n\n private cancelForegroundRefresh() {\n if (this.foregroundRefreshTimer !== null) {\n window.clearTimeout(this.foregroundRefreshTimer);\n this.foregroundRefreshTimer = null;\n }\n this.el.classList.remove('shell-app--foreground-refresh');\n }\n\n private refreshAfterForegroundRestore() {\n if (!this.el.isConnected) return;\n this.updateResponsiveMode();\n this.syncSlottedMobileState();\n this.scheduleChromeSync();\n\n if (this.resolvedMode !== 'mobile') return;\n\n /*\n * Mobile WebKit can restore a live, correctly sized DOM without repainting\n * its suspended shell layer. Re-promote the complete mobile stage for one\n * painted frame; routed and tool owners remain mounted and keep their state.\n */\n this.cancelForegroundRefresh();\n this.el.classList.add('shell-app--foreground-refresh');\n void this.el.offsetHeight;\n // A short timer spans a real compositor commit; resumed WebKit may coalesce\n // nested animation frames before presenting either one.\n this.foregroundRefreshTimer = window.setTimeout(() => {\n this.foregroundRefreshTimer = null;\n this.el.classList.remove('shell-app--foreground-refresh');\n // WebKit otherwise may keep presenting the retired composite layer.\n void this.el.querySelector<HTMLElement>('.shell-app__main')?.offsetHeight;\n }, ShellApp.FOREGROUND_REFRESH_MS);\n }\n\n private cachePanelWidthTokens() {\n const navRoot = this.el.querySelector('ds-panel-nav .panel-nav') as HTMLElement | null;\n if (!navRoot) return;\n this.panelWidthTokens = readPanelNavWidthTokens(navRoot);\n }\n\n private applyGradientVars(target: HTMLElement, vars: Record<string, string | null>) {\n const style = target.style;\n for (const [name, value] of Object.entries(vars)) {\n if (value === null) style.removeProperty(name);\n else style.setProperty(name, value);\n }\n }\n\n private clearGradientPaintVars(targets: HTMLElement[]) {\n const keys = [SHELL_GRADIENT_IMAGE_VAR, SHELL_GRADIENT_SIZE_VAR, SHELL_GRADIENT_OPACITY_VAR];\n for (const target of targets) {\n for (const key of keys) target.style.removeProperty(key);\n }\n }\n\n /** Panel/bar wash offsets for scroll fades and badge rings. */\n private syncChromeLayoutVars(\n panel: HTMLElement | null,\n bar: HTMLElement | null,\n panelPosition: string,\n barPosition: string\n ) {\n if (panel) {\n panel.style.setProperty(SHELL_GRADIENT_POSITION_PANEL_VAR, panelPosition);\n }\n if (bar) {\n bar.style.setProperty(SHELL_GRADIENT_POSITION_BAR_VAR, barPosition);\n }\n }\n\n private resolvePanelWidthPx(panelNav: HTMLElement | null): number {\n const navRoot = panelNav?.querySelector('.panel-nav') as HTMLElement | null;\n\n if (this.panelNavTransition.isActive && this.panelWidthTokens.expandedPx > 0) {\n const collapsed = isPanelNavCollapsed(panelNav, navRoot);\n return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);\n }\n\n const panelWrap = this.el.querySelector('.shell-app__panel') as HTMLElement | null;\n const measured = panelWrap?.getBoundingClientRect().width ?? 0;\n if (measured > 0) return measured;\n\n if (this.panelWidthTokens.expandedPx > 0) {\n const collapsed = isPanelNavCollapsed(panelNav, navRoot);\n return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);\n }\n\n return 0;\n }\n\n /** Fixed-attachment wash/grid size — always the viewport, never the shell element box. */\n private resolveViewportDimensions(): { width: number; height: number } {\n if (this.panelNavTransition.isActive && this.cachedViewportWidth > 0) {\n return {\n width: this.cachedViewportWidth,\n height: this.cachedViewportHeight,\n };\n }\n\n return readShellViewportDimensions();\n }\n\n private chromeLayerActive(): boolean {\n return this.gradientLayerActive() || this.paperTextureLayerActive();\n }\n\n private gradientLayerActive(): boolean {\n return (\n this.resolvedMode !== 'mobile' && normalizeShellGradientPreset(this.gradientPreset) !== 'none'\n );\n }\n\n private paperTextureLayerActive(): boolean {\n return this.resolvedMode !== 'mobile' && this.paperTexture != null;\n }\n\n @Listen('keydown', { target: 'window', capture: true })\n handleWindowKeyDown(e: KeyboardEvent) {\n if (!this.shortcutsEnabled) return;\n if (isEditableShortcutTarget(e.target)) return;\n\n const action = resolveShellShortcut(e);\n if (!action) return;\n\n e.preventDefault();\n\n const panel = this.el.querySelector('ds-panel-nav') as HTMLDsPanelNavElement | null;\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & {\n closeDrawer?: () => Promise<void>;\n activateTool?: (id: PanelToolsToolId) => Promise<void>;\n })\n | null;\n\n if (action === 'toggle-panel-nav') {\n void panel?.toggleCollapsed();\n return;\n }\n\n if (action === 'close-panel-tools') {\n void tools?.closeDrawer?.();\n return;\n }\n\n if (action.startsWith('open-tool:') && tools) {\n const toolId = action.slice('open-tool:'.length) as PanelToolsToolId;\n void tools.activateTool?.(toolId);\n }\n }\n\n /** Open or toggle a managed global tool from application-owned UI. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & { activateTool?: (tool: PanelToolsToolId) => Promise<void> })\n | null;\n await tools?.activateTool?.(id);\n }\n\n /** Close whichever managed global surface is currently visible. */\n @Method()\n async closeGlobalSurface() {\n if (this.managedMobileSheetNavOpen) {\n this.managedMobileSheetNavOpen = false;\n return;\n }\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & { closeDrawer?: () => Promise<void> })\n | null;\n await tools?.closeDrawer?.();\n }\n\n /** Request drawer or fullscreen presentation for the active managed tool. */\n @Method()\n async setToolPresentation(presentation: 'drawer' | 'fullscreen') {\n this.managedToolPresentation = presentation;\n this.toolsFullscreen = presentation === 'fullscreen';\n }\n\n private handleManagedNavSelect = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsNavSelect.emit(event.detail);\n };\n\n private handleManagedNavChildSelect = (event: CustomEvent<PanelNavChildSelectDetail>) => {\n event.stopPropagation();\n this.dsNavChildSelect.emit(event.detail);\n };\n\n private handleManagedTabChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsTabChange.emit(event.detail);\n };\n\n private handleManagedPageBack = (event: CustomEvent<MouseEvent>) => {\n event.stopPropagation();\n this.dsPageBack.emit(event.detail);\n };\n\n private handleManagedBreadcrumbSelect = (event: CustomEvent<BreadcrumbSelectDetail>) => {\n event.stopPropagation();\n const forwarded = this.dsBreadcrumbSelect.emit(event.detail);\n if (forwarded.defaultPrevented) event.preventDefault();\n };\n\n private handleManagedSubsectionChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsSubsectionChange.emit(event.detail);\n };\n\n private handleManagedPageAction = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsPageAction.emit(event.detail);\n };\n\n private handleManagedToolChange = (\n event: CustomEvent<{ id: PanelToolsToolId; selected: boolean }>\n ) => {\n event.stopPropagation();\n const { id, selected } = event.detail;\n this.managedActiveTool = id;\n this.managedToolsOpen = selected;\n const item = this.resolvedToolItems.find(candidate => candidate.id === id);\n if (item && itemUsesShellInbox(item)) {\n this.managedInboxTool = id;\n }\n this.managedMobileDestination = selected\n ? (item?.mobileDestination ?? shellMobileDestinationForTool(true, id))\n : 'area';\n this.managedMobileSheetNavOpen = false;\n this.dsToolChange.emit(event.detail);\n };\n\n private handleManagedPresentationChange = (\n event: CustomEvent<{ presentation: 'drawer' | 'fullscreen' }>\n ) => {\n event.stopPropagation();\n this.managedToolPresentation = event.detail.presentation;\n this.toolsFullscreen = event.detail.presentation === 'fullscreen';\n this.dsPresentationChange.emit(event.detail);\n };\n\n private handleManagedHeaderBack = (event: CustomEvent<{ tool: PanelToolsToolId }>) => {\n event.stopPropagation();\n this.dsHeaderBack.emit(event.detail);\n };\n\n private handleManagedHeaderAction = (\n event: CustomEvent<{ tool: PanelToolsToolId; id: string }>\n ) => {\n event.stopPropagation();\n this.dsHeaderAction.emit(event.detail);\n };\n\n private handleManagedRailAccessoryAction = (\n event: CustomEvent<PanelToolsRailAccessoryActionDetail>\n ) => {\n event.stopPropagation();\n this.dsRailAccessoryAction.emit(event.detail);\n };\n\n private handleManagedSheetToggle = (event: CustomEvent<boolean>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = event.detail;\n };\n\n private activateManagedMobileTool(destination: Exclude<MobileDestination, 'area'>): boolean {\n const canonicalId =\n destination === 'inbox'\n ? resolveAvailableInboxTool(this.managedInboxTool, this.availableInboxTools)\n : destination;\n if (!canonicalId) return false;\n\n const item = this.resolvedToolItems.find(\n candidate =>\n candidate.id === canonicalId ||\n (destination !== 'inbox' && candidate.mobileDestination === destination)\n );\n if (!item || item.isInactive) return false;\n\n const id = item.id;\n this.managedActiveTool = id;\n this.managedToolsOpen = true;\n if (itemUsesShellInbox(item)) this.managedInboxTool = id;\n if (destination === 'help') this.managedMobilePrimaryDestination = 'help';\n this.dsToolChange.emit({ id: id as PanelToolsToolId, selected: true });\n return true;\n }\n\n private handleManagedMobileDestination = (event: CustomEvent<MobileBarNavDestinationDetail>) => {\n event.stopPropagation();\n const destination = event.detail.destination;\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = destination;\n\n if (destination === 'area') {\n this.managedToolsOpen = false;\n return;\n }\n\n this.activateManagedMobileTool(destination);\n };\n\n private handleManagedAreaSelect = (event: CustomEvent<string>) => {\n event.stopPropagation();\n if (event.detail === 'help') {\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'help';\n if (this.activateManagedMobileTool('help')) return;\n }\n\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'area';\n this.managedMobilePrimaryDestination = 'area';\n this.managedToolsOpen = false;\n this.dsNavSelect.emit(event.detail);\n };\n\n private handleManagedMobileChildSelect = (event: CustomEvent<PanelNavChildSelectDetail>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'area';\n this.managedMobilePrimaryDestination = 'area';\n this.managedToolsOpen = false;\n this.dsNavChildSelect.emit(event.detail);\n };\n\n private handleManagedBrowseContext = (event: CustomEvent<NavChromeStyle>) => {\n event.stopPropagation();\n this.managedBrowseContext = event.detail;\n this.dsBrowseContextChange.emit(event.detail);\n };\n\n private handleManagedSheetClose = (event: CustomEvent<void>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = false;\n };\n\n private handleManagedNavFooterAction = (event: CustomEvent<void>) => {\n event.stopPropagation();\n this.dsNavFooterAction.emit();\n };\n\n private handleManagedNavUserAction = (event: CustomEvent<PanelNavUserActionDetail>) => {\n event.stopPropagation();\n this.dsNavUserAction.emit(event.detail);\n };\n\n private get currentArea(): PanelNavItem {\n const groups = [\n ...(this.navigation.dashboardGroups ?? []),\n ...(this.navigation.settingsGroups ?? []),\n ...this.resolvedNavigationGroups,\n ];\n const items = groups.flatMap(group => group.items);\n const activeId =\n this.navigation.activeId ||\n deriveActiveIdFromUrl(this.navigation.currentUrl ?? this.pageChrome.currentUrl ?? '', items);\n return (\n items.find(item => item.id === activeId) ??\n items[0] ?? {\n id: 'area',\n icon: 'MapPage',\n label: this.pageChrome.heading || 'Area',\n }\n );\n }\n\n private toolDot(id: PanelToolsToolId): boolean {\n return this.resolvedToolItems.find(item => item.id === id)?.dot ?? false;\n }\n\n private syncChrome() {\n if (!this.el.isConnected) return;\n const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;\n const bar = this.el.querySelector(\n 'ds-bar-nav, ds-bar-page-title, ds-bar-title[placement=\"shell-bar\"]'\n ) as HTMLElement | null;\n const targets = [this.el, panelNav, bar].filter((el): el is HTMLElement => el !== null);\n\n const clearLayoutVars = () => {\n if (panelNav) panelNav.style.removeProperty(SHELL_GRADIENT_POSITION_PANEL_VAR);\n if (bar) bar.style.removeProperty(SHELL_GRADIENT_POSITION_BAR_VAR);\n };\n\n const preset = normalizeShellGradientPreset(this.gradientPreset);\n if (this.resolvedMode === 'mobile') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n return;\n }\n\n const viewport = this.resolveViewportDimensions();\n if (preset === 'none') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n this.syncPaperTextureLayout(viewport);\n return;\n }\n\n const panelWidth = this.resolvePanelWidthPx(panelNav);\n\n const panelPosition = shellGradientPositionPanel();\n const barPosition = shellGradientPositionBar(panelWidth);\n\n this.syncChromeLayoutVars(panelNav, bar, panelPosition, barPosition);\n\n const image = buildShellRadialGradient(preset);\n\n const size = shellGradientSize({\n width: viewport.width,\n height: viewport.height,\n });\n const opacity = shellGradientPresetOpacity(preset);\n\n for (const target of targets) {\n if (target === this.el) {\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: barPosition,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n });\n continue;\n }\n\n const isBar = target === bar;\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: isBar ? null : panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: isBar ? barPosition : null,\n });\n }\n\n this.syncPaperTextureLayout(viewport);\n }\n\n private syncPaperTextureLayout(viewport: { width: number; height: number }) {\n const chrome = this.el.querySelector<HTMLElement>('.shell-app__chrome');\n const paperTexture = this.el.querySelector<HTMLElement>(\n '.shell-app__chrome > ds-paper-texture'\n );\n if (!chrome || !paperTexture) return;\n\n const chromeRect = chrome.getBoundingClientRect();\n paperTexture.style.setProperty('width', `${Math.round(viewport.width)}px`);\n paperTexture.style.setProperty('height', `${Math.round(viewport.height)}px`);\n paperTexture.style.setProperty('left', `${-Math.round(chromeRect.left)}px`);\n paperTexture.style.setProperty('top', `${-Math.round(chromeRect.top)}px`);\n }\n\n private renderPaperTexture() {\n if (!this.paperTextureLayerActive()) return null;\n return <ds-paper-texture class=\"shell-app__paper-texture\" config={this.paperTexture} />;\n }\n\n private renderManagedPanelNav() {\n const navigation = this.navigation;\n return (\n <ds-panel-nav\n navStyle={this.navStyle}\n presentation={this.sectionNavigation === 'panel' ? 'nested' : 'flat'}\n groups={this.resolvedNavigationGroups}\n breakpoint={SHELL_DESKTOP_BREAKPOINT}\n routerMode={navigation.routerMode ?? 'event'}\n activeId={navigation.activeId ?? ''}\n activeChildId={this.pageChrome.value ?? ''}\n currentUrl={navigation.currentUrl ?? this.pageChrome.currentUrl ?? ''}\n storageKey={navigation.storageKey ?? ''}\n userName={navigation.userName ?? ''}\n userInitial={navigation.userInitial ?? ''}\n accountMenuExpanded={navigation.accountMenuExpanded ?? false}\n accountMenuSurfaceOpen={navigation.accountMenuSurfaceOpen}\n dashboardLabel={navigation.dashboardLabel ?? 'Dashboard'}\n settingsLabel={navigation.settingsLabel ?? 'Settings'}\n accountLabel={navigation.accountLabel ?? 'Account'}\n dashboardNavigationLabel={navigation.dashboardNavigationLabel ?? 'Dashboard navigation'}\n settingsNavigationLabel={navigation.settingsNavigationLabel ?? 'Settings navigation'}\n navigationItemsLabel={navigation.navigationLabel ?? 'Navigation items'}\n onDsNavSelect={this.handleManagedNavSelect}\n onDsNavChildSelect={this.handleManagedNavChildSelect}\n onDsNavFooterAction={this.handleManagedNavFooterAction}\n onDsNavUserAction={this.handleManagedNavUserAction}\n />\n );\n }\n\n private renderManagedBarNav() {\n return (\n <ds-bar-nav\n navStyle={this.navStyle}\n tabs={this.resolvedPageTabs}\n value={this.pageChrome.value ?? ''}\n heading={this.pageChrome.routeHeading ?? this.pageChrome.heading}\n basePath={this.pageChrome.basePath ?? ''}\n currentUrl={this.pageChrome.currentUrl ?? this.navigation.currentUrl ?? ''}\n onDsTabChange={this.handleManagedTabChange}\n />\n );\n }\n\n private renderManagedTopBar() {\n return this.sectionNavigation === 'panel'\n ? this.renderManagedBarPageTitle()\n : this.renderManagedBarNav();\n }\n\n private renderManagedToolSlots() {\n const ids = [...new Set(this.resolvedToolItems.map(item => item.id))].sort();\n return ids.flatMap(id => [\n <div key={`tool:${id}`} class=\"shell-app__tool-slot-proxy\" slot={id}>\n <slot name={id} />\n </div>,\n <div key={`tool-view:${id}`} class=\"shell-app__tool-slot-proxy\" slot={`${id}-view`}>\n <slot name={`${id}-view`} />\n </div>,\n ]);\n }\n\n private renderManagedTools() {\n const tools = this.tools;\n return (\n <ds-shell-tools\n responsiveMode={this.resolvedMode}\n open={this.managedToolsOpen}\n activeTool={this.managedActiveTool}\n presentation={this.managedToolPresentation}\n fullscreenHeaderMode={tools.fullscreenHeaderMode ?? 'shared'}\n items={this.resolvedToolItems}\n accessories={tools.accessories ?? []}\n headers={tools.headers ?? {}}\n storageKey={tools.storageKey ?? ''}\n toolsLabel={tools.toolsLabel ?? 'Tools'}\n toolShortcutsLabel={tools.toolShortcutsLabel ?? 'Tool shortcuts'}\n inboxLabel={tools.inboxLabel ?? 'Inbox'}\n inboxNavigationLabel={tools.inboxNavigationLabel ?? 'Inbox sections'}\n onDsToolChange={this.handleManagedToolChange}\n onDsPresentationChange={this.handleManagedPresentationChange}\n onDsHeaderBack={this.handleManagedHeaderBack}\n onDsHeaderAction={this.handleManagedHeaderAction}\n onDsRailAccessoryAction={this.handleManagedRailAccessoryAction}\n >\n {this.renderManagedToolSlots()}\n </ds-shell-tools>\n );\n }\n\n private renderManagedMobileSheetNav() {\n const navigation = this.navigation;\n return (\n <ds-mobile-sheet-nav\n open={this.managedMobileSheetNavOpen}\n presentation={this.sectionNavigation === 'panel' ? 'nested' : 'flat'}\n browseContext={this.managedBrowseContext}\n dashboardGroups={navigation.dashboardGroups ?? navigation.groups ?? []}\n settingsGroups={navigation.settingsGroups ?? []}\n currentUrl={navigation.currentUrl ?? this.pageChrome.currentUrl ?? ''}\n routeSelectionActive={this.managedMobileDestination === 'area'}\n navigationLabel={navigation.navigationLabel ?? 'Application navigation'}\n dashboardLabel={navigation.dashboardLabel ?? 'Dashboard'}\n settingsLabel={navigation.settingsLabel ?? 'Settings'}\n accountLabel={navigation.accountLabel ?? 'Account'}\n showAccount={navigation.showMobileAccount ?? true}\n onDsAreaSelect={this.handleManagedAreaSelect}\n onDsNavChildSelect={this.handleManagedMobileChildSelect}\n onDsBrowseContextChange={this.handleManagedBrowseContext}\n onDsClose={this.handleManagedSheetClose}\n />\n );\n }\n\n private renderManagedMobileBarNav() {\n const item = (id: PanelToolsToolId) =>\n this.resolvedToolItems.find(candidate => candidate.id === id);\n const label = (id: PanelToolsToolId, fallback: string) =>\n item(id)?.ariaLabel ?? item(id)?.label ?? fallback;\n const directActivity = this.resolvedToolItems.some(\n candidate => !candidate.isInactive && candidate.mobileDestination === 'activity'\n );\n\n return (\n <ds-mobile-bar-nav\n hidden={this.tools.mobileBarHidden ?? false}\n activeDestination={this.managedMobileDestination}\n primaryDestination={this.managedMobilePrimaryDestination}\n currentArea={this.currentArea}\n sheetNavExpanded={this.managedMobileSheetNavOpen}\n searchLabel={label('search', 'Search')}\n activityMode={directActivity ? 'direct' : 'inbox'}\n activityLabel={label('activity', 'Activity')}\n inboxLabel={this.tools.inboxLabel ?? 'Inbox'}\n messagesLabel={label('messages', 'Messages')}\n agentsLabel={label('agents', 'Agents')}\n helpLabel={label('help', 'Help & Support')}\n searchDot={this.toolDot('search')}\n activityDot={this.toolDot('activity')}\n inboxDot={this.toolDot('stacks') || this.toolDot('activity')}\n messagesDot={this.toolDot('messages')}\n agentsDot={this.toolDot('agents')}\n onDsSheetNavToggle={this.handleManagedSheetToggle}\n onDsDestinationChange={this.handleManagedMobileDestination}\n />\n );\n }\n\n private renderManagedMobileLeading() {\n if (!this.pageChrome.showBack) {\n return <slot name=\"page-header-leading\" slot=\"leading\" />;\n }\n return [\n <ds-tooltip\n slot=\"leading\"\n label={this.pageChrome.backAriaLabel ?? 'Back'}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n icon={this.pageChrome.backIcon ?? 'ChevronLeft'}\n size=\"lg\"\n aria-label={this.pageChrome.backAriaLabel ?? 'Back'}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.dsPageBack.emit(event.detail)}\n />\n </ds-tooltip>,\n <slot name=\"page-header-leading\" slot=\"leading\" />,\n ];\n }\n\n private renderManagedMobileActions() {\n const legacyActions = this.pageChrome.mobileActions ?? [];\n const visibleActions = visibleBarTitleActions(this.resolvedHeaderActions, 'mobile');\n return [\n ...legacyActions.map((action: PanelToolsHeaderAction) => (\n <ds-tooltip\n slot=\"trailing\"\n key={action.id}\n label={action.ariaLabel}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n id={action.triggerId || undefined}\n variant=\"icon\"\n icon={action.icon}\n size=\"lg\"\n aria-label={action.ariaLabel}\n haspopup={action.haspopup}\n controls={action.controls}\n expanded={action.expanded}\n surfaceOpen={action.surfaceOpen}\n pressed={action.pressed}\n isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n </ds-tooltip>\n )),\n ...visibleActions.map(action => {\n if (action.type !== 'icon') return null;\n const button =\n (action.appearance ?? 'unfilled') === 'filled' ? (\n <ds-button-filled\n variant=\"icon\"\n icon={action.icon}\n aria-label={action.ariaLabel}\n size=\"lg\"\n intent={action.intent ?? 'brand'}\n contrast={action.contrast ?? 'bold'}\n isInactive={action.isInactive}\n isLoading={action.isLoading}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n ) : (\n <ds-button-unfilled\n variant=\"icon\"\n icon={action.icon}\n aria-label={action.ariaLabel}\n size=\"lg\"\n isInactive={action.isInactive}\n isLoading={action.isLoading}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n );\n return (\n <ds-tooltip slot=\"trailing\" key={action.id} label={action.label} side=\"bottom\" size=\"sm\">\n {button}\n </ds-tooltip>\n );\n }),\n ...(this.mobileActionMenuSections.length > 0\n ? [\n <ds-tooltip slot=\"trailing\" label=\"Page options\" side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n ref={el => {\n this.mobileActionTriggerEl = el ?? null;\n }}\n id={this.mobileActionMenuTriggerId}\n variant=\"icon\"\n icon=\"Ellipses\"\n aria-label={this.pageChrome.actionsAriaLabel ?? 'More page actions'}\n size=\"lg\"\n activeFill={false}\n hasBorder={false}\n haspopup=\"menu\"\n controls={this.mobileActionMenuId}\n expanded={this.mobileActionMenuOpen}\n surfaceOpen={this.mobileActionSurfaceOpen}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n this.mobileActionMenuInitialFocusVisible = event.detail.detail === 0;\n this.mobileActionMenuOpen = !this.mobileActionMenuOpen;\n if (this.mobileActionMenuOpen) this.mobileActionSurfaceOpen = true;\n }}\n />\n </ds-tooltip>,\n ]\n : []),\n <slot name=\"page-header-trailing\" slot=\"trailing\" />,\n ];\n }\n\n private get resolvedHeaderActions(): BarTitleActionConfigItem[] {\n return resolveBarTitleActionItems(\n this.pageChrome.actionItems,\n this.pageChrome.primaryAction ?? null,\n this.pageChrome.actions ?? []\n );\n }\n\n private get mobileActionMenuSections() {\n return overflowBarTitleActionSections(this.resolvedHeaderActions, 'mobile');\n }\n\n private closeMobileActionMenu = () => {\n this.mobileActionMenuOpen = false;\n };\n\n private handleMobileActionSelect = (event: CustomEvent<MenuItemData>) => {\n const id = String(event.detail.value ?? '');\n const action = findBarTitleAction(this.resolvedHeaderActions, id);\n if (!action || action.isInactive || ('isLoading' in action && action.isLoading)) return;\n this.mobileActionMenuOpen = false;\n this.dsPageAction.emit(id);\n this.connectionTasks.frame(() => void this.mobileActionTriggerEl?.setFocus?.());\n };\n\n private renderManagedMobileActionMenu() {\n if (this.mobileActionMenuSections.length === 0) return null;\n return (\n <ds-menu\n id={this.mobileActionMenuId}\n anchorId={this.mobileActionMenuTriggerId}\n align=\"end\"\n menuLabel={this.pageChrome.actionsAriaLabel ?? 'More page actions'}\n open={this.mobileActionMenuOpen}\n initialFocusVisible={this.mobileActionMenuInitialFocusVisible}\n sections={this.mobileActionMenuSections}\n onDsClose={this.closeMobileActionMenu}\n onDsAfterClose={() => {\n if (!this.mobileActionMenuOpen) this.mobileActionSurfaceOpen = false;\n }}\n onDsSelect={this.handleMobileActionSelect}\n />\n );\n }\n\n private get barTitleSections(): BarTitleSectionItem[] {\n return (this.pageChrome.subsections ?? []).map(item =>\n 'type' in item\n ? { type: 'divider' }\n : {\n id: item.id,\n label: item.label,\n isInactive: item.isInactive,\n }\n );\n }\n\n private renderManagedBarPageTitle() {\n const page = this.pageChrome;\n return (\n <ds-bar-page-title\n data-shell-bar=\"\"\n ref={el => {\n if (el) {\n (el as HTMLElement & { actionItems?: BarTitleActionConfigItem[] }).actionItems =\n page.actionItems;\n }\n }}\n heading={page.heading ?? ''}\n showBack={page.showBack ?? false}\n backAriaLabel={page.backAriaLabel ?? 'Back'}\n sections={this.barTitleSections}\n value={page.subvalue ?? ''}\n sectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n primaryAction={page.primaryAction ?? null}\n actions={page.actions ?? []}\n actionsAriaLabel={page.actionsAriaLabel ?? 'More page actions'}\n showDivider={page.showHeaderDivider ?? true}\n showCompactDivider={page.showCompactHeaderDivider}\n onDsBack={this.handleManagedPageBack}\n onDsSectionChange={this.handleManagedSubsectionChange}\n onDsAction={this.handleManagedPageAction}\n />\n );\n }\n\n private renderManagedBarTitle(placement: BarTitlePlacement) {\n const page = this.pageChrome;\n return (\n <ds-bar-title\n ref={el => {\n if (el) {\n (el as HTMLElement & { actionItems?: BarTitleActionConfigItem[] }).actionItems =\n page.actionItems;\n }\n }}\n slot={placement === 'page' ? 'header' : undefined}\n placement={placement}\n variant={placement === 'shell-bar' ? 'compact' : 'expanded'}\n heading={page.heading ?? ''}\n description={page.description ?? ''}\n showBack={page.showBack ?? false}\n backAriaLabel={page.backAriaLabel ?? 'Back'}\n backLabel={page.backLabel ?? 'Back'}\n breadcrumbs={page.breadcrumbs ?? []}\n breadcrumbAriaLabel={page.breadcrumbAriaLabel ?? 'Breadcrumb'}\n sections={this.barTitleSections}\n value={page.subvalue ?? ''}\n sectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n primaryAction={page.primaryAction ?? null}\n actions={page.actions ?? []}\n actionsAriaLabel={page.actionsAriaLabel ?? 'More page actions'}\n showDivider={page.showHeaderDivider ?? true}\n showCompactDivider={page.showCompactHeaderDivider}\n onDsBack={this.handleManagedPageBack}\n onDsBreadcrumbSelect={this.handleManagedBreadcrumbSelect}\n onDsSectionChange={this.handleManagedSubsectionChange}\n onDsAction={this.handleManagedPageAction}\n />\n );\n }\n\n private renderManagedPage() {\n const page = this.pageChrome;\n const nestedNavigation = this.sectionNavigation === 'panel';\n const mobileSections = page.showBack || nestedNavigation ? [] : this.resolvedPageTabs;\n return [\n <ds-shell-page\n responsiveMode={this.resolvedMode}\n headerCapacity={resolveManagedShellPageCapacity(this.resolvedMode)}\n headerPresentation={page.headerPresentation ?? 'auto'}\n desktopHeaderPlacement={this.sectionNavigation === 'panel' ? 'shell-bar' : 'page'}\n contentInset={page.contentInset ?? 'default'}\n contentInsetBlockStart={page.contentInsetBlockStart ?? 'default'}\n contentInsetBlockStartSize={page.contentInsetBlockStartSize}\n compactContentInsetBlockStartSize={page.compactContentInsetBlockStartSize}\n scrollCompaction={page.scrollCompaction ?? true}\n contentSurface={page.contentSurface ?? 'primary'}\n >\n {this.sectionNavigation === 'bar' ? this.renderManagedBarTitle('page') : null}\n <ds-mobile-header\n slot=\"mobile-header\"\n heading={page.heading ?? ''}\n headingLevel={page.headingLevel ?? 'h1'}\n sections={mobileSections}\n value={page.showBack ? '' : (page.value ?? '')}\n sectionsAriaLabel={page.sectionsAriaLabel ?? 'Change page section'}\n subsections={page.subsections ?? []}\n subsectionsPlacement={page.showBack || nestedNavigation ? 'combined' : 'below'}\n subvalue={page.subvalue ?? ''}\n subsectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n tone={page.tone ?? 'default'}\n onDsSectionChange={this.handleManagedTabChange}\n onDsSubsectionChange={this.handleManagedSubsectionChange}\n >\n {this.renderManagedMobileLeading()}\n {this.renderManagedMobileActions()}\n </ds-mobile-header>\n <slot />\n </ds-shell-page>,\n this.renderManagedMobileActionMenu(),\n ];\n }\n\n private renderSlottedShell(\n shellCls: Record<string, boolean>,\n mobile: boolean,\n mobileToolActive: boolean,\n mobileStageBlocked: boolean,\n fullscreen: boolean\n ) {\n return (\n <Host 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={fullscreen ? undefined : true}\n aria-hidden={\n mobile && (!mobileToolActive || this.mobileSheetNavOpen) ? 'true' : undefined\n }\n inert={mobile && (!mobileToolActive || this.mobileSheetNavOpen) ? true : undefined}\n hidden={mobile && !mobileToolActive}\n >\n <slot name=\"tools\" />\n </div>\n <div\n class=\"shell-app__content\"\n data-ds-overlay-boundary\n aria-hidden={fullscreen || mobileStageBlocked ? 'true' : undefined}\n inert={fullscreen || mobileStageBlocked ? true : undefined}\n tabIndex={0}\n >\n <slot />\n </div>\n <div\n class=\"shell-app__mobile-sheet-nav\"\n hidden={!mobile || !this.mobileSheetNavOpen}\n aria-hidden={mobile && this.mobileSheetNavOpen ? undefined : 'true'}\n inert={mobile && this.mobileSheetNavOpen ? undefined : true}\n >\n <slot name=\"mobile-sheet-nav\" />\n </div>\n <div class=\"shell-app__mobile-bar-nav\" hidden={!mobile}>\n <slot name=\"mobile-bar-nav\" />\n </div>\n </div>\n </div>\n </Host>\n );\n }\n\n render() {\n const chromeActive = this.chromeLayerActive();\n const gradientActive = this.gradientLayerActive();\n const paperTextureActive = this.paperTextureLayerActive();\n const mobile = this.resolvedMode === 'mobile';\n const mobileToolActive = mobile && this.effectiveMobileDestination !== 'area';\n const mobileStageBlocked = mobile && (mobileToolActive || this.effectiveMobileSheetNavOpen);\n const fullscreen = !mobile && this.toolsFullscreen;\n const shellCls: Record<string, boolean> = {\n 'shell-app': true,\n 'shell-app--chrome': chromeActive,\n 'shell-app--gradient': gradientActive,\n 'shell-app--paper-texture': paperTextureActive,\n [`shell-app--${this.navStyle}`]: true,\n [`shell-app--${this.resolvedMode}`]: true,\n 'shell-app--tools-fullscreen': fullscreen,\n 'shell-app--mobile-tool-active': mobileToolActive,\n 'shell-app--mobile-sheet-nav-open': mobile && this.effectiveMobileSheetNavOpen,\n 'shell-app--managed': this.managed,\n 'shell-app--slotted': !this.managed,\n };\n\n if (!this.managed) {\n return this.renderSlottedShell(\n shellCls,\n mobile,\n mobileToolActive,\n mobileStageBlocked,\n fullscreen\n );\n }\n\n return (\n <Host 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={fullscreen ? undefined : true}\n aria-hidden={\n mobile && (!mobileToolActive || this.effectiveMobileSheetNavOpen)\n ? 'true'\n : undefined\n }\n inert={\n mobile && (!mobileToolActive || this.effectiveMobileSheetNavOpen) ? true : undefined\n }\n hidden={mobile && !mobileToolActive}\n >\n {this.renderManagedTools()}\n </div>\n <div\n class=\"shell-app__content\"\n data-ds-overlay-boundary\n aria-hidden={fullscreen || mobileStageBlocked ? 'true' : undefined}\n inert={fullscreen || mobileStageBlocked ? true : undefined}\n tabIndex={0}\n >\n {this.renderManagedPage()}\n </div>\n <div\n class=\"shell-app__mobile-sheet-nav\"\n hidden={!mobile || !this.effectiveMobileSheetNavOpen}\n aria-hidden={mobile && this.effectiveMobileSheetNavOpen ? undefined : 'true'}\n inert={mobile && this.effectiveMobileSheetNavOpen ? undefined : true}\n >\n {this.renderManagedMobileSheetNav()}\n </div>\n <div class=\"shell-app__mobile-bar-nav\" hidden={!mobile}>\n {this.renderManagedMobileBarNav()}\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
729
729
|
"type": "registry:ui"
|
|
730
730
|
},
|
|
731
731
|
{
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
"attribute": "locale",
|
|
226
226
|
"required": false,
|
|
227
227
|
"mutable": false,
|
|
228
|
-
"description": "Locale used
|
|
228
|
+
"description": "Locale used for the visible value. Defaults to English (en-US)."
|
|
229
229
|
},
|
|
230
230
|
"max": {
|
|
231
231
|
"type": "number",
|
|
@@ -490,7 +490,7 @@
|
|
|
490
490
|
"attribute": "locale",
|
|
491
491
|
"required": false,
|
|
492
492
|
"mutable": false,
|
|
493
|
-
"description": "Locale used
|
|
493
|
+
"description": "Locale used for the visible value. Defaults to English (en-US)."
|
|
494
494
|
},
|
|
495
495
|
"max": {
|
|
496
496
|
"type": "number",
|
|
@@ -717,7 +717,7 @@
|
|
|
717
717
|
},
|
|
718
718
|
{
|
|
719
719
|
"path": "src/wc/components/Slider/Slider.tsx",
|
|
720
|
-
"content": "import {\n AttachInternals,\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n restoreNumberArrayFormState,\n restoreStringFormState,\n setFormControlValue,\n setRepeatedFormControlValue,\n} from '../../utils';\n\nexport interface SliderTick {\n value: number;\n label?: string;\n}\n\nexport type SliderValue = number | number[];\nexport type SliderSize = 'md' | 'sm' | 'xs';\nexport type SliderOrientation = 'horizontal' | 'vertical';\nexport type SliderThumbAlignment = 'edge' | 'center';\n\nlet sliderId = 0;\nlet sliderExternalLabelId = 0;\n\n@Component({\n tag: 'ds-slider',\n styleUrl: 'Slider.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Slider {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Current value. Assign a two-number array through the JavaScript property for a range slider. */\n @Prop({ mutable: true }) value: SliderValue = 0;\n @Prop() min: number = 0;\n @Prop() max: number = 100;\n @Prop() step: number | 'any' = 1;\n /** Explicit ticks are the selectable snap points. Empty ticks with step='any' allow smooth adjustment. */\n @Prop() ticks: SliderTick[] = [];\n /** Minimum number of steps kept between two range thumbs. */\n @Prop() minStepsBetweenValues: number = 0;\n /** Visible field label. Supply aria-label when the label is intentionally hidden. */\n @Prop() label: string | undefined;\n /** Show the formatted current value beside the visible label. */\n @Prop() showValue: boolean = true;\n @Prop() size: SliderSize = 'md';\n @Prop() orientation: SliderOrientation = 'horizontal';\n /** Align the thumb edge with full-width rail endpoints, or its center with inset rail endpoints. */\n @Prop() thumbAlignment: SliderThumbAlignment = 'edge';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Keep the value focusable and submittable while preventing changes. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop() isInactive: boolean = false;\n /** Direct accessible name for a single-thumb slider without a visible label. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Id references for a visible single-thumb slider label. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n /** Id references for supporting guidance or error text. */\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n /** Accessible label for the lower thumb in a range slider. */\n @Prop() startLabel: string = 'Minimum value';\n /** Accessible label for the upper thumb in a range slider. */\n @Prop() endLabel: string = 'Maximum value';\n /** Human-readable value text for a single slider or both range thumbs. */\n @Prop() valueText: string | undefined;\n /** Per-thumb human-readable value text for a range slider. Assign through the JavaScript property. */\n @Prop() valueTexts: string[] = [];\n /** Locale used by Intl.NumberFormat for the visible value. */\n @Prop() locale: string | undefined;\n /** Intl.NumberFormat options. Assign objects through the JavaScript property. */\n @Prop() formatOptions: Intl.NumberFormatOptions | undefined;\n @Prop() valuePrefix: string = '';\n @Prop() valueSuffix: string = '';\n @Prop() rangeSeparator: string = '–';\n /** Id applied to the first native range input. */\n @Prop() inputId: string | undefined;\n\n /** Emitted continuously while the value changes. */\n @Event() dsChange!: EventEmitter<SliderValue>;\n /** Emitted when a pointer or keyboard value change is committed. */\n @Event() dsCommit!: EventEmitter<SliderValue>;\n\n @State() private formDisabled = false;\n @State() private dragging = false;\n @State() private focused = false;\n @State() private touched = false;\n @State() private activeThumbIndex = 0;\n @State() private externalLabelledby: string | undefined;\n\n private readonly generatedId = `ds-slider-${++sliderId}`;\n private readonly labelId = `${this.generatedId}-label`;\n private initialValue: SliderValue = 0;\n private lastCommittedValue: SliderValue = 0;\n private pointerId: number | null = null;\n private pointerStartValue: SliderValue = 0;\n private controlEl: HTMLDivElement | null = null;\n\n componentWillLoad() {\n this.initialValue = this.cloneValue(this.publicValue);\n this.lastCommittedValue = this.cloneValue(this.publicValue);\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.syncNativeLabels();\n }\n\n @Watch('value')\n @Watch('min')\n @Watch('max')\n @Watch('step')\n @Watch('ticks')\n @Watch('minStepsBetweenValues')\n @Watch('name')\n @Watch('disabled')\n @Watch('isInactive')\n syncFormValue() {\n const values = this.resolvedValues;\n if (this.isRange) {\n setRepeatedFormControlValue(this.internals, this.name, values, {\n inactive: this.isDisabled,\n });\n } else {\n const value = String(values[0]);\n setFormControlValue(this.internals, this.name ? value : null, {\n inactive: this.isDisabled,\n state: value,\n });\n }\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.cloneValue(this.initialValue);\n this.lastCommittedValue = this.cloneValue(this.initialValue);\n this.dragging = false;\n this.focused = false;\n this.touched = false;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n if (this.isRange) {\n const restored = restoreNumberArrayFormState(state, 2);\n this.value = restored.length ? restored : this.cloneValue(this.initialValue);\n } else {\n const restored = Number(restoreStringFormState(state, 'NaN'));\n if (Number.isFinite(restored)) this.value = restored;\n }\n this.lastCommittedValue = this.cloneValue(this.publicValue);\n }\n\n @Method()\n async setFocus(index: number = 0) {\n const inputs = this.el.querySelectorAll<HTMLInputElement>('.slider__input');\n inputs[Math.max(0, Math.min(index, inputs.length - 1))]?.focus();\n }\n\n private get isDisabled(): boolean {\n return this.disabled || this.isInactive || this.formDisabled;\n }\n\n private get isRange(): boolean {\n return Array.isArray(this.value);\n }\n\n private get safeStep(): number {\n const step = Number(this.step);\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n private get resolvedTicks(): SliderTick[] {\n const unique = new Map<number, SliderTick>();\n for (const tick of this.ticks) {\n if (Number.isFinite(tick.value) && tick.value >= this.safeMin && tick.value <= this.safeMax)\n unique.set(tick.value, tick);\n }\n return [...unique.values()].sort((a, b) => a.value - b.value);\n }\n\n private get safeMin(): number {\n return Number.isFinite(this.min) ? this.min : 0;\n }\n\n private get safeMax(): number {\n const minimum = this.safeMin;\n return Number.isFinite(this.max) && this.max > minimum ? this.max : minimum + this.safeStep;\n }\n\n private get minimumGap(): number {\n const steps = Number.isFinite(this.minStepsBetweenValues)\n ? Math.max(0, Math.floor(this.minStepsBetweenValues))\n : 0;\n const ticks = this.resolvedTicks;\n const span = ticks.length\n ? ticks[ticks.length - 1].value - ticks[0].value\n : this.safeMax - this.safeMin;\n return Math.min(span, steps * this.safeStep);\n }\n\n private get resolvedValues(): number[] {\n const minimum = this.safeMin;\n const maximum = this.safeMax;\n if (!this.isRange) return [this.snap(Number(this.value))];\n\n const raw = this.value as number[];\n let lower = this.snap(Number(raw[0] ?? minimum));\n let upper = this.snap(Number(raw[1] ?? maximum));\n if (lower > upper) [lower, upper] = [upper, lower];\n\n const gap = Math.min(this.minimumGap, maximum - minimum);\n if (upper - lower < gap) {\n upper = Math.min(maximum, this.snap(lower + gap));\n lower = Math.max(minimum, this.snap(upper - gap));\n const ticks = this.resolvedTicks;\n if (ticks.length && upper - lower < gap) {\n upper =\n ticks.find(tick => tick.value >= lower + gap)?.value ?? ticks[ticks.length - 1].value;\n lower =\n [...ticks].reverse().find(tick => tick.value <= upper - gap)?.value ?? ticks[0].value;\n }\n }\n return [lower, upper];\n }\n\n private get publicValue(): SliderValue {\n const values = this.resolvedValues;\n return this.isRange ? [...values] : values[0];\n }\n\n private cloneValue(value: SliderValue): SliderValue {\n return Array.isArray(value) ? [...value] : value;\n }\n\n private valuesEqual(a: SliderValue, b: SliderValue): boolean {\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n }\n return !Array.isArray(a) && !Array.isArray(b) && a === b;\n }\n\n private decimalPlaces(value: number): number {\n const text = String(value).toLowerCase();\n if (text.includes('e-')) return Number(text.split('e-')[1]) || 0;\n return text.includes('.') ? text.split('.')[1].length : 0;\n }\n\n private snap(value: number, minimum = this.safeMin, maximum = this.safeMax): number {\n const ticks = this.resolvedTicks.filter(tick => tick.value >= minimum && tick.value <= maximum);\n if (ticks.length)\n return ticks.reduce((nearest, tick) =>\n Math.abs(tick.value - value) < Math.abs(nearest.value - value) ? tick : nearest\n ).value;\n if (this.step === 'any')\n return Math.min(maximum, Math.max(minimum, Number.isFinite(value) ? value : minimum));\n const candidate = Number.isFinite(value) ? value : minimum;\n const stepped = minimum + Math.round((candidate - minimum) / this.safeStep) * this.safeStep;\n const precision = Math.min(\n 10,\n Math.max(\n this.decimalPlaces(minimum),\n this.decimalPlaces(maximum),\n this.decimalPlaces(this.safeStep)\n )\n );\n return Number(Math.min(maximum, Math.max(minimum, stepped)).toFixed(precision));\n }\n\n private percent(value: number): number {\n return ((value - this.safeMin) / (this.safeMax - this.safeMin)) * 100;\n }\n\n private allowedMin(index: number, values = this.resolvedValues): number {\n return this.isRange && index === 1\n ? Math.min(this.safeMax, values[0] + this.minimumGap)\n : this.safeMin;\n }\n\n private allowedMax(index: number, values = this.resolvedValues): number {\n return this.isRange && index === 0\n ? Math.max(this.safeMin, values[1] - this.minimumGap)\n : this.safeMax;\n }\n\n private setValueAt(index: number, next: number): boolean {\n const previous = this.publicValue;\n const values = this.resolvedValues;\n const constrained = Math.min(\n this.allowedMax(index, values),\n Math.max(\n this.allowedMin(index, values),\n this.snap(next, this.allowedMin(index, values), this.allowedMax(index, values))\n )\n );\n values[index] = constrained;\n const nextValue: SliderValue = this.isRange ? [...values] : values[0];\n if (this.valuesEqual(previous, nextValue)) return false;\n\n this.value = nextValue;\n this.dsChange.emit(this.cloneValue(nextValue));\n return true;\n }\n\n private commitIfChanged() {\n const current = this.publicValue;\n if (this.valuesEqual(current, this.lastCommittedValue)) return;\n this.lastCommittedValue = this.cloneValue(current);\n this.dsCommit.emit(this.cloneValue(current));\n }\n\n private syncNativeLabels() {\n if (this.label || this.ariaLabel || this.ariaLabelledby || this.isRange) return;\n const ids = (Array.from(this.internals.labels) as HTMLLabelElement[]).map(label => {\n if (!label.id) label.id = `ds-slider-label-${++sliderExternalLabelId}`;\n return label.id;\n });\n if (ids.length) this.externalLabelledby = ids.join(' ');\n }\n\n private formattedValue(value: number, index: number): string {\n const authored =\n this.valueTexts[index] ??\n this.valueText ??\n this.resolvedTicks.find(tick => tick.value === value)?.label;\n if (authored) return authored;\n let number: string;\n try {\n number = new Intl.NumberFormat(this.locale, this.formatOptions).format(value);\n } catch {\n number = String(value);\n }\n return `${this.valuePrefix}${number}${this.valueSuffix}`;\n }\n\n private ariaValueText(value: number, index: number): string | undefined {\n if (\n this.resolvedTicks.some(tick => tick.value === value && tick.label) ||\n this.valueTexts[index] ||\n this.valueText ||\n this.valuePrefix ||\n this.valueSuffix ||\n this.formatOptions\n ) {\n return this.formattedValue(value, index);\n }\n return undefined;\n }\n\n private handleNativeInput = (event: Event, index: number) => {\n const input = event.target as HTMLInputElement;\n this.activeThumbIndex = index;\n if (this.readOnly || this.isDisabled) {\n input.value = String(this.resolvedValues[index]);\n return;\n }\n this.setValueAt(index, Number(input.value));\n };\n\n private handleNativeChange = () => {\n if (!this.readOnly && !this.isDisabled) this.commitIfChanged();\n };\n\n private handleKeyDown = (event: KeyboardEvent, index: number) => {\n const keys = [\n 'ArrowLeft',\n 'ArrowRight',\n 'ArrowUp',\n 'ArrowDown',\n 'Home',\n 'End',\n 'PageUp',\n 'PageDown',\n ];\n if (!this.readOnly && this.resolvedTicks.length && keys.includes(event.key)) {\n event.preventDefault();\n if (this.isDisabled) return;\n const ticks = this.resolvedTicks.filter(\n tick => tick.value >= this.allowedMin(index) && tick.value <= this.allowedMax(index)\n );\n const current = this.resolvedValues[index];\n const forward = ['ArrowRight', 'ArrowUp', 'PageUp'].includes(event.key);\n const next =\n event.key === 'Home'\n ? ticks[0]\n : event.key === 'End'\n ? ticks[ticks.length - 1]\n : forward\n ? ticks.find(tick => tick.value > current)\n : [...ticks].reverse().find(tick => tick.value < current);\n if (next && this.setValueAt(index, next.value)) this.commitIfChanged();\n return;\n }\n if (!this.readOnly) return;\n if (\n [\n 'ArrowLeft',\n 'ArrowRight',\n 'ArrowUp',\n 'ArrowDown',\n 'Home',\n 'End',\n 'PageUp',\n 'PageDown',\n ].includes(event.key)\n ) {\n event.preventDefault();\n }\n };\n\n private handleFocus = (index: number) => {\n this.activeThumbIndex = index;\n this.focused = true;\n };\n\n private handleBlur = (event: FocusEvent) => {\n const next = event.relatedTarget;\n if (next instanceof Node && this.el.contains(next)) return;\n this.focused = false;\n this.touched = true;\n };\n\n private handleHostClick = (event: MouseEvent) => {\n if (event.target !== this.el || this.isDisabled) return;\n const inputs = this.el.querySelectorAll<HTMLInputElement>('.slider__input');\n inputs[Math.min(this.activeThumbIndex, inputs.length - 1)]?.focus();\n };\n\n private pointerPercent(event: PointerEvent): number {\n const control = this.controlEl;\n if (!control) return 0;\n const rect = control.getBoundingClientRect();\n const thumb = this.el.querySelector<HTMLElement>('.slider__thumb');\n const thumbSize = thumb\n ? this.orientation === 'horizontal'\n ? thumb.offsetWidth\n : thumb.offsetHeight\n : 0;\n // Both alignment modes keep the complete thumb inside the control. Edge\n // alignment lets the rail span the control while center alignment insets\n // the rail to the same thumb-center travel.\n const inset = thumbSize / 2;\n\n if (this.orientation === 'vertical') {\n const travel = Math.max(1, rect.height - inset * 2);\n return Math.min(1, Math.max(0, (rect.bottom - inset - event.clientY) / travel));\n }\n\n const travel = Math.max(1, rect.width - inset * 2);\n const percent = (event.clientX - rect.left - inset) / travel;\n return Math.min(1, Math.max(0, percent));\n }\n\n private valueFromPointer(event: PointerEvent): number {\n return this.snap(this.safeMin + this.pointerPercent(event) * (this.safeMax - this.safeMin));\n }\n\n private closestThumbIndex(value: number, target: EventTarget | null): number {\n const targetElement =\n target instanceof HTMLElement ? target.closest<HTMLElement>('.slider__thumb') : null;\n if (targetElement?.dataset['index']) return Number(targetElement.dataset['index']);\n if (!this.isRange) return 0;\n\n const values = this.resolvedValues;\n const lowerDistance = Math.abs(value - values[0]);\n const upperDistance = Math.abs(value - values[1]);\n if (lowerDistance === upperDistance) return value >= values[0] ? 1 : 0;\n return lowerDistance < upperDistance ? 0 : 1;\n }\n\n private handlePointerDown = (event: PointerEvent) => {\n if (event.button !== 0 || this.isDisabled || this.readOnly) return;\n event.preventDefault();\n const value = this.valueFromPointer(event);\n const index = this.closestThumbIndex(value, event.target);\n this.activeThumbIndex = index;\n this.pointerId = event.pointerId;\n this.pointerStartValue = this.cloneValue(this.publicValue);\n this.dragging = true;\n this.setValueAt(index, value);\n this.controlEl?.setPointerCapture(event.pointerId);\n requestAnimationFrame(() => {\n const input = this.el.querySelectorAll<HTMLInputElement>('.slider__input')[index];\n input?.focus({ preventScroll: true });\n });\n };\n\n private handlePointerMove = (event: PointerEvent) => {\n if (this.pointerId !== event.pointerId || !this.dragging) return;\n event.preventDefault();\n this.setValueAt(this.activeThumbIndex, this.valueFromPointer(event));\n };\n\n private endPointerInteraction(event: PointerEvent, canceled: boolean) {\n if (this.pointerId !== event.pointerId) return;\n if (this.controlEl?.hasPointerCapture(event.pointerId))\n this.controlEl.releasePointerCapture(event.pointerId);\n this.pointerId = null;\n this.dragging = false;\n this.touched = true;\n if (!canceled && !this.valuesEqual(this.pointerStartValue, this.publicValue))\n this.commitIfChanged();\n }\n\n private renderThumb(value: number, index: number) {\n const inputId =\n index === 0\n ? (this.inputId ?? `${this.generatedId}-input`)\n : `${this.inputId ?? `${this.generatedId}-input`}-end`;\n const rangeLabel = index === 0 ? this.startLabel : this.endLabel;\n const labelledby = this.isRange\n ? undefined\n : (this.ariaLabelledby ?? (this.label ? this.labelId : this.externalLabelledby));\n const directLabel = this.isRange ? rangeLabel : labelledby ? undefined : this.ariaLabel;\n\n return (\n <div\n class={{\n slider__thumb: true,\n 'slider__thumb--active': index === this.activeThumbIndex,\n }}\n data-index={String(index)}\n style={{ '--ds-slider-thumb-pct': String(this.percent(value)) } as Record<string, string>}\n >\n <input\n class=\"slider__input\"\n type=\"range\"\n id={inputId}\n min={this.allowedMin(index)}\n max={this.allowedMax(index)}\n step={this.resolvedTicks.length || this.step === 'any' ? 'any' : this.safeStep}\n value={value}\n disabled={this.isDisabled}\n aria-label={directLabel}\n aria-labelledby={labelledby}\n aria-describedby={this.ariaDescribedby}\n aria-valuetext={this.ariaValueText(value, index)}\n aria-orientation={this.orientation === 'vertical' ? 'vertical' : undefined}\n aria-readonly={this.readOnly ? 'true' : undefined}\n onInput={(event: Event) => this.handleNativeInput(event, index)}\n onChange={this.handleNativeChange}\n onKeyDown={event => this.handleKeyDown(event, index)}\n onFocus={() => this.handleFocus(index)}\n onBlur={this.handleBlur}\n />\n <span class=\"slider__thumb-visual\" aria-hidden=\"true\">\n <span class=\"slider__thumb-wash\" />\n </span>\n </div>\n );\n }\n\n render() {\n const values = this.resolvedValues;\n const first = values[0];\n const last = values[values.length - 1];\n const firstInputId = this.inputId ?? `${this.generatedId}-input`;\n const dirty = !this.valuesEqual(this.publicValue, this.initialValue);\n const valueDisplay = values\n .map((value, index) => this.formattedValue(value, index))\n .join(` ${this.rangeSeparator} `);\n\n return (\n <Host\n aria-disabled={this.isDisabled ? 'true' : undefined}\n class={{\n slider: true,\n 'ds-field-stack': true,\n [`slider--${this.size}`]: true,\n [`slider--${this.orientation}`]: true,\n [`slider--thumb-${this.thumbAlignment}`]: true,\n 'slider--range': this.isRange,\n 'slider--ticks': this.resolvedTicks.length > 0,\n 'slider--tick-labels': this.resolvedTicks.some(tick => Boolean(tick.label)),\n 'slider--readonly': this.readOnly,\n 'ds-control-inactive': this.isDisabled,\n }}\n data-orientation={this.orientation}\n data-disabled={this.isDisabled ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-dragging={this.dragging ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-filled=\"\"\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n data-valid={!this.isDisabled ? '' : undefined}\n onClick={this.handleHostClick}\n >\n {(this.label || this.showValue) && (\n <div class=\"slider__label-row\">\n {this.label && (\n <ds-text\n class=\"slider__label\"\n as=\"label\"\n variant=\"text-body-small\"\n color=\"primary\"\n emphasis\n for={firstInputId}\n textId={this.labelId}\n >\n {this.label}\n </ds-text>\n )}\n {this.showValue && (\n <ds-text\n class=\"slider__value\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"primary\"\n fontFeature=\"tabular-nums\"\n >\n {valueDisplay}\n </ds-text>\n )}\n </div>\n )}\n <div\n class=\"slider__control\"\n ref={element => {\n this.controlEl = element ?? null;\n }}\n style={\n {\n '--ds-slider-start-pct': String(this.isRange ? this.percent(first) : 0),\n '--ds-slider-end-pct': String(this.percent(last)),\n } as Record<string, string>\n }\n onPointerDown={this.handlePointerDown}\n onPointerMove={this.handlePointerMove}\n onPointerUp={(event: PointerEvent) => this.endPointerInteraction(event, false)}\n onPointerCancel={(event: PointerEvent) => this.endPointerInteraction(event, true)}\n >\n <div class=\"slider__rail\" aria-hidden=\"true\">\n <div class=\"slider__indicator\" />\n </div>\n {this.resolvedTicks.map(tick => (\n <span\n class=\"slider__tick\"\n data-edge={\n tick.value === this.safeMin\n ? 'start'\n : tick.value === this.safeMax\n ? 'end'\n : undefined\n }\n aria-hidden=\"true\"\n style={{ '--ds-slider-tick-pct': String(this.percent(tick.value)) }}\n >\n <span class=\"slider__tick-mark\" />\n {tick.label && (\n <ds-text\n class=\"slider__tick-label\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"secondary\"\n >\n {tick.label}\n </ds-text>\n )}\n </span>\n ))}\n {values.map((value, index) => this.renderThumb(value, index))}\n </div>\n </Host>\n );\n }\n}\n",
|
|
720
|
+
"content": "import { numberFormatter } from '../../utils/intl-formatters';\nimport {\n AttachInternals,\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n restoreNumberArrayFormState,\n restoreStringFormState,\n setFormControlValue,\n setRepeatedFormControlValue,\n} from '../../utils';\n\nexport interface SliderTick {\n value: number;\n label?: string;\n}\n\nexport type SliderValue = number | number[];\nexport type SliderSize = 'md' | 'sm' | 'xs';\nexport type SliderOrientation = 'horizontal' | 'vertical';\nexport type SliderThumbAlignment = 'edge' | 'center';\n\nlet sliderId = 0;\nlet sliderExternalLabelId = 0;\n\n@Component({\n tag: 'ds-slider',\n styleUrl: 'Slider.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Slider {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Current value. Assign a two-number array through the JavaScript property for a range slider. */\n @Prop({ mutable: true }) value: SliderValue = 0;\n @Prop() min: number = 0;\n @Prop() max: number = 100;\n @Prop() step: number | 'any' = 1;\n /** Explicit ticks are the selectable snap points. Empty ticks with step='any' allow smooth adjustment. */\n @Prop() ticks: SliderTick[] = [];\n /** Minimum number of steps kept between two range thumbs. */\n @Prop() minStepsBetweenValues: number = 0;\n /** Visible field label. Supply aria-label when the label is intentionally hidden. */\n @Prop() label: string | undefined;\n /** Show the formatted current value beside the visible label. */\n @Prop() showValue: boolean = true;\n @Prop() size: SliderSize = 'md';\n @Prop() orientation: SliderOrientation = 'horizontal';\n /** Align the thumb edge with full-width rail endpoints, or its center with inset rail endpoints. */\n @Prop() thumbAlignment: SliderThumbAlignment = 'edge';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Keep the value focusable and submittable while preventing changes. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop() isInactive: boolean = false;\n /** Direct accessible name for a single-thumb slider without a visible label. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Id references for a visible single-thumb slider label. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n /** Id references for supporting guidance or error text. */\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n /** Accessible label for the lower thumb in a range slider. */\n @Prop() startLabel: string = 'Minimum value';\n /** Accessible label for the upper thumb in a range slider. */\n @Prop() endLabel: string = 'Maximum value';\n /** Human-readable value text for a single slider or both range thumbs. */\n @Prop() valueText: string | undefined;\n /** Per-thumb human-readable value text for a range slider. Assign through the JavaScript property. */\n @Prop() valueTexts: string[] = [];\n /** Locale used for the visible value. Defaults to English (en-US). */\n @Prop() locale: string | undefined;\n /** Intl.NumberFormat options. Assign objects through the JavaScript property. */\n @Prop() formatOptions: Intl.NumberFormatOptions | undefined;\n @Prop() valuePrefix: string = '';\n @Prop() valueSuffix: string = '';\n @Prop() rangeSeparator: string = '–';\n /** Id applied to the first native range input. */\n @Prop() inputId: string | undefined;\n\n /** Emitted continuously while the value changes. */\n @Event() dsChange!: EventEmitter<SliderValue>;\n /** Emitted when a pointer or keyboard value change is committed. */\n @Event() dsCommit!: EventEmitter<SliderValue>;\n\n @State() private formDisabled = false;\n @State() private dragging = false;\n @State() private focused = false;\n @State() private touched = false;\n @State() private activeThumbIndex = 0;\n @State() private externalLabelledby: string | undefined;\n\n private readonly generatedId = `ds-slider-${++sliderId}`;\n private readonly labelId = `${this.generatedId}-label`;\n private initialValue: SliderValue = 0;\n private lastCommittedValue: SliderValue = 0;\n private pointerId: number | null = null;\n private pointerStartValue: SliderValue = 0;\n private controlEl: HTMLDivElement | null = null;\n\n componentWillLoad() {\n this.initialValue = this.cloneValue(this.publicValue);\n this.lastCommittedValue = this.cloneValue(this.publicValue);\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.syncNativeLabels();\n }\n\n @Watch('value')\n @Watch('min')\n @Watch('max')\n @Watch('step')\n @Watch('ticks')\n @Watch('minStepsBetweenValues')\n @Watch('name')\n @Watch('disabled')\n @Watch('isInactive')\n syncFormValue() {\n const values = this.resolvedValues;\n if (this.isRange) {\n setRepeatedFormControlValue(this.internals, this.name, values, {\n inactive: this.isDisabled,\n });\n } else {\n const value = String(values[0]);\n setFormControlValue(this.internals, this.name ? value : null, {\n inactive: this.isDisabled,\n state: value,\n });\n }\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.cloneValue(this.initialValue);\n this.lastCommittedValue = this.cloneValue(this.initialValue);\n this.dragging = false;\n this.focused = false;\n this.touched = false;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n if (this.isRange) {\n const restored = restoreNumberArrayFormState(state, 2);\n this.value = restored.length ? restored : this.cloneValue(this.initialValue);\n } else {\n const restored = Number(restoreStringFormState(state, 'NaN'));\n if (Number.isFinite(restored)) this.value = restored;\n }\n this.lastCommittedValue = this.cloneValue(this.publicValue);\n }\n\n @Method()\n async setFocus(index: number = 0) {\n const inputs = this.el.querySelectorAll<HTMLInputElement>('.slider__input');\n inputs[Math.max(0, Math.min(index, inputs.length - 1))]?.focus();\n }\n\n private get isDisabled(): boolean {\n return this.disabled || this.isInactive || this.formDisabled;\n }\n\n private get isRange(): boolean {\n return Array.isArray(this.value);\n }\n\n private get safeStep(): number {\n const step = Number(this.step);\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n private get resolvedTicks(): SliderTick[] {\n const unique = new Map<number, SliderTick>();\n for (const tick of this.ticks) {\n if (Number.isFinite(tick.value) && tick.value >= this.safeMin && tick.value <= this.safeMax)\n unique.set(tick.value, tick);\n }\n return [...unique.values()].sort((a, b) => a.value - b.value);\n }\n\n private get safeMin(): number {\n return Number.isFinite(this.min) ? this.min : 0;\n }\n\n private get safeMax(): number {\n const minimum = this.safeMin;\n return Number.isFinite(this.max) && this.max > minimum ? this.max : minimum + this.safeStep;\n }\n\n private get minimumGap(): number {\n const steps = Number.isFinite(this.minStepsBetweenValues)\n ? Math.max(0, Math.floor(this.minStepsBetweenValues))\n : 0;\n const ticks = this.resolvedTicks;\n const span = ticks.length\n ? ticks[ticks.length - 1].value - ticks[0].value\n : this.safeMax - this.safeMin;\n return Math.min(span, steps * this.safeStep);\n }\n\n private get resolvedValues(): number[] {\n const minimum = this.safeMin;\n const maximum = this.safeMax;\n if (!this.isRange) return [this.snap(Number(this.value))];\n\n const raw = this.value as number[];\n let lower = this.snap(Number(raw[0] ?? minimum));\n let upper = this.snap(Number(raw[1] ?? maximum));\n if (lower > upper) [lower, upper] = [upper, lower];\n\n const gap = Math.min(this.minimumGap, maximum - minimum);\n if (upper - lower < gap) {\n upper = Math.min(maximum, this.snap(lower + gap));\n lower = Math.max(minimum, this.snap(upper - gap));\n const ticks = this.resolvedTicks;\n if (ticks.length && upper - lower < gap) {\n upper =\n ticks.find(tick => tick.value >= lower + gap)?.value ?? ticks[ticks.length - 1].value;\n lower =\n [...ticks].reverse().find(tick => tick.value <= upper - gap)?.value ?? ticks[0].value;\n }\n }\n return [lower, upper];\n }\n\n private get publicValue(): SliderValue {\n const values = this.resolvedValues;\n return this.isRange ? [...values] : values[0];\n }\n\n private cloneValue(value: SliderValue): SliderValue {\n return Array.isArray(value) ? [...value] : value;\n }\n\n private valuesEqual(a: SliderValue, b: SliderValue): boolean {\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n }\n return !Array.isArray(a) && !Array.isArray(b) && a === b;\n }\n\n private decimalPlaces(value: number): number {\n const text = String(value).toLowerCase();\n if (text.includes('e-')) return Number(text.split('e-')[1]) || 0;\n return text.includes('.') ? text.split('.')[1].length : 0;\n }\n\n private snap(value: number, minimum = this.safeMin, maximum = this.safeMax): number {\n const ticks = this.resolvedTicks.filter(tick => tick.value >= minimum && tick.value <= maximum);\n if (ticks.length)\n return ticks.reduce((nearest, tick) =>\n Math.abs(tick.value - value) < Math.abs(nearest.value - value) ? tick : nearest\n ).value;\n if (this.step === 'any')\n return Math.min(maximum, Math.max(minimum, Number.isFinite(value) ? value : minimum));\n const candidate = Number.isFinite(value) ? value : minimum;\n const stepped = minimum + Math.round((candidate - minimum) / this.safeStep) * this.safeStep;\n const precision = Math.min(\n 10,\n Math.max(\n this.decimalPlaces(minimum),\n this.decimalPlaces(maximum),\n this.decimalPlaces(this.safeStep)\n )\n );\n return Number(Math.min(maximum, Math.max(minimum, stepped)).toFixed(precision));\n }\n\n private percent(value: number): number {\n return ((value - this.safeMin) / (this.safeMax - this.safeMin)) * 100;\n }\n\n private allowedMin(index: number, values = this.resolvedValues): number {\n return this.isRange && index === 1\n ? Math.min(this.safeMax, values[0] + this.minimumGap)\n : this.safeMin;\n }\n\n private allowedMax(index: number, values = this.resolvedValues): number {\n return this.isRange && index === 0\n ? Math.max(this.safeMin, values[1] - this.minimumGap)\n : this.safeMax;\n }\n\n private setValueAt(index: number, next: number): boolean {\n const previous = this.publicValue;\n const values = this.resolvedValues;\n const constrained = Math.min(\n this.allowedMax(index, values),\n Math.max(\n this.allowedMin(index, values),\n this.snap(next, this.allowedMin(index, values), this.allowedMax(index, values))\n )\n );\n values[index] = constrained;\n const nextValue: SliderValue = this.isRange ? [...values] : values[0];\n if (this.valuesEqual(previous, nextValue)) return false;\n\n this.value = nextValue;\n this.dsChange.emit(this.cloneValue(nextValue));\n return true;\n }\n\n private commitIfChanged() {\n const current = this.publicValue;\n if (this.valuesEqual(current, this.lastCommittedValue)) return;\n this.lastCommittedValue = this.cloneValue(current);\n this.dsCommit.emit(this.cloneValue(current));\n }\n\n private syncNativeLabels() {\n if (this.label || this.ariaLabel || this.ariaLabelledby || this.isRange) return;\n const ids = (Array.from(this.internals.labels) as HTMLLabelElement[]).map(label => {\n if (!label.id) label.id = `ds-slider-label-${++sliderExternalLabelId}`;\n return label.id;\n });\n if (ids.length) this.externalLabelledby = ids.join(' ');\n }\n\n private formattedValue(value: number, index: number): string {\n const authored =\n this.valueTexts[index] ??\n this.valueText ??\n this.resolvedTicks.find(tick => tick.value === value)?.label;\n if (authored) return authored;\n let number: string;\n try {\n number = numberFormatter(this.locale, this.formatOptions).format(value);\n } catch {\n number = String(value);\n }\n return `${this.valuePrefix}${number}${this.valueSuffix}`;\n }\n\n private ariaValueText(value: number, index: number): string | undefined {\n if (\n this.resolvedTicks.some(tick => tick.value === value && tick.label) ||\n this.valueTexts[index] ||\n this.valueText ||\n this.valuePrefix ||\n this.valueSuffix ||\n this.formatOptions\n ) {\n return this.formattedValue(value, index);\n }\n return undefined;\n }\n\n private handleNativeInput = (event: Event, index: number) => {\n const input = event.target as HTMLInputElement;\n this.activeThumbIndex = index;\n if (this.readOnly || this.isDisabled) {\n input.value = String(this.resolvedValues[index]);\n return;\n }\n this.setValueAt(index, Number(input.value));\n };\n\n private handleNativeChange = () => {\n if (!this.readOnly && !this.isDisabled) this.commitIfChanged();\n };\n\n private handleKeyDown = (event: KeyboardEvent, index: number) => {\n const keys = [\n 'ArrowLeft',\n 'ArrowRight',\n 'ArrowUp',\n 'ArrowDown',\n 'Home',\n 'End',\n 'PageUp',\n 'PageDown',\n ];\n if (!this.readOnly && this.resolvedTicks.length && keys.includes(event.key)) {\n event.preventDefault();\n if (this.isDisabled) return;\n const ticks = this.resolvedTicks.filter(\n tick => tick.value >= this.allowedMin(index) && tick.value <= this.allowedMax(index)\n );\n const current = this.resolvedValues[index];\n const forward = ['ArrowRight', 'ArrowUp', 'PageUp'].includes(event.key);\n const next =\n event.key === 'Home'\n ? ticks[0]\n : event.key === 'End'\n ? ticks[ticks.length - 1]\n : forward\n ? ticks.find(tick => tick.value > current)\n : [...ticks].reverse().find(tick => tick.value < current);\n if (next && this.setValueAt(index, next.value)) this.commitIfChanged();\n return;\n }\n if (!this.readOnly) return;\n if (\n [\n 'ArrowLeft',\n 'ArrowRight',\n 'ArrowUp',\n 'ArrowDown',\n 'Home',\n 'End',\n 'PageUp',\n 'PageDown',\n ].includes(event.key)\n ) {\n event.preventDefault();\n }\n };\n\n private handleFocus = (index: number) => {\n this.activeThumbIndex = index;\n this.focused = true;\n };\n\n private handleBlur = (event: FocusEvent) => {\n const next = event.relatedTarget;\n if (next instanceof Node && this.el.contains(next)) return;\n this.focused = false;\n this.touched = true;\n };\n\n private handleHostClick = (event: MouseEvent) => {\n if (event.target !== this.el || this.isDisabled) return;\n const inputs = this.el.querySelectorAll<HTMLInputElement>('.slider__input');\n inputs[Math.min(this.activeThumbIndex, inputs.length - 1)]?.focus();\n };\n\n private pointerPercent(event: PointerEvent): number {\n const control = this.controlEl;\n if (!control) return 0;\n const rect = control.getBoundingClientRect();\n const thumb = this.el.querySelector<HTMLElement>('.slider__thumb');\n const thumbSize = thumb\n ? this.orientation === 'horizontal'\n ? thumb.offsetWidth\n : thumb.offsetHeight\n : 0;\n // Both alignment modes keep the complete thumb inside the control. Edge\n // alignment lets the rail span the control while center alignment insets\n // the rail to the same thumb-center travel.\n const inset = thumbSize / 2;\n\n if (this.orientation === 'vertical') {\n const travel = Math.max(1, rect.height - inset * 2);\n return Math.min(1, Math.max(0, (rect.bottom - inset - event.clientY) / travel));\n }\n\n const travel = Math.max(1, rect.width - inset * 2);\n const percent = (event.clientX - rect.left - inset) / travel;\n return Math.min(1, Math.max(0, percent));\n }\n\n private valueFromPointer(event: PointerEvent): number {\n return this.snap(this.safeMin + this.pointerPercent(event) * (this.safeMax - this.safeMin));\n }\n\n private closestThumbIndex(value: number, target: EventTarget | null): number {\n const targetElement =\n target instanceof HTMLElement ? target.closest<HTMLElement>('.slider__thumb') : null;\n if (targetElement?.dataset['index']) return Number(targetElement.dataset['index']);\n if (!this.isRange) return 0;\n\n const values = this.resolvedValues;\n const lowerDistance = Math.abs(value - values[0]);\n const upperDistance = Math.abs(value - values[1]);\n if (lowerDistance === upperDistance) return value >= values[0] ? 1 : 0;\n return lowerDistance < upperDistance ? 0 : 1;\n }\n\n private handlePointerDown = (event: PointerEvent) => {\n if (event.button !== 0 || this.isDisabled || this.readOnly) return;\n event.preventDefault();\n const value = this.valueFromPointer(event);\n const index = this.closestThumbIndex(value, event.target);\n this.activeThumbIndex = index;\n this.pointerId = event.pointerId;\n this.pointerStartValue = this.cloneValue(this.publicValue);\n this.dragging = true;\n this.setValueAt(index, value);\n this.controlEl?.setPointerCapture(event.pointerId);\n requestAnimationFrame(() => {\n const input = this.el.querySelectorAll<HTMLInputElement>('.slider__input')[index];\n input?.focus({ preventScroll: true });\n });\n };\n\n private handlePointerMove = (event: PointerEvent) => {\n if (this.pointerId !== event.pointerId || !this.dragging) return;\n event.preventDefault();\n this.setValueAt(this.activeThumbIndex, this.valueFromPointer(event));\n };\n\n private endPointerInteraction(event: PointerEvent, canceled: boolean) {\n if (this.pointerId !== event.pointerId) return;\n if (this.controlEl?.hasPointerCapture(event.pointerId))\n this.controlEl.releasePointerCapture(event.pointerId);\n this.pointerId = null;\n this.dragging = false;\n this.touched = true;\n if (!canceled && !this.valuesEqual(this.pointerStartValue, this.publicValue))\n this.commitIfChanged();\n }\n\n private renderThumb(value: number, index: number) {\n const inputId =\n index === 0\n ? (this.inputId ?? `${this.generatedId}-input`)\n : `${this.inputId ?? `${this.generatedId}-input`}-end`;\n const rangeLabel = index === 0 ? this.startLabel : this.endLabel;\n const labelledby = this.isRange\n ? undefined\n : (this.ariaLabelledby ?? (this.label ? this.labelId : this.externalLabelledby));\n const directLabel = this.isRange ? rangeLabel : labelledby ? undefined : this.ariaLabel;\n\n return (\n <div\n class={{\n slider__thumb: true,\n 'slider__thumb--active': index === this.activeThumbIndex,\n }}\n data-index={String(index)}\n style={{ '--ds-slider-thumb-pct': String(this.percent(value)) } as Record<string, string>}\n >\n <input\n class=\"slider__input\"\n type=\"range\"\n id={inputId}\n min={this.allowedMin(index)}\n max={this.allowedMax(index)}\n step={this.resolvedTicks.length || this.step === 'any' ? 'any' : this.safeStep}\n value={value}\n disabled={this.isDisabled}\n aria-label={directLabel}\n aria-labelledby={labelledby}\n aria-describedby={this.ariaDescribedby}\n aria-valuetext={this.ariaValueText(value, index)}\n aria-orientation={this.orientation === 'vertical' ? 'vertical' : undefined}\n aria-readonly={this.readOnly ? 'true' : undefined}\n onInput={(event: Event) => this.handleNativeInput(event, index)}\n onChange={this.handleNativeChange}\n onKeyDown={event => this.handleKeyDown(event, index)}\n onFocus={() => this.handleFocus(index)}\n onBlur={this.handleBlur}\n />\n <span class=\"slider__thumb-visual\" aria-hidden=\"true\">\n <span class=\"slider__thumb-wash\" />\n </span>\n </div>\n );\n }\n\n render() {\n const values = this.resolvedValues;\n const first = values[0];\n const last = values[values.length - 1];\n const firstInputId = this.inputId ?? `${this.generatedId}-input`;\n const dirty = !this.valuesEqual(this.publicValue, this.initialValue);\n const valueDisplay = values\n .map((value, index) => this.formattedValue(value, index))\n .join(` ${this.rangeSeparator} `);\n\n return (\n <Host\n aria-disabled={this.isDisabled ? 'true' : undefined}\n class={{\n slider: true,\n 'ds-field-stack': true,\n [`slider--${this.size}`]: true,\n [`slider--${this.orientation}`]: true,\n [`slider--thumb-${this.thumbAlignment}`]: true,\n 'slider--range': this.isRange,\n 'slider--ticks': this.resolvedTicks.length > 0,\n 'slider--tick-labels': this.resolvedTicks.some(tick => Boolean(tick.label)),\n 'slider--readonly': this.readOnly,\n 'ds-control-inactive': this.isDisabled,\n }}\n data-orientation={this.orientation}\n data-disabled={this.isDisabled ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-dragging={this.dragging ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-filled=\"\"\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n data-valid={!this.isDisabled ? '' : undefined}\n onClick={this.handleHostClick}\n >\n {(this.label || this.showValue) && (\n <div class=\"slider__label-row\">\n {this.label && (\n <ds-text\n class=\"slider__label\"\n as=\"label\"\n variant=\"text-body-small\"\n color=\"primary\"\n emphasis\n for={firstInputId}\n textId={this.labelId}\n >\n {this.label}\n </ds-text>\n )}\n {this.showValue && (\n <ds-text\n class=\"slider__value\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"primary\"\n fontFeature=\"tabular-nums\"\n >\n {valueDisplay}\n </ds-text>\n )}\n </div>\n )}\n <div\n class=\"slider__control\"\n ref={element => {\n this.controlEl = element ?? null;\n }}\n style={\n {\n '--ds-slider-start-pct': String(this.isRange ? this.percent(first) : 0),\n '--ds-slider-end-pct': String(this.percent(last)),\n } as Record<string, string>\n }\n onPointerDown={this.handlePointerDown}\n onPointerMove={this.handlePointerMove}\n onPointerUp={(event: PointerEvent) => this.endPointerInteraction(event, false)}\n onPointerCancel={(event: PointerEvent) => this.endPointerInteraction(event, true)}\n >\n <div class=\"slider__rail\" aria-hidden=\"true\">\n <div class=\"slider__indicator\" />\n </div>\n {this.resolvedTicks.map(tick => (\n <span\n class=\"slider__tick\"\n data-edge={\n tick.value === this.safeMin\n ? 'start'\n : tick.value === this.safeMax\n ? 'end'\n : undefined\n }\n aria-hidden=\"true\"\n style={{ '--ds-slider-tick-pct': String(this.percent(tick.value)) }}\n >\n <span class=\"slider__tick-mark\" />\n {tick.label && (\n <ds-text\n class=\"slider__tick-label\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"secondary\"\n >\n {tick.label}\n </ds-text>\n )}\n </span>\n ))}\n {values.map((value, index) => this.renderThumb(value, index))}\n </div>\n </Host>\n );\n }\n}\n",
|
|
721
721
|
"type": "registry:ui"
|
|
722
722
|
}
|
|
723
723
|
]
|
|
@@ -398,7 +398,7 @@
|
|
|
398
398
|
},
|
|
399
399
|
{
|
|
400
400
|
"path": "src/wc/components/Switch/Switch.tsx",
|
|
401
|
-
"content": "import {\n AttachInternals,\n Component,\n Element,\n Prop,\n State,\n Event,\n EventEmitter,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport { DEFAULT_REQUIRED_MESSAGE, setFormControlValue, setRequiredValidity } from '../../utils';\n\nexport type SwitchSize = 'lg' | 'md' | 'sm' | 'xs';\n\nlet generatedLabelId = 0;\n\n@Component({\n tag: 'ds-switch',\n styleUrl: 'Switch.css',\n shadow: true,\n formAssociated: true,\n})\nexport class Switch {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Current on/off state. The initial value is restored when the owning form resets. */\n @Prop({ mutable: true }) checked: boolean = false;\n\n @Prop({ reflect: true }) name: string | undefined;\n @Prop() value: string = 'on';\n\n /** Optional value submitted when unchecked; omitted by default like a native checkbox. */\n @Prop() uncheckedValue: string | undefined;\n\n /** Associates the switch with a form by id when it is rendered outside that form. */\n @Prop({ reflect: true }) form: string | undefined;\n\n @Prop({ reflect: true }) disabled: boolean = false;\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() isInactive: boolean = false;\n\n /** Compact track size for placement inside controls, menus, and form rows. */\n @Prop() size: SwitchSize = 'md';\n\n /**\n * Removes standalone control semantics so a composite owner such as a\n * `menuitemcheckbox` can use the switch as an aria-hidden visual indicator.\n */\n @Prop() presentation: boolean = false;\n\n @Event() dsChange!: EventEmitter<boolean>;\n\n private initialChecked = false;\n private checkedUpdateObserved = false;\n private hasLoaded = false;\n @State() private formDisabled = false;\n @State() private focused = false;\n @State() private touched = false;\n @State() private transitionsEnabled = false;\n\n private transitionsFrame?: number;\n\n componentWillLoad() {\n this.initialChecked = this.checked;\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.syncNativeLabels();\n this.scheduleTransitionsEnable();\n }\n\n disconnectedCallback() {\n
|
|
401
|
+
"content": "import {\n AttachInternals,\n Component,\n Element,\n Prop,\n State,\n Event,\n EventEmitter,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport { DEFAULT_REQUIRED_MESSAGE, setFormControlValue, setRequiredValidity } from '../../utils';\nimport { ConnectionTasks } from '../../utils/connection-tasks';\n\nexport type SwitchSize = 'lg' | 'md' | 'sm' | 'xs';\n\nlet generatedLabelId = 0;\n\n@Component({\n tag: 'ds-switch',\n styleUrl: 'Switch.css',\n shadow: true,\n formAssociated: true,\n})\nexport class Switch {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Current on/off state. The initial value is restored when the owning form resets. */\n @Prop({ mutable: true }) checked: boolean = false;\n\n @Prop({ reflect: true }) name: string | undefined;\n @Prop() value: string = 'on';\n\n /** Optional value submitted when unchecked; omitted by default like a native checkbox. */\n @Prop() uncheckedValue: string | undefined;\n\n /** Associates the switch with a form by id when it is rendered outside that form. */\n @Prop({ reflect: true }) form: string | undefined;\n\n @Prop({ reflect: true }) disabled: boolean = false;\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() isInactive: boolean = false;\n\n /** Compact track size for placement inside controls, menus, and form rows. */\n @Prop() size: SwitchSize = 'md';\n\n /**\n * Removes standalone control semantics so a composite owner such as a\n * `menuitemcheckbox` can use the switch as an aria-hidden visual indicator.\n */\n @Prop() presentation: boolean = false;\n\n @Event() dsChange!: EventEmitter<boolean>;\n\n private initialChecked = false;\n private checkedUpdateObserved = false;\n private hasLoaded = false;\n @State() private formDisabled = false;\n @State() private focused = false;\n @State() private touched = false;\n @State() private transitionsEnabled = false;\n\n private transitionsFrame?: number;\n private readonly tasks = new ConnectionTasks(() => this.el.isConnected);\n\n connectedCallback() {\n if (!this.hasLoaded) return;\n this.syncNativeLabels();\n if (!this.transitionsEnabled) this.scheduleTransitionsEnable();\n }\n\n componentWillLoad() {\n this.initialChecked = this.checked;\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.syncNativeLabels();\n this.scheduleTransitionsEnable();\n }\n\n disconnectedCallback() {\n this.tasks.cancel();\n this.transitionsFrame = undefined;\n this.focused = false;\n }\n\n @Watch('checked')\n handleCheckedChange() {\n if (!this.hasLoaded || this.checkedUpdateObserved) return;\n\n this.checkedUpdateObserved = true;\n this.transitionsEnabled = false;\n this.scheduleTransitionsEnable();\n }\n\n private scheduleTransitionsEnable() {\n this.tasks.cancelFrame(this.transitionsFrame);\n this.transitionsFrame = this.tasks.frame(() => {\n this.transitionsEnabled = true;\n this.transitionsFrame = undefined;\n });\n }\n\n @Watch('checked')\n @Watch('value')\n @Watch('uncheckedValue')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('presentation')\n @Watch('required')\n @Watch('requiredMessage')\n syncFormValue() {\n const inactive = this.presentation || this.isInactive || this.disabled || this.formDisabled;\n const submissionValue = this.checked ? this.value : (this.uncheckedValue ?? null);\n const state = this.checked ? 'checked' : 'unchecked';\n setFormControlValue(this.internals, submissionValue, { inactive, state });\n\n const missing = this.required && !inactive && !this.checked;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.checked = this.initialChecked;\n this.focused = false;\n this.touched = false;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.checked = state === 'checked';\n }\n\n private syncNativeLabels() {\n if (this.presentation) return;\n if (this.el.hasAttribute('aria-label') || this.el.hasAttribute('aria-labelledby')) return;\n\n const labelIds = (Array.from(this.internals.labels) as HTMLLabelElement[]).map(label => {\n if (!label.id) label.id = `ds-switch-label-${++generatedLabelId}`;\n return label.id;\n });\n if (labelIds.length) this.el.setAttribute('aria-labelledby', labelIds.join(' '));\n }\n\n private handleClick = () => {\n if (this.presentation || this.readOnly || this.isInactive || this.disabled || this.formDisabled)\n return;\n this.checkedUpdateObserved = true;\n this.checked = !this.checked;\n this.dsChange.emit(this.checked);\n };\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (this.presentation || this.readOnly || this.isInactive || this.disabled || this.formDisabled)\n return;\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault();\n this.handleClick();\n }\n };\n\n private handleFocus = () => {\n if (!this.presentation) this.focused = true;\n };\n\n private handleBlur = () => {\n if (this.presentation) return;\n this.focused = false;\n this.touched = true;\n };\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const invalid = this.required && !inactive && !this.checked;\n const dirty = this.checked !== this.initialChecked;\n // Switch owns its structural track and thumb strokes so nested interaction layers cannot cover them.\n return (\n <Host\n role={this.presentation ? undefined : 'switch'}\n aria-checked={this.presentation ? undefined : String(this.checked)}\n aria-disabled={!this.presentation && inactive ? 'true' : undefined}\n aria-readonly={!this.presentation && this.readOnly ? 'true' : undefined}\n aria-required={!this.presentation && this.required ? 'true' : undefined}\n aria-invalid={!this.presentation && invalid ? 'true' : undefined}\n aria-hidden={this.presentation ? 'true' : undefined}\n data-checked={this.checked ? '' : undefined}\n data-unchecked={!this.checked ? '' : undefined}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-focused={!this.presentation && this.focused ? '' : undefined}\n data-filled={!this.presentation && this.checked ? '' : undefined}\n data-dirty={!this.presentation && dirty ? '' : undefined}\n data-touched={!this.presentation && this.touched ? '' : undefined}\n data-valid={!this.presentation && !inactive && !invalid ? '' : undefined}\n data-invalid={!this.presentation && invalid ? '' : undefined}\n tabIndex={this.presentation || inactive ? -1 : 0}\n class={{\n switch: true,\n checked: this.checked,\n 'switch--lg': this.size === 'lg',\n 'switch--md': this.size === 'md',\n 'switch--sm': this.size === 'sm',\n 'switch--xs': this.size === 'xs',\n 'switch--readonly': this.readOnly,\n 'switch--presentation': this.presentation,\n 'switch--transitions-enabled': this.transitionsEnabled,\n 'ds-focus-ring': true,\n 'ds-control-inactive': inactive,\n }}\n onClick={this.handleClick}\n onKeyDown={this.handleKeyDown}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n >\n <span class=\"thumb\" />\n </Host>\n );\n }\n}\n",
|
|
402
402
|
"type": "registry:ui"
|
|
403
403
|
}
|
|
404
404
|
]
|