@ds-mo/ui 15.0.0 → 15.1.1

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 (146) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +7 -0
  3. package/dist/.package-ready.json +1 -1
  4. package/dist/agent-patterns.json +1 -1
  5. package/dist/agent.json +2320 -90
  6. package/dist/components/ds-agent-questionnaire.js +1 -1
  7. package/dist/components/ds-agent-response.js +1 -1
  8. package/dist/components/ds-banner.js +1 -1
  9. package/dist/components/ds-banner.js.map +1 -1
  10. package/dist/components/ds-card-chart.js +1 -1
  11. package/dist/components/ds-card-chart.js.map +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-card-setting.js.map +1 -1
  16. package/dist/components/ds-chart.js +1 -1
  17. package/dist/components/ds-chart.js.map +1 -1
  18. package/dist/components/ds-checkbox.js +1 -1
  19. package/dist/components/ds-conversation-list-item.js +1 -1
  20. package/dist/components/ds-conversation-list-item.js.map +1 -1
  21. package/dist/components/ds-data-filter.js +1 -1
  22. package/dist/components/ds-data-preferences.js +1 -1
  23. package/dist/components/ds-data-saved-views.js +1 -1
  24. package/dist/components/ds-data-search.js +1 -1
  25. package/dist/components/ds-data-search.js.map +1 -1
  26. package/dist/components/ds-filter-menu.js +1 -1
  27. package/dist/components/ds-message-scroller.js.map +1 -1
  28. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  29. package/dist/components/ds-pagination.js +1 -1
  30. package/dist/components/ds-scroll-overlay.js.map +1 -1
  31. package/dist/components/ds-select.js +1 -1
  32. package/dist/components/ds-shell-app.js +1 -1
  33. package/dist/components/ds-table.js +1 -1
  34. package/dist/components/p-0FYD2bev.js.map +1 -1
  35. package/dist/components/{p-Dpr2y6_S.js → p-B_jdoM7e.js} +2 -2
  36. package/dist/components/{p-CElUM-xU.js → p-C6xVxqOn.js} +2 -2
  37. package/dist/components/{p-MG4lcN0D.js → p-CdGXzRP8.js} +2 -2
  38. package/dist/components/{p-D6Wl3dRO.js → p-Cqc2U1ZQ.js} +2 -2
  39. package/dist/components/{p-D6Wl3dRO.js.map → p-Cqc2U1ZQ.js.map} +1 -1
  40. package/dist/components/{p-Nc76seSq.js → p-DEW7r0U9.js} +2 -2
  41. package/dist/components/{p-IEq_06J0.js → p-Dfs98tpL.js} +2 -2
  42. package/dist/components/{p-IEq_06J0.js.map → p-Dfs98tpL.js.map} +1 -1
  43. package/dist/components/p-ZhP2tVmM.js.map +1 -1
  44. package/dist/components/p-etifxiDc.js.map +1 -1
  45. package/dist/docs/components.json +1 -1
  46. package/dist/lint/STYLELINT-LICENSE +20 -0
  47. package/dist/lint/THIRD-PARTY-NOTICES +553 -0
  48. package/dist/lint/index.d.ts +7 -0
  49. package/dist/lint/index.js +51385 -0
  50. package/dist/lint/types.d.ts +13 -0
  51. package/dist/mcp-data/registry/agent-activity.json +6 -1
  52. package/dist/mcp-data/registry/agent-questionnaire.json +6 -1
  53. package/dist/mcp-data/registry/agent-response.json +12 -1
  54. package/dist/mcp-data/registry/agent-source-list.json +6 -1
  55. package/dist/mcp-data/registry/agent-tool-call.json +6 -1
  56. package/dist/mcp-data/registry/attachment-list.json +6 -1
  57. package/dist/mcp-data/registry/avatar.json +21 -1
  58. package/dist/mcp-data/registry/badge.json +30 -2
  59. package/dist/mcp-data/registry/banner.json +28 -2
  60. package/dist/mcp-data/registry/bar-action.json +24 -1
  61. package/dist/mcp-data/registry/bar-nav.json +6 -1
  62. package/dist/mcp-data/registry/bar-page-title.json +24 -1
  63. package/dist/mcp-data/registry/bar-title.json +26 -1
  64. package/dist/mcp-data/registry/bar-workflow.json +24 -1
  65. package/dist/mcp-data/registry/breadcrumb.json +6 -1
  66. package/dist/mcp-data/registry/button-filled.json +68 -1
  67. package/dist/mcp-data/registry/button-inverted.json +64 -1
  68. package/dist/mcp-data/registry/button-unfilled.json +66 -1
  69. package/dist/mcp-data/registry/calendar.json +15 -2
  70. package/dist/mcp-data/registry/card-action-center.json +24 -1
  71. package/dist/mcp-data/registry/card-chart.json +37 -2
  72. package/dist/mcp-data/registry/card-navigation.json +36 -1
  73. package/dist/mcp-data/registry/card-overview.json +27 -2
  74. package/dist/mcp-data/registry/card-setting.json +35 -2
  75. package/dist/mcp-data/registry/card-settings-scope.json +24 -1
  76. package/dist/mcp-data/registry/chart-legend.json +6 -1
  77. package/dist/mcp-data/registry/chart.json +7 -2
  78. package/dist/mcp-data/registry/checkbox.json +38 -2
  79. package/dist/mcp-data/registry/chip.json +50 -1
  80. package/dist/mcp-data/registry/code-block.json +30 -1
  81. package/dist/mcp-data/registry/conversation-list-item.json +25 -2
  82. package/dist/mcp-data/registry/conversation-list-section.json +14 -1
  83. package/dist/mcp-data/registry/conversation-list.json +6 -1
  84. package/dist/mcp-data/registry/data-customize.json +26 -1
  85. package/dist/mcp-data/registry/data-filter.json +26 -1
  86. package/dist/mcp-data/registry/data-group.json +26 -1
  87. package/dist/mcp-data/registry/data-preferences.json +26 -1
  88. package/dist/mcp-data/registry/data-saved-views.json +26 -1
  89. package/dist/mcp-data/registry/data-search.json +27 -2
  90. package/dist/mcp-data/registry/data-sort.json +26 -1
  91. package/dist/mcp-data/registry/data-toolbar.json +14 -1
  92. package/dist/mcp-data/registry/divider.json +6 -1
  93. package/dist/mcp-data/registry/empty-state.json +6 -1
  94. package/dist/mcp-data/registry/field.json +14 -1
  95. package/dist/mcp-data/registry/filter-menu.json +48 -1
  96. package/dist/mcp-data/registry/icon.json +21 -1
  97. package/dist/mcp-data/registry/input-date.json +48 -1
  98. package/dist/mcp-data/registry/input-time.json +48 -1
  99. package/dist/mcp-data/registry/input.json +48 -1
  100. package/dist/mcp-data/registry/loader.json +21 -1
  101. package/dist/mcp-data/registry/markdown.json +17 -1
  102. package/dist/mcp-data/registry/menu.json +26 -1
  103. package/dist/mcp-data/registry/message-actions.json +6 -1
  104. package/dist/mcp-data/registry/message-bubble.json +26 -1
  105. package/dist/mcp-data/registry/message-composer.json +24 -1
  106. package/dist/mcp-data/registry/message-scroller.json +12 -1
  107. package/dist/mcp-data/registry/message.json +6 -1
  108. package/dist/mcp-data/registry/mobile-bar-nav.json +24 -1
  109. package/dist/mcp-data/registry/mobile-header.json +26 -1
  110. package/dist/mcp-data/registry/mobile-section-switcher.json +26 -1
  111. package/dist/mcp-data/registry/mobile-sheet-nav.json +27 -2
  112. package/dist/mcp-data/registry/modal.json +24 -1
  113. package/dist/mcp-data/registry/pagination.json +6 -1
  114. package/dist/mcp-data/registry/panel-nav.json +28 -2
  115. package/dist/mcp-data/registry/panel-sub-nav.json +25 -0
  116. package/dist/mcp-data/registry/panel-tool-header.json +24 -1
  117. package/dist/mcp-data/registry/panel-tool-search.json +24 -1
  118. package/dist/mcp-data/registry/panel-tools.json +25 -1
  119. package/dist/mcp-data/registry/paper-texture.json +6 -1
  120. package/dist/mcp-data/registry/radio.json +35 -1
  121. package/dist/mcp-data/registry/registry.json +2319 -89
  122. package/dist/mcp-data/registry/score.json +38 -1
  123. package/dist/mcp-data/registry/scroll-overlay.json +6 -1
  124. package/dist/mcp-data/registry/select.json +52 -1
  125. package/dist/mcp-data/registry/shell-app.json +26 -1
  126. package/dist/mcp-data/registry/shell-page.json +6 -1
  127. package/dist/mcp-data/registry/shell-tools.json +25 -1
  128. package/dist/mcp-data/registry/skeleton.json +41 -1
  129. package/dist/mcp-data/registry/slider.json +42 -1
  130. package/dist/mcp-data/registry/swatch-picker.json +24 -1
  131. package/dist/mcp-data/registry/switch.json +37 -1
  132. package/dist/mcp-data/registry/tab-group.json +49 -1
  133. package/dist/mcp-data/registry/table.json +102 -1
  134. package/dist/mcp-data/registry/tag.json +52 -1
  135. package/dist/mcp-data/registry/text.json +22 -1
  136. package/dist/mcp-data/registry/textarea.json +38 -1
  137. package/dist/mcp-data/registry/time-picker.json +14 -1
  138. package/dist/mcp-data/registry/toast.json +24 -1
  139. package/dist/mcp-data/registry/tooltip-chart.json +24 -1
  140. package/dist/mcp-data/registry/tooltip.json +26 -1
  141. package/dist/mcp-data/registry/typing-indicator.json +6 -1
  142. package/package.json +32 -7
  143. /package/dist/components/{p-Dpr2y6_S.js.map → p-B_jdoM7e.js.map} +0 -0
  144. /package/dist/components/{p-CElUM-xU.js.map → p-C6xVxqOn.js.map} +0 -0
  145. /package/dist/components/{p-MG4lcN0D.js.map → p-CdGXzRP8.js.map} +0 -0
  146. /package/dist/components/{p-Nc76seSq.js.map → p-DEW7r0U9.js.map} +0 -0
@@ -64,7 +64,12 @@
64
64
  "label": "Chart guide",
65
65
  "path": "src/docs/Charts.mdx"
66
66
  }
67
- ]
67
+ ],
68
+ "styling": {
69
+ "rationale": "The renderer observes the host; application-supplied dimensions and chart definition remain authoritative.",
70
+ "protected": [],
71
+ "customProperties": []
72
+ }
68
73
  },
