@adia-ai/web-components 0.0.27 → 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.
- package/components/agent-questions/agent-questions.css +20 -1
- package/components/agent-trace/agent-trace.css +17 -12
- package/components/badge/badge.js +9 -1
- package/components/breadcrumb/breadcrumb.a2ui.json +16 -1
- package/components/breadcrumb/breadcrumb.css +27 -0
- package/components/breadcrumb/breadcrumb.js +95 -17
- package/components/breadcrumb/breadcrumb.yaml +15 -1
- package/components/chart/chart.css +20 -13
- package/components/chart/chart.js +49 -17
- package/components/chart-legend/chart-legend.css +30 -54
- package/components/chart-legend/chart-legend.js +48 -30
- package/components/command/command.js +52 -1
- package/components/feed/feed-item.yaml +50 -0
- package/components/feed/feed.a2ui.json +59 -0
- package/components/feed/feed.css +141 -0
- package/components/feed/feed.js +276 -0
- package/components/feed/feed.yaml +33 -0
- package/components/index.js +2 -0
- package/components/swatch/swatch.a2ui.json +116 -0
- package/components/swatch/swatch.css +141 -0
- package/components/swatch/swatch.js +121 -0
- package/components/swatch/swatch.yaml +101 -0
- package/components/timeline/timeline.css +5 -1
- package/components/tooltip/tooltip.css +3 -3
- package/core/provider.js +1 -0
- package/package.json +1 -1
- package/styles/components.css +1 -0
|
@@ -63,7 +63,13 @@
|
|
|
63
63
|
|
|
64
64
|
[data-questions-option] {
|
|
65
65
|
display: flex;
|
|
66
|
-
align
|
|
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 —
|
|
110
|
-
|
|
111
|
-
|
|
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:
|
|
115
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
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 =>
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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:
|
|
@@ -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
|
-
|
|
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 += `<
|
|
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,
|
|
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,
|
|
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 += `<
|
|
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
|
|
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
|
|
1952
|
-
//
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
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
|
|
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 += `<
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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])
|
|
89
|
+
badge-ui[data-row]:not([data-active]) swatch-ui {
|
|
87
90
|
opacity: 0.4;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
/* Swatch
|
|
91
|
-
|
|
92
|
-
--
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
}
|