@adia-ai/web-components 0.0.27 → 0.0.29

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 (93) hide show
  1. package/README.md +4 -8
  2. package/a2ui/index.js +1 -1
  3. package/components/agent-questions/agent-questions.css +20 -1
  4. package/components/agent-trace/agent-trace.css +17 -12
  5. package/components/badge/badge.js +9 -1
  6. package/components/breadcrumb/breadcrumb.a2ui.json +16 -1
  7. package/components/breadcrumb/breadcrumb.css +27 -0
  8. package/components/breadcrumb/breadcrumb.js +95 -17
  9. package/components/breadcrumb/breadcrumb.yaml +15 -1
  10. package/components/canvas/canvas.js +1 -1
  11. package/components/chart/chart.css +20 -13
  12. package/components/chart/chart.js +49 -17
  13. package/components/chart-legend/chart-legend.css +30 -54
  14. package/components/chart-legend/chart-legend.js +48 -30
  15. package/components/command/command.js +52 -1
  16. package/components/feed/feed-item.yaml +50 -0
  17. package/components/feed/feed.a2ui.json +59 -0
  18. package/components/feed/feed.css +150 -0
  19. package/components/feed/feed.js +385 -0
  20. package/components/feed/feed.yaml +33 -0
  21. package/components/index.js +2 -0
  22. package/components/swatch/swatch.a2ui.json +116 -0
  23. package/components/swatch/swatch.css +141 -0
  24. package/components/swatch/swatch.js +121 -0
  25. package/components/swatch/swatch.yaml +101 -0
  26. package/components/timeline/timeline.css +5 -1
  27. package/components/toast/toast.js +48 -178
  28. package/components/tooltip/tooltip.css +3 -3
  29. package/core/provider.js +1 -0
  30. package/index.css +3 -2
  31. package/index.js +15 -7
  32. package/package.json +1 -5
  33. package/styles/components.css +1 -0
  34. package/patterns/a2ui-root/a2ui-root.a2ui.json +0 -125
  35. package/patterns/a2ui-root/a2ui-root.js +0 -191
  36. package/patterns/a2ui-root/a2ui-root.yaml +0 -87
  37. package/patterns/adia-chat/adia-chat.a2ui.json +0 -149
  38. package/patterns/adia-chat/adia-chat.css +0 -10
  39. package/patterns/adia-chat/adia-chat.js +0 -297
  40. package/patterns/adia-chat/adia-chat.yaml +0 -118
  41. package/patterns/adia-chat/css/adia-chat.empty.css +0 -12
  42. package/patterns/adia-chat/css/adia-chat.layout.css +0 -60
  43. package/patterns/adia-chat/css/adia-chat.markdown.css +0 -74
  44. package/patterns/adia-chat/css/adia-chat.messages.css +0 -87
  45. package/patterns/adia-chat/css/adia-chat.streaming.css +0 -30
  46. package/patterns/adia-chat/css/adia-chat.tokens.css +0 -95
  47. package/patterns/adia-editor/adia-editor.a2ui.json +0 -73
  48. package/patterns/adia-editor/adia-editor.css +0 -6
  49. package/patterns/adia-editor/adia-editor.js +0 -56
  50. package/patterns/adia-editor/adia-editor.yaml +0 -59
  51. package/patterns/adia-editor/css/adia-editor.layout.css +0 -171
  52. package/patterns/adia-editor/css/adia-editor.tokens.css +0 -28
  53. package/patterns/app-nav/app-nav.a2ui.json +0 -89
  54. package/patterns/app-nav/app-nav.css +0 -92
  55. package/patterns/app-nav/app-nav.js +0 -112
  56. package/patterns/app-nav/app-nav.yaml +0 -54
  57. package/patterns/app-nav-group/app-nav-group.a2ui.json +0 -82
  58. package/patterns/app-nav-group/app-nav-group.css +0 -264
  59. package/patterns/app-nav-group/app-nav-group.js +0 -116
  60. package/patterns/app-nav-group/app-nav-group.yaml +0 -59
  61. package/patterns/app-nav-item/app-nav-item.a2ui.json +0 -83
  62. package/patterns/app-nav-item/app-nav-item.css +0 -162
  63. package/patterns/app-nav-item/app-nav-item.js +0 -42
  64. package/patterns/app-nav-item/app-nav-item.yaml +0 -62
  65. package/patterns/app-shell/app-shell.a2ui.json +0 -129
  66. package/patterns/app-shell/app-shell.css +0 -14
  67. package/patterns/app-shell/app-shell.js +0 -251
  68. package/patterns/app-shell/app-shell.yaml +0 -89
  69. package/patterns/app-shell/css/app-shell.collapsed.css +0 -86
  70. package/patterns/app-shell/css/app-shell.helpers.css +0 -42
  71. package/patterns/app-shell/css/app-shell.main.css +0 -172
  72. package/patterns/app-shell/css/app-shell.shell.css +0 -44
  73. package/patterns/app-shell/css/app-shell.sidebar.css +0 -161
  74. package/patterns/app-shell/css/app-shell.templates.css +0 -214
  75. package/patterns/app-shell/css/app-shell.tokens.css +0 -119
  76. package/patterns/gen-ui/gen-ui.a2ui.json +0 -72
  77. package/patterns/gen-ui/gen-ui.css +0 -83
  78. package/patterns/gen-ui/gen-ui.js +0 -136
  79. package/patterns/gen-ui/gen-ui.yaml +0 -43
  80. package/patterns/index.js +0 -11
  81. package/patterns/section-nav/section-nav.a2ui.json +0 -91
  82. package/patterns/section-nav/section-nav.css +0 -60
  83. package/patterns/section-nav/section-nav.js +0 -42
  84. package/patterns/section-nav/section-nav.yaml +0 -58
  85. package/patterns/section-nav-group/section-nav-group.a2ui.json +0 -95
  86. package/patterns/section-nav-group/section-nav-group.css +0 -74
  87. package/patterns/section-nav-group/section-nav-group.js +0 -84
  88. package/patterns/section-nav-group/section-nav-group.yaml +0 -66
  89. package/patterns/section-nav-item/section-nav-item.a2ui.json +0 -97
  90. package/patterns/section-nav-item/section-nav-item.css +0 -106
  91. package/patterns/section-nav-item/section-nav-item.js +0 -66
  92. package/patterns/section-nav-item/section-nav-item.yaml +0 -70
  93. package/styles/layouts/admin.css +0 -7