69
74
  "api": {
70
75
  "props": {
@@ -256,7 +261,7 @@
256
261
  },
257
262
  {
258
263
  "path": "src/wc/components/Chart/Chart.tsx",
259
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n Prop,\n State,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport type {\n ChartDefinition,\n ChartPoint,\n ChartTooltipItem,\n ChartTooltipOptions,\n} from '../../utils/chart-grammar';\nimport {\n compileChartScene,\n findChartFocus,\n type ChartScene,\n type ChartSceneNode,\n} from '../../utils/chart-scene';\nimport { resolveChartTheme } from '../../utils/chart-theme';\n\nconst DEFAULT_WIDTH = 480;\nconst DEFAULT_HEIGHT = 320;\n\nexport type ChartFocusSource = 'pointer' | 'keyboard';\n\nexport interface ChartFocusChangeDetail {\n primary: ChartPoint | null;\n points: readonly ChartPoint[];\n source: ChartFocusSource;\n}\n\ninterface FocusState {\n primary: ChartPoint;\n points: ChartPoint[];\n source: ChartFocusSource;\n}\n\ninterface PointerAnchor {\n x: number;\n y: number;\n}\n\n@Component({\n tag: 'ds-chart',\n styleUrl: 'Chart.css',\n scoped: true,\n})\nexport class Chart {\n @Element() el!: HTMLElement;\n\n /** Typed chart definition. Set as a JavaScript property, not an HTML attribute. */\n @Prop() definition!: ChartDefinition;\n /** Required accessible name for the visualization. */\n @Prop() label!: string;\n /** Optional longer accessible description. */\n @Prop() description?: string;\n /** Locale used by axes and default tooltip formatters. */\n @Prop() locale?: string;\n /** Fixed surface width. Otherwise the chart observes its container. */\n @Prop() width?: number;\n /** Product-owned surface height. Wins over aspectRatio. */\n @Prop() height?: number;\n /** Derives height from the current width when height is not supplied. */\n @Prop() aspectRatio?: number;\n\n /** Fires whenever pointer or keyboard focus resolves to semantic chart points. */\n @Event() dsChartFocusChange!: EventEmitter<ChartFocusChangeDetail>;\n\n @State() private surfaceWidth = DEFAULT_WIDTH;\n @State() private containerHeight?: number;\n @State() private scene?: ChartScene;\n @State() private focusState?: FocusState;\n @State() private pointerAnchor?: PointerAnchor;\n\n private resizeObserver?: ResizeObserver;\n private resizeFrame?: number;\n private compileFrame?: number;\n private warnedConflictingHeight = false;\n private measuredLabels = new Map<string, { width: number; height: number }>();\n private descriptionId = `ds-chart-description-${Math.random().toString(36).slice(2)}`;\n private clipId = `ds-chart-clip-${Math.random().toString(36).slice(2)}`;\n\n componentWillLoad() {\n if (this.width && this.width > 0) this.surfaceWidth = this.width;\n this.compile();\n }\n\n componentDidLoad() {\n this.connectResizeObserver();\n this.waitForFonts();\n }\n\n componentDidRender() {\n this.measureRenderedLabels();\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n if (this.resizeFrame) cancelAnimationFrame(this.resizeFrame);\n if (this.compileFrame) cancelAnimationFrame(this.compileFrame);\n }\n\n @Watch('definition')\n @Watch('locale')\n @Watch('height')\n @Watch('aspectRatio')\n onDefinitionInputChange() {\n this.scheduleCompile();\n }\n\n @Watch('width')\n onWidthChange() {\n if (this.width && this.width > 0) this.surfaceWidth = this.width;\n else this.measureHost();\n this.scheduleCompile();\n }\n\n private get surfaceHeight(): number {\n if (this.height && this.height > 0) {\n if (this.aspectRatio && !this.warnedConflictingHeight) {\n this.warnedConflictingHeight = true;\n console.warn('[ds-chart] height wins when both height and aspectRatio are supplied.');\n }\n return this.height;\n }\n if (this.aspectRatio && this.aspectRatio > 0) {\n return Math.max(1, this.surfaceWidth / this.aspectRatio);\n }\n return this.containerHeight ?? DEFAULT_HEIGHT;\n }\n\n private connectResizeObserver() {\n this.resizeObserver?.disconnect();\n if (typeof ResizeObserver === 'undefined') return;\n this.resizeObserver = new ResizeObserver(entries => {\n const entry = entries[0];\n if (!entry) return;\n if (this.resizeFrame) cancelAnimationFrame(this.resizeFrame);\n this.resizeFrame = requestAnimationFrame(() => {\n this.resizeFrame = undefined;\n this.applyMeasuredSize(entry.contentRect.width, entry.contentRect.height);\n });\n });\n this.resizeObserver.observe(this.el);\n this.measureHost();\n }\n\n private measureHost() {\n const bounds = this.el.getBoundingClientRect();\n this.applyMeasuredSize(bounds.width, bounds.height);\n }\n\n private applyMeasuredSize(width: number, height: number) {\n let changed = false;\n if (!this.width && width > 0) {\n const nextWidth = Math.round(width * 100) / 100;\n if (nextWidth !== this.surfaceWidth) {\n this.surfaceWidth = nextWidth;\n changed = true;\n }\n }\n if (!this.height && !this.aspectRatio && height > 0) {\n const nextHeight = Math.round(height * 100) / 100;\n if (nextHeight !== this.containerHeight) {\n this.containerHeight = nextHeight;\n changed = true;\n }\n }\n if (changed) {\n this.scheduleCompile();\n }\n }\n\n private scheduleCompile() {\n if (this.compileFrame) cancelAnimationFrame(this.compileFrame);\n this.compileFrame = requestAnimationFrame(() => {\n this.compileFrame = undefined;\n this.compile();\n });\n }\n\n private compile() {\n if (!this.definition || !(this.surfaceWidth > 0) || !(this.surfaceHeight > 0)) return;\n const previousKey = this.focusState?.primary.sceneKey;\n const scene = compileChartScene(\n this.definition,\n this.surfaceWidth,\n this.surfaceHeight,\n this.locale ?? document.documentElement.lang ?? 'en',\n text => this.measuredLabels.get(text) ?? { width: text.length * 7, height: 14 },\n resolveChartTheme(this.el)\n );\n this.scene = scene;\n if (previousKey) {\n const restored = scene.points.find(point => point.sceneKey === previousKey);\n if (restored && this.focusState) {\n const grouped = findChartFocus(\n scene.points,\n scene.focus,\n restored.x,\n restored.y,\n scene.maxFocusDistance,\n scene.arcHitRegions\n );\n this.focusState = grouped ? { ...grouped, source: this.focusState.source } : undefined;\n } else {\n this.focusState = undefined;\n }\n }\n }\n\n private waitForFonts() {\n if (!document.fonts) return;\n void document.fonts.ready.then(() => {\n this.measuredLabels.clear();\n this.scheduleCompile();\n });\n }\n\n private measureRenderedLabels() {\n const labels = this.el.querySelectorAll<SVGTextElement>('[data-chart-measure]');\n let changed = false;\n labels.forEach(label => {\n const text = label.textContent ?? '';\n let box: DOMRect;\n try {\n box = label.getBBox();\n } catch {\n return;\n }\n const previous = this.measuredLabels.get(text);\n if (\n !previous ||\n Math.abs(previous.width - box.width) > 0.5 ||\n Math.abs(previous.height - box.height) > 0.5\n ) {\n this.measuredLabels.set(text, { width: box.width, height: box.height });\n changed = true;\n }\n });\n if (changed) this.scheduleCompile();\n }\n\n private resolveFocus(x: number, y: number, source: ChartFocusSource) {\n if (!this.scene) return;\n const result = findChartFocus(\n this.scene.points,\n this.scene.focus,\n x,\n y,\n this.scene.maxFocusDistance,\n this.scene.arcHitRegions\n );\n if (!result) {\n this.clearFocus(source);\n return;\n }\n if (\n this.focusState?.primary.sceneKey === result.primary.sceneKey &&\n this.focusState.source === source\n )\n return;\n this.focusState = { ...result, source };\n this.dsChartFocusChange.emit({ ...result, source });\n }\n\n private clearFocus(source: ChartFocusSource) {\n if (!this.focusState) return;\n this.focusState = undefined;\n this.dsChartFocusChange.emit({ primary: null, points: [], source });\n }\n\n private onPointerMove = (event: PointerEvent) => {\n const svg = event.currentTarget as SVGSVGElement;\n const rect = svg.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n this.pointerAnchor = this.scene?.arcHitRegions.length ? { x, y } : undefined;\n this.resolveFocus(x, y, 'pointer');\n };\n\n private onPointerLeave = () => {\n this.pointerAnchor = undefined;\n this.clearFocus('pointer');\n };\n\n private keyboardPoints(): ChartPoint[] {\n if (!this.scene) return [];\n return [...this.scene.points].sort((a, b) => a.x - b.x || a.y - b.y);\n }\n\n private onKeyDown = (event: KeyboardEvent) => {\n const points = this.keyboardPoints();\n if (points.length === 0) return;\n if (event.key === 'Escape') {\n event.preventDefault();\n this.clearFocus('keyboard');\n return;\n }\n const current = this.focusState\n ? points.findIndex(point => point.sceneKey === this.focusState?.primary.sceneKey)\n : -1;\n let next: number | undefined;\n if (event.key === 'Home') next = 0;\n else if (event.key === 'End') next = points.length - 1;\n else if (event.key === 'ArrowRight' || event.key === 'ArrowDown')\n next = Math.min(points.length - 1, current + 1);\n else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp')\n next = Math.max(0, current < 0 ? 0 : current - 1);\n else return;\n event.preventDefault();\n const point = points[next ?? 0] ?? points[0];\n this.resolveFocus(point.x, point.y, 'keyboard');\n };\n\n private renderNode(node: ChartSceneNode, className = 'chart__mark') {\n const resolvedClassName = node.className ?? className;\n const presentation = {\n fill: node.style.fill,\n stroke: node.style.stroke,\n opacity: node.style.opacity,\n 'fill-opacity': node.style.fillOpacity,\n 'stroke-opacity': node.style.strokeOpacity,\n 'stroke-width': node.style.strokeWidth,\n 'stroke-dasharray': node.style.strokeDasharray || undefined,\n };\n if (node.type === 'path')\n return (\n <path\n key={node.key}\n class={resolvedClassName}\n d={node.d}\n transform={node.transform}\n {...presentation}\n />\n );\n if (node.type === 'rect') {\n return (\n <rect\n key={node.key}\n class={resolvedClassName}\n x={node.x}\n y={node.y}\n width={node.width}\n height={node.height}\n rx={node.style.radius}\n {...presentation}\n />\n );\n }\n if (node.type === 'circle')\n return (\n <circle\n key={node.key}\n class={resolvedClassName}\n cx={node.x}\n cy={node.y}\n r={node.radius}\n {...presentation}\n />\n );\n if (node.type === 'line')\n return (\n <line\n key={node.key}\n class={resolvedClassName}\n x1={node.x1}\n x2={node.x2}\n y1={node.y1}\n y2={node.y2}\n {...presentation}\n />\n );\n return (\n <text\n key={node.key}\n class={node.className ?? `${className} chart__text-mark`}\n data-chart-measure={node.measure ? '' : undefined}\n x={node.x}\n y={node.y}\n dx={node.dx}\n dy={node.dy}\n text-anchor={node.style.textAnchor}\n font-family={node.style.fontFamily}\n font-size={node.style.fontSize}\n font-weight={node.style.fontWeight}\n dominant-baseline={node.dominantBaseline ?? 'middle'}\n transform={node.rotate ? `rotate(${node.rotate} ${node.x} ${node.y})` : undefined}\n {...presentation}\n >\n {node.text}\n </text>\n );\n }\n\n private renderMarkLayers(scene: ChartScene) {\n const layers: Array<{ clipped: boolean; nodes: ChartSceneNode[] }> = [];\n scene.nodes.forEach(node => {\n const clipped = scene.clip && node.clipToPlot !== false;\n const current = layers[layers.length - 1];\n if (current?.clipped === clipped) current.nodes.push(node);\n else layers.push({ clipped, nodes: [node] });\n });\n return layers.map((layer, index) => (\n <g\n key={`marks-${index}`}\n class=\"chart__marks\"\n aria-hidden=\"true\"\n clip-path={layer.clipped ? `url(#${this.clipId})` : undefined}\n >\n {layer.nodes.map(node => this.renderNode(node))}\n </g>\n ));\n }\n\n private focusedDotPoints(scene: ChartScene): ChartPoint[] {\n if (!this.focusState) return [];\n const focusDotPointKeys = new Set(scene.focusDotPointKeys);\n return this.focusState.points.filter(point => focusDotPointKeys.has(point.sceneKey));\n }\n\n private tooltipContent(): { heading?: string; items: ChartTooltipItem[] } | undefined {\n if (!this.scene || !this.focusState || this.scene.tooltip === false) return undefined;\n const options: ChartTooltipOptions =\n typeof this.scene.tooltip === 'object' ? this.scene.tooltip : {};\n const locale = this.locale ?? document.documentElement.lang ?? 'en';\n const points = this.focusState.points;\n const formatValue = (point: ChartPoint) => {\n const value = point.value ?? point.yValue;\n return (\n options.format?.(point, locale) ??\n (typeof value === 'number'\n ? new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(value)\n : String(value))\n );\n };\n return {\n heading:\n points.length > 1\n ? (options.formatGroupHeading?.(points, locale) ?? String(this.focusState.primary.xValue))\n : undefined,\n items: points.map(\n point =>\n options.formatGroupItem?.(point, locale) ?? {\n label: point.groupLabel ?? point.markId,\n value: formatValue(point),\n color: point.color,\n }\n ),\n };\n }\n\n private statusText(): string {\n const content = this.tooltipContent();\n if (!content) return '';\n return [content.heading, ...content.items.map(item => `${item.label}: ${item.value}`)]\n .filter(Boolean)\n .join('. ');\n }\n\n render() {\n const scene = this.scene;\n const tooltip = this.tooltipContent();\n const focus = this.focusState?.primary;\n const tooltipAnchor =\n focus &&\n this.focusState?.source === 'pointer' &&\n scene?.arcHitRegions.length &&\n this.pointerAnchor\n ? this.pointerAnchor\n : focus;\n const focusedDotPoints = scene ? this.focusedDotPoints(scene) : [];\n const groupedPointFocus =\n scene?.coordinate === 'cartesian' && scene.focus === 'group-x' && focusedDotPoints.length > 1;\n const center =\n focus && scene?.center?.focused\n ? scene.center.focused(focus, this.locale ?? document.documentElement.lang ?? 'en')\n : scene?.center;\n return (\n <Host\n class=\"chart\"\n style={{\n '--ds-chart-width': this.width ? `${this.width}px` : '100%',\n '--ds-chart-height':\n this.height || this.aspectRatio\n ? `${this.surfaceHeight}px`\n : 'var(--ds-chart-container-height, 320px)',\n }}\n >\n {this.description && (\n <span id={this.descriptionId} class=\"chart__description\">\n {this.description}\n </span>\n )}\n {scene && (\n /* eslint-disable-next-line local/prefer-ds-icon -- SVG is the chart renderer. */\n <svg\n class=\"chart__surface\"\n width={scene.width}\n height={scene.height}\n viewBox={`0 0 ${scene.width} ${scene.height}`}\n role=\"img\"\n aria-label={this.label || 'Chart'}\n aria-describedby={this.description ? this.descriptionId : undefined}\n tabindex={0}\n onPointerMove={this.onPointerMove}\n onPointerLeave={this.onPointerLeave}\n onKeyDown={this.onKeyDown}\n >\n <defs aria-hidden=\"true\">\n <clipPath id={this.clipId}>\n <rect\n x={scene.plot.left}\n y={scene.plot.top}\n width={scene.plot.width}\n height={scene.plot.height}\n />\n </clipPath>\n </defs>\n <rect\n class=\"chart__interaction-surface\"\n x=\"0\"\n y=\"0\"\n width={scene.width}\n height={scene.height}\n aria-hidden=\"true\"\n />\n <g class=\"chart__guides\" aria-hidden=\"true\">\n {scene.guides.map(node => this.renderNode(node, 'chart__grid chart__polar-guide'))}\n {scene.yAxis.line && (\n <line\n class=\"chart__axis-line\"\n x1={scene.plot.left}\n x2={scene.plot.left}\n y1={scene.plot.top}\n y2={scene.plot.bottom}\n />\n )}\n {scene.yAxis.ticks.map(tick => (\n <g key={`y-${tick.key}`}>\n {scene.yAxis.grid && (\n <line\n class=\"chart__grid\"\n x1={scene.plot.left}\n x2={scene.plot.right}\n y1={tick.position}\n y2={tick.position}\n />\n )}\n <line\n class=\"chart__axis-line chart__tick-stub\"\n x1={scene.plot.left - scene.yAxis.tickSize}\n x2={scene.plot.left}\n y1={tick.position}\n y2={tick.position}\n />\n {tick.labelVisible && (\n <text\n data-chart-measure\n class=\"chart__tick chart__tick--y\"\n x={scene.plot.left - scene.yAxis.tickSize - scene.yAxis.tickPadding}\n y={tick.position}\n text-anchor=\"end\"\n dominant-baseline=\"middle\"\n >\n {tick.label}\n </text>\n )}\n </g>\n ))}\n {scene.xAxis.line && (\n <line\n class=\"chart__axis-line\"\n x1={scene.plot.left}\n x2={scene.plot.right}\n y1={scene.plot.bottom}\n y2={scene.plot.bottom}\n />\n )}\n {scene.xAxis.ticks.map(tick => (\n <g key={`x-${tick.key}`}>\n {scene.xAxis.grid && (\n <line\n class=\"chart__grid\"\n x1={tick.position}\n x2={tick.position}\n y1={scene.plot.top}\n y2={scene.plot.bottom}\n />\n )}\n <line\n class=\"chart__axis-line chart__tick-stub\"\n x1={tick.position}\n x2={tick.position}\n y1={scene.plot.bottom}\n y2={scene.plot.bottom + scene.xAxis.tickSize}\n />\n {tick.labelVisible && (\n <text\n data-chart-measure\n class=\"chart__tick chart__tick--x\"\n x={tick.position}\n y={scene.plot.bottom + scene.xAxis.tickSize + scene.xAxis.tickPadding}\n text-anchor=\"middle\"\n dominant-baseline=\"hanging\"\n transform={\n scene.xAxis.rotate\n ? `rotate(${scene.xAxis.rotate} ${tick.position} ${scene.plot.bottom + scene.xAxis.tickSize + scene.xAxis.tickPadding})`\n : undefined\n }\n >\n {tick.label}\n </text>\n )}\n </g>\n ))}\n {scene.xAxis.label && (\n <text\n class=\"chart__axis-title\"\n x={(scene.plot.left + scene.plot.right) / 2}\n y={scene.height - 4}\n text-anchor=\"middle\"\n >\n {scene.xAxis.label}\n </text>\n )}\n {scene.yAxis.label && (\n <text\n class=\"chart__axis-title\"\n x=\"8\"\n y={(scene.plot.top + scene.plot.bottom) / 2}\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n transform={`rotate(-90 8 ${(scene.plot.top + scene.plot.bottom) / 2})`}\n >\n {scene.yAxis.label}\n </text>\n )}\n {scene.coordinate === 'cartesian' && (\n <g class=\"chart__plot-frame\">\n <line\n class=\"chart__plot-boundary chart__plot-boundary--top\"\n x1={scene.plot.left}\n x2={scene.plot.right}\n y1={scene.plot.top}\n y2={scene.plot.top}\n />\n <line\n class=\"chart__plot-boundary chart__plot-boundary--right\"\n x1={scene.plot.right}\n x2={scene.plot.right}\n y1={scene.plot.top}\n y2={scene.plot.bottom}\n />\n </g>\n )}\n </g>\n {this.renderMarkLayers(scene)}\n {groupedPointFocus && focus && (\n <line\n class=\"chart__focus-guide\"\n x1={focus.x}\n x2={focus.x}\n y1={scene.plot.top}\n y2={scene.plot.bottom}\n aria-hidden=\"true\"\n />\n )}\n {focusedDotPoints.map(point => (\n <circle\n key={`focus-${point.sceneKey}`}\n class=\"chart__focus-point\"\n cx={point.x}\n cy={point.y}\n r={scene.focusDotCircleRadius}\n fill={point.color}\n stroke=\"var(--color-background-primary)\"\n stroke-width={scene.focusDotHaloWidth}\n aria-hidden=\"true\"\n />\n ))}\n {focus && focusedDotPoints.length === 0 && (\n <circle class=\"chart__focus\" cx={focus.x} cy={focus.y} r=\"6\" aria-hidden=\"true\" />\n )}\n {center?.value && (\n <text\n class=\"chart__center-value\"\n x={(scene.plot.left + scene.plot.right) / 2}\n y={(scene.plot.top + scene.plot.bottom) / 2 - (center.caption ? 10 : 0)}\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n aria-hidden=\"true\"\n >\n {center.value}\n </text>\n )}\n {center?.caption && (\n <text\n class=\"chart__center-caption\"\n x={(scene.plot.left + scene.plot.right) / 2}\n y={(scene.plot.top + scene.plot.bottom) / 2 + 14}\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n aria-hidden=\"true\"\n >\n {center.caption}\n </text>\n )}\n </svg>\n )}\n {tooltip && tooltipAnchor && (\n <ds-tooltip-chart\n heading={tooltip.heading}\n items={tooltip.items}\n x={tooltipAnchor.x}\n y={tooltipAnchor.y}\n />\n )}\n <span class=\"chart__status\" aria-live=\"polite\" aria-atomic=\"true\">\n {this.statusText()}\n </span>\n </Host>\n );\n }\n}\n",
264
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n Prop,\n State,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport type {\n ChartDefinition,\n ChartPoint,\n ChartTooltipItem,\n ChartTooltipOptions,\n} from '../../utils/chart-grammar';\nimport {\n compileChartScene,\n findChartFocus,\n type ChartScene,\n type ChartSceneNode,\n} from '../../utils/chart-scene';\nimport { resolveChartTheme } from '../../utils/chart-theme';\n\nconst DEFAULT_WIDTH = 480;\nconst DEFAULT_HEIGHT = 320;\n\nexport type ChartFocusSource = 'pointer' | 'keyboard';\n\nexport interface ChartFocusChangeDetail {\n primary: ChartPoint | null;\n points: readonly ChartPoint[];\n source: ChartFocusSource;\n}\n\ninterface FocusState {\n primary: ChartPoint;\n points: ChartPoint[];\n source: ChartFocusSource;\n}\n\ninterface PointerAnchor {\n x: number;\n y: number;\n}\n\n@Component({\n tag: 'ds-chart',\n styleUrl: 'Chart.css',\n scoped: true,\n})\nexport class Chart {\n @Element() el!: HTMLElement;\n\n /** Typed chart definition. Set as a JavaScript property, not an HTML attribute. */\n @Prop() definition!: ChartDefinition;\n /** Required accessible name for the visualization. */\n @Prop() label!: string;\n /** Optional longer accessible description. */\n @Prop() description?: string;\n /** Locale used by axes and default tooltip formatters. */\n @Prop() locale?: string;\n /** Fixed surface width. Otherwise the chart observes its container. */\n @Prop() width?: number;\n /** Product-owned surface height. Wins over aspectRatio. */\n @Prop() height?: number;\n /** Derives height from the current width when height is not supplied. */\n @Prop() aspectRatio?: number;\n\n /** Fires whenever pointer or keyboard focus resolves to semantic chart points. */\n @Event() dsChartFocusChange!: EventEmitter<ChartFocusChangeDetail>;\n\n @State() private surfaceWidth = DEFAULT_WIDTH;\n @State() private containerHeight?: number;\n @State() private scene?: ChartScene;\n @State() private focusState?: FocusState;\n @State() private pointerAnchor?: PointerAnchor;\n\n private resizeObserver?: ResizeObserver;\n private resizeFrame?: number;\n private compileFrame?: number;\n private warnedConflictingHeight = false;\n private measuredLabels = new Map<string, { width: number; height: number }>();\n private descriptionId = `ds-chart-description-${Math.random().toString(36).slice(2)}`;\n private clipId = `ds-chart-clip-${Math.random().toString(36).slice(2)}`;\n\n componentWillLoad() {\n if (this.width && this.width > 0) this.surfaceWidth = this.width;\n this.compile();\n }\n\n componentDidLoad() {\n this.connectResizeObserver();\n this.waitForFonts();\n }\n\n componentDidRender() {\n this.measureRenderedLabels();\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n if (this.resizeFrame) cancelAnimationFrame(this.resizeFrame);\n if (this.compileFrame) cancelAnimationFrame(this.compileFrame);\n }\n\n @Watch('definition')\n @Watch('locale')\n @Watch('height')\n @Watch('aspectRatio')\n onDefinitionInputChange() {\n this.scheduleCompile();\n }\n\n @Watch('width')\n onWidthChange() {\n if (this.width && this.width > 0) this.surfaceWidth = this.width;\n else this.measureHost();\n this.scheduleCompile();\n }\n\n private get surfaceHeight(): number {\n if (this.height && this.height > 0) {\n if (this.aspectRatio && !this.warnedConflictingHeight) {\n this.warnedConflictingHeight = true;\n console.warn('[ds-chart] height wins when both height and aspectRatio are supplied.');\n }\n return this.height;\n }\n if (this.aspectRatio && this.aspectRatio > 0) {\n return Math.max(1, this.surfaceWidth / this.aspectRatio);\n }\n return this.containerHeight ?? DEFAULT_HEIGHT;\n }\n\n private connectResizeObserver() {\n this.resizeObserver?.disconnect();\n if (typeof ResizeObserver === 'undefined') return;\n this.resizeObserver = new ResizeObserver(entries => {\n const entry = entries[0];\n if (!entry) return;\n if (this.resizeFrame) cancelAnimationFrame(this.resizeFrame);\n this.resizeFrame = requestAnimationFrame(() => {\n this.resizeFrame = undefined;\n this.applyMeasuredSize(entry.contentRect.width, entry.contentRect.height);\n });\n });\n this.resizeObserver.observe(this.el);\n this.measureHost();\n }\n\n private measureHost() {\n const bounds = this.el.getBoundingClientRect();\n this.applyMeasuredSize(bounds.width, bounds.height);\n }\n\n private applyMeasuredSize(width: number, height: number) {\n let changed = false;\n if (!this.width && width > 0) {\n const nextWidth = Math.round(width * 100) / 100;\n if (nextWidth !== this.surfaceWidth) {\n this.surfaceWidth = nextWidth;\n changed = true;\n }\n }\n if (!this.height && !this.aspectRatio && height > 0) {\n const nextHeight = Math.round(height * 100) / 100;\n if (nextHeight !== this.containerHeight) {\n this.containerHeight = nextHeight;\n changed = true;\n }\n }\n if (changed) {\n this.scheduleCompile();\n }\n }\n\n private scheduleCompile() {\n if (this.compileFrame) cancelAnimationFrame(this.compileFrame);\n this.compileFrame = requestAnimationFrame(() => {\n this.compileFrame = undefined;\n this.compile();\n });\n }\n\n private compile() {\n if (!this.definition || !(this.surfaceWidth > 0) || !(this.surfaceHeight > 0)) return;\n const previousKey = this.focusState?.primary.sceneKey;\n const scene = compileChartScene(\n this.definition,\n this.surfaceWidth,\n this.surfaceHeight,\n this.locale ?? document.documentElement.lang ?? 'en',\n text => this.measuredLabels.get(text) ?? { width: text.length * 7, height: 14 },\n resolveChartTheme(this.el)\n );\n this.scene = scene;\n if (previousKey) {\n const restored = scene.points.find(point => point.sceneKey === previousKey);\n if (restored && this.focusState) {\n const grouped = findChartFocus(\n scene.points,\n scene.focus,\n restored.x,\n restored.y,\n scene.maxFocusDistance,\n scene.arcHitRegions\n );\n this.focusState = grouped ? { ...grouped, source: this.focusState.source } : undefined;\n } else {\n this.focusState = undefined;\n }\n }\n }\n\n private waitForFonts() {\n if (!document.fonts) return;\n void document.fonts.ready.then(() => {\n this.measuredLabels.clear();\n this.scheduleCompile();\n });\n }\n\n private measureRenderedLabels() {\n const labels = this.el.querySelectorAll<SVGTextElement>('[data-chart-measure]');\n let changed = false;\n labels.forEach(label => {\n const text = label.textContent ?? '';\n let box: DOMRect;\n try {\n box = label.getBBox();\n } catch {\n return;\n }\n const previous = this.measuredLabels.get(text);\n if (\n !previous ||\n Math.abs(previous.width - box.width) > 0.5 ||\n Math.abs(previous.height - box.height) > 0.5\n ) {\n this.measuredLabels.set(text, { width: box.width, height: box.height });\n changed = true;\n }\n });\n if (changed) this.scheduleCompile();\n }\n\n private resolveFocus(x: number, y: number, source: ChartFocusSource) {\n if (!this.scene) return;\n const result = findChartFocus(\n this.scene.points,\n this.scene.focus,\n x,\n y,\n this.scene.maxFocusDistance,\n this.scene.arcHitRegions\n );\n if (!result) {\n this.clearFocus(source);\n return;\n }\n if (\n this.focusState?.primary.sceneKey === result.primary.sceneKey &&\n this.focusState.source === source\n )\n return;\n this.focusState = { ...result, source };\n this.dsChartFocusChange.emit({ ...result, source });\n }\n\n private clearFocus(source: ChartFocusSource) {\n if (!this.focusState) return;\n this.focusState = undefined;\n this.dsChartFocusChange.emit({ primary: null, points: [], source });\n }\n\n private onPointerMove = (event: PointerEvent) => {\n const svg = event.currentTarget as SVGSVGElement;\n const rect = svg.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n this.pointerAnchor = this.scene?.arcHitRegions.length ? { x, y } : undefined;\n this.resolveFocus(x, y, 'pointer');\n };\n\n private onPointerLeave = () => {\n this.pointerAnchor = undefined;\n this.clearFocus('pointer');\n };\n\n private keyboardPoints(): ChartPoint[] {\n if (!this.scene) return [];\n return [...this.scene.points].sort((a, b) => a.x - b.x || a.y - b.y);\n }\n\n private onKeyDown = (event: KeyboardEvent) => {\n const points = this.keyboardPoints();\n if (points.length === 0) return;\n if (event.key === 'Escape') {\n event.preventDefault();\n this.clearFocus('keyboard');\n return;\n }\n const current = this.focusState\n ? points.findIndex(point => point.sceneKey === this.focusState?.primary.sceneKey)\n : -1;\n let next: number | undefined;\n if (event.key === 'Home') next = 0;\n else if (event.key === 'End') next = points.length - 1;\n else if (event.key === 'ArrowRight' || event.key === 'ArrowDown')\n next = Math.min(points.length - 1, current + 1);\n else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp')\n next = Math.max(0, current < 0 ? 0 : current - 1);\n else return;\n event.preventDefault();\n const point = points[next ?? 0] ?? points[0];\n this.resolveFocus(point.x, point.y, 'keyboard');\n };\n\n private renderNode(node: ChartSceneNode, className = 'chart__mark') {\n const resolvedClassName = node.className ?? className;\n const presentation = {\n fill: node.style.fill,\n stroke: node.style.stroke,\n opacity: node.style.opacity,\n 'fill-opacity': node.style.fillOpacity,\n 'stroke-opacity': node.style.strokeOpacity,\n 'stroke-width': node.style.strokeWidth,\n 'stroke-dasharray': node.style.strokeDasharray || undefined,\n };\n if (node.type === 'path')\n return (\n <path\n key={node.key}\n class={resolvedClassName}\n d={node.d}\n transform={node.transform}\n {...presentation}\n />\n );\n if (node.type === 'rect') {\n return (\n <rect\n key={node.key}\n class={resolvedClassName}\n x={node.x}\n y={node.y}\n width={node.width}\n height={node.height}\n rx={node.style.radius}\n {...presentation}\n />\n );\n }\n if (node.type === 'circle')\n return (\n <circle\n key={node.key}\n class={resolvedClassName}\n cx={node.x}\n cy={node.y}\n r={node.radius}\n {...presentation}\n />\n );\n if (node.type === 'line')\n return (\n <line\n key={node.key}\n class={resolvedClassName}\n x1={node.x1}\n x2={node.x2}\n y1={node.y1}\n y2={node.y2}\n {...presentation}\n />\n );\n return (\n <text\n key={node.key}\n class={node.className ?? `${className} chart__text-mark`}\n data-chart-measure={node.measure ? '' : undefined}\n x={node.x}\n y={node.y}\n dx={node.dx}\n dy={node.dy}\n text-anchor={node.style.textAnchor}\n font-family={node.style.fontFamily}\n font-size={node.style.fontSize}\n font-weight={node.style.fontWeight}\n dominant-baseline={node.dominantBaseline ?? 'middle'}\n transform={node.rotate ? `rotate(${node.rotate} ${node.x} ${node.y})` : undefined}\n {...presentation}\n >\n {node.text}\n </text>\n );\n }\n\n private renderMarkLayers(scene: ChartScene) {\n const layers: Array<{ clipped: boolean; nodes: ChartSceneNode[] }> = [];\n scene.nodes.forEach(node => {\n const clipped = scene.clip && node.clipToPlot !== false;\n const current = layers[layers.length - 1];\n if (current?.clipped === clipped) current.nodes.push(node);\n else layers.push({ clipped, nodes: [node] });\n });\n return layers.map((layer, index) => (\n <g\n key={`marks-${index}`}\n class=\"chart__marks\"\n aria-hidden=\"true\"\n clip-path={layer.clipped ? `url(#${this.clipId})` : undefined}\n >\n {layer.nodes.map(node => this.renderNode(node))}\n </g>\n ));\n }\n\n private focusedDotPoints(scene: ChartScene): ChartPoint[] {\n if (!this.focusState) return [];\n const focusDotPointKeys = new Set(scene.focusDotPointKeys);\n return this.focusState.points.filter(point => focusDotPointKeys.has(point.sceneKey));\n }\n\n private tooltipContent(): { heading?: string; items: ChartTooltipItem[] } | undefined {\n if (!this.scene || !this.focusState || this.scene.tooltip === false) return undefined;\n const options: ChartTooltipOptions =\n typeof this.scene.tooltip === 'object' ? this.scene.tooltip : {};\n const locale = this.locale ?? document.documentElement.lang ?? 'en';\n const points = this.focusState.points;\n const formatValue = (point: ChartPoint) => {\n const value = point.value ?? point.yValue;\n return (\n options.format?.(point, locale) ??\n (typeof value === 'number'\n ? new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(value)\n : String(value))\n );\n };\n return {\n heading:\n points.length > 1\n ? (options.formatGroupHeading?.(points, locale) ?? String(this.focusState.primary.xValue))\n : undefined,\n items: points.map(\n point =>\n options.formatGroupItem?.(point, locale) ?? {\n label: point.groupLabel ?? point.markId,\n value: formatValue(point),\n color: point.color,\n }\n ),\n };\n }\n\n private statusText(): string {\n const content = this.tooltipContent();\n if (!content) return '';\n return [content.heading, ...content.items.map(item => `${item.label}: ${item.value}`)]\n .filter(Boolean)\n .join('. ');\n }\n\n render() {\n const scene = this.scene;\n const tooltip = this.tooltipContent();\n const focus = this.focusState?.primary;\n const tooltipAnchor =\n focus &&\n this.focusState?.source === 'pointer' &&\n scene?.arcHitRegions.length &&\n this.pointerAnchor\n ? this.pointerAnchor\n : focus;\n const focusedDotPoints = scene ? this.focusedDotPoints(scene) : [];\n const groupedPointFocus =\n scene?.coordinate === 'cartesian' && scene.focus === 'group-x' && focusedDotPoints.length > 1;\n const center =\n focus && scene?.center?.focused\n ? scene.center.focused(focus, this.locale ?? document.documentElement.lang ?? 'en')\n : scene?.center;\n return (\n <Host\n class=\"chart\"\n style={{\n '--ds-chart-width': this.width ? `${this.width}px` : '100%',\n '--ds-chart-height':\n this.height || this.aspectRatio\n ? `${this.surfaceHeight}px`\n : 'var(--ds-chart-container-height, 320px)',\n }}\n >\n {this.description && (\n <span id={this.descriptionId} class=\"chart__description\">\n {this.description}\n </span>\n )}\n {scene && (\n /* eslint-disable-next-line compomo/prefer-ds-icon -- SVG is the chart renderer. */\n <svg\n class=\"chart__surface\"\n width={scene.width}\n height={scene.height}\n viewBox={`0 0 ${scene.width} ${scene.height}`}\n role=\"img\"\n aria-label={this.label || 'Chart'}\n aria-describedby={this.description ? this.descriptionId : undefined}\n tabindex={0}\n onPointerMove={this.onPointerMove}\n onPointerLeave={this.onPointerLeave}\n onKeyDown={this.onKeyDown}\n >\n <defs aria-hidden=\"true\">\n <clipPath id={this.clipId}>\n <rect\n x={scene.plot.left}\n y={scene.plot.top}\n width={scene.plot.width}\n height={scene.plot.height}\n />\n </clipPath>\n </defs>\n <rect\n class=\"chart__interaction-surface\"\n x=\"0\"\n y=\"0\"\n width={scene.width}\n height={scene.height}\n aria-hidden=\"true\"\n />\n <g class=\"chart__guides\" aria-hidden=\"true\">\n {scene.guides.map(node => this.renderNode(node, 'chart__grid chart__polar-guide'))}\n {scene.yAxis.line && (\n <line\n class=\"chart__axis-line\"\n x1={scene.plot.left}\n x2={scene.plot.left}\n y1={scene.plot.top}\n y2={scene.plot.bottom}\n />\n )}\n {scene.yAxis.ticks.map(tick => (\n <g key={`y-${tick.key}`}>\n {scene.yAxis.grid && (\n <line\n class=\"chart__grid\"\n x1={scene.plot.left}\n x2={scene.plot.right}\n y1={tick.position}\n y2={tick.position}\n />\n )}\n <line\n class=\"chart__axis-line chart__tick-stub\"\n x1={scene.plot.left - scene.yAxis.tickSize}\n x2={scene.plot.left}\n y1={tick.position}\n y2={tick.position}\n />\n {tick.labelVisible && (\n <text\n data-chart-measure\n class=\"chart__tick chart__tick--y\"\n x={scene.plot.left - scene.yAxis.tickSize - scene.yAxis.tickPadding}\n y={tick.position}\n text-anchor=\"end\"\n dominant-baseline=\"middle\"\n >\n {tick.label}\n </text>\n )}\n </g>\n ))}\n {scene.xAxis.line && (\n <line\n class=\"chart__axis-line\"\n x1={scene.plot.left}\n x2={scene.plot.right}\n y1={scene.plot.bottom}\n y2={scene.plot.bottom}\n />\n )}\n {scene.xAxis.ticks.map(tick => (\n <g key={`x-${tick.key}`}>\n {scene.xAxis.grid && (\n <line\n class=\"chart__grid\"\n x1={tick.position}\n x2={tick.position}\n y1={scene.plot.top}\n y2={scene.plot.bottom}\n />\n )}\n <line\n class=\"chart__axis-line chart__tick-stub\"\n x1={tick.position}\n x2={tick.position}\n y1={scene.plot.bottom}\n y2={scene.plot.bottom + scene.xAxis.tickSize}\n />\n {tick.labelVisible && (\n <text\n data-chart-measure\n class=\"chart__tick chart__tick--x\"\n x={tick.position}\n y={scene.plot.bottom + scene.xAxis.tickSize + scene.xAxis.tickPadding}\n text-anchor=\"middle\"\n dominant-baseline=\"hanging\"\n transform={\n scene.xAxis.rotate\n ? `rotate(${scene.xAxis.rotate} ${tick.position} ${scene.plot.bottom + scene.xAxis.tickSize + scene.xAxis.tickPadding})`\n : undefined\n }\n >\n {tick.label}\n </text>\n )}\n </g>\n ))}\n {scene.xAxis.label && (\n <text\n class=\"chart__axis-title\"\n x={(scene.plot.left + scene.plot.right) / 2}\n y={scene.height - 4}\n text-anchor=\"middle\"\n >\n {scene.xAxis.label}\n </text>\n )}\n {scene.yAxis.label && (\n <text\n class=\"chart__axis-title\"\n x=\"8\"\n y={(scene.plot.top + scene.plot.bottom) / 2}\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n transform={`rotate(-90 8 ${(scene.plot.top + scene.plot.bottom) / 2})`}\n >\n {scene.yAxis.label}\n </text>\n )}\n {scene.coordinate === 'cartesian' && (\n <g class=\"chart__plot-frame\">\n <line\n class=\"chart__plot-boundary chart__plot-boundary--top\"\n x1={scene.plot.left}\n x2={scene.plot.right}\n y1={scene.plot.top}\n y2={scene.plot.top}\n />\n <line\n class=\"chart__plot-boundary chart__plot-boundary--right\"\n x1={scene.plot.right}\n x2={scene.plot.right}\n y1={scene.plot.top}\n y2={scene.plot.bottom}\n />\n </g>\n )}\n </g>\n {this.renderMarkLayers(scene)}\n {groupedPointFocus && focus && (\n <line\n class=\"chart__focus-guide\"\n x1={focus.x}\n x2={focus.x}\n y1={scene.plot.top}\n y2={scene.plot.bottom}\n aria-hidden=\"true\"\n />\n )}\n {focusedDotPoints.map(point => (\n <circle\n key={`focus-${point.sceneKey}`}\n class=\"chart__focus-point\"\n cx={point.x}\n cy={point.y}\n r={scene.focusDotCircleRadius}\n fill={point.color}\n stroke=\"var(--color-background-primary)\"\n stroke-width={scene.focusDotHaloWidth}\n aria-hidden=\"true\"\n />\n ))}\n {focus && focusedDotPoints.length === 0 && (\n <circle class=\"chart__focus\" cx={focus.x} cy={focus.y} r=\"6\" aria-hidden=\"true\" />\n )}\n {center?.value && (\n <text\n class=\"chart__center-value\"\n x={(scene.plot.left + scene.plot.right) / 2}\n y={(scene.plot.top + scene.plot.bottom) / 2 - (center.caption ? 10 : 0)}\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n aria-hidden=\"true\"\n >\n {center.value}\n </text>\n )}\n {center?.caption && (\n <text\n class=\"chart__center-caption\"\n x={(scene.plot.left + scene.plot.right) / 2}\n y={(scene.plot.top + scene.plot.bottom) / 2 + 14}\n text-anchor=\"middle\"\n dominant-baseline=\"middle\"\n aria-hidden=\"true\"\n >\n {center.caption}\n </text>\n )}\n </svg>\n )}\n {tooltip && tooltipAnchor && (\n <ds-tooltip-chart\n heading={tooltip.heading}\n items={tooltip.items}\n x={tooltipAnchor.x}\n y={tooltipAnchor.y}\n />\n )}\n <span class=\"chart__status\" aria-live=\"polite\" aria-atomic=\"true\">\n {this.statusText()}\n </span>\n </Host>\n );\n }\n}\n",
260
265
  "type": "registry:ui"
261
266
  }
262
267
  ]
@@ -88,7 +88,43 @@
88
88
  "label": "Cross-browser form and accessibility coverage",
89
89
  "path": "tests/e2e/forms.spec.ts"
90
90
  }
91
- ]
91
+ ],
92
+ "styling": {
93
+ "rationale": "The component owns the protected recipes below; unspecified host properties and application layout remain customizable.",
94
+ "protected": [
95
+ {
96
+ "properties": [
97
+ "font",
98
+ "font-size",
99
+ "font-weight",
100
+ "font-family",
101
+ "line-height",
102
+ "letter-spacing",
103
+ "padding",
104
+ "border-radius"
105
+ ],
106
+ "reason": "The field primitive owns its density and selection anatomy; use size and presentation props without replacing the recipe.",
107
+ "props": [
108
+ "size",
109
+ "presentation"
110
+ ]
111
+ },
112
+ {
113
+ "properties": [
114
+ "background",
115
+ "border",
116
+ "box-shadow",
117
+ "color"
118
+ ],
119
+ "reason": "The component owns its surface and interaction treatment; use its semantic presentation props or compose an application-owned surrounding surface.",
120
+ "props": [
121
+ "size",
122
+ "presentation"
123
+ ]
124
+ }
125
+ ],
126
+ "customProperties": []
127
+ }
92
128
  },
