@ds-mo/ui 13.0.0 → 13.2.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 (170) hide show
  1. package/README.md +6 -0
  2. package/dist/.package-ready.json +1 -1
  3. package/dist/agent-patterns.json +1 -1
  4. package/dist/agent.json +1 -1
  5. package/dist/components/ds-agent-response.js +1 -1
  6. package/dist/components/ds-bar-nav.js +1 -1
  7. package/dist/components/ds-bar-title.js +1 -1
  8. package/dist/components/ds-bar-workflow.js +1 -1
  9. package/dist/components/ds-button-filled.js +1 -1
  10. package/dist/components/ds-button-unfilled.js +1 -1
  11. package/dist/components/ds-card-data-viz.js +1 -1
  12. package/dist/components/ds-card-overview.js +1 -1
  13. package/dist/components/ds-card-overview.js.map +1 -1
  14. package/dist/components/ds-card-setting.js +1 -1
  15. package/dist/components/ds-chart-donut.js +1 -1
  16. package/dist/components/ds-chart-donut.js.map +1 -1
  17. package/dist/components/ds-chart-legend.js +1 -1
  18. package/dist/components/ds-chart-legend.js.map +1 -1
  19. package/dist/components/ds-checkbox.js +1 -1
  20. package/dist/components/ds-chip.js +1 -1
  21. package/dist/components/ds-chip.js.map +1 -1
  22. package/dist/components/ds-code-block.js +1 -1
  23. package/dist/components/ds-conversation-list-item.js +1 -1
  24. package/dist/components/ds-conversation-list-item.js.map +1 -1
  25. package/dist/components/ds-input.js +1 -1
  26. package/dist/components/ds-input.js.map +1 -1
  27. package/dist/components/ds-markdown.js +1 -1
  28. package/dist/components/ds-menu.js +1 -1
  29. package/dist/components/ds-message-composer.js +1 -1
  30. package/dist/components/ds-message-scroller.js +1 -1
  31. package/dist/components/ds-mobile-header.js +1 -1
  32. package/dist/components/ds-mobile-section-switcher.js +1 -1
  33. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  34. package/dist/components/ds-modal.js +1 -1
  35. package/dist/components/ds-panel-nav.js +1 -1
  36. package/dist/components/ds-panel-sub-nav.js +1 -1
  37. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  38. package/dist/components/ds-panel-tool-header.js +1 -1
  39. package/dist/components/ds-panel-tool-search.js +1 -1
  40. package/dist/components/ds-panel-tools.js +1 -1
  41. package/dist/components/ds-radio.js +1 -1
  42. package/dist/components/ds-radio.js.map +1 -1
  43. package/dist/components/ds-select.js +1 -1
  44. package/dist/components/ds-select.js.map +1 -1
  45. package/dist/components/ds-shell-app.js +1 -1
  46. package/dist/components/ds-shell-tools.js +1 -1
  47. package/dist/components/ds-slider.js +1 -1
  48. package/dist/components/ds-slider.js.map +1 -1
  49. package/dist/components/ds-swatch-picker.js +1 -1
  50. package/dist/components/ds-switch.js +1 -1
  51. package/dist/components/ds-tab-group.js +1 -1
  52. package/dist/components/ds-tag.js +1 -1
  53. package/dist/components/ds-tag.js.map +1 -1
  54. package/dist/components/ds-toast.js +1 -1
  55. package/dist/components/{p-B1cbQ0SG.js → p-B1XW4VSn.js} +2 -2
  56. package/dist/components/p-B9p6w-_T.js +2 -0
  57. package/dist/components/{p-z5RIrXuX.js.map → p-B9p6w-_T.js.map} +1 -1
  58. package/dist/components/p-BBFEhNzM.js +2 -0
  59. package/dist/components/{p-CS6CHlHK.js.map → p-BBFEhNzM.js.map} +1 -1
  60. package/dist/components/p-BGUK8-Qm.js +2 -0
  61. package/dist/components/{p-C3ArcO50.js.map → p-BGUK8-Qm.js.map} +1 -1
  62. package/dist/components/p-BO4cj3iX.js +2 -0
  63. package/dist/components/{p-BaUsq_VK.js.map → p-BO4cj3iX.js.map} +1 -1
  64. package/dist/components/p-BYWkksDO.js +2 -0
  65. package/dist/components/{p-Bkv3UjZz.js.map → p-BYWkksDO.js.map} +1 -1
  66. package/dist/components/{p-CWoXKe_h.js → p-C-aCzwhA.js} +2 -2
  67. package/dist/components/{p-DdvEZyX4.js → p-C-kzknkR.js} +2 -2
  68. package/dist/components/p-C4uYvyq6.js.map +1 -1
  69. package/dist/components/{p-Dg1LBXJ2.js → p-CH9yArA1.js} +2 -2
  70. package/dist/components/p-CHpNyAo1.js +2 -0
  71. package/dist/components/{p-CMWata9g.js.map → p-CHpNyAo1.js.map} +1 -1
  72. package/dist/components/{p-D09HINr_.js → p-CJDTh9Tg.js} +2 -2
  73. package/dist/components/p-CJbLY1tu.js +2 -0
  74. package/dist/components/{p-CwwuSGER.js.map → p-CJbLY1tu.js.map} +1 -1
  75. package/dist/components/{p-DeQwkbT1.js → p-CreGah0j.js} +2 -2
  76. package/dist/components/p-Cvxm1OZB.js +2 -0
  77. package/dist/components/{p-lyZNvRvO.js.map → p-Cvxm1OZB.js.map} +1 -1
  78. package/dist/components/{p-CTTnM7cE.js → p-DhEdICfD.js} +2 -2
  79. package/dist/components/{p-BwT6B5Fi.js → p-bxoryr-3.js} +2 -2
  80. package/dist/components/p-eAfNzlMa.js +2 -0
  81. package/dist/components/{p-DsiCDzoF.js.map → p-eAfNzlMa.js.map} +1 -1
  82. package/dist/components/{p-_DzUXA86.js → p-gZni7A2L.js} +2 -2
  83. package/dist/docs/components.json +1 -1
  84. package/dist/mcp/cli.js +15613 -17399
  85. package/dist/mcp-data/registry/agent-activity.json +1 -1
  86. package/dist/mcp-data/registry/agent-response.json +1 -1
  87. package/dist/mcp-data/registry/agent-source-list.json +1 -1
  88. package/dist/mcp-data/registry/agent-tool-call.json +1 -1
  89. package/dist/mcp-data/registry/attachment-list.json +1 -1
  90. package/dist/mcp-data/registry/avatar.json +1 -1
  91. package/dist/mcp-data/registry/badge.json +1 -1
  92. package/dist/mcp-data/registry/bar-nav.json +1 -1
  93. package/dist/mcp-data/registry/bar-title.json +1 -1
  94. package/dist/mcp-data/registry/bar-workflow.json +1 -1
  95. package/dist/mcp-data/registry/breadcrumb.json +1 -1
  96. package/dist/mcp-data/registry/button-filled.json +1 -1
  97. package/dist/mcp-data/registry/button-unfilled.json +1 -1
  98. package/dist/mcp-data/registry/card-data-viz.json +1 -1
  99. package/dist/mcp-data/registry/card-overview.json +3 -3
  100. package/dist/mcp-data/registry/card-setting.json +1 -1
  101. package/dist/mcp-data/registry/chart-bar.json +1 -1
  102. package/dist/mcp-data/registry/chart-donut.json +3 -3
  103. package/dist/mcp-data/registry/chart-legend.json +3 -3
  104. package/dist/mcp-data/registry/chart-line.json +1 -1
  105. package/dist/mcp-data/registry/checkbox.json +1 -1
  106. package/dist/mcp-data/registry/chip.json +1 -1
  107. package/dist/mcp-data/registry/code-block.json +1 -1
  108. package/dist/mcp-data/registry/conversation-list-item.json +1 -1
  109. package/dist/mcp-data/registry/conversation-list-section.json +1 -1
  110. package/dist/mcp-data/registry/conversation-list.json +1 -1
  111. package/dist/mcp-data/registry/divider.json +1 -1
  112. package/dist/mcp-data/registry/empty-state.json +1 -1
  113. package/dist/mcp-data/registry/field.json +1 -1
  114. package/dist/mcp-data/registry/icon.json +1 -1
  115. package/dist/mcp-data/registry/input.json +1 -1
  116. package/dist/mcp-data/registry/loader.json +1 -1
  117. package/dist/mcp-data/registry/markdown.json +1 -1
  118. package/dist/mcp-data/registry/menu.json +1 -1
  119. package/dist/mcp-data/registry/message-bubble.json +1 -1
  120. package/dist/mcp-data/registry/message-composer.json +1 -1
  121. package/dist/mcp-data/registry/message-scroller.json +1 -1
  122. package/dist/mcp-data/registry/message.json +1 -1
  123. package/dist/mcp-data/registry/mobile-bar-nav.json +1 -1
  124. package/dist/mcp-data/registry/mobile-header.json +1 -1
  125. package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
  126. package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
  127. package/dist/mcp-data/registry/modal.json +1 -1
  128. package/dist/mcp-data/registry/panel-nav.json +1 -1
  129. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  130. package/dist/mcp-data/registry/panel-tool-header.json +1 -1
  131. package/dist/mcp-data/registry/panel-tool-search.json +1 -1
  132. package/dist/mcp-data/registry/panel-tools.json +1 -1
  133. package/dist/mcp-data/registry/radio.json +1 -1
  134. package/dist/mcp-data/registry/registry.json +1 -1
  135. package/dist/mcp-data/registry/scroll-overlay.json +1 -1
  136. package/dist/mcp-data/registry/select.json +1 -1
  137. package/dist/mcp-data/registry/shell-app.json +1 -1
  138. package/dist/mcp-data/registry/shell-page.json +1 -1
  139. package/dist/mcp-data/registry/shell-tools.json +1 -1
  140. package/dist/mcp-data/registry/skeleton.json +1 -1
  141. package/dist/mcp-data/registry/slider.json +1 -1
  142. package/dist/mcp-data/registry/swatch-picker.json +1 -1
  143. package/dist/mcp-data/registry/switch.json +1 -1
  144. package/dist/mcp-data/registry/tab-group.json +1 -1
  145. package/dist/mcp-data/registry/tag.json +1 -1
  146. package/dist/mcp-data/registry/text.json +1 -1
  147. package/dist/mcp-data/registry/toast.json +1 -1
  148. package/dist/mcp-data/registry/tooltip-data-viz.json +1 -1
  149. package/dist/mcp-data/registry/tooltip.json +1 -1
  150. package/dist/mcp-data/registry/typing-indicator.json +1 -1
  151. package/dist/types/components/CardOverview/CardOverview.d.ts +2 -3
  152. package/package.json +5 -4
  153. package/dist/components/p-BaUsq_VK.js +0 -2
  154. package/dist/components/p-Bkv3UjZz.js +0 -2
  155. package/dist/components/p-C3ArcO50.js +0 -2
  156. package/dist/components/p-CMWata9g.js +0 -2
  157. package/dist/components/p-CS6CHlHK.js +0 -2
  158. package/dist/components/p-CwwuSGER.js +0 -2
  159. package/dist/components/p-DsiCDzoF.js +0 -2
  160. package/dist/components/p-lyZNvRvO.js +0 -2
  161. package/dist/components/p-z5RIrXuX.js +0 -2
  162. /package/dist/components/{p-B1cbQ0SG.js.map → p-B1XW4VSn.js.map} +0 -0
  163. /package/dist/components/{p-CWoXKe_h.js.map → p-C-aCzwhA.js.map} +0 -0
  164. /package/dist/components/{p-DdvEZyX4.js.map → p-C-kzknkR.js.map} +0 -0
  165. /package/dist/components/{p-Dg1LBXJ2.js.map → p-CH9yArA1.js.map} +0 -0
  166. /package/dist/components/{p-D09HINr_.js.map → p-CJDTh9Tg.js.map} +0 -0
  167. /package/dist/components/{p-DeQwkbT1.js.map → p-CreGah0j.js.map} +0 -0
  168. /package/dist/components/{p-CTTnM7cE.js.map → p-DhEdICfD.js.map} +0 -0
  169. /package/dist/components/{p-BwT6B5Fi.js.map → p-bxoryr-3.js.map} +0 -0
  170. /package/dist/components/{p-_DzUXA86.js.map → p-gZni7A2L.js.map} +0 -0
