@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
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <feed-ui> + <feed-item-ui> — Shared top-layer feed channel.
|
|
3
|
+
*
|
|
4
|
+
* Per docs/specs/feed-channel.md (SPEC-FEED-CHANNEL-001).
|
|
5
|
+
*
|
|
6
|
+
* Phase 1 (skeleton) ships here:
|
|
7
|
+
* - Per-position singletons (`<feed-ui position="bottom-right">`)
|
|
8
|
+
* resolving OD-FEED-1; mounted lazily into document.body via
|
|
9
|
+
* Popover API for top-layer placement.
|
|
10
|
+
* - Auto-fade `<feed-item-ui>` policy. Sticky-dismissible and
|
|
11
|
+
* action-required policies are stubs that respect the contract
|
|
12
|
+
* (no auto-dismiss when `duration` is null/0; close button
|
|
13
|
+
* surfaces when `dismissible` is set) but the action-required
|
|
14
|
+
* focus-trap is deferred.
|
|
15
|
+
* - Static API: AdiaFeed.post() / .get() / .clear() / .purge()
|
|
16
|
+
* — `purge()` directly addresses the audit's L-B4 container-leak
|
|
17
|
+
* finding by giving consumers a tear-down hook.
|
|
18
|
+
* - Global event channel: window.dispatchEvent(new CustomEvent(
|
|
19
|
+
* 'feed', { detail: { text, position, ... } })) — same shape as
|
|
20
|
+
* AdiaFeed.post() so any code can post without importing the
|
|
21
|
+
* module directly. Idempotent listener install (HMR-safe).
|
|
22
|
+
*
|
|
23
|
+
* Toast migration (spec § 2.5) is deferred — toast-ui keeps its own
|
|
24
|
+
* container + API for now; the migration happens once the feed-ui
|
|
25
|
+
* surface has soaked.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { AdiaElement, html } from '../../core/element.js';
|
|
29
|
+
|
|
30
|
+
/* ── Container — one per position, mounted into document.body ── */
|
|
31
|
+
|
|
32
|
+
class AdiaFeedContainer extends AdiaElement {
|
|
33
|
+
static properties = {
|
|
34
|
+
position: { type: String, default: 'bottom-right', reflect: true },
|
|
35
|
+
max: { type: Number, default: 5, reflect: true },
|
|
36
|
+
};
|
|
37
|
+
static template = () => html``;
|
|
38
|
+
|
|
39
|
+
connected() {
|
|
40
|
+
if (!this.hasAttribute('role')) this.setAttribute('role', 'region');
|
|
41
|
+
if (!this.hasAttribute('aria-label')) this.setAttribute('aria-label', 'Feed');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
customElements.define('feed-ui', AdiaFeedContainer);
|
|
46
|
+
|
|
47
|
+
/* ── Item — one per posted message ── */
|
|
48
|
+
|
|
49
|
+
class AdiaFeedItem extends AdiaElement {
|
|
50
|
+
#timer = null;
|
|
51
|
+
#removing = false;
|
|
52
|
+
#closeTimer = null;
|
|
53
|
+
#openRaf = null;
|
|
54
|
+
|
|
55
|
+
static properties = {
|
|
56
|
+
text: { type: String, default: '', reflect: true },
|
|
57
|
+
heading: { type: String, default: '', reflect: true },
|
|
58
|
+
icon: { type: String, default: '', reflect: true },
|
|
59
|
+
variant: { type: String, default: 'default', reflect: true },
|
|
60
|
+
duration: { type: Number, default: 4000, reflect: true },
|
|
61
|
+
dismissible: { type: Boolean, default: false, reflect: true },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
static parts = {
|
|
65
|
+
body: '<div slot="body"></div>',
|
|
66
|
+
};
|
|
67
|
+
static template = () => html``;
|
|
68
|
+
|
|
69
|
+
#onPress = (e) => {
|
|
70
|
+
if (e.target.closest('[data-feed-close]')) this.dismiss();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
connected() {
|
|
74
|
+
this.addEventListener('press', this.#onPress);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#getDuration() {
|
|
78
|
+
const raw = getComputedStyle(this).getPropertyValue('--feed-item-duration').trim();
|
|
79
|
+
return parseFloat(raw) || 200;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
render() {
|
|
83
|
+
const body = this.ensure('body');
|
|
84
|
+
body.textContent = '';
|
|
85
|
+
if (this.heading) {
|
|
86
|
+
const h = document.createElement('strong');
|
|
87
|
+
h.textContent = this.heading;
|
|
88
|
+
h.style.display = 'block';
|
|
89
|
+
body.appendChild(h);
|
|
90
|
+
}
|
|
91
|
+
if (this.text) {
|
|
92
|
+
const t = document.createElement('span');
|
|
93
|
+
t.textContent = this.text;
|
|
94
|
+
body.appendChild(t);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Inferred policy roles per spec §2.4. Auto-fade gets `status`;
|
|
98
|
+
sticky danger/warning gets `alert`; action-required gets
|
|
99
|
+
`alertdialog` (a future phase wires the focus trap). */
|
|
100
|
+
const isSticky = !this.duration || this.duration <= 0;
|
|
101
|
+
const isLoud = this.variant === 'danger' || this.variant === 'warning';
|
|
102
|
+
let role = 'status';
|
|
103
|
+
if (isSticky && isLoud) role = 'alert';
|
|
104
|
+
this.setAttribute('role', role);
|
|
105
|
+
this.setAttribute('aria-live', role === 'alert' ? 'assertive' : 'polite');
|
|
106
|
+
|
|
107
|
+
/* Render dismiss button for sticky items (spec §2.2 — default true
|
|
108
|
+
for sticky, false for auto-fade). */
|
|
109
|
+
const wantsClose = this.dismissible || isSticky;
|
|
110
|
+
let close = this.querySelector(':scope > [data-feed-close]');
|
|
111
|
+
if (wantsClose && !close) {
|
|
112
|
+
close = document.createElement('button-ui');
|
|
113
|
+
close.setAttribute('data-feed-close', '');
|
|
114
|
+
close.setAttribute('icon', 'x');
|
|
115
|
+
close.setAttribute('variant', 'ghost');
|
|
116
|
+
close.setAttribute('size', 'sm');
|
|
117
|
+
close.setAttribute('aria-label', 'Dismiss');
|
|
118
|
+
this.appendChild(close);
|
|
119
|
+
} else if (!wantsClose && close) {
|
|
120
|
+
close.remove();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!this.hasAttribute('data-open') && !this.#removing) {
|
|
124
|
+
this.#openRaf = requestAnimationFrame(() => {
|
|
125
|
+
this.#openRaf = null;
|
|
126
|
+
this.setAttribute('data-open', '');
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
this.#scheduleAutoDismiss();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#scheduleAutoDismiss() {
|
|
133
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
134
|
+
if (this.duration && this.duration > 0) {
|
|
135
|
+
this.#timer = setTimeout(() => this.dismiss(), this.duration);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
dismiss() {
|
|
140
|
+
if (this.#removing) return;
|
|
141
|
+
this.#removing = true;
|
|
142
|
+
if (this.#timer) { clearTimeout(this.#timer); this.#timer = null; }
|
|
143
|
+
this.removeAttribute('data-open');
|
|
144
|
+
this.setAttribute('data-closing', '');
|
|
145
|
+
this.#closeTimer = setTimeout(() => {
|
|
146
|
+
this.#closeTimer = null;
|
|
147
|
+
const container = this.parentElement;
|
|
148
|
+
this.dispatchEvent(new Event('close', { bubbles: true }));
|
|
149
|
+
this.remove();
|
|
150
|
+
if (container?.matches?.('feed-ui')) AdiaFeed.releaseContainerIfEmpty(container);
|
|
151
|
+
}, this.#getDuration());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
update(patch) {
|
|
155
|
+
if (!patch || typeof patch !== 'object') return;
|
|
156
|
+
for (const k of Object.keys(patch)) {
|
|
157
|
+
if (k in AdiaFeedItem.properties) this[k] = patch[k];
|
|
158
|
+
}
|
|
159
|
+
this.#scheduleAutoDismiss();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
disconnected() {
|
|
163
|
+
this.removeEventListener('press', this.#onPress);
|
|
164
|
+
if (this.#timer) { clearTimeout(this.#timer); this.#timer = null; }
|
|
165
|
+
if (this.#closeTimer) { clearTimeout(this.#closeTimer); this.#closeTimer = null; }
|
|
166
|
+
if (this.#openRaf != null) { cancelAnimationFrame(this.#openRaf); this.#openRaf = null; }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
customElements.define('feed-item-ui', AdiaFeedItem);
|
|
171
|
+
|
|
172
|
+
/* ── Static API — AdiaFeed ── */
|
|
173
|
+
|
|
174
|
+
class AdiaFeed {
|
|
175
|
+
static #containers = new Map();
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get (or lazily create) the per-position lane. Each lane is a
|
|
179
|
+
* manual Popover-API container — `[popover="manual"]` puts it in
|
|
180
|
+
* the browser's top-layer with no z-index wars and the native
|
|
181
|
+
* popover stack lets multiple lanes coexist (e.g. one feed in
|
|
182
|
+
* top-right + another in bottom-center) without collision.
|
|
183
|
+
*/
|
|
184
|
+
static get(position = 'bottom-right') {
|
|
185
|
+
let el = AdiaFeed.#containers.get(position);
|
|
186
|
+
if (el && el.isConnected) return el;
|
|
187
|
+
el = document.createElement('feed-ui');
|
|
188
|
+
el.setAttribute('position', position);
|
|
189
|
+
if ('popover' in HTMLElement.prototype) el.setAttribute('popover', 'manual');
|
|
190
|
+
document.body.appendChild(el);
|
|
191
|
+
try { el.showPopover?.(); } catch { /* graceful fallback */ }
|
|
192
|
+
AdiaFeed.#containers.set(position, el);
|
|
193
|
+
return el;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Post a feed item. Returns a `FeedHandle` (`{id, dismiss, update}`).
|
|
198
|
+
*
|
|
199
|
+
* @param {Object} opts
|
|
200
|
+
* @param {string} [opts.text]
|
|
201
|
+
* @param {string} [opts.heading]
|
|
202
|
+
* @param {string} [opts.icon]
|
|
203
|
+
* @param {string} [opts.variant='default'] default | info | success | warning | danger
|
|
204
|
+
* @param {number|null} [opts.duration=4000] ms; null/0 = sticky (requires close click)
|
|
205
|
+
* @param {string} [opts.position='bottom-right']
|
|
206
|
+
* @param {boolean} [opts.dismissible] override default (true for sticky, false for auto)
|
|
207
|
+
* @param {string} [opts.id]
|
|
208
|
+
*/
|
|
209
|
+
static post(opts = {}) {
|
|
210
|
+
const {
|
|
211
|
+
text = '',
|
|
212
|
+
heading = '',
|
|
213
|
+
icon = '',
|
|
214
|
+
variant = 'default',
|
|
215
|
+
duration = 4000,
|
|
216
|
+
position = 'bottom-right',
|
|
217
|
+
dismissible,
|
|
218
|
+
id,
|
|
219
|
+
} = opts;
|
|
220
|
+
const v = variant === 'error' ? 'danger' : variant; // documented alias
|
|
221
|
+
const container = AdiaFeed.get(position);
|
|
222
|
+
const item = document.createElement('feed-item-ui');
|
|
223
|
+
if (id) item.setAttribute('data-id', id);
|
|
224
|
+
item.text = text;
|
|
225
|
+
item.heading = heading;
|
|
226
|
+
item.icon = icon;
|
|
227
|
+
item.variant = v;
|
|
228
|
+
item.duration = duration;
|
|
229
|
+
if (dismissible != null) item.dismissible = !!dismissible;
|
|
230
|
+
container.appendChild(item);
|
|
231
|
+
return {
|
|
232
|
+
id: id ?? null,
|
|
233
|
+
dismiss: () => item.dismiss(),
|
|
234
|
+
update: (patch) => item.update(patch),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Clear all items in a single lane. */
|
|
239
|
+
static clear(position = 'bottom-right') {
|
|
240
|
+
const el = AdiaFeed.#containers.get(position);
|
|
241
|
+
if (!el) return;
|
|
242
|
+
for (const item of [...el.children]) {
|
|
243
|
+
if (item.tagName === 'FEED-ITEM-UI') item.dismiss?.();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Tear down ALL containers. Test cleanup; addresses audit L-B4. */
|
|
248
|
+
static purge() {
|
|
249
|
+
for (const el of AdiaFeed.#containers.values()) {
|
|
250
|
+
try { el.hidePopover?.(); } catch { /* noop */ }
|
|
251
|
+
el.remove();
|
|
252
|
+
}
|
|
253
|
+
AdiaFeed.#containers.clear();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Internal: drop a lane when its last item exits. */
|
|
257
|
+
static releaseContainerIfEmpty(container) {
|
|
258
|
+
if (!container || container.children.length > 0) return;
|
|
259
|
+
try { container.hidePopover?.(); } catch { /* noop */ }
|
|
260
|
+
container.remove();
|
|
261
|
+
for (const [pos, el] of AdiaFeed.#containers) {
|
|
262
|
+
if (el === container) AdiaFeed.#containers.delete(pos);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Global 'feed' CustomEvent listener — same shape as AdiaFeed.post().
|
|
268
|
+
Idempotent (HMR-safe via window flag). */
|
|
269
|
+
if (typeof window !== 'undefined' && !window.__adiaFeedListenerInstalled) {
|
|
270
|
+
window.__adiaFeedListenerInstalled = true;
|
|
271
|
+
window.addEventListener('feed', (e) => {
|
|
272
|
+
if (e?.detail && typeof e.detail === 'object') AdiaFeed.post(e.detail);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export { AdiaFeedContainer, AdiaFeedItem, AdiaFeed };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Generated alongside feed.js — kept in sync by hand for now (feed
|
|
2
|
+
# is a Phase-1 skeleton; once it stabilizes, run the regen pipeline).
|
|
3
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
4
|
+
name: AdiaFeedContainer
|
|
5
|
+
tag: feed-ui
|
|
6
|
+
component: Feed
|
|
7
|
+
category: container
|
|
8
|
+
version: 1
|
|
9
|
+
description: >-
|
|
10
|
+
Shared top-layer feed channel. Per docs/specs/feed-channel.md
|
|
11
|
+
(SPEC-FEED-CHANNEL-001). Per-position singletons mounted lazily into
|
|
12
|
+
document.body via Popover API; consumers post via the static API
|
|
13
|
+
(`AdiaFeed.post()`) or the global 'feed' CustomEvent.
|
|
14
|
+
props:
|
|
15
|
+
position:
|
|
16
|
+
description: Lane the feed renders into
|
|
17
|
+
type: string
|
|
18
|
+
default: bottom-right
|
|
19
|
+
enum:
|
|
20
|
+
- top-left
|
|
21
|
+
- top-center
|
|
22
|
+
- top-right
|
|
23
|
+
- bottom-left
|
|
24
|
+
- bottom-center
|
|
25
|
+
- bottom-right
|
|
26
|
+
- inline
|
|
27
|
+
max:
|
|
28
|
+
description: Cap on simultaneously visible items per lane
|
|
29
|
+
type: number
|
|
30
|
+
default: 5
|
|
31
|
+
events: {}
|
|
32
|
+
slots: {}
|
|
33
|
+
states: {}
|
package/components/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export { AdiaChat } from './chat/chat.js';
|
|
|
30
30
|
export { AdiaDrawer } from './drawer/drawer.js';
|
|
31
31
|
export { AdiaModal } from './modal/modal.js';
|
|
32
32
|
export { AdiaToast } from './toast/toast.js';
|
|
33
|
+
export { AdiaFeedContainer, AdiaFeedItem, AdiaFeed } from './feed/feed.js';
|
|
33
34
|
export { AdiaTabs } from './tabs/tabs.js';
|
|
34
35
|
export { AdiaTab } from './tabs/tab.js';
|
|
35
36
|
export { AdiaTooltip } from './tooltip/tooltip.js';
|
|
@@ -51,6 +52,7 @@ export { AdiaSkeleton } from './skeleton/skeleton.js';
|
|
|
51
52
|
export { AdiaAlert } from './alert/alert.js';
|
|
52
53
|
export { AdiaKbd } from './kbd/kbd.js';
|
|
53
54
|
export { AdiaTag } from './tag/tag.js';
|
|
55
|
+
export { AdiaSwatch } from './swatch/swatch.js';
|
|
54
56
|
export { AdiaCol } from './col/col.js';
|
|
55
57
|
export { AdiaField } from './field/field.js';
|
|
56
58
|
export { AdiaRow } from './row/row.js';
|
package/components/list/list.js
CHANGED
|
@@ -161,15 +161,19 @@ class AdiaListItem extends AdiaElement {
|
|
|
161
161
|
return null;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// Mark slot elements we create so render() never deletes consumer-provided ones.
|
|
165
|
+
#stampMark(el) { el.dataset.listStamped = '1'; return el; }
|
|
166
|
+
#wasStamped(el) { return el?.dataset?.listStamped === '1'; }
|
|
167
|
+
|
|
164
168
|
#stamp() {
|
|
165
169
|
if (this.#ownChild('[slot="content"]')) return;
|
|
166
170
|
|
|
167
171
|
if (this.icon) {
|
|
168
172
|
let iconEl = this.#ownChild('[slot="icon"]') || this.#ownChild('icon-ui');
|
|
169
173
|
if (!iconEl) {
|
|
170
|
-
iconEl = document.createElement('icon-ui');
|
|
174
|
+
iconEl = this.#stampMark(document.createElement('icon-ui'));
|
|
171
175
|
iconEl.setAttribute('slot', 'icon');
|
|
172
|
-
this.
|
|
176
|
+
this.prepend(iconEl);
|
|
173
177
|
}
|
|
174
178
|
iconEl.setAttribute('name', this.icon);
|
|
175
179
|
}
|
|
@@ -177,56 +181,56 @@ class AdiaListItem extends AdiaElement {
|
|
|
177
181
|
if (this.text) {
|
|
178
182
|
let span = this.#ownChild('[slot="text"]');
|
|
179
183
|
if (!span) {
|
|
180
|
-
span = document.createElement('span');
|
|
184
|
+
span = this.#stampMark(document.createElement('span'));
|
|
181
185
|
span.setAttribute('slot', 'text');
|
|
182
186
|
this.appendChild(span);
|
|
183
187
|
}
|
|
184
|
-
span.textContent = this.text;
|
|
188
|
+
if (this.#wasStamped(span)) span.textContent = this.text;
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
if (this.description) {
|
|
188
192
|
let desc = this.#ownChild('[slot="description"]');
|
|
189
193
|
if (!desc) {
|
|
190
|
-
desc = document.createElement('span');
|
|
194
|
+
desc = this.#stampMark(document.createElement('span'));
|
|
191
195
|
desc.setAttribute('slot', 'description');
|
|
192
196
|
this.appendChild(desc);
|
|
193
197
|
}
|
|
194
|
-
desc.textContent = this.description;
|
|
198
|
+
if (this.#wasStamped(desc)) desc.textContent = this.description;
|
|
195
199
|
}
|
|
196
200
|
}
|
|
197
201
|
|
|
198
202
|
render() {
|
|
199
|
-
// Sync icon
|
|
203
|
+
// Sync icon — only touch elements we stamped.
|
|
200
204
|
const iconEl = this.#ownChild('[slot="icon"]');
|
|
201
205
|
if (this.icon) {
|
|
202
206
|
if (iconEl) {
|
|
203
|
-
iconEl.setAttribute('name', this.icon);
|
|
207
|
+
if (this.#wasStamped(iconEl)) iconEl.setAttribute('name', this.icon);
|
|
204
208
|
} else {
|
|
205
|
-
const el = document.createElement('icon-ui');
|
|
209
|
+
const el = this.#stampMark(document.createElement('icon-ui'));
|
|
206
210
|
el.setAttribute('slot', 'icon');
|
|
207
211
|
el.setAttribute('name', this.icon);
|
|
208
212
|
this.prepend(el);
|
|
209
213
|
}
|
|
210
|
-
} else if (iconEl) {
|
|
214
|
+
} else if (this.#wasStamped(iconEl)) {
|
|
211
215
|
iconEl.remove();
|
|
212
216
|
}
|
|
213
217
|
|
|
214
|
-
// Sync text
|
|
218
|
+
// Sync text — only touch elements we stamped.
|
|
215
219
|
const textEl = this.#ownChild('[slot="text"]');
|
|
216
|
-
if (textEl) textEl.textContent = this.text;
|
|
220
|
+
if (this.#wasStamped(textEl)) textEl.textContent = this.text;
|
|
217
221
|
|
|
218
|
-
// Sync description
|
|
222
|
+
// Sync description — only touch elements we stamped.
|
|
219
223
|
const descEl = this.#ownChild('[slot="description"]');
|
|
220
224
|
if (this.description) {
|
|
221
225
|
if (descEl) {
|
|
222
|
-
descEl.textContent = this.description;
|
|
226
|
+
if (this.#wasStamped(descEl)) descEl.textContent = this.description;
|
|
223
227
|
} else {
|
|
224
|
-
const el = document.createElement('span');
|
|
228
|
+
const el = this.#stampMark(document.createElement('span'));
|
|
225
229
|
el.setAttribute('slot', 'description');
|
|
226
230
|
el.textContent = this.description;
|
|
227
231
|
this.appendChild(el);
|
|
228
232
|
}
|
|
229
|
-
} else if (descEl) {
|
|
233
|
+
} else if (this.#wasStamped(descEl)) {
|
|
230
234
|
descEl.remove();
|
|
231
235
|
}
|
|
232
236
|
}
|
package/components/menu/menu.css
CHANGED
|
@@ -38,6 +38,24 @@ menu-ui [data-menu-popover] {
|
|
|
38
38
|
font-family: inherit;
|
|
39
39
|
font-size: var(--a-ui-size);
|
|
40
40
|
color: var(--a-fg);
|
|
41
|
+
/* Fade + lift in on first paint via @starting-style. Plain `transition`
|
|
42
|
+
applies during exit too (display: none can't transition, but opacity
|
|
43
|
+
can — and the close path is JS-controlled so no exit anim is needed). */
|
|
44
|
+
opacity: 1;
|
|
45
|
+
translate: 0 0;
|
|
46
|
+
transition: opacity var(--a-duration-fast) var(--a-easing-out),
|
|
47
|
+
translate var(--a-duration-fast) var(--a-easing-out);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
menu-ui [data-menu-popover]:popover-open {
|
|
51
|
+
@starting-style {
|
|
52
|
+
opacity: 0;
|
|
53
|
+
translate: 0 -4px;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@media (prefers-reduced-motion: reduce) {
|
|
58
|
+
menu-ui [data-menu-popover] { transition: none; }
|
|
41
59
|
}
|
|
42
60
|
|
|
43
61
|
/* Safari 17.x bug: `:scope:hover` inside `@scope` doesn't match the scope
|
package/components/menu/menu.js
CHANGED
|
@@ -251,18 +251,30 @@ class AdiaMenuItem extends AdiaElement {
|
|
|
251
251
|
this.#syncAria();
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
// Mark slot elements we create so render() never deletes consumer-provided ones.
|
|
255
|
+
// See ADR-0010 (slot content is source of truth).
|
|
256
|
+
#stampMark(el) { el.dataset.menuItemStamped = '1'; return el; }
|
|
257
|
+
#wasStamped(el) { return el?.dataset?.menuItemStamped === '1'; }
|
|
258
|
+
|
|
259
|
+
#ownChild(selector) {
|
|
260
|
+
for (const ch of this.children) {
|
|
261
|
+
if (ch.matches(selector)) return ch;
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
254
266
|
#stamp() {
|
|
255
|
-
if (this
|
|
267
|
+
if (this.#ownChild('[slot="text"]')) return;
|
|
256
268
|
|
|
257
|
-
if (this.icon) {
|
|
258
|
-
const iconEl = document.createElement('icon-ui');
|
|
269
|
+
if (this.icon && !this.#ownChild('[slot="icon"]')) {
|
|
270
|
+
const iconEl = this.#stampMark(document.createElement('icon-ui'));
|
|
259
271
|
iconEl.setAttribute('slot', 'icon');
|
|
260
272
|
iconEl.setAttribute('name', this.icon);
|
|
261
273
|
this.appendChild(iconEl);
|
|
262
274
|
}
|
|
263
275
|
|
|
264
276
|
if (this.text) {
|
|
265
|
-
const span = document.createElement('span');
|
|
277
|
+
const span = this.#stampMark(document.createElement('span'));
|
|
266
278
|
span.setAttribute('slot', 'text');
|
|
267
279
|
span.textContent = this.text;
|
|
268
280
|
this.appendChild(span);
|
|
@@ -275,22 +287,24 @@ class AdiaMenuItem extends AdiaElement {
|
|
|
275
287
|
}
|
|
276
288
|
|
|
277
289
|
render() {
|
|
278
|
-
|
|
290
|
+
// Sync icon — only touch elements we stamped.
|
|
291
|
+
const iconEl = this.#ownChild('[slot="icon"]');
|
|
279
292
|
if (this.icon) {
|
|
280
293
|
if (iconEl) {
|
|
281
|
-
iconEl.setAttribute('name', this.icon);
|
|
294
|
+
if (this.#wasStamped(iconEl)) iconEl.setAttribute('name', this.icon);
|
|
282
295
|
} else {
|
|
283
|
-
const el = document.createElement('icon-ui');
|
|
296
|
+
const el = this.#stampMark(document.createElement('icon-ui'));
|
|
284
297
|
el.setAttribute('slot', 'icon');
|
|
285
298
|
el.setAttribute('name', this.icon);
|
|
286
299
|
this.prepend(el);
|
|
287
300
|
}
|
|
288
|
-
} else if (iconEl) {
|
|
301
|
+
} else if (this.#wasStamped(iconEl)) {
|
|
289
302
|
iconEl.remove();
|
|
290
303
|
}
|
|
291
304
|
|
|
292
|
-
|
|
293
|
-
|
|
305
|
+
// Sync text — only touch elements we stamped.
|
|
306
|
+
const textEl = this.#ownChild('[slot="text"]');
|
|
307
|
+
if (this.#wasStamped(textEl)) textEl.textContent = this.text;
|
|
294
308
|
|
|
295
309
|
this.#syncAria();
|
|
296
310
|
}
|
package/components/pane/pane.css
CHANGED
|
@@ -91,6 +91,11 @@
|
|
|
91
91
|
background: var(--pane-header-bg-hover);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
> header:focus-visible {
|
|
95
|
+
outline: none;
|
|
96
|
+
box-shadow: var(--a-focus-ring) inset;
|
|
97
|
+
}
|
|
98
|
+
|
|
94
99
|
/* Collapse indicator — stamped by JS as icon-ui */
|
|
95
100
|
> header > [slot="chevron"] {
|
|
96
101
|
--a-icon-size: var(--a-caret-size);
|
|
@@ -130,7 +130,21 @@
|
|
|
130
130
|
font-size: var(--pipeline-status-history-size);
|
|
131
131
|
color: var(--pipeline-status-history-fg);
|
|
132
132
|
user-select: none;
|
|
133
|
-
padding: var(--pipeline-status-history-pad-y)
|
|
133
|
+
padding: var(--pipeline-status-history-pad-y) var(--a-space-1);
|
|
134
|
+
margin-inline: calc(var(--a-space-1) * -1);
|
|
135
|
+
border-radius: var(--a-radius-sm);
|
|
136
|
+
transition: background var(--pipeline-status-duration) var(--pipeline-status-easing),
|
|
137
|
+
color var(--pipeline-status-duration) var(--pipeline-status-easing);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
[data-pipeline-history] summary:hover {
|
|
141
|
+
background: var(--a-bg-subtle);
|
|
142
|
+
color: var(--pipeline-status-label-fg);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
[data-pipeline-history] summary:focus-visible {
|
|
146
|
+
outline: none;
|
|
147
|
+
box-shadow: var(--a-focus-ring);
|
|
134
148
|
}
|
|
135
149
|
|
|
136
150
|
[data-pipeline-history] summary::marker {
|
|
@@ -47,6 +47,23 @@
|
|
|
47
47
|
color: var(--popover-fg);
|
|
48
48
|
max-height: calc(100vh - 3rem);
|
|
49
49
|
overflow-y: auto;
|
|
50
|
+
/* Fade + lift in on first paint. @starting-style is the initial frame
|
|
51
|
+
browsers paint before the popover transitions to its open state. */
|
|
52
|
+
opacity: 1;
|
|
53
|
+
translate: 0 0;
|
|
54
|
+
transition: opacity var(--a-duration-fast) var(--a-easing-out),
|
|
55
|
+
translate var(--a-duration-fast) var(--a-easing-out);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
[slot="content"]:popover-open {
|
|
59
|
+
@starting-style {
|
|
60
|
+
opacity: 0;
|
|
61
|
+
translate: 0 -4px;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@media (prefers-reduced-motion: reduce) {
|
|
66
|
+
[slot="content"] { transition: none; }
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
/* Collapse default margins on the first/last block child so the
|
|
@@ -199,6 +199,24 @@ select-ui [slot="listbox"] {
|
|
|
199
199
|
|
|
200
200
|
/* Positioned by JS (#positionListbox) — fixed to viewport */
|
|
201
201
|
width: max-content;
|
|
202
|
+
|
|
203
|
+
/* Fade + lift in on first paint (popover top-layer cannot inherit
|
|
204
|
+
component tokens, so reference --a-* directly). */
|
|
205
|
+
opacity: 1;
|
|
206
|
+
translate: 0 0;
|
|
207
|
+
transition: opacity var(--a-duration-fast) var(--a-easing-out),
|
|
208
|
+
translate var(--a-duration-fast) var(--a-easing-out);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
select-ui [slot="listbox"]:popover-open {
|
|
212
|
+
@starting-style {
|
|
213
|
+
opacity: 0;
|
|
214
|
+
translate: 0 -4px;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@media (prefers-reduced-motion: reduce) {
|
|
219
|
+
select-ui [slot="listbox"] { transition: none; }
|
|
202
220
|
}
|
|
203
221
|
|
|
204
222
|
select-ui [role="option"] {
|