93
129
  "api": {
94
130
  "props": {
@@ -379,7 +415,7 @@
379
415
  },
380
416
  {
381
417
  "path": "src/wc/components/Checkbox/Checkbox.tsx",
382
- "content": "import {\n AttachInternals,\n Component,\n Prop,\n State,\n Event,\n EventEmitter,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport {\n CONTROL_SUPPORTING_TEXT_VARIANT,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n setFormControlValue,\n setRequiredValidity,\n} from '../../utils';\n\nexport type CheckboxSize = 'lg' | 'md' | 'sm' | 'xs';\n\nlet idCounter = 0;\n\n@Component({\n tag: 'ds-checkbox',\n styleUrl: 'Checkbox.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Checkbox {\n @AttachInternals() internals!: ElementInternals;\n private labelId = `ds-checkbox-label-${++idCounter}`;\n private descriptionId = `${this.labelId}-description`;\n\n /** Visible label and accessible name. Omitted only in presentation mode. */\n @Prop() label!: string;\n /** Optional supporting description associated with the checkbox. */\n @Prop() description?: string;\n /** Current checked state. */\n @Prop({ mutable: true }) checked: boolean = false;\n /** Visual and placement density. */\n @Prop() size: CheckboxSize = 'md';\n /** Native form field name. */\n @Prop({ reflect: true }) name: string | undefined;\n /** Submitted value when checked. */\n @Prop() value: string = 'on';\n /** Native disabled state. */\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Require the checkbox to be checked for form validity. */\n @Prop({ reflect: true }) required: boolean = false;\n /** Validation message used when a required checkbox is unchecked. */\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n /** Mixed visual state. Activation clears it before toggling checked. */\n @Prop({ mutable: true }) indeterminate: boolean = false;\n /** Design-system inactive state. */\n @Prop() isInactive: boolean = false;\n /** Visual-only indicator for a composite control that owns selection semantics. */\n @Prop() presentation: boolean = false;\n\n /** Emitted after user activation with the new checked state. */\n @Event() dsChange!: EventEmitter<boolean>;\n\n private initialChecked = false;\n private initialIndeterminate = false;\n @State() private formDisabled = false;\n\n componentWillLoad() {\n this.initialChecked = this.checked;\n this.initialIndeterminate = this.indeterminate;\n this.syncFormValue();\n }\n\n @Watch('checked')\n @Watch('value')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n @Watch('presentation')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled || this.presentation;\n setFormControlValue(this.internals, this.checked ? this.value : null, { inactive });\n const missing = this.required && !inactive && !this.checked;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.checked = this.initialChecked;\n this.indeterminate = this.initialIndeterminate;\n }\n\n private handleActivate = () => {\n if (this.isInactive || this.disabled || this.formDisabled || this.presentation) return;\n if (this.indeterminate) this.indeterminate = false;\n this.checked = !this.checked;\n this.dsChange.emit(this.checked);\n };\n\n private handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.handleActivate();\n }\n };\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const isMarked = this.checked || this.indeterminate;\n const invalid = this.required && !inactive && !this.checked;\n\n return (\n <Host\n role={this.presentation ? undefined : 'checkbox'}\n aria-checked={\n this.presentation ? undefined : this.indeterminate ? 'mixed' : String(this.checked)\n }\n aria-disabled={!this.presentation && inactive ? 'true' : undefined}\n aria-required={!this.presentation && this.required ? 'true' : undefined}\n aria-invalid={!this.presentation && invalid ? 'true' : undefined}\n aria-labelledby={this.presentation ? undefined : this.labelId}\n aria-describedby={!this.presentation && this.description ? this.descriptionId : undefined}\n aria-hidden={this.presentation ? 'true' : undefined}\n tabIndex={this.presentation || inactive ? -1 : 0}\n class={{\n checkbox: true,\n 'checkbox--presentation': this.presentation,\n 'checkbox--described': Boolean(this.description),\n [`checkbox--${this.size}`]: true,\n [`ds-control--${this.size}`]: true,\n 'ds-control-inactive': inactive && !this.presentation,\n 'ds-focus-ring-inset': !this.presentation,\n 'ds-interaction-fill': !inactive && !this.presentation,\n }}\n onClick={this.presentation ? undefined : this.handleActivate}\n onKeyDown={this.presentation ? undefined : this.handleKeyDown}\n >\n <span class=\"checkbox__placement ds-interaction-fill__content\" aria-hidden=\"true\">\n <span class={{ box: true, 'box--marked': isMarked }}>\n {isMarked && (\n // eslint-disable-next-line local/prefer-ds-icon -- Checkbox owns this fixed state-mark geometry.\n <svg class=\"checkbox__mark\" viewBox=\"0 0 16 16\" fill=\"currentColor\" focusable=\"false\">\n <path\n d={\n this.indeterminate\n ? 'M12 7.375V8.625H4V7.375H12Z'\n : 'M12.9756 4.65527L7.22559 11.4053C7.11258 11.5379 6.94946 11.6169 6.77539 11.624C6.60111 11.631 6.43095 11.5657 6.30762 11.4424L3.05762 8.19238L3.94238 7.30762L6.71289 10.0781L12.0244 3.84473L12.9756 4.65527Z'\n }\n />\n </svg>\n )}\n </span>\n </span>\n {!this.presentation && (\n <span class=\"checkbox__copy ds-interaction-fill__content\">\n <ds-text\n class=\"checkbox__label\"\n as=\"span\"\n variant={CONTROL_TEXT_VARIANT[this.size]}\n textId={this.labelId}\n >\n {this.label}\n </ds-text>\n {this.description ? (\n <ds-text\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"secondary\"\n textId={this.descriptionId}\n >\n {this.description}\n </ds-text>\n ) : null}\n </span>\n )}\n </Host>\n );\n }\n}\n",
418
+ "content": "import {\n AttachInternals,\n Component,\n Prop,\n State,\n Event,\n EventEmitter,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport {\n CONTROL_SUPPORTING_TEXT_VARIANT,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n setFormControlValue,\n setRequiredValidity,\n} from '../../utils';\n\nexport type CheckboxSize = 'lg' | 'md' | 'sm' | 'xs';\n\nlet idCounter = 0;\n\n@Component({\n tag: 'ds-checkbox',\n styleUrl: 'Checkbox.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Checkbox {\n @AttachInternals() internals!: ElementInternals;\n private labelId = `ds-checkbox-label-${++idCounter}`;\n private descriptionId = `${this.labelId}-description`;\n\n /** Visible label and accessible name. Omitted only in presentation mode. */\n @Prop() label!: string;\n /** Optional supporting description associated with the checkbox. */\n @Prop() description?: string;\n /** Current checked state. */\n @Prop({ mutable: true }) checked: boolean = false;\n /** Visual and placement density. */\n @Prop() size: CheckboxSize = 'md';\n /** Native form field name. */\n @Prop({ reflect: true }) name: string | undefined;\n /** Submitted value when checked. */\n @Prop() value: string = 'on';\n /** Native disabled state. */\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Require the checkbox to be checked for form validity. */\n @Prop({ reflect: true }) required: boolean = false;\n /** Validation message used when a required checkbox is unchecked. */\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n /** Mixed visual state. Activation clears it before toggling checked. */\n @Prop({ mutable: true }) indeterminate: boolean = false;\n /** Design-system inactive state. */\n @Prop() isInactive: boolean = false;\n /** Visual-only indicator for a composite control that owns selection semantics. */\n @Prop() presentation: boolean = false;\n\n /** Emitted after user activation with the new checked state. */\n @Event() dsChange!: EventEmitter<boolean>;\n\n private initialChecked = false;\n private initialIndeterminate = false;\n @State() private formDisabled = false;\n\n componentWillLoad() {\n this.initialChecked = this.checked;\n this.initialIndeterminate = this.indeterminate;\n this.syncFormValue();\n }\n\n @Watch('checked')\n @Watch('value')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n @Watch('presentation')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled || this.presentation;\n setFormControlValue(this.internals, this.checked ? this.value : null, { inactive });\n const missing = this.required && !inactive && !this.checked;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.checked = this.initialChecked;\n this.indeterminate = this.initialIndeterminate;\n }\n\n private handleActivate = () => {\n if (this.isInactive || this.disabled || this.formDisabled || this.presentation) return;\n if (this.indeterminate) this.indeterminate = false;\n this.checked = !this.checked;\n this.dsChange.emit(this.checked);\n };\n\n private handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.handleActivate();\n }\n };\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const isMarked = this.checked || this.indeterminate;\n const invalid = this.required && !inactive && !this.checked;\n\n return (\n <Host\n role={this.presentation ? undefined : 'checkbox'}\n aria-checked={\n this.presentation ? undefined : this.indeterminate ? 'mixed' : String(this.checked)\n }\n aria-disabled={!this.presentation && inactive ? 'true' : undefined}\n aria-required={!this.presentation && this.required ? 'true' : undefined}\n aria-invalid={!this.presentation && invalid ? 'true' : undefined}\n aria-labelledby={this.presentation ? undefined : this.labelId}\n aria-describedby={!this.presentation && this.description ? this.descriptionId : undefined}\n aria-hidden={this.presentation ? 'true' : undefined}\n tabIndex={this.presentation || inactive ? -1 : 0}\n class={{\n checkbox: true,\n 'checkbox--presentation': this.presentation,\n 'checkbox--described': Boolean(this.description),\n [`checkbox--${this.size}`]: true,\n [`ds-control--${this.size}`]: true,\n 'ds-control-inactive': inactive && !this.presentation,\n 'ds-focus-ring-inset': !this.presentation,\n 'ds-interaction-fill': !inactive && !this.presentation,\n }}\n onClick={this.presentation ? undefined : this.handleActivate}\n onKeyDown={this.presentation ? undefined : this.handleKeyDown}\n >\n <span class=\"checkbox__placement ds-interaction-fill__content\" aria-hidden=\"true\">\n <span class={{ box: true, 'box--marked': isMarked }}>\n {isMarked && (\n // eslint-disable-next-line compomo/prefer-ds-icon -- Checkbox owns this fixed state-mark geometry.\n <svg class=\"checkbox__mark\" viewBox=\"0 0 16 16\" fill=\"currentColor\" focusable=\"false\">\n <path\n d={\n this.indeterminate\n ? 'M12 7.375V8.625H4V7.375H12Z'\n : 'M12.9756 4.65527L7.22559 11.4053C7.11258 11.5379 6.94946 11.6169 6.77539 11.624C6.60111 11.631 6.43095 11.5657 6.30762 11.4424L3.05762 8.19238L3.94238 7.30762L6.71289 10.0781L12.0244 3.84473L12.9756 4.65527Z'\n }\n />\n </svg>\n )}\n </span>\n </span>\n {!this.presentation && (\n <span class=\"checkbox__copy ds-interaction-fill__content\">\n <ds-text\n class=\"checkbox__label\"\n as=\"span\"\n variant={CONTROL_TEXT_VARIANT[this.size]}\n textId={this.labelId}\n >\n {this.label}\n </ds-text>\n {this.description ? (\n <ds-text\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"secondary\"\n textId={this.descriptionId}\n >\n {this.description}\n </ds-text>\n ) : null}\n </span>\n )}\n </Host>\n );\n }\n}\n",
383
419
  "type": "registry:ui"
384
420
  }
385
421
  ]
