@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.
- package/components/agent-artifact/agent-artifact.a2ui.json +1 -1
- package/components/agent-artifact/agent-artifact.css +11 -0
- package/components/agent-artifact/agent-artifact.js +23 -2
- package/components/agent-artifact/agent-artifact.yaml +1 -1
- package/components/agent-questions/agent-questions.css +20 -1
- package/components/agent-reasoning/agent-reasoning.css +11 -0
- package/components/agent-reasoning/agent-reasoning.js +16 -0
- package/components/agent-trace/agent-trace.css +36 -12
- package/components/alert/alert.a2ui.json +10 -4
- package/components/alert/alert.css +13 -0
- package/components/alert/alert.js +1 -1
- package/components/alert/alert.yaml +21 -4
- package/components/badge/badge.a2ui.json +0 -2
- package/components/badge/badge.css +20 -0
- package/components/badge/badge.js +10 -2
- package/components/badge/badge.yaml +0 -2
- 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/calendar-picker/calendar-picker.css +17 -0
- 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/code/code.css +41 -0
- package/components/code/code.js +44 -3
- package/components/command/command.js +52 -1
- package/components/empty-state/empty-state.js +32 -21
- 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/list/list.js +20 -16
- package/components/menu/menu.css +18 -0
- package/components/menu/menu.js +24 -10
- package/components/pane/pane.css +5 -0
- package/components/pipeline-status/pipeline-status.css +15 -1
- package/components/popover/popover.css +17 -0
- package/components/select/select.css +18 -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/swiper/swiper.css +9 -0
- package/components/table/table.css +5 -0
- package/components/table/table.js +45 -1
- package/components/table-toolbar/table-toolbar.css +13 -0
- package/components/tag/tag.css +10 -0
- package/components/timeline/timeline.css +15 -4
- package/components/toast/toast.css +93 -48
- package/components/toast/toast.js +101 -22
- package/components/toolbar/toolbar.css +13 -0
- package/components/tooltip/tooltip.css +11 -3
- package/core/provider.js +1 -0
- package/package.json +1 -1
- package/styles/colors/semantics.css +1 -1
- package/styles/components.css +1 -0
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
--agent-artifact-fg-muted: var(--a-fg-muted);
|
|
14
14
|
--agent-artifact-border: var(--a-border-subtle);
|
|
15
15
|
--agent-artifact-header-bg: var(--a-bg);
|
|
16
|
+
--agent-artifact-header-bg-hover: var(--a-bg-hover);
|
|
16
17
|
|
|
17
18
|
/* ── Typography ── */
|
|
18
19
|
--agent-artifact-title-size: var(--a-ui-md);
|
|
@@ -46,6 +47,16 @@
|
|
|
46
47
|
cursor: pointer;
|
|
47
48
|
user-select: none;
|
|
48
49
|
min-width: 0;
|
|
50
|
+
transition: background var(--agent-artifact-duration);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
[data-artifact-header]:hover {
|
|
54
|
+
background: var(--agent-artifact-header-bg-hover);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
[data-artifact-header]:focus-visible {
|
|
58
|
+
outline: none;
|
|
59
|
+
box-shadow: var(--a-focus-ring) inset;
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
[data-artifact-icon] {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Attributes:
|
|
19
19
|
* title — header title (string)
|
|
20
|
-
* kind — small
|
|
20
|
+
* kind — small badge, e.g. "A2UI", "JSON", "TICKET" (value is
|
|
21
|
+
* normalized to uppercase before rendering)
|
|
21
22
|
* icon — icon-ui name for the header
|
|
22
23
|
* collapsed — start collapsed (body hidden)
|
|
23
24
|
* tone — neutral (default) | accent | warning | danger
|
|
@@ -59,6 +60,7 @@ class AdiaAgentArtifact extends AdiaElement {
|
|
|
59
60
|
|
|
60
61
|
disconnected() {
|
|
61
62
|
this.#headerEl?.removeEventListener('click', this.#onHeaderClick);
|
|
63
|
+
this.#headerEl?.removeEventListener('keydown', this.#onHeaderKey);
|
|
62
64
|
this.#headerEl = this.#iconEl = this.#titleEl = null;
|
|
63
65
|
this.#kindEl = this.#chevronEl = this.#bodyEl = null;
|
|
64
66
|
}
|
|
@@ -73,6 +75,17 @@ class AdiaAgentArtifact extends AdiaElement {
|
|
|
73
75
|
this.render();
|
|
74
76
|
};
|
|
75
77
|
|
|
78
|
+
// Keyboard activation — Space/Enter toggle (matches role="button" semantics).
|
|
79
|
+
#onHeaderKey = (e) => {
|
|
80
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
81
|
+
// Don't fire when the focus is on a slotted action button — let those
|
|
82
|
+
// handle their own activation.
|
|
83
|
+
if (e.target.closest('[slot="primary"], [slot="secondary"]')) return;
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
this.#onHeaderClick(e);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
76
89
|
render() {
|
|
77
90
|
if (!this.#headerEl) return;
|
|
78
91
|
|
|
@@ -95,6 +108,9 @@ class AdiaAgentArtifact extends AdiaElement {
|
|
|
95
108
|
if (this.#bodyEl) {
|
|
96
109
|
this.#bodyEl.hidden = this.collapsed;
|
|
97
110
|
}
|
|
111
|
+
if (this.#headerEl) {
|
|
112
|
+
this.#headerEl.setAttribute('aria-expanded', String(!this.collapsed));
|
|
113
|
+
}
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
#build() {
|
|
@@ -120,10 +136,15 @@ class AdiaAgentArtifact extends AdiaElement {
|
|
|
120
136
|
|
|
121
137
|
this.innerHTML = '';
|
|
122
138
|
|
|
123
|
-
// Header
|
|
139
|
+
// Header — keyboard-focusable button-style row that toggles collapsed.
|
|
140
|
+
// role/tabindex make Space/Enter activate it the same as click.
|
|
124
141
|
this.#headerEl = document.createElement('div');
|
|
125
142
|
this.#headerEl.setAttribute('data-artifact-header', '');
|
|
143
|
+
this.#headerEl.setAttribute('role', 'button');
|
|
144
|
+
this.#headerEl.setAttribute('tabindex', '0');
|
|
145
|
+
this.#headerEl.setAttribute('aria-expanded', String(!this.collapsed));
|
|
126
146
|
this.#headerEl.addEventListener('click', this.#onHeaderClick);
|
|
147
|
+
this.#headerEl.addEventListener('keydown', this.#onHeaderKey);
|
|
127
148
|
|
|
128
149
|
this.#iconEl = document.createElement('icon-ui');
|
|
129
150
|
this.#iconEl.setAttribute('data-artifact-icon', '');
|
|
@@ -9,7 +9,7 @@ version: 1
|
|
|
9
9
|
description: Inline container for structured agent artifacts (A2UI, JSON, tickets).
|
|
10
10
|
props:
|
|
11
11
|
kind:
|
|
12
|
-
description:
|
|
12
|
+
description: Badge label; value is normalized to uppercase before rendering.
|
|
13
13
|
type: string
|
|
14
14
|
default: ""
|
|
15
15
|
collapsed:
|
|
@@ -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] {
|
|
@@ -55,6 +55,17 @@
|
|
|
55
55
|
user-select: none;
|
|
56
56
|
padding-bottom: var(--a-space-1);
|
|
57
57
|
color: var(--agent-reasoning-fg-muted);
|
|
58
|
+
border-radius: var(--a-radius-sm);
|
|
59
|
+
transition: color var(--agent-reasoning-duration) var(--agent-reasoning-easing);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
[data-reasoning-summary]:hover {
|
|
63
|
+
color: var(--agent-reasoning-fg);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
[data-reasoning-summary]:focus-visible {
|
|
67
|
+
outline: none;
|
|
68
|
+
box-shadow: var(--a-focus-ring);
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
[data-reasoning-summary] > [data-reasoning-check],
|
|
@@ -109,6 +109,7 @@ class AdiaAgentReasoning extends AdiaElement {
|
|
|
109
109
|
this.#finishTimer = null;
|
|
110
110
|
}
|
|
111
111
|
this.#summaryEl?.removeEventListener('click', this.#onSummaryClick);
|
|
112
|
+
this.#summaryEl?.removeEventListener('keydown', this.#onSummaryKey);
|
|
112
113
|
this.#bodyEl?.removeEventListener('timeline-toggle', this.#onStepToggle);
|
|
113
114
|
this.#summaryEl = null;
|
|
114
115
|
this.#bodyEl = null;
|
|
@@ -265,12 +266,26 @@ class AdiaAgentReasoning extends AdiaElement {
|
|
|
265
266
|
this.#render();
|
|
266
267
|
};
|
|
267
268
|
|
|
269
|
+
// Keyboard activation — Space/Enter toggle (matches role="button" semantics).
|
|
270
|
+
#onSummaryKey = (e) => {
|
|
271
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
272
|
+
e.preventDefault();
|
|
273
|
+
this.#onSummaryClick();
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
268
277
|
#buildShell() {
|
|
269
278
|
this.innerHTML = '';
|
|
270
279
|
|
|
280
|
+
// Summary is a click-to-toggle row; mark up + key-activate so it
|
|
281
|
+
// behaves like a button under keyboard nav too.
|
|
271
282
|
this.#summaryEl = document.createElement('div');
|
|
272
283
|
this.#summaryEl.setAttribute('data-reasoning-summary', '');
|
|
284
|
+
this.#summaryEl.setAttribute('role', 'button');
|
|
285
|
+
this.#summaryEl.setAttribute('tabindex', '0');
|
|
286
|
+
this.#summaryEl.setAttribute('aria-expanded', String(!this.collapsed));
|
|
273
287
|
this.#summaryEl.addEventListener('click', this.#onSummaryClick);
|
|
288
|
+
this.#summaryEl.addEventListener('keydown', this.#onSummaryKey);
|
|
274
289
|
this.appendChild(this.#summaryEl);
|
|
275
290
|
|
|
276
291
|
this.#bodyEl = document.createElement('div');
|
|
@@ -341,6 +356,7 @@ class AdiaAgentReasoning extends AdiaElement {
|
|
|
341
356
|
`;
|
|
342
357
|
|
|
343
358
|
this.#bodyEl.hidden = this.collapsed;
|
|
359
|
+
this.#summaryEl.setAttribute('aria-expanded', String(!this.collapsed));
|
|
344
360
|
if (this.collapsed) return;
|
|
345
361
|
|
|
346
362
|
// Body rendering: we group contiguous steps into timeline-ui blocks and
|
|
@@ -43,9 +43,23 @@
|
|
|
43
43
|
gap: var(--agent-trace-inline-gap);
|
|
44
44
|
cursor: pointer;
|
|
45
45
|
white-space: nowrap;
|
|
46
|
+
border-radius: var(--a-radius-sm);
|
|
47
|
+
padding: var(--a-space-0-5) var(--a-space-1);
|
|
48
|
+
margin: calc(var(--a-space-0-5) * -1) calc(var(--a-space-1) * -1);
|
|
49
|
+
transition: background var(--agent-trace-chevron-dur);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[data-trace-root] > summary:hover {
|
|
53
|
+
background: var(--a-bg-subtle);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
[data-trace-root] > summary:focus-visible {
|
|
57
|
+
outline: none;
|
|
58
|
+
box-shadow: var(--a-focus-ring);
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
[data-trace-root] > summary::-webkit-details-marker { display: none; }
|
|
62
|
+
[data-trace-root] > summary::marker { content: ''; }
|
|
49
63
|
|
|
50
64
|
[data-trace-status] {
|
|
51
65
|
flex: 1;
|
|
@@ -92,35 +106,40 @@
|
|
|
92
106
|
margin-block-start: var(--agent-trace-block-gap);
|
|
93
107
|
}
|
|
94
108
|
|
|
95
|
-
/* Rows container —
|
|
96
|
-
|
|
97
|
-
|
|
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. */
|
|
98
115
|
[data-trace-rows] {
|
|
99
116
|
--trace-row-cols: var(--agent-trace-row-label-col) max-content 1fr;
|
|
100
|
-
display:
|
|
101
|
-
|
|
117
|
+
display: grid;
|
|
118
|
+
grid-template-columns: var(--trace-row-cols);
|
|
119
|
+
column-gap: var(--agent-trace-block-gap);
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
[data-trace-rows][data-has-details] {
|
|
105
123
|
--trace-row-cols: var(--agent-trace-row-label-col) max-content 1fr auto;
|
|
106
124
|
}
|
|
107
125
|
|
|
108
|
-
/* Each row
|
|
109
|
-
|
|
110
|
-
|
|
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. */
|
|
111
129
|
[data-trace-headers],
|
|
112
130
|
div[data-trace-row],
|
|
131
|
+
details[data-trace-row],
|
|
113
132
|
details[data-trace-row] > summary {
|
|
114
133
|
display: grid;
|
|
115
|
-
grid-template-columns:
|
|
134
|
+
grid-template-columns: subgrid;
|
|
135
|
+
grid-column: 1 / -1;
|
|
116
136
|
column-gap: var(--agent-trace-block-gap);
|
|
117
137
|
align-items: baseline;
|
|
118
138
|
min-width: 0;
|
|
119
139
|
}
|
|
120
140
|
|
|
121
|
-
details[data-trace-row] {
|
|
122
|
-
|
|
123
|
-
min-width: 0;
|
|
141
|
+
details[data-trace-row] > [data-trace-row-body] {
|
|
142
|
+
grid-column: 1 / -1;
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
/* Column headers — small caps with subtle underline */
|
|
@@ -163,6 +182,11 @@
|
|
|
163
182
|
background: var(--a-bg-subtle);
|
|
164
183
|
}
|
|
165
184
|
|
|
185
|
+
details[data-trace-row] > summary:focus-visible {
|
|
186
|
+
outline: none;
|
|
187
|
+
box-shadow: var(--a-focus-ring);
|
|
188
|
+
}
|
|
189
|
+
|
|
166
190
|
[data-trace-label] {
|
|
167
191
|
color: var(--agent-trace-fg-subtle);
|
|
168
192
|
}
|
|
@@ -93,11 +93,17 @@
|
|
|
93
93
|
"button"
|
|
94
94
|
],
|
|
95
95
|
"slots": {
|
|
96
|
-
"
|
|
97
|
-
"description": "
|
|
96
|
+
"action": {
|
|
97
|
+
"description": "Optional trailing action button (e.g. \"Refresh\", \"Status page\"). Right-aligned in the flex layout. See system-banners pattern for examples."
|
|
98
98
|
},
|
|
99
|
-
"
|
|
100
|
-
"description": "
|
|
99
|
+
"close": {
|
|
100
|
+
"description": "Close button. Stamped automatically when `closable` is set; the stamped element is `<button-ui slot=\"close\" icon=\"x\" variant=\"ghost\" size=\"sm\">`. Override by passing a custom `slot=\"close\"` button."
|
|
101
|
+
},
|
|
102
|
+
"content": {
|
|
103
|
+
"description": "Alert body. For single-line text use the `text=` attribute (alert.js stamps a `<span slot=\"content\">` automatically). For rich content with title + description, wrap in `<col-ui slot=\"content\">` — the alert root is `display: flex` row, so multiple bare children would lay out side-by-side instead of stacking."
|
|
104
|
+
},
|
|
105
|
+
"leading": {
|
|
106
|
+
"description": "Leading icon. Stamped automatically from the `icon=` attribute; consumers can override by passing a custom `<icon-ui slot=\"leading\">`."
|
|
101
107
|
}
|
|
102
108
|
},
|
|
103
109
|
"states": [
|
|
@@ -65,6 +65,19 @@
|
|
|
65
65
|
--alert-icon-fg: var(--a-danger-strong);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/* `muted` and `neutral` are semantic aliases of the base — same tokens
|
|
69
|
+
as the default variant, declared explicitly so the yaml enum and the
|
|
70
|
+
CSS contract agree. Use cases: passive callouts (beta banners,
|
|
71
|
+
tertiary notes) where the alert should read as quiet chrome rather
|
|
72
|
+
than carrying tonal weight. */
|
|
73
|
+
:scope[variant="muted"],
|
|
74
|
+
:scope[variant="neutral"] {
|
|
75
|
+
--alert-bg: var(--a-bg-muted);
|
|
76
|
+
--alert-fg: var(--a-fg);
|
|
77
|
+
--alert-border: var(--a-border-subtle);
|
|
78
|
+
--alert-icon-fg: var(--a-fg-muted);
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
/* ── Slots ── */
|
|
69
82
|
:scope [slot="leading"] {
|
|
70
83
|
flex-shrink: 0;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Inline alert banner with optional icon and close button.
|
|
6
6
|
*
|
|
7
|
-
* Variants: default, info, success, warning, danger
|
|
7
|
+
* Variants: default, info, success, warning, danger, muted, neutral
|
|
8
8
|
* Slots: leading (icon), content (text), close (dismiss button)
|
|
9
9
|
*
|
|
10
10
|
* Events:
|
|
@@ -36,10 +36,27 @@ events:
|
|
|
36
36
|
close:
|
|
37
37
|
description: Fired when the close button is clicked
|
|
38
38
|
slots:
|
|
39
|
-
|
|
40
|
-
description:
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
content:
|
|
40
|
+
description: >-
|
|
41
|
+
Alert body. For single-line text use the `text=` attribute (alert.js
|
|
42
|
+
stamps a `<span slot="content">` automatically). For rich content
|
|
43
|
+
with title + description, wrap in `<col-ui slot="content">` — the
|
|
44
|
+
alert root is `display: flex` row, so multiple bare children would
|
|
45
|
+
lay out side-by-side instead of stacking.
|
|
46
|
+
leading:
|
|
47
|
+
description: >-
|
|
48
|
+
Leading icon. Stamped automatically from the `icon=` attribute;
|
|
49
|
+
consumers can override by passing a custom `<icon-ui slot="leading">`.
|
|
50
|
+
close:
|
|
51
|
+
description: >-
|
|
52
|
+
Close button. Stamped automatically when `closable` is set; the
|
|
53
|
+
stamped element is `<button-ui slot="close" icon="x" variant="ghost"
|
|
54
|
+
size="sm">`. Override by passing a custom `slot="close"` button.
|
|
55
|
+
action:
|
|
56
|
+
description: >-
|
|
57
|
+
Optional trailing action button (e.g. "Refresh", "Status page").
|
|
58
|
+
Right-aligned in the flex layout. See system-banners pattern for
|
|
59
|
+
examples.
|
|
43
60
|
states:
|
|
44
61
|
- name: idle
|
|
45
62
|
description: Default, ready for interaction.
|
|
@@ -74,5 +74,25 @@
|
|
|
74
74
|
--badge-fg: var(--a-danger-text);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/* `primary` is the accent-filled variant — solid bg + on-accent text,
|
|
78
|
+
used for "PRO"-style emphasis chips that should read as a brand
|
|
79
|
+
stamp rather than tinted-on-canvas. Uses the L3 primary-* surface
|
|
80
|
+
matrix so it tracks the same accent-strong source as button-ui's
|
|
81
|
+
primary variant. */
|
|
82
|
+
:scope[variant="primary"] {
|
|
83
|
+
--badge-bg: var(--a-primary-bg);
|
|
84
|
+
--badge-fg: var(--a-primary-fg);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* `muted` and `neutral` are semantic aliases of the base — same tokens
|
|
88
|
+
as the default variant, declared explicitly so the yaml enum and the
|
|
89
|
+
CSS contract agree. Use cases: passive metadata chips (counts, IDs,
|
|
90
|
+
"Not set" states) where the badge should read as quiet chrome. */
|
|
91
|
+
:scope[variant="muted"],
|
|
92
|
+
:scope[variant="neutral"] {
|
|
93
|
+
--badge-bg: var(--a-bg-muted);
|
|
94
|
+
--badge-fg: var(--a-fg);
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
/* Size handled by universal [size] attribute system. */
|
|
78
98
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Inline badge / tag. Pill-shaped, text rendered via CSS attr().
|
|
8
8
|
*
|
|
9
|
-
* Variants: default, accent, info, success, warning, danger
|
|
9
|
+
* Variants: default, accent, info, success, warning, danger, primary, muted, neutral
|
|
10
10
|
* Sizes: sm, md (default)
|
|
11
11
|
* Icon: optional leading icon (any registered icon name; "dot" for legend
|
|
12
12
|
* markers). Inherits the variant's foreground color so legend chips
|
|
@@ -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
|
|