@ds-mo/ui 15.11.2 → 15.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.package-ready.json +1 -1
- package/dist/agent-patterns.json +1 -1
- package/dist/agent.json +114 -18
- package/dist/angular/ds-card-action-center.d.ts +1 -1
- package/dist/angular/ds-card-action-center.js +3 -3
- package/dist/angular/ds-card-chart.d.ts +1 -1
- package/dist/angular/ds-card-chart.js +3 -3
- package/dist/angular/ds-card-navigation.d.ts +1 -1
- package/dist/angular/ds-card-navigation.js +3 -3
- package/dist/angular/ds-card-setting.d.ts +1 -1
- package/dist/angular/ds-card-setting.js +3 -3
- package/dist/angular/ds-card-settings-scope.d.ts +1 -1
- package/dist/angular/ds-card-settings-scope.js +3 -3
- package/dist/angular/ds-field.d.ts +1 -1
- package/dist/angular/ds-field.js +3 -3
- package/dist/angular/ds-inline-banner-settings.d.ts +1 -1
- package/dist/angular/ds-inline-banner-settings.js +3 -3
- package/dist/angular/ds-pagination.d.ts +1 -1
- package/dist/angular/ds-pagination.js +3 -3
- package/dist/angular/ds-radio.d.ts +1 -1
- package/dist/angular/ds-radio.js +3 -3
- package/dist/angular/ds-setting-row-radio.d.ts +1 -1
- package/dist/angular/ds-setting-row-radio.js +3 -3
- package/dist/angular/ds-setting-row-toggle.d.ts +1 -1
- package/dist/angular/ds-setting-row-toggle.js +3 -3
- package/dist/angular/ds-skeleton.d.ts +1 -1
- package/dist/angular/ds-skeleton.js +3 -3
- 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-response.js.map +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-badge.js +1 -1
- package/dist/components/ds-banner.js +1 -1
- package/dist/components/ds-bar-action.js +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-page-title.js +1 -1
- package/dist/components/ds-bar-title.js +1 -1
- package/dist/components/ds-bar-workflow.js +1 -1
- package/dist/components/ds-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-action-center.js.map +1 -1
- package/dist/components/ds-card-chart.js +1 -1
- package/dist/components/ds-card-chart.js.map +1 -1
- package/dist/components/ds-card-navigation.js +1 -1
- package/dist/components/ds-card-navigation.js.map +1 -1
- package/dist/components/ds-card-overview.js +1 -1
- package/dist/components/ds-card-overview.js.map +1 -1
- package/dist/components/ds-card-setting.js +1 -1
- package/dist/components/ds-card-setting.js.map +1 -1
- package/dist/components/ds-card-settings-scope.js +1 -1
- package/dist/components/ds-card-settings-scope.js.map +1 -1
- package/dist/components/ds-chart-legend.js +1 -1
- package/dist/components/ds-chart.js +1 -1
- package/dist/components/ds-checkbox-group.js +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-chip.js +1 -1
- package/dist/components/ds-code-block.js +1 -1
- package/dist/components/ds-conversation-list-item.js +1 -1
- package/dist/components/ds-conversation-list-section.js +1 -1
- package/dist/components/ds-conversation-list.js +1 -1
- package/dist/components/ds-data-customize.js +1 -1
- package/dist/components/ds-data-customize.js.map +1 -1
- package/dist/components/ds-data-filter.js +1 -1
- package/dist/components/ds-data-group.js +1 -1
- package/dist/components/ds-data-preferences.js +1 -1
- package/dist/components/ds-data-saved-views.js +1 -1
- package/dist/components/ds-data-saved-views.js.map +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-field.js +1 -1
- package/dist/components/ds-filter-menu.js +1 -1
- package/dist/components/ds-inline-banner-settings.js +1 -1
- package/dist/components/ds-inline-banner-settings.js.map +1 -1
- package/dist/components/ds-input-date.js +1 -1
- package/dist/components/ds-input-time.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-map-cluster-marker.js +1 -1
- package/dist/components/ds-map-entity-marker.js +1 -1
- package/dist/components/ds-map-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-navigation-tab-group.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-paper-texture.js +1 -1
- package/dist/components/ds-radio-tile.js +1 -1
- package/dist/components/ds-radio.js +1 -1
- package/dist/components/ds-score.js +1 -1
- package/dist/components/ds-scroll-overlay.js +1 -1
- package/dist/components/ds-select-toggle.js +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-setting-row-checkbox.js +1 -1
- package/dist/components/ds-setting-row-radio.js +1 -1
- package/dist/components/ds-setting-row-radio.js.map +1 -1
- package/dist/components/ds-setting-row-toggle.js +1 -1
- package/dist/components/ds-setting-row-toggle.js.map +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-skeleton.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-swatch-picker.js +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-text.js +1 -1
- package/dist/components/ds-textarea.js +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/ds-typing-indicator.js +1 -1
- package/dist/components/{p-6QYDIPd1.js → p-3dG9v5Aw.js} +2 -2
- package/dist/components/{p-C4kVTIBc.js → p-5PReYMiY.js} +2 -2
- package/dist/components/{p-CzTJAO2J.js → p-8fddX3V3.js} +2 -2
- package/dist/components/{p-Cw1ljgW1.js → p-9u2WDF0J.js} +2 -2
- package/dist/components/{p-M9pOLcyK.js → p-B-ChwMFE.js} +2 -2
- package/dist/components/{p-BvlkO__o.js → p-BAIYOobl.js} +2 -2
- package/dist/components/{p-BrJebF5t.js → p-BIMW5_Z7.js} +2 -2
- package/dist/components/{p-BkKdzYZP.js → p-BMI0F08u.js} +2 -2
- package/dist/components/{p-D0kzIzGQ.js → p-BV8sNEVP.js} +2 -2
- package/dist/components/{p-DNtwJJOC.js → p-BVUdtuog.js} +2 -2
- package/dist/components/{p-CgYDQSLW.js → p-BYo1-6b5.js} +2 -2
- package/dist/components/{p-CJ-igzsZ.js → p-BZbJkJpG.js} +2 -2
- package/dist/components/{p-D2rASHHq.js → p-BcYzHn1E.js} +2 -2
- package/dist/components/{p-0EpjqRWc.js → p-BfRAVR40.js} +2 -2
- package/dist/components/p-BnE9MeXS.js +2 -0
- package/dist/components/{p-BI6L3JoU.js.map → p-BnE9MeXS.js.map} +1 -1
- package/dist/components/p-BoFK9lmP.js +2 -0
- package/dist/components/p-BoFK9lmP.js.map +1 -0
- package/dist/components/{p-BBZz2Dvg.js → p-BoWpIl_m.js} +2 -2
- package/dist/components/{p-529k8euH.js → p-BpPHCF63.js} +2 -2
- package/dist/components/p-BpssHpam.js +2 -0
- package/dist/components/p-BpssHpam.js.map +1 -0
- package/dist/components/{p-DXEInGEq.js → p-By_jYvHp.js} +2 -2
- package/dist/components/p-C5HGwzy9.js +2 -0
- package/dist/components/{p-CXF8wj9I.js.map → p-C5HGwzy9.js.map} +1 -1
- package/dist/components/{p-D4r6oWyQ.js → p-C860ilDn.js} +2 -2
- package/dist/components/{p-DcVK0W4X.js → p-CEHhx4VQ.js} +2 -2
- package/dist/components/p-CL0FpMKo.js +2 -0
- package/dist/components/{p-BjYoM1f8.js.map → p-CL0FpMKo.js.map} +1 -1
- package/dist/components/{p-CEm0AoDR.js → p-CQnjpuMX.js} +2 -2
- package/dist/components/{p-4qKHg502.js → p-CUG4EkKn.js} +2 -2
- package/dist/components/{p-CTstnsLe.js → p-CYmz92EF.js} +2 -2
- package/dist/components/{p-YE1r-XSR.js → p-CbP9ASmI.js} +2 -2
- package/dist/components/{p-C31AjGbg.js → p-CbPGO7vl.js} +2 -2
- package/dist/components/{p-DfphSI9S.js → p-CdMIA5hz.js} +2 -2
- package/dist/components/{p-Ds58qo_J.js → p-CeaIWw1Q.js} +2 -2
- package/dist/components/{p-DxVokKm8.js → p-Cfqvxxs2.js} +2 -2
- package/dist/components/{p-CsPqGA8B.js → p-CpqLGzv4.js} +2 -2
- package/dist/components/{p-DKg181YU.js → p-DFR9hqDU.js} +2 -2
- package/dist/components/{p-DEJLfD7k.js → p-DKZP3jCY.js} +2 -2
- package/dist/components/{p-CTAF7SkL.js → p-DMPn_dt7.js} +2 -2
- package/dist/components/{p-CYRTkP1A.js → p-DYruK9cb.js} +2 -2
- package/dist/components/{p-CqfYCn-e.js → p-DiUaIJ91.js} +2 -2
- package/dist/components/{p-Cqg9T781.js → p-DimFbKZH.js} +2 -2
- package/dist/components/p-DmyU1fd-.js +2 -0
- package/dist/components/p-DmyU1fd-.js.map +1 -0
- package/dist/components/{p-DHmHllUM.js → p-DuhJ49iN.js} +2 -2
- package/dist/components/{p-D_iZPukF.js → p-G6r6yYg8.js} +2 -2
- package/dist/components/{p-87FjWil8.js → p-IQdLCfJJ.js} +2 -2
- package/dist/components/{p-CAARHqjE.js → p-KTaiJztz.js} +2 -2
- package/dist/components/{p-BM-UYlIS.js → p-N-2CZze7.js} +2 -2
- package/dist/components/{p-Ch00yccW.js → p-Nx52go85.js} +2 -2
- package/dist/components/{p-CC4X8rpT.js → p-WEffoQQD.js} +2 -2
- package/dist/components/{p-DDawngok.js → p-XKt6j1UN.js} +2 -2
- package/dist/components/{p-vrRbdZAI.js → p-nakO4viw.js} +2 -2
- package/dist/components/{p-BC9xYdJD.js → p-pkJ3X3s7.js} +2 -2
- package/dist/components/{p-C6VWVB87.js → p-r_e6_0nP.js} +2 -2
- package/dist/components/p-zsWHUNJ9.js +2 -0
- package/dist/components/p-zsWHUNJ9.js.map +1 -0
- package/dist/docs/components.json +627 -7
- package/dist/lint/index.js +1 -1
- package/dist/mcp-data/registry/card-action-center.json +23 -3
- package/dist/mcp-data/registry/card-chart.json +23 -3
- package/dist/mcp-data/registry/card-navigation.json +23 -3
- package/dist/mcp-data/registry/card-overview.json +2 -2
- package/dist/mcp-data/registry/card-setting.json +23 -3
- package/dist/mcp-data/registry/card-settings-scope.json +22 -2
- package/dist/mcp-data/registry/field.json +22 -2
- package/dist/mcp-data/registry/inline-banner-settings.json +22 -2
- package/dist/mcp-data/registry/pagination.json +22 -2
- package/dist/mcp-data/registry/radio.json +22 -2
- package/dist/mcp-data/registry/registry.json +113 -17
- package/dist/mcp-data/registry/setting-row-radio.json +22 -2
- package/dist/mcp-data/registry/setting-row-toggle.json +23 -3
- package/dist/mcp-data/registry/shell-app.json +3 -2
- package/dist/mcp-data/registry/skeleton.json +100 -8
- package/dist/mcp-data/registry/table.json +7 -6
- package/dist/types/components/CardActionCenter/CardActionCenter.d.ts +2 -0
- package/dist/types/components/CardChart/CardChart.d.ts +2 -0
- package/dist/types/components/CardNavigation/CardNavigation.d.ts +2 -0
- package/dist/types/components/CardSetting/CardSetting.d.ts +2 -0
- package/dist/types/components/CardSettingsScope/CardSettingsScope.d.ts +2 -0
- package/dist/types/components/Field/Field.d.ts +3 -0
- package/dist/types/components/InlineBannerSettings/InlineBannerSettings.d.ts +2 -0
- package/dist/types/components/Pagination/Pagination.d.ts +3 -0
- package/dist/types/components/Radio/Radio.d.ts +2 -0
- package/dist/types/components/SettingRowRadio/SettingRowRadio.d.ts +2 -0
- package/dist/types/components/SettingRowToggle/SettingRowToggle.d.ts +2 -0
- package/dist/types/components/Skeleton/Skeleton.d.ts +13 -1
- package/dist/types/components/Skeleton/index.d.ts +1 -1
- package/dist/types/components/Table/Table.d.ts +1 -1
- package/dist/types/components.d.ts +169 -4
- package/dist/vue/ds-card-action-center.js +1 -0
- package/dist/vue/ds-card-chart.js +1 -0
- package/dist/vue/ds-card-navigation.js +1 -0
- package/dist/vue/ds-card-setting.js +1 -0
- package/dist/vue/ds-card-settings-scope.js +1 -0
- package/dist/vue/ds-field.js +2 -1
- package/dist/vue/ds-inline-banner-settings.js +2 -1
- package/dist/vue/ds-pagination.js +1 -0
- package/dist/vue/ds-radio.js +1 -0
- package/dist/vue/ds-setting-row-radio.js +2 -1
- package/dist/vue/ds-setting-row-toggle.js +1 -0
- package/dist/vue/ds-skeleton.js +4 -0
- package/package.json +1 -1
- package/dist/components/p-BI6L3JoU.js +0 -2
- package/dist/components/p-BjYoM1f8.js +0 -2
- package/dist/components/p-CXF8wj9I.js +0 -2
- package/dist/components/p-Cnzry0_v.js +0 -2
- package/dist/components/p-Cnzry0_v.js.map +0 -1
- package/dist/components/p-DwcAROOo.js +0 -2
- package/dist/components/p-DwcAROOo.js.map +0 -1
- package/dist/components/p-hWKa8Jn8.js +0 -2
- package/dist/components/p-hWKa8Jn8.js.map +0 -1
- package/dist/components/p-xCqqM1Rg.js +0 -2
- package/dist/components/p-xCqqM1Rg.js.map +0 -1
- /package/dist/components/{p-6QYDIPd1.js.map → p-3dG9v5Aw.js.map} +0 -0
- /package/dist/components/{p-C4kVTIBc.js.map → p-5PReYMiY.js.map} +0 -0
- /package/dist/components/{p-CzTJAO2J.js.map → p-8fddX3V3.js.map} +0 -0
- /package/dist/components/{p-Cw1ljgW1.js.map → p-9u2WDF0J.js.map} +0 -0
- /package/dist/components/{p-M9pOLcyK.js.map → p-B-ChwMFE.js.map} +0 -0
- /package/dist/components/{p-BvlkO__o.js.map → p-BAIYOobl.js.map} +0 -0
- /package/dist/components/{p-BrJebF5t.js.map → p-BIMW5_Z7.js.map} +0 -0
- /package/dist/components/{p-BkKdzYZP.js.map → p-BMI0F08u.js.map} +0 -0
- /package/dist/components/{p-D0kzIzGQ.js.map → p-BV8sNEVP.js.map} +0 -0
- /package/dist/components/{p-DNtwJJOC.js.map → p-BVUdtuog.js.map} +0 -0
- /package/dist/components/{p-CgYDQSLW.js.map → p-BYo1-6b5.js.map} +0 -0
- /package/dist/components/{p-CJ-igzsZ.js.map → p-BZbJkJpG.js.map} +0 -0
- /package/dist/components/{p-D2rASHHq.js.map → p-BcYzHn1E.js.map} +0 -0
- /package/dist/components/{p-0EpjqRWc.js.map → p-BfRAVR40.js.map} +0 -0
- /package/dist/components/{p-BBZz2Dvg.js.map → p-BoWpIl_m.js.map} +0 -0
- /package/dist/components/{p-529k8euH.js.map → p-BpPHCF63.js.map} +0 -0
- /package/dist/components/{p-DXEInGEq.js.map → p-By_jYvHp.js.map} +0 -0
- /package/dist/components/{p-D4r6oWyQ.js.map → p-C860ilDn.js.map} +0 -0
- /package/dist/components/{p-DcVK0W4X.js.map → p-CEHhx4VQ.js.map} +0 -0
- /package/dist/components/{p-CEm0AoDR.js.map → p-CQnjpuMX.js.map} +0 -0
- /package/dist/components/{p-4qKHg502.js.map → p-CUG4EkKn.js.map} +0 -0
- /package/dist/components/{p-CTstnsLe.js.map → p-CYmz92EF.js.map} +0 -0
- /package/dist/components/{p-YE1r-XSR.js.map → p-CbP9ASmI.js.map} +0 -0
- /package/dist/components/{p-C31AjGbg.js.map → p-CbPGO7vl.js.map} +0 -0
- /package/dist/components/{p-DfphSI9S.js.map → p-CdMIA5hz.js.map} +0 -0
- /package/dist/components/{p-Ds58qo_J.js.map → p-CeaIWw1Q.js.map} +0 -0
- /package/dist/components/{p-DxVokKm8.js.map → p-Cfqvxxs2.js.map} +0 -0
- /package/dist/components/{p-CsPqGA8B.js.map → p-CpqLGzv4.js.map} +0 -0
- /package/dist/components/{p-DKg181YU.js.map → p-DFR9hqDU.js.map} +0 -0
- /package/dist/components/{p-DEJLfD7k.js.map → p-DKZP3jCY.js.map} +0 -0
- /package/dist/components/{p-CTAF7SkL.js.map → p-DMPn_dt7.js.map} +0 -0
- /package/dist/components/{p-CYRTkP1A.js.map → p-DYruK9cb.js.map} +0 -0
- /package/dist/components/{p-CqfYCn-e.js.map → p-DiUaIJ91.js.map} +0 -0
- /package/dist/components/{p-Cqg9T781.js.map → p-DimFbKZH.js.map} +0 -0
- /package/dist/components/{p-DHmHllUM.js.map → p-DuhJ49iN.js.map} +0 -0
- /package/dist/components/{p-D_iZPukF.js.map → p-G6r6yYg8.js.map} +0 -0
- /package/dist/components/{p-87FjWil8.js.map → p-IQdLCfJJ.js.map} +0 -0
- /package/dist/components/{p-CAARHqjE.js.map → p-KTaiJztz.js.map} +0 -0
- /package/dist/components/{p-BM-UYlIS.js.map → p-N-2CZze7.js.map} +0 -0
- /package/dist/components/{p-Ch00yccW.js.map → p-Nx52go85.js.map} +0 -0
- /package/dist/components/{p-CC4X8rpT.js.map → p-WEffoQQD.js.map} +0 -0
- /package/dist/components/{p-DDawngok.js.map → p-XKt6j1UN.js.map} +0 -0
- /package/dist/components/{p-vrRbdZAI.js.map → p-nakO4viw.js.map} +0 -0
- /package/dist/components/{p-BC9xYdJD.js.map → p-pkJ3X3s7.js.map} +0 -0
- /package/dist/components/{p-C6VWVB87.js.map → p-r_e6_0nP.js.map} +0 -0
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"states": [
|
|
52
52
|
"Rows expose distinct rest, hover, pressed, keyboard-focus, and inactive states.",
|
|
53
53
|
"An empty sections array or groups with no items render the configured empty message.",
|
|
54
|
-
"The application owns
|
|
54
|
+
"The application owns data fetching, error, permission, routing, and post-activation consequences and reflects availability through the supplied item data.",
|
|
55
|
+
"isLoading preserves the section and row layout while skeletonizing all headings, labels, and values; actions become inert and the card exposes its busy state."
|
|
55
56
|
],
|
|
56
57
|
"responsiveBehavior": [
|
|
57
58
|
"The card fills its parent width and never establishes a product breakpoint.",
|
|
@@ -117,6 +118,15 @@
|
|
|
117
118
|
"mutable": false,
|
|
118
119
|
"description": "Copy shown when no section contains an action."
|
|
119
120
|
},
|
|
121
|
+
"isLoading": {
|
|
122
|
+
"type": "boolean",
|
|
123
|
+
"resolvedType": "boolean",
|
|
124
|
+
"attribute": "is-loading",
|
|
125
|
+
"default": "false",
|
|
126
|
+
"required": false,
|
|
127
|
+
"mutable": false,
|
|
128
|
+
"description": "Retain sections and rows while replacing their headings, labels, and values with skeletons."
|
|
129
|
+
},
|
|
120
130
|
"sections": {
|
|
121
131
|
"type": "ReadonlyArray<CardActionCenterSection>",
|
|
122
132
|
"resolvedType": "readonly CardActionCenterSection[]",
|
|
@@ -149,6 +159,15 @@
|
|
|
149
159
|
"mutable": false,
|
|
150
160
|
"description": "Copy shown when no section contains an action."
|
|
151
161
|
},
|
|
162
|
+
"isLoading": {
|
|
163
|
+
"type": "boolean",
|
|
164
|
+
"resolvedType": "boolean",
|
|
165
|
+
"attribute": "is-loading",
|
|
166
|
+
"default": "false",
|
|
167
|
+
"required": false,
|
|
168
|
+
"mutable": false,
|
|
169
|
+
"description": "Retain sections and rows while replacing their headings, labels, and values with skeletons."
|
|
170
|
+
},
|
|
152
171
|
"sections": {
|
|
153
172
|
"type": "ReadonlyArray<CardActionCenterSection>",
|
|
154
173
|
"resolvedType": "readonly CardActionCenterSection[]",
|
|
@@ -211,18 +230,19 @@
|
|
|
211
230
|
],
|
|
212
231
|
"registryDependencies": [
|
|
213
232
|
"divider",
|
|
233
|
+
"skeleton",
|
|
214
234
|
"tag",
|
|
215
235
|
"text"
|
|
216
236
|
],
|
|
217
237
|
"files": [
|
|
218
238
|
{
|
|
219
239
|
"path": "src/wc/components/CardActionCenter/CardActionCenter.css",
|
|
220
|
-
"content": "@import '../../utils/radius.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/control-inactive.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n display: flex;\n flex-direction: column;\n gap: 0;\n min-width: 0;\n padding: calc(var(--dimension-space-100) - var(--dimension-stroke-width-012));\n box-sizing: border-box;\n border: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n border-radius: var(--ds-radius-card);\n background: var(--color-background-primary);\n}\n\n.card-action-center__group,\n.card-action-center__section {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.card-action-center__section {\n gap: var(--dimension-space-050);\n}\n\n.card-action-center__divider {\n display: flex;\n flex: 0 0 var(--dimension-space-400);\n align-items: center;\n min-width: 0;\n}\n\n.card-action-center__section-heading {\n display: flex;\n flex: 1 1 auto;\n align-items: center;\n min-width: 0;\n min-height: var(--dimension-size-400);\n padding-inline: var(--dimension-space-100);\n box-sizing: border-box;\n user-select: none;\n}\n\n.card-action-center__list {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.card-action-center__item {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n width: 100%;\n height: var(--dimension-size-400);\n padding: 0 var(--dimension-space-075);\n overflow: hidden;\n box-sizing: border-box;\n appearance: none;\n border: none;\n border-radius: var(--dimension-radius-025);\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: start;\n text-decoration: none;\n}\n\n.card-action-center__item--tagged {\n padding-inline-end: var(--dimension-space-075);\n}\n\n.card-action-center__item-content {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--dimension-space-025);\n min-width: 0;\n padding-inline: var(--dimension-space-025);\n}\n\n.card-action-center__item-label {\n display: block;\n min-width: 0;\n}\n\n.card-action-center__item-value {\n flex: 0 0 auto;\n}\n\
|
|
240
|
+
"content": "@import '../../utils/radius.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/control-inactive.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n display: flex;\n flex-direction: column;\n gap: 0;\n min-width: 0;\n padding: calc(var(--dimension-space-100) - var(--dimension-stroke-width-012));\n box-sizing: border-box;\n border: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n border-radius: var(--ds-radius-card);\n background: var(--color-background-primary);\n}\n\n.card-action-center__group,\n.card-action-center__section {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.card-action-center__section {\n gap: var(--dimension-space-050);\n}\n\n.card-action-center__divider {\n display: flex;\n flex: 0 0 var(--dimension-space-400);\n align-items: center;\n min-width: 0;\n}\n\n.card-action-center__section-heading {\n display: flex;\n flex: 1 1 auto;\n align-items: center;\n min-width: 0;\n min-height: var(--dimension-size-400);\n padding-inline: var(--dimension-space-100);\n box-sizing: border-box;\n user-select: none;\n}\n\n.card-action-center__list {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.card-action-center__item {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n width: 100%;\n height: var(--dimension-size-400);\n padding: 0 var(--dimension-space-075);\n overflow: hidden;\n box-sizing: border-box;\n appearance: none;\n border: none;\n border-radius: var(--dimension-radius-025);\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: start;\n text-decoration: none;\n}\n\n.card-action-center__item--tagged {\n padding-inline-end: var(--dimension-space-075);\n}\n\n.card-action-center__item-content {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--dimension-space-025);\n min-width: 0;\n padding-inline: var(--dimension-space-025);\n}\n\n.card-action-center__item-label {\n display: block;\n min-width: 0;\n}\n\n.card-action-center__item-value {\n flex: 0 0 auto;\n}\n\n.card-action-center__item-value ds-tag {\n font-variant-numeric: tabular-nums;\n font-feature-settings: 'tnum';\n}\n\n.card-action-center__section-heading,\n.card-action-center__item-label {\n position: relative;\n}\n\n:host(.card-action-center--loading) .card-action-center__copy {\n visibility: hidden;\n}\n\n.card-action-center__copy-skeleton {\n position: absolute;\n inset-block-start: 50%;\n transform: translateY(-50%);\n}\n\n.card-action-center__empty {\n margin: 0;\n padding: var(--dimension-space-100);\n text-align: center;\n}\n\n@media (forced-colors: active) {\n :host {\n border-color: var(--ds-forced-color-content);\n background: var(--ds-forced-color-surface);\n }\n}\n\n.card-action-center__section-heading .card-action-center__copy-skeleton {\n inset-inline-start: var(--dimension-space-100);\n}\n.card-action-center__item-label .card-action-center__copy-skeleton {\n inset-inline-start: var(--dimension-space-025);\n}\n",
|
|
221
241
|
"type": "registry:ui"
|
|
222
242
|
},
|
|
223
243
|
{
|
|
224
244
|
"path": "src/wc/components/CardActionCenter/CardActionCenter.tsx",
|
|
225
|
-
"content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport { resolveSafeUrl } from '../../utils';\nimport type { TagContrast, TagIntent } from '../Tag/Tag';\n\nexport interface CardActionCenterTag {\n intent?: TagIntent;\n contrast?: TagContrast;\n}\n\nexport interface CardActionCenterItem {\n id: string;\n label: string;\n value?: string | number;\n tag?: CardActionCenterTag;\n href?: string;\n isInactive?: boolean;\n}\n\nexport interface CardActionCenterSection {\n id: string;\n heading: string;\n items: ReadonlyArray<CardActionCenterItem>;\n}\n\nexport interface CardActionCenterActionDetail {\n sectionId: string;\n itemId: string;\n item: CardActionCenterItem;\n href?: string;\n originalEvent: MouseEvent;\n}\n\nlet nextCardActionCenterId = 0;\n\n@Component({\n tag: 'ds-card-action-center',\n styleUrl: 'CardActionCenter.css',\n scoped: true,\n})\nexport class CardActionCenter {\n /** Ordered groups of actions rendered in the card. Empty groups are omitted. */\n @Prop() sections: ReadonlyArray<CardActionCenterSection> = [];\n\n /** Copy shown when no section contains an action. */\n @Prop() emptyMessage: string = 'No actions available';\n\n /**\n * Emitted when an available row is activated. When a row has an `href`,\n * prevent this event to take over navigation with an application router.\n */\n @Event({ cancelable: true }) dsAction!: EventEmitter<CardActionCenterActionDetail>;\n\n private readonly instanceId = ++nextCardActionCenterId;\n\n private get visibleSections(): ReadonlyArray<CardActionCenterSection> {\n return this.sections.filter(section => section.items.length > 0);\n }\n\n private headingId(section: CardActionCenterSection, index: number): string {\n const sectionId = section.id.replace(/[^a-zA-Z0-9_-]/g, '-');\n return `ds-card-action-center-${this.instanceId}-${index}-${sectionId}`;\n }\n\n private handleAction(\n section: CardActionCenterSection,\n item: CardActionCenterItem,\n href: string | undefined,\n originalEvent: MouseEvent\n ) {\n if (item.isInactive) return;\n\n const actionEvent = this.dsAction.emit({\n sectionId: section.id,\n itemId: item.id,\n item,\n href,\n originalEvent,\n });\n if (href && actionEvent.defaultPrevented) originalEvent.preventDefault();\n }\n\n private renderValue(item: CardActionCenterItem) {\n if (item.value === undefined || item.value === null) return null;\n const value = String(item.value);\n\n if (item.tag) {\n return (\n <ds-
|
|
245
|
+
"content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport { resolveSafeUrl } from '../../utils';\nimport type { TagContrast, TagIntent } from '../Tag/Tag';\n\nexport interface CardActionCenterTag {\n intent?: TagIntent;\n contrast?: TagContrast;\n}\n\nexport interface CardActionCenterItem {\n id: string;\n label: string;\n value?: string | number;\n tag?: CardActionCenterTag;\n href?: string;\n isInactive?: boolean;\n}\n\nexport interface CardActionCenterSection {\n id: string;\n heading: string;\n items: ReadonlyArray<CardActionCenterItem>;\n}\n\nexport interface CardActionCenterActionDetail {\n sectionId: string;\n itemId: string;\n item: CardActionCenterItem;\n href?: string;\n originalEvent: MouseEvent;\n}\n\nlet nextCardActionCenterId = 0;\n\n@Component({\n tag: 'ds-card-action-center',\n styleUrl: 'CardActionCenter.css',\n scoped: true,\n})\nexport class CardActionCenter {\n /** Ordered groups of actions rendered in the card. Empty groups are omitted. */\n @Prop() sections: ReadonlyArray<CardActionCenterSection> = [];\n\n /** Copy shown when no section contains an action. */\n @Prop() emptyMessage: string = 'No actions available';\n /** Retain sections and rows while replacing their headings, labels, and values with skeletons. */\n @Prop() isLoading: boolean = false;\n\n /**\n * Emitted when an available row is activated. When a row has an `href`,\n * prevent this event to take over navigation with an application router.\n */\n @Event({ cancelable: true }) dsAction!: EventEmitter<CardActionCenterActionDetail>;\n\n private readonly instanceId = ++nextCardActionCenterId;\n\n private get visibleSections(): ReadonlyArray<CardActionCenterSection> {\n return this.sections.filter(section => section.items.length > 0);\n }\n\n private headingId(section: CardActionCenterSection, index: number): string {\n const sectionId = section.id.replace(/[^a-zA-Z0-9_-]/g, '-');\n return `ds-card-action-center-${this.instanceId}-${index}-${sectionId}`;\n }\n\n private handleAction(\n section: CardActionCenterSection,\n item: CardActionCenterItem,\n href: string | undefined,\n originalEvent: MouseEvent\n ) {\n if (item.isInactive || this.isLoading) return;\n\n const actionEvent = this.dsAction.emit({\n sectionId: section.id,\n itemId: item.id,\n item,\n href,\n originalEvent,\n });\n if (href && actionEvent.defaultPrevented) originalEvent.preventDefault();\n }\n\n private renderValue(item: CardActionCenterItem) {\n if (item.value === undefined || item.value === null) return null;\n const value = String(item.value);\n\n if (item.tag) {\n return (\n <ds-skeleton\n class=\"card-action-center__item-value\"\n preserveLayout\n isLoading={this.isLoading}\n variant=\"control\"\n rounded\n >\n <ds-tag\n class=\"ds-interaction-fill__content\"\n label={value}\n intent={item.tag.intent ?? 'neutral'}\n contrast={item.tag.contrast ?? 'faint'}\n size=\"sm\"\n isInset\n rounded\n ></ds-tag>\n </ds-skeleton>\n );\n }\n\n return (\n <ds-skeleton class=\"card-action-center__item-value\" preserveLayout isLoading={this.isLoading}>\n <ds-text\n class=\"ds-interaction-fill__content\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n emphasis\n wrap=\"nowrap\"\n fontFeature=\"tabular-nums\"\n as=\"span\"\n >\n {value}\n </ds-text>\n </ds-skeleton>\n );\n }\n\n private renderItem(section: CardActionCenterSection, item: CardActionCenterItem) {\n const href = item.isInactive || this.isLoading ? undefined : resolveSafeUrl(item.href);\n const classes = {\n 'card-action-center__item': true,\n 'card-action-center__item--tagged': !!item.tag && item.value != null,\n 'ds-interaction-fill': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-inactive': !!item.isInactive && !this.isLoading,\n };\n const content = [\n <ds-text\n class=\"card-action-center__item-content card-action-center__item-label ds-interaction-fill__content\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n wrap=\"balance\"\n as=\"span\"\n aria-hidden={this.isLoading ? 'true' : undefined}\n >\n <span class=\"card-action-center__copy\">{item.label}</span>\n {this.isLoading && <ds-skeleton class=\"card-action-center__copy-skeleton\" width=\"75%\" />}\n </ds-text>,\n this.renderValue(item),\n ];\n\n if (href) {\n return (\n <a\n class={classes}\n href={href}\n onClick={(event: MouseEvent) => this.handleAction(section, item, href, event)}\n >\n {content}\n </a>\n );\n }\n\n return (\n <button\n type=\"button\"\n class={classes}\n disabled={this.isLoading || item.isInactive || undefined}\n inert={this.isLoading}\n aria-hidden={this.isLoading ? 'true' : undefined}\n onClick={(event: MouseEvent) => this.handleAction(section, item, undefined, event)}\n >\n {content}\n </button>\n );\n }\n\n render() {\n const sections = this.visibleSections;\n\n return (\n <Host\n class={{ 'card-action-center--loading': this.isLoading }}\n aria-busy={this.isLoading ? 'true' : undefined}\n >\n {sections.length ? (\n sections.map((section, sectionIndex) => (\n <div class=\"card-action-center__group\">\n {sectionIndex > 0 ? (\n <div class=\"card-action-center__divider\">\n <ds-divider\n inset=\"space-100\"\n length=\"calc(100% - var(--dimension-space-200))\"\n ></ds-divider>\n </div>\n ) : null}\n <section\n class=\"card-action-center__section\"\n aria-labelledby={this.headingId(section, sectionIndex)}\n >\n <ds-text\n id={this.headingId(section, sectionIndex)}\n class=\"card-action-center__section-heading\"\n variant=\"text-title-small\"\n emphasis\n color=\"primary\"\n wrap=\"balance\"\n as=\"h2\"\n aria-hidden={this.isLoading ? 'true' : undefined}\n >\n <span class=\"card-action-center__copy\">{section.heading}</span>\n {this.isLoading && (\n <ds-skeleton\n class=\"card-action-center__copy-skeleton\"\n textVariant=\"text-title-small\"\n width=\"min(60%, calc(var(--dimension-size-800) * 2))\"\n />\n )}\n </ds-text>\n <ul class=\"card-action-center__list\">\n {section.items.map(item => (\n <li>{this.renderItem(section, item)}</li>\n ))}\n </ul>\n </section>\n </div>\n ))\n ) : (\n <ds-text\n class=\"card-action-center__empty\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n wrap=\"balance\"\n as=\"p\"\n >\n {this.emptyMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
|
|
226
246
|
"type": "registry:ui"
|
|
227
247
|
}
|
|
228
248
|
]
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"states": [
|
|
46
46
|
"Custom mode provides flexible body chrome and does not mutate chart or legend interaction.",
|
|
47
47
|
"Chart mode keeps a slotted external legend static by disabling hover emphasis.",
|
|
48
|
-
"showFilter adds the standard filter action and emits dsFilterClick without owning filter state."
|
|
48
|
+
"showFilter adds the standard filter action and emits dsFilterClick without owning filter state.",
|
|
49
|
+
"isLoading preserves card geometry, skeletonizes the heading and standard filter control, and replaces a chart canvas with one quiet placeholder. The owner supplies skeletons for custom body content and slotted actions."
|
|
49
50
|
],
|
|
50
51
|
"responsiveBehavior": [
|
|
51
52
|
"The sm, md, or lg width sets a matching token-based minimum height and remains constrained by its parent.",
|
|
@@ -132,6 +133,15 @@
|
|
|
132
133
|
"mutable": false,
|
|
133
134
|
"description": "Chart heading shown in the card header."
|
|
134
135
|
},
|
|
136
|
+
"isLoading": {
|
|
137
|
+
"type": "boolean",
|
|
138
|
+
"resolvedType": "boolean",
|
|
139
|
+
"attribute": "is-loading",
|
|
140
|
+
"default": "false",
|
|
141
|
+
"required": false,
|
|
142
|
+
"mutable": false,
|
|
143
|
+
"description": "Skeletonize the header and chart canvas; custom body content owns its loading atoms."
|
|
144
|
+
},
|
|
135
145
|
"showFilter": {
|
|
136
146
|
"type": "boolean",
|
|
137
147
|
"resolvedType": "boolean",
|
|
@@ -190,6 +200,15 @@
|
|
|
190
200
|
"mutable": false,
|
|
191
201
|
"description": "Chart heading shown in the card header."
|
|
192
202
|
},
|
|
203
|
+
"isLoading": {
|
|
204
|
+
"type": "boolean",
|
|
205
|
+
"resolvedType": "boolean",
|
|
206
|
+
"attribute": "is-loading",
|
|
207
|
+
"default": "false",
|
|
208
|
+
"required": false,
|
|
209
|
+
"mutable": false,
|
|
210
|
+
"description": "Skeletonize the header and chart canvas; custom body content owns its loading atoms."
|
|
211
|
+
},
|
|
193
212
|
"showFilter": {
|
|
194
213
|
"type": "boolean",
|
|
195
214
|
"resolvedType": "boolean",
|
|
@@ -261,17 +280,18 @@
|
|
|
261
280
|
],
|
|
262
281
|
"registryDependencies": [
|
|
263
282
|
"button-unfilled",
|
|
283
|
+
"skeleton",
|
|
264
284
|
"text"
|
|
265
285
|
],
|
|
266
286
|
"files": [
|
|
267
287
|
{
|
|
268
288
|
"path": "src/wc/components/CardChart/CardChart.css",
|
|
269
|
-
"content": "@import '../../utils/radius.css';\n@import '../../utils/chrome-header.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n position: relative;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n width: var(--_card-chart-width);\n min-height: var(--_card-chart-min-height);\n max-width: 100%;\n overflow: visible;\n border: none;\n border-radius: var(--ds-radius-card);\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-shadow-elevated-sm);\n}\n\n:host::after {\n content: '';\n position: absolute;\n inset: 0;\n z-index: 4;\n border-radius: inherit;\n box-shadow: var(--effect-highlight-elevated-sm);\n pointer-events: none;\n}\n\n.card-chart__header {\n flex-shrink: 0;\n border-bottom: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n}\n\n.card-chart__copy {\n align-items: center;\n}\n\nds-text.card-chart__title {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.card-chart__actions {\n flex-shrink: 0;\n}\n\n.card-chart__actions:empty {\n display: none;\n}\n\n.card-chart__body,\n.card-chart__layout {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n box-sizing: border-box;\n min-height: 0;\n}\n\n.card-chart__body {\n border-radius: 0 0 var(--ds-radius-card) var(--ds-radius-card);\n background-color: var(--color-background-primary);\n}\n\n.card-chart__layout {\n height: 100%;\n gap: var(--dimension-space-200);\n}\n\n.card-chart__chart {\n display: grid;\n flex: 1 1 auto;\n align-items: center;\n box-sizing: border-box;\n min-height: 0;\n padding: var(--dimension-space-200) var(--dimension-space-200) 0 var(--dimension-space-200);\n}\n\n.card-chart__chart > * {\n width: 100%;\n min-width: 0;\n max-width: 100%;\n}\n\n.card-chart__chart > ds-chart {\n --ds-chart-container-height: 100%;\n}\n\n.card-chart__legend {\n flex: 0 0 auto;\n box-sizing: border-box;\n padding: 0 var(--dimension-space-100) var(--dimension-space-100) var(--dimension-space-100);\n}\n\n@media (forced-colors: active) {\n :host {\n box-shadow: none;\n outline: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n outline-offset: calc(-1 * var(--dimension-stroke-width-012));\n }\n\n :host::after {\n box-shadow: none;\n }\n}\n",
|
|
289
|
+
"content": "@import '../../utils/radius.css';\n@import '../../utils/chrome-header.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n position: relative;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n width: var(--_card-chart-width);\n min-height: var(--_card-chart-min-height);\n max-width: 100%;\n overflow: visible;\n border: none;\n border-radius: var(--ds-radius-card);\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-shadow-elevated-sm);\n}\n\n:host::after {\n content: '';\n position: absolute;\n inset: 0;\n z-index: 4;\n border-radius: inherit;\n box-shadow: var(--effect-highlight-elevated-sm);\n pointer-events: none;\n}\n\n.card-chart__header {\n flex-shrink: 0;\n border-bottom: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n}\n\n.card-chart__copy {\n position: relative;\n align-items: center;\n}\n\nds-text.card-chart__title {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.card-chart__actions {\n flex-shrink: 0;\n}\n\n.card-chart__actions:empty {\n display: none;\n}\n\n.card-chart__body,\n.card-chart__layout {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n box-sizing: border-box;\n min-height: 0;\n}\n\n.card-chart__body {\n border-radius: 0 0 var(--ds-radius-card) var(--ds-radius-card);\n background-color: var(--color-background-primary);\n}\n\n.card-chart__layout {\n height: 100%;\n gap: var(--dimension-space-200);\n}\n\n.card-chart__chart {\n position: relative;\n display: grid;\n flex: 1 1 auto;\n align-items: center;\n box-sizing: border-box;\n min-height: 0;\n padding: var(--dimension-space-200) var(--dimension-space-200) 0 var(--dimension-space-200);\n}\n\n.card-chart__chart > * {\n width: 100%;\n min-width: 0;\n max-width: 100%;\n}\n\n.card-chart__chart > ds-chart {\n --ds-chart-container-height: 100%;\n}\n\n.card-chart__legend {\n position: relative;\n flex: 0 0 auto;\n box-sizing: border-box;\n padding: 0 var(--dimension-space-100) var(--dimension-space-100) var(--dimension-space-100);\n}\n\n:host(.card-chart--loading) .card-chart__title,\n:host(.card-chart--loading) .card-chart__chart,\n:host(.card-chart--loading) .card-chart__legend {\n visibility: hidden;\n}\n\n.card-chart__heading-skeleton {\n position: absolute;\n inset-inline-start: calc(var(--ds-control-padding-inline) + var(--ds-control-label-inset));\n width: min(60%, calc(var(--dimension-size-800) * 2));\n}\n\n.card-chart__chart > .card-chart__plot-skeleton {\n visibility: visible;\n position: absolute;\n inset: var(--dimension-space-200);\n width: auto;\n height: auto;\n}\n\n@media (forced-colors: active) {\n :host {\n box-shadow: none;\n outline: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n outline-offset: calc(-1 * var(--dimension-stroke-width-012));\n }\n\n :host::after {\n box-shadow: none;\n }\n}\n\n.card-chart__legend-skeleton {\n position: absolute;\n inset-block-start: 0;\n inset-inline-start: var(--dimension-space-100);\n visibility: visible;\n}\n",
|
|
270
290
|
"type": "registry:ui"
|
|
271
291
|
},
|
|
272
292
|
{
|
|
273
293
|
"path": "src/wc/components/CardChart/CardChart.tsx",
|
|
274
|
-
"content": "import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';\n\nexport type CardChartWidth = 'sm' | 'md' | 'lg';\nexport type CardChartVariant = 'custom' | 'chart';\n\ntype LegendSlot = HTMLElement & {\n activeLabel?: string | null;\n highlightOnHover?: boolean;\n};\n\nconst CARD_WIDTH_VARS: Record<CardChartWidth, string> = {\n sm: 'var(--dimension-card-width-sm)',\n md: 'var(--dimension-card-width-md)',\n lg: 'var(--dimension-card-width-lg)',\n};\n\nconst CARD_HEIGHT_VARS: Record<CardChartWidth, string> = {\n sm: 'var(--dimension-card-height-sm)',\n md: 'var(--dimension-card-height-md)',\n lg: 'var(--dimension-card-height-lg)',\n};\n\n/**\n * Standard chart card chrome and composition. The variant owns\n * only the chart/legend relationship; applications continue to own data.\n */\n@Component({\n tag: 'ds-card-chart',\n styleUrl: 'CardChart.css',\n scoped: true,\n})\nexport class CardChart {\n @Element() el!: HTMLElement;\n\n /** Chart heading shown in the card header. */\n @Prop() heading!: string;\n /** Chart composition behavior. */\n @Prop() variant: CardChartVariant = 'custom';\n /** Width token with the matching chart-card min-height. */\n @Prop() cardWidth: CardChartWidth = 'md';\n /** Renders the standard filter action before custom actions. */\n @Prop() showFilter: boolean = false;\n @Prop() filterLabel: string = 'Filter';\n\n /** Emits when the standard header filter control is activated. */\n @Event() dsFilterClick!: EventEmitter<void>;\n\n @State() private hasChartSlot = false;\n @State() private hasLegendSlot = false;\n\n componentWillRender() {\n this.hasChartSlot = !!this.el.querySelector('[slot=\"chart\"]');\n this.hasLegendSlot = !!this.el.querySelector('[slot=\"legend\"]');\n }\n\n componentDidRender() {\n const legend = this.el.querySelector('ds-chart-legend[slot=\"legend\"]') as LegendSlot | null;\n if (this.variant === 'chart' && legend) {\n legend.setAttribute('highlight-on-hover', 'false');\n if ('highlightOnHover' in legend) legend.highlightOnHover = false;\n }\n }\n\n private handleFilterClick = () => {\n this.dsFilterClick.emit();\n };\n\n render() {\n const usesChartLayout = this.variant !== 'custom' || this.hasChartSlot;\n return (\n <Host\n class={{\n 'card-chart': true,\n [`card-chart--${this.variant}`]: true,\n }}\n style={{\n '--_card-chart-width': CARD_WIDTH_VARS[this.cardWidth],\n '--_card-chart-min-height': CARD_HEIGHT_VARS[this.cardWidth],\n }}\n >\n <header class=\"card-chart__header ds-chrome-header\">\n
|
|
294
|
+
"content": "import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';\n\nexport type CardChartWidth = 'sm' | 'md' | 'lg';\nexport type CardChartVariant = 'custom' | 'chart';\n\ntype LegendSlot = HTMLElement & {\n activeLabel?: string | null;\n highlightOnHover?: boolean;\n};\n\nconst CARD_WIDTH_VARS: Record<CardChartWidth, string> = {\n sm: 'var(--dimension-card-width-sm)',\n md: 'var(--dimension-card-width-md)',\n lg: 'var(--dimension-card-width-lg)',\n};\n\nconst CARD_HEIGHT_VARS: Record<CardChartWidth, string> = {\n sm: 'var(--dimension-card-height-sm)',\n md: 'var(--dimension-card-height-md)',\n lg: 'var(--dimension-card-height-lg)',\n};\n\n/**\n * Standard chart card chrome and composition. The variant owns\n * only the chart/legend relationship; applications continue to own data.\n */\n@Component({\n tag: 'ds-card-chart',\n styleUrl: 'CardChart.css',\n scoped: true,\n})\nexport class CardChart {\n @Element() el!: HTMLElement;\n\n /** Chart heading shown in the card header. */\n @Prop() heading!: string;\n /** Chart composition behavior. */\n @Prop() variant: CardChartVariant = 'custom';\n /** Width token with the matching chart-card min-height. */\n @Prop() cardWidth: CardChartWidth = 'md';\n /** Renders the standard filter action before custom actions. */\n @Prop() showFilter: boolean = false;\n @Prop() filterLabel: string = 'Filter';\n /** Skeletonize the header and chart canvas; custom body content owns its loading atoms. */\n @Prop() isLoading: boolean = false;\n\n /** Emits when the standard header filter control is activated. */\n @Event() dsFilterClick!: EventEmitter<void>;\n\n @State() private hasChartSlot = false;\n @State() private hasLegendSlot = false;\n\n componentWillRender() {\n this.hasChartSlot = !!this.el.querySelector('[slot=\"chart\"]');\n this.hasLegendSlot = !!this.el.querySelector('[slot=\"legend\"]');\n }\n\n componentDidRender() {\n const legend = this.el.querySelector('ds-chart-legend[slot=\"legend\"]') as LegendSlot | null;\n if (this.variant === 'chart' && legend) {\n legend.setAttribute('highlight-on-hover', 'false');\n if ('highlightOnHover' in legend) legend.highlightOnHover = false;\n }\n }\n\n private handleFilterClick = () => {\n if (this.isLoading) return;\n this.dsFilterClick.emit();\n };\n\n render() {\n const usesChartLayout = this.variant !== 'custom' || this.hasChartSlot;\n return (\n <Host\n class={{\n 'card-chart': true,\n [`card-chart--${this.variant}`]: true,\n 'card-chart--loading': this.isLoading,\n }}\n aria-busy={this.isLoading ? 'true' : undefined}\n style={{\n '--_card-chart-width': CARD_WIDTH_VARS[this.cardWidth],\n '--_card-chart-min-height': CARD_HEIGHT_VARS[this.cardWidth],\n }}\n >\n <header class=\"card-chart__header ds-chrome-header\">\n <div class=\"card-chart__copy ds-chrome-header__copy ds-control--md\">\n <ds-text\n class=\"card-chart__title ds-chrome-header__heading\"\n variant=\"text-title-small\"\n emphasis\n color=\"primary\"\n as=\"h2\"\n aria-hidden={this.isLoading ? 'true' : undefined}\n >\n {this.heading}\n </ds-text>\n {this.isLoading && (\n <ds-skeleton class=\"card-chart__heading-skeleton\" textVariant=\"text-title-small\" />\n )}\n </div>\n <div class=\"card-chart__actions ds-chrome-header__trailing\">\n {this.showFilter ? (\n <ds-skeleton\n preserveLayout\n isLoading={this.isLoading}\n variant=\"control\"\n controlAppearance=\"outlined\"\n controlContent=\"icon\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n type=\"button\"\n icon=\"Filters\"\n aria-label={this.filterLabel}\n onDsClick={this.handleFilterClick}\n />\n </ds-skeleton>\n ) : null}\n <slot name=\"actions\" />\n </div>\n </header>\n <div class=\"card-chart__body\">\n {usesChartLayout ? (\n <div class=\"card-chart__layout\">\n {this.hasChartSlot || this.isLoading ? (\n <div\n class={{\n 'card-chart__chart': true,\n }}\n >\n <slot name=\"chart\" />\n {this.isLoading && (\n <ds-skeleton class=\"card-chart__plot-skeleton\" variant=\"control\" />\n )}\n </div>\n ) : null}\n {this.hasLegendSlot ? (\n <div class=\"card-chart__legend\">\n <slot name=\"legend\" />\n {this.isLoading && (\n <ds-skeleton\n class=\"card-chart__legend-skeleton\"\n width=\"min(60%, calc(var(--dimension-size-800) * 2))\"\n />\n )}\n </div>\n ) : null}\n <slot />\n </div>\n ) : (\n <slot />\n )}\n </div>\n </Host>\n );\n }\n}\n",
|
|
275
295
|
"type": "registry:ui"
|
|
276
296
|
}
|
|
277
297
|
]
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"In content mode, the native link owns those interaction states only across the header while the body remains application-owned.",
|
|
53
53
|
"An empty or unsafe href renders non-interactive card chrome without a false keyboard target.",
|
|
54
54
|
"Copy is always stacked, so a missing description keeps the heading at the start of the column instead of centering it in the header.",
|
|
55
|
-
"The application owns destination loading, authorization, not-found, and recovery states after navigation."
|
|
55
|
+
"The application owns destination loading, authorization, not-found, and recovery states after navigation.",
|
|
56
|
+
"isLoading preserves card geometry and copy wrapping while showing skeletons and preventing navigation."
|
|
56
57
|
],
|
|
57
58
|
"responsiveBehavior": [
|
|
58
59
|
"Choose sm, md, or lg from the parent layout; the surface never exceeds its parent width.",
|
|
@@ -155,6 +156,15 @@
|
|
|
155
156
|
"mutable": false,
|
|
156
157
|
"description": "Destination for the card's native link."
|
|
157
158
|
},
|
|
159
|
+
"isLoading": {
|
|
160
|
+
"type": "boolean",
|
|
161
|
+
"resolvedType": "boolean",
|
|
162
|
+
"attribute": "is-loading",
|
|
163
|
+
"default": "false",
|
|
164
|
+
"required": false,
|
|
165
|
+
"mutable": false,
|
|
166
|
+
"description": "Preserve the card layout while its copy and navigation affordance show skeletons."
|
|
167
|
+
},
|
|
158
168
|
"variant": {
|
|
159
169
|
"type": "CardNavigationVariant",
|
|
160
170
|
"resolvedType": "\"content\" | \"navigation-only\"",
|
|
@@ -212,6 +222,15 @@
|
|
|
212
222
|
"mutable": false,
|
|
213
223
|
"description": "Destination for the card's native link."
|
|
214
224
|
},
|
|
225
|
+
"isLoading": {
|
|
226
|
+
"type": "boolean",
|
|
227
|
+
"resolvedType": "boolean",
|
|
228
|
+
"attribute": "is-loading",
|
|
229
|
+
"default": "false",
|
|
230
|
+
"required": false,
|
|
231
|
+
"mutable": false,
|
|
232
|
+
"description": "Preserve the card layout while its copy and navigation affordance show skeletons."
|
|
233
|
+
},
|
|
215
234
|
"variant": {
|
|
216
235
|
"type": "CardNavigationVariant",
|
|
217
236
|
"resolvedType": "\"content\" | \"navigation-only\"",
|
|
@@ -275,17 +294,18 @@
|
|
|
275
294
|
],
|
|
276
295
|
"registryDependencies": [
|
|
277
296
|
"icon",
|
|
297
|
+
"skeleton",
|
|
278
298
|
"text"
|
|
279
299
|
],
|
|
280
300
|
"files": [
|
|
281
301
|
{
|
|
282
302
|
"path": "src/wc/components/CardNavigation/CardNavigation.css",
|
|
283
|
-
"content": "@import '../../utils/radius.css';\n@import '../../utils/chrome-header.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n position: relative;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n width: var(--_card-navigation-width);\n max-width: 100%;\n overflow: hidden;\n border: none;\n border-radius: var(--ds-radius-card);\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-shadow-elevated-sm);\n}\n\n:host(.card-navigation--content) {\n min-height: var(--_card-navigation-min-height);\n}\n\n:host::after {\n content: '';\n position: absolute;\n inset: 0;\n z-index: 4;\n border-radius: inherit;\n box-shadow: var(--effect-highlight-elevated-sm);\n pointer-events: none;\n}\n\n.card-navigation__target {\n color: inherit;\n text-decoration: none;\n cursor: pointer;\n}\n\n.card-navigation__header {\n flex: 0 0 auto;\n align-items: flex-start;\n border-radius: inherit;\n}\n\n:host(.card-navigation--content) .card-navigation__header {\n border-radius: var(--ds-radius-card) var(--ds-radius-card) 0 0;\n --ds-interaction-divider-width: var(--dimension-stroke-width-012);\n --ds-interaction-divider-color: var(--color-border-tertiary);\n}\n\n.card-navigation__copy {\n min-height: var(--dimension-size-400);\n justify-content: flex-start;\n}\n\nds-text.card-navigation__title,\nds-text.card-navigation__description {\n overflow-wrap: anywhere;\n}\n\n.card-navigation__chevron {\n align-self: center;\n justify-content: center;\n width: var(--dimension-size-400);\n color: var(--color-foreground-secondary);\n}\n\n.card-navigation__body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n box-sizing: border-box;\n background-color: var(--color-background-primary);\n border-radius: 0 0 var(--ds-radius-card) var(--ds-radius-card);\n}\n\n@media (forced-colors: active) {\n :host {\n box-shadow: none;\n outline: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n outline-offset: calc(-1 * var(--dimension-stroke-width-012));\n }\n\n :host::after {\n box-shadow: none;\n }\n}\n",
|
|
303
|
+
"content": "@import '../../utils/radius.css';\n@import '../../utils/chrome-header.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n position: relative;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n width: var(--_card-navigation-width);\n max-width: 100%;\n overflow: hidden;\n border: none;\n border-radius: var(--ds-radius-card);\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-shadow-elevated-sm);\n}\n\n:host(.card-navigation--content) {\n min-height: var(--_card-navigation-min-height);\n}\n\n:host::after {\n content: '';\n position: absolute;\n inset: 0;\n z-index: 4;\n border-radius: inherit;\n box-shadow: var(--effect-highlight-elevated-sm);\n pointer-events: none;\n}\n\n.card-navigation__target {\n color: inherit;\n text-decoration: none;\n cursor: pointer;\n}\n\n.card-navigation__header {\n flex: 0 0 auto;\n align-items: flex-start;\n border-radius: inherit;\n}\n\n:host(.card-navigation--content) .card-navigation__header {\n border-radius: var(--ds-radius-card) var(--ds-radius-card) 0 0;\n --ds-interaction-divider-width: var(--dimension-stroke-width-012);\n --ds-interaction-divider-color: var(--color-border-tertiary);\n}\n\n.card-navigation__copy {\n min-height: var(--dimension-size-400);\n justify-content: flex-start;\n}\n\n.card-navigation__line {\n display: grid;\n min-width: 0;\n align-items: start;\n}\n\n.card-navigation__line > * {\n grid-area: 1 / 1;\n}\n\n:host(.card-navigation--loading) ds-text {\n visibility: hidden;\n}\n\nds-text.card-navigation__title,\nds-text.card-navigation__description {\n overflow-wrap: anywhere;\n}\n\n.card-navigation__chevron {\n align-self: center;\n justify-content: center;\n width: var(--dimension-size-400);\n color: var(--color-foreground-secondary);\n}\n\n.card-navigation__body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n box-sizing: border-box;\n background-color: var(--color-background-primary);\n border-radius: 0 0 var(--ds-radius-card) var(--ds-radius-card);\n}\n\n@media (forced-colors: active) {\n :host {\n box-shadow: none;\n outline: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n outline-offset: calc(-1 * var(--dimension-stroke-width-012));\n }\n\n :host::after {\n box-shadow: none;\n }\n}\n",
|
|
284
304
|
"type": "registry:ui"
|
|
285
305
|
},
|
|
286
306
|
{
|
|
287
307
|
"path": "src/wc/components/CardNavigation/CardNavigation.tsx",
|
|
288
|
-
"content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport { resolveSafeUrl } from '../../utils';\n\nexport type CardNavigationWidth = 'sm' | 'md' | 'lg';\nexport type CardNavigationVariant = 'navigation-only' | 'content';\n\nexport interface CardNavigationDetail {\n href: string;\n originalEvent: MouseEvent;\n}\n\nconst CARD_WIDTH_VARS: Record<CardNavigationWidth, string> = {\n sm: 'var(--dimension-card-width-sm)',\n md: 'var(--dimension-card-width-md)',\n lg: 'var(--dimension-card-width-lg)',\n};\n\nconst CARD_HEIGHT_VARS: Record<CardNavigationWidth, string> = {\n sm: 'var(--dimension-card-height-sm)',\n md: 'var(--dimension-card-height-md)',\n lg: 'var(--dimension-card-height-lg)',\n};\n\n@Component({\n tag: 'ds-card-navigation',\n styleUrl: 'CardNavigation.css',\n scoped: true,\n})\nexport class CardNavigation {\n /** Destination for the card's native link. */\n @Prop() href!: string;\n\n /** Section or destination heading shown in the card header. */\n @Prop() heading!: string;\n\n /** Optional supporting copy shown below the heading. */\n @Prop() description: string | undefined;\n\n /**\n * `navigation-only` makes the complete card the link. `content` keeps the\n * header as the link and exposes a non-interactive body slot below it.\n */\n @Prop() variant: CardNavigationVariant = 'navigation-only';\n\n /** Card width token (`sm` / `md` / `lg`). */\n @Prop() cardWidth: CardNavigationWidth = 'md';\n\n /**\n * Emits before native navigation. Prevent this event to take over routing;\n * the component will then prevent the original link navigation.\n */\n @Event({ cancelable: true }) dsNavigate!: EventEmitter<CardNavigationDetail>;\n\n private handleNavigation(href: string, originalEvent: MouseEvent) {\n const navigateEvent = this.dsNavigate.emit({ href, originalEvent });\n if (navigateEvent.defaultPrevented) originalEvent.preventDefault();\n }\n\n private renderHeader(href: string | undefined) {\n const Target = href ? 'a' : 'div';\n const description = this.description?.trim();\n\n return (\n <Target\n class={{\n 'card-navigation__target': true,\n 'card-navigation__header': true,\n 'ds-chrome-header': true,\n 'ds-interaction-fill': !!href,\n 'ds-focus-ring-inset': !!href,\n }}\n href={href}\n onClick={href ? (event: MouseEvent) => this.handleNavigation(href, event) : undefined}\n >\n <div\n class={{\n 'card-navigation__copy': true,\n 'ds-chrome-header__copy': true,\n 'ds-chrome-header__copy--stacked': true,\n 'ds-control--md': true,\n 'ds-interaction-fill__content': true,\n }}\n >\n <ds-text\n
|
|
308
|
+
"content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport { resolveSafeUrl } from '../../utils';\n\nexport type CardNavigationWidth = 'sm' | 'md' | 'lg';\nexport type CardNavigationVariant = 'navigation-only' | 'content';\n\nexport interface CardNavigationDetail {\n href: string;\n originalEvent: MouseEvent;\n}\n\nconst CARD_WIDTH_VARS: Record<CardNavigationWidth, string> = {\n sm: 'var(--dimension-card-width-sm)',\n md: 'var(--dimension-card-width-md)',\n lg: 'var(--dimension-card-width-lg)',\n};\n\nconst CARD_HEIGHT_VARS: Record<CardNavigationWidth, string> = {\n sm: 'var(--dimension-card-height-sm)',\n md: 'var(--dimension-card-height-md)',\n lg: 'var(--dimension-card-height-lg)',\n};\n\n@Component({\n tag: 'ds-card-navigation',\n styleUrl: 'CardNavigation.css',\n scoped: true,\n})\nexport class CardNavigation {\n /** Destination for the card's native link. */\n @Prop() href!: string;\n\n /** Section or destination heading shown in the card header. */\n @Prop() heading!: string;\n\n /** Optional supporting copy shown below the heading. */\n @Prop() description: string | undefined;\n /** Preserve the card layout while its copy and navigation affordance show skeletons. */\n @Prop() isLoading = false;\n\n /**\n * `navigation-only` makes the complete card the link. `content` keeps the\n * header as the link and exposes a non-interactive body slot below it.\n */\n @Prop() variant: CardNavigationVariant = 'navigation-only';\n\n /** Card width token (`sm` / `md` / `lg`). */\n @Prop() cardWidth: CardNavigationWidth = 'md';\n\n /**\n * Emits before native navigation. Prevent this event to take over routing;\n * the component will then prevent the original link navigation.\n */\n @Event({ cancelable: true }) dsNavigate!: EventEmitter<CardNavigationDetail>;\n\n private handleNavigation(href: string, originalEvent: MouseEvent) {\n const navigateEvent = this.dsNavigate.emit({ href, originalEvent });\n if (navigateEvent.defaultPrevented) originalEvent.preventDefault();\n }\n\n private renderHeader(href: string | undefined) {\n const Target = href ? 'a' : 'div';\n const description = this.description?.trim();\n\n return (\n <Target\n class={{\n 'card-navigation__target': true,\n 'card-navigation__header': true,\n 'ds-chrome-header': true,\n 'ds-interaction-fill': !!href,\n 'ds-focus-ring-inset': !!href,\n }}\n href={href}\n inert={this.isLoading}\n onClick={href ? (event: MouseEvent) => this.handleNavigation(href, event) : undefined}\n >\n <div\n class={{\n 'card-navigation__copy': true,\n 'ds-chrome-header__copy': true,\n 'ds-chrome-header__copy--stacked': true,\n 'ds-control--md': true,\n 'ds-interaction-fill__content': true,\n }}\n >\n <div class=\"card-navigation__line\">\n <ds-text\n class=\"card-navigation__title ds-chrome-header__heading\"\n variant=\"text-title-small\"\n emphasis\n color=\"primary\"\n as=\"h2\"\n >\n {this.heading}\n </ds-text>\n {this.isLoading && (\n <ds-skeleton\n width=\"min(60%, calc(var(--dimension-size-800) * 2))\"\n textVariant=\"text-title-small\"\n />\n )}\n </div>\n {description ? (\n <div class=\"card-navigation__line\">\n <ds-text\n class=\"card-navigation__description ds-chrome-header__description\"\n variant=\"text-body-small\"\n color=\"secondary\"\n as=\"span\"\n >\n {description}\n </ds-text>\n {this.isLoading && <ds-skeleton width=\"95%\" textVariant=\"text-body-small\" />}\n </div>\n ) : null}\n </div>\n <span class=\"card-navigation__chevron ds-chrome-header__trailing ds-interaction-fill__content\">\n {this.isLoading ? (\n <ds-skeleton variant=\"icon\" iconSize=\"md\" />\n ) : (\n <ds-icon name=\"ChevronRight\" size=\"md\" color=\"inherit\" aria-hidden=\"true\" />\n )}\n </span>\n </Target>\n );\n }\n\n render() {\n const href = this.isLoading ? undefined : resolveSafeUrl(this.href);\n const hasContent = this.variant === 'content';\n\n return (\n <Host\n aria-busy={this.isLoading ? 'true' : undefined}\n class={{\n 'card-navigation': true,\n 'card-navigation--loading': this.isLoading,\n 'card-navigation--navigation-only': !hasContent,\n 'card-navigation--content': hasContent,\n }}\n style={{\n '--_card-navigation-width': CARD_WIDTH_VARS[this.cardWidth],\n '--_card-navigation-min-height': CARD_HEIGHT_VARS[this.cardWidth],\n }}\n >\n {this.renderHeader(href)}\n {hasContent ? (\n <div class=\"card-navigation__body\">\n <slot />\n </div>\n ) : null}\n </Host>\n );\n }\n}\n",
|
|
289
309
|
"type": "registry:ui"
|
|
290
310
|
}
|
|
291
311
|
]
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"Trend is supplied already resolved as a direction, a formatted value, and a tone. Whether a rise reads well is a product decision, so the component never infers tone from the numbers. Derive it with resolveMetricTrend.",
|
|
57
57
|
"Omit a trend when there is nothing to report; resolveMetricTrend returns null in that case so no zero change is rendered.",
|
|
58
58
|
"Omit score to render a centered Score unavailable cell with a tertiary border and no score fill. Set scoreErrorMessage to replace that copy while the measures remain readable.",
|
|
59
|
-
"Loading replaces the score and
|
|
59
|
+
"Loading replaces the score, measures, and fixed period copy with skeletons. The owner supplies loading atoms for slotted controls. Score loading replaces only the fill figure; the overview still skeletons the score-copy spacer and trend. When measures are already supplied, their count preserves the resolved grid shape; otherwise five measure placeholders join the score in the standard six-cell grid.",
|
|
60
60
|
"At most seven measures are shown. Keep larger collections outside the summary bar rather than turning it into a dense dashboard.",
|
|
61
61
|
"The period and filter controls are slotted rather than owned. Use the period slot when the current range is selectable; otherwise periodLabel supplies fixed current copy in the same md control-frame and label-inset anatomy. Use the filter slot for the application-owned comparison-window control. Slotted Selects use hasBorder false so the 48px bar owns their surrounding boundary and allowClear false because both controls always require one selected reporting period.",
|
|
62
62
|
"A numeric safety score infers fair from 0–50, good from 51–80, and excellent from 81–100. The level can be supplied explicitly when the displayed value is not numeric. The deprecated band field is ignored.",
|
|
@@ -401,7 +401,7 @@
|
|
|
401
401
|
},
|
|
402
402
|
{
|
|
403
403
|
"path": "src/wc/components/CardOverview/CardOverview.tsx",
|
|
404
|
-
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n forceUpdate,\n h,\n Host,\n Prop,\n State,\n} from '@stencil/core';\nimport { resolveCssLengthPx, TOKEN_DEFAULTS } from '../../utils';\nimport { ConnectionTasks } from '../../utils/connection-tasks';\nimport type { TextColor, TextVariant } from '../Text/text-types';\nimport type { MetricTrend } from '../../utils/metric-change';\nimport type {\n CardOverviewLayout,\n CardOverviewVariant,\n OverviewMetric,\n OverviewScore,\n} from './card-overview-types';\nimport {\n findNextOverviewMetricIndex,\n resolveCardOverviewCollapseGeometry,\n resolveOverviewGridColumns,\n resolveOverviewRovingIndex,\n} from './card-overview-controller';\n\n/**\n * Metric cells below this width drop a grid column rather than compress further.\n *\n * A literal rather than a token: this is a layout threshold for a grid cell, and\n * the dimension scale has no token for that. The 200px tokens it could borrow\n * (`menu-width-xs`, `form-width-xs`, `panel-width-2xs`) all name other\n * components, so binding to one would couple this grid to an unrelated contract.\n * Consumers override with the `metricMinWidth` prop.\n */\nconst DEFAULT_METRIC_MIN_WIDTH = '200px';\n\n/** Skeleton cells rendered while loading with no metrics supplied yet. */\nconst DEFAULT_LOADING_METRIC_COUNT = 5;\n\n/** Maximum number of comparable measures the summary bar presents at once. */\nconst MAX_METRIC_COUNT = 7;\n\nconst TREND_COLORS: Record<MetricTrend['tone'], TextColor> = {\n positive: 'var(--color-always-dark-foreground-positive)',\n negative: 'var(--color-always-dark-foreground-negative)',\n neutral: 'var(--color-always-dark-foreground-secondary)',\n};\n\nconst ALWAYS_DARK_PRIMARY: TextColor = 'var(--color-always-dark-foreground-primary)';\nconst ALWAYS_DARK_SECONDARY: TextColor = 'var(--color-always-dark-foreground-secondary)';\n\n@Component({\n tag: 'ds-card-overview',\n styleUrl: 'CardOverview.css',\n scoped: true,\n})\nexport class CardOverview {\n @Element() el!: HTMLElement;\n\n /** Full summary card, or the condensed 48px summary bar. */\n @Prop() variant: CardOverviewVariant = 'default';\n\n /** Intrinsic responsive layout, or a page-owned single-column stack. */\n @Prop() layout: CardOverviewLayout = 'auto';\n\n /** Optional nonselectable safety score rendered as the first grid cell. */\n @Prop() score: OverviewScore | undefined;\n\n /** Fixed current date or range. Replaced by content in the `period` slot. */\n @Prop() periodLabel: string = '';\n\n /** Copy between the current period and comparison control, usually `vs.`. */\n @Prop() comparisonLabel: string = '';\n\n /** Measures rendered in the responsive grid. Only the first seven are shown. */\n @Prop() metrics: OverviewMetric[] = [];\n\n /**\n * Width a metric cell may shrink to before the grid drops a column. The grid\n * uses this threshold while choosing an evenly distributed column count.\n */\n @Prop() metricMinWidth: string = DEFAULT_METRIC_MIN_WIDTH;\n\n /** Replace the score and metrics with skeletons while data resolves. */\n @Prop() isLoading: boolean = false;\n\n /** Message shown in place of the score when its figure cannot be resolved. */\n @Prop() scoreErrorMessage: string | undefined;\n\n /** Accessible name for the region. */\n @Prop() overviewLabel: string = 'Overview';\n\n /**\n * Page-controlled visual collapse from the full card (`0`) toward its 48px\n * compact handoff height (`1`). The component preserves its expanded flow\n * height, keeps the period bar stationary, and moves/clips the grid beneath\n * it. The page still owns sticky positioning and the final compact handoff.\n */\n @Prop() scrollCollapseProgress: number = 0;\n\n /** Emitted when a metric that is not inactive is activated. */\n @Event() dsMetricSelect!: EventEmitter<OverviewMetric>;\n\n /** Roving tab stop across selectable metrics. */\n @State() private focusedMetricIndex: number = 0;\n @State() private expandedHeight: number = 0;\n @State() private gridColumns: number | undefined;\n\n private layoutEl?: HTMLElement;\n private layoutResizeObserver: ResizeObserver | null = null;\n private layoutResizeFrame: number | undefined;\n private hasLoaded = false;\n private readonly connectionTasks = new ConnectionTasks(() => this.el.isConnected);\n /** The node the observer is bound to, which a later render can replace. */\n private observedLayoutEl?: HTMLElement;\n\n connectedCallback() {\n if (this.hasLoaded) this.observeLayout();\n }\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.observeLayout();\n }\n\n componentDidRender() {\n if (!this.el.isConnected) return;\n // Re-bind after HMR when DidLoad's observer was dropped, and whenever a\n // render swapped the layout node out from under the observer: the ref points\n // at the new node while the observer still watches the detached one, which\n // never resizes again. The grid would then keep its first measured column\n // count for the life of the component.\n if (!this.layoutResizeObserver || this.observedLayoutEl !== this.layoutEl) {\n this.observeLayout();\n } else {\n this.updateLayoutGeometry();\n }\n }\n\n disconnectedCallback() {\n this.connectionTasks.cancel();\n this.layoutResizeObserver?.disconnect();\n this.layoutResizeObserver = null;\n this.layoutResizeFrame = undefined;\n this.observedLayoutEl = undefined;\n }\n\n private get visibleMetrics(): OverviewMetric[] {\n return this.metrics.slice(0, MAX_METRIC_COUNT);\n }\n\n private get visibleCellCount(): number {\n const metricCount = this.isLoading ? this.loadingMetricCount : this.visibleMetrics.length;\n return 1 + metricCount;\n }\n\n /** Preserve an authored data shape while loading, or use the standard six-cell grid. */\n private get loadingMetricCount(): number {\n return this.visibleMetrics.length || DEFAULT_LOADING_METRIC_COUNT;\n }\n\n private get resolvedScrollCollapseProgress(): number {\n if (this.variant === 'compact' || !Number.isFinite(this.scrollCollapseProgress)) return 0;\n return Math.min(1, Math.max(0, this.scrollCollapseProgress));\n }\n\n private get scrollCollapseGeometry() {\n const compactHeight = resolveCssLengthPx(TOKEN_DEFAULTS.size600, 48);\n return resolveCardOverviewCollapseGeometry({\n variant: this.variant,\n progress: this.resolvedScrollCollapseProgress,\n expandedHeight: this.expandedHeight,\n compactHeight,\n });\n }\n\n private observeLayout() {\n const layout = this.layoutEl;\n if (!this.el.isConnected || !layout) return;\n\n this.connectionTasks.cancelFrame(this.layoutResizeFrame);\n this.layoutResizeFrame = undefined;\n\n const bounds = layout.getBoundingClientRect();\n this.updateLayoutGeometry(bounds.width, bounds.height);\n if (typeof ResizeObserver === 'undefined') return;\n\n this.layoutResizeObserver?.disconnect();\n this.layoutResizeObserver = new ResizeObserver(entries => {\n const rect = entries[0]?.contentRect;\n if (!rect) return;\n this.connectionTasks.cancelFrame(this.layoutResizeFrame);\n this.layoutResizeFrame = this.connectionTasks.frame(() => {\n this.layoutResizeFrame = undefined;\n if (this.layoutEl !== layout) return;\n this.updateLayoutGeometry(rect.width, rect.height);\n });\n });\n this.layoutResizeObserver.observe(layout);\n this.observedLayoutEl = layout;\n }\n\n private updateLayoutGeometry(width?: number, height?: number) {\n const layout = this.layoutEl;\n if (!this.el.isConnected || !layout) return;\n\n const bounds =\n width === undefined || height === undefined ? layout.getBoundingClientRect() : null;\n const resolvedWidth = width ?? bounds?.width ?? 0;\n const resolvedHeight = height ?? bounds?.height ?? 0;\n\n let changed = false;\n\n if (resolvedHeight > 0 && Math.abs(resolvedHeight - this.expandedHeight) >= 0.5) {\n this.expandedHeight = resolvedHeight;\n changed = true;\n }\n\n const columns = resolveOverviewGridColumns({\n cellCount: this.visibleCellCount,\n availableWidth: resolvedWidth,\n minCellWidth: resolveCssLengthPx(this.metricMinWidth, DEFAULT_METRIC_MIN_WIDTH),\n });\n if (columns !== this.gridColumns) {\n this.gridColumns = columns;\n changed = true;\n }\n\n // The column count is written straight to the host rather than through the\n // rendered style object. Measurement happens in `componentDidLoad` and in the\n // ResizeObserver callback, both outside a render cycle, and the host's style\n // is not patched again after the first render — so a rendered value would stay\n // pinned to whatever the first paint produced, before any width was known.\n if (this.gridColumns) {\n this.el.style.setProperty('--ds-card-overview-grid-columns', String(this.gridColumns));\n } else {\n this.el.style.removeProperty('--ds-card-overview-grid-columns');\n }\n\n if (changed) forceUpdate(this);\n }\n\n /** Index of the roving tab stop, skipping inactive metrics. */\n private get rovingIndex(): number {\n return resolveOverviewRovingIndex(this.visibleMetrics, this.focusedMetricIndex);\n }\n\n private selectMetric(metric: OverviewMetric, index: number) {\n if (metric.isInactive) return;\n this.focusedMetricIndex = index;\n this.dsMetricSelect.emit(metric);\n }\n\n /** Move the roving tab stop, wrapping and skipping inactive metrics. */\n private moveFocus(from: number, step: number) {\n const metrics = this.visibleMetrics;\n const next = findNextOverviewMetricIndex(metrics, from, step < 0 ? -1 : 1);\n if (next < 0) return;\n\n this.focusedMetricIndex = next;\n const cells = this.el.querySelectorAll<HTMLElement>('.card-overview__metric-action');\n cells[next]?.focus();\n }\n\n private handleMetricKeyDown = (event: KeyboardEvent, index: number) => {\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowDown':\n event.preventDefault();\n this.moveFocus(index, 1);\n break;\n case 'ArrowLeft':\n case 'ArrowUp':\n event.preventDefault();\n this.moveFocus(index, -1);\n break;\n default:\n break;\n }\n };\n\n /**\n * A skeleton bar matching the type metrics of the text it stands in for, so the\n * loading state holds the same shape and rhythm as the resolved content.\n *\n * `background` is always passed so the base and shimmer follow the card's\n * permanently dark surface in both themes.\n */\n private bar(textVariant: TextVariant, width: string, className?: string) {\n return (\n <ds-skeleton\n class={className}\n variant=\"text\"\n textVariant={textVariant}\n width={width}\n background=\"always-dark\"\n />\n );\n }\n\n /**\n * Arrow and change in one text node.\n *\n * The arrow is a typeface glyph rather than an icon so it inherits the exact\n * size, weight, and baseline of the value beside it; an icon would need its\n * own size scale and would drift whenever the type scale changed.\n *\n * Tone is supplied by the caller and never inferred here.\n */\n private renderTrend(trend: MetricTrend | undefined, variant: TextVariant) {\n if (!trend) return null;\n return (\n <ds-text\n as=\"span\"\n class={{\n 'card-overview__trend': true,\n 'ds-control-label-box': true,\n [`card-overview__trend--${trend.tone}`]: true,\n }}\n variant={variant}\n color={TREND_COLORS[trend.tone]}\n fontFeature=\"tabular-nums\"\n >\n {trend.direction === 'up' ? '↑' : '↓'} {trend.value}\n </ds-text>\n );\n }\n\n private renderScore() {\n if (this.isLoading) {\n return (\n <div class=\"card-overview__score\" part=\"score\">\n <div class=\"card-overview__score-content\">\n <ds-score size=\"lg\" variant=\"dense\" isLoading></ds-score>\n <div class=\"card-overview__score-copy\">\n {this.bar(\n 'text-body-small',\n 'var(--dimension-size-800)',\n 'card-overview__score-label-spacer'\n )}\n {this.bar(\n 'text-body-medium',\n 'calc(var(--dimension-size-300) + var(--dimension-space-050))'\n )}\n </div>\n </div>\n </div>\n );\n }\n\n const unavailableMessage =\n this.scoreErrorMessage || (!this.score ? 'Score unavailable' : undefined);\n if (unavailableMessage) {\n return (\n /* eslint-disable-next-line compomo/prefer-direct-ds-text -- The score track owns the grid divider and 8px outer inset. */\n <div class=\"card-overview__score\" part=\"score\">\n <ds-text\n as=\"span\"\n class=\"card-overview__score-content card-overview__score-content--error\"\n variant=\"text-body-medium\"\n color=\"inherit\"\n role=\"alert\"\n >\n {unavailableMessage}\n </ds-text>\n </div>\n );\n }\n\n const score = this.score;\n if (!score) return null;\n\n return (\n <div class=\"card-overview__score\" part=\"score\">\n <div class=\"card-overview__score-content\">\n <ds-score\n size=\"lg\"\n variant=\"dense\"\n value={score.value}\n level={score.level}\n label=\"Safety score\"\n />\n <div class=\"card-overview__score-copy\">\n <ds-text\n as=\"span\"\n class=\"card-overview__score-label-spacer ds-control-label-box\"\n variant=\"text-body-small\"\n color={ALWAYS_DARK_SECONDARY}\n aria-hidden=\"true\"\n >\n \n </ds-text>\n {this.renderTrend(score.trend, 'text-body-medium')}\n </div>\n </div>\n </div>\n );\n }\n\n private renderMetric(metric: OverviewMetric, index: number) {\n const selectable = !metric.isInactive;\n const label = (\n <ds-text\n as=\"span\"\n class=\"card-overview__metric-label ds-control-label-box\"\n variant=\"text-body-small\"\n color={ALWAYS_DARK_SECONDARY}\n wrap=\"nowrap\"\n >\n {metric.label}\n </ds-text>\n );\n\n return (\n <div class=\"card-overview__metric\" part=\"metric\">\n <div\n class={{\n 'card-overview__metric-action': true,\n 'card-overview__metric-action--inactive': !selectable,\n // Selection targets get the shared wash; press scaling is not used here\n // so the grid keeps its columns aligned. See docs/control-press-policy.md.\n 'ds-interaction-fill': selectable,\n 'ds-interaction-fill--on-always-dark': selectable,\n 'ds-focus-ring-inset': selectable,\n }}\n role={selectable ? 'button' : undefined}\n tabIndex={selectable && index === this.rovingIndex ? 0 : -1}\n onClick={() => this.selectMetric(metric, index)}\n onKeyDown={event => this.handleMetricKeyDown(event, index)}\n onFocusin={() => {\n if (selectable) this.focusedMetricIndex = index;\n }}\n >\n <div class=\"ds-interaction-fill__content card-overview__metric-content\">\n {metric.labelTooltip ? (\n <ds-tooltip label={metric.labelTooltip} side=\"top\" size=\"sm\">\n {label}\n </ds-tooltip>\n ) : (\n label\n )}\n <div class=\"card-overview__metric-figure\">\n <ds-text\n as=\"span\"\n class=\"card-overview__metric-value ds-control-label-box\"\n variant=\"text-body-medium\"\n color={ALWAYS_DARK_PRIMARY}\n wrap=\"nowrap\"\n fontFeature=\"tabular-nums\"\n >\n {metric.value}\n </ds-text>\n {this.renderTrend(metric.trend, 'text-body-medium')}\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n private renderMetrics() {\n if (this.isLoading) {\n return [\n this.renderScore(),\n ...Array.from({ length: this.loadingMetricCount }, (_, index) => (\n <div class=\"card-overview__metric\" key={`loading-${index}`}>\n <div class=\"card-overview__metric-action card-overview__metric-action--inactive\">\n <div class=\"card-overview__metric-content\">\n {this.bar('text-body-small', '35%', 'card-overview__metric-label')}\n <div class=\"card-overview__metric-figure\">\n {this.bar('text-body-medium', 'var(--dimension-size-500)')}\n {/* Match renderTrend's type canvas and the score trend's 28px width. */}\n {this.bar(\n 'text-body-medium',\n 'calc(var(--dimension-size-300) + var(--dimension-space-050))'\n )}\n </div>\n </div>\n </div>\n </div>\n )),\n ];\n }\n\n return [\n this.renderScore(),\n ...this.visibleMetrics.map((metric, index) => this.renderMetric(metric, index)),\n ];\n }\n\n render() {\n const compact = this.variant === 'compact';\n const collapse = this.scrollCollapseGeometry;\n\n // Omit absent custom properties rather than handing them an undefined value.\n // Stencil serialises undefined to the string \"undefined\", which is invalid in\n // `repeat()` and voids the whole grid-template-columns declaration — the grid\n // then collapses to one implicit column instead of using the `auto-fit`\n // fallback baked into the stylesheet.\n const customProperties: Record<string, string> = {\n '--ds-card-overview-metric-min': this.metricMinWidth,\n };\n if (this.gridColumns) {\n customProperties['--ds-card-overview-grid-columns'] = String(this.gridColumns);\n }\n if (collapse.active) {\n customProperties['--ds-card-overview-expanded-height'] = `${collapse.expandedHeight}px`;\n customProperties['--ds-card-overview-visible-height'] = `${collapse.visibleHeight}px`;\n customProperties['--ds-card-overview-content-offset'] = `${collapse.offset}px`;\n }\n\n return (\n <Host\n class={{\n 'card-overview': true,\n 'card-overview--compact': compact,\n 'card-overview--stacked': !compact && this.layout === 'stacked',\n 'card-overview--scroll-collapsing': collapse.active,\n }}\n role=\"region\"\n aria-label={this.overviewLabel}\n aria-busy={this.isLoading ? 'true' : undefined}\n style={customProperties}\n >\n <div\n class={{\n 'card-overview__surface': true,\n // Split shadow and inset highlight, so the opaque surface cannot cover\n // the highlight the way a combined elevation shadow would.\n 'ds-control-elevation': true,\n 'ds-control-elevation--sm': !compact,\n 'ds-control-elevation--floating': compact,\n }}\n >\n <div class=\"card-overview__clip\">\n <div\n ref={element => {\n this.layoutEl = (element as HTMLElement) ?? undefined;\n }}\n class={{\n 'card-overview__layout': true,\n }}\n >\n <div class=\"card-overview__summary\">\n <div class=\"card-overview__header ds-control--md\">\n <div class=\"card-overview__period\">\n <div class=\"card-overview__period-current\">\n <slot name=\"period\">\n {this.periodLabel && (\n /* eslint-disable-next-line compomo/prefer-direct-ds-text -- Fixed copy uses the same structural frame and label inset as a slotted Select. */\n <div class=\"card-overview__period-fixed ds-control-frame\">\n <ds-text\n as=\"span\"\n class=\"ds-control-label-box\"\n variant=\"text-body-medium\"\n emphasis\n color={ALWAYS_DARK_PRIMARY}\n >\n {this.periodLabel}\n </ds-text>\n </div>\n )}\n </slot>\n </div>\n {this.comparisonLabel && (\n <ds-text\n as=\"span\"\n class=\"card-overview__period-comparison ds-control-label-box\"\n variant=\"text-body-medium\"\n color={ALWAYS_DARK_SECONDARY}\n >\n {this.comparisonLabel}\n </ds-text>\n )}\n </div>\n <div class=\"card-overview__filter\">\n <slot name=\"filter\" />\n </div>\n </div>\n </div>\n\n {!compact && (\n <div class=\"card-overview__body\">\n <div class=\"card-overview__metrics\" part=\"metrics\">\n {this.renderMetrics()}\n </div>\n\n <slot name=\"footer\" />\n </div>\n )}\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
404
|
+
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n forceUpdate,\n h,\n Host,\n Prop,\n State,\n} from '@stencil/core';\nimport { resolveCssLengthPx, TOKEN_DEFAULTS } from '../../utils';\nimport { ConnectionTasks } from '../../utils/connection-tasks';\nimport type { TextColor, TextVariant } from '../Text/text-types';\nimport type { MetricTrend } from '../../utils/metric-change';\nimport type {\n CardOverviewLayout,\n CardOverviewVariant,\n OverviewMetric,\n OverviewScore,\n} from './card-overview-types';\nimport {\n findNextOverviewMetricIndex,\n resolveCardOverviewCollapseGeometry,\n resolveOverviewGridColumns,\n resolveOverviewRovingIndex,\n} from './card-overview-controller';\n\n/**\n * Metric cells below this width drop a grid column rather than compress further.\n *\n * A literal rather than a token: this is a layout threshold for a grid cell, and\n * the dimension scale has no token for that. The 200px tokens it could borrow\n * (`menu-width-xs`, `form-width-xs`, `panel-width-2xs`) all name other\n * components, so binding to one would couple this grid to an unrelated contract.\n * Consumers override with the `metricMinWidth` prop.\n */\nconst DEFAULT_METRIC_MIN_WIDTH = '200px';\n\n/** Skeleton cells rendered while loading with no metrics supplied yet. */\nconst DEFAULT_LOADING_METRIC_COUNT = 5;\n\n/** Maximum number of comparable measures the summary bar presents at once. */\nconst MAX_METRIC_COUNT = 7;\n\nconst TREND_COLORS: Record<MetricTrend['tone'], TextColor> = {\n positive: 'var(--color-always-dark-foreground-positive)',\n negative: 'var(--color-always-dark-foreground-negative)',\n neutral: 'var(--color-always-dark-foreground-secondary)',\n};\n\nconst ALWAYS_DARK_PRIMARY: TextColor = 'var(--color-always-dark-foreground-primary)';\nconst ALWAYS_DARK_SECONDARY: TextColor = 'var(--color-always-dark-foreground-secondary)';\n\n@Component({\n tag: 'ds-card-overview',\n styleUrl: 'CardOverview.css',\n scoped: true,\n})\nexport class CardOverview {\n @Element() el!: HTMLElement;\n\n /** Full summary card, or the condensed 48px summary bar. */\n @Prop() variant: CardOverviewVariant = 'default';\n\n /** Intrinsic responsive layout, or a page-owned single-column stack. */\n @Prop() layout: CardOverviewLayout = 'auto';\n\n /** Optional nonselectable safety score rendered as the first grid cell. */\n @Prop() score: OverviewScore | undefined;\n\n /** Fixed current date or range. Replaced by content in the `period` slot. */\n @Prop() periodLabel: string = '';\n\n /** Copy between the current period and comparison control, usually `vs.`. */\n @Prop() comparisonLabel: string = '';\n\n /** Measures rendered in the responsive grid. Only the first seven are shown. */\n @Prop() metrics: OverviewMetric[] = [];\n\n /**\n * Width a metric cell may shrink to before the grid drops a column. The grid\n * uses this threshold while choosing an evenly distributed column count.\n */\n @Prop() metricMinWidth: string = DEFAULT_METRIC_MIN_WIDTH;\n\n /** Replace the score and metrics with skeletons while data resolves. */\n @Prop() isLoading: boolean = false;\n\n /** Message shown in place of the score when its figure cannot be resolved. */\n @Prop() scoreErrorMessage: string | undefined;\n\n /** Accessible name for the region. */\n @Prop() overviewLabel: string = 'Overview';\n\n /**\n * Page-controlled visual collapse from the full card (`0`) toward its 48px\n * compact handoff height (`1`). The component preserves its expanded flow\n * height, keeps the period bar stationary, and moves/clips the grid beneath\n * it. The page still owns sticky positioning and the final compact handoff.\n */\n @Prop() scrollCollapseProgress: number = 0;\n\n /** Emitted when a metric that is not inactive is activated. */\n @Event() dsMetricSelect!: EventEmitter<OverviewMetric>;\n\n /** Roving tab stop across selectable metrics. */\n @State() private focusedMetricIndex: number = 0;\n @State() private expandedHeight: number = 0;\n @State() private gridColumns: number | undefined;\n\n private layoutEl?: HTMLElement;\n private layoutResizeObserver: ResizeObserver | null = null;\n private layoutResizeFrame: number | undefined;\n private hasLoaded = false;\n private readonly connectionTasks = new ConnectionTasks(() => this.el.isConnected);\n /** The node the observer is bound to, which a later render can replace. */\n private observedLayoutEl?: HTMLElement;\n\n connectedCallback() {\n if (this.hasLoaded) this.observeLayout();\n }\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.observeLayout();\n }\n\n componentDidRender() {\n if (!this.el.isConnected) return;\n // Re-bind after HMR when DidLoad's observer was dropped, and whenever a\n // render swapped the layout node out from under the observer: the ref points\n // at the new node while the observer still watches the detached one, which\n // never resizes again. The grid would then keep its first measured column\n // count for the life of the component.\n if (!this.layoutResizeObserver || this.observedLayoutEl !== this.layoutEl) {\n this.observeLayout();\n } else {\n this.updateLayoutGeometry();\n }\n }\n\n disconnectedCallback() {\n this.connectionTasks.cancel();\n this.layoutResizeObserver?.disconnect();\n this.layoutResizeObserver = null;\n this.layoutResizeFrame = undefined;\n this.observedLayoutEl = undefined;\n }\n\n private get visibleMetrics(): OverviewMetric[] {\n return this.metrics.slice(0, MAX_METRIC_COUNT);\n }\n\n private get visibleCellCount(): number {\n const metricCount = this.isLoading ? this.loadingMetricCount : this.visibleMetrics.length;\n return 1 + metricCount;\n }\n\n /** Preserve an authored data shape while loading, or use the standard six-cell grid. */\n private get loadingMetricCount(): number {\n return this.visibleMetrics.length || DEFAULT_LOADING_METRIC_COUNT;\n }\n\n private get resolvedScrollCollapseProgress(): number {\n if (this.variant === 'compact' || !Number.isFinite(this.scrollCollapseProgress)) return 0;\n return Math.min(1, Math.max(0, this.scrollCollapseProgress));\n }\n\n private get scrollCollapseGeometry() {\n const compactHeight = resolveCssLengthPx(TOKEN_DEFAULTS.size600, 48);\n return resolveCardOverviewCollapseGeometry({\n variant: this.variant,\n progress: this.resolvedScrollCollapseProgress,\n expandedHeight: this.expandedHeight,\n compactHeight,\n });\n }\n\n private observeLayout() {\n const layout = this.layoutEl;\n if (!this.el.isConnected || !layout) return;\n\n this.connectionTasks.cancelFrame(this.layoutResizeFrame);\n this.layoutResizeFrame = undefined;\n\n const bounds = layout.getBoundingClientRect();\n this.updateLayoutGeometry(bounds.width, bounds.height);\n if (typeof ResizeObserver === 'undefined') return;\n\n this.layoutResizeObserver?.disconnect();\n this.layoutResizeObserver = new ResizeObserver(entries => {\n const rect = entries[0]?.contentRect;\n if (!rect) return;\n this.connectionTasks.cancelFrame(this.layoutResizeFrame);\n this.layoutResizeFrame = this.connectionTasks.frame(() => {\n this.layoutResizeFrame = undefined;\n if (this.layoutEl !== layout) return;\n this.updateLayoutGeometry(rect.width, rect.height);\n });\n });\n this.layoutResizeObserver.observe(layout);\n this.observedLayoutEl = layout;\n }\n\n private updateLayoutGeometry(width?: number, height?: number) {\n const layout = this.layoutEl;\n if (!this.el.isConnected || !layout) return;\n\n const bounds =\n width === undefined || height === undefined ? layout.getBoundingClientRect() : null;\n const resolvedWidth = width ?? bounds?.width ?? 0;\n const resolvedHeight = height ?? bounds?.height ?? 0;\n\n let changed = false;\n\n if (resolvedHeight > 0 && Math.abs(resolvedHeight - this.expandedHeight) >= 0.5) {\n this.expandedHeight = resolvedHeight;\n changed = true;\n }\n\n const columns = resolveOverviewGridColumns({\n cellCount: this.visibleCellCount,\n availableWidth: resolvedWidth,\n minCellWidth: resolveCssLengthPx(this.metricMinWidth, DEFAULT_METRIC_MIN_WIDTH),\n });\n if (columns !== this.gridColumns) {\n this.gridColumns = columns;\n changed = true;\n }\n\n // The column count is written straight to the host rather than through the\n // rendered style object. Measurement happens in `componentDidLoad` and in the\n // ResizeObserver callback, both outside a render cycle, and the host's style\n // is not patched again after the first render — so a rendered value would stay\n // pinned to whatever the first paint produced, before any width was known.\n if (this.gridColumns) {\n this.el.style.setProperty('--ds-card-overview-grid-columns', String(this.gridColumns));\n } else {\n this.el.style.removeProperty('--ds-card-overview-grid-columns');\n }\n\n if (changed) forceUpdate(this);\n }\n\n /** Index of the roving tab stop, skipping inactive metrics. */\n private get rovingIndex(): number {\n return resolveOverviewRovingIndex(this.visibleMetrics, this.focusedMetricIndex);\n }\n\n private selectMetric(metric: OverviewMetric, index: number) {\n if (metric.isInactive) return;\n this.focusedMetricIndex = index;\n this.dsMetricSelect.emit(metric);\n }\n\n /** Move the roving tab stop, wrapping and skipping inactive metrics. */\n private moveFocus(from: number, step: number) {\n const metrics = this.visibleMetrics;\n const next = findNextOverviewMetricIndex(metrics, from, step < 0 ? -1 : 1);\n if (next < 0) return;\n\n this.focusedMetricIndex = next;\n const cells = this.el.querySelectorAll<HTMLElement>('.card-overview__metric-action');\n cells[next]?.focus();\n }\n\n private handleMetricKeyDown = (event: KeyboardEvent, index: number) => {\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowDown':\n event.preventDefault();\n this.moveFocus(index, 1);\n break;\n case 'ArrowLeft':\n case 'ArrowUp':\n event.preventDefault();\n this.moveFocus(index, -1);\n break;\n default:\n break;\n }\n };\n\n /**\n * A skeleton bar matching the type metrics of the text it stands in for, so the\n * loading state holds the same shape and rhythm as the resolved content.\n *\n * `background` is always passed so the base and shimmer follow the card's\n * permanently dark surface in both themes.\n */\n private bar(textVariant: TextVariant, width: string, className?: string) {\n return (\n <ds-skeleton\n class={className}\n variant=\"text\"\n textVariant={textVariant}\n width={width}\n background=\"always-dark\"\n />\n );\n }\n\n /**\n * Arrow and change in one text node.\n *\n * The arrow is a typeface glyph rather than an icon so it inherits the exact\n * size, weight, and baseline of the value beside it; an icon would need its\n * own size scale and would drift whenever the type scale changed.\n *\n * Tone is supplied by the caller and never inferred here.\n */\n private renderTrend(trend: MetricTrend | undefined, variant: TextVariant) {\n if (!trend) return null;\n return (\n <ds-text\n as=\"span\"\n class={{\n 'card-overview__trend': true,\n 'ds-control-label-box': true,\n [`card-overview__trend--${trend.tone}`]: true,\n }}\n variant={variant}\n color={TREND_COLORS[trend.tone]}\n fontFeature=\"tabular-nums\"\n >\n {trend.direction === 'up' ? '↑' : '↓'} {trend.value}\n </ds-text>\n );\n }\n\n private renderScore() {\n if (this.isLoading) {\n return (\n <div class=\"card-overview__score\" part=\"score\">\n <div class=\"card-overview__score-content\">\n <ds-score size=\"lg\" variant=\"dense\" isLoading></ds-score>\n <div class=\"card-overview__score-copy\">\n {this.bar(\n 'text-body-small',\n 'var(--dimension-size-800)',\n 'card-overview__score-label-spacer'\n )}\n {this.bar(\n 'text-body-medium',\n 'calc(var(--dimension-size-300) + var(--dimension-space-050))'\n )}\n </div>\n </div>\n </div>\n );\n }\n\n const unavailableMessage =\n this.scoreErrorMessage || (!this.score ? 'Score unavailable' : undefined);\n if (unavailableMessage) {\n return (\n /* eslint-disable-next-line compomo/prefer-direct-ds-text -- The score track owns the grid divider and 8px outer inset. */\n <div class=\"card-overview__score\" part=\"score\">\n <ds-text\n as=\"span\"\n class=\"card-overview__score-content card-overview__score-content--error\"\n variant=\"text-body-medium\"\n color=\"inherit\"\n role=\"alert\"\n >\n {unavailableMessage}\n </ds-text>\n </div>\n );\n }\n\n const score = this.score;\n if (!score) return null;\n\n return (\n <div class=\"card-overview__score\" part=\"score\">\n <div class=\"card-overview__score-content\">\n <ds-score\n size=\"lg\"\n variant=\"dense\"\n value={score.value}\n level={score.level}\n label=\"Safety score\"\n />\n <div class=\"card-overview__score-copy\">\n <ds-text\n as=\"span\"\n class=\"card-overview__score-label-spacer ds-control-label-box\"\n variant=\"text-body-small\"\n color={ALWAYS_DARK_SECONDARY}\n aria-hidden=\"true\"\n >\n \n </ds-text>\n {this.renderTrend(score.trend, 'text-body-medium')}\n </div>\n </div>\n </div>\n );\n }\n\n private renderMetric(metric: OverviewMetric, index: number) {\n const selectable = !metric.isInactive;\n const label = (\n <ds-text\n as=\"span\"\n class=\"card-overview__metric-label ds-control-label-box\"\n variant=\"text-body-small\"\n color={ALWAYS_DARK_SECONDARY}\n wrap=\"nowrap\"\n >\n {metric.label}\n </ds-text>\n );\n\n return (\n <div class=\"card-overview__metric\" part=\"metric\">\n <div\n class={{\n 'card-overview__metric-action': true,\n 'card-overview__metric-action--inactive': !selectable,\n // Selection targets get the shared wash; press scaling is not used here\n // so the grid keeps its columns aligned. See docs/control-press-policy.md.\n 'ds-interaction-fill': selectable,\n 'ds-interaction-fill--on-always-dark': selectable,\n 'ds-focus-ring-inset': selectable,\n }}\n role={selectable ? 'button' : undefined}\n tabIndex={selectable && index === this.rovingIndex ? 0 : -1}\n onClick={() => this.selectMetric(metric, index)}\n onKeyDown={event => this.handleMetricKeyDown(event, index)}\n onFocusin={() => {\n if (selectable) this.focusedMetricIndex = index;\n }}\n >\n <div class=\"ds-interaction-fill__content card-overview__metric-content\">\n {metric.labelTooltip ? (\n <ds-tooltip label={metric.labelTooltip} side=\"top\" size=\"sm\">\n {label}\n </ds-tooltip>\n ) : (\n label\n )}\n <div class=\"card-overview__metric-figure\">\n <ds-text\n as=\"span\"\n class=\"card-overview__metric-value ds-control-label-box\"\n variant=\"text-body-medium\"\n color={ALWAYS_DARK_PRIMARY}\n wrap=\"nowrap\"\n fontFeature=\"tabular-nums\"\n >\n {metric.value}\n </ds-text>\n {this.renderTrend(metric.trend, 'text-body-medium')}\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n private renderMetrics() {\n if (this.isLoading) {\n return [\n this.renderScore(),\n ...Array.from({ length: this.loadingMetricCount }, (_, index) => (\n <div class=\"card-overview__metric\" key={`loading-${index}`}>\n <div class=\"card-overview__metric-action card-overview__metric-action--inactive\">\n <div class=\"card-overview__metric-content\">\n {this.bar('text-body-small', '35%', 'card-overview__metric-label')}\n <div class=\"card-overview__metric-figure\">\n {this.bar('text-body-medium', 'var(--dimension-size-500)')}\n {/* Match renderTrend's type canvas and the score trend's 28px width. */}\n {this.bar(\n 'text-body-medium',\n 'calc(var(--dimension-size-300) + var(--dimension-space-050))'\n )}\n </div>\n </div>\n </div>\n </div>\n )),\n ];\n }\n\n return [\n this.renderScore(),\n ...this.visibleMetrics.map((metric, index) => this.renderMetric(metric, index)),\n ];\n }\n\n render() {\n const compact = this.variant === 'compact';\n const collapse = this.scrollCollapseGeometry;\n\n // Omit absent custom properties rather than handing them an undefined value.\n // Stencil serialises undefined to the string \"undefined\", which is invalid in\n // `repeat()` and voids the whole grid-template-columns declaration — the grid\n // then collapses to one implicit column instead of using the `auto-fit`\n // fallback baked into the stylesheet.\n const customProperties: Record<string, string> = {\n '--ds-card-overview-metric-min': this.metricMinWidth,\n };\n if (this.gridColumns) {\n customProperties['--ds-card-overview-grid-columns'] = String(this.gridColumns);\n }\n if (collapse.active) {\n customProperties['--ds-card-overview-expanded-height'] = `${collapse.expandedHeight}px`;\n customProperties['--ds-card-overview-visible-height'] = `${collapse.visibleHeight}px`;\n customProperties['--ds-card-overview-content-offset'] = `${collapse.offset}px`;\n }\n\n return (\n <Host\n class={{\n 'card-overview': true,\n 'card-overview--compact': compact,\n 'card-overview--stacked': !compact && this.layout === 'stacked',\n 'card-overview--scroll-collapsing': collapse.active,\n }}\n role=\"region\"\n aria-label={this.overviewLabel}\n aria-busy={this.isLoading ? 'true' : undefined}\n style={customProperties}\n >\n <div\n class={{\n 'card-overview__surface': true,\n // Split shadow and inset highlight, so the opaque surface cannot cover\n // the highlight the way a combined elevation shadow would.\n 'ds-control-elevation': true,\n 'ds-control-elevation--sm': !compact,\n 'ds-control-elevation--floating': compact,\n }}\n >\n <div class=\"card-overview__clip\">\n <div\n ref={element => {\n this.layoutEl = (element as HTMLElement) ?? undefined;\n }}\n class={{\n 'card-overview__layout': true,\n }}\n >\n <div class=\"card-overview__summary\">\n <div class=\"card-overview__header ds-control--md\">\n <div class=\"card-overview__period\">\n <div class=\"card-overview__period-current\">\n <slot name=\"period\">\n {this.periodLabel && (\n <div class=\"card-overview__period-fixed ds-control-frame\">\n <ds-skeleton\n preserveLayout\n isLoading={this.isLoading}\n background=\"always-dark\"\n >\n <ds-text\n as=\"span\"\n class=\"ds-control-label-box\"\n variant=\"text-body-medium\"\n emphasis\n color={ALWAYS_DARK_PRIMARY}\n >\n {this.periodLabel}\n </ds-text>\n </ds-skeleton>\n </div>\n )}\n </slot>\n </div>\n {this.comparisonLabel && (\n <ds-skeleton\n class=\"card-overview__period-comparison\"\n preserveLayout\n isLoading={this.isLoading}\n background=\"always-dark\"\n >\n <ds-text\n as=\"span\"\n class=\"ds-control-label-box\"\n variant=\"text-body-medium\"\n color={ALWAYS_DARK_SECONDARY}\n >\n {this.comparisonLabel}\n </ds-text>\n </ds-skeleton>\n )}\n </div>\n <div class=\"card-overview__filter\">\n <slot name=\"filter\" />\n </div>\n </div>\n </div>\n\n {!compact && (\n <div class=\"card-overview__body\">\n <div class=\"card-overview__metrics\" part=\"metrics\">\n {this.renderMetrics()}\n </div>\n\n <slot name=\"footer\" />\n </div>\n )}\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
405
405
|
"type": "registry:ui"
|
|
406
406
|
},
|
|
407
407
|
{
|
|
@@ -71,7 +71,8 @@
|
|
|
71
71
|
"The parent owns read-only versus editable body composition, dirty state, validation, persistence, loading, success, and error consequences.",
|
|
72
72
|
"A save request keeps the section editing while validation or persistence is pending; the parent exits only after success.",
|
|
73
73
|
"A cancel request does not persist changes; the parent restores its chosen value snapshot and exits editing.",
|
|
74
|
-
"The immediate variant keeps the resting card surface and omits header actions even if editing is supplied. The application applies and persists each control change independently."
|
|
74
|
+
"The immediate variant keeps the resting card surface and omits header actions even if editing is supplied. The application applies and persists each control change independently.",
|
|
75
|
+
"isLoading replaces header copy and actions with skeletons while retaining card geometry; projected body loading remains owned by the application."
|
|
75
76
|
],
|
|
76
77
|
"responsiveBehavior": [
|
|
77
78
|
"Choose sm, md, or lg from the parent layout. Empty editable cards use the matching minimum height. Cards with body content, and immediate cards, fit that content.",
|
|
@@ -191,6 +192,15 @@
|
|
|
191
192
|
"mutable": false,
|
|
192
193
|
"description": "Section heading shown in the card header."
|
|
193
194
|
},
|
|
195
|
+
"isLoading": {
|
|
196
|
+
"type": "boolean",
|
|
197
|
+
"resolvedType": "boolean",
|
|
198
|
+
"attribute": "is-loading",
|
|
199
|
+
"default": "false",
|
|
200
|
+
"required": false,
|
|
201
|
+
"mutable": false,
|
|
202
|
+
"description": "Keep the card and projected content in place while its header shows skeletons."
|
|
203
|
+
},
|
|
194
204
|
"saveLabel": {
|
|
195
205
|
"type": "string",
|
|
196
206
|
"resolvedType": "string",
|
|
@@ -265,6 +275,15 @@
|
|
|
265
275
|
"mutable": false,
|
|
266
276
|
"description": "Section heading shown in the card header."
|
|
267
277
|
},
|
|
278
|
+
"isLoading": {
|
|
279
|
+
"type": "boolean",
|
|
280
|
+
"resolvedType": "boolean",
|
|
281
|
+
"attribute": "is-loading",
|
|
282
|
+
"default": "false",
|
|
283
|
+
"required": false,
|
|
284
|
+
"mutable": false,
|
|
285
|
+
"description": "Keep the card and projected content in place while its header shows skeletons."
|
|
286
|
+
},
|
|
268
287
|
"saveLabel": {
|
|
269
288
|
"type": "string",
|
|
270
289
|
"resolvedType": "string",
|
|
@@ -336,17 +355,18 @@
|
|
|
336
355
|
"registryDependencies": [
|
|
337
356
|
"button-filled",
|
|
338
357
|
"button-unfilled",
|
|
358
|
+
"skeleton",
|
|
339
359
|
"text"
|
|
340
360
|
],
|
|
341
361
|
"files": [
|
|
342
362
|
{
|
|
343
363
|
"path": "src/wc/components/CardSetting/CardSetting.css",
|
|
344
|
-
"content": "@import '../../utils/radius.css';\n@import '../../utils/chrome-header.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n position: relative;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n width: var(--_card-setting-width);\n min-height: var(--_card-setting-min-height);\n max-width: 100%;\n overflow: hidden;\n border: none;\n border-radius: var(--ds-radius-card);\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-shadow-elevated-sm);\n}\n\n:host::after {\n content: '';\n position: absolute;\n inset: 0;\n z-index: 4;\n border-radius: inherit;\n box-shadow: var(--effect-highlight-elevated-sm);\n pointer-events: none;\n}\n\n:host(.card-setting--editing) {\n background-color: var(--color-background-bold-brand);\n box-shadow: 0 0 0 var(--dimension-stroke-width-025) var(--color-border-bold-brand);\n}\n\n:host(.card-setting--editing)::after {\n box-shadow: none;\n}\n\n.card-setting__header {\n flex: 0 0 auto;\n border-bottom: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n}\n\n:host(.card-setting--editing) .card-setting__header {\n border-bottom-color: transparent;\n}\n\n.card-setting__copy {\n align-items: center;\n}\n\nds-text.card-setting__title {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.card-setting__actions {\n flex-shrink: 0;\n}\n\n.card-setting__panel {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n box-sizing: border-box;\n background-color: var(--color-background-primary);\n border-radius: 0 0 var(--ds-radius-card) var(--ds-radius-card);\n}\n\n:host(.card-setting--editing) .card-setting__panel {\n border-radius: var(--ds-radius-card);\n}\n\n.card-setting__banner {\n flex: 0 0 auto;\n min-width: 0;\n}\n\n.card-setting__banner--empty {\n display: none;\n}\n\n.card-setting__body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n box-sizing: border-box;\n padding-block: var(--dimension-space-100);\n}\n\n@media (forced-colors: active) {\n :host {\n box-shadow: none;\n outline: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n outline-offset: calc(-1 * var(--dimension-stroke-width-012));\n }\n\n :host::after {\n box-shadow: none;\n }\n}\n",
|
|
364
|
+
"content": "@import '../../utils/radius.css';\n@import '../../utils/chrome-header.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n position: relative;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n width: var(--_card-setting-width);\n min-height: var(--_card-setting-min-height);\n max-width: 100%;\n overflow: hidden;\n border: none;\n border-radius: var(--ds-radius-card);\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-shadow-elevated-sm);\n}\n\n:host::after {\n content: '';\n position: absolute;\n inset: 0;\n z-index: 4;\n border-radius: inherit;\n box-shadow: var(--effect-highlight-elevated-sm);\n pointer-events: none;\n}\n\n:host(.card-setting--editing) {\n background-color: var(--color-background-bold-brand);\n box-shadow: 0 0 0 var(--dimension-stroke-width-025) var(--color-border-bold-brand);\n}\n\n:host(.card-setting--editing)::after {\n box-shadow: none;\n}\n\n.card-setting__header {\n flex: 0 0 auto;\n border-bottom: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n}\n\n:host(.card-setting--editing) .card-setting__header {\n border-bottom-color: transparent;\n}\n\n.card-setting__copy {\n align-items: center;\n position: relative;\n}\n\nds-text.card-setting__title {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.card-setting__actions {\n flex-shrink: 0;\n position: relative;\n}\n\n:host(.card-setting--loading) .card-setting__title {\n visibility: hidden;\n}\n\n.card-setting__heading-skeleton {\n position: absolute;\n inset-inline-start: var(--dimension-space-100);\n}\n\n.card-setting__panel {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n box-sizing: border-box;\n background-color: var(--color-background-primary);\n border-radius: 0 0 var(--ds-radius-card) var(--ds-radius-card);\n}\n\n:host(.card-setting--editing) .card-setting__panel {\n border-radius: var(--ds-radius-card);\n}\n\n.card-setting__banner {\n flex: 0 0 auto;\n min-width: 0;\n}\n\n.card-setting__banner--empty {\n display: none;\n}\n\n.card-setting__body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n box-sizing: border-box;\n padding-block: var(--dimension-space-100);\n}\n\n@media (forced-colors: active) {\n :host {\n box-shadow: none;\n outline: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n outline-offset: calc(-1 * var(--dimension-stroke-width-012));\n }\n\n :host::after {\n box-shadow: none;\n }\n}\n",
|
|
345
365
|
"type": "registry:ui"
|
|
346
366
|
},
|
|
347
367
|
{
|
|
348
368
|
"path": "src/wc/components/CardSetting/CardSetting.tsx",
|
|
349
|
-
"content": "import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';\n\nexport type CardSettingVariant = 'editable' | 'immediate';\nexport type CardSettingWidth = 'sm' | 'md' | 'lg';\nexport type CardSettingAction = 'edit' | 'save' | 'cancel';\n\nexport interface CardSettingActionDetail {\n action: CardSettingAction;\n originalEvent: MouseEvent;\n}\n\nconst CARD_WIDTH_VARS: Record<CardSettingWidth, string> = {\n sm: 'var(--dimension-card-width-sm)',\n md: 'var(--dimension-card-width-md)',\n lg: 'var(--dimension-card-width-lg)',\n};\n\nconst CARD_HEIGHT_VARS: Record<CardSettingWidth, string> = {\n sm: 'var(--dimension-card-height-sm)',\n md: 'var(--dimension-card-height-md)',\n lg: 'var(--dimension-card-height-lg)',\n};\n\nconst FAINT_BRAND_TITLE_COLOR = 'var(--color-foreground-faint-brand)';\n\nconst isRenderableNode = (node: Node): boolean => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n return (node as Element).tagName !== 'SLOT';\n }\n return node.nodeType === Node.TEXT_NODE && Boolean(node.textContent?.trim());\n};\n\nconst slotName = (node: Node): string | null =>\n node.nodeType === Node.ELEMENT_NODE ? (node as Element).getAttribute('slot') : null;\n\nconst isDefaultSlottedNode = (node: Node): boolean => isRenderableNode(node) && !slotName(node);\n\nconst isBannerSlottedNode = (node: Node): boolean =>\n isRenderableNode(node) && slotName(node) === 'banner';\n\nconst slotHasRenderableContent = (slot: HTMLSlotElement | null): boolean =>\n Boolean(slot?.assignedNodes({ flatten: true }).some(isRenderableNode));\n\n@Component({\n tag: 'ds-card-setting',\n styleUrl: 'CardSetting.css',\n scoped: true,\n})\nexport class CardSetting {\n @Element() el!: HTMLElement;\n\n /** Section heading shown in the card header. */\n @Prop() heading!: string;\n\n /**\n * Card width token (`sm` / `md` / `lg`). Empty editable cards use the matching\n * minimum-height token. Cards with body content, and immediate cards, fit\n * that content.\n */\n @Prop() cardWidth: CardSettingWidth = 'md';\n\n /** Immediate settings omit edit actions and apply changes through their own controls. */\n @Prop() variant: CardSettingVariant = 'editable';\n\n /** Controlled edit state — parent owns single-edit orchestration. */\n @Prop() editing = false;\n @Prop() editLabel: string = 'Edit';\n @Prop() cancelLabel: string = 'Cancel';\n @Prop() saveLabel: string = 'Save';\n\n /** Emits a controlled edit, save, or cancel request. */\n @Event() dsAction!: EventEmitter<CardSettingActionDetail>;\n\n @State() private hasBannerContent = false;\n @State() private hasBodyContent = false;\n\n componentWillLoad() {\n const nodes = Array.from(this.el.childNodes);\n this.hasBannerContent = nodes.some(isBannerSlottedNode);\n this.hasBodyContent = nodes.some(isDefaultSlottedNode);\n }\n\n componentDidLoad() {\n this.syncBannerContent();\n this.syncBodyContent();\n }\n\n private emitAction(action: CardSettingAction, originalEvent: MouseEvent) {\n this.dsAction.emit({ action, originalEvent });\n }\n\n private readSlotContent(\n event: Event | undefined,\n slotSelector: string,\n containerSelector: string\n ): boolean {\n const slot =\n event?.target instanceof HTMLSlotElement\n ? event.target\n : this.el.querySelector<HTMLSlotElement>(slotSelector);\n if (slotHasRenderableContent(slot)) return true;\n const container = this.el.querySelector(containerSelector);\n return Array.from(container?.childNodes ?? []).some(isRenderableNode);\n }\n\n private syncBannerContent = (event?: Event) => {\n const hasContent = this.readSlotContent(\n event,\n ':scope > .card-setting__panel > .card-setting__banner > slot',\n ':scope > .card-setting__panel > .card-setting__banner'\n );\n if (hasContent !== this.hasBannerContent) this.hasBannerContent = hasContent;\n };\n\n private syncBodyContent = (event?: Event) => {\n const hasContent = this.readSlotContent(\n event,\n ':scope > .card-setting__panel > .card-setting__body > slot',\n ':scope > .card-setting__panel > .card-setting__body'\n );\n if (hasContent !== this.hasBodyContent) this.hasBodyContent = hasContent;\n };\n\n private get hugContent(): boolean {\n return this.variant === 'immediate' || this.hasBannerContent || this.hasBodyContent;\n }\n\n render() {\n const editing = this.variant === 'editable' && this.editing;\n\n return (\n <Host\n class={{\n 'card-setting': true,\n 'card-setting--editing': editing,\n 'card-setting--immediate': this.variant === 'immediate',\n 'card-setting--empty': !this.hugContent,\n }}\n style={{\n '--_card-setting-width': CARD_WIDTH_VARS[this.cardWidth],\n '--_card-setting-min-height': this.hugContent ? '0' : CARD_HEIGHT_VARS[this.cardWidth],\n }}\n >\n <header class=\"card-setting__header ds-chrome-header\">\n
|
|
369
|
+
"content": "import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';\n\nexport type CardSettingVariant = 'editable' | 'immediate';\nexport type CardSettingWidth = 'sm' | 'md' | 'lg';\nexport type CardSettingAction = 'edit' | 'save' | 'cancel';\n\nexport interface CardSettingActionDetail {\n action: CardSettingAction;\n originalEvent: MouseEvent;\n}\n\nconst CARD_WIDTH_VARS: Record<CardSettingWidth, string> = {\n sm: 'var(--dimension-card-width-sm)',\n md: 'var(--dimension-card-width-md)',\n lg: 'var(--dimension-card-width-lg)',\n};\n\nconst CARD_HEIGHT_VARS: Record<CardSettingWidth, string> = {\n sm: 'var(--dimension-card-height-sm)',\n md: 'var(--dimension-card-height-md)',\n lg: 'var(--dimension-card-height-lg)',\n};\n\nconst FAINT_BRAND_TITLE_COLOR = 'var(--color-foreground-faint-brand)';\n\nconst isRenderableNode = (node: Node): boolean => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n return (node as Element).tagName !== 'SLOT';\n }\n return node.nodeType === Node.TEXT_NODE && Boolean(node.textContent?.trim());\n};\n\nconst slotName = (node: Node): string | null =>\n node.nodeType === Node.ELEMENT_NODE ? (node as Element).getAttribute('slot') : null;\n\nconst isDefaultSlottedNode = (node: Node): boolean => isRenderableNode(node) && !slotName(node);\n\nconst isBannerSlottedNode = (node: Node): boolean =>\n isRenderableNode(node) && slotName(node) === 'banner';\n\nconst slotHasRenderableContent = (slot: HTMLSlotElement | null): boolean =>\n Boolean(slot?.assignedNodes({ flatten: true }).some(isRenderableNode));\n\n@Component({\n tag: 'ds-card-setting',\n styleUrl: 'CardSetting.css',\n scoped: true,\n})\nexport class CardSetting {\n @Element() el!: HTMLElement;\n\n /** Section heading shown in the card header. */\n @Prop() heading!: string;\n\n /**\n * Card width token (`sm` / `md` / `lg`). Empty editable cards use the matching\n * minimum-height token. Cards with body content, and immediate cards, fit\n * that content.\n */\n @Prop() cardWidth: CardSettingWidth = 'md';\n\n /** Immediate settings omit edit actions and apply changes through their own controls. */\n @Prop() variant: CardSettingVariant = 'editable';\n\n /** Controlled edit state — parent owns single-edit orchestration. */\n @Prop() editing = false;\n /** Keep the card and projected content in place while its header shows skeletons. */\n @Prop() isLoading = false;\n @Prop() editLabel: string = 'Edit';\n @Prop() cancelLabel: string = 'Cancel';\n @Prop() saveLabel: string = 'Save';\n\n /** Emits a controlled edit, save, or cancel request. */\n @Event() dsAction!: EventEmitter<CardSettingActionDetail>;\n\n @State() private hasBannerContent = false;\n @State() private hasBodyContent = false;\n\n componentWillLoad() {\n const nodes = Array.from(this.el.childNodes);\n this.hasBannerContent = nodes.some(isBannerSlottedNode);\n this.hasBodyContent = nodes.some(isDefaultSlottedNode);\n }\n\n componentDidLoad() {\n this.syncBannerContent();\n this.syncBodyContent();\n }\n\n private emitAction(action: CardSettingAction, originalEvent: MouseEvent) {\n this.dsAction.emit({ action, originalEvent });\n }\n\n private readSlotContent(\n event: Event | undefined,\n slotSelector: string,\n containerSelector: string\n ): boolean {\n const slot =\n event?.target instanceof HTMLSlotElement\n ? event.target\n : this.el.querySelector<HTMLSlotElement>(slotSelector);\n if (slotHasRenderableContent(slot)) return true;\n const container = this.el.querySelector(containerSelector);\n return Array.from(container?.childNodes ?? []).some(isRenderableNode);\n }\n\n private syncBannerContent = (event?: Event) => {\n const hasContent = this.readSlotContent(\n event,\n ':scope > .card-setting__panel > .card-setting__banner > slot',\n ':scope > .card-setting__panel > .card-setting__banner'\n );\n if (hasContent !== this.hasBannerContent) this.hasBannerContent = hasContent;\n };\n\n private syncBodyContent = (event?: Event) => {\n const hasContent = this.readSlotContent(\n event,\n ':scope > .card-setting__panel > .card-setting__body > slot',\n ':scope > .card-setting__panel > .card-setting__body'\n );\n if (hasContent !== this.hasBodyContent) this.hasBodyContent = hasContent;\n };\n\n private get hugContent(): boolean {\n return this.variant === 'immediate' || this.hasBannerContent || this.hasBodyContent;\n }\n\n render() {\n const editing = this.variant === 'editable' && this.editing;\n\n return (\n <Host\n aria-busy={this.isLoading ? 'true' : undefined}\n class={{\n 'card-setting': true,\n 'card-setting--loading': this.isLoading,\n 'card-setting--editing': editing,\n 'card-setting--immediate': this.variant === 'immediate',\n 'card-setting--empty': !this.hugContent,\n }}\n style={{\n '--_card-setting-width': CARD_WIDTH_VARS[this.cardWidth],\n '--_card-setting-min-height': this.hugContent ? '0' : CARD_HEIGHT_VARS[this.cardWidth],\n }}\n >\n <header class=\"card-setting__header ds-chrome-header\">\n <div class=\"card-setting__copy ds-chrome-header__copy ds-control--md\">\n <ds-text\n class=\"card-setting__title ds-chrome-header__heading\"\n variant=\"text-title-small\"\n emphasis\n color={editing ? FAINT_BRAND_TITLE_COLOR : 'primary'}\n as=\"h2\"\n >\n {this.heading}\n </ds-text>\n {this.isLoading && (\n <ds-skeleton\n class=\"card-setting__heading-skeleton\"\n width=\"min(60%, calc(var(--dimension-size-800) * 2))\"\n textVariant=\"text-title-small\"\n />\n )}\n </div>\n {this.variant === 'editable' && (\n <div class=\"card-setting__actions ds-chrome-header__trailing\" inert={this.isLoading}>\n {!editing ? (\n <ds-skeleton\n preserveLayout\n isLoading={this.isLoading}\n variant=\"control\"\n controlAppearance=\"outlined\"\n controlContent=\"icon\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n type=\"button\"\n icon=\"Pencil\"\n aria-label={this.editLabel}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.emitAction('edit', event.detail)\n }\n />\n </ds-skeleton>\n ) : (\n [\n <ds-skeleton\n key=\"cancel\"\n preserveLayout\n isLoading={this.isLoading}\n variant=\"control\"\n controlAppearance=\"outlined\"\n controlContent=\"icon\"\n background=\"bold\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n type=\"button\"\n icon=\"Cross\"\n background=\"bold\"\n aria-label={this.cancelLabel}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.emitAction('cancel', event.detail)\n }\n />\n </ds-skeleton>,\n <ds-skeleton\n key=\"save\"\n preserveLayout\n isLoading={this.isLoading}\n variant=\"control\"\n >\n <ds-button-filled\n variant=\"icon\"\n type=\"button\"\n icon=\"Check\"\n intent=\"brand\"\n contrast=\"faint\"\n aria-label={this.saveLabel}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.emitAction('save', event.detail)\n }\n />\n </ds-skeleton>,\n ]\n )}\n </div>\n )}\n </header>\n <div class=\"card-setting__panel\">\n <div\n class={{\n 'card-setting__banner': true,\n 'card-setting__banner--empty': !this.hasBannerContent,\n }}\n >\n <slot name=\"banner\" onSlotchange={this.syncBannerContent} />\n </div>\n <div class=\"card-setting__body\">\n <slot onSlotchange={this.syncBodyContent} />\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
350
370
|
"type": "registry:ui"
|
|
351
371
|
}
|
|
352
372
|
]
|