@@ -81,7 +81,56 @@
81
81
  "label": "Storybook examples",
82
82
  "path": "src/wc/components/Chip/Chip.stories.ts"
83
83
  }
84
- ]
84
+ ],
85
+ "styling": {
86
+ "rationale": "The component owns the protected recipes below; unspecified host properties and application layout remain customizable.",
87
+ "protected": [
88
+ {
89
+ "properties": [
90
+ "font",
91
+ "font-size",
92
+ "font-weight",
93
+ "font-family",
94
+ "line-height",
95
+ "letter-spacing",
96
+ "height",
97
+ "min-height",
98
+ "max-height",
99
+ "padding",
100
+ "gap"
101
+ ],
102
+ "reason": "The control density is one atomic recipe; use the size and inset props instead of replacing internal metrics.",
103
+ "props": [
104
+ "size",
105
+ "isInset",
106
+ "insetDepth"
107
+ ]
108
+ },
109
+ {
110
+ "properties": [
111
+ "border-radius"
112
+ ],
113
+ "reason": "The component owns its corner treatment; select the supported radius treatment.",
114
+ "props": [
115
+ "rounded",
116
+ "size"
117
+ ]
118
+ },
119
+ {
120
+ "properties": [
121
+ "background",
122
+ "border",
123
+ "box-shadow",
124
+ "color"
125
+ ],
126
+ "reason": "The component owns its surface and interaction treatment; use its semantic presentation props or compose an application-owned surrounding surface.",
127
+ "props": [
128
+ "size"
129
+ ]
130
+ }
131
+ ],
132
+ "customProperties": []
133
+ }
85
134
  },