@@ -126,7 +126,7 @@
126
126
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: items.",
127
127
  "peerDependencies": {
128
128
  "required": [
129
- "@ds-mo/tokens ^5.0.0",
129
+ "@ds-mo/tokens ^5.1.0",
130
130
  "@ds-mo/icons ^6.4.0"
131
131
  ],
132
132
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -182,7 +182,7 @@
182
182
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: parts.",
183
183
  "peerDependencies": {
184
184
  "required": [
185
- "@ds-mo/tokens ^5.0.0",
185
+ "@ds-mo/tokens ^5.1.0",
186
186
  "@ds-mo/icons ^6.4.0"
187
187
  ],
188
188
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -122,7 +122,7 @@
122
122
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: items.",
123
123
  "peerDependencies": {
124
124
  "required": [
125
- "@ds-mo/tokens ^5.0.0",
125
+ "@ds-mo/tokens ^5.1.0",
126
126
  "@ds-mo/icons ^6.4.0"
127
127
  ],
128
128
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -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.0.0",
184
+ "@ds-mo/tokens ^5.1.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.0.0",
109
+ "@ds-mo/tokens ^5.1.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.0.0",
146
+ "@ds-mo/tokens ^5.1.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.0.0",
263
+ "@ds-mo/tokens ^5.1.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."
@@ -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.0.0",
279
+ "@ds-mo/tokens ^5.1.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."
@@ -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.0.0",
452
+ "@ds-mo/tokens ^5.1.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.0.0",
354
+ "@ds-mo/tokens ^5.1.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.0.0",
174
+ "@ds-mo/tokens ^5.1.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.0.0",
494
+ "@ds-mo/tokens ^5.1.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.0.0",
555
+ "@ds-mo/tokens ^5.1.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."
@@ -211,7 +211,7 @@
211
211
  },
212
212
  "peerDependencies": {
213
213
  "required": [
214
- "@ds-mo/tokens ^5.0.0",
214
+ "@ds-mo/tokens ^5.1.0",
215
215
  "@ds-mo/icons ^6.4.0"
216
216
  ],
217
217
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -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.0.0",
348
+ "@ds-mo/tokens ^5.1.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."
@@ -366,12 +366,12 @@
366
366
  "files": [
367
367
  {
368
368
  "path": "src/wc/components/CardOverview/CardOverview.css",
369
- "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/*\n * A permanently dark surface, independent of the page theme, so the summary bar\n * reads the same on every product surface. Every foreground, border, and\n * interaction token below therefore comes from the always-dark family rather\n * than the theme-reactive one.\n */\n\n:host {\n display: block;\n box-sizing: border-box;\n /* Drives the narrow layout below from the card's own width, not the viewport's. */\n container-type: inline-size;\n /* A summary is read, not copied; stray selection while clicking cells is noise. */\n -webkit-user-select: none;\n user-select: none;\n /* Width a metric may shrink to before the grid drops a column. */\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 --ds-card-overview-divider: var(--ds-card-overview-divider-width) solid\n var(--ds-card-overview-divider-color);\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-050);\n background: var(--color-always-dark-background);\n color: var(--color-always-dark-foreground-primary);\n overflow-anchor: none;\n}\n\n/*\n * The visual surface owns elevation outside this clipping viewport. Keeping\n * those layers separate lets a page shrink the surface while content moves and\n * clips inside it without cutting off the shadow or inset highlight.\n */\n.card-overview__clip {\n height: 100%;\n min-width: 0;\n border-radius: inherit;\n background: inherit;\n overflow: clip;\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__layout {\n transform: translateY(calc(-1 * var(--ds-card-overview-content-offset)));\n will-change: transform;\n}\n\n.card-overview__layout {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n grid-template-columns: minmax(0, 1fr);\n align-items: stretch;\n min-width: 0;\n}\n\n.card-overview__layout--has-score {\n grid-template-columns: auto minmax(0, 1fr);\n}\n\n/*\n * Transparent at regular widths so the score and header participate in the\n * inner layout grid directly. At the narrow breakpoint it becomes the unified\n * summary block above the metrics.\n */\n.card-overview__summary {\n display: contents;\n}\n\n/* ── Score ────────────────────────────────────────────────────────────────── */\n\n/*\n * Hugs its content and never shrinks, so the headline figure keeps its size and\n * the metric grid absorbs the remaining width.\n */\n/*\n * Top aligned rather than centred, and padded to match the header, so the score\n * label sits in the same 32px box at the same offset as the period text beside\n * it. Both use the same type variant, so their baselines land together.\n */\n.card-overview__score {\n display: flex;\n grid-row: 1 / span 2;\n grid-column: 1;\n flex: 0 0 auto;\n flex-direction: column;\n gap: 0;\n min-width: 0;\n padding: var(--dimension-space-100) var(--dimension-space-200);\n border-inline-end: var(--ds-card-overview-divider);\n}\n\n.card-overview__score--resolved {\n min-inline-size: max-content;\n}\n\n/* The control-height box that aligns this label with the period text. */\n.card-overview__score-label-row {\n display: flex;\n align-items: center;\n min-height: var(--ds-control-height);\n}\n\n/*\n * The score is its own column, so its rows cannot baseline-align with the measure\n * rows through flex or grid — those only align items that share a line. These two\n * offsets pull the column onto the measure baselines instead.\n *\n * They are compensation for how much lower a display-scale figure sits in its line\n * box than the body text it aligns to, so they are coupled to the type pairing:\n * change the score or measure variants and both need re-measuring.\n */\n.card-overview__score-figure {\n display: flex;\n align-items: baseline;\n gap: var(--dimension-space-050);\n min-width: 0;\n margin-block-start: calc(-1 * (var(--dimension-space-150) + var(--dimension-space-012)));\n}\n\n.card-overview__score-value {\n flex: 0 0 auto;\n}\n\n/* Second half of the baseline compensation described above the score figure. */\n.card-overview__score-band {\n margin-block-start: calc(-1 * var(--dimension-space-025));\n}\n\n/*\n * A skeleton bar is a solid block, so in a baseline-aligned row it aligns on its\n * bottom edge while the text it stands in for aligns on its baseline. Next to a\n * display-scale figure that difference is the descent under the figure's\n * baseline, which drops the bar below where the resolved trend sits.\n *\n * The offset is applied with relative positioning rather than a margin: a margin\n * is not part of a synthesized baseline, so it grows the figure instead of moving\n * the bar, which then pushes the band below it out of place.\n *\n * Only needed beside the score figure; the measure rows pair body variants whose\n * bottom edge and baseline already agree.\n */\n.card-overview__skeleton-baseline {\n position: relative;\n inset-block-start: calc(-1 * var(--dimension-space-150));\n}\n\n.card-overview__score--error {\n flex-direction: row;\n align-items: center;\n gap: var(--dimension-space-050);\n color: var(--color-reference-dark-red-30-l70-c20-bold);\n}\n\n/* ── Body ─────────────────────────────────────────────────────────────────── */\n\n/*\n * Takes the remaining inline space. The minimum is what makes the host wrap: once\n * the score and roughly two measure columns no longer fit side by side, the body\n * drops below the score instead of squeezing it. `min()` keeps that from becoming\n * an overflow floor on very narrow viewports.\n */\n.card-overview__body {\n display: flex;\n grid-row: 2;\n grid-column: 1;\n flex-direction: column;\n min-width: 0;\n}\n\n.card-overview__body--has-score {\n grid-column: 2;\n}\n\n/*\n * One control row: 8px of padding on every side around a 32px md control, so the\n * bar is 8 + 32 + 8. Both ends of the bar are control-shaped boxes — the period\n * text on the left and the slotted control on the right — so the row reads\n * balanced rather than text on one side and a control on the other.\n */\n.card-overview__header {\n display: flex;\n grid-row: 1;\n grid-column: 1;\n align-items: center;\n gap: var(--dimension-space-100);\n min-width: 0;\n padding: var(--dimension-space-100);\n border-block-end: var(--ds-card-overview-divider);\n box-sizing: border-box;\n}\n\n.card-overview__header--has-score {\n grid-column: 2;\n}\n\n/*\n * Matches the control's box on the opposite end, including its inline padding, so\n * the text lands at the same optical inset the slotted control does:\n * 8 from the bar + 6 from this box + 2 from the label inset = 16.\n *\n * Nowrap keeps the row at the control height; the period truncates rather than\n * growing the bar to two lines.\n */\n.card-overview__period {\n display: flex;\n flex: 1 1 auto;\n align-items: center;\n gap: var(--dimension-space-050);\n min-height: var(--ds-control-height);\n min-width: 0;\n padding-inline: var(--ds-control-padding-inline);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The period control is slotted, so only reserve space for it. */\n.card-overview__filter {\n display: flex;\n flex: 0 0 auto;\n align-items: center;\n}\n\n/* ── Metrics ──────────────────────────────────────────────────────────────── */\n\n/*\n * `auto-fit` gives every occupied track an equal share while all metrics fit on\n * one row. Once wrapping creates another row, its cells stay on those same\n * tracks, so a partial final row leaves empty space instead of stretching one\n * measure wider than its peers.\n *\n * Dividers are painted outward from each cell and clipped by this grid. That\n * leaves only the lines between cells: anything emitted from the final column or\n * final row lands outside the grid and is clipped without needing to detect the\n * number of tracks.\n *\n * `flex: 1 1 auto` makes the grid absorb whatever height the body has\n * left, so cells reach the card bottom and their hover wash has no dead strip.\n */\n.card-overview__metrics {\n display: grid;\n flex: 1 1 auto;\n grid-template-columns: repeat(\n 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/*\n * Square by design. Cells tile edge to edge against their dividers, so a radius\n * would break the seam; the card's own radius clips the outer corners instead.\n *\n * Padding is plain rather than control-derived: a cell is a block of content, not\n * a control box that has to optically match something on the far side.\n */\n.card-overview__metric {\n display: flex;\n position: relative;\n flex-direction: column;\n gap: var(--dimension-space-050);\n min-width: 0;\n padding: var(--dimension-space-175) var(--dimension-space-200);\n border-radius: 0;\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}\n\n.card-overview__metric[role='button'] {\n cursor: default;\n -webkit-user-select: none;\n user-select: none;\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/* ── Trend ────────────────────────────────────────────────────────────────── */\n\n/*\n * Tone arrives resolved from the caller: the arrow direction and the sentiment\n * are independent, so a falling metric can read positive or neutral. The arrow is\n * a glyph inside this text node, so it always matches the surrounding weight.\n */\n.card-overview__trend {\n flex: 0 0 auto;\n white-space: nowrap;\n}\n\n/* ── Narrow layout ────────────────────────────────────────────────────────── */\n\n/*\n * Below roughly two measure columns the card becomes a full stack. The summary\n * wrapper owns one block above the metrics: score label, figure, and trend stay\n * together at the start; the period control sits at the upper end; and the period\n * copy follows the score instead of becoming a separate divided row.\n */\n/*\n * Selectors are host-qualified purely for specificity: the compiled sheet places\n * these rules ahead of the base ones, so a plain class here would only ever add\n * properties and never override them.\n */\n@container (max-width: 480px) {\n :host .card-overview__layout {\n grid-template-rows: auto minmax(0, 1fr);\n grid-template-columns: minmax(0, 1fr);\n }\n\n :host .card-overview__summary {\n display: grid;\n grid-row: 1;\n grid-column: 1;\n grid-template-columns: minmax(0, 1fr) auto;\n grid-template-rows: auto;\n border-block-end: var(--ds-card-overview-divider);\n }\n\n :host .card-overview__score {\n display: grid;\n grid-row: 1;\n grid-column: 1;\n grid-template-rows: auto auto;\n grid-template-columns: auto auto;\n justify-content: start;\n column-gap: var(--dimension-space-100);\n width: auto;\n padding-block-end: var(--dimension-space-100);\n padding-inline-end: var(--dimension-space-100);\n border-inline-end: none;\n }\n\n :host .card-overview__score-label-row {\n grid-row: 1;\n grid-column: 1 / -1;\n min-height: var(--ds-control-height);\n }\n\n /*\n * The wide score uses optical offsets to meet the neighbouring metric\n * baselines. In the stacked summary those neighbours are gone, so natural\n * document flow keeps the label, figure, band, and period clearly ordered.\n */\n :host .card-overview__score-band,\n :host .card-overview__score-figure {\n grid-row: 2;\n margin-block-start: 0;\n }\n\n :host .card-overview__score-figure {\n grid-column: 1;\n align-self: baseline;\n gap: var(--dimension-space-100);\n }\n\n :host .card-overview__score-band {\n grid-column: 2;\n align-self: baseline;\n display: none;\n }\n\n :host .card-overview__header {\n display: grid;\n grid-row: 1;\n grid-column: 2;\n grid-template-rows: auto auto;\n align-content: start;\n gap: 0;\n padding: 0;\n border-block-end: none;\n }\n\n :host .card-overview__filter {\n grid-row: 1;\n grid-column: 1;\n align-self: start;\n justify-self: end;\n padding: var(--dimension-space-100) var(--dimension-space-100) 0 0;\n }\n\n :host .card-overview__period {\n grid-row: 2;\n grid-column: 1;\n flex-direction: column;\n align-items: flex-end;\n justify-self: stretch;\n gap: 0;\n min-height: 0;\n padding-block: var(--dimension-space-150) var(--dimension-space-100);\n padding-inline: var(--dimension-space-100) var(--dimension-space-200);\n overflow: visible;\n text-align: end;\n white-space: nowrap;\n }\n\n :host .card-overview__body {\n grid-row: 2;\n grid-column: 1;\n }\n}\n\n/*\n * Page shells may enter a responsive mode before this card itself becomes\n * narrow. The explicit stacked layout applies the same composition without\n * coupling CardOverview to a viewport breakpoint.\n */\n:host(.card-overview--stacked) .card-overview__layout {\n grid-template-rows: auto minmax(0, 1fr);\n grid-template-columns: minmax(0, 1fr);\n}\n\n:host(.card-overview--stacked) .card-overview__summary {\n display: grid;\n grid-row: 1;\n grid-column: 1;\n grid-template-columns: minmax(0, 1fr) auto;\n grid-template-rows: auto;\n border-block-end: var(--ds-card-overview-divider);\n}\n\n:host(.card-overview--stacked) .card-overview__score {\n display: grid;\n grid-row: 1;\n grid-column: 1;\n grid-template-rows: auto auto;\n grid-template-columns: auto auto;\n justify-content: start;\n column-gap: var(--dimension-space-100);\n width: auto;\n padding-block-end: var(--dimension-space-100);\n padding-inline-end: var(--dimension-space-100);\n border-inline-end: none;\n}\n\n:host(.card-overview--stacked) .card-overview__score-label-row {\n grid-row: 1;\n grid-column: 1 / -1;\n min-height: var(--ds-control-height);\n}\n\n:host(.card-overview--stacked) .card-overview__score-band,\n:host(.card-overview--stacked) .card-overview__score-figure {\n grid-row: 2;\n margin-block-start: 0;\n}\n\n:host(.card-overview--stacked) .card-overview__score-figure {\n grid-column: 1;\n align-self: baseline;\n gap: var(--dimension-space-100);\n}\n\n:host(.card-overview--stacked) .card-overview__score-band {\n grid-column: 2;\n align-self: baseline;\n display: none;\n}\n\n:host(.card-overview--stacked) .card-overview__header {\n display: grid;\n grid-row: 1;\n grid-column: 2;\n grid-template-rows: auto auto;\n align-content: start;\n gap: 0;\n padding: 0;\n border-block-end: none;\n}\n\n:host(.card-overview--stacked) .card-overview__filter {\n grid-row: 1;\n grid-column: 1;\n align-self: start;\n justify-self: end;\n padding: var(--dimension-space-100) var(--dimension-space-100) 0 0;\n}\n\n:host(.card-overview--stacked) .card-overview__period {\n grid-row: 2;\n grid-column: 1;\n flex-direction: column;\n align-items: flex-end;\n justify-self: stretch;\n gap: 0;\n min-height: 0;\n padding-block: var(--dimension-space-150) var(--dimension-space-100);\n padding-inline: var(--dimension-space-100) var(--dimension-space-200);\n overflow: visible;\n text-align: end;\n white-space: nowrap;\n}\n\n:host(.card-overview--stacked) .card-overview__body {\n grid-row: 2;\n grid-column: 1;\n}\n\n:host(.card-overview--stacked) .card-overview__metrics {\n grid-template-columns: minmax(0, 1fr);\n}\n\n/* ── Compact presentation ───────────────────────────────────────────────── */\n\n/*\n * Matches the shared 48px bar contract: one 32px control row with 8px above\n * and below. The score remains a distinct leading cell, while reporting copy\n * absorbs the available width and the application-owned control never shrinks.\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 height: 100%;\n}\n\n:host(.card-overview--compact) .card-overview__layout,\n:host(.card-overview--compact) .card-overview__summary {\n display: flex;\n width: 100%;\n height: 100%;\n min-width: 0;\n}\n\n:host(.card-overview--compact) .card-overview__score {\n display: flex;\n flex: 0 0 auto;\n flex-direction: row;\n align-items: center;\n gap: var(--dimension-space-100);\n width: auto;\n height: 100%;\n min-width: 0;\n padding: var(--dimension-space-100) var(--dimension-space-200);\n border-inline-end: var(--ds-card-overview-divider);\n box-sizing: border-box;\n}\n\n:host(.card-overview--compact) .card-overview__score-label-row {\n display: contents;\n min-height: 0;\n}\n\n:host(.card-overview--compact) .card-overview__score-label,\n:host(.card-overview--compact) .card-overview__score-figure,\n:host(.card-overview--compact) .card-overview__trend {\n flex: 0 0 auto;\n}\n\n:host(.card-overview--compact) .card-overview__score-figure {\n display: flex;\n align-self: center;\n align-items: baseline;\n gap: var(--dimension-space-050);\n margin-block-start: 0;\n}\n\n:host(.card-overview--compact) .card-overview__score-band {\n display: none;\n}\n\n:host(.card-overview--compact) .card-overview__header {\n display: flex;\n flex: 1 1 auto;\n grid-row: auto;\n grid-column: auto;\n align-items: center;\n gap: var(--dimension-space-100);\n height: 100%;\n min-width: 0;\n padding: var(--dimension-space-100);\n border-block-end: none;\n box-sizing: border-box;\n}\n\n:host(.card-overview--compact) .card-overview__period {\n display: flex;\n flex: 1 1 auto;\n flex-direction: row;\n align-items: center;\n gap: var(--dimension-space-050);\n min-width: 0;\n min-height: var(--ds-control-height);\n padding-inline: var(--ds-control-padding-inline);\n overflow: hidden;\n text-align: start;\n white-space: nowrap;\n}\n\n:host(.card-overview--compact) .card-overview__period-current,\n:host(.card-overview--compact) .card-overview__period-comparison {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n:host(.card-overview--compact) .card-overview__period-current {\n flex: 0 1 auto;\n}\n\n:host(.card-overview--compact) .card-overview__period-comparison {\n flex: 1 1 auto;\n}\n\n:host(.card-overview--compact) .card-overview__filter {\n display: flex;\n flex: 0 0 auto;\n align-items: center;\n padding: 0;\n}\n",
369
+ "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/* The summary bar follows the theme-reactive inverted sub-theme. */\n\n:host {\n display: block;\n box-sizing: border-box;\n /* Drives the narrow layout below from the card's own width, not the viewport's. */\n container-type: inline-size;\n /* A summary is read, not copied; stray selection while clicking cells is noise. */\n -webkit-user-select: none;\n user-select: none;\n /* Width a metric may shrink to before the grid drops a column. */\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-inverted-border-tertiary);\n --ds-card-overview-divider: var(--ds-card-overview-divider-width) solid\n var(--ds-card-overview-divider-color);\n color: var(--color-inverted-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-050);\n background: var(--color-inverted-background);\n color: var(--color-inverted-foreground-primary);\n overflow-anchor: none;\n}\n\n/*\n * The visual surface owns elevation outside this clipping viewport. Keeping\n * those layers separate lets a page shrink the surface while content moves and\n * clips inside it without cutting off the shadow or inset highlight.\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__layout {\n transform: translateY(calc(-1 * var(--ds-card-overview-content-offset)));\n will-change: transform;\n}\n\n.card-overview__layout {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n grid-template-columns: minmax(0, 1fr);\n align-items: stretch;\n min-width: 0;\n}\n\n.card-overview__layout--has-score {\n grid-template-columns: auto minmax(0, 1fr);\n}\n\n/*\n * Transparent at regular widths so the score and header participate in the\n * inner layout grid directly. At the narrow breakpoint it becomes the unified\n * summary block above the metrics.\n */\n.card-overview__summary {\n display: contents;\n}\n\n/* ── Score ────────────────────────────────────────────────────────────────── */\n\n/*\n * Hugs its content and never shrinks, so the headline figure keeps its size and\n * the metric grid absorbs the remaining width.\n */\n/*\n * Top aligned rather than centred, and padded to match the header, so the score\n * label sits in the same 32px box at the same offset as the period text beside\n * it. Both use the same type variant, so their baselines land together.\n */\n.card-overview__score {\n display: flex;\n grid-row: 1 / span 2;\n grid-column: 1;\n flex: 0 0 auto;\n flex-direction: column;\n gap: 0;\n min-width: 0;\n padding: var(--dimension-space-100) var(--dimension-space-200);\n border-inline-end: var(--ds-card-overview-divider);\n}\n\n.card-overview__score--resolved {\n min-inline-size: max-content;\n}\n\n/* The control-height box that aligns this label with the period text. */\n.card-overview__score-label-row {\n display: flex;\n align-items: center;\n min-height: var(--ds-control-height);\n}\n\n/*\n * The score is its own column, so its rows cannot baseline-align with the measure\n * rows through flex or grid — those only align items that share a line. These two\n * offsets pull the column onto the measure baselines instead.\n *\n * They are compensation for how much lower a display-scale figure sits in its line\n * box than the body text it aligns to, so they are coupled to the type pairing:\n * change the score or measure variants and both need re-measuring.\n */\n.card-overview__score-figure {\n display: flex;\n align-items: baseline;\n gap: var(--dimension-space-050);\n min-width: 0;\n margin-block-start: calc(-1 * (var(--dimension-space-150) + var(--dimension-space-012)));\n}\n\n.card-overview__score-value {\n flex: 0 0 auto;\n}\n\n/* Second half of the baseline compensation described above the score figure. */\n.card-overview__score-band {\n margin-block-start: calc(-1 * var(--dimension-space-025));\n}\n\n/*\n * A skeleton bar is a solid block, so in a baseline-aligned row it aligns on its\n * bottom edge while the text it stands in for aligns on its baseline. Next to a\n * display-scale figure that difference is the descent under the figure's\n * baseline, which drops the bar below where the resolved trend sits.\n *\n * The offset is applied with relative positioning rather than a margin: a margin\n * is not part of a synthesized baseline, so it grows the figure instead of moving\n * the bar, which then pushes the band below it out of place.\n *\n * Only needed beside the score figure; the measure rows pair body variants whose\n * bottom edge and baseline already agree.\n */\n.card-overview__skeleton-baseline {\n position: relative;\n inset-block-start: calc(-1 * var(--dimension-space-150));\n}\n\n.card-overview__score--error {\n flex-direction: row;\n align-items: center;\n gap: var(--dimension-space-050);\n color: var(--color-inverted-foreground-negative);\n}\n\n/* ── Body ─────────────────────────────────────────────────────────────────── */\n\n/*\n * Takes the remaining inline space. The minimum is what makes the host wrap: once\n * the score and roughly two measure columns no longer fit side by side, the body\n * drops below the score instead of squeezing it. `min()` keeps that from becoming\n * an overflow floor on very narrow viewports.\n */\n.card-overview__body {\n display: flex;\n grid-row: 2;\n grid-column: 1;\n flex-direction: column;\n min-width: 0;\n}\n\n.card-overview__body--has-score {\n grid-column: 2;\n}\n\n/*\n * One control row: 8px of padding on every side around a 32px md control, so the\n * bar is 8 + 32 + 8. Both ends of the bar are control-shaped boxes — the period\n * text on the left and the slotted control on the right — so the row reads\n * balanced rather than text on one side and a control on the other.\n */\n.card-overview__header {\n display: flex;\n grid-row: 1;\n grid-column: 1;\n align-items: center;\n gap: var(--dimension-space-100);\n min-width: 0;\n padding: var(--dimension-space-100);\n border-block-end: var(--ds-card-overview-divider);\n box-sizing: border-box;\n}\n\n.card-overview__header--has-score {\n grid-column: 2;\n}\n\n/*\n * Matches the control's box on the opposite end, including its inline padding, so\n * the text lands at the same optical inset the slotted control does:\n * 8 from the bar + 6 from this box + 2 from the label inset = 16.\n *\n * Nowrap keeps the row at the control height; the period truncates rather than\n * growing the bar to two lines.\n */\n.card-overview__period {\n display: flex;\n flex: 1 1 auto;\n align-items: center;\n gap: var(--dimension-space-050);\n min-height: var(--ds-control-height);\n min-width: 0;\n padding-inline: var(--ds-control-padding-inline);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The period control is slotted, so only reserve space for it. */\n.card-overview__filter {\n display: flex;\n flex: 0 0 auto;\n align-items: center;\n}\n\n/* ── Metrics ──────────────────────────────────────────────────────────────── */\n\n/*\n * `auto-fit` gives every occupied track an equal share while all metrics fit on\n * one row. Once wrapping creates another row, its cells stay on those same\n * tracks, so a partial final row leaves empty space instead of stretching one\n * measure wider than its peers.\n *\n * Dividers are painted outward from each cell and clipped by this grid. That\n * leaves only the lines between cells: anything emitted from the final column or\n * final row lands outside the grid and is clipped without needing to detect the\n * number of tracks.\n *\n * `flex: 1 1 auto` makes the grid absorb whatever height the body has\n * left, so cells reach the card bottom and their hover wash has no dead strip.\n */\n.card-overview__metrics {\n display: grid;\n flex: 1 1 auto;\n grid-template-columns: repeat(\n 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/*\n * Square by design. Cells tile edge to edge against their dividers, so a radius\n * would break the seam; the card's own radius clips the outer corners instead.\n *\n * Padding is plain rather than control-derived: a cell is a block of content, not\n * a control box that has to optically match something on the far side.\n */\n.card-overview__metric {\n display: flex;\n position: relative;\n flex-direction: column;\n gap: var(--dimension-space-050);\n min-width: 0;\n padding: var(--dimension-space-175) var(--dimension-space-200);\n border-radius: 0;\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}\n\n.card-overview__metric[role='button'] {\n cursor: default;\n -webkit-user-select: none;\n user-select: none;\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/* ── Trend ────────────────────────────────────────────────────────────────── */\n\n/*\n * Tone arrives resolved from the caller: the arrow direction and the sentiment\n * are independent, so a falling metric can read positive or neutral. The arrow is\n * a glyph inside this text node, so it always matches the surrounding weight.\n */\n.card-overview__trend {\n flex: 0 0 auto;\n white-space: nowrap;\n}\n\n/* ── Narrow layout ────────────────────────────────────────────────────────── */\n\n/*\n * Below roughly two measure columns the card becomes a full stack. The summary\n * wrapper owns one block above the metrics: score label, figure, and trend stay\n * together at the start; the period control sits at the upper end; and the period\n * copy follows the score instead of becoming a separate divided row.\n */\n/*\n * Selectors are host-qualified purely for specificity: the compiled sheet places\n * these rules ahead of the base ones, so a plain class here would only ever add\n * properties and never override them.\n */\n@container (max-width: 480px) {\n :host .card-overview__layout {\n grid-template-rows: auto minmax(0, 1fr);\n grid-template-columns: minmax(0, 1fr);\n }\n\n :host .card-overview__summary {\n display: grid;\n grid-row: 1;\n grid-column: 1;\n grid-template-columns: minmax(0, 1fr) auto;\n grid-template-rows: auto;\n border-block-end: var(--ds-card-overview-divider);\n }\n\n :host .card-overview__score {\n display: grid;\n grid-row: 1;\n grid-column: 1;\n grid-template-rows: auto auto;\n grid-template-columns: auto auto;\n justify-content: start;\n column-gap: var(--dimension-space-100);\n width: auto;\n padding-block-end: var(--dimension-space-100);\n padding-inline-end: var(--dimension-space-100);\n border-inline-end: none;\n }\n\n :host .card-overview__score-label-row {\n grid-row: 1;\n grid-column: 1 / -1;\n min-height: var(--ds-control-height);\n }\n\n /*\n * The wide score uses optical offsets to meet the neighbouring metric\n * baselines. In the stacked summary those neighbours are gone, so natural\n * document flow keeps the label, figure, band, and period clearly ordered.\n */\n :host .card-overview__score-band,\n :host .card-overview__score-figure {\n grid-row: 2;\n margin-block-start: 0;\n }\n\n :host .card-overview__score-figure {\n grid-column: 1;\n align-self: baseline;\n gap: var(--dimension-space-100);\n }\n\n :host .card-overview__score-band {\n grid-column: 2;\n align-self: baseline;\n display: none;\n }\n\n :host .card-overview__header {\n display: grid;\n grid-row: 1;\n grid-column: 2;\n grid-template-rows: auto auto;\n align-content: start;\n gap: 0;\n padding: 0;\n border-block-end: none;\n }\n\n :host .card-overview__filter {\n grid-row: 1;\n grid-column: 1;\n align-self: start;\n justify-self: end;\n padding: var(--dimension-space-100) var(--dimension-space-100) 0 0;\n }\n\n :host .card-overview__period {\n grid-row: 2;\n grid-column: 1;\n flex-direction: column;\n align-items: flex-end;\n justify-self: stretch;\n gap: 0;\n min-height: 0;\n padding-block: var(--dimension-space-150) var(--dimension-space-100);\n padding-inline: var(--dimension-space-100) var(--dimension-space-200);\n overflow: visible;\n text-align: end;\n white-space: nowrap;\n }\n\n :host .card-overview__body {\n grid-row: 2;\n grid-column: 1;\n }\n}\n\n/*\n * Page shells may enter a responsive mode before this card itself becomes\n * narrow. The explicit stacked layout applies the same composition without\n * coupling CardOverview to a viewport breakpoint.\n */\n:host(.card-overview--stacked) .card-overview__layout {\n grid-template-rows: auto minmax(0, 1fr);\n grid-template-columns: minmax(0, 1fr);\n}\n\n:host(.card-overview--stacked) .card-overview__summary {\n display: grid;\n grid-row: 1;\n grid-column: 1;\n grid-template-columns: minmax(0, 1fr) auto;\n grid-template-rows: auto;\n border-block-end: var(--ds-card-overview-divider);\n}\n\n:host(.card-overview--stacked) .card-overview__score {\n display: grid;\n grid-row: 1;\n grid-column: 1;\n grid-template-rows: auto auto;\n grid-template-columns: auto auto;\n justify-content: start;\n column-gap: var(--dimension-space-100);\n width: auto;\n padding-block-end: var(--dimension-space-100);\n padding-inline-end: var(--dimension-space-100);\n border-inline-end: none;\n}\n\n:host(.card-overview--stacked) .card-overview__score-label-row {\n grid-row: 1;\n grid-column: 1 / -1;\n min-height: var(--ds-control-height);\n}\n\n:host(.card-overview--stacked) .card-overview__score-band,\n:host(.card-overview--stacked) .card-overview__score-figure {\n grid-row: 2;\n margin-block-start: 0;\n}\n\n:host(.card-overview--stacked) .card-overview__score-figure {\n grid-column: 1;\n align-self: baseline;\n gap: var(--dimension-space-100);\n}\n\n:host(.card-overview--stacked) .card-overview__score-band {\n grid-column: 2;\n align-self: baseline;\n display: none;\n}\n\n:host(.card-overview--stacked) .card-overview__header {\n display: grid;\n grid-row: 1;\n grid-column: 2;\n grid-template-rows: auto auto;\n align-content: start;\n gap: 0;\n padding: 0;\n border-block-end: none;\n}\n\n:host(.card-overview--stacked) .card-overview__filter {\n grid-row: 1;\n grid-column: 1;\n align-self: start;\n justify-self: end;\n padding: var(--dimension-space-100) var(--dimension-space-100) 0 0;\n}\n\n:host(.card-overview--stacked) .card-overview__period {\n grid-row: 2;\n grid-column: 1;\n flex-direction: column;\n align-items: flex-end;\n justify-self: stretch;\n gap: 0;\n min-height: 0;\n padding-block: var(--dimension-space-150) var(--dimension-space-100);\n padding-inline: var(--dimension-space-100) var(--dimension-space-200);\n overflow: visible;\n text-align: end;\n white-space: nowrap;\n}\n\n:host(.card-overview--stacked) .card-overview__body {\n grid-row: 2;\n grid-column: 1;\n}\n\n:host(.card-overview--stacked) .card-overview__metrics {\n grid-template-columns: minmax(0, 1fr);\n}\n\n/* ── Compact presentation ───────────────────────────────────────────────── */\n\n/*\n * Matches the shared 48px bar contract: one 32px control row with 8px above\n * and below. The score remains a distinct leading cell, while reporting copy\n * absorbs the available width and the application-owned control never shrinks.\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 height: 100%;\n}\n\n:host(.card-overview--compact) .card-overview__layout,\n:host(.card-overview--compact) .card-overview__summary {\n display: flex;\n width: 100%;\n height: 100%;\n min-width: 0;\n}\n\n:host(.card-overview--compact) .card-overview__score {\n display: flex;\n flex: 0 0 auto;\n flex-direction: row;\n align-items: center;\n gap: var(--dimension-space-100);\n width: auto;\n height: 100%;\n min-width: 0;\n padding: var(--dimension-space-100) var(--dimension-space-200);\n border-inline-end: var(--ds-card-overview-divider);\n box-sizing: border-box;\n}\n\n:host(.card-overview--compact) .card-overview__score-label-row {\n display: contents;\n min-height: 0;\n}\n\n:host(.card-overview--compact) .card-overview__score-label,\n:host(.card-overview--compact) .card-overview__score-figure,\n:host(.card-overview--compact) .card-overview__trend {\n flex: 0 0 auto;\n}\n\n:host(.card-overview--compact) .card-overview__score-figure {\n display: flex;\n align-self: center;\n align-items: baseline;\n gap: var(--dimension-space-050);\n margin-block-start: 0;\n}\n\n:host(.card-overview--compact) .card-overview__score-band {\n display: none;\n}\n\n:host(.card-overview--compact) .card-overview__header {\n display: flex;\n flex: 1 1 auto;\n grid-row: auto;\n grid-column: auto;\n align-items: center;\n gap: var(--dimension-space-100);\n height: 100%;\n min-width: 0;\n padding: var(--dimension-space-100);\n border-block-end: none;\n box-sizing: border-box;\n}\n\n:host(.card-overview--compact) .card-overview__period {\n display: flex;\n flex: 1 1 auto;\n flex-direction: row;\n align-items: center;\n gap: var(--dimension-space-050);\n min-width: 0;\n min-height: var(--ds-control-height);\n padding-inline: var(--ds-control-padding-inline);\n overflow: hidden;\n text-align: start;\n white-space: nowrap;\n}\n\n:host(.card-overview--compact) .card-overview__period-current,\n:host(.card-overview--compact) .card-overview__period-comparison {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n:host(.card-overview--compact) .card-overview__period-current {\n flex: 0 1 auto;\n}\n\n:host(.card-overview--compact) .card-overview__period-comparison {\n flex: 1 1 auto;\n}\n\n:host(.card-overview--compact) .card-overview__filter {\n display: flex;\n flex: 0 0 auto;\n align-items: center;\n padding: 0;\n}\n",
370
370
  "type": "registry:ui"
371
371
  },
372
372
  {
373
373
  "path": "src/wc/components/CardOverview/CardOverview.tsx",
374
- "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 resolveOverviewRovingIndex,\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 = 6;\n\nconst TREND_COLORS: Record<MetricTrend['tone'], TextColor> = {\n // CardOverview is permanently always-dark. Reference-level dark-surface\n // intent tokens avoid theme overrides and retain AA contrast at body size.\n positive: 'var(--color-reference-dark-green-145-l70-c19-bold)',\n negative: 'var(--color-reference-dark-red-30-l70-c20-bold)',\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 /** Leading summary block. Omit to render the bar without a headline figure. */\n @Prop() score: OverviewScore | undefined;\n\n /** Current reporting period, for example `Jun 29, 2026 – Jul 26, 2026`. */\n @Prop() periodLabel: string = '';\n\n /** Comparison caption, for example `vs Previous period`. */\n @Prop() comparisonLabel: string = '';\n\n /** Measures rendered in the responsive grid. Only the first six 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 * reflows and then stacks from this alone, so no measurement is required.\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 elevation on the shrinking surface, and clips translated\n * content internally. The page still owns sticky positioning and the final\n * swap to `variant=\"compact\"`.\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\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 }\n\n disconnectedCallback() {\n this.layoutResizeObserver?.disconnect();\n this.layoutResizeObserver = null;\n }\n\n private get hasScore(): boolean {\n return this.score !== undefined || this.scoreErrorMessage !== undefined || this.isLoading;\n }\n\n private get visibleMetrics(): OverviewMetric[] {\n return this.metrics.slice(0, MAX_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 updateHeight = (height: number) => {\n if (height > 0 && Math.abs(height - this.expandedHeight) >= 0.5) {\n this.expandedHeight = height;\n }\n };\n\n updateHeight(layout.getBoundingClientRect().height);\n if (typeof ResizeObserver === 'undefined') return;\n\n this.layoutResizeObserver?.disconnect();\n this.layoutResizeObserver = new ResizeObserver(entries => {\n const height = entries[0]?.contentRect.height;\n if (height !== undefined) updateHeight(height);\n });\n this.layoutResizeObserver.observe(layout);\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');\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: the default base is the theme's quaternary\n * foreground, which is black and therefore invisible on this permanently dark\n * surface.\n */\n private bar(textVariant: TextVariant, width: string, className?: string) {\n return (\n <ds-skeleton\n class={className}\n variant=\"text\"\n textVariant={textVariant}\n width={width}\n background=\"always-dark\"\n />\n );\n }\n\n /**\n * Arrow and change in one text node.\n *\n * The arrow is a typeface glyph rather than an icon so it inherits the exact\n * size, weight, and baseline of the value beside it; an icon would need its\n * own size scale and would drift whenever the type scale changed.\n *\n * Tone is supplied by the caller and never inferred here.\n */\n private renderTrend(trend: MetricTrend | undefined, variant: TextVariant, emphasis = false) {\n if (!trend) return null;\n return (\n <ds-text\n as=\"span\"\n class={{\n 'card-overview__trend': true,\n [`card-overview__trend--${trend.tone}`]: true,\n }}\n variant={variant}\n emphasis={emphasis}\n color={TREND_COLORS[trend.tone]}\n fontFeature=\"tabular-nums\"\n >\n {trend.direction === 'up' ? '↑' : '↓'} {trend.value}\n </ds-text>\n );\n }\n\n private renderScore(compact: boolean) {\n if (this.isLoading) {\n if (compact) {\n return (\n <div class=\"card-overview__score ds-control--md\" part=\"score\">\n {this.bar('text-body-medium', '40px', 'card-overview__score-label')}\n <div class=\"card-overview__score-figure\">\n {this.bar('text-body-medium', '24px')}\n {this.bar('text-body-medium', '32px')}\n </div>\n </div>\n );\n }\n\n return (\n <div class=\"card-overview__score ds-control--md\" part=\"score\">\n {/*\n * The label row is kept but left empty. It carries the control-height\n * box that aligns the score block with the period control, so dropping\n * it would shift the whole block up; a bar inside it would collide with\n * the figure, which sits under the label by design.\n */}\n <div class=\"card-overview__score-label-row\" />\n <div class=\"card-overview__score-figure\">\n {this.bar('text-display-medium', '56px')}\n {this.bar('text-body-small', '24px', 'card-overview__skeleton-baseline')}\n </div>\n {/*\n * Class goes on the skeleton itself, matching the resolved band. In a\n * wrapper the bar picks up inline leading and drops below the resolved\n * band position.\n */}\n {this.bar(\n 'text-caption',\n '80px',\n 'card-overview__score-band card-overview__score-band--loading'\n )}\n </div>\n );\n }\n\n if (this.scoreErrorMessage) {\n return (\n <div class=\"card-overview__score card-overview__score--error\" part=\"score\" role=\"alert\">\n <ds-icon name=\"CircleExclamation\" size=\"sm\" color=\"inherit\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n {this.scoreErrorMessage}\n </ds-text>\n </div>\n );\n }\n\n const score = this.score;\n if (!score) return null;\n\n return (\n <div class=\"card-overview__score card-overview__score--resolved ds-control--md\" part=\"score\">\n {/* The row owns cross-layout baseline geometry and becomes display: contents in compact mode. */}\n {/* eslint-disable-next-line local/prefer-direct-ds-text */}\n <div class=\"card-overview__score-label-row\">\n <ds-text\n as=\"span\"\n class=\"card-overview__score-label\"\n variant=\"text-body-medium\"\n emphasis={!compact}\n color={ALWAYS_DARK_SECONDARY}\n >\n {score.label}\n </ds-text>\n </div>\n <div class=\"card-overview__score-figure\">\n <ds-text\n as=\"span\"\n class=\"card-overview__score-value\"\n variant={compact ? 'text-body-medium' : 'text-display-medium'}\n emphasis\n color={ALWAYS_DARK_PRIMARY}\n fontFeature=\"tabular-nums\"\n >\n {score.value}\n </ds-text>\n {this.renderTrend(\n score.trend,\n compact ? 'text-body-medium' : 'text-body-small',\n !compact\n )}\n </div>\n {score.band && !compact && (\n <ds-text\n as=\"span\"\n class=\"card-overview__score-band\"\n variant=\"text-caption\"\n color={ALWAYS_DARK_SECONDARY}\n >\n {score.band}\n </ds-text>\n )}\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\"\n variant=\"text-body-small\"\n color={ALWAYS_DARK_SECONDARY}\n >\n {metric.label}\n </ds-text>\n );\n\n return (\n <div\n class={{\n 'card-overview__metric': true,\n 'card-overview__metric--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 // The card is permanently dark, so interaction tokens come from that family.\n 'ds-interaction-fill--on-always-dark': selectable,\n 'ds-focus-ring-inset': selectable,\n }}\n part=\"metric\"\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 {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\"\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 );\n }\n\n private renderMetrics() {\n if (this.isLoading && this.metrics.length === 0) {\n return Array.from({ length: LOADING_PLACEHOLDER_COUNT }, (_, index) => (\n <div class=\"card-overview__metric\" key={`loading-${index}`}>\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 ));\n }\n\n return this.visibleMetrics.map((metric, index) => this.renderMetric(metric, index));\n }\n\n render() {\n const hasHeader = Boolean(this.periodLabel || this.comparisonLabel);\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--has-score': this.hasScore,\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-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 'card-overview__layout--has-score': this.hasScore,\n }}\n >\n <div class=\"card-overview__summary\">\n {this.hasScore && this.renderScore(compact)}\n\n {/* `ds-control--md` supplies the 32px height and the label inset below. */}\n <div\n class={{\n 'card-overview__header': true,\n 'card-overview__header--has-score': this.hasScore,\n 'ds-control--md': true,\n }}\n >\n {this.isLoading ? (\n <div class=\"card-overview__period ds-control-label-box\">\n {this.bar('text-body-medium', '184px')}\n {this.bar('text-body-medium', '248px')}\n </div>\n ) : (\n hasHeader && (\n <div class=\"card-overview__period ds-control-label-box\">\n {this.periodLabel && (\n <ds-text\n as=\"span\"\n class=\"card-overview__period-current ds-control-label-box\"\n variant=\"text-body-medium\"\n emphasis\n color={ALWAYS_DARK_PRIMARY}\n >\n {this.periodLabel}\n </ds-text>\n )}\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 )}\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\n class={{\n 'card-overview__body': true,\n 'card-overview__body--has-score': this.hasScore,\n }}\n >\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",
374
+ "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 resolveOverviewRovingIndex,\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 = 6;\n\nconst TREND_COLORS: Record<MetricTrend['tone'], TextColor> = {\n positive: 'var(--color-inverted-foreground-positive)',\n negative: 'var(--color-inverted-foreground-negative)',\n neutral: 'var(--color-inverted-foreground-secondary)',\n};\n\nconst INVERTED_PRIMARY: TextColor = 'var(--color-inverted-foreground-primary)';\nconst INVERTED_SECONDARY: TextColor = 'var(--color-inverted-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 /** Leading summary block. Omit to render the bar without a headline figure. */\n @Prop() score: OverviewScore | undefined;\n\n /** Current reporting period, for example `Jun 29, 2026 – Jul 26, 2026`. */\n @Prop() periodLabel: string = '';\n\n /** Comparison caption, for example `vs Previous period`. */\n @Prop() comparisonLabel: string = '';\n\n /** Measures rendered in the responsive grid. Only the first six 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 * reflows and then stacks from this alone, so no measurement is required.\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 elevation on the shrinking surface, and clips translated\n * content internally. The page still owns sticky positioning and the final\n * swap to `variant=\"compact\"`.\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\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 }\n\n disconnectedCallback() {\n this.layoutResizeObserver?.disconnect();\n this.layoutResizeObserver = null;\n }\n\n private get hasScore(): boolean {\n return this.score !== undefined || this.scoreErrorMessage !== undefined || this.isLoading;\n }\n\n private get visibleMetrics(): OverviewMetric[] {\n return this.metrics.slice(0, MAX_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 updateHeight = (height: number) => {\n if (height > 0 && Math.abs(height - this.expandedHeight) >= 0.5) {\n this.expandedHeight = height;\n }\n };\n\n updateHeight(layout.getBoundingClientRect().height);\n if (typeof ResizeObserver === 'undefined') return;\n\n this.layoutResizeObserver?.disconnect();\n this.layoutResizeObserver = new ResizeObserver(entries => {\n const height = entries[0]?.contentRect.height;\n if (height !== undefined) updateHeight(height);\n });\n this.layoutResizeObserver.observe(layout);\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');\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 * inverted 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=\"inverted\"\n />\n );\n }\n\n /**\n * Arrow and change in one text node.\n *\n * The arrow is a typeface glyph rather than an icon so it inherits the exact\n * size, weight, and baseline of the value beside it; an icon would need its\n * own size scale and would drift whenever the type scale changed.\n *\n * Tone is supplied by the caller and never inferred here.\n */\n private renderTrend(trend: MetricTrend | undefined, variant: TextVariant, emphasis = false) {\n if (!trend) return null;\n return (\n <ds-text\n as=\"span\"\n class={{\n 'card-overview__trend': true,\n [`card-overview__trend--${trend.tone}`]: true,\n }}\n variant={variant}\n emphasis={emphasis}\n color={TREND_COLORS[trend.tone]}\n fontFeature=\"tabular-nums\"\n >\n {trend.direction === 'up' ? '↑' : '↓'} {trend.value}\n </ds-text>\n );\n }\n\n private renderScore(compact: boolean) {\n if (this.isLoading) {\n if (compact) {\n return (\n <div class=\"card-overview__score ds-control--md\" part=\"score\">\n {this.bar('text-body-medium', '40px', 'card-overview__score-label')}\n <div class=\"card-overview__score-figure\">\n {this.bar('text-body-medium', '24px')}\n {this.bar('text-body-medium', '32px')}\n </div>\n </div>\n );\n }\n\n return (\n <div class=\"card-overview__score ds-control--md\" part=\"score\">\n {/*\n * The label row is kept but left empty. It carries the control-height\n * box that aligns the score block with the period control, so dropping\n * it would shift the whole block up; a bar inside it would collide with\n * the figure, which sits under the label by design.\n */}\n <div class=\"card-overview__score-label-row\" />\n <div class=\"card-overview__score-figure\">\n {this.bar('text-display-medium', '56px')}\n {this.bar('text-body-small', '24px', 'card-overview__skeleton-baseline')}\n </div>\n {/*\n * Class goes on the skeleton itself, matching the resolved band. In a\n * wrapper the bar picks up inline leading and drops below the resolved\n * band position.\n */}\n {this.bar(\n 'text-caption',\n '80px',\n 'card-overview__score-band card-overview__score-band--loading'\n )}\n </div>\n );\n }\n\n if (this.scoreErrorMessage) {\n return (\n <div class=\"card-overview__score card-overview__score--error\" part=\"score\" role=\"alert\">\n <ds-icon name=\"CircleExclamation\" size=\"sm\" color=\"inherit\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n {this.scoreErrorMessage}\n </ds-text>\n </div>\n );\n }\n\n const score = this.score;\n if (!score) return null;\n\n return (\n <div class=\"card-overview__score card-overview__score--resolved ds-control--md\" part=\"score\">\n {/* The row owns cross-layout baseline geometry and becomes display: contents in compact mode. */}\n {/* eslint-disable-next-line local/prefer-direct-ds-text */}\n <div class=\"card-overview__score-label-row\">\n <ds-text\n as=\"span\"\n class=\"card-overview__score-label\"\n variant=\"text-body-medium\"\n emphasis={!compact}\n color={INVERTED_SECONDARY}\n >\n {score.label}\n </ds-text>\n </div>\n <div class=\"card-overview__score-figure\">\n <ds-text\n as=\"span\"\n class=\"card-overview__score-value\"\n variant={compact ? 'text-body-medium' : 'text-display-medium'}\n emphasis\n color={INVERTED_PRIMARY}\n fontFeature=\"tabular-nums\"\n >\n {score.value}\n </ds-text>\n {this.renderTrend(\n score.trend,\n compact ? 'text-body-medium' : 'text-body-small',\n !compact\n )}\n </div>\n {score.band && !compact && (\n <ds-text\n as=\"span\"\n class=\"card-overview__score-band\"\n variant=\"text-caption\"\n color={INVERTED_SECONDARY}\n >\n {score.band}\n </ds-text>\n )}\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\"\n variant=\"text-body-small\"\n color={INVERTED_SECONDARY}\n >\n {metric.label}\n </ds-text>\n );\n\n return (\n <div\n class={{\n 'card-overview__metric': true,\n 'card-overview__metric--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-inverted': selectable,\n 'ds-focus-ring-inset': selectable,\n }}\n part=\"metric\"\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 {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\"\n variant=\"text-body-medium\"\n color={INVERTED_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 );\n }\n\n private renderMetrics() {\n if (this.isLoading && this.metrics.length === 0) {\n return Array.from({ length: LOADING_PLACEHOLDER_COUNT }, (_, index) => (\n <div class=\"card-overview__metric\" key={`loading-${index}`}>\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 ));\n }\n\n return this.visibleMetrics.map((metric, index) => this.renderMetric(metric, index));\n }\n\n render() {\n const hasHeader = Boolean(this.periodLabel || this.comparisonLabel);\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--has-score': this.hasScore,\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-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 'card-overview__layout--has-score': this.hasScore,\n }}\n >\n <div class=\"card-overview__summary\">\n {this.hasScore && this.renderScore(compact)}\n\n {/* `ds-control--md` supplies the 32px height and the label inset below. */}\n <div\n class={{\n 'card-overview__header': true,\n 'card-overview__header--has-score': this.hasScore,\n 'ds-control--md': true,\n }}\n >\n {this.isLoading ? (\n <div class=\"card-overview__period ds-control-label-box\">\n {this.bar('text-body-medium', '184px')}\n {this.bar('text-body-medium', '248px')}\n </div>\n ) : (\n hasHeader && (\n <div class=\"card-overview__period ds-control-label-box\">\n {this.periodLabel && (\n <ds-text\n as=\"span\"\n class=\"card-overview__period-current ds-control-label-box\"\n variant=\"text-body-medium\"\n emphasis\n color={INVERTED_PRIMARY}\n >\n {this.periodLabel}\n </ds-text>\n )}\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={INVERTED_SECONDARY}\n >\n {this.comparisonLabel}\n </ds-text>\n )}\n </div>\n )\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=\"inverted\"\n />\n ) : (\n <slot name=\"filter\" />\n )}\n </div>\n </div>\n </div>\n\n {!compact && (\n <div\n class={{\n 'card-overview__body': true,\n 'card-overview__body--has-score': this.hasScore,\n }}\n >\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",
375
375
  "type": "registry:ui"
376
376
  },
377
377
  {
@@ -249,7 +249,7 @@
249
249
  },
250
250
  "peerDependencies": {
251
251
  "required": [
252
- "@ds-mo/tokens ^5.0.0",
252
+ "@ds-mo/tokens ^5.1.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."
@@ -218,7 +218,7 @@
218
218
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: categories, data, series.",
219
219
  "peerDependencies": {
220
220
  "required": [
221
- "@ds-mo/tokens ^5.0.0",
221
+ "@ds-mo/tokens ^5.1.0",
222
222
  "@ds-mo/icons ^6.4.0"
223
223
  ],
224
224
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -315,7 +315,7 @@
315
315
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: data.",
316
316
  "peerDependencies": {
317
317
  "required": [
318
- "@ds-mo/tokens ^5.0.0",
318
+ "@ds-mo/tokens ^5.1.0",
319
319
  "@ds-mo/icons ^6.4.0"
320
320
  ],
321
321
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -332,12 +332,12 @@
332
332
  "files": [
333
333
  {
334
334
  "path": "src/wc/components/ChartDonut/ChartDonut.css",
335
- "content": ":host {\n display: inline-block;\n font-family: var(--typography-font-family);\n user-select: none;\n}\n\n:host(.chart-donut--fill) {\n display: block;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n}\n\n.chart-donut__wrapper {\n position: relative;\n overflow: visible;\n}\n\n:host(.chart-donut--fill) .chart-donut__wrapper {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.chart-donut__svg path {\n cursor: default;\n transition: opacity var(--effect-motion-short-2);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .chart-donut__svg path {\n transition: none;\n }\n}\n\n.chart-donut__svg path:focus-visible {\n outline: none;\n stroke: var(--color-interaction-focus);\n stroke-width: var(--dimension-stroke-width-025);\n vector-effect: non-scaling-stroke;\n}\n\n/* Matches ds-text text-title-large + emphasis (SVG cannot host ds-text). */\n.chart-donut__center-value {\n fill: var(--color-foreground-primary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-xl);\n line-height: var(--typography-lineheight-xl);\n font-weight: var(--typography-weight-semibold);\n letter-spacing: var(--typography-letterspacing-negative-double);\n cursor: default;\n pointer-events: none;\n}\n\n/* Matches ds-text text-body-medium regular. */\n.chart-donut__center-caption {\n fill: var(--color-foreground-secondary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n cursor: default;\n pointer-events: none;\n}\n",
335
+ "content": ":host {\n display: inline-block;\n font-family: var(--typography-font-family);\n user-select: none;\n}\n\n:host(.chart-donut--fill) {\n display: block;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n}\n\n.chart-donut__wrapper {\n position: relative;\n overflow: visible;\n}\n\n:host(.chart-donut--fill) .chart-donut__wrapper {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.chart-donut__svg path {\n cursor: default;\n transition: opacity var(--effect-motion-short-2);\n}\n\n.chart-donut__slice--dimmed {\n opacity: var(--effect-opacity-medium);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .chart-donut__svg path {\n transition: none;\n }\n}\n\n.chart-donut__svg path:focus-visible {\n outline: none;\n stroke: var(--color-interaction-focus);\n stroke-width: var(--dimension-stroke-width-025);\n vector-effect: non-scaling-stroke;\n}\n\n/* Matches ds-text text-title-large + emphasis (SVG cannot host ds-text). */\n.chart-donut__center-value {\n fill: var(--color-foreground-primary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-xl);\n line-height: var(--typography-lineheight-xl);\n font-weight: var(--typography-weight-semibold);\n letter-spacing: var(--typography-letterspacing-negative-double);\n cursor: default;\n pointer-events: none;\n}\n\n/* Matches ds-text text-body-medium regular. */\n.chart-donut__center-caption {\n fill: var(--color-foreground-secondary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n cursor: default;\n pointer-events: none;\n}\n",
336
336
  "type": "registry:ui"
337
337
  },
338
338
  {
339
339
  "path": "src/wc/components/ChartDonut/ChartDonut.tsx",
340
- "content": "import { Component, Prop, State, Event, EventEmitter, Element, Watch, h, Host } from '@stencil/core';\nimport { arc, pie } from 'd3-shape';\nimport { categoryColor } from '../../utils/chart-colors';\nimport { formatCompactNumber, resolveCssLengthPx, truncateSvgTextToWidth, TOKEN_DEFAULTS } from '../../utils';\nimport type { ChartDatum } from '../../utils/chart-types';\n\nconst DIMMED_OPACITY = 0.25;\n/** Gap between the center value and caption line boxes. */\nconst CENTER_GAP_PX = 0;\n/** Fraction of the inner-circle diameter center text is allowed to use before truncating. */\nconst CENTER_TEXT_SAFE_WIDTH_RATIO = 0.82;\n/** Fallback when filling before the first ResizeObserver measurement (matches fill min). */\nconst FILL_FALLBACK_PX = 128;\n\n@Component({\n tag: 'ds-chart-donut',\n styleUrl: 'ChartDonut.css',\n scoped: true,\n})\nexport class ChartDonut {\n @Prop() locale: string | undefined;\n @Prop() noDataLabel: string = 'No data';\n @Element() el!: HTMLElement;\n\n /** Slices to render. Set as a JS property (not an HTML attribute). */\n @Prop() data: ChartDatum[] = [];\n /**\n * Explicit diameter in px. When unset, the donut sizes to its container\n * (ResizeObserver) clamped between `--dimension-size-base * 16` (128px) and\n * `* 24` (192px), and stays centered in the leftover space. Prefer unset\n * inside card layouts.\n */\n @Prop() size: number | undefined;\n /** Ring thickness — number (px) or TokoMo length. Defaults to `--dimension-size-200` (16px). */\n @Prop() thickness: number | string = TOKEN_DEFAULTS.size200;\n /** Corner radius on each slice — number (px) or TokoMo length. Defaults to `--dimension-radius-025` (2px). */\n @Prop() cornerRadius: number | string = TOKEN_DEFAULTS.radius025;\n /** Gap between slices, in degrees. */\n @Prop() gap: number = 1;\n /**\n * Show a chart-owned value callout for genuine pointer or keyboard focus.\n * Disable when a visible legend already owns persistent label and value detail.\n */\n @Prop() showTooltip: boolean = true;\n /** Primary center value; defaults to the sum of `data` values (e.g. \"187\", \"40.9%\"). */\n @Prop() centerValue: string | undefined;\n /** Secondary caption below the center value (e.g. \"Total\", \"Normal\"). Rendered uppercase. */\n @Prop() centerCaption: string | undefined;\n /**\n * Externally controlled highlight, matched by `label` — e.g. drive this from a sibling\n * `ds-chart-legend`'s `dsItemHover` event to keep chart and legend hover in sync.\n * Falls back to this component's own pointer/focus hover when unset.\n * Slice hover dims peer slices and emits `dsSliceHover` for legend sync. External\n * highlight never opens the local tooltip.\n */\n @Prop() activeLabel: string | null = null;\n\n @State() private hoveredLabel: string | null = null;\n /** Center text, truncated with an ellipsis if it doesn't fit inside the inner circle. */\n @State() private displayedValueText: string = '';\n @State() private displayedCaptionText: string = '';\n /** Measured fill diameter when `size` is unset. */\n @State() private measuredSize: number = FILL_FALLBACK_PX;\n @State() private tooltipX: number = 0;\n @State() private tooltipY: number = 0;\n\n /** Fires with the hovered/focused slice's datum, or `null` on leave/blur. */\n @Event() dsSliceHover!: EventEmitter<ChartDatum | null>;\n\n private valueTextEl?: SVGTextElement;\n private captionTextEl?: SVGTextElement;\n private resizeObserver: ResizeObserver | null = null;\n\n private get isFillMode(): boolean {\n return this.size == null || !Number.isFinite(this.size) || this.size <= 0;\n }\n\n private get fillMinPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.donutFillMin, FILL_FALLBACK_PX);\n }\n\n private get fillMaxPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.donutFillMax, 192);\n }\n\n /** Clamp available square side into the fill min/max band. */\n private clampFillSize(availablePx: number): number {\n const minPx = this.fillMinPx;\n const maxPx = this.fillMaxPx;\n if (!(availablePx > 0)) return minPx;\n return Math.round(Math.min(maxPx, Math.max(minPx, availablePx)));\n }\n\n private get resolvedSize(): number {\n if (!this.isFillMode) return this.size as number;\n return this.clampFillSize(this.measuredSize);\n }\n\n componentDidLoad() {\n this.syncResizeObserver();\n }\n\n @Watch('size')\n onSizePropChange() {\n this.syncResizeObserver();\n }\n\n disconnectedCallback() {\n this.teardownResizeObserver();\n }\n\n private syncResizeObserver() {\n this.teardownResizeObserver();\n if (!this.isFillMode) return;\n // Observe the host (which fills its parent), not the wrapper — the wrapper\n // only wraps the SVG and would collapse to the clamped size, locking fill at min.\n const target = this.el;\n if (!target || typeof ResizeObserver === 'undefined') return;\n\n this.resizeObserver = new ResizeObserver(entries => {\n const entry = entries[0];\n if (!entry) return;\n this.applyAvailableSize(entry.contentRect.width, entry.contentRect.height);\n });\n this.resizeObserver.observe(target);\n this.measureHost();\n }\n\n private measureHost() {\n const rect = this.el.getBoundingClientRect();\n this.applyAvailableSize(rect.width, rect.height);\n }\n\n private applyAvailableSize(width: number, height: number) {\n const available = Math.floor(Math.min(width, height));\n if (available > 0 && available !== this.measuredSize) {\n this.measuredSize = available;\n }\n }\n\n private teardownResizeObserver() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n }\n\n private handleHover(datum: ChartDatum | null) {\n this.hoveredLabel = datum?.label ?? null;\n this.dsSliceHover.emit(datum);\n }\n\n private get tooltipDatum(): ChartDatum | null {\n if (!this.showTooltip || this.hoveredLabel == null) return null;\n return this.data.find(datum => datum.label === this.hoveredLabel) ?? null;\n }\n\n private updateTooltipFromPointer(event: MouseEvent) {\n if (!this.showTooltip) return;\n const wrapper = this.el.querySelector<HTMLElement>('.chart-donut__wrapper');\n if (!wrapper) return;\n const rect = wrapper.getBoundingClientRect();\n this.tooltipX = event.clientX - rect.left;\n this.tooltipY = event.clientY - rect.top;\n }\n\n private updateTooltipFromSvgPoint(\n path: SVGPathElement,\n svgX: number,\n svgY: number,\n viewBoxSize: number,\n ) {\n if (!this.showTooltip) return;\n const wrapper = this.el.querySelector<HTMLElement>('.chart-donut__wrapper');\n const svg = path.ownerSVGElement;\n if (!wrapper || !svg) return;\n const wrapperRect = wrapper.getBoundingClientRect();\n const svgRect = svg.getBoundingClientRect();\n this.tooltipX = svgRect.left - wrapperRect.left + svgX * (svgRect.width / viewBoxSize);\n this.tooltipY = svgRect.top - wrapperRect.top + svgY * (svgRect.height / viewBoxSize);\n }\n\n componentDidRender() {\n // Re-bind after HMR when DidLoad's observer was dropped.\n if (this.isFillMode && !this.resizeObserver) {\n this.syncResizeObserver();\n }\n\n const size = this.resolvedSize;\n const radius = size / 2;\n const thicknessPx = resolveCssLengthPx(this.thickness, TOKEN_DEFAULTS.size200);\n const innerRadius = radius - thicknessPx;\n const maxWidth = innerRadius * 2 * CENTER_TEXT_SAFE_WIDTH_RATIO;\n const total = this.data.reduce((sum, d) => sum + d.value, 0);\n\n const fullValueText = this.centerValue ?? formatCompactNumber(total, this.locale);\n if (this.valueTextEl) {\n const truncated = truncateSvgTextToWidth(this.valueTextEl, fullValueText, maxWidth);\n if (truncated !== this.displayedValueText) this.displayedValueText = truncated;\n else this.valueTextEl.textContent = truncated;\n }\n\n const fullCaptionText = this.centerCaption ?? '';\n if (this.captionTextEl && fullCaptionText) {\n const truncated = truncateSvgTextToWidth(this.captionTextEl, fullCaptionText, maxWidth);\n if (truncated !== this.displayedCaptionText) this.displayedCaptionText = truncated;\n else this.captionTextEl.textContent = truncated;\n }\n }\n\n render() {\n const size = this.resolvedSize;\n const radius = size / 2;\n const thicknessPx = resolveCssLengthPx(this.thickness, TOKEN_DEFAULTS.size200);\n const cornerRadiusPx = resolveCssLengthPx(this.cornerRadius, TOKEN_DEFAULTS.radius025);\n const innerRadius = radius - thicknessPx;\n const total = this.data.reduce((sum, d) => sum + d.value, 0);\n // d3's pie() divides each value by the total to get an angle — with an all-zero total\n // that's 0/0, producing NaN angles and broken paths. Render a flat neutral ring instead.\n const isEmpty = total <= 0;\n // Highlight/dim from either this component's own hover or an externally-synced label\n // (e.g. a sibling ds-chart-legend row).\n const highlightLabel = this.activeLabel ?? this.hoveredLabel;\n\n const arcGenerator = arc<{ startAngle: number; endAngle: number }>()\n .innerRadius(innerRadius)\n .outerRadius(radius)\n .cornerRadius(cornerRadiusPx)\n .padAngle((this.gap * Math.PI) / 180);\n\n const slices = isEmpty\n ? []\n : pie<ChartDatum>()\n .value(d => d.value)\n .sort(null)(this.data);\n\n const emptyRingPath = isEmpty\n ? arc<{ startAngle: number; endAngle: number }>()\n .innerRadius(innerRadius)\n .outerRadius(radius)({ startAngle: 0, endAngle: Math.PI * 2 })\n : null;\n\n // Precise vertical centering for the two-line center text, using the real line-height of\n // text-title-large (value) and text-body-medium (caption) rather than eyeballed em offsets.\n const valueLineHeight = resolveCssLengthPx(TOKEN_DEFAULTS.lineheightXl, 32);\n const captionLineHeight = resolveCssLengthPx(TOKEN_DEFAULTS.lineheightMd, 20);\n const centerValueY = this.centerCaption ? radius - (CENTER_GAP_PX + captionLineHeight) / 2 : radius;\n const centerCaptionY = radius + (valueLineHeight + CENTER_GAP_PX) / 2;\n\n // Show the full text before the post-render truncation pass has measured it (avoids a blank first paint).\n const fullValueText = this.centerValue ?? formatCompactNumber(total, this.locale);\n const fullCaptionText = this.centerCaption ?? '';\n const fill = this.isFillMode;\n\n return (\n <Host\n class={{\n 'chart-donut': true,\n 'chart-donut--fill': fill,\n 'chart-donut--fixed': !fill,\n }}\n style={!fill ? { width: `${size}px`, height: `${size}px` } : undefined}\n >\n <div\n class=\"chart-donut__wrapper\"\n style={!fill ? { width: `${size}px`, height: `${size}px` } : undefined}\n >\n <svg\n class=\"chart-donut__svg\"\n viewBox={`0 0 ${size} ${size}`}\n width={size}\n height={size}\n role={isEmpty ? 'img' : undefined}\n aria-label={isEmpty ? this.noDataLabel : undefined}\n >\n <g transform={`translate(${radius}, ${radius})`}>\n {isEmpty ? (\n <path d={emptyRingPath ?? undefined} fill=\"var(--color-background-faint-neutral)\" aria-hidden=\"true\" />\n ) : (\n slices.map((slice, i) => {\n const datum = this.data[i];\n const isDimmed = highlightLabel != null && datum.label !== highlightLabel;\n const [centroidX, centroidY] = arcGenerator.centroid(slice);\n return (\n <path\n key={datum.label}\n d={arcGenerator(slice) ?? undefined}\n fill={datum.color ?? categoryColor(i)}\n opacity={isDimmed ? DIMMED_OPACITY : 1}\n tabindex={0}\n role=\"img\"\n aria-label={`${datum.label}: ${datum.value}`}\n onMouseEnter={(event: MouseEvent) => {\n this.updateTooltipFromPointer(event);\n this.handleHover(datum);\n }}\n onMouseMove={(event: MouseEvent) => this.updateTooltipFromPointer(event)}\n onMouseLeave={() => this.handleHover(null)}\n onFocus={(event: FocusEvent) => {\n this.updateTooltipFromSvgPoint(\n event.currentTarget as SVGPathElement,\n radius + centroidX,\n radius + centroidY,\n size,\n );\n this.handleHover(datum);\n }}\n onBlur={() => this.handleHover(null)}\n />\n );\n })\n )}\n </g>\n <text\n class=\"chart-donut__center-value\"\n x={radius}\n y={centerValueY}\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n ref={el => {\n this.valueTextEl = (el as SVGTextElement) ?? undefined;\n }}\n >\n {this.displayedValueText || fullValueText}\n </text>\n {fullCaptionText && (\n <text\n class=\"chart-donut__center-caption\"\n x={radius}\n y={centerCaptionY}\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n ref={el => {\n this.captionTextEl = (el as SVGTextElement) ?? undefined;\n }}\n >\n {this.displayedCaptionText || fullCaptionText}\n </text>\n )}\n </svg>\n {this.tooltipDatum && (\n <ds-tooltip-data-viz\n label={this.tooltipDatum.label}\n value={formatCompactNumber(this.tooltipDatum.value, this.locale)}\n x={this.tooltipX}\n y={this.tooltipY}\n />\n )}\n </div>\n </Host>\n );\n }\n}\n",
340
+ "content": "import { Component, Prop, State, Event, EventEmitter, Element, Watch, h, Host } from '@stencil/core';\nimport { arc, pie } from 'd3-shape';\nimport { categoryColor } from '../../utils/chart-colors';\nimport { formatCompactNumber, resolveCssLengthPx, truncateSvgTextToWidth, TOKEN_DEFAULTS } from '../../utils';\nimport type { ChartDatum } from '../../utils/chart-types';\n\n/** Gap between the center value and caption line boxes. */\nconst CENTER_GAP_PX = 0;\n/** Fraction of the inner-circle diameter center text is allowed to use before truncating. */\nconst CENTER_TEXT_SAFE_WIDTH_RATIO = 0.82;\n/** Fallback when filling before the first ResizeObserver measurement (matches fill min). */\nconst FILL_FALLBACK_PX = 128;\n\n@Component({\n tag: 'ds-chart-donut',\n styleUrl: 'ChartDonut.css',\n scoped: true,\n})\nexport class ChartDonut {\n @Prop() locale: string | undefined;\n @Prop() noDataLabel: string = 'No data';\n @Element() el!: HTMLElement;\n\n /** Slices to render. Set as a JS property (not an HTML attribute). */\n @Prop() data: ChartDatum[] = [];\n /**\n * Explicit diameter in px. When unset, the donut sizes to its container\n * (ResizeObserver) clamped between `--dimension-size-base * 16` (128px) and\n * `* 24` (192px), and stays centered in the leftover space. Prefer unset\n * inside card layouts.\n */\n @Prop() size: number | undefined;\n /** Ring thickness — number (px) or TokoMo length. Defaults to `--dimension-size-200` (16px). */\n @Prop() thickness: number | string = TOKEN_DEFAULTS.size200;\n /** Corner radius on each slice — number (px) or TokoMo length. Defaults to `--dimension-radius-025` (2px). */\n @Prop() cornerRadius: number | string = TOKEN_DEFAULTS.radius025;\n /** Gap between slices, in degrees. */\n @Prop() gap: number = 1;\n /**\n * Show a chart-owned value callout for genuine pointer or keyboard focus.\n * Disable when a visible legend already owns persistent label and value detail.\n */\n @Prop() showTooltip: boolean = true;\n /** Primary center value; defaults to the sum of `data` values (e.g. \"187\", \"40.9%\"). */\n @Prop() centerValue: string | undefined;\n /** Secondary caption below the center value (e.g. \"Total\", \"Normal\"). Rendered uppercase. */\n @Prop() centerCaption: string | undefined;\n /**\n * Externally controlled highlight, matched by `label` — e.g. drive this from a sibling\n * `ds-chart-legend`'s `dsItemHover` event to keep chart and legend hover in sync.\n * Falls back to this component's own pointer/focus hover when unset.\n * Slice hover dims peer slices and emits `dsSliceHover` for legend sync. External\n * highlight never opens the local tooltip.\n */\n @Prop() activeLabel: string | null = null;\n\n @State() private hoveredLabel: string | null = null;\n /** Center text, truncated with an ellipsis if it doesn't fit inside the inner circle. */\n @State() private displayedValueText: string = '';\n @State() private displayedCaptionText: string = '';\n /** Measured fill diameter when `size` is unset. */\n @State() private measuredSize: number = FILL_FALLBACK_PX;\n @State() private tooltipX: number = 0;\n @State() private tooltipY: number = 0;\n\n /** Fires with the hovered/focused slice's datum, or `null` on leave/blur. */\n @Event() dsSliceHover!: EventEmitter<ChartDatum | null>;\n\n private valueTextEl?: SVGTextElement;\n private captionTextEl?: SVGTextElement;\n private resizeObserver: ResizeObserver | null = null;\n\n private get isFillMode(): boolean {\n return this.size == null || !Number.isFinite(this.size) || this.size <= 0;\n }\n\n private get fillMinPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.donutFillMin, FILL_FALLBACK_PX);\n }\n\n private get fillMaxPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.donutFillMax, 192);\n }\n\n /** Clamp available square side into the fill min/max band. */\n private clampFillSize(availablePx: number): number {\n const minPx = this.fillMinPx;\n const maxPx = this.fillMaxPx;\n if (!(availablePx > 0)) return minPx;\n return Math.round(Math.min(maxPx, Math.max(minPx, availablePx)));\n }\n\n private get resolvedSize(): number {\n if (!this.isFillMode) return this.size as number;\n return this.clampFillSize(this.measuredSize);\n }\n\n componentDidLoad() {\n this.syncResizeObserver();\n }\n\n @Watch('size')\n onSizePropChange() {\n this.syncResizeObserver();\n }\n\n disconnectedCallback() {\n this.teardownResizeObserver();\n }\n\n private syncResizeObserver() {\n this.teardownResizeObserver();\n if (!this.isFillMode) return;\n // Observe the host (which fills its parent), not the wrapper — the wrapper\n // only wraps the SVG and would collapse to the clamped size, locking fill at min.\n const target = this.el;\n if (!target || typeof ResizeObserver === 'undefined') return;\n\n this.resizeObserver = new ResizeObserver(entries => {\n const entry = entries[0];\n if (!entry) return;\n this.applyAvailableSize(entry.contentRect.width, entry.contentRect.height);\n });\n this.resizeObserver.observe(target);\n this.measureHost();\n }\n\n private measureHost() {\n const rect = this.el.getBoundingClientRect();\n this.applyAvailableSize(rect.width, rect.height);\n }\n\n private applyAvailableSize(width: number, height: number) {\n const available = Math.floor(Math.min(width, height));\n if (available > 0 && available !== this.measuredSize) {\n this.measuredSize = available;\n }\n }\n\n private teardownResizeObserver() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n }\n\n private handleHover(datum: ChartDatum | null) {\n this.hoveredLabel = datum?.label ?? null;\n this.dsSliceHover.emit(datum);\n }\n\n private get tooltipDatum(): ChartDatum | null {\n if (!this.showTooltip || this.hoveredLabel == null) return null;\n return this.data.find(datum => datum.label === this.hoveredLabel) ?? null;\n }\n\n private updateTooltipFromPointer(event: MouseEvent) {\n if (!this.showTooltip) return;\n const wrapper = this.el.querySelector<HTMLElement>('.chart-donut__wrapper');\n if (!wrapper) return;\n const rect = wrapper.getBoundingClientRect();\n this.tooltipX = event.clientX - rect.left;\n this.tooltipY = event.clientY - rect.top;\n }\n\n private updateTooltipFromSvgPoint(\n path: SVGPathElement,\n svgX: number,\n svgY: number,\n viewBoxSize: number,\n ) {\n if (!this.showTooltip) return;\n const wrapper = this.el.querySelector<HTMLElement>('.chart-donut__wrapper');\n const svg = path.ownerSVGElement;\n if (!wrapper || !svg) return;\n const wrapperRect = wrapper.getBoundingClientRect();\n const svgRect = svg.getBoundingClientRect();\n this.tooltipX = svgRect.left - wrapperRect.left + svgX * (svgRect.width / viewBoxSize);\n this.tooltipY = svgRect.top - wrapperRect.top + svgY * (svgRect.height / viewBoxSize);\n }\n\n componentDidRender() {\n // Re-bind after HMR when DidLoad's observer was dropped.\n if (this.isFillMode && !this.resizeObserver) {\n this.syncResizeObserver();\n }\n\n const size = this.resolvedSize;\n const radius = size / 2;\n const thicknessPx = resolveCssLengthPx(this.thickness, TOKEN_DEFAULTS.size200);\n const innerRadius = radius - thicknessPx;\n const maxWidth = innerRadius * 2 * CENTER_TEXT_SAFE_WIDTH_RATIO;\n const total = this.data.reduce((sum, d) => sum + d.value, 0);\n\n const fullValueText = this.centerValue ?? formatCompactNumber(total, this.locale);\n if (this.valueTextEl) {\n const truncated = truncateSvgTextToWidth(this.valueTextEl, fullValueText, maxWidth);\n if (truncated !== this.displayedValueText) this.displayedValueText = truncated;\n else this.valueTextEl.textContent = truncated;\n }\n\n const fullCaptionText = this.centerCaption ?? '';\n if (this.captionTextEl && fullCaptionText) {\n const truncated = truncateSvgTextToWidth(this.captionTextEl, fullCaptionText, maxWidth);\n if (truncated !== this.displayedCaptionText) this.displayedCaptionText = truncated;\n else this.captionTextEl.textContent = truncated;\n }\n }\n\n render() {\n const size = this.resolvedSize;\n const radius = size / 2;\n const thicknessPx = resolveCssLengthPx(this.thickness, TOKEN_DEFAULTS.size200);\n const cornerRadiusPx = resolveCssLengthPx(this.cornerRadius, TOKEN_DEFAULTS.radius025);\n const innerRadius = radius - thicknessPx;\n const total = this.data.reduce((sum, d) => sum + d.value, 0);\n // d3's pie() divides each value by the total to get an angle — with an all-zero total\n // that's 0/0, producing NaN angles and broken paths. Render a flat neutral ring instead.\n const isEmpty = total <= 0;\n // Highlight/dim from either this component's own hover or an externally-synced label\n // (e.g. a sibling ds-chart-legend row).\n const highlightLabel = this.activeLabel ?? this.hoveredLabel;\n\n const arcGenerator = arc<{ startAngle: number; endAngle: number }>()\n .innerRadius(innerRadius)\n .outerRadius(radius)\n .cornerRadius(cornerRadiusPx)\n .padAngle((this.gap * Math.PI) / 180);\n\n const slices = isEmpty\n ? []\n : pie<ChartDatum>()\n .value(d => d.value)\n .sort(null)(this.data);\n\n const emptyRingPath = isEmpty\n ? arc<{ startAngle: number; endAngle: number }>()\n .innerRadius(innerRadius)\n .outerRadius(radius)({ startAngle: 0, endAngle: Math.PI * 2 })\n : null;\n\n // Precise vertical centering for the two-line center text, using the real line-height of\n // text-title-large (value) and text-body-medium (caption) rather than eyeballed em offsets.\n const valueLineHeight = resolveCssLengthPx(TOKEN_DEFAULTS.lineheightXl, 32);\n const captionLineHeight = resolveCssLengthPx(TOKEN_DEFAULTS.lineheightMd, 20);\n const centerValueY = this.centerCaption ? radius - (CENTER_GAP_PX + captionLineHeight) / 2 : radius;\n const centerCaptionY = radius + (valueLineHeight + CENTER_GAP_PX) / 2;\n\n // Show the full text before the post-render truncation pass has measured it (avoids a blank first paint).\n const fullValueText = this.centerValue ?? formatCompactNumber(total, this.locale);\n const fullCaptionText = this.centerCaption ?? '';\n const fill = this.isFillMode;\n\n return (\n <Host\n class={{\n 'chart-donut': true,\n 'chart-donut--fill': fill,\n 'chart-donut--fixed': !fill,\n }}\n style={!fill ? { width: `${size}px`, height: `${size}px` } : undefined}\n >\n <div\n class=\"chart-donut__wrapper\"\n style={!fill ? { width: `${size}px`, height: `${size}px` } : undefined}\n >\n <svg\n class=\"chart-donut__svg\"\n viewBox={`0 0 ${size} ${size}`}\n width={size}\n height={size}\n role={isEmpty ? 'img' : undefined}\n aria-label={isEmpty ? this.noDataLabel : undefined}\n >\n <g transform={`translate(${radius}, ${radius})`}>\n {isEmpty ? (\n <path d={emptyRingPath ?? undefined} fill=\"var(--color-background-faint-neutral)\" aria-hidden=\"true\" />\n ) : (\n slices.map((slice, i) => {\n const datum = this.data[i];\n const isDimmed = highlightLabel != null && datum.label !== highlightLabel;\n const [centroidX, centroidY] = arcGenerator.centroid(slice);\n return (\n <path\n key={datum.label}\n class={{ 'chart-donut__slice--dimmed': isDimmed }}\n d={arcGenerator(slice) ?? undefined}\n fill={datum.color ?? categoryColor(i)}\n tabindex={0}\n role=\"img\"\n aria-label={`${datum.label}: ${datum.value}`}\n onMouseEnter={(event: MouseEvent) => {\n this.updateTooltipFromPointer(event);\n this.handleHover(datum);\n }}\n onMouseMove={(event: MouseEvent) => this.updateTooltipFromPointer(event)}\n onMouseLeave={() => this.handleHover(null)}\n onFocus={(event: FocusEvent) => {\n this.updateTooltipFromSvgPoint(\n event.currentTarget as SVGPathElement,\n radius + centroidX,\n radius + centroidY,\n size,\n );\n this.handleHover(datum);\n }}\n onBlur={() => this.handleHover(null)}\n />\n );\n })\n )}\n </g>\n <text\n class=\"chart-donut__center-value\"\n x={radius}\n y={centerValueY}\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n ref={el => {\n this.valueTextEl = (el as SVGTextElement) ?? undefined;\n }}\n >\n {this.displayedValueText || fullValueText}\n </text>\n {fullCaptionText && (\n <text\n class=\"chart-donut__center-caption\"\n x={radius}\n y={centerCaptionY}\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n ref={el => {\n this.captionTextEl = (el as SVGTextElement) ?? undefined;\n }}\n >\n {this.displayedCaptionText || fullCaptionText}\n </text>\n )}\n </svg>\n {this.tooltipDatum && (\n <ds-tooltip-data-viz\n label={this.tooltipDatum.label}\n value={formatCompactNumber(this.tooltipDatum.value, this.locale)}\n x={this.tooltipX}\n y={this.tooltipY}\n />\n )}\n </div>\n </Host>\n );\n }\n}\n",
341
341
  "type": "registry:ui"
342
342
  }
343
343
  ]
@@ -274,7 +274,7 @@
274
274
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: items.",
275
275
  "peerDependencies": {
276
276
  "required": [
277
- "@ds-mo/tokens ^5.0.0",
277
+ "@ds-mo/tokens ^5.1.0",
278
278
  "@ds-mo/icons ^6.4.0"
279
279
  ],
280
280
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -291,12 +291,12 @@
291
291
  "files": [
292
292
  {
293
293
  "path": "src/wc/components/ChartLegend/ChartLegend.css",
294
- "content": "@import '../../utils/control-density.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/focus-ring.css';\n\n:host {\n display: block;\n font-family: var(--typography-font-family);\n user-select: none;\n}\n\n.chart-legend__list {\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n/* Non-visual wrapper — the real box is .chart-legend__item so optional highlight/link\n affordances cover the whole row while children align to the shared grid columns. */\n.chart-legend__list-item {\n display: contents;\n}\n\n:host(.chart-legend--vertical) .chart-legend__list {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr) var(--dimension-size-600);\n column-gap: var(--dimension-space-075);\n row-gap: var(--dimension-space-050);\n}\n\n:host(.chart-legend--vertical.chart-legend--show-percentage) .chart-legend__list {\n grid-template-columns: auto minmax(0, 1fr) var(--dimension-size-600) var(--dimension-size-800);\n}\n\n:host(.chart-legend--vertical) .chart-legend__item {\n grid-column: 1 / -1;\n display: grid;\n grid-template-columns: subgrid;\n}\n\n:host(.chart-legend--horizontal) .chart-legend__list {\n display: flex;\n flex-wrap: wrap;\n gap: var(--dimension-space-200);\n}\n\n:host(.chart-legend--horizontal) .chart-legend__item {\n display: flex;\n gap: var(--dimension-space-100);\n}\n\n/*\n * Density: md control recipe (height / pad / radius / label inset).\n * Hover/press: `.ds-interaction-fill` (token overlay on ::after).\n */\n.chart-legend__item {\n box-sizing: border-box;\n height: var(--ds-control-height, var(--dimension-size-400));\n align-items: center;\n padding: 0 var(--ds-control-padding-inline, var(--dimension-space-075));\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n text-decoration: none;\n color: inherit;\n transition: opacity var(--effect-motion-short-2);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .chart-legend__item {\n transition: none;\n }\n}\n\n.chart-legend__item--interactive {\n cursor: pointer;\n}\n\n/* Raise row content above interaction fills (positive z-index on util layers). */\n.chart-legend__item > * {\n position: relative;\n z-index: 2;\n}\n\n.chart-legend__swatch-box {\n width: var(--dimension-size-250);\n height: var(--dimension-size-250);\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.chart-legend__swatch {\n width: var(--dimension-space-150);\n height: var(--dimension-space-150);\n border-radius: var(--dimension-radius-025);\n flex-shrink: 0;\n}\n\n.chart-legend__label {\n min-width: 0;\n}\n\n.chart-legend__percentage {\n width: 100%;\n min-width: 0;\n text-align: right;\n}\n\n.chart-legend__value {\n width: 100%;\n text-align: right;\n min-width: 0;\n}\n",
294
+ "content": "@import '../../utils/control-density.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/focus-ring.css';\n\n:host {\n display: block;\n font-family: var(--typography-font-family);\n user-select: none;\n}\n\n.chart-legend__list {\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n/* Non-visual wrapper — the real box is .chart-legend__item so optional highlight/link\n affordances cover the whole row while children align to the shared grid columns. */\n.chart-legend__list-item {\n display: contents;\n}\n\n:host(.chart-legend--vertical) .chart-legend__list {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr) var(--dimension-size-600);\n column-gap: var(--dimension-space-075);\n row-gap: var(--dimension-space-050);\n}\n\n:host(.chart-legend--vertical.chart-legend--show-percentage) .chart-legend__list {\n grid-template-columns: auto minmax(0, 1fr) var(--dimension-size-600) var(--dimension-size-800);\n}\n\n:host(.chart-legend--vertical) .chart-legend__item {\n grid-column: 1 / -1;\n display: grid;\n grid-template-columns: subgrid;\n}\n\n:host(.chart-legend--horizontal) .chart-legend__list {\n display: flex;\n flex-wrap: wrap;\n gap: var(--dimension-space-200);\n}\n\n:host(.chart-legend--horizontal) .chart-legend__item {\n display: flex;\n gap: var(--dimension-space-100);\n}\n\n/*\n * Density: md control recipe (height / pad / radius / label inset).\n * Hover/press: `.ds-interaction-fill` (token overlay on ::after).\n */\n.chart-legend__item {\n box-sizing: border-box;\n height: var(--ds-control-height, var(--dimension-size-400));\n align-items: center;\n padding: 0 var(--ds-control-padding-inline, var(--dimension-space-075));\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n text-decoration: none;\n color: inherit;\n transition: opacity var(--effect-motion-short-2);\n}\n\n.chart-legend__item--dimmed {\n opacity: var(--effect-opacity-medium);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .chart-legend__item {\n transition: none;\n }\n}\n\n.chart-legend__item--interactive {\n cursor: pointer;\n}\n\n/* Raise row content above interaction fills (positive z-index on util layers). */\n.chart-legend__item > * {\n position: relative;\n z-index: 2;\n}\n\n.chart-legend__swatch-box {\n width: var(--dimension-size-250);\n height: var(--dimension-size-250);\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.chart-legend__swatch {\n width: var(--dimension-space-150);\n height: var(--dimension-space-150);\n border-radius: var(--dimension-radius-025);\n flex-shrink: 0;\n}\n\n.chart-legend__label {\n min-width: 0;\n}\n\n.chart-legend__percentage {\n width: 100%;\n min-width: 0;\n text-align: right;\n}\n\n.chart-legend__value {\n width: 100%;\n text-align: right;\n min-width: 0;\n}\n",
295
295
  "type": "registry:ui"
296
296
  },
297
297
  {
298
298
  "path": "src/wc/components/ChartLegend/ChartLegend.tsx",
299
- "content": "import { Component, Prop, State, Event, EventEmitter, h, Host, Watch } from '@stencil/core';\nimport { categoryColor } from '../../utils/chart-colors';\nimport { formatCompactNumber, formatPercentage, resolveSafeUrl } from '../../utils';\nimport type { ChartLegendItem } from '../../utils/chart-types';\n\nexport type ChartLegendDirection = 'vertical' | 'horizontal';\nexport type ChartLegendPercentageDecimals = 1 | 2;\n\nconst DIMMED_OPACITY = 0.5;\n\n/**\n * Base legend for `ds-chart-*` components. Webapp's Overview widgets each style\n * legends differently (list w/ values, compact chips, bare swatches, external\n * stat callouts) — this covers the common \"swatch + label + optional value\" case.\n * Consumers who need a different treatment skip this component entirely and\n * render their own markup against the same `ChartDatum[]` data.\n */\n@Component({\n tag: 'ds-chart-legend',\n styleUrl: 'ChartLegend.css',\n scoped: true,\n})\nexport class ChartLegend {\n @Prop() locale: string | undefined;\n /** Legend entries. Set as a JS property (not an HTML attribute). */\n @Prop() items: ChartLegendItem[] = [];\n @Prop() direction: ChartLegendDirection = 'vertical';\n /** Show each item's share of the total (of items with a `value`) alongside its count. */\n @Prop() showPercentage: boolean = true;\n /** Fixed number of decimal places shown for percentages. */\n @Prop() percentageDecimals: ChartLegendPercentageDecimals = 1;\n /** Let local pointer/focus movement highlight one item and emit `dsItemHover`. */\n @Prop() highlightOnHover: boolean = true;\n /**\n * Externally controlled highlight, matched by `label` — e.g. drive this from a sibling\n * chart's `dsSliceHover` event to keep chart and legend hover in sync. Only dims the other\n * rows' opacity (like a chart dimming its other slices) — it never shows the hover-fill,\n * since that's reserved for a real pointer/keyboard interaction on this row.\n */\n @Prop() activeLabel: string | null = null;\n\n /** Own pointer/focus hover, kept separate from `activeLabel` so the hover-fill (a \"you can\n * click here\" affordance) never shows just because an external row was synced in. */\n @State() private hoveredLabel: string | null = null;\n\n /** Fires on row hover/focus with the item, or `null` on leave/blur. */\n @Event() dsItemHover!: EventEmitter<ChartLegendItem | null>;\n /** Fires when a deep-linkable row (`item.href` set) is activated. */\n @Event() dsItemClick!: EventEmitter<{ item: ChartLegendItem; originalEvent: MouseEvent }>;\n\n @Watch('highlightOnHover')\n handleHighlightOnHoverChange(enabled: boolean) {\n if (!enabled) this.hoveredLabel = null;\n }\n\n private handleHover(item: ChartLegendItem | null) {\n this.hoveredLabel = item?.label ?? null;\n this.dsItemHover.emit(item);\n }\n\n private handleClick = (item: ChartLegendItem, originalEvent: MouseEvent) => {\n this.dsItemClick.emit({ item, originalEvent });\n };\n\n render() {\n const total = this.items.reduce((sum, item) => sum + (item.value ?? 0), 0);\n // Highlight/dim from either this component's own hover or an externally-synced label\n // (e.g. a sibling ds-chart-donut slice) — same mechanism ds-chart-donut uses for its slices.\n const highlightLabel = this.activeLabel ?? (this.highlightOnHover ? this.hoveredLabel : null);\n\n return (\n <Host\n class={{\n 'chart-legend': true,\n [`chart-legend--${this.direction}`]: true,\n 'chart-legend--show-percentage': this.showPercentage,\n }}\n >\n {/* mouseleave lives on the list, not each row — the row-gap between items is still\n inside the list's box, so crossing it while moving between rows never fires this.\n Only actually leaving the whole list does, and that clears with no delay. */}\n <ul\n class=\"chart-legend__list\"\n onMouseLeave={this.highlightOnHover ? () => this.handleHover(null) : undefined}\n >\n {this.items.map((item, i) => {\n const isDimmed = highlightLabel != null && item.label !== highlightLabel;\n const href = resolveSafeUrl(item.href);\n const RowTag = href ? 'a' : 'div';\n const percentage = item.value != null\n ? formatPercentage(total ? item.value / total : 0, this.percentageDecimals, this.locale)\n : '';\n\n return (\n <li class=\"chart-legend__list-item\" key={item.label}>\n <RowTag\n class={{\n 'chart-legend__item': true,\n 'chart-legend__item--interactive': !!href,\n 'ds-control--md': true,\n 'ds-interaction-fill': this.highlightOnHover || !!href,\n 'ds-focus-ring-inset': !!href,\n }}\n style={{ opacity: isDimmed ? String(DIMMED_OPACITY) : '1' }}\n href={href}\n onClick={href ? (e: MouseEvent) => this.handleClick(item, e) : undefined}\n onMouseEnter={\n this.highlightOnHover ? () => this.handleHover(item) : undefined\n }\n onFocus={this.highlightOnHover ? () => this.handleHover(item) : undefined}\n onBlur={this.highlightOnHover ? () => this.handleHover(null) : undefined}\n >\n <span class=\"chart-legend__swatch-box\">\n <span class=\"chart-legend__swatch\" style={{ backgroundColor: item.color ?? categoryColor(i) }} />\n </span>\n <ds-text\n class=\"chart-legend__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n lineTruncation={1}\n title={item.label}\n >\n {item.label}\n </ds-text>\n {item.value != null && (\n <ds-text\n class=\"chart-legend__value\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"primary\"\n lineTruncation={1}\n align=\"right\"\n fontFeature=\"tabular-nums\"\n title={String(item.value)}\n >\n {formatCompactNumber(item.value, this.locale)}\n </ds-text>\n )}\n {item.value != null && this.showPercentage && (\n <ds-text\n class=\"chart-legend__percentage\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"primary\"\n lineTruncation={1}\n align=\"right\"\n fontFeature=\"tabular-nums\"\n title={percentage}\n >\n {percentage}\n </ds-text>\n )}\n </RowTag>\n </li>\n );\n })}\n </ul>\n </Host>\n );\n }\n}\n",
299
+ "content": "import { Component, Prop, State, Event, EventEmitter, h, Host, Watch } from '@stencil/core';\nimport { categoryColor } from '../../utils/chart-colors';\nimport { formatCompactNumber, formatPercentage, resolveSafeUrl } from '../../utils';\nimport type { ChartLegendItem } from '../../utils/chart-types';\n\nexport type ChartLegendDirection = 'vertical' | 'horizontal';\nexport type ChartLegendPercentageDecimals = 1 | 2;\n\n/**\n * Base legend for `ds-chart-*` components. Webapp's Overview widgets each style\n * legends differently (list w/ values, compact chips, bare swatches, external\n * stat callouts) — this covers the common \"swatch + label + optional value\" case.\n * Consumers who need a different treatment skip this component entirely and\n * render their own markup against the same `ChartDatum[]` data.\n */\n@Component({\n tag: 'ds-chart-legend',\n styleUrl: 'ChartLegend.css',\n scoped: true,\n})\nexport class ChartLegend {\n @Prop() locale: string | undefined;\n /** Legend entries. Set as a JS property (not an HTML attribute). */\n @Prop() items: ChartLegendItem[] = [];\n @Prop() direction: ChartLegendDirection = 'vertical';\n /** Show each item's share of the total (of items with a `value`) alongside its count. */\n @Prop() showPercentage: boolean = true;\n /** Fixed number of decimal places shown for percentages. */\n @Prop() percentageDecimals: ChartLegendPercentageDecimals = 1;\n /** Let local pointer/focus movement highlight one item and emit `dsItemHover`. */\n @Prop() highlightOnHover: boolean = true;\n /**\n * Externally controlled highlight, matched by `label` — e.g. drive this from a sibling\n * chart's `dsSliceHover` event to keep chart and legend hover in sync. Only dims the other\n * rows' opacity (like a chart dimming its other slices) — it never shows the hover-fill,\n * since that's reserved for a real pointer/keyboard interaction on this row.\n */\n @Prop() activeLabel: string | null = null;\n\n /** Own pointer/focus hover, kept separate from `activeLabel` so the hover-fill (a \"you can\n * click here\" affordance) never shows just because an external row was synced in. */\n @State() private hoveredLabel: string | null = null;\n\n /** Fires on row hover/focus with the item, or `null` on leave/blur. */\n @Event() dsItemHover!: EventEmitter<ChartLegendItem | null>;\n /** Fires when a deep-linkable row (`item.href` set) is activated. */\n @Event() dsItemClick!: EventEmitter<{ item: ChartLegendItem; originalEvent: MouseEvent }>;\n\n @Watch('highlightOnHover')\n handleHighlightOnHoverChange(enabled: boolean) {\n if (!enabled) this.hoveredLabel = null;\n }\n\n private handleHover(item: ChartLegendItem | null) {\n this.hoveredLabel = item?.label ?? null;\n this.dsItemHover.emit(item);\n }\n\n private handleClick = (item: ChartLegendItem, originalEvent: MouseEvent) => {\n this.dsItemClick.emit({ item, originalEvent });\n };\n\n render() {\n const total = this.items.reduce((sum, item) => sum + (item.value ?? 0), 0);\n // Highlight/dim from either this component's own hover or an externally-synced label\n // (e.g. a sibling ds-chart-donut slice) — same mechanism ds-chart-donut uses for its slices.\n const highlightLabel = this.activeLabel ?? (this.highlightOnHover ? this.hoveredLabel : null);\n\n return (\n <Host\n class={{\n 'chart-legend': true,\n [`chart-legend--${this.direction}`]: true,\n 'chart-legend--show-percentage': this.showPercentage,\n }}\n >\n {/* mouseleave lives on the list, not each row — the row-gap between items is still\n inside the list's box, so crossing it while moving between rows never fires this.\n Only actually leaving the whole list does, and that clears with no delay. */}\n <ul\n class=\"chart-legend__list\"\n onMouseLeave={this.highlightOnHover ? () => this.handleHover(null) : undefined}\n >\n {this.items.map((item, i) => {\n const isDimmed = highlightLabel != null && item.label !== highlightLabel;\n const href = resolveSafeUrl(item.href);\n const RowTag = href ? 'a' : 'div';\n const percentage = item.value != null\n ? formatPercentage(total ? item.value / total : 0, this.percentageDecimals, this.locale)\n : '';\n\n return (\n <li class=\"chart-legend__list-item\" key={item.label}>\n <RowTag\n class={{\n 'chart-legend__item': true,\n 'chart-legend__item--interactive': !!href,\n 'chart-legend__item--dimmed': isDimmed,\n 'ds-control--md': true,\n 'ds-interaction-fill': this.highlightOnHover || !!href,\n 'ds-focus-ring-inset': !!href,\n }}\n href={href}\n onClick={href ? (e: MouseEvent) => this.handleClick(item, e) : undefined}\n onMouseEnter={\n this.highlightOnHover ? () => this.handleHover(item) : undefined\n }\n onFocus={this.highlightOnHover ? () => this.handleHover(item) : undefined}\n onBlur={this.highlightOnHover ? () => this.handleHover(null) : undefined}\n >\n <span class=\"chart-legend__swatch-box\">\n <span class=\"chart-legend__swatch\" style={{ backgroundColor: item.color ?? categoryColor(i) }} />\n </span>\n <ds-text\n class=\"chart-legend__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n lineTruncation={1}\n title={item.label}\n >\n {item.label}\n </ds-text>\n {item.value != null && (\n <ds-text\n class=\"chart-legend__value\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"primary\"\n lineTruncation={1}\n align=\"right\"\n fontFeature=\"tabular-nums\"\n title={String(item.value)}\n >\n {formatCompactNumber(item.value, this.locale)}\n </ds-text>\n )}\n {item.value != null && this.showPercentage && (\n <ds-text\n class=\"chart-legend__percentage\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"primary\"\n lineTruncation={1}\n align=\"right\"\n fontFeature=\"tabular-nums\"\n title={percentage}\n >\n {percentage}\n </ds-text>\n )}\n </RowTag>\n </li>\n );\n })}\n </ul>\n </Host>\n );\n }\n}\n",
300
300
  "type": "registry:ui"
301
301
  }
302
302
  ]
@@ -196,7 +196,7 @@
196
196
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: categories, series.",
197
197
  "peerDependencies": {
198
198
  "required": [
199
- "@ds-mo/tokens ^5.0.0",
199
+ "@ds-mo/tokens ^5.1.0",
200
200
  "@ds-mo/icons ^6.4.0"
201
201
  ],
202
202
  "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.0.0",
335
+ "@ds-mo/tokens ^5.1.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.0.0",
248
+ "@ds-mo/tokens ^5.1.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.0.0",
123
+ "@ds-mo/tokens ^5.1.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."