@ds-mo/ui 13.7.0 → 13.9.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.
Files changed (208) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +4 -4
  4. package/dist/components/ds-agent-activity.js +1 -1
  5. package/dist/components/ds-agent-response.js +1 -1
  6. package/dist/components/ds-agent-source-list.js +1 -1
  7. package/dist/components/ds-agent-tool-call.js +1 -1
  8. package/dist/components/ds-attachment-list.js +1 -1
  9. package/dist/components/ds-badge.js +1 -1
  10. package/dist/components/ds-banner.js +1 -1
  11. package/dist/components/ds-bar-nav.js +1 -1
  12. package/dist/components/ds-bar-title.js +1 -1
  13. package/dist/components/ds-bar-workflow.js +1 -1
  14. package/dist/components/ds-breadcrumb.js +1 -1
  15. package/dist/components/ds-button-filled.js +1 -1
  16. package/dist/components/ds-button-unfilled.js +1 -1
  17. package/dist/components/ds-card-chart.js +1 -1
  18. package/dist/components/ds-card-chart.js.map +1 -1
  19. package/dist/components/ds-card-overview.js +1 -1
  20. package/dist/components/ds-card-overview.js.map +1 -1
  21. package/dist/components/ds-card-setting.js +1 -1
  22. package/dist/components/ds-card-setting.js.map +1 -1
  23. package/dist/components/ds-chart-legend.js +1 -1
  24. package/dist/components/ds-chart.js +1 -1
  25. package/dist/components/ds-checkbox.js +1 -1
  26. package/dist/components/ds-chip.js +1 -1
  27. package/dist/components/ds-code-block.js +1 -1
  28. package/dist/components/ds-conversation-list-item.js +1 -1
  29. package/dist/components/ds-conversation-list-section.js +1 -1
  30. package/dist/components/ds-conversation-list.js +1 -1
  31. package/dist/components/ds-divider.js +1 -1
  32. package/dist/components/ds-empty-state.js +1 -1
  33. package/dist/components/ds-field.js +1 -1
  34. package/dist/components/ds-input.js +1 -1
  35. package/dist/components/ds-markdown.js +1 -1
  36. package/dist/components/ds-menu.js +1 -1
  37. package/dist/components/ds-message-actions.js +1 -1
  38. package/dist/components/ds-message-bubble.js +1 -1
  39. package/dist/components/ds-message-composer.js +1 -1
  40. package/dist/components/ds-message-composer.js.map +1 -1
  41. package/dist/components/ds-message-scroller.js +1 -1
  42. package/dist/components/ds-message.js +1 -1
  43. package/dist/components/ds-mobile-bar-nav.js +1 -1
  44. package/dist/components/ds-mobile-header.js +1 -1
  45. package/dist/components/ds-mobile-section-switcher.js +1 -1
  46. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  47. package/dist/components/ds-modal.js +1 -1
  48. package/dist/components/ds-panel-nav.js +1 -1
  49. package/dist/components/ds-panel-sub-nav.js +1 -1
  50. package/dist/components/ds-panel-tool-header.js +1 -1
  51. package/dist/components/ds-panel-tool-search.js +1 -1
  52. package/dist/components/ds-panel-tools.js +1 -1
  53. package/dist/components/ds-radio.js +1 -1
  54. package/dist/components/ds-scroll-overlay.js +1 -1
  55. package/dist/components/ds-scroll-overlay.js.map +1 -1
  56. package/dist/components/ds-select.js +1 -1
  57. package/dist/components/ds-shell-app.js +1 -1
  58. package/dist/components/ds-shell-page.js +1 -1
  59. package/dist/components/ds-shell-tools.js +1 -1
  60. package/dist/components/ds-skeleton.js +1 -1
  61. package/dist/components/ds-slider.js +1 -1
  62. package/dist/components/ds-swatch-picker.js +1 -1
  63. package/dist/components/ds-switch.js +1 -1
  64. package/dist/components/ds-tab-group.js +1 -1
  65. package/dist/components/ds-tag.js +1 -1
  66. package/dist/components/ds-text.js +1 -1
  67. package/dist/components/ds-toast.js +1 -1
  68. package/dist/components/ds-tooltip-chart.js +1 -1
  69. package/dist/components/ds-tooltip.js +1 -1
  70. package/dist/components/ds-typing-indicator.js +1 -1
  71. package/dist/components/{p-CywKWfgD.js → p-2inC7Jm-.js} +2 -2
  72. package/dist/components/{p-CywKWfgD.js.map → p-2inC7Jm-.js.map} +1 -1
  73. package/dist/components/{p-DAftUxvL.js → p-B16OuqyJ.js} +2 -2
  74. package/dist/components/{p-CkLffZGn.js → p-B4VlfBNc.js} +2 -2
  75. package/dist/components/{p-DzM3TcyQ.js → p-B6z51yBf.js} +2 -2
  76. package/dist/components/{p-jf1zCsuw.js → p-BNiIyZCn.js} +2 -2
  77. package/dist/components/{p-DQ94PkZA.js → p-BP-EtYbR.js} +2 -2
  78. package/dist/components/{p-u2OZHaLY.js → p-BQ7Nzx9A.js} +2 -2
  79. package/dist/components/{p-DSpq7xQF.js → p-BQDi0v_h.js} +2 -2
  80. package/dist/components/{p-BjiRqLtB.js → p-BYB0f1_F.js} +2 -2
  81. package/dist/components/p-BjIYFiuQ.js +2 -0
  82. package/dist/components/{p-BZCUFHYa.js.map → p-BjIYFiuQ.js.map} +1 -1
  83. package/dist/components/{p-CMZVPEZ_.js → p-BjoyHNaM.js} +2 -2
  84. package/dist/components/{p-D7z9bJ6n.js → p-BkWhTEqe.js} +2 -2
  85. package/dist/components/{p-ByfXC1wu.js → p-BnGJrBPG.js} +2 -2
  86. package/dist/components/{p-UH2AGfR0.js → p-BtS5na9O.js} +2 -2
  87. package/dist/components/p-CCYgBUUS.js +2 -0
  88. package/dist/components/{p-CWLrXsDQ.js.map → p-CCYgBUUS.js.map} +1 -1
  89. package/dist/components/{p-Z2zfShlF.js → p-CnbrksLl.js} +2 -2
  90. package/dist/components/{p-oiR4IoH1.js → p-Cog5NTLa.js} +2 -2
  91. package/dist/components/{p-_mClULFL.js → p-D2dQyoyK.js} +2 -2
  92. package/dist/components/{p-C2U48kDH.js → p-D6g070eY.js} +2 -2
  93. package/dist/components/{p-BVsLc3t5.js → p-D8qgVmHn.js} +2 -2
  94. package/dist/components/{p-D3UKwTEo.js → p-DJ0XMH9E.js} +2 -2
  95. package/dist/components/{p-C-ENnZzq.js → p-DK4PGrHm.js} +2 -2
  96. package/dist/components/{p-CfTThRNX.js → p-DLwM1RNL.js} +2 -2
  97. package/dist/components/{p-CbAfTqlb.js → p-DW9p068S.js} +2 -2
  98. package/dist/components/{p-BsBw-w1q.js → p-Dg9qiMDa.js} +2 -2
  99. package/dist/components/p-Dg9qiMDa.js.map +1 -0
  100. package/dist/components/{p-CkyXU1YX.js → p-DgglLVCj.js} +2 -2
  101. package/dist/components/{p-Bk1A9jLm.js → p-DmblUQ4D.js} +2 -2
  102. package/dist/components/{p-CL8XL1eh.js → p-Dv8_hmUZ.js} +2 -2
  103. package/dist/components/{p-1Zbh1MRy.js → p-DwKC_9u2.js} +2 -2
  104. package/dist/components/{p-CWL61nKt.js → p-HIg1XX62.js} +2 -2
  105. package/dist/components/{p-CX7QIytv.js → p-KFHFBznh.js} +2 -2
  106. package/dist/components/{p-C7mCjS2t.js → p-PQgDzzn9.js} +2 -2
  107. package/dist/components/{p-Mmr9b1bB.js → p-iyiOFsws.js} +2 -2
  108. package/dist/docs/components.json +1 -1
  109. package/dist/mcp-data/registry/agent-activity.json +1 -1
  110. package/dist/mcp-data/registry/agent-response.json +1 -1
  111. package/dist/mcp-data/registry/agent-source-list.json +1 -1
  112. package/dist/mcp-data/registry/agent-tool-call.json +1 -1
  113. package/dist/mcp-data/registry/attachment-list.json +1 -1
  114. package/dist/mcp-data/registry/avatar.json +1 -1
  115. package/dist/mcp-data/registry/badge.json +1 -1
  116. package/dist/mcp-data/registry/banner.json +1 -1
  117. package/dist/mcp-data/registry/bar-nav.json +2 -2
  118. package/dist/mcp-data/registry/bar-title.json +1 -1
  119. package/dist/mcp-data/registry/bar-workflow.json +1 -1
  120. package/dist/mcp-data/registry/breadcrumb.json +1 -1
  121. package/dist/mcp-data/registry/button-filled.json +1 -1
  122. package/dist/mcp-data/registry/button-unfilled.json +1 -1
  123. package/dist/mcp-data/registry/card-chart.json +2 -2
  124. package/dist/mcp-data/registry/card-overview.json +5 -5
  125. package/dist/mcp-data/registry/card-setting.json +3 -3
  126. package/dist/mcp-data/registry/chart-legend.json +1 -1
  127. package/dist/mcp-data/registry/chart.json +1 -1
  128. package/dist/mcp-data/registry/checkbox.json +1 -1
  129. package/dist/mcp-data/registry/chip.json +1 -1
  130. package/dist/mcp-data/registry/code-block.json +1 -1
  131. package/dist/mcp-data/registry/conversation-list-item.json +1 -1
  132. package/dist/mcp-data/registry/conversation-list-section.json +1 -1
  133. package/dist/mcp-data/registry/conversation-list.json +1 -1
  134. package/dist/mcp-data/registry/divider.json +1 -1
  135. package/dist/mcp-data/registry/empty-state.json +1 -1
  136. package/dist/mcp-data/registry/field.json +1 -1
  137. package/dist/mcp-data/registry/icon.json +1 -1
  138. package/dist/mcp-data/registry/input.json +1 -1
  139. package/dist/mcp-data/registry/loader.json +1 -1
  140. package/dist/mcp-data/registry/markdown.json +1 -1
  141. package/dist/mcp-data/registry/menu.json +1 -1
  142. package/dist/mcp-data/registry/message-actions.json +1 -1
  143. package/dist/mcp-data/registry/message-bubble.json +1 -1
  144. package/dist/mcp-data/registry/message-composer.json +1 -1
  145. package/dist/mcp-data/registry/message-scroller.json +1 -1
  146. package/dist/mcp-data/registry/message.json +1 -1
  147. package/dist/mcp-data/registry/mobile-bar-nav.json +1 -1
  148. package/dist/mcp-data/registry/mobile-header.json +1 -1
  149. package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
  150. package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
  151. package/dist/mcp-data/registry/modal.json +1 -1
  152. package/dist/mcp-data/registry/panel-nav.json +1 -1
  153. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  154. package/dist/mcp-data/registry/panel-tool-header.json +1 -1
  155. package/dist/mcp-data/registry/panel-tool-search.json +1 -1
  156. package/dist/mcp-data/registry/panel-tools.json +1 -1
  157. package/dist/mcp-data/registry/radio.json +1 -1
  158. package/dist/mcp-data/registry/registry.json +4 -4
  159. package/dist/mcp-data/registry/scroll-overlay.json +1 -1
  160. package/dist/mcp-data/registry/select.json +1 -1
  161. package/dist/mcp-data/registry/shell-app.json +1 -1
  162. package/dist/mcp-data/registry/shell-page.json +1 -1
  163. package/dist/mcp-data/registry/shell-tools.json +1 -1
  164. package/dist/mcp-data/registry/skeleton.json +3 -3
  165. package/dist/mcp-data/registry/slider.json +1 -1
  166. package/dist/mcp-data/registry/swatch-picker.json +1 -1
  167. package/dist/mcp-data/registry/switch.json +1 -1
  168. package/dist/mcp-data/registry/tab-group.json +1 -1
  169. package/dist/mcp-data/registry/tag.json +1 -1
  170. package/dist/mcp-data/registry/text.json +1 -1
  171. package/dist/mcp-data/registry/toast.json +1 -1
  172. package/dist/mcp-data/registry/tooltip-chart.json +1 -1
  173. package/dist/mcp-data/registry/tooltip.json +1 -1
  174. package/dist/mcp-data/registry/typing-indicator.json +1 -1
  175. package/dist/types/components/CardOverview/CardOverview.d.ts +2 -0
  176. package/package.json +3 -3
  177. package/dist/components/p-BZCUFHYa.js +0 -2
  178. package/dist/components/p-BsBw-w1q.js.map +0 -1
  179. package/dist/components/p-CWLrXsDQ.js +0 -2
  180. /package/dist/components/{p-DAftUxvL.js.map → p-B16OuqyJ.js.map} +0 -0
  181. /package/dist/components/{p-CkLffZGn.js.map → p-B4VlfBNc.js.map} +0 -0
  182. /package/dist/components/{p-DzM3TcyQ.js.map → p-B6z51yBf.js.map} +0 -0
  183. /package/dist/components/{p-jf1zCsuw.js.map → p-BNiIyZCn.js.map} +0 -0
  184. /package/dist/components/{p-DQ94PkZA.js.map → p-BP-EtYbR.js.map} +0 -0
  185. /package/dist/components/{p-u2OZHaLY.js.map → p-BQ7Nzx9A.js.map} +0 -0
  186. /package/dist/components/{p-DSpq7xQF.js.map → p-BQDi0v_h.js.map} +0 -0
  187. /package/dist/components/{p-BjiRqLtB.js.map → p-BYB0f1_F.js.map} +0 -0
  188. /package/dist/components/{p-CMZVPEZ_.js.map → p-BjoyHNaM.js.map} +0 -0
  189. /package/dist/components/{p-D7z9bJ6n.js.map → p-BkWhTEqe.js.map} +0 -0
  190. /package/dist/components/{p-ByfXC1wu.js.map → p-BnGJrBPG.js.map} +0 -0
  191. /package/dist/components/{p-UH2AGfR0.js.map → p-BtS5na9O.js.map} +0 -0
  192. /package/dist/components/{p-Z2zfShlF.js.map → p-CnbrksLl.js.map} +0 -0
  193. /package/dist/components/{p-oiR4IoH1.js.map → p-Cog5NTLa.js.map} +0 -0
  194. /package/dist/components/{p-_mClULFL.js.map → p-D2dQyoyK.js.map} +0 -0
  195. /package/dist/components/{p-C2U48kDH.js.map → p-D6g070eY.js.map} +0 -0
  196. /package/dist/components/{p-BVsLc3t5.js.map → p-D8qgVmHn.js.map} +0 -0
  197. /package/dist/components/{p-D3UKwTEo.js.map → p-DJ0XMH9E.js.map} +0 -0
  198. /package/dist/components/{p-C-ENnZzq.js.map → p-DK4PGrHm.js.map} +0 -0
  199. /package/dist/components/{p-CfTThRNX.js.map → p-DLwM1RNL.js.map} +0 -0
  200. /package/dist/components/{p-CbAfTqlb.js.map → p-DW9p068S.js.map} +0 -0
  201. /package/dist/components/{p-CkyXU1YX.js.map → p-DgglLVCj.js.map} +0 -0
  202. /package/dist/components/{p-Bk1A9jLm.js.map → p-DmblUQ4D.js.map} +0 -0
  203. /package/dist/components/{p-CL8XL1eh.js.map → p-Dv8_hmUZ.js.map} +0 -0
  204. /package/dist/components/{p-1Zbh1MRy.js.map → p-DwKC_9u2.js.map} +0 -0
  205. /package/dist/components/{p-CWL61nKt.js.map → p-HIg1XX62.js.map} +0 -0
  206. /package/dist/components/{p-CX7QIytv.js.map → p-KFHFBznh.js.map} +0 -0
  207. /package/dist/components/{p-C7mCjS2t.js.map → p-PQgDzzn9.js.map} +0 -0
  208. /package/dist/components/{p-Mmr9b1bB.js.map → p-iyiOFsws.js.map} +0 -0