86
135
  "api": {
87
136
  "props": {
@@ -44,7 +44,36 @@
44
44
  "label": "Font ownership and loading",
45
45
  "path": "docs/font-ownership.md"
46
46
  }
47
- ]
47
+ ],
48
+ "styling": {
49
+ "rationale": "The component owns the protected recipes below; unspecified host properties and application layout remain customizable.",
50
+ "protected": [
51
+ {
52
+ "properties": [
53
+ "background",
54
+ "border",
55
+ "box-shadow",
56
+ "color"
57
+ ],
58
+ "reason": "The component owns its surface and interaction treatment; use its semantic presentation props or compose an application-owned surrounding surface.",
59
+ "props": []
60
+ },
61
+ {
62
+ "properties": [
63
+ "border-radius"
64
+ ],
65
+ "reason": "The component owns its surface radius; retain the shared recipe and customize an outer wrapper when needed.",
66
+ "props": []
67
+ }
68
+ ],
69
+ "customProperties": [
70
+ {
71
+ "name": "--ds-code-font-variant-ligatures",
72
+ "source": "src/wc/components/CodeBlock/CodeBlock.css",
73
+ "reason": "Application-owned code ligature preference."
74
+ }
75
+ ]
76
+ }
48
77
  },
49
78
  "api": {
50
79
  "props": {
@@ -51,7 +51,30 @@
51
51
  "label": "Storybook examples",
52
52
  "path": "src/wc/components/ConversationList/ConversationList.stories.ts"
53
53
  }
54
- ]
54
+ ],
55
+ "styling": {
56
+ "rationale": "The component owns the protected recipes below; unspecified host properties and application layout remain customizable.",
57
+ "protected": [
58
+ {
59
+ "properties": [
60
+ "background",
61
+ "border",
62
+ "box-shadow",
63
+ "color"
64
+ ],
65
+ "reason": "The component owns its surface and interaction treatment; use its semantic presentation props or compose an application-owned surrounding surface.",
66
+ "props": []
67
+ },
68
+ {
69
+ "properties": [
70
+ "border-radius"
71
+ ],
72
+ "reason": "The component owns its surface radius; retain the shared recipe and customize an outer wrapper when needed.",
73
+ "props": []
74
+ }
75
+ ],
76
+ "customProperties": []
77
+ }
55
78
  },
