@adia-ai/web-components 0.0.26 → 0.0.28

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 (60) hide show
  1. package/components/agent-artifact/agent-artifact.a2ui.json +1 -1
  2. package/components/agent-artifact/agent-artifact.css +11 -0
  3. package/components/agent-artifact/agent-artifact.js +23 -2
  4. package/components/agent-artifact/agent-artifact.yaml +1 -1
  5. package/components/agent-questions/agent-questions.css +20 -1
  6. package/components/agent-reasoning/agent-reasoning.css +11 -0
  7. package/components/agent-reasoning/agent-reasoning.js +16 -0
  8. package/components/agent-trace/agent-trace.css +36 -12
  9. package/components/alert/alert.a2ui.json +10 -4
  10. package/components/alert/alert.css +13 -0
  11. package/components/alert/alert.js +1 -1
  12. package/components/alert/alert.yaml +21 -4
  13. package/components/badge/badge.a2ui.json +0 -2
  14. package/components/badge/badge.css +20 -0
  15. package/components/badge/badge.js +10 -2
  16. package/components/badge/badge.yaml +0 -2
  17. package/components/breadcrumb/breadcrumb.a2ui.json +16 -1
  18. package/components/breadcrumb/breadcrumb.css +27 -0
  19. package/components/breadcrumb/breadcrumb.js +95 -17
  20. package/components/breadcrumb/breadcrumb.yaml +15 -1
  21. package/components/calendar-picker/calendar-picker.css +17 -0
  22. package/components/chart/chart.css +20 -13
  23. package/components/chart/chart.js +49 -17
  24. package/components/chart-legend/chart-legend.css +30 -54
  25. package/components/chart-legend/chart-legend.js +48 -30
  26. package/components/code/code.css +41 -0
  27. package/components/code/code.js +44 -3
  28. package/components/command/command.js +52 -1
  29. package/components/empty-state/empty-state.js +32 -21
  30. package/components/feed/feed-item.yaml +50 -0
  31. package/components/feed/feed.a2ui.json +59 -0
  32. package/components/feed/feed.css +141 -0
  33. package/components/feed/feed.js +276 -0
  34. package/components/feed/feed.yaml +33 -0
  35. package/components/index.js +2 -0
  36. package/components/list/list.js +20 -16
  37. package/components/menu/menu.css +18 -0
  38. package/components/menu/menu.js +24 -10
  39. package/components/pane/pane.css +5 -0
  40. package/components/pipeline-status/pipeline-status.css +15 -1
  41. package/components/popover/popover.css +17 -0
  42. package/components/select/select.css +18 -0
  43. package/components/swatch/swatch.a2ui.json +116 -0
  44. package/components/swatch/swatch.css +141 -0
  45. package/components/swatch/swatch.js +121 -0
  46. package/components/swatch/swatch.yaml +101 -0
  47. package/components/swiper/swiper.css +9 -0
  48. package/components/table/table.css +5 -0
  49. package/components/table/table.js +45 -1
  50. package/components/table-toolbar/table-toolbar.css +13 -0
  51. package/components/tag/tag.css +10 -0
  52. package/components/timeline/timeline.css +15 -4
  53. package/components/toast/toast.css +93 -48
  54. package/components/toast/toast.js +101 -22
  55. package/components/toolbar/toolbar.css +13 -0
  56. package/components/tooltip/tooltip.css +11 -3
  57. package/core/provider.js +1 -0
  58. package/package.json +1 -1
  59. package/styles/colors/semantics.css +1 -1
  60. package/styles/components.css +1 -0
@@ -6,12 +6,26 @@ tag: breadcrumb-ui
6
6
  component: Breadcrumb
7
7
  category: navigation
8
8
  version: 1
9
- description: Breadcrumb trail with auto-inserted separators.
9
+ description: Breadcrumb trail with auto-inserted separators. Supports a leading icon (first child) and an overflow popover that collapses middle crumbs into a `…` menu.
10
10
  props:
11
11
  separator:
12
12
  description: Character or string rendered between breadcrumb items via CSS ::before.
13
13
  type: string
14
14
  default: /
15
+ collapse:
16
+ description: Collapse middle crumbs into a `…` overflow popover when there are 4+ items.
17
+ type: boolean
18
+ default: false
19
+ collapseKeepLeading:
20
+ description: Number of leading items to keep visible when [collapse] is active. The first item (often a home/icon link) sits before the overflow popover.
21
+ type: number
22
+ default: 1
23
+ attribute: collapse-keep-leading
24
+ collapseKeepTrailing:
25
+ description: Number of trailing items to keep visible when [collapse] is active. The last item is always the current page.
26
+ type: number
27
+ default: 2
28
+ attribute: collapse-keep-trailing
15
29
  events: {}