package/README.md CHANGED
@@ -46,20 +46,16 @@ web-components/
46
46
  │ └── data-stream.js `data-stream-*` attribute trait (HTTP/SSE/WS,
47
47
  │ signal-backed, refcounted shared transports)
48
48
 
49
- ├── components/ — 80 *-ui custom elements
49
+ ├── components/ — 80 *-ui custom elements (primitives)
50
50
  │ └── <tag>/
51
51
  │ ├── <tag>.js class definition (extends AdiaElement)
52
52
  │ ├── <tag>.css @scope(tag-ui) two-block: tokens + styles
53
53
  │ ├── <tag>.yaml authoring contract (props, slots, events, examples)
54
54
  │ └── <tag>.a2ui.json generated — do NOT edit
55
55
 
56
- ├── patterns/ — Higher-level compositions
57
- ├── app-shell, app-nav*, section-nav* — admin layout scaffolding
58
- ├── adia-chat, adia-editor, gen-ui LLM + editor + gen-UI patterns
59
- │ ├── a2ui-root — A2UI declarative host (moved
60
- │ │ from a2ui/ in 0.0.4; pairs
61
- │ │ with @adia-ai/a2ui-utils)
62
- │ └── index.js — registers all patterns
56
+ │ Composite elements (app-shell, app-nav*, adia-chat, adia-editor,
57
+ gen-ui, a2ui-root) ship in the sibling `@adia-ai/web-modules`
58
+ package as of 0.0.29 see ADR-0012 for the three-tier rationale.
63
59
 