56
79
  "api": {
57
80
  "props": {
@@ -282,7 +305,7 @@
282
305
  },
283
306
  {
284
307
  "path": "src/wc/components/ConversationListItem/ConversationListItem.tsx",
285
- "content": "/* eslint-disable local/no-selected-fill-emphasis-change -- Unread emphasis is message state, independent of the selected-row fill state. */\nimport { Component, Element, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport type { ConversationItemState } from '../conversation-types';\n\n@Component({\n tag: 'ds-conversation-list-item',\n styleUrl: 'ConversationListItem.css',\n scoped: true,\n})\nexport class ConversationListItem {\n @Element() el!: HTMLElement;\n\n @Prop() conversationId: string = '';\n /** Visible conversation title. Named to avoid the native HTMLElement.title contract. */\n @Prop() conversationTitle: string = '';\n @Prop() preview: string = '';\n /** ISO timestamp for semantic activity metadata. */\n @Prop() updatedAt: string = '';\n @Prop() selected: boolean = false;\n @Prop() state: ConversationItemState = 'default';\n @Prop() statusLabel: string = '';\n @Prop() unreadCount: number = 0;\n /** Keep the contextual-actions surface visible while its popup is open or closing. */\n @Prop({ reflect: true }) actionsOpen: boolean = false;\n\n @Event() dsSelect!: EventEmitter<{ id: string }>;\n\n private handleClick = () => this.dsSelect.emit({ id: this.conversationId });\n\n private handleActionsSlotChange = (event: Event) => {\n const slot = event.target as HTMLSlotElement;\n this.el.classList.toggle(\n 'conversation-list-item--has-actions',\n slot.assignedElements().length > 0\n );\n };\n\n render() {\n const busy = this.state === 'busy';\n const subtext = busy && this.statusLabel ? this.statusLabel : this.preview;\n const accessibleName = [\n this.conversationTitle,\n subtext,\n this.unreadCount > 0 ? `${this.unreadCount} unread` : '',\n ]\n .filter(Boolean)\n .join('. ');\n\n return (\n <Host role=\"listitem\" class={{ 'conversation-list-item--actions-open': this.actionsOpen }}>\n <div class=\"conversation-list-item__row\">\n <button\n type=\"button\"\n class={{\n 'conversation-list-item': true,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': this.selected,\n 'conversation-list-item--error': this.state === 'error',\n }}\n aria-label={accessibleName}\n aria-current={this.selected ? 'true' : undefined}\n aria-busy={busy ? 'true' : undefined}\n onClick={this.handleClick}\n >\n <span class=\"conversation-list-item__leading ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\">\n <slot name=\"leading\" />\n </span>\n <span class=\"conversation-list-item__content ds-choice-item__content ds-interaction-fill__content\">\n <span class=\"conversation-list-item__title-row\">\n <ds-text\n class=\"conversation-list-item__title ds-choice-item__label ds-control-label-box\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={this.unreadCount > 0}\n color={this.unreadCount > 0 ? 'primary' : 'secondary'}\n lineTruncation={1}\n >\n {this.conversationTitle}\n </ds-text>\n {this.unreadCount > 0 ? (\n <span class=\"conversation-list-item__unread\">\n <ds-badge variant=\"dot\" surface=\"secondary\" hasRing={false} />\n </span>\n ) : null}\n </span>\n <span class=\"conversation-list-item__preview-row\">\n {busy ? <ds-loader size=\"sm\" color=\"secondary\" /> : null}\n <ds-text\n class=\"conversation-list-item__preview ds-choice-item__subtext ds-control-label-box\"\n as=\"span\"\n variant=\"text-body-small\"\n color={this.state === 'error' ? 'negative' : 'secondary'}\n lineTruncation={1}\n >\n {subtext}\n </ds-text>\n </span>\n {this.updatedAt ? (\n <time\n class=\"conversation-list-item__time ds-visually-hidden\"\n dateTime={this.updatedAt}\n >\n {this.updatedAt}\n </time>\n ) : null}\n </span>\n <span class=\"conversation-list-item__trailing ds-interaction-fill__content\">\n <slot name=\"trailing\" />\n </span>\n </button>\n <div class=\"conversation-list-item__actions ds-control-elevation ds-control-elevation--md ds-control-elevation--press-scale\">\n <slot name=\"actions\" onSlotchange={this.handleActionsSlotChange} />\n </div>\n </div>\n </Host>\n );\n }\n}\n",
308
+ "content": "/* eslint-disable compomo/no-selected-fill-emphasis-change -- Unread emphasis is message state, independent of the selected-row fill state. */\nimport { Component, Element, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport type { ConversationItemState } from '../conversation-types';\n\n@Component({\n tag: 'ds-conversation-list-item',\n styleUrl: 'ConversationListItem.css',\n scoped: true,\n})\nexport class ConversationListItem {\n @Element() el!: HTMLElement;\n\n @Prop() conversationId: string = '';\n /** Visible conversation title. Named to avoid the native HTMLElement.title contract. */\n @Prop() conversationTitle: string = '';\n @Prop() preview: string = '';\n /** ISO timestamp for semantic activity metadata. */\n @Prop() updatedAt: string = '';\n @Prop() selected: boolean = false;\n @Prop() state: ConversationItemState = 'default';\n @Prop() statusLabel: string = '';\n @Prop() unreadCount: number = 0;\n /** Keep the contextual-actions surface visible while its popup is open or closing. */\n @Prop({ reflect: true }) actionsOpen: boolean = false;\n\n @Event() dsSelect!: EventEmitter<{ id: string }>;\n\n private handleClick = () => this.dsSelect.emit({ id: this.conversationId });\n\n private handleActionsSlotChange = (event: Event) => {\n const slot = event.target as HTMLSlotElement;\n this.el.classList.toggle(\n 'conversation-list-item--has-actions',\n slot.assignedElements().length > 0\n );\n };\n\n render() {\n const busy = this.state === 'busy';\n const subtext = busy && this.statusLabel ? this.statusLabel : this.preview;\n const accessibleName = [\n this.conversationTitle,\n subtext,\n this.unreadCount > 0 ? `${this.unreadCount} unread` : '',\n ]\n .filter(Boolean)\n .join('. ');\n\n return (\n <Host role=\"listitem\" class={{ 'conversation-list-item--actions-open': this.actionsOpen }}>\n <div class=\"conversation-list-item__row\">\n <button\n type=\"button\"\n class={{\n 'conversation-list-item': true,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': this.selected,\n 'conversation-list-item--error': this.state === 'error',\n }}\n aria-label={accessibleName}\n aria-current={this.selected ? 'true' : undefined}\n aria-busy={busy ? 'true' : undefined}\n onClick={this.handleClick}\n >\n <span class=\"conversation-list-item__leading ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\">\n <slot name=\"leading\" />\n </span>\n <span class=\"conversation-list-item__content ds-choice-item__content ds-interaction-fill__content\">\n <span class=\"conversation-list-item__title-row\">\n <ds-text\n class=\"conversation-list-item__title ds-choice-item__label ds-control-label-box\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={this.unreadCount > 0}\n color={this.unreadCount > 0 ? 'primary' : 'secondary'}\n lineTruncation={1}\n >\n {this.conversationTitle}\n </ds-text>\n {this.unreadCount > 0 ? (\n <span class=\"conversation-list-item__unread\">\n <ds-badge variant=\"dot\" surface=\"secondary\" hasRing={false} />\n </span>\n ) : null}\n </span>\n <span class=\"conversation-list-item__preview-row\">\n {busy ? <ds-loader size=\"sm\" color=\"secondary\" /> : null}\n <ds-text\n class=\"conversation-list-item__preview ds-choice-item__subtext ds-control-label-box\"\n as=\"span\"\n variant=\"text-body-small\"\n color={this.state === 'error' ? 'negative' : 'secondary'}\n lineTruncation={1}\n >\n {subtext}\n </ds-text>\n </span>\n {this.updatedAt ? (\n <time\n class=\"conversation-list-item__time ds-visually-hidden\"\n dateTime={this.updatedAt}\n >\n {this.updatedAt}\n </time>\n ) : null}\n </span>\n <span class=\"conversation-list-item__trailing ds-interaction-fill__content\">\n <slot name=\"trailing\" />\n </span>\n </button>\n <div class=\"conversation-list-item__actions ds-control-elevation ds-control-elevation--md ds-control-elevation--press-scale\">\n <slot name=\"actions\" onSlotchange={this.handleActionsSlotChange} />\n </div>\n </div>\n </Host>\n );\n }\n}\n",
286
309
  "type": "registry:ui"
287
310
  }
288
311
  ]