@@ -181,7 +181,7 @@
181
181
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: input, output.",
182
182
  "peerDependencies": {
183
183
  "required": [
184
- "@ds-mo/tokens ^5.1.1",
184
+ "@ds-mo/tokens ^6.0.0",
185
185
  "@ds-mo/icons ^6.4.0"
186
186
  ],
187
187
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -106,7 +106,7 @@
106
106
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: items.",
107
107
  "peerDependencies": {
108
108
  "required": [
109
- "@ds-mo/tokens ^5.1.1",
109
+ "@ds-mo/tokens ^6.0.0",
110
110
  "@ds-mo/icons ^6.4.0"
111
111
  ],
112
112
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -143,7 +143,7 @@
143
143
  },
144
144
  "peerDependencies": {
145
145
  "required": [
146
- "@ds-mo/tokens ^5.1.1",
146
+ "@ds-mo/tokens ^6.0.0",
147
147
  "@ds-mo/icons ^6.4.0"
148
148
  ],
149
149
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -260,7 +260,7 @@
260
260
  },
261
261
  "peerDependencies": {
262
262
  "required": [
263
- "@ds-mo/tokens ^5.1.1",
263
+ "@ds-mo/tokens ^6.0.0",
264
264
  "@ds-mo/icons ^6.4.0"
265
265
  ],
266
266
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -302,7 +302,7 @@
302
302
  },
303
303
  "peerDependencies": {
304
304
  "required": [
305
- "@ds-mo/tokens ^5.1.1",
305
+ "@ds-mo/tokens ^6.0.0",
306
306
  "@ds-mo/icons ^6.4.0"
307
307
  ],
308
308
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -276,7 +276,7 @@
276
276
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: tabs.",
277
277
  "peerDependencies": {
278
278
  "required": [
279
- "@ds-mo/tokens ^5.1.1",
279
+ "@ds-mo/tokens ^6.0.0",
280
280
  "@ds-mo/icons ^6.4.0"
281
281
  ],
282
282
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -301,7 +301,7 @@
301
301
  },
302
302
  {
303
303
  "path": "src/wc/components/BarNav/BarNav.tsx",
304
- "content": "import {\n Component,\n Prop,\n Event,\n EventEmitter,\n Element,\n Watch,\n State,\n h,\n Host,\n} from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport { getSelectableTabs, isTabDivider, type TabItemTab } from '../TabGroup/tab-item-utils';\nimport type { BarNavTab } from './bar-nav-types';\nimport {\n deriveBarNavValueFromUrl,\n shouldResyncBarNavProps,\n} from './bar-nav-utils';\nimport {\n tabsOverflowContainer,\n tabsToOverflowMenuSections,\n trimTrailingDividers,\n visibleTabCountForWidth,\n} from './bar-nav-tabs-menu-utils';\nimport {\n CHROME_TRANSITION_END,\n CHROME_TRANSITION_START,\n ChromeTransitionGate,\n createRafCoalescer,\n readChromeTransitionSource,\n readChromeTransitionPhase,\n} from '../../shell/chrome-transition';\n\n@Component({\n tag: 'ds-bar-nav',\n styleUrl: 'BarNav.css',\n scoped: true,\n})\nexport class BarNav {\n /** Style slot: `dashboard` or `settings`. Colors match for now. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n @Element() el!: HTMLElement;\n\n /**\n * Tab items for the left section.\n * Set via JS property: `el.tabs = [...]`. Replace the array reference to update.\n */\n @Prop() tabs: BarNavTab[] = [];\n\n /** ID of the currently active tab. Overridden when `currentUrl` + `basePath` are set. */\n @Prop({ mutable: true }) value: string = '';\n\n /**\n * Fallback heading shown when no tabs are provided.\n * When tabs are present the heading is hidden.\n */\n @Prop() heading: string | undefined;\n\n /** Section base path (e.g. `/dashboard/safety`). Used with `currentUrl` to derive `value`. */\n @Prop() basePath: string = '';\n\n /** Current route URL. When set with `basePath`, the active tab is derived automatically. */\n @Prop() currentUrl: string = '';\n @Prop() moreTabsLabel: string = 'More tabs';\n\n /** Emitted when the active tab changes. Detail = tab id. */\n @Event() dsTabChange!: EventEmitter<string>;\n\n @State() private resolvedTabs: BarNavTab[] = [];\n @State() private urlDerivedValue: string = '';\n @State() private hideTabsForDetailRoute = false;\n @State() private tabsCollapsed = false;\n @State() private menuOpen = false;\n @State() private menuTriggerActive = false;\n /** When false, defer showing expanded tabs or the collapsed trigger until intrinsic width is measured (avoids a one-frame flash of the full tab row on narrow viewports). */\n @State() private tabLayoutCommitted = false;\n @State() private visibleTabs: BarNavTab[] = [];\n @State() private overflowTabs: BarNavTab[] = [];\n @State() private menuInitialFocusVisible = false;\n @State() private overflowRovingFocused = false;\n @State() private focusedTabId = '';\n\n private static readonly HOST_PROP_SYNC_BUDGET = 8;\n private static readonly INTRINSIC_WIDTH_RETRY_MAX = 3;\n private static readonly TAB_LAYOUT_COMMIT_MAX_FRAMES = 16;\n private static readonly OVERFLOW_HYSTERESIS_PX = 8;\n\n private headerEl: HTMLElement | null = null;\n private triggerEl: (HTMLElement & { setFocus?: () => Promise<void> }) | null = null;\n private menuEl: HTMLDsMenuElement | null = null;\n private visibleTabListEl: HTMLElement | null = null;\n private probeTabListEl: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private intrinsicWidthRetryCount = 0;\n private tabLayoutPendingFrames = 0;\n private readonly panelNavTransition = new ChromeTransitionGate();\n private readonly panelToolsTransition = new ChromeTransitionGate();\n private readonly overflowCoalescer = createRafCoalescer(() => {\n this.updateTabsCollapsed();\n });\n private chromeTransitionShell: HTMLElement | null = null;\n /** Matches `basePath` once tabs + URL are reconciled for the active section. */\n @State() private committedSection = '';\n /** Section waiting for URL + tabs to catch up after a cross-area navigation. */\n @State() private pendingSection = '';\n\n private get effectiveValue(): string {\n if (this.currentUrl && this.basePath) {\n return this.urlDerivedValue;\n }\n return this.value;\n }\n\n private get renderedSelectableTabs(): TabItemTab[] {\n const tabs = this.hasOverflowTabs ? this.visibleTabs : this.resolvedTabs;\n return getSelectableTabs(tabs);\n }\n\n private get hasOverflowTabs(): boolean {\n return this.tabLayoutCommitted && this.tabsCollapsed && this.overflowTabs.length > 0;\n }\n\n private syncFocusedTabId(preferred: string, tabs: TabItemTab[]) {\n if (tabs.some(tab => tab.id === preferred && !tab.isInactive)) {\n this.focusedTabId = preferred;\n return;\n }\n const first = tabs.find(tab => !tab.isInactive);\n this.focusedTabId = first?.id ?? '';\n }\n\n private digestTabsConfig(tabs: BarNavTab[]): string {\n return tabs.map(t => (isTabDivider(t) ? '|' : t.id)).join(',');\n }\n\n private resetTabOverflowLayout() {\n this.tabLayoutCommitted = false;\n this.tabsCollapsed = false;\n this.menuOpen = false;\n this.menuTriggerActive = false;\n this.menuInitialFocusVisible = false;\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.visibleTabs = [];\n this.overflowTabs = [];\n }\n\n @Watch('tabs')\n @Watch('currentUrl')\n @Watch('basePath')\n onHostPropsChange() {\n this.applyHostProps();\n }\n\n @Watch('value')\n @Watch('urlDerivedValue')\n onSelectionChange() {\n this.syncFocusedTabId(this.effectiveValue, this.renderedSelectableTabs);\n this.syncMenuSections();\n this.scheduleOverflowCheck();\n }\n\n @Watch('menuOpen')\n onMenuOpenChange(open: boolean) {\n if (this.menuEl) {\n this.menuEl.open = open;\n }\n }\n\n @Watch('tabsCollapsed')\n onTabsCollapsedChange(collapsed: boolean, prevCollapsed: boolean | undefined) {\n if (!collapsed) {\n this.menuOpen = false;\n this.menuTriggerActive = false;\n this.menuInitialFocusVisible = false;\n this.menuEl = null;\n }\n this.scheduleOverflowCheck();\n if (collapsed) {\n requestAnimationFrame(() => {\n this.scheduleOverflowCheck();\n });\n }\n if (collapsed || prevCollapsed === undefined || prevCollapsed === collapsed) return;\n requestAnimationFrame(() => this.scheduleOverflowCheck());\n queueMicrotask(() => this.focusVisibleSelectedTab());\n }\n\n componentWillLoad() {\n this.applyHostProps();\n this.syncFocusedTabId(this.effectiveValue, getSelectableTabs(this.resolvedTabs));\n }\n\n componentDidLoad() {\n this.syncHostPropsIfNeeded();\n this.scheduleDeferredHostPropSync();\n this.setupOverflowObserver();\n this.bindChromeTransitionListeners();\n this.scheduleOverflowCheck();\n }\n\n componentDidRender() {\n if (this.chromeOverflowPaused()) {\n this.syncMenuAnchor();\n return;\n }\n this.scheduleOverflowCheck();\n this.syncMenuAnchor();\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.overflowCoalescer.cancel();\n this.unbindChromeTransitionListeners();\n }\n\n private bindChromeTransitionListeners() {\n const shell = this.el.closest<HTMLElement>('ds-shell-app');\n if (!shell) return;\n this.chromeTransitionShell = shell;\n shell.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n shell.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n }\n\n private unbindChromeTransitionListeners() {\n if (!this.chromeTransitionShell) return;\n this.chromeTransitionShell.removeEventListener(\n CHROME_TRANSITION_START,\n this.onChromeTransitionStart,\n );\n this.chromeTransitionShell.removeEventListener(\n CHROME_TRANSITION_END,\n this.onChromeTransitionEnd,\n );\n this.chromeTransitionShell = null;\n }\n\n private onChromeTransitionStart = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source === 'panel-nav') {\n this.panelNavTransition.enter();\n return;\n }\n if (source === 'panel-tools') {\n const phase = readChromeTransitionPhase(event) ?? 'opening';\n if (phase === 'closing') {\n this.panelToolsTransition.enter();\n }\n }\n };\n\n private chromeOverflowPaused(): boolean {\n return this.panelNavTransition.isActive || this.panelToolsTransition.isActive;\n }\n\n private onChromeTransitionEnd = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source === 'panel-nav') {\n this.panelNavTransition.exit();\n if (!this.panelNavTransition.isActive) {\n this.scheduleOverflowCheck();\n }\n return;\n }\n if (source === 'panel-tools') {\n this.panelToolsTransition.exit();\n this.scheduleOverflowCheck();\n }\n };\n\n /** Re-resolve props assigned by the host after componentWillLoad (Angular ngAfterViewInit). */\n private syncHostPropsIfNeeded() {\n if (shouldResyncBarNavProps(this.resolvedTabs, this.tabs)) {\n this.applyHostProps();\n } else if (this.currentUrl && this.basePath) {\n this.syncValueFromUrl();\n }\n }\n\n /** Poll across animation frames — host props may land without triggering @Watch. */\n private scheduleDeferredHostPropSync() {\n let remaining = BarNav.HOST_PROP_SYNC_BUDGET;\n const tick = () => {\n this.syncHostPropsIfNeeded();\n if (--remaining > 0) {\n requestAnimationFrame(tick);\n }\n };\n queueMicrotask(tick);\n }\n\n private setupOverflowObserver() {\n if (typeof ResizeObserver === 'undefined' || !this.headerEl) return;\n this.resizeObserver?.disconnect();\n this.resizeObserver = new ResizeObserver(() => {\n if (this.chromeOverflowPaused()) return;\n this.scheduleOverflowCheck();\n });\n this.resizeObserver.observe(this.headerEl);\n }\n\n private scheduleOverflowCheck() {\n if (this.chromeOverflowPaused()) return;\n this.overflowCoalescer.schedule();\n }\n\n /** Width available for the tab row or collapsed trigger. */\n private getLeftZoneAvailableWidth(): number {\n if (!this.headerEl) return 0;\n\n const headerStyle = getComputedStyle(this.headerEl);\n const horizontalPadding =\n (parseFloat(headerStyle.paddingLeft) || 0) + (parseFloat(headerStyle.paddingRight) || 0);\n\n return Math.max(0, this.headerEl.clientWidth - horizontalPadding);\n }\n\n private getTabsIntrinsicWidth(): number {\n return this.probeTabListEl?.scrollWidth ?? 0;\n }\n\n private getHeaderGap(): number {\n if (!this.headerEl) return 0;\n const style = getComputedStyle(this.headerEl);\n return parseFloat(style.columnGap || style.gap) || 0;\n }\n\n private getOverflowTriggerReserveWidth(): number {\n if (!this.triggerEl) {\n return 40;\n }\n return this.triggerEl.getBoundingClientRect().width + this.getHeaderGap();\n }\n\n private getProbeTabMetrics():\n | {\n itemWidths: number[];\n itemGap: number;\n listExtraWidth: number;\n }\n | null {\n const tabList = this.probeTabListEl;\n if (!tabList) return null;\n\n const children = Array.from(tabList.children) as HTMLElement[];\n const itemWidths = children.map(child => child.getBoundingClientRect().width);\n if (itemWidths.length === 0 || itemWidths.some(width => width === 0)) return null;\n\n const style = getComputedStyle(tabList);\n const itemGap = parseFloat(style.columnGap || style.gap) || 0;\n const childrenWidth = itemWidths.reduce((sum, width) => sum + width, 0);\n const gapWidth = itemGap * Math.max(0, itemWidths.length - 1);\n const listExtraWidth = Math.max(0, tabList.scrollWidth - childrenWidth - gapWidth);\n\n return { itemWidths, itemGap, listExtraWidth };\n }\n\n private setOverflowTabLayout(visibleTabs: BarNavTab[], overflowTabs: BarNavTab[]) {\n const nextCollapsed = overflowTabs.length > 0;\n const visibleChanged =\n this.digestTabsConfig(visibleTabs) !== this.digestTabsConfig(this.visibleTabs);\n const overflowChanged =\n this.digestTabsConfig(overflowTabs) !== this.digestTabsConfig(this.overflowTabs);\n\n if (visibleChanged) {\n this.visibleTabs = visibleTabs;\n }\n if (overflowChanged) {\n this.overflowTabs = overflowTabs;\n }\n if (nextCollapsed !== this.tabsCollapsed) {\n this.tabsCollapsed = nextCollapsed;\n if (!nextCollapsed) {\n this.menuOpen = false;\n this.menuTriggerActive = false;\n }\n }\n this.syncFocusedTabId(\n this.effectiveValue,\n getSelectableTabs(nextCollapsed ? visibleTabs : this.resolvedTabs),\n );\n }\n\n private scheduleIntrinsicWidthRetry() {\n if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) return;\n this.intrinsicWidthRetryCount++;\n requestAnimationFrame(() => this.scheduleOverflowCheck());\n }\n\n private forceTabLayoutCommit(fallbackCollapsed: boolean) {\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.setOverflowTabLayout(\n fallbackCollapsed ? [] : this.resolvedTabs,\n fallbackCollapsed ? this.resolvedTabs : [],\n );\n this.tabLayoutCommitted = true;\n }\n\n private updateTabsCollapsed() {\n if (!this.headerEl || this.resolvedTabs.length === 0 || this.hideTabsForDetailRoute) {\n if (this.tabsCollapsed) {\n this.setOverflowTabLayout([], []);\n }\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.tabLayoutCommitted = true;\n return;\n }\n\n const metrics = this.getProbeTabMetrics();\n const intrinsicWidth = this.getTabsIntrinsicWidth();\n if (!metrics || intrinsicWidth === 0) {\n this.tabLayoutPendingFrames += 1;\n if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) {\n this.forceTabLayoutCommit(false);\n } else if (this.tabLayoutPendingFrames >= BarNav.TAB_LAYOUT_COMMIT_MAX_FRAMES) {\n this.forceTabLayoutCommit(false);\n } else {\n this.scheduleIntrinsicWidthRetry();\n }\n return;\n }\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n\n const shouldCollapse = tabsOverflowContainer(\n intrinsicWidth,\n this.getLeftZoneAvailableWidth(),\n this.tabsCollapsed,\n BarNav.OVERFLOW_HYSTERESIS_PX,\n );\n\n if (!shouldCollapse) {\n this.setOverflowTabLayout(this.resolvedTabs, []);\n this.tabLayoutCommitted = true;\n return;\n }\n\n const visibleCount = visibleTabCountForWidth(\n metrics.itemWidths,\n this.getLeftZoneAvailableWidth(),\n this.getOverflowTriggerReserveWidth(),\n metrics.itemGap,\n metrics.listExtraWidth,\n );\n const visibleTabs = trimTrailingDividers(this.resolvedTabs.slice(0, visibleCount));\n const overflowTabs = this.resolvedTabs.slice(visibleTabs.length);\n this.setOverflowTabLayout(visibleTabs, overflowTabs);\n this.tabLayoutCommitted = true;\n }\n\n private syncMenuAnchor() {\n if (!this.menuEl || !this.triggerEl || !this.hasOverflowTabs) return;\n this.menuEl.anchor = this.triggerEl;\n this.syncMenuSections();\n }\n\n private handleMenuSelect(e: Event) {\n const detail = (e as CustomEvent<MenuItemData>).detail;\n if (detail?.value !== undefined) {\n this.selectTab(String(detail.value));\n }\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n }\n\n private handleMenuClose() {\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n }\n\n private handleMenuAfterClose() {\n if (!this.menuOpen) this.menuTriggerActive = false;\n }\n\n /** Batch tabs/basePath/currentUrl updates so URL derivation never runs with a mixed section. */\n private incomingTabs(): BarNavTab[] {\n return this.tabs ?? [];\n }\n\n private applyHostProps() {\n const nextBasePath = this.basePath;\n const sectionChanged =\n nextBasePath !== '' &&\n this.committedSection !== '' &&\n nextBasePath !== this.committedSection;\n\n if (sectionChanged) {\n this.urlDerivedValue = '';\n this.committedSection = '';\n this.pendingSection = nextBasePath;\n this.resolvedTabs = [];\n this.resetTabOverflowLayout();\n return;\n }\n\n if (this.pendingSection) {\n if (!nextBasePath || nextBasePath !== this.pendingSection) {\n return;\n }\n if (!this.currentUrl.startsWith(this.pendingSection)) {\n return;\n }\n const tabs = this.incomingTabs();\n if (tabs.length === 0) {\n return;\n }\n this.pendingSection = '';\n }\n\n const tabs = this.incomingTabs();\n const tabsConfigChanged =\n this.digestTabsConfig(tabs) !== this.digestTabsConfig(this.resolvedTabs);\n\n this.resolvedTabs = tabs;\n this.syncValueFromUrl();\n this.committedSection = nextBasePath;\n if (tabsConfigChanged) {\n this.resetTabOverflowLayout();\n }\n this.scheduleOverflowCheck();\n }\n\n private syncMenuSections() {\n if (!this.menuEl) return;\n this.menuEl.sections = tabsToOverflowMenuSections(this.overflowTabs, this.effectiveValue);\n }\n\n private syncValueFromUrl() {\n const tabs = this.incomingTabs();\n\n if (!this.currentUrl || !this.basePath) {\n this.urlDerivedValue = '';\n this.hideTabsForDetailRoute = false;\n return;\n }\n\n if (!this.currentUrl.startsWith(this.basePath)) {\n this.urlDerivedValue = '';\n this.hideTabsForDetailRoute = false;\n return;\n }\n\n const { value, hideTabs } = deriveBarNavValueFromUrl(\n this.currentUrl,\n this.basePath,\n tabs,\n );\n this.urlDerivedValue = value;\n this.hideTabsForDetailRoute = hideTabs;\n }\n\n private selectTab(id: string) {\n this.value = id;\n this.dsTabChange.emit(id);\n }\n\n /** Move focus to the selected tab in the visible tab group (after expand). */\n private focusVisibleSelectedTab() {\n const tab = this.visibleTabListEl?.querySelector(\n `[data-tab-id=\"${this.effectiveValue}\"]`,\n ) as HTMLElement | null;\n tab?.focus({ preventScroll: true });\n }\n\n private focusVisibleTab(id: string) {\n this.focusedTabId = id;\n const tab = this.visibleTabListEl?.querySelector(\n `[data-tab-id=\"${id}\"]`,\n ) as HTMLElement | null;\n tab?.focus({ preventScroll: true });\n }\n\n private focusLastVisibleTab() {\n const last = [...this.renderedSelectableTabs].reverse().find(tab => !tab.isInactive);\n if (last) this.focusVisibleTab(last.id);\n }\n\n private findEnabledVisibleTab(from: number, step: 1 | -1): number | null {\n const tabs = this.renderedSelectableTabs;\n for (let index = from + step; index >= 0 && index < tabs.length; index += step) {\n if (!tabs[index]?.isInactive) return index;\n }\n return null;\n }\n\n private handleTabKeyDown(e: KeyboardEvent) {\n const tabs = this.renderedSelectableTabs;\n if (!tabs.length) return;\n const targetId = (e.target as HTMLElement | null)?.getAttribute('data-tab-id');\n const focusedId = targetId ?? this.focusedTabId;\n\n if (e.key === 'Enter' || e.key === ' ') {\n const focused = tabs.find(tab => tab.id === focusedId);\n if (!focused || focused.isInactive) return;\n e.preventDefault();\n this.selectTab(focused.id);\n return;\n }\n\n const currentIndex = tabs.findIndex(tab => tab.id === focusedId);\n if (currentIndex < 0) return;\n\n let nextIndex: number | null;\n if (e.key === 'ArrowRight') {\n nextIndex = this.findEnabledVisibleTab(currentIndex, 1);\n if (nextIndex === null && this.hasOverflowTabs) {\n e.preventDefault();\n this.overflowRovingFocused = true;\n void this.triggerEl?.setFocus?.();\n return;\n }\n } else if (e.key === 'ArrowLeft') {\n nextIndex = this.findEnabledVisibleTab(currentIndex, -1);\n } else if (e.key === 'Home') {\n nextIndex = tabs.findIndex(tab => !tab.isInactive);\n } else if (e.key === 'End') {\n const reversedIndex = [...tabs].reverse().findIndex(tab => !tab.isInactive);\n nextIndex = reversedIndex < 0 ? null : tabs.length - 1 - reversedIndex;\n } else {\n return;\n }\n\n if (nextIndex === null || nextIndex < 0 || nextIndex === currentIndex) return;\n e.preventDefault();\n this.focusVisibleTab(tabs[nextIndex].id);\n }\n\n private toggleTabMenu(options?: { focusVisible?: boolean }) {\n if (this.menuOpen) {\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n return;\n }\n\n if (this.menuEl && this.triggerEl) {\n this.menuEl.anchor = this.triggerEl;\n this.menuEl.minWidth = '';\n this.syncMenuSections();\n }\n\n this.menuInitialFocusVisible = options?.focusVisible ?? false;\n this.menuTriggerActive = true;\n this.menuOpen = true;\n }\n\n private handleOverflowFocus = () => {\n this.overflowRovingFocused = true;\n };\n\n private handleOverflowBlur = () => {\n this.overflowRovingFocused = false;\n };\n\n private handleTriggerKeyDown(e: KeyboardEvent) {\n if (!this.hasOverflowTabs) return;\n\n if (!this.menuOpen) {\n if (e.key === 'ArrowLeft') {\n e.preventDefault();\n this.overflowRovingFocused = false;\n this.focusLastVisibleTab();\n return;\n }\n\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.toggleTabMenu({ focusVisible: true });\n }\n return;\n }\n\n if (e.key === 'Escape') {\n e.preventDefault();\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n void this.triggerEl?.setFocus?.();\n }\n }\n\n private renderTabList(tabs: BarNavTab[], probe: boolean) {\n return (\n <div\n role=\"tablist\"\n class=\"bar-nav__tab-list\"\n aria-label={this.heading}\n ref={el => {\n if (probe) {\n const next = (el as HTMLElement) ?? null;\n if (next === this.probeTabListEl) return;\n this.probeTabListEl = next;\n if (next) {\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.scheduleOverflowCheck();\n }\n } else {\n this.visibleTabListEl = (el as HTMLElement) ?? null;\n }\n }}\n onKeyDown={probe ? undefined : (e: KeyboardEvent) => this.handleTabKeyDown(e)}\n onFocusin={probe ? undefined : () => {\n this.overflowRovingFocused = false;\n }}\n >\n {tabs.map((tab, index) => {\n if (isTabDivider(tab)) {\n return (\n <div class=\"bar-nav__tab-divider\" key={`divider-${index}`} aria-hidden=\"true\">\n <div class=\"bar-nav__tab-divider-line\" />\n </div>\n );\n }\n\n const isSelected = tab.id === this.effectiveValue;\n return (\n <button\n key={tab.id}\n type=\"button\"\n role=\"tab\"\n data-tab-id={tab.id}\n class={{\n 'bar-nav__tab': true,\n 'bar-nav__tab--selected': isSelected,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-inactive': !!tab.isInactive,\n }}\n aria-selected={isSelected ? 'true' : 'false'}\n aria-disabled={tab.isInactive ? 'true' : undefined}\n aria-controls={tab.panelId ?? undefined}\n disabled={tab.isInactive}\n tabIndex={\n !probe && !this.overflowRovingFocused && tab.id === this.focusedTabId\n ? 0\n : -1\n }\n onClick={() => !probe && !tab.isInactive && this.selectTab(tab.id)}\n onFocus={() => {\n if (!probe) this.focusedTabId = tab.id;\n }}\n >\n <span\n class={{\n 'bar-nav__tab-label': true,\n 'bar-nav__tab-label--dot': !!tab.dot,\n }}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" emphasis={isSelected} color=\"inherit\">\n {tab.label}\n </ds-text>\n {tab.dot && (\n <ds-badge\n class=\"bar-nav__tab-dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n aria-hidden=\"true\"\n />\n )}\n </span>\n </button>\n );\n })}\n </div>\n );\n }\n\n render() {\n // Section gating only applies when `basePath` is set (SPA section sync).\n // Tabs without a base path must still render — otherwise a single-tab bar\n // (or any non-routed tabs) silently falls through to an empty header.\n const sectionReady =\n this.basePath === '' || this.committedSection === this.basePath;\n const hasTabs =\n sectionReady && this.resolvedTabs.length > 0 && !this.hideTabsForDetailRoute;\n const tabGroupKey = this.basePath || 'no-section';\n const renderedTabs = this.hasOverflowTabs ? this.visibleTabs : this.resolvedTabs;\n\n return (\n <Host>\n <header\n class={{\n 'bar-nav': true,\n 'ds-control--md': true,\n 'ds-chrome-row': true,\n 'ds-chrome-space--md': true,\n 'bar-nav--dashboard': this.navStyle === 'dashboard',\n 'bar-nav--settings': this.navStyle === 'settings',\n 'bar-nav--tabs-collapsed':\n hasTabs && this.hasOverflowTabs,\n }}\n ref={el => {\n this.headerEl = el as HTMLElement;\n if (el && !this.resizeObserver) {\n this.setupOverflowObserver();\n }\n }}\n >\n {hasTabs && (\n <div class=\"bar-nav__tabs-probe\" aria-hidden=\"true\" inert>\n <div key={`probe-${tabGroupKey}`}>\n {this.renderTabList(this.resolvedTabs, true)}\n </div>\n </div>\n )}\n\n {hasTabs && this.tabLayoutCommitted && renderedTabs.length > 0 && (\n <div class=\"bar-nav__left\">\n <div key={`visible-${tabGroupKey}`} class=\"bar-nav__tabs-visible\">\n {this.renderTabList(renderedTabs, false)}\n </div>\n </div>\n )}\n\n {hasTabs && !this.tabLayoutCommitted && (\n <div class=\"bar-nav__tabs-pending\" aria-hidden=\"true\" />\n )}\n\n {!hasTabs && this.heading && (\n <ds-text\n class=\"bar-nav__heading\"\n lineTruncation={1}\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n color=\"primary\"\n wrap=\"nowrap\"\n >\n {this.heading}\n </ds-text>\n )}\n\n {hasTabs && this.hasOverflowTabs && (\n <ds-button-unfilled variant=\"icon\"\n class={{\n 'bar-nav__overflow-trigger': true,\n }}\n icon=\"Ellipses\"\n activeFill={false}\n hasBorder={false}\n focusTabIndex={this.overflowRovingFocused ? 0 : -1}\n ref={(el?: HTMLDsButtonUnfilledElement) => {\n this.triggerEl = (el as (HTMLElement & { setFocus?: () => Promise<void> })) ?? null;\n }}\n haspopup=\"menu\"\n expanded={this.menuTriggerActive}\n aria-label={this.moreTabsLabel}\n onDsClick={() => this.toggleTabMenu({ focusVisible: false })}\n onFocusin={this.handleOverflowFocus}\n onFocusout={this.handleOverflowBlur}\n onKeyDown={(e: KeyboardEvent) => this.handleTriggerKeyDown(e)}\n />\n )}\n\n </header>\n\n {hasTabs && this.hasOverflowTabs && (\n <ds-menu\n ref={el => {\n this.menuEl = (el as HTMLDsMenuElement) ?? null;\n }}\n open={this.menuOpen}\n sections={tabsToOverflowMenuSections(this.overflowTabs, this.effectiveValue)}\n side=\"bottom\"\n align=\"end\"\n sideOffset=\"calc(var(--dimension-space-100) + var(--dimension-space-050))\"\n initialFocusVisible={this.menuInitialFocusVisible}\n onDsSelect={(e: Event) => this.handleMenuSelect(e)}\n onDsClose={() => this.handleMenuClose()}\n onDsAfterClose={() => this.handleMenuAfterClose()}\n />\n )}\n </Host>\n );\n }\n}\n",
304
+ "content": "import {\n Component,\n Prop,\n Event,\n EventEmitter,\n Element,\n Watch,\n State,\n h,\n Host,\n} from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport { getSelectableTabs, isTabDivider, type TabItemTab } from '../TabGroup/tab-item-utils';\nimport type { BarNavTab } from './bar-nav-types';\nimport {\n deriveBarNavValueFromUrl,\n shouldResyncBarNavProps,\n} from './bar-nav-utils';\nimport {\n tabsOverflowContainer,\n tabsToOverflowMenuSections,\n trimTrailingDividers,\n visibleTabCountForWidth,\n} from './bar-nav-tabs-menu-utils';\nimport {\n CHROME_TRANSITION_END,\n CHROME_TRANSITION_START,\n ChromeTransitionGate,\n createRafCoalescer,\n readChromeTransitionSource,\n readChromeTransitionPhase,\n} from '../../shell/chrome-transition';\n\n@Component({\n tag: 'ds-bar-nav',\n styleUrl: 'BarNav.css',\n scoped: true,\n})\nexport class BarNav {\n /** Style slot: `dashboard` or `settings`. Colors match for now. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n @Element() el!: HTMLElement;\n\n /**\n * Tab items for the left section.\n * Set via JS property: `el.tabs = [...]`. Replace the array reference to update.\n */\n @Prop() tabs: BarNavTab[] = [];\n\n /** ID of the currently active tab. Overridden when `currentUrl` + `basePath` are set. */\n @Prop({ mutable: true }) value: string = '';\n\n /**\n * Fallback heading shown when no tabs are provided.\n * When tabs are present the heading is hidden.\n */\n @Prop() heading: string | undefined;\n\n /** Section base path (e.g. `/dashboard/safety`). Used with `currentUrl` to derive `value`. */\n @Prop() basePath: string = '';\n\n /** Current route URL. When set with `basePath`, the active tab is derived automatically. */\n @Prop() currentUrl: string = '';\n @Prop() moreTabsLabel: string = 'More tabs';\n\n /** Emitted when the active tab changes. Detail = tab id. */\n @Event() dsTabChange!: EventEmitter<string>;\n\n @State() private resolvedTabs: BarNavTab[] = [];\n @State() private urlDerivedValue: string = '';\n @State() private hideTabsForDetailRoute = false;\n @State() private tabsCollapsed = false;\n @State() private menuOpen = false;\n @State() private menuTriggerActive = false;\n /** When false, defer showing expanded tabs or the collapsed trigger until intrinsic width is measured (avoids a one-frame flash of the full tab row on narrow viewports). */\n @State() private tabLayoutCommitted = false;\n @State() private visibleTabs: BarNavTab[] = [];\n @State() private overflowTabs: BarNavTab[] = [];\n @State() private menuInitialFocusVisible = false;\n @State() private overflowRovingFocused = false;\n @State() private focusedTabId = '';\n\n private static readonly HOST_PROP_SYNC_BUDGET = 8;\n private static readonly INTRINSIC_WIDTH_RETRY_MAX = 3;\n private static readonly TAB_LAYOUT_COMMIT_MAX_FRAMES = 16;\n private static readonly OVERFLOW_HYSTERESIS_PX = 8;\n\n private headerEl: HTMLElement | null = null;\n private triggerEl: (HTMLElement & { setFocus?: () => Promise<void> }) | null = null;\n private menuEl: HTMLDsMenuElement | null = null;\n private visibleTabListEl: HTMLElement | null = null;\n private probeTabListEl: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private intrinsicWidthRetryCount = 0;\n private tabLayoutPendingFrames = 0;\n private readonly panelNavTransition = new ChromeTransitionGate();\n private readonly panelToolsTransition = new ChromeTransitionGate();\n private readonly overflowCoalescer = createRafCoalescer(() => {\n this.updateTabsCollapsed();\n });\n private chromeTransitionShell: HTMLElement | null = null;\n /** Matches `basePath` once tabs + URL are reconciled for the active section. */\n @State() private committedSection = '';\n /** Section waiting for URL + tabs to catch up after a cross-area navigation. */\n @State() private pendingSection = '';\n\n private get effectiveValue(): string {\n if (this.currentUrl && this.basePath) {\n return this.urlDerivedValue;\n }\n return this.value;\n }\n\n private get renderedSelectableTabs(): TabItemTab[] {\n const tabs = this.hasOverflowTabs ? this.visibleTabs : this.resolvedTabs;\n return getSelectableTabs(tabs);\n }\n\n private get hasOverflowTabs(): boolean {\n return this.tabLayoutCommitted && this.tabsCollapsed && this.overflowTabs.length > 0;\n }\n\n private syncFocusedTabId(preferred: string, tabs: TabItemTab[]) {\n if (tabs.some(tab => tab.id === preferred && !tab.isInactive)) {\n this.focusedTabId = preferred;\n return;\n }\n const first = tabs.find(tab => !tab.isInactive);\n this.focusedTabId = first?.id ?? '';\n }\n\n private digestTabsConfig(tabs: BarNavTab[]): string {\n return tabs.map(t => (isTabDivider(t) ? '|' : t.id)).join(',');\n }\n\n private resetTabOverflowLayout() {\n this.tabLayoutCommitted = false;\n this.tabsCollapsed = false;\n this.menuOpen = false;\n this.menuTriggerActive = false;\n this.menuInitialFocusVisible = false;\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.visibleTabs = [];\n this.overflowTabs = [];\n }\n\n @Watch('tabs')\n @Watch('currentUrl')\n @Watch('basePath')\n onHostPropsChange() {\n this.applyHostProps();\n }\n\n @Watch('value')\n @Watch('urlDerivedValue')\n onSelectionChange() {\n this.syncFocusedTabId(this.effectiveValue, this.renderedSelectableTabs);\n this.syncMenuSections();\n this.scheduleOverflowCheck();\n }\n\n @Watch('menuOpen')\n onMenuOpenChange(open: boolean) {\n if (this.menuEl) {\n this.menuEl.open = open;\n }\n }\n\n @Watch('tabsCollapsed')\n onTabsCollapsedChange(collapsed: boolean, prevCollapsed: boolean | undefined) {\n if (!collapsed) {\n this.menuOpen = false;\n this.menuTriggerActive = false;\n this.menuInitialFocusVisible = false;\n this.menuEl = null;\n }\n this.scheduleOverflowCheck();\n if (collapsed) {\n requestAnimationFrame(() => {\n this.scheduleOverflowCheck();\n });\n }\n if (collapsed || prevCollapsed === undefined || prevCollapsed === collapsed) return;\n requestAnimationFrame(() => this.scheduleOverflowCheck());\n queueMicrotask(() => this.focusVisibleSelectedTab());\n }\n\n componentWillLoad() {\n this.applyHostProps();\n this.syncFocusedTabId(this.effectiveValue, getSelectableTabs(this.resolvedTabs));\n }\n\n componentDidLoad() {\n this.syncHostPropsIfNeeded();\n this.scheduleDeferredHostPropSync();\n this.setupOverflowObserver();\n this.bindChromeTransitionListeners();\n this.scheduleOverflowCheck();\n }\n\n componentDidRender() {\n if (this.chromeOverflowPaused()) {\n this.syncMenuAnchor();\n return;\n }\n this.scheduleOverflowCheck();\n this.syncMenuAnchor();\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.overflowCoalescer.cancel();\n this.unbindChromeTransitionListeners();\n }\n\n private bindChromeTransitionListeners() {\n const shell = this.el.closest<HTMLElement>('ds-shell-app');\n if (!shell) return;\n this.chromeTransitionShell = shell;\n shell.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n shell.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n }\n\n private unbindChromeTransitionListeners() {\n if (!this.chromeTransitionShell) return;\n this.chromeTransitionShell.removeEventListener(\n CHROME_TRANSITION_START,\n this.onChromeTransitionStart,\n );\n this.chromeTransitionShell.removeEventListener(\n CHROME_TRANSITION_END,\n this.onChromeTransitionEnd,\n );\n this.chromeTransitionShell = null;\n }\n\n private onChromeTransitionStart = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source === 'panel-nav') {\n this.panelNavTransition.enter();\n return;\n }\n if (source === 'panel-tools') {\n const phase = readChromeTransitionPhase(event) ?? 'opening';\n if (phase === 'closing') {\n this.panelToolsTransition.enter();\n }\n }\n };\n\n private chromeOverflowPaused(): boolean {\n return this.panelNavTransition.isActive || this.panelToolsTransition.isActive;\n }\n\n private onChromeTransitionEnd = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source === 'panel-nav') {\n this.panelNavTransition.exit();\n if (!this.panelNavTransition.isActive) {\n this.scheduleOverflowCheck();\n }\n return;\n }\n if (source === 'panel-tools') {\n this.panelToolsTransition.exit();\n this.scheduleOverflowCheck();\n }\n };\n\n /** Re-resolve props assigned by the host after componentWillLoad (Angular ngAfterViewInit). */\n private syncHostPropsIfNeeded() {\n if (shouldResyncBarNavProps(this.resolvedTabs, this.tabs)) {\n this.applyHostProps();\n } else if (this.currentUrl && this.basePath) {\n this.syncValueFromUrl();\n }\n }\n\n /** Poll across animation frames — host props may land without triggering @Watch. */\n private scheduleDeferredHostPropSync() {\n let remaining = BarNav.HOST_PROP_SYNC_BUDGET;\n const tick = () => {\n this.syncHostPropsIfNeeded();\n if (--remaining > 0) {\n requestAnimationFrame(tick);\n }\n };\n queueMicrotask(tick);\n }\n\n private setupOverflowObserver() {\n if (typeof ResizeObserver === 'undefined' || !this.headerEl) return;\n this.resizeObserver?.disconnect();\n this.resizeObserver = new ResizeObserver(() => {\n if (this.chromeOverflowPaused()) return;\n this.scheduleOverflowCheck();\n });\n this.resizeObserver.observe(this.headerEl);\n }\n\n private scheduleOverflowCheck() {\n if (this.chromeOverflowPaused()) return;\n this.overflowCoalescer.schedule();\n }\n\n /** Width available for the tab row or collapsed trigger. */\n private getLeftZoneAvailableWidth(): number {\n if (!this.headerEl) return 0;\n\n const headerStyle = getComputedStyle(this.headerEl);\n const horizontalPadding =\n (parseFloat(headerStyle.paddingLeft) || 0) + (parseFloat(headerStyle.paddingRight) || 0);\n\n return Math.max(0, this.headerEl.clientWidth - horizontalPadding);\n }\n\n private getTabsIntrinsicWidth(): number {\n return this.probeTabListEl?.scrollWidth ?? 0;\n }\n\n private getHeaderGap(): number {\n if (!this.headerEl) return 0;\n const style = getComputedStyle(this.headerEl);\n return parseFloat(style.columnGap || style.gap) || 0;\n }\n\n private getOverflowTriggerReserveWidth(): number {\n if (!this.triggerEl) {\n return 40;\n }\n return this.triggerEl.getBoundingClientRect().width + this.getHeaderGap();\n }\n\n private getProbeTabMetrics():\n | {\n itemWidths: number[];\n itemGap: number;\n listExtraWidth: number;\n }\n | null {\n const tabList = this.probeTabListEl;\n if (!tabList) return null;\n\n const children = Array.from(tabList.children) as HTMLElement[];\n const itemWidths = children.map(child => child.getBoundingClientRect().width);\n if (itemWidths.length === 0 || itemWidths.some(width => width === 0)) return null;\n\n const style = getComputedStyle(tabList);\n const itemGap = parseFloat(style.columnGap || style.gap) || 0;\n const childrenWidth = itemWidths.reduce((sum, width) => sum + width, 0);\n const gapWidth = itemGap * Math.max(0, itemWidths.length - 1);\n const listExtraWidth = Math.max(0, tabList.scrollWidth - childrenWidth - gapWidth);\n\n return { itemWidths, itemGap, listExtraWidth };\n }\n\n private setOverflowTabLayout(visibleTabs: BarNavTab[], overflowTabs: BarNavTab[]) {\n const nextCollapsed = overflowTabs.length > 0;\n const visibleChanged =\n this.digestTabsConfig(visibleTabs) !== this.digestTabsConfig(this.visibleTabs);\n const overflowChanged =\n this.digestTabsConfig(overflowTabs) !== this.digestTabsConfig(this.overflowTabs);\n\n if (visibleChanged) {\n this.visibleTabs = visibleTabs;\n }\n if (overflowChanged) {\n this.overflowTabs = overflowTabs;\n }\n if (nextCollapsed !== this.tabsCollapsed) {\n this.tabsCollapsed = nextCollapsed;\n if (!nextCollapsed) {\n this.menuOpen = false;\n this.menuTriggerActive = false;\n }\n }\n this.syncFocusedTabId(\n this.effectiveValue,\n getSelectableTabs(nextCollapsed ? visibleTabs : this.resolvedTabs),\n );\n }\n\n private scheduleIntrinsicWidthRetry() {\n if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) return;\n this.intrinsicWidthRetryCount++;\n requestAnimationFrame(() => this.scheduleOverflowCheck());\n }\n\n private forceTabLayoutCommit(fallbackCollapsed: boolean) {\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.setOverflowTabLayout(\n fallbackCollapsed ? [] : this.resolvedTabs,\n fallbackCollapsed ? this.resolvedTabs : [],\n );\n this.tabLayoutCommitted = true;\n }\n\n private updateTabsCollapsed() {\n if (!this.headerEl || this.resolvedTabs.length === 0 || this.hideTabsForDetailRoute) {\n if (this.tabsCollapsed) {\n this.setOverflowTabLayout([], []);\n }\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.tabLayoutCommitted = true;\n return;\n }\n\n const metrics = this.getProbeTabMetrics();\n const intrinsicWidth = this.getTabsIntrinsicWidth();\n if (!metrics || intrinsicWidth === 0) {\n this.tabLayoutPendingFrames += 1;\n if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) {\n this.forceTabLayoutCommit(false);\n } else if (this.tabLayoutPendingFrames >= BarNav.TAB_LAYOUT_COMMIT_MAX_FRAMES) {\n this.forceTabLayoutCommit(false);\n } else {\n this.scheduleIntrinsicWidthRetry();\n }\n return;\n }\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n\n const shouldCollapse = tabsOverflowContainer(\n intrinsicWidth,\n this.getLeftZoneAvailableWidth(),\n this.tabsCollapsed,\n BarNav.OVERFLOW_HYSTERESIS_PX,\n );\n\n if (!shouldCollapse) {\n this.setOverflowTabLayout(this.resolvedTabs, []);\n this.tabLayoutCommitted = true;\n return;\n }\n\n const visibleCount = visibleTabCountForWidth(\n metrics.itemWidths,\n this.getLeftZoneAvailableWidth(),\n this.getOverflowTriggerReserveWidth(),\n metrics.itemGap,\n metrics.listExtraWidth,\n );\n const visibleTabs = trimTrailingDividers(this.resolvedTabs.slice(0, visibleCount));\n const overflowTabs = this.resolvedTabs.slice(visibleTabs.length);\n this.setOverflowTabLayout(visibleTabs, overflowTabs);\n this.tabLayoutCommitted = true;\n }\n\n private syncMenuAnchor() {\n if (!this.menuEl || !this.triggerEl || !this.hasOverflowTabs) return;\n this.menuEl.anchor = this.triggerEl;\n this.syncMenuSections();\n }\n\n private handleMenuSelect(e: Event) {\n const detail = (e as CustomEvent<MenuItemData>).detail;\n if (detail?.value !== undefined) {\n this.selectTab(String(detail.value));\n }\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n }\n\n private handleMenuClose() {\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n }\n\n private handleMenuAfterClose() {\n if (!this.menuOpen) this.menuTriggerActive = false;\n }\n\n /** Batch tabs/basePath/currentUrl updates so URL derivation never runs with a mixed section. */\n private incomingTabs(): BarNavTab[] {\n return this.tabs ?? [];\n }\n\n private applyHostProps() {\n const nextBasePath = this.basePath;\n const sectionChanged =\n nextBasePath !== '' &&\n this.committedSection !== '' &&\n nextBasePath !== this.committedSection;\n\n if (sectionChanged) {\n this.urlDerivedValue = '';\n this.committedSection = '';\n this.pendingSection = nextBasePath;\n this.resolvedTabs = [];\n this.resetTabOverflowLayout();\n return;\n }\n\n if (this.pendingSection) {\n if (!nextBasePath || nextBasePath !== this.pendingSection) {\n return;\n }\n if (!this.currentUrl.startsWith(this.pendingSection)) {\n return;\n }\n const tabs = this.incomingTabs();\n if (tabs.length === 0) {\n return;\n }\n this.pendingSection = '';\n }\n\n const tabs = this.incomingTabs();\n const tabsConfigChanged =\n this.digestTabsConfig(tabs) !== this.digestTabsConfig(this.resolvedTabs);\n\n this.resolvedTabs = tabs;\n this.syncValueFromUrl();\n this.committedSection = nextBasePath;\n if (tabsConfigChanged) {\n this.resetTabOverflowLayout();\n }\n this.scheduleOverflowCheck();\n }\n\n private syncMenuSections() {\n if (!this.menuEl) return;\n this.menuEl.sections = tabsToOverflowMenuSections(this.overflowTabs, this.effectiveValue);\n }\n\n private syncValueFromUrl() {\n const tabs = this.incomingTabs();\n\n if (!this.currentUrl || !this.basePath) {\n this.urlDerivedValue = '';\n this.hideTabsForDetailRoute = false;\n return;\n }\n\n if (!this.currentUrl.startsWith(this.basePath)) {\n this.urlDerivedValue = '';\n this.hideTabsForDetailRoute = false;\n return;\n }\n\n const { value, hideTabs } = deriveBarNavValueFromUrl(\n this.currentUrl,\n this.basePath,\n tabs,\n );\n this.urlDerivedValue = value;\n this.hideTabsForDetailRoute = hideTabs;\n }\n\n private selectTab(id: string) {\n this.value = id;\n this.dsTabChange.emit(id);\n }\n\n /** Move focus to the selected tab in the visible tab group (after expand). */\n private focusVisibleSelectedTab() {\n const tab = this.visibleTabListEl?.querySelector(\n `[data-tab-id=\"${this.effectiveValue}\"]`,\n ) as HTMLElement | null;\n tab?.focus({ preventScroll: true });\n }\n\n private focusVisibleTab(id: string) {\n this.focusedTabId = id;\n const tab = this.visibleTabListEl?.querySelector(\n `[data-tab-id=\"${id}\"]`,\n ) as HTMLElement | null;\n tab?.focus({ preventScroll: true });\n }\n\n private focusLastVisibleTab() {\n const last = [...this.renderedSelectableTabs].reverse().find(tab => !tab.isInactive);\n if (last) this.focusVisibleTab(last.id);\n }\n\n private findEnabledVisibleTab(from: number, step: 1 | -1): number | null {\n const tabs = this.renderedSelectableTabs;\n for (let index = from + step; index >= 0 && index < tabs.length; index += step) {\n if (!tabs[index]?.isInactive) return index;\n }\n return null;\n }\n\n private handleTabKeyDown(e: KeyboardEvent) {\n const tabs = this.renderedSelectableTabs;\n if (!tabs.length) return;\n const targetId = (e.target as HTMLElement | null)?.getAttribute('data-tab-id');\n const focusedId = targetId ?? this.focusedTabId;\n\n if (e.key === 'Enter' || e.key === ' ') {\n const focused = tabs.find(tab => tab.id === focusedId);\n if (!focused || focused.isInactive) return;\n e.preventDefault();\n this.selectTab(focused.id);\n return;\n }\n\n const currentIndex = tabs.findIndex(tab => tab.id === focusedId);\n if (currentIndex < 0) return;\n\n let nextIndex: number | null;\n if (e.key === 'ArrowRight') {\n nextIndex = this.findEnabledVisibleTab(currentIndex, 1);\n if (nextIndex === null && this.hasOverflowTabs) {\n e.preventDefault();\n this.overflowRovingFocused = true;\n void this.triggerEl?.setFocus?.();\n return;\n }\n } else if (e.key === 'ArrowLeft') {\n nextIndex = this.findEnabledVisibleTab(currentIndex, -1);\n } else if (e.key === 'Home') {\n nextIndex = tabs.findIndex(tab => !tab.isInactive);\n } else if (e.key === 'End') {\n const reversedIndex = [...tabs].reverse().findIndex(tab => !tab.isInactive);\n nextIndex = reversedIndex < 0 ? null : tabs.length - 1 - reversedIndex;\n } else {\n return;\n }\n\n if (nextIndex === null || nextIndex < 0 || nextIndex === currentIndex) return;\n e.preventDefault();\n this.focusVisibleTab(tabs[nextIndex].id);\n }\n\n private toggleTabMenu(options?: { focusVisible?: boolean }) {\n if (this.menuOpen) {\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n return;\n }\n\n if (this.menuEl && this.triggerEl) {\n this.menuEl.anchor = this.triggerEl;\n this.menuEl.minWidth = '';\n this.syncMenuSections();\n }\n\n this.menuInitialFocusVisible = options?.focusVisible ?? false;\n this.menuTriggerActive = true;\n this.menuOpen = true;\n }\n\n private handleOverflowFocus = () => {\n this.overflowRovingFocused = true;\n };\n\n private handleOverflowBlur = () => {\n this.overflowRovingFocused = false;\n };\n\n private handleTriggerKeyDown(e: KeyboardEvent) {\n if (!this.hasOverflowTabs) return;\n\n if (!this.menuOpen) {\n if (e.key === 'ArrowLeft') {\n e.preventDefault();\n this.overflowRovingFocused = false;\n this.focusLastVisibleTab();\n return;\n }\n\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.toggleTabMenu({ focusVisible: true });\n }\n return;\n }\n\n if (e.key === 'Escape') {\n e.preventDefault();\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n void this.triggerEl?.setFocus?.();\n }\n }\n\n private renderTabList(tabs: BarNavTab[], probe: boolean) {\n return (\n <div\n role=\"tablist\"\n class=\"bar-nav__tab-list\"\n aria-label={this.heading}\n ref={el => {\n if (probe) {\n const next = (el as HTMLElement) ?? null;\n if (next === this.probeTabListEl) return;\n this.probeTabListEl = next;\n if (next) {\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.scheduleOverflowCheck();\n }\n } else {\n this.visibleTabListEl = (el as HTMLElement) ?? null;\n }\n }}\n onKeyDown={probe ? undefined : (e: KeyboardEvent) => this.handleTabKeyDown(e)}\n onFocusin={probe ? undefined : () => {\n this.overflowRovingFocused = false;\n }}\n >\n {tabs.map((tab, index) => {\n if (isTabDivider(tab)) {\n return (\n <div class=\"bar-nav__tab-divider\" key={`divider-${index}`} aria-hidden=\"true\">\n <div class=\"bar-nav__tab-divider-line\" />\n </div>\n );\n }\n\n const isSelected = tab.id === this.effectiveValue;\n return (\n <button\n key={tab.id}\n type=\"button\"\n role=\"tab\"\n data-tab-id={tab.id}\n class={{\n 'bar-nav__tab': true,\n 'bar-nav__tab--selected': isSelected,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-inactive': !!tab.isInactive,\n }}\n aria-selected={isSelected ? 'true' : 'false'}\n aria-disabled={tab.isInactive ? 'true' : undefined}\n aria-controls={tab.panelId ?? undefined}\n disabled={tab.isInactive}\n tabIndex={\n !probe && !this.overflowRovingFocused && tab.id === this.focusedTabId\n ? 0\n : -1\n }\n onClick={() => !probe && !tab.isInactive && this.selectTab(tab.id)}\n onFocus={() => {\n if (!probe) this.focusedTabId = tab.id;\n }}\n >\n <span\n class={{\n 'bar-nav__tab-label': true,\n 'bar-nav__tab-label--dot': !!tab.dot,\n }}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" emphasis={isSelected} color=\"inherit\">\n {tab.label}\n </ds-text>\n {tab.dot && (\n <ds-badge\n class=\"bar-nav__tab-dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n aria-hidden=\"true\"\n />\n )}\n </span>\n </button>\n );\n })}\n </div>\n );\n }\n\n render() {\n // Section gating only applies when `basePath` is set (SPA section sync).\n // Tabs without a base path must still render — otherwise a single-tab bar\n // (or any non-routed tabs) silently falls through to an empty header.\n const sectionReady =\n this.basePath === '' || this.committedSection === this.basePath;\n const hasTabs =\n sectionReady && this.resolvedTabs.length > 0 && !this.hideTabsForDetailRoute;\n const tabGroupKey = this.basePath || 'no-section';\n const renderedTabs = this.hasOverflowTabs ? this.visibleTabs : this.resolvedTabs;\n\n return (\n <Host>\n <header\n class={{\n 'bar-nav': true,\n 'ds-control--md': true,\n 'ds-chrome-row': true,\n 'ds-chrome-space--md': true,\n 'bar-nav--dashboard': this.navStyle === 'dashboard',\n 'bar-nav--settings': this.navStyle === 'settings',\n 'bar-nav--tabs-collapsed':\n hasTabs && this.hasOverflowTabs,\n }}\n ref={el => {\n this.headerEl = el as HTMLElement;\n if (el && !this.resizeObserver) {\n this.setupOverflowObserver();\n }\n }}\n >\n {hasTabs && (\n <div class=\"bar-nav__tabs-probe\" aria-hidden=\"true\" inert>\n <div key={`probe-${tabGroupKey}`}>\n {this.renderTabList(this.resolvedTabs, true)}\n </div>\n </div>\n )}\n\n {hasTabs && this.tabLayoutCommitted && renderedTabs.length > 0 && (\n <div class=\"bar-nav__left\">\n <div key={`visible-${tabGroupKey}`} class=\"bar-nav__tabs-visible\">\n {this.renderTabList(renderedTabs, false)}\n </div>\n </div>\n )}\n\n {hasTabs && !this.tabLayoutCommitted && (\n <div class=\"bar-nav__tabs-pending\" aria-hidden=\"true\" />\n )}\n\n {!hasTabs && this.heading && (\n <ds-text\n class=\"bar-nav__heading\"\n lineTruncation={1}\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n color=\"primary\"\n wrap=\"nowrap\"\n >\n {this.heading}\n </ds-text>\n )}\n\n {hasTabs && this.hasOverflowTabs && (\n <ds-button-unfilled variant=\"icon\"\n class={{\n 'bar-nav__overflow-trigger': true,\n }}\n icon=\"ChevronDown\"\n activeFill={false}\n hasBorder={false}\n focusTabIndex={this.overflowRovingFocused ? 0 : -1}\n ref={(el?: HTMLDsButtonUnfilledElement) => {\n this.triggerEl = (el as (HTMLElement & { setFocus?: () => Promise<void> })) ?? null;\n }}\n haspopup=\"menu\"\n expanded={this.menuTriggerActive}\n aria-label={this.moreTabsLabel}\n onDsClick={() => this.toggleTabMenu({ focusVisible: false })}\n onFocusin={this.handleOverflowFocus}\n onFocusout={this.handleOverflowBlur}\n onKeyDown={(e: KeyboardEvent) => this.handleTriggerKeyDown(e)}\n />\n )}\n\n </header>\n\n {hasTabs && this.hasOverflowTabs && (\n <ds-menu\n ref={el => {\n this.menuEl = (el as HTMLDsMenuElement) ?? null;\n }}\n open={this.menuOpen}\n sections={tabsToOverflowMenuSections(this.overflowTabs, this.effectiveValue)}\n side=\"bottom\"\n align=\"end\"\n sideOffset=\"calc(var(--dimension-space-100) + var(--dimension-space-050))\"\n initialFocusVisible={this.menuInitialFocusVisible}\n onDsSelect={(e: Event) => this.handleMenuSelect(e)}\n onDsClose={() => this.handleMenuClose()}\n onDsAfterClose={() => this.handleMenuAfterClose()}\n />\n )}\n </Host>\n );\n }\n}\n",
305
305
  "type": "registry:ui"
306
306
  },
307
307
  {
@@ -449,7 +449,7 @@
449
449
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: actions, breadcrumbs, primaryAction, sections.",
450
450
  "peerDependencies": {
451
451
  "required": [
452
- "@ds-mo/tokens ^5.1.1",
452
+ "@ds-mo/tokens ^6.0.0",
453
453
  "@ds-mo/icons ^6.4.0"
454
454
  ],
455
455
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -351,7 +351,7 @@
351
351
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: steps, submitAction.",
352
352
  "peerDependencies": {
353
353
  "required": [
354
- "@ds-mo/tokens ^5.1.1",
354
+ "@ds-mo/tokens ^6.0.0",
355
355
  "@ds-mo/icons ^6.4.0"
356
356
  ],
357
357
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -171,7 +171,7 @@
171
171
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: items.",
172
172
  "peerDependencies": {
173
173
  "required": [
174
- "@ds-mo/tokens ^5.1.1",
174
+ "@ds-mo/tokens ^6.0.0",
175
175
  "@ds-mo/icons ^6.4.0"
176
176
  ],
177
177
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -491,7 +491,7 @@
491
491
  },
492
492
  "peerDependencies": {
493
493
  "required": [
494
- "@ds-mo/tokens ^5.1.1",
494
+ "@ds-mo/tokens ^6.0.0",
495
495
  "@ds-mo/icons ^6.4.0"
496
496
  ],
497
497
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -552,7 +552,7 @@
552
552
  },
553
553
  "peerDependencies": {
554
554
  "required": [
555
- "@ds-mo/tokens ^5.1.1",
555
+ "@ds-mo/tokens ^6.0.0",
556
556
  "@ds-mo/icons ^6.4.0"
557
557
  ],
558
558
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -208,7 +208,7 @@
208
208
  },
209
209
  "peerDependencies": {
210
210
  "required": [
211
- "@ds-mo/tokens ^5.1.1",
211
+ "@ds-mo/tokens ^6.0.0",
212
212
  "@ds-mo/icons ^6.4.0"
213
213
  ],
214
214
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -226,7 +226,7 @@
226
226
  "files": [
227
227
  {
228
228
  "path": "src/wc/components/CardChart/CardChart.css",
229
- "content": "@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(--dimension-radius-125);\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 background-color: var(--color-background-primary);\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(--dimension-radius-125) var(--dimension-radius-125);\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\n 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)\n 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",
229
+ "content": "@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(--dimension-radius-125);\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(--dimension-radius-125) var(--dimension-radius-125);\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\n 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)\n 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",
230
230
  "type": "registry:ui"
231
231
  },
232
232
  {
@@ -55,11 +55,11 @@
55
55
  "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.",
56
56
  "Omit a trend when there is nothing to report; resolveMetricTrend returns null in that case so no zero change is rendered.",
57
57
  "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.",
58
- "Loading replaces the score and the measures with skeletons and does not alter the slotted period control.",
58
+ "Loading replaces the score and measures with skeletons and does not alter the slotted period control. 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.",
59
59
  "At most seven measures are shown. Keep larger collections outside the summary bar rather than turning it into a dense dashboard.",
60
60
  "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.",
61
61
  "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.",
62
- "The safety score is the first grid cell and uses the same track, 8px outer inset, 6px inner padding, dividers, and responsive behavior as metric cells. The display value removes 12px from both block edges of its line box so it does not increase the shared row height. Score remains nonselectable and excluded from metric roving focus.",
62
+ "The safety score is the first grid cell and uses the same track, 8px outer inset, 6px inner padding, dividers, and responsive behavior as metric cells. The display value removes 8px from both block edges of its line box so it does not increase the shared row height. Score remains nonselectable and excluded from metric roving focus.",
63
63
  "During page-controlled scroll collapse, the component preserves its expanded flow height and stationary reporting bar while the metric grid moves and clips beneath it. Elevation remains fitted to the visible surface.",
64
64
  "compact is exactly 48px high and renders only the reporting bar. It omits Score, measures, and footer."
65
65
  ],
@@ -345,7 +345,7 @@
345
345
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: metrics, score.",
346
346
  "peerDependencies": {
347
347
  "required": [
348
- "@ds-mo/tokens ^5.1.1",
348
+ "@ds-mo/tokens ^6.0.0",
349
349
  "@ds-mo/icons ^6.4.0"
350
350
  ],
351
351
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -364,12 +364,12 @@
364
364
  "files": [
365
365
  {
366
366
  "path": "src/wc/components/CardOverview/CardOverview.css",
367
- "content": "@import '../../utils/interaction-fill.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/control-density.css';\n@import '../../utils/control-parts.css';\n@import '../../styles/control-elevation.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n container-type: inline-size;\n -webkit-user-select: none;\n user-select: none;\n --ds-card-overview-metric-min: calc(var(--dimension-size-400) * 5);\n --ds-card-overview-divider-width: var(--dimension-stroke-width-012);\n --ds-card-overview-divider-color: var(--color-always-dark-border-tertiary);\n color: var(--color-always-dark-foreground-primary);\n}\n\n.card-overview__surface {\n width: 100%;\n min-width: 0;\n box-sizing: border-box;\n border-radius: var(--dimension-radius-125);\n background: var(--color-always-dark-background);\n color: var(--color-always-dark-foreground-primary);\n overflow-anchor: none;\n}\n\n.card-overview__clip {\n height: 100%;\n min-width: 0;\n border-radius: inherit;\n background: inherit;\n overflow: hidden;\n}\n\n:host(.card-overview--scroll-collapsing) {\n height: var(--ds-card-overview-expanded-height);\n pointer-events: none;\n}\n\n:host(.card-overview--scroll-collapsing) .card-overview__surface {\n height: var(--ds-card-overview-visible-height);\n pointer-events: auto;\n will-change: height;\n}\n\n:host(.card-overview--scroll-collapsing) .card-overview__body {\n transform: translateY(calc(-1 * var(--ds-card-overview-content-offset)));\n will-change: transform;\n}\n\n.card-overview__layout {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.card-overview__summary {\n position: relative;\n z-index: 1;\n flex: 0 0 auto;\n min-width: 0;\n background: var(--color-always-dark-background);\n}\n\n.card-overview__header {\n display: flex;\n position: relative;\n align-items: center;\n gap: 0;\n width: 100%;\n height: var(--dimension-size-600);\n min-width: 0;\n padding: var(--dimension-space-100);\n box-sizing: border-box;\n}\n\n.card-overview__header::after {\n content: '';\n position: absolute;\n inset-inline: 0;\n inset-block-end: 0;\n height: var(--ds-card-overview-divider-width);\n background: var(--ds-card-overview-divider-color);\n pointer-events: none;\n}\n\n.card-overview__period {\n display: flex;\n flex: 0 1 auto;\n align-items: center;\n gap: 0;\n min-width: 0;\n min-height: var(--ds-control-height);\n overflow: hidden;\n white-space: nowrap;\n}\n\n.card-overview__period-current,\n.card-overview__period-comparison {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.card-overview__period-current {\n display: flex;\n flex: 0 1 auto;\n align-items: center;\n}\n\n.card-overview__period-fixed {\n flex: 0 1 auto;\n min-width: 0;\n}\n\n.card-overview__period-comparison {\n flex: 0 0 auto;\n}\n\n.card-overview__filter {\n display: flex;\n flex: 0 1 auto;\n align-items: center;\n min-width: 0;\n}\n\n.card-overview__body {\n display: flex;\n position: relative;\n z-index: 0;\n flex-direction: column;\n min-width: 0;\n transform: translateY(0);\n}\n\n.card-overview__metrics {\n display: grid;\n flex: 1 1 auto;\n grid-template-columns: repeat(\n var(--ds-card-overview-grid-columns, auto-fit),\n minmax(min(100%, var(--ds-card-overview-metric-min)), 1fr)\n );\n align-content: stretch;\n min-width: 0;\n overflow: clip;\n}\n\n.card-overview__score,\n.card-overview__metric {\n display: flex;\n min-width: 0;\n padding: var(--dimension-space-100);\n box-shadow:\n var(--ds-card-overview-divider-width) 0 0 0 var(--ds-card-overview-divider-color),\n 0 var(--ds-card-overview-divider-width) 0 0 var(--ds-card-overview-divider-color);\n box-sizing: border-box;\n}\n\n.card-overview__score-content,\n.card-overview__metric-action {\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--dimension-space-075);\n border-radius: var(--dimension-radius-025);\n box-sizing: border-box;\n}\n\n.card-overview__score-content {\n align-items: center;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n background: var(--color-always-dark-interaction-active);\n color: var(--color-always-dark-foreground-primary);\n}\n\n.card-overview__score-content--fair {\n background: var(--color-safety-score-background-fair);\n color: var(--color-safety-score-foreground-on-fair);\n}\n\n.card-overview__score-content--good {\n background: var(--color-safety-score-background-good);\n color: var(--color-safety-score-foreground-on-good);\n}\n\n.card-overview__score-content--excellent {\n background: var(--color-safety-score-background-excellent);\n color: var(--color-safety-score-foreground-on-excellent);\n}\n\n.card-overview__score-copy {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--dimension-space-025);\n min-width: 0;\n}\n\n.card-overview__score-value,\n.card-overview__score-figure {\n flex: 0 0 auto;\n margin-block: calc(-1 * var(--dimension-space-150));\n}\n\n.card-overview__score-content--error {\n align-items: center;\n justify-content: center;\n border: var(--ds-card-overview-divider-width) solid\n var(--color-always-dark-border-tertiary);\n background: transparent;\n color: var(--color-always-dark-foreground-negative);\n text-align: center;\n}\n\n.card-overview__metric-action {\n flex-direction: column;\n justify-content: center;\n gap: var(--dimension-space-050);\n}\n\n.card-overview__metric-action[role='button'] {\n cursor: default;\n}\n\n.card-overview__metric-content {\n display: flex;\n flex-direction: column;\n justify-content: center;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n.card-overview__metric-label {\n min-width: 0;\n}\n\n.card-overview__metric-figure {\n display: flex;\n align-items: baseline;\n gap: var(--dimension-space-100);\n min-width: 0;\n}\n\n.card-overview__trend {\n flex: 0 0 auto;\n white-space: nowrap;\n}\n\n:host(.card-overview--stacked) .card-overview__metrics {\n grid-template-columns: minmax(0, 1fr);\n}\n\n:host(.card-overview--compact) {\n height: var(--dimension-size-600);\n min-height: var(--dimension-size-600);\n}\n\n:host(.card-overview--compact) .card-overview__surface,\n:host(.card-overview--compact) .card-overview__clip,\n:host(.card-overview--compact) .card-overview__layout,\n:host(.card-overview--compact) .card-overview__summary {\n width: 100%;\n height: 100%;\n}\n\n:host(.card-overview--compact) .card-overview__header::after {\n display: none;\n}\n\n@media (forced-colors: active) {\n .card-overview__header::after {\n background: var(--ds-forced-color-content);\n }\n\n .card-overview__score-content {\n border: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n background: var(--ds-forced-color-surface);\n color: var(--ds-forced-color-content);\n }\n}\n",
367
+ "content": "@import '../../utils/interaction-fill.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/control-density.css';\n@import '../../utils/control-parts.css';\n@import '../../styles/control-elevation.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n container-type: inline-size;\n -webkit-user-select: none;\n user-select: none;\n --ds-card-overview-metric-min: calc(var(--dimension-size-400) * 5);\n --ds-card-overview-divider-width: var(--dimension-stroke-width-012);\n --ds-card-overview-divider-color: var(--color-always-dark-border-tertiary);\n color: var(--color-always-dark-foreground-primary);\n}\n\n.card-overview__surface {\n width: 100%;\n min-width: 0;\n box-sizing: border-box;\n border-radius: var(--dimension-radius-125);\n background: var(--color-always-dark-background);\n color: var(--color-always-dark-foreground-primary);\n overflow-anchor: none;\n}\n\n.card-overview__clip {\n height: 100%;\n min-width: 0;\n border-radius: inherit;\n background: inherit;\n overflow: hidden;\n}\n\n:host(.card-overview--scroll-collapsing) {\n height: var(--ds-card-overview-expanded-height);\n pointer-events: none;\n}\n\n:host(.card-overview--scroll-collapsing) .card-overview__surface {\n height: var(--ds-card-overview-visible-height);\n pointer-events: auto;\n will-change: height;\n}\n\n:host(.card-overview--scroll-collapsing) .card-overview__body {\n transform: translateY(calc(-1 * var(--ds-card-overview-content-offset)));\n will-change: transform;\n}\n\n.card-overview__layout {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.card-overview__summary {\n position: relative;\n z-index: 1;\n flex: 0 0 auto;\n min-width: 0;\n background: var(--color-always-dark-background);\n}\n\n.card-overview__header {\n display: flex;\n position: relative;\n align-items: center;\n gap: 0;\n width: 100%;\n height: var(--dimension-size-600);\n min-width: 0;\n padding: var(--dimension-space-100);\n box-sizing: border-box;\n}\n\n.card-overview__header::after {\n content: '';\n position: absolute;\n inset-inline: 0;\n inset-block-end: 0;\n height: var(--ds-card-overview-divider-width);\n background: var(--ds-card-overview-divider-color);\n pointer-events: none;\n}\n\n.card-overview__period {\n display: flex;\n flex: 0 1 auto;\n align-items: center;\n gap: 0;\n min-width: 0;\n min-height: var(--ds-control-height);\n overflow: hidden;\n white-space: nowrap;\n}\n\n.card-overview__period-current,\n.card-overview__period-comparison {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.card-overview__period-current {\n display: flex;\n flex: 0 1 auto;\n align-items: center;\n}\n\n.card-overview__period-fixed {\n flex: 0 1 auto;\n min-width: 0;\n}\n\n.card-overview__period-comparison {\n flex: 0 0 auto;\n}\n\n.card-overview__filter {\n display: flex;\n flex: 0 1 auto;\n align-items: center;\n min-width: 0;\n}\n\n.card-overview__body {\n display: flex;\n position: relative;\n z-index: 0;\n flex-direction: column;\n min-width: 0;\n transform: translateY(0);\n}\n\n.card-overview__metrics {\n display: grid;\n flex: 1 1 auto;\n grid-template-columns: repeat(\n var(--ds-card-overview-grid-columns, auto-fit),\n minmax(min(100%, var(--ds-card-overview-metric-min)), 1fr)\n );\n align-content: stretch;\n min-width: 0;\n overflow: clip;\n}\n\n.card-overview__score,\n.card-overview__metric {\n display: flex;\n min-width: 0;\n padding: var(--dimension-space-100);\n box-shadow:\n var(--ds-card-overview-divider-width) 0 0 0 var(--ds-card-overview-divider-color),\n 0 var(--ds-card-overview-divider-width) 0 0 var(--ds-card-overview-divider-color);\n box-sizing: border-box;\n}\n\n.card-overview__score-content,\n.card-overview__metric-action {\n display: flex;\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--dimension-space-075);\n border-radius: var(--dimension-radius-025);\n box-sizing: border-box;\n}\n\n.card-overview__score-content {\n align-items: center;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n background: var(--color-always-dark-interaction-active);\n color: var(--color-always-dark-foreground-primary);\n}\n\n.card-overview__score-content--fair {\n background: var(--color-safety-score-background-fair);\n color: var(--color-safety-score-foreground-on-fair);\n}\n\n.card-overview__score-content--good {\n background: var(--color-safety-score-background-good);\n color: var(--color-safety-score-foreground-on-good);\n}\n\n.card-overview__score-content--excellent {\n background: var(--color-safety-score-background-excellent);\n color: var(--color-safety-score-foreground-on-excellent);\n}\n\n.card-overview__score-copy {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--dimension-space-025);\n min-width: 0;\n}\n\n.card-overview__score-value,\n.card-overview__score-figure {\n flex: 0 0 auto;\n margin-block: calc(-1 * var(--dimension-space-100));\n}\n\n.card-overview__score-content--error {\n align-items: center;\n justify-content: center;\n border: var(--ds-card-overview-divider-width) solid\n var(--color-always-dark-border-tertiary);\n background: transparent;\n color: var(--color-always-dark-foreground-negative);\n text-align: center;\n}\n\n.card-overview__metric-action {\n flex-direction: column;\n justify-content: center;\n gap: var(--dimension-space-050);\n}\n\n.card-overview__metric-action[role='button'] {\n cursor: default;\n}\n\n.card-overview__metric-content {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n justify-content: center;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n.card-overview__metric-label {\n min-width: 0;\n}\n\n.card-overview__metric-figure {\n display: flex;\n align-items: baseline;\n gap: var(--dimension-space-100);\n min-width: 0;\n}\n\n.card-overview__trend {\n flex: 0 0 auto;\n white-space: nowrap;\n}\n\n:host(.card-overview--stacked) .card-overview__metrics {\n grid-template-columns: minmax(0, 1fr);\n}\n\n:host(.card-overview--compact) {\n height: var(--dimension-size-600);\n min-height: var(--dimension-size-600);\n}\n\n:host(.card-overview--compact) .card-overview__surface,\n:host(.card-overview--compact) .card-overview__clip,\n:host(.card-overview--compact) .card-overview__layout,\n:host(.card-overview--compact) .card-overview__summary {\n width: 100%;\n height: 100%;\n}\n\n:host(.card-overview--compact) .card-overview__header::after {\n display: none;\n}\n\n@media (forced-colors: active) {\n .card-overview__header::after {\n background: var(--ds-forced-color-content);\n }\n\n .card-overview__score-content {\n border: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n background: var(--ds-forced-color-surface);\n color: var(--ds-forced-color-content);\n }\n}\n",
368
368
  "type": "registry:ui"
369
369
  },
370
370
  {
371
371
  "path": "src/wc/components/CardOverview/CardOverview.tsx",
372
- "content": "import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';\nimport { resolveCssLengthPx, TOKEN_DEFAULTS } from '../../utils';\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 resolveSafetyScoreLevel,\n} from './card-overview-controller';\n\n/** Metric cells below this width drop a grid column rather than compress further. */\nconst DEFAULT_METRIC_MIN_WIDTH = 'var(--dimension-menu-width-xs)';\n\n/** Skeleton cells rendered while loading with no metrics supplied yet. */\nconst LOADING_PLACEHOLDER_COUNT = 4;\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\n componentDidLoad() {\n this.observeLayout();\n }\n\n componentDidRender() {\n // Re-bind after HMR when DidLoad's observer was dropped.\n if (!this.layoutResizeObserver) this.observeLayout();\n else this.updateLayoutGeometry();\n }\n\n disconnectedCallback() {\n this.layoutResizeObserver?.disconnect();\n this.layoutResizeObserver = null;\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 =\n this.isLoading && this.metrics.length === 0\n ? LOADING_PLACEHOLDER_COUNT\n : this.visibleMetrics.length;\n return 1 + metricCount;\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 (!layout) return;\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) this.updateLayoutGeometry(rect.width, rect.height);\n });\n this.layoutResizeObserver.observe(layout);\n }\n\n private updateLayoutGeometry(width?: number, height?: number) {\n const layout = this.layoutEl;\n if (!layout) return;\n\n const bounds = width === undefined || height === undefined ? layout.getBoundingClientRect() : null;\n const resolvedWidth = width ?? bounds?.width ?? 0;\n const resolvedHeight = height ?? bounds?.height ?? 0;\n\n if (resolvedHeight > 0 && Math.abs(resolvedHeight - this.expandedHeight) >= 0.5) {\n this.expandedHeight = resolvedHeight;\n }\n\n const columns = resolveOverviewGridColumns({\n cellCount: this.visibleCellCount,\n availableWidth: resolvedWidth,\n minCellWidth: resolveCssLengthPx(this.metricMinWidth, TOKEN_DEFAULTS.menuWidthXs),\n });\n if (columns !== this.gridColumns) this.gridColumns = columns;\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(\n trend: MetricTrend | undefined,\n variant: TextVariant,\n emphasis = false,\n color?: TextColor\n ) {\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 emphasis={emphasis}\n color={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 <div class=\"card-overview__score-copy\">\n {this.bar('text-body-small', '64px', 'card-overview__score-label')}\n {this.bar('text-body-medium', '28px')}\n </div>\n <div class=\"card-overview__score-figure\">\n {this.bar('text-display-medium', '56px')}\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 local/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 const level = score.level ?? resolveSafetyScoreLevel(score.value);\n\n return (\n <div class=\"card-overview__score\" part=\"score\">\n <div\n class={{\n 'card-overview__score-content': true,\n [`card-overview__score-content--${level}`]: Boolean(level),\n }}\n >\n <div class=\"card-overview__score-copy\">\n <ds-text\n as=\"span\"\n class=\"card-overview__score-label ds-control-label-box\"\n variant=\"text-body-small\"\n color=\"inherit\"\n >\n {score.label}\n </ds-text>\n {this.renderTrend(score.trend, 'text-body-medium', false, 'inherit')}\n </div>\n <ds-text\n as=\"span\"\n class=\"card-overview__score-value ds-control-label-box\"\n variant=\"text-display-medium\"\n emphasis\n color=\"inherit\"\n fontFeature=\"tabular-nums\"\n >\n {score.value}\n </ds-text>\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 >\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 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 && this.metrics.length === 0) {\n return [\n this.renderScore(),\n ...Array.from({ length: LOADING_PLACEHOLDER_COUNT }, (_, index) => (\n <div class=\"card-overview__metric\" key={`loading-${index}`}>\n <div class=\"card-overview__metric-action card-overview__metric-action--inactive\">\n {this.bar('text-body-small', '70%', 'card-overview__metric-label')}\n <div class=\"card-overview__metric-figure\">\n {this.bar('text-body-medium', '36px')}\n {/* text-body-medium, matching renderTrend for a metric — a smaller\n * variant here leaves the trend bar short of the value beside it. */}\n {this.bar('text-body-medium', '40px')}\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 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={{\n '--ds-card-overview-metric-min': this.metricMinWidth,\n '--ds-card-overview-grid-columns': this.gridColumns\n ? String(this.gridColumns)\n : undefined,\n '--ds-card-overview-expanded-height': collapse.active\n ? `${collapse.expandedHeight}px`\n : undefined,\n '--ds-card-overview-visible-height': collapse.active\n ? `${collapse.visibleHeight}px`\n : undefined,\n '--ds-card-overview-content-offset': collapse.active\n ? `${collapse.offset}px`\n : undefined,\n }}\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 {this.isLoading ? (\n <div class=\"card-overview__period\">\n {this.bar('text-body-medium', '184px')}\n {this.bar('text-body-medium', '248px')}\n </div>\n ) : (\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 local/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 )}\n <div class=\"card-overview__filter\">\n {/*\n Period control is application owned: it varies by product surface.\n While loading it is stood in for by a control-shaped skeleton so the\n header keeps its resolved proportions.\n */}\n {this.isLoading ? (\n <ds-skeleton\n variant=\"control\"\n controlSize=\"md\"\n width=\"128px\"\n background=\"always-dark\"\n />\n ) : (\n <slot name=\"filter\" />\n )}\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",
372
+ "content": "import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';\nimport { resolveCssLengthPx, TOKEN_DEFAULTS } from '../../utils';\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 resolveSafetyScoreLevel,\n} from './card-overview-controller';\n\n/** Metric cells below this width drop a grid column rather than compress further. */\nconst DEFAULT_METRIC_MIN_WIDTH = 'var(--dimension-menu-width-xs)';\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\n componentDidLoad() {\n this.observeLayout();\n }\n\n componentDidRender() {\n // Re-bind after HMR when DidLoad's observer was dropped.\n if (!this.layoutResizeObserver) this.observeLayout();\n else this.updateLayoutGeometry();\n }\n\n disconnectedCallback() {\n this.layoutResizeObserver?.disconnect();\n this.layoutResizeObserver = null;\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 (!layout) return;\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) this.updateLayoutGeometry(rect.width, rect.height);\n });\n this.layoutResizeObserver.observe(layout);\n }\n\n private updateLayoutGeometry(width?: number, height?: number) {\n const layout = this.layoutEl;\n if (!layout) return;\n\n const bounds = width === undefined || height === undefined ? layout.getBoundingClientRect() : null;\n const resolvedWidth = width ?? bounds?.width ?? 0;\n const resolvedHeight = height ?? bounds?.height ?? 0;\n\n if (resolvedHeight > 0 && Math.abs(resolvedHeight - this.expandedHeight) >= 0.5) {\n this.expandedHeight = resolvedHeight;\n }\n\n const columns = resolveOverviewGridColumns({\n cellCount: this.visibleCellCount,\n availableWidth: resolvedWidth,\n minCellWidth: resolveCssLengthPx(this.metricMinWidth, TOKEN_DEFAULTS.menuWidthXs),\n });\n if (columns !== this.gridColumns) this.gridColumns = columns;\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(\n trend: MetricTrend | undefined,\n variant: TextVariant,\n emphasis = false,\n color?: TextColor\n ) {\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 emphasis={emphasis}\n color={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 <div class=\"card-overview__score-copy\">\n {this.bar('text-body-small', '64px', 'card-overview__score-label')}\n {this.bar('text-body-medium', '28px')}\n </div>\n <div class=\"card-overview__score-figure\">\n {this.bar('text-display-medium', '56px')}\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 local/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 const level = score.level ?? resolveSafetyScoreLevel(score.value);\n\n return (\n <div class=\"card-overview__score\" part=\"score\">\n <div\n class={{\n 'card-overview__score-content': true,\n [`card-overview__score-content--${level}`]: Boolean(level),\n }}\n >\n <div class=\"card-overview__score-copy\">\n <ds-text\n as=\"span\"\n class=\"card-overview__score-label ds-control-label-box\"\n variant=\"text-body-small\"\n color=\"inherit\"\n >\n {score.label}\n </ds-text>\n {this.renderTrend(score.trend, 'text-body-medium', false, 'inherit')}\n </div>\n <ds-text\n as=\"span\"\n class=\"card-overview__score-value ds-control-label-box\"\n variant=\"text-display-medium\"\n emphasis\n color=\"inherit\"\n fontFeature=\"tabular-nums\"\n >\n {score.value}\n </ds-text>\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 >\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 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', '40px')}\n {/* Match renderTrend's type canvas and the score trend's 28px width. */}\n {this.bar('text-body-medium', '28px')}\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 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={{\n '--ds-card-overview-metric-min': this.metricMinWidth,\n '--ds-card-overview-grid-columns': this.gridColumns\n ? String(this.gridColumns)\n : undefined,\n '--ds-card-overview-expanded-height': collapse.active\n ? `${collapse.expandedHeight}px`\n : undefined,\n '--ds-card-overview-visible-height': collapse.active\n ? `${collapse.visibleHeight}px`\n : undefined,\n '--ds-card-overview-content-offset': collapse.active\n ? `${collapse.offset}px`\n : undefined,\n }}\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 local/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",
373
373
  "type": "registry:ui"
374
374
  },
375
375
  {
@@ -249,7 +249,7 @@
249
249
  },
250
250
  "peerDependencies": {
251
251
  "required": [
252
- "@ds-mo/tokens ^5.1.1",
252
+ "@ds-mo/tokens ^6.0.0",
253
253
  "@ds-mo/icons ^6.4.0"
254
254
  ],
255
255
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -268,12 +268,12 @@
268
268
  "files": [
269
269
  {
270
270
  "path": "src/wc/components/CardSetting/CardSetting.css",
271
- "content": "@import '../../utils/chrome-layout.css';\n\n:host {\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 background-color: var(--color-background-primary);\n border: none;\n border-radius: var(--dimension-radius-025);\n box-shadow: 0 0 0 var(--dimension-stroke-width-012) var(--color-border-tertiary);\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.card-setting__header {\n justify-content: space-between;\n height: var(--dimension-size-600);\n flex: 0 0 auto;\n background-color: var(--color-background-secondary);\n border-bottom: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n}\n\n:host(.card-setting--editing) .card-setting__header {\n background-color: transparent;\n border-bottom-color: transparent;\n}\n\nds-text.card-setting__title {\n display: flex;\n flex: 1 1 auto;\n align-items: center;\n min-width: 0;\n padding: calc(var(--dimension-space-075) + var(--dimension-space-025));\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.card-setting__actions {\n display: flex;\n flex-shrink: 0;\n align-items: center;\n gap: var(--dimension-space-100);\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 background-color: var(--color-background-primary);\n border-radius: 0 0 var(--dimension-radius-025) var(--dimension-radius-025);\n}\n\n:host(.card-setting--editing) .card-setting__body {\n border-radius: var(--dimension-radius-025);\n}\n",
271
+ "content": "@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(--dimension-radius-125);\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)\n 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__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(--dimension-radius-125) var(--dimension-radius-125);\n}\n\n:host(.card-setting--editing) .card-setting__body {\n border-radius: var(--dimension-radius-125);\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",
272
272
  "type": "registry:ui"
273
273
  },
274
274
  {
275
275
  "path": "src/wc/components/CardSetting/CardSetting.tsx",
276
- "content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\n\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\n@Component({\n tag: 'ds-card-setting',\n styleUrl: 'CardSetting.css',\n scoped: true,\n})\nexport class CardSetting {\n /** Section heading shown in the card header. */\n @Prop() heading!: string;\n\n /**\n * Card width token (`sm` / `md` / `lg`). Also sets host `min-height` to the\n * matching `--dimension-card-height-*` so the body fills available space\n * even when the slot is empty.\n */\n @Prop() cardWidth: CardSettingWidth = 'md';\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 private emitAction(action: CardSettingAction, originalEvent: MouseEvent) {\n this.dsAction.emit({ action, originalEvent });\n }\n\n render() {\n const editing = this.editing;\n\n return (\n <Host\n class={{\n 'card-setting': true,\n 'card-setting--editing': editing,\n }}\n style={{\n '--_card-setting-width': CARD_WIDTH_VARS[this.cardWidth],\n '--_card-setting-min-height': CARD_HEIGHT_VARS[this.cardWidth],\n }}\n >\n <header class=\"card-setting__header ds-chrome-row ds-chrome-space--md\">\n <ds-text\n class=\"card-setting__title\"\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 <div class=\"card-setting__actions\">\n {!editing ? (\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 ) : (\n [\n <ds-button-unfilled\n key=\"cancel\"\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-button-filled\n key=\"save\"\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 ]\n )}\n </div>\n </header>\n <div class=\"card-setting__body\">\n <slot />\n </div>\n </Host>\n );\n }\n}\n",
276
+ "content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\n\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\n@Component({\n tag: 'ds-card-setting',\n styleUrl: 'CardSetting.css',\n scoped: true,\n})\nexport class CardSetting {\n /** Section heading shown in the card header. */\n @Prop() heading!: string;\n\n /**\n * Card width token (`sm` / `md` / `lg`). Also sets host `min-height` to the\n * matching `--dimension-card-height-*` so the body fills available space\n * even when the slot is empty.\n */\n @Prop() cardWidth: CardSettingWidth = 'md';\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 private emitAction(action: CardSettingAction, originalEvent: MouseEvent) {\n this.dsAction.emit({ action, originalEvent });\n }\n\n render() {\n const editing = this.editing;\n\n return (\n <Host\n class={{\n 'card-setting': true,\n 'card-setting--editing': editing,\n }}\n style={{\n '--_card-setting-width': CARD_WIDTH_VARS[this.cardWidth],\n '--_card-setting-min-height': CARD_HEIGHT_VARS[this.cardWidth],\n }}\n >\n <header class=\"card-setting__header ds-chrome-header\">\n {/* eslint-disable-next-line local/prefer-direct-ds-text -- Shared header copy owns the control-density geometry around the semantic heading. */}\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 </div>\n <div class=\"card-setting__actions ds-chrome-header__trailing\">\n {!editing ? (\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 ) : (\n [\n <ds-button-unfilled\n key=\"cancel\"\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-button-filled\n key=\"save\"\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 ]\n )}\n </div>\n </header>\n <div class=\"card-setting__body\">\n <slot />\n </div>\n </Host>\n );\n }\n}\n",
277
277
  "type": "registry:ui"
278
278
  }
279
279
  ]
@@ -267,7 +267,7 @@
267
267
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: items.",
268
268
  "peerDependencies": {
269
269
  "required": [
270
- "@ds-mo/tokens ^5.1.1",
270
+ "@ds-mo/tokens ^6.0.0",
271
271
  "@ds-mo/icons ^6.4.0"
272
272
  ],
273
273
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -229,7 +229,7 @@
229
229
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: definition.",
230
230
  "peerDependencies": {
231
231
  "required": [
232
- "@ds-mo/tokens ^5.1.1",
232
+ "@ds-mo/tokens ^6.0.0",
233
233
  "@ds-mo/icons ^6.4.0"
234
234
  ],
235
235
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -332,7 +332,7 @@
332
332
  },
333
333
  "peerDependencies": {
334
334
  "required": [
335
- "@ds-mo/tokens ^5.1.1",
335
+ "@ds-mo/tokens ^6.0.0",
336
336
  "@ds-mo/icons ^6.4.0"
337
337
  ],
338
338
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -245,7 +245,7 @@
245
245
  },
246
246
  "peerDependencies": {
247
247
  "required": [
248
- "@ds-mo/tokens ^5.1.1",
248
+ "@ds-mo/tokens ^6.0.0",
249
249
  "@ds-mo/icons ^6.4.0"
250
250
  ],
251
251
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -120,7 +120,7 @@
120
120
  },
121
121
  "peerDependencies": {
122
122
  "required": [
123
- "@ds-mo/tokens ^5.1.1",
123
+ "@ds-mo/tokens ^6.0.0",
124
124
  "@ds-mo/icons ^6.4.0"
125
125
  ],
126
126
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -253,7 +253,7 @@
253
253
  },
254
254
  "peerDependencies": {
255
255
  "required": [
256
- "@ds-mo/tokens ^5.1.1",
256
+ "@ds-mo/tokens ^6.0.0",
257
257
  "@ds-mo/icons ^6.4.0"
258
258
  ],
259
259
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -110,7 +110,7 @@
110
110
  },
111
111
  "peerDependencies": {
112
112
  "required": [
113
- "@ds-mo/tokens ^5.1.1",
113
+ "@ds-mo/tokens ^6.0.0",
114
114
  "@ds-mo/icons ^6.4.0"
115
115
  ],
116
116
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."