16
30
  slots: {}
17
31
  states:
@@ -180,6 +180,23 @@ calendar-picker-ui [slot="popover"] {
180
180
  font-family: inherit;
181
181
  font-size: var(--calendar-picker-font-size);
182
182
  color: var(--calendar-picker-popover-fg);
183
+ /* Fade + lift in on first paint via @starting-style. Same pattern
184
+ as menu/select/popover surfaces — see journal 2026-04-29 §18. */
185
+ opacity: 1;
186
+ translate: 0 0;
187
+ transition: opacity var(--a-duration-fast) var(--a-easing-out),
188
+ translate var(--a-duration-fast) var(--a-easing-out);
189
+ }
190
+
191
+ calendar-picker-ui [slot="popover"]:popover-open {
192
+ @starting-style {
193
+ opacity: 0;
194
+ translate: 0 -4px;
195
+ }
196
+ }
197
+
198
+ @media (prefers-reduced-motion: reduce) {
199
+ calendar-picker-ui [slot="popover"] { transition: none; }
183
200
  }
184
201
 
185
202
  /* Header row */
@@ -463,24 +463,31 @@
463
463
  /* ── Radial bar ──
464
464
  Concentric rings — each ring stroked with bandwidth. Track is the
465
465
  faint backing, filled arc is accent. Stroke-linecap:round in the path
466
- gives rounded end-caps on the arcs. */
467
- [data-radial-track] {
466
+ gives rounded end-caps on the arcs.
467
+
468
+ Element-qualified `circle[data-radial-*]` selectors (specificity 0,1,1)
469
+ are required to override the generic `circle[data-slice="N"] { fill }`
470
+ rule above, which would otherwise fill the ring circles solid and
471
+ produce a solid-disc render. The inline `fill="none"` attribute on the
472
+ SVG element loses to any CSS rule that names `fill`. */
473
+ circle[data-radial-track] {
468
474
  stroke: var(--a-border-subtle);
469
475
  fill: none;
470
476
  }
471
- [data-radial-bar] {
477
+ circle[data-radial-bar] {
472
478
  stroke: var(--chart-bar);
479
+ fill: none;
473
480
  }
474
- [data-radial-bar][data-slice="0"] { stroke: var(--chart-0); }
475
- [data-radial-bar][data-slice="1"] { stroke: var(--chart-1); }
476
- [data-radial-bar][data-slice="2"] { stroke: var(--chart-2); }
477
- [data-radial-bar][data-slice="3"] { stroke: var(--chart-3); }
478
- [data-radial-bar][data-slice="4"] { stroke: var(--chart-4); }
479
- [data-radial-bar][data-slice="5"] { stroke: var(--chart-5); }
480
- [data-radial-bar][data-slice="6"] { stroke: var(--chart-6); }
481
- [data-radial-bar][data-slice="7"] { stroke: var(--chart-7); }
482
- [data-radial-bar][data-slice="8"] { stroke: var(--chart-8); }
483
- [data-radial-bar][data-slice="9"] { stroke: var(--chart-9); }
481
+ circle[data-radial-bar][data-slice="0"] { stroke: var(--chart-0); }
482
+ circle[data-radial-bar][data-slice="1"] { stroke: var(--chart-1); }
483
+ circle[data-radial-bar][data-slice="2"] { stroke: var(--chart-2); }
484
+ circle[data-radial-bar][data-slice="3"] { stroke: var(--chart-3); }
485
+ circle[data-radial-bar][data-slice="4"] { stroke: var(--chart-4); }
486
+ circle[data-radial-bar][data-slice="5"] { stroke: var(--chart-5); }
487
+ circle[data-radial-bar][data-slice="6"] { stroke: var(--chart-6); }
488
+ circle[data-radial-bar][data-slice="7"] { stroke: var(--chart-7); }
489
+ circle[data-radial-bar][data-slice="8"] { stroke: var(--chart-8); }
490
+ circle[data-radial-bar][data-slice="9"] { stroke: var(--chart-9); }
484
491
 
485
492
  /* ── Gauge ──
486
493
  Half-donut — track is faint backing, fill reads as primary accent. */
@@ -173,6 +173,20 @@ function smoothAreaPath(points, baselineY, t = 0.4) {
173
173
  return `${line} L${last.x},${baselineY} L${first.x},${baselineY} Z`;
174
174
  }
175
175
 
176
+ /** Build a column-bar path with only the TOP corners rounded so the bar
177
+ * sits flush on its baseline axis. Used for bar / grouped-bar / composed
178
+ * bar series / stacked-bar single + top segments — the value end gets a
179
+ * cap, the axis end stays square. r is clamped to (w/2, h) to handle
180
+ * short bars without the arcs overlapping or escaping the rect. */
181
+ function topRoundedBarPath(x, y, w, h, r = 0) {
182
+ if (h <= 0 || w <= 0) return '';
183
+ const rr = Math.max(0, Math.min(r, w / 2, h));
184
+ if (rr === 0) {
185
+ return `M${x},${y} H${x + w} V${y + h} H${x} Z`;
186
+ }
187
+ return `M${x},${y + h} V${y + rr} Q${x},${y} ${x + rr},${y} H${x + w - rr} Q${x + w},${y} ${x + w},${y + rr} V${y + h} Z`;
188
+ }
189
+
176
190
  /* ── Aspect ratios ─────────────────────────────────────────────── */
177
191
 
178
192
  const ASPECTS = {
@@ -263,6 +277,22 @@ class AdiaChart extends AdiaElement {
263
277
  if (!this.hasAttribute('role')) this.setAttribute('role', 'img');
264
278
  if (!this.hasAttribute('aria-label')) this.setAttribute('aria-label', this.heading || `${this.type} chart`);
265
279
 
280
+ /* Hydrate from inline `data="[…]"` HTML attribute. The canonical
281
+ entry point is the `.data` property (set programmatically), but
282
+ consumers commonly try the same declarative attribute shape that
283
+ every other chart prop accepts — `<chart-ui data='[…]' x="m"
284
+ y="v">`. AdiaElement's property system doesn't deserialize JSON
285
+ array attributes, so a static-HTML chart authored that way would
286
+ otherwise render empty. JSON-parse once at connect; malformed
287
+ payloads are ignored silently and a property assignment later
288
+ still wins. */
289
+ if (this.#data.length === 0 && this.hasAttribute('data')) {
290
+ try {
291
+ const parsed = JSON.parse(this.getAttribute('data'));
292
+ if (Array.isArray(parsed)) this.data = parsed;
293
+ } catch (_) { /* malformed JSON — leave empty, render() bails on no data */ }
294
+ }
295
+
266
296
  /* Listen for canonical `toggle` events bubbled from external
267
297
  chart-legend-ui[for=self] descendants. The handler filters by
268
298
  target so other components dispatching `toggle` (accordion-ui,
@@ -1004,7 +1034,7 @@ class AdiaChart extends AdiaElement {
1004
1034
  const bx = pad.left + barW * i + barGap;
1005
1035
  const by = pad.top + plotH - barH;
1006
1036
 
1007
- svg += `<rect data-bar${tip({ label: labels[i], value: v })} x="${bx}" y="${by}" width="${barInner}" height="${barH}" rx="${this.#resolveRadius()}"/>`;
1037
+ svg += `<path data-bar${tip({ label: labels[i], value: v })} d="${topRoundedBarPath(bx, by, barInner, barH, this.#resolveRadius())}"/>`;
1008
1038
 
1009
1039
  if (showValues) {
1010
1040
  svg += `<text data-value x="${bx + barInner / 2}" y="${by - 4}" text-anchor="middle" font-size="${dims.valueSize}">${this.#fmtValue(v)}</text>`;
@@ -1450,6 +1480,12 @@ class AdiaChart extends AdiaElement {
1450
1480
  const cy = height * 0.68;
1451
1481
  const outerR = Math.max(40, Math.min(width * 0.45, height * 0.6));
1452
1482
  const innerR = outerR * 0.72;
1483
+ /* End-cap radius — fully-rounded pill ends matching the
1484
+ progress-ui convention (--progress-radius: var(--a-radius-full)).
1485
+ donutArcPath clamps the corner radius to (outerR - innerR) / 2,
1486
+ so passing the ring half-thickness gives full pill caps on both
1487
+ ends of the arc. */
1488
+ const capR = (outerR - innerR) / 2;
1453
1489
 
1454
1490
  let svg = '';
1455
1491
 
@@ -1457,14 +1493,14 @@ class AdiaChart extends AdiaElement {
1457
1493
  clockwise through 12 o'clock (3π/2) to 3 o'clock (2π). donutArcPath
1458
1494
  produces a filled ring wedge; with start=π, end=2π the sliceAngle
1459
1495
  equals π so large-arc-flag=0 and the arc passes over the top. */
1460
- svg += `<path data-radial-track d="${donutArcPath(cx, cy, outerR, innerR, Math.PI, 2 * Math.PI, 0)}"/>`;
1496
+ svg += `<path data-radial-track d="${donutArcPath(cx, cy, outerR, innerR, Math.PI, 2 * Math.PI, capR)}"/>`;
1461
1497
 
1462
1498
  /* Filled portion — 0..180° proportional to pct. pct=0 draws nothing;
1463
1499
  pct=1 overlays the full backing arc. */
1464
1500
  if (pct > 0) {
1465
1501
  const fillEnd = Math.PI + Math.PI * pct;
1466
1502
  const tipAttrs = tip({ label: data[0]?.[this.x] ?? 'Value', value: primary, pct: (pct * 100).toFixed(1) });
1467
- svg += `<path data-slice="0"${tipAttrs} data-gauge-fill d="${donutArcPath(cx, cy, outerR, innerR, Math.PI, fillEnd, 0)}"/>`;
1503
+ svg += `<path data-slice="0"${tipAttrs} data-gauge-fill d="${donutArcPath(cx, cy, outerR, innerR, Math.PI, fillEnd, capR)}"/>`;
1468
1504
  }
1469
1505
 
1470
1506
  /* Center value label */
@@ -1807,7 +1843,7 @@ class AdiaChart extends AdiaElement {
1807
1843
  const barH = maxVal ? (v / maxVal) * plotH : 0;
1808
1844
  const bx = pad.left + barW * i + barGap;
1809
1845
  const by = pad.top + plotH - barH;
1810
- svg += `<rect${this.#seriesFill(0, barKey)}${tip({ label: labels[i], value: v, series: barKey })} x="${bx}" y="${by}" width="${barInner}" height="${barH}" rx="${this.#resolveRadius()}"/>`;
1846
+ svg += `<path${this.#seriesFill(0, barKey)}${tip({ label: labels[i], value: v, series: barKey })} d="${topRoundedBarPath(bx, by, barInner, barH, this.#resolveRadius())}"/>`;
1811
1847
  }
1812
1848
  }
1813
1849
 
@@ -1943,22 +1979,18 @@ class AdiaChart extends AdiaElement {
1943
1979
  const by = stackY;
1944
1980
  const bh = segH;
1945
1981
  const isTop = k === segCount - 1;
1946
- const isBottom = k === 0;
1947
- const r = Math.min(this.#resolveRadius(), barInner / 2, bh / 2);
1982
+ const r = this.#resolveRadius();
1948
1983
 
1949
1984
  const attrs = `${this.#seriesFill(k % 10, keys[k])}${tip({ label: labels[i], value: v, series: keys[k] })}`;
1950
1985
 
1951
- if (isTop && isBottom) {
1952
- // Single segment — round top + bottom
1953
- svg += `<path${attrs} d="M${bx + r},${by} H${bx + barInner - r} Q${bx + barInner},${by} ${bx + barInner},${by + r} V${by + bh - r} Q${bx + barInner},${by + bh} ${bx + barInner - r},${by + bh} H${bx + r} Q${bx},${by + bh} ${bx},${by + bh - r} V${by + r} Q${bx},${by} ${bx + r},${by} Z"/>`;
1954
- } else if (isTop) {
1955
- // Top segment round top corners only
1956
- svg += `<path${attrs} d="M${bx},${by + bh} V${by + r} Q${bx},${by} ${bx + r},${by} H${bx + barInner - r} Q${bx + barInner},${by} ${bx + barInner},${by + r} V${by + bh} Z"/>`;
1957
- } else if (isBottom) {
1958
- // Bottom segment — round bottom corners only
1959
- svg += `<path${attrs} d="M${bx},${by} H${bx + barInner} V${by + bh - r} Q${bx + barInner},${by + bh} ${bx + barInner - r},${by + bh} H${bx + r} Q${bx},${by + bh} ${bx},${by + bh - r} Z"/>`;
1986
+ if (isTop) {
1987
+ // Top segment (or single segment) — round top corners only.
1988
+ // Bottom edge sits on the next segment OR the axis baseline; either
1989
+ // way it should be flush, so we never round the bottom corners.
1990
+ svg += `<path${attrs} d="${topRoundedBarPath(bx, by, barInner, bh, r)}"/>`;
1960
1991
  } else {
1961
- // Middle segmentno radius
1992
+ // Middle and bottom segments flush on both ends. Bottom sits on
1993
+ // the axis baseline; middle sits between segments.
1962
1994
  svg += `<rect${attrs} x="${bx}" y="${by}" width="${barInner}" height="${bh}"/>`;
1963
1995
  }
1964
1996
  }
@@ -1999,7 +2031,7 @@ class AdiaChart extends AdiaElement {
1999
2031
  const barH = maxVal ? (v / maxVal) * plotH : 0;
2000
2032
  const bx = pad.left + groupW * i + groupPad + (subBarW + barGap) * k;
2001
2033
  const by = pad.top + plotH - barH;
2002
- svg += `<rect${this.#seriesFill(k % 10, keys[k])}${tip({ label: labels[i], value: v, series: keys[k] })} x="${bx}" y="${by}" width="${subBarW}" height="${barH}" rx="${this.#resolveRadius()}"/>`;
2034
+ svg += `<path${this.#seriesFill(k % 10, keys[k])}${tip({ label: labels[i], value: v, series: keys[k] })} d="${topRoundedBarPath(bx, by, subBarW, barH, this.#resolveRadius())}"/>`;
2003
2035
 
2004
2036
  if (!this.hideValues) {
2005
2037
  svg += `<text data-value x="${bx + subBarW / 2}" y="${by - 4}" text-anchor="middle" font-size="${dims.valueSize}">${this.#fmtValue(v)}</text>`;
@@ -48,77 +48,53 @@
48
48
  align-items: flex-start;
49
49
  }
50
50
 
51
- /* Rows both <button> (interactive) and <span> (static) variants */
52
- [data-row] {
53
- all: unset;
54
- box-sizing: border-box;
55
- display: inline-flex;
56
- align-items: center;
57
- gap: var(--chart-legend-row-gap);
58
- padding: var(--chart-legend-py) var(--a-space-1);
59
- border-radius: var(--chart-legend-row-radius);
51
+ /* Rows are <badge-ui> chips. We override badge-ui's default tokens
52
+ to give them a quieter, click-to-toggle appearance — transparent
53
+ bg by default (so they read as inline text not chips), highlighted
54
+ on hover when interactive. The pill chrome (radius, padding,
55
+ font-size, line-height) comes from badge-ui itself. */
56
+ badge-ui[data-row] {
57
+ --badge-bg: transparent;
58
+ --badge-fg: var(--chart-legend-fg);
59
+ --badge-radius: var(--chart-legend-row-radius);
60
+ --badge-px: var(--a-space-1);
61
+ --badge-py: var(--chart-legend-py);
62
+ --badge-gap: var(--chart-legend-row-gap);
63
+ --badge-font-size: var(--chart-legend-font-size);
60
64
  cursor: pointer;
61
- color: inherit;
62
- line-height: 1.2;
63
65
  transition: background var(--a-duration-fast) var(--a-easing),
64
66
  color var(--a-duration-fast) var(--a-easing);
65
67
  }
66
68
 
67
69
  /* Static rows — no interaction */
68
- :scope[static] [data-row] {
70
+ :scope[static] badge-ui[data-row] {
69
71
  cursor: default;
70
72
  }
71
73
 
72
- button[data-row]:hover {
73
- background: var(--chart-legend-row-hover);
74
- color: var(--chart-legend-row-fg-hover);
74
+ badge-ui[data-row][role="button"]:hover {
75
+ --badge-bg: var(--chart-legend-row-hover);
76
+ --badge-fg: var(--chart-legend-row-fg-hover);
75
77
  }
76
78
 
77
- button[data-row]:focus-visible {
79
+ badge-ui[data-row][role="button"]:focus-visible {
78
80
  outline: none;
79
81
  box-shadow: var(--chart-legend-focus-ring);
80
82
  }
81
83
 
82
- /* Inactive (toggled-off) rows — dim the swatch + fade label */
83
- [data-row]:not([data-active]) {
84
- color: var(--chart-legend-fg-inactive);
84
+ /* Inactive (toggled-off) rows — dim the swatch + fade label. Drives
85
+ badge-ui's --badge-fg so the label fades alongside the swatch. */
86
+ badge-ui[data-row]:not([data-active]) {
87
+ --badge-fg: var(--chart-legend-fg-inactive);
85
88
  }
86
- [data-row]:not([data-active]) [data-swatch] {
89
+ badge-ui[data-row]:not([data-active]) swatch-ui {
87
90
  opacity: 0.4;
88
91
  }
89
92
 
90
- /* Swatch dedicated span per row, styled by the legend's [shape] attr.
91
- --swatch-color is set inline per-row from --color-{key} with a
92
- --chart-{slot} fallback; overrides cascade through atomically. */
93
- [data-swatch] {
94
- display: inline-block;
95
- flex-shrink: 0;
96
- background: var(--swatch-color, var(--chart-0));
97
- line-height: 0;
98
- }
99
-
100
- :scope[shape="dot"] [data-swatch] {
101
- width: var(--chart-legend-swatch-size);
102
- height: var(--chart-legend-swatch-size);
103
- border-radius: 50%;
104
- }
105
-
106
- :scope[shape="square"] [data-swatch] {
107
- width: var(--chart-legend-swatch-size);
108
- height: var(--chart-legend-swatch-size);
109
- border-radius: var(--a-radius-sm);
110
- }
111
-
112
- :scope[shape="line"] [data-swatch] {
113
- width: calc(var(--chart-legend-swatch-size) * 1.75);
114
- height: var(--chart-legend-line-w);
115
- border-radius: var(--chart-legend-line-w);
116
- }
117
-
118
- :scope[shape="dashed"] [data-swatch] {
119
- width: calc(var(--chart-legend-swatch-size) * 1.75);
120
- height: 0;
121
- background: transparent;
122
- border-top: var(--chart-legend-line-w) dashed var(--swatch-color, var(--chart-0));
123
- }
93
+ /* Swatch is composed via <swatch-ui shape="…">; per-shape styling
94
+ lives in swatch.css. The legend just hands its [shape] attr through
95
+ and writes --swatch-color inline. Pre-2026-05-01 this file owned a
96
+ `<span data-swatch>` element + dot/square/line/dashed CSS rules,
97
+ which collided with the docs-shell's `[data-swatch]` token-demo
98
+ rule (cascade leak). Composing the primitive moves the styling to
99
+ one place. */
124
100
  }
@@ -14,14 +14,17 @@
14
14
  * override when both are provided.)
15
15
  * items — JSON array of {key, label, slot?, pct?} legend items.
16
16
  * Takes precedence over [for] if both are provided.
17
- * shape — dot | square | line | dashed. Default: dot. Maps to
18
- * badge-ui's icon slot (dot = phosphor 'dot', square =
19
- * 'square-fill', line/dashed = custom CSS swatches).
17
+ * shape — dot | square | line | dashed. Default: dot. The swatch is
18
+ * a `[data-swatch]` span inside each badge-ui row, styled by
19
+ * the legend's [shape] attr. Custom span (rather than badge-
20
+ * ui's [icon] slot) is required because line/dashed shapes
21
+ * aren't representable as icon glyphs.
20
22
  * position — top | bottom | left | right. Layout hint; actual placement
21
23
  * is where the element is placed in the DOM. Drives
22
24
  * flex-direction.
23
- * static — when set, rows render as non-interactive <span>s. Default
24
- * is interactive <button>s that toggle.
25
+ * static — when set, rows render as non-interactive badges (no
26
+ * role/tabindex/handlers). Default is interactive
27
+ * `role="button"` badges that toggle on click + Enter/Space.
25
28
  * on-toggle — hide | opacity. Default hide. Escape hatch per OD-CHART-09.
26
29
  * Reported via the `toggle` event detail; chart-ui reads it
27
30
  * when wired via [for].
@@ -127,45 +130,59 @@ class AdiaChartLegend extends AdiaElement {
127
130
  const slot = item.slot != null ? item.slot : 0;
128
131
  const active = !this.#hiddenKeys.has(key);
129
132
 
130
- const row = this.static
131
- ? document.createElement('span')
132
- : document.createElement('button');
133
-
133
+ /* Each row is a <badge-ui> chip — the canonical AdiaUI primitive for
134
+ legend pills (per badge.yaml's documented "chart-legend" example).
135
+ badge-ui carries the pill chrome (radius, padding, font-size, hover
136
+ affordance), the legend layers shape-specific swatch CSS + the
137
+ click-to-toggle behavior on top. Interactive rows get
138
+ role="button" + tabindex="0" + Enter/Space handlers so they're
139
+ keyboard-operable without a wrapping <button>. */
140
+ const row = document.createElement('badge-ui');
134
141
  row.setAttribute('data-row', '');
135
- row.setAttribute('role', 'listitem');
142
+ row.setAttribute('role', this.static ? 'listitem' : 'button');
143
+ row.setAttribute('text', label);
136
144
  if (key) row.setAttribute('data-key', key);
137
145
  if (active) row.setAttribute('data-active', '');
138
- if (!this.static) row.setAttribute('type', 'button');
139
-
140
- /* Swatch + label. A dedicated `[data-swatch]` span lets shape-specific
141
- CSS handle dot/square/line/dashed variants without fighting badge-
142
- ui's icon slot (which is shadow-DOM-less and doesn't expose an
143
- `icon` partprevious badge-ui compose produced visual artifacts
144
- for line/dashed shapes). Color resolves via --color-{key} with a
145
- chart-{slot} fallback, mirroring chart-ui's per-series CSS var
146
- injection so ancestor overrides cascade through atomically. */
147
- const swatch = document.createElement('span');
148
- swatch.setAttribute('data-swatch', '');
146
+ if (!this.static) {
147
+ row.setAttribute('tabindex', '0');
148
+ row.setAttribute('aria-pressed', active ? 'true' : 'false');
149
+ }
150
+
151
+ /* Swatchcomposed via `<swatch-ui>` with the legend's [shape] attr
152
+ passed through. Pre-2026-05-01 this was a hand-rolled `<span
153
+ data-swatch>` plus per-shape CSS in this file; that drift caused
154
+ a cascade-leak collision with the docs-shell's `[data-swatch]`
155
+ demo rule (token-colors page). Compose the primitive instead and
156
+ the swatch's dot / square / line / dashed variants live in one
157
+ place. Color resolves via --color-{key} with a --a-data-{slot}
158
+ fallback; the legend can render standalone (without a chart-ui
159
+ ancestor) so the fallback must reference the global data palette
160
+ directly, not chart-ui's scoped --chart-{N} alias. */
161
+ const swatchShape = this.shape || 'dot';
162
+ const swatch = document.createElement('swatch-ui');
163
+ swatch.setAttribute('shape', swatchShape);
164
+ swatch.setAttribute('size', 'sm');
149
165
  const swatchColor = key
150
- ? `var(--color-${key}, var(--chart-${slot}))`
151
- : `var(--chart-${slot})`;
166
+ ? `var(--color-${key}, var(--a-data-${slot}))`
167
+ : `var(--a-data-${slot})`;
152
168
  swatch.style.setProperty('--swatch-color', swatchColor);
153
169
  row.appendChild(swatch);
154
170
 
155
- const labelEl = document.createElement('span');
156
- labelEl.setAttribute('data-label', '');
157
- labelEl.textContent = label;
158
- row.appendChild(labelEl);
159
-
160
171
  if (!this.static) {
161
- row.addEventListener('click', (e) => this.#onRowClick(e, key));
172
+ row.addEventListener('click', (e) => this.#onRowToggle(e, key));
173
+ row.addEventListener('keydown', (e) => {
174
+ if (e.key === 'Enter' || e.key === ' ') {
175
+ e.preventDefault();
176
+ this.#onRowToggle(e, key);
177
+ }
178
+ });
162
179
  }
163
180
 
164
181
  this.appendChild(row);
165
182
  }
166
183
  }
167
184
 
168
- #onRowClick(e, key) {
185
+ #onRowToggle(e, key) {
169
186
  if (!key) return;
170
187
  const currentlyActive = !this.#hiddenKeys.has(key);
171
188
  const newActive = !currentlyActive;
@@ -175,6 +192,7 @@ class AdiaChartLegend extends AdiaElement {
175
192
  const row = e.currentTarget;
176
193
  if (newActive) row.setAttribute('data-active', '');
177
194
  else row.removeAttribute('data-active');
195
+ row.setAttribute('aria-pressed', newActive ? 'true' : 'false');
178
196
 
179
197
  this.dispatchEvent(new CustomEvent('toggle', {
180
198
  bubbles: true,
@@ -156,6 +156,47 @@
156
156
  color: inherit;
157
157
  }
158
158
 
159
+ /* ── Diff line states (static path) ──
160
+ Active when the block uses `language="diff"` (auto-parse +/- prefix)
161
+ or carries `data-line-states="..."` (explicit per-line). Each line is
162
+ a `[data-line-state]` row whose bg picks up the state token. The
163
+ CodeMirror path doesn't use these markers; line decorations there go
164
+ through CM extensions instead. */
165
+ > pre > code[data-line-state-mode] {
166
+ display: block;
167
+ }
168
+ > pre > code[data-line-state-mode] > [data-line-state] {
169
+ display: grid;
170
+ grid-template-columns: 1fr;
171
+ /* Bleed the row tint to the pre's padding edges so it reads as a
172
+ full-width line, not a gap-margined pill. */
173
+ margin-inline: calc(-1 * var(--code-px));
174
+ padding-inline: var(--code-px);
175
+ }
176
+ > pre > code[data-line-numbers] > [data-line-state] {
177
+ grid-template-columns: auto 1fr;
178
+ column-gap: var(--a-space-3);
179
+ }
180
+ > pre > code[data-line-state-mode] [data-line-num] {
181
+ color: var(--a-fg-subtle);
182
+ text-align: end;
183
+ user-select: none;
184
+ min-width: 1.5ch;
185
+ }
186
+ > pre > code[data-line-state-mode] [data-line-body] {
187
+ white-space: pre;
188
+ }
189
+ /* Empty lines still need height so the diff column counts line up. */
190
+ > pre > code[data-line-state-mode] [data-line-body]:empty::before {
191
+ content: " ";
192
+ }
193
+ > pre > code [data-line-state="added"] {
194
+ background: var(--a-success-muted);
195
+ }
196
+ > pre > code [data-line-state="removed"] {
197
+ background: var(--a-danger-muted);
198
+ }
199
+
159
200
  /* Footer — optional chrome band below the code block
160
201
  (line counts, byte size, language family, etc.) */
161
202
  > footer {
@@ -90,9 +90,12 @@ class AdiaCode extends AdiaElement {
90
90
  // Mount CodeMirror when the language is supported OR the element is
91
91
  // editable (editable plain-text is still useful). Inline instances
92
92
  // stay on the static <code> path. Mount failures leave the static
93
- // fallback in place — it's already visible.
93
+ // fallback in place — it's already visible. Diff line-state mode
94
+ // (auto via `language="diff"` or explicit `data-line-states`) also
95
+ // stays on the static path — bg tinting lives on the wrapper spans.
94
96
  const lang = canonicalLanguage(this.language);
95
- const shouldMount = !this.inline && (SUPPORTED_LANGUAGES.has(lang) || this.editable);
97
+ const useLineState = this.hasAttribute('data-line-states') || lang === 'diff';
98
+ const shouldMount = !this.inline && !useLineState && (SUPPORTED_LANGUAGES.has(lang) || this.editable);
96
99
  if (shouldMount) {
97
100
  this.#mountEditor();
98
101
  }
@@ -145,7 +148,45 @@ class AdiaCode extends AdiaElement {
145
148
  // Pre > Code — direct semantic elements, no slot attr
146
149
  const pre = document.createElement('pre');
147
150
  const code = document.createElement('code');
148
- code.textContent = raw;
151
+
152
+ // Diff line-state mode — wrap each line so it can carry a state-driven
153
+ // bg tint and (when [line-numbers]) a leading line-number gutter.
154
+ // Triggered by `language="diff"` (auto-parses +/- prefix) or by an
155
+ // explicit `data-line-states` CSV that maps positionally onto lines.
156
+ const lang = canonicalLanguage(this.language);
157
+ const explicit = this.getAttribute('data-line-states');
158
+ const useLineState = explicit != null || lang === 'diff';
159
+ if (useLineState) {
160
+ code.setAttribute('data-line-state-mode', '');
161
+ if (this.lineNumbers) code.setAttribute('data-line-numbers', '');
162
+ const states = explicit?.split(',').map((s) => s.trim()) ?? null;
163
+ const lines = raw.split('\n');
164
+ lines.forEach((line, i) => {
165
+ let state = 'unchanged';
166
+ if (states && states[i]) {
167
+ state = states[i];
168
+ } else if (lang === 'diff') {
169
+ const head = line.charAt(0);
170
+ if (head === '+') state = 'added';
171
+ else if (head === '-') state = 'removed';
172
+ }
173
+ const row = document.createElement('span');
174
+ row.setAttribute('data-line-state', state);
175
+ if (this.lineNumbers) {
176
+ const num = document.createElement('span');
177
+ num.setAttribute('data-line-num', '');
178
+ num.textContent = String(i + 1);
179
+ row.appendChild(num);
180
+ }
181
+ const body = document.createElement('span');
182
+ body.setAttribute('data-line-body', '');
183
+ body.textContent = line;
184
+ row.appendChild(body);
185
+ code.appendChild(row);
186
+ });
187
+ } else {
188
+ code.textContent = raw;
189
+ }
149
190
  pre.appendChild(code);
150
191
 
151
192
  this.appendChild(pre);