@@ -38,7 +38,20 @@
38
38
  "label": "Storybook examples",
39
39
  "path": "src/wc/components/ConversationList/ConversationList.stories.ts"
40
40
  }
41
- ]
41
+ ],
42
+ "styling": {
43
+ "rationale": "The component owns the protected recipes below; unspecified host properties and application layout remain customizable.",
44
+ "protected": [
45
+ {
46
+ "properties": [
47
+ "gap"
48
+ ],
49
+ "reason": "The component owns spacing between its immediate content regions; the application controls surrounding margins and placement.",
50
+ "props": []
51
+ }
52
+ ],
53
+ "customProperties": []
54
+ }
42
55
  },
43
56
  "api": {
44
57
  "props": {
@@ -46,7 +46,12 @@
46
46
  "label": "Storybook examples",
47
47
  "path": "src/wc/components/ConversationList/ConversationList.stories.ts"
48
48
  }
49
- ]
49
+ ],
50
+ "styling": {
51
+ "rationale": "The application supplies the bounded list region; internal scroll and action geometry are implementation-owned.",
52
+ "protected": [],
53
+ "customProperties": []
54
+ }
50
55
  },
51
56
  "api": {
52
57
  "props": {},
@@ -74,7 +74,32 @@
74
74
  "label": "Shared catalog state",
75
75
  "path": "src/wc/utils/list-customizer.ts"
76
76
  }