64
60
  ├── traits/ — 42 composable behaviors via defineTrait()
65
61
  │ (pressable, focusTrap, confetti, resizable, …)
package/a2ui/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * The declarative `<a2ui-root>` custom element also moved — it now lives
13
13
  * in the patterns directory. Import it via:
14
- * import '@adia-ai/web-components/patterns/a2ui-root/a2ui-root.js';
14
+ * import '@adia-ai/web-modules/runtime/a2ui-root/a2ui-root.js';
15
15
  */
16
16
 
17
17
  if (typeof console !== 'undefined' && !globalThis.__a2ui_subpath_warned) {
@@ -63,7 +63,13 @@
63
63
 
64
64
  [data-questions-option] {
65
65
  display: flex;
66
- align-items: center;
66
+ /* Top-align so the leading icon stays with the label's first line
67
+ when a description wraps below; we restore visual centering on
68
+ the icon and check by nudging them to the label's first-line
69
+ center via margin-top. Centering the whole row dragged the icon
70
+ down to the body's vertical midpoint, which sat between label
71
+ and description rather than on the label. */
72
+ align-items: start;
67
73
  gap: var(--agent-questions-option-gap);
68
74
  padding: var(--agent-questions-option-padding);
69
75
  border: 1px solid var(--agent-questions-option-border);
@@ -101,6 +107,16 @@
101
107
 
102
108
  [data-questions-option-icon] {
103
109
  flex-shrink: 0;
110
+ /* Align the leading icon's center with the label's first-line
111
+ center. `1.5em` matches the label's effective line-height
112
+ (label is `font-size:` only — line-height inherits as 1.5).
113
+ `var(--a-icon-size)` is icon-ui's rendered height. Centering
114
+ the icon inside one line-box of the label's height puts its
115
+ midpoint on the label's first-line midpoint. We use em on the
116
+ icon's own font-size (which icon-ui keeps at the option's
117
+ font-size) so the offset scales with whatever the consumer
118
+ sets `--agent-questions-label-size` to. */
119
+ margin-top: calc((1.5em - var(--a-icon-size, 1em)) / 2);
104
120
  }
105
121
 
106
122
  [data-questions-option-body] {
@@ -134,6 +150,9 @@
134
150
  background: transparent;
135
151
  opacity: 0;
136
152
  transition: opacity var(--agent-questions-duration) var(--agent-questions-easing);
153
+ /* Match the leading-icon offset so a row of [icon · label · check]
154
+ reads as one horizontal line aligned to the label's first line. */
155
+ margin-top: calc((1.5em - var(--agent-questions-check-size)) / 2);
137
156
  }
138
157
 
139
158
  [data-questions-option][data-selected] [data-questions-option-check] {
@@ -106,35 +106,40 @@
106
106
  margin-block-start: var(--agent-trace-block-gap);
107
107
  }
108
108
 
109
- /* Rows container — a stack; each row owns its own grid. This makes
110
- expandable details elements play well, since <details> can't be a
111
- grid item spanning multiple tracks cleanly. */
109
+ /* Rows container — one grid, every row participates via subgrid.
110
+ This is the only way to get score + detail to land on the same X
111
+ across all rows; per-row grids let `max-content` widen the score
112
+ track row-by-row, and "94/100" pushes its row's detail column ~16px
113
+ right of "7%"'s detail column. Subgrid pulls the track widths up
114
+ to the parent so every row sees the same column stops. */
112
115
  [data-trace-rows] {
113
116
  --trace-row-cols: var(--agent-trace-row-label-col) max-content 1fr;
114
- display: flex;
115
- flex-direction: column;
117
+ display: grid;
118
+ grid-template-columns: var(--trace-row-cols);
119
+ column-gap: var(--agent-trace-block-gap);
116
120
  }
117
121
 
118
122
  [data-trace-rows][data-has-details] {
119
123
  --trace-row-cols: var(--agent-trace-row-label-col) max-content 1fr auto;
120
124
  }
121
125
 
122
- /* Each row's summary lays out the columns. For non-expandable rows
123
- (plain divs), the row itself is the grid. For expandable rows
124
- (<details>), the summary is the grid; the body is a block below. */
126
+ /* Each row + the headers row + each <details>'s <summary> all use
127
+ subgrid so they inherit the parent's column stops. <details> itself
128
+ is also a grid child so its open-state body can span all columns. */
125
129
  [data-trace-headers],
126
130
  div[data-trace-row],
131
+ details[data-trace-row],
127
132
  details[data-trace-row] > summary {
128
133
  display: grid;
129
- grid-template-columns: var(--trace-row-cols);
134
+ grid-template-columns: subgrid;
135
+ grid-column: 1 / -1;
130
136
  column-gap: var(--agent-trace-block-gap);
131
137
  align-items: baseline;
132
138
  min-width: 0;
133
139
  }
134
140
 
135
- details[data-trace-row] {
136
- display: block;
137
- min-width: 0;
141
+ details[data-trace-row] > [data-trace-row-body] {
142
+ grid-column: 1 / -1;
138
143
  }
139
144
 
140
145
  /* Column headers — small caps with subtle underline */
@@ -36,7 +36,15 @@ class AdiaBadge extends AdiaElement {
36
36
  static template = () => null;
37
37
 
38
38
  connected() {
39
- this.setAttribute('role', 'status');
39
+ /* Default role is `status` (matches badge's "passive callout" semantic
40
+ — a screen reader announces the badge text as a state without
41
+ interrupting). Consumers that wire interactivity (e.g.
42
+ `<chart-legend-ui>` toggling series) are free to set
43
+ `role="button"` (or any other role) before connection — we don't
44
+ overwrite an explicit consumer choice. */
45
+ if (!this.hasAttribute('role')) {
46
+ this.setAttribute('role', 'status');
47
+ }
40
48
  }
41
49
 
42
50
  render() {
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://adiaui.dev/a2ui/v0_9/components/Breadcrumb.json",
4
4
  "title": "Breadcrumb",
5
- "description": "Breadcrumb trail with auto-inserted separators.",
5
+ "description": "Breadcrumb trail with auto-inserted separators. Supports a leading icon (first child) and an overflow popover that collapses middle crumbs into a `…` menu.",
6
6
  "type": "object",
7
7
  "allOf": [
8
8
  {
@@ -13,6 +13,21 @@
13
13
  }
14
14
  ],
15
15
  "properties": {
16
+ "collapse": {
17
+ "description": "Collapse middle crumbs into a `…` overflow popover when there are 4+ items.",
18
+ "type": "boolean",
19
+ "default": false
20
+ },
21
+ "collapseKeepLeading": {
22
+ "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.",
23
+ "type": "number",
24
+ "default": 1
25
+ },
26
+ "collapseKeepTrailing": {
27
+ "description": "Number of trailing items to keep visible when [collapse] is active. The last item is always the current page.",
28
+ "type": "number",
29
+ "default": 2
30
+ },
16
31
  "component": {
17
32
  "const": "Breadcrumb"
18
33
  },
@@ -75,4 +75,31 @@
75
75
  pointer-events: none;
76
76
  text-decoration: none;
77
77
  }
78
+
79
+ /* Items moved into the overflow popover are kept in the light DOM
80
+ so the consumer's order of truth stays intact; we only hide them
81
+ visually when [collapse] is active. */
82
+ [data-collapsed] {
83
+ display: none;
84
+ }
85
+
86
+ /* Overflow trigger — `<menu-ui data-overflow>` stamped between
87
+ the leading and trailing visible groups. Layout-wise it behaves
88
+ like any other [data-item], but the trigger button inside it
89
+ handles its own padding/focus, so we strip the item max-width to
90
+ let the `…` glyph size to its content. The popover surface and
91
+ the menu-item-ui rows inherit menu-ui's canonical styling — no
92
+ visual overrides needed here. */
93
+ [data-overflow] {
94
+ max-width: none;
95
+ overflow: visible;
96
+ }
97
+
98
+ /* Icon-leading shape — a `<icon-ui>` (or a link wrapping one) as
99
+ the first child should optical-center on the text baseline of
100
+ the rest of the row without inheriting the text-truncate cap. */
101
+ [data-item] > icon-ui:only-child,
102
+ a[data-item]:has(> icon-ui:only-child) {
103
+ max-width: none;
104
+ }
78
105
  }
@@ -1,8 +1,32 @@
1
+ /**
2
+ * <breadcrumb-ui separator="/">
3
+ * <a href="/"><icon-ui name="house"></icon-ui></a>
4
+ * <a href="#">Workspace</a>
5
+ * <span>Current page</span>
6
+ * </breadcrumb-ui>
7
+ *
8
+ * Trail with auto-inserted separators. Items can be plain elements or
9
+ * `<a href>` links; the last item is marked `aria-current="page"`.
10
+ *
11
+ * Variants:
12
+ * • Default — text-only crumbs
13
+ * • Icon-leading — first child is `<icon-ui>` (or a link wrapping one)
14
+ * • Collapsed — `[collapse]` hides middle crumbs into a `…` overflow
15
+ * popover; keeps `[collapse-keep-leading]` (default 1) and
16
+ * `[collapse-keep-trailing]` (default 2) visible at the edges.
17
+ *
18
+ * The overflow uses `<popover-ui>` so it rides the top-layer (no
19
+ * z-index battles inside scrolling containers).
20
+ */
21
+
1
22
  import { AdiaElement } from '../../core/element.js';
2
23
 
3
24
  class AdiaBreadcrumb extends AdiaElement {
4
25
  static properties = {
5
- separator: { type: String, default: '/', reflect: true },
26
+ separator: { type: String, default: '/', reflect: true },
27
+ collapse: { type: Boolean, default: false, reflect: true },
28
+ collapseKeepLeading: { type: Number, default: 1, attribute: 'collapse-keep-leading', reflect: true },
29
+ collapseKeepTrailing: { type: Number, default: 2, attribute: 'collapse-keep-trailing', reflect: true },
6
30
  };
7
31
 
8
32
  static template = () => null;
@@ -13,30 +37,84 @@ class AdiaBreadcrumb extends AdiaElement {
13
37
  }
14
38
 
15
39
  render() {
16
- // Remove old separators
17
- this.querySelectorAll('[data-sep]').forEach(el => el.remove());
40
+ // Strip prior chrome so render is idempotent across attribute changes.
41
+ this.querySelectorAll('[data-sep], [data-overflow]').forEach(el => el.remove());
18
42
 
19
- const items = Array.from(this.children).filter(c => !c.hasAttribute('data-sep'));
43
+ const items = Array.from(this.children).filter(c =>
44
+ !c.hasAttribute('data-sep') && !c.hasAttribute('data-overflow')
45
+ );
20
46
  const last = items.length - 1;
21
47
 
48
+ // Reset per-item chrome
22
49
  items.forEach((child, i) => {
23
- child.removeAttribute('data-separator');
24
50
  child.setAttribute('data-item', '');
51
+ child.removeAttribute('data-collapsed');
52
+ if (i === last) child.setAttribute('aria-current', 'page');
53
+ else child.removeAttribute('aria-current');
54
+ });
55
+
56
+ const keepLeading = Math.max(0, this.collapseKeepLeading | 0);
57
+ const keepTrailing = Math.max(0, this.collapseKeepTrailing | 0);
58
+ const minLen = keepLeading + keepTrailing + 1;
59
+ const shouldCollapse = this.collapse && items.length >= Math.max(minLen, 4);
25
60
 
26
- if (i === last) {
27
- child.setAttribute('aria-current', 'page');
28
- } else {
29
- child.removeAttribute('aria-current');
30
- // Insert separator span after this item (before next sibling)
31
- if (!child.nextElementSibling?.hasAttribute('data-sep')) {
32
- const sep = document.createElement('span');
33
- sep.setAttribute('data-sep', '');
34
- sep.setAttribute('aria-hidden', 'true');
35
- sep.textContent = this.separator;
36
- child.after(sep);
37
- }
61
+ if (shouldCollapse) {
62
+ const collapsed = items.slice(keepLeading, items.length - keepTrailing);
63
+ collapsed.forEach(el => el.setAttribute('data-collapsed', ''));
64
+
65
+ const overflow = this.#buildOverflow(collapsed);
66
+ if (keepLeading > 0) items[keepLeading - 1].after(overflow);
67
+ else this.prepend(overflow);
68
+ }
69
+
70
+ // Stamp separators between every consecutive visible sibling.
71
+ const visible = Array.from(this.children).filter(c =>
72
+ !c.hasAttribute('data-sep') && !c.hasAttribute('data-collapsed')
73
+ );
74
+ for (let i = 0; i < visible.length - 1; i++) {
75
+ const sep = document.createElement('span');
76
+ sep.setAttribute('data-sep', '');
77
+ sep.setAttribute('aria-hidden', 'true');
78
+ sep.textContent = this.separator;
79
+ visible[i].after(sep);
80
+ }
81
+ }
82
+
83
+ #buildOverflow(collapsedItems) {
84
+ // Use <menu-ui> as the canonical popover-list primitive — same surface
85
+ // tokens as select-ui's listbox + menu-ui's action menu, with keyboard
86
+ // nav, top-layer rendering, and anchor positioning for free.
87
+ const menu = document.createElement('menu-ui');
88
+ menu.setAttribute('data-overflow', '');
89
+ menu.setAttribute('data-item', '');
90
+ menu.setAttribute('placement', 'bottom-start');
91
+
92
+ const trigger = document.createElement('button-ui');
93
+ trigger.setAttribute('slot', 'trigger');
94
+ trigger.setAttribute('text', '…');
95
+ trigger.setAttribute('variant', 'ghost');
96
+ trigger.setAttribute('size', 'sm');
97
+ trigger.setAttribute('aria-label', 'Show collapsed crumbs');
98
+ menu.appendChild(trigger);
99
+
100
+ for (const item of collapsedItems) {
101
+ const mi = document.createElement('menu-item-ui');
102
+ mi.setAttribute('text', item.textContent.trim());
103
+ // Encode the link target as the menu-item value; navigated on action.
104
+ if (item.tagName === 'A' && item.hasAttribute('href')) {
105
+ mi.setAttribute('value', item.getAttribute('href'));
38
106
  }
107
+ menu.appendChild(mi);
108
+ }
109
+
110
+ // menu-ui dispatches `action` with `detail: { value, text }` on item
111
+ // activation. Navigate to the encoded href; ignore placeholder `#`.
112
+ menu.addEventListener('action', (e) => {
113
+ const href = e.detail?.value;
114
+ if (href && href !== '#') window.location.href = href;
39
115
  });
116
+
117
+ return menu;
40
118
  }
41
119
  }
42
120
 
@@ -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:
@@ -1,5 +1,5 @@
1
1
  import { AdiaElement } from '../../core/element.js';
2
- import '../../patterns/a2ui-root/a2ui-root.js';
2
+ import '../../../web-modules/runtime/a2ui-root/a2ui-root.js';
3
3
 
4
4
  /**
5
5
  * <canvas-ui> — A2UI rendering surface.
@@ -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>`;