77
- ]
77
+ ],
78
+ "styling": {
79
+ "rationale": "The component owns the protected recipes below; unspecified host properties and application layout remain customizable.",
80
+ "protected": [
81
+ {
82
+ "properties": [
83
+ "background",
84
+ "border",
85
+ "box-shadow",
86
+ "color"
87
+ ],
88
+ "reason": "The component owns its surface and interaction treatment; use its semantic presentation props or compose an application-owned surrounding surface.",
89
+ "props": [
90
+ "hasBorder"
91
+ ]
92
+ },
93
+ {
94
+ "properties": [
95
+ "border-radius"
96
+ ],
97
+ "reason": "The component owns its surface radius; retain the shared recipe and customize an outer wrapper when needed.",
98
+ "props": []
99
+ }
100
+ ],
101
+ "customProperties": []
102
+ }
78
103
  },
79
104
  "api": {
80
105
  "props": {
@@ -82,7 +82,32 @@
82
82
  "label": "Toolbar composition owner",
83
83
  "path": "src/wc/components/DataToolbar/DataToolbar.agent.json"
84
84
  }
85
- ]
85
+ ],
86
+ "styling": {
87
+ "rationale": "The component owns the protected recipes below; unspecified host properties and application layout remain customizable.",
88
+ "protected": [
89
+ {
90
+ "properties": [
91
+ "background",
92
+ "border",
93
+ "box-shadow",
94
+ "color"
95
+ ],
96
+ "reason": "The component owns its surface and interaction treatment; use its semantic presentation props or compose an application-owned surrounding surface.",
97
+ "props": [
98
+ "hasBorder"
99
+ ]
100
+ },
101
+ {
102
+ "properties": [
103
+ "border-radius"
104
+ ],
105
+ "reason": "The component owns its surface radius; retain the shared recipe and customize an outer wrapper when needed.",
106
+ "props": []
107
+ }
108
+ ],
109
+ "customProperties": []
110
+ }
86
111
  },
87
112
  "api": {
88
113
  "props": {
@@ -82,7 +82,32 @@
82
82
  "label": "Surface ownership and trigger state",
83
83
  "path": "src/docs/SurfaceOwnership.mdx"
84
84
  }
85
- ]
85
+ ],
86
+ "styling": {
87
+ "rationale": "The component owns the protected recipes below; unspecified host properties and application layout remain customizable.",
88
+ "protected": [
89
+ {
90
+ "properties": [
91
+ "background",
92
+ "border",
93
+ "box-shadow",
94
+ "color"
95
+ ],
96
+ "reason": "The component owns its surface and interaction treatment; use its semantic presentation props or compose an application-owned surrounding surface.",
97
+ "props": [
98
+ "hasBorder"
99
+ ]
100
+ },
101
+ {
102
+ "properties": [
103
+ "border-radius"
104
+ ],
105
+ "reason": "The component owns its surface radius; retain the shared recipe and customize an outer wrapper when needed.",
106
+ "props": []
107
+ }
108
+ ],
109
+ "customProperties": []
110
+ }
86
111
  },
87
112
  "api": {
88
113
  "props": {
@@ -44,7 +44,32 @@
44
44
  "component:ds-menu",
45
45
  "component:ds-data-group",
46
46
  "component:ds-tab-group"
47
- ]
47
+ ],
48
+ "styling": {
49
+ "rationale": "The component owns the protected recipes below; unspecified host properties and application layout remain customizable.",
50
+ "protected": [
51
+ {
52
+ "properties": [
53
+ "background",
54
+ "border",
55
+ "box-shadow",
56
+ "color"
57
+ ],
58
+ "reason": "The component owns its surface and interaction treatment; use its semantic presentation props or compose an application-owned surrounding surface.",
59
+ "props": [
60
+ "hasBorder"
61
+ ]
62
+ },
63
+ {
64
+ "properties": [
65
+ "border-radius"
66
+ ],
67
+ "reason": "The component owns its surface radius; retain the shared recipe and customize an outer wrapper when needed.",
68
+ "props": []
69
+ }
70
+ ],
71
+ "customProperties": []
72
+ }
48
73
  },
49
74
  "api": {
50
75
  "props": {
@@ -77,7 +77,32 @@
77
77
  "label": "Surface ownership and trigger state",
78
78
  "path": "src/docs/SurfaceOwnership.mdx"
79
79
  }
80
- ]
80
+ ],
81
+ "styling": {
82
+ "rationale": "The component owns the protected recipes below; unspecified host properties and application layout remain customizable.",
83
+ "protected": [
84
+ {
85
+ "properties": [
86
+ "background",
87
+ "border",
88
+ "box-shadow",
89
+ "color"
90
+ ],
91
+ "reason": "The component owns its surface and interaction treatment; use its semantic presentation props or compose an application-owned surrounding surface.",
92
+ "props": [
93
+ "hasBorder"
94
+ ]
95
+ },
96
+ {
97
+ "properties": [
98
+ "border-radius"
99
+ ],
100
+ "reason": "The component owns its surface radius; retain the shared recipe and customize an outer wrapper when needed.",
101
+ "props": []
102
+ }
103
+ ],
104
+ "customProperties": []
105
+ }
81
106
  },
82
107
  "api": {
83
108
  "props": {