@adia-ai/web-components 0.0.28 → 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.
- package/README.md +4 -8
- package/a2ui/index.js +1 -1
- package/components/canvas/canvas.js +1 -1
- package/components/feed/feed.css +9 -0
- package/components/feed/feed.js +118 -9
- package/components/toast/toast.js +48 -178
- package/index.css +3 -2
- package/index.js +15 -7
- package/package.json +1 -5
- package/patterns/a2ui-root/a2ui-root.a2ui.json +0 -125
- package/patterns/a2ui-root/a2ui-root.js +0 -191
- package/patterns/a2ui-root/a2ui-root.yaml +0 -87
- package/patterns/adia-chat/adia-chat.a2ui.json +0 -149
- package/patterns/adia-chat/adia-chat.css +0 -10
- package/patterns/adia-chat/adia-chat.js +0 -297
- package/patterns/adia-chat/adia-chat.yaml +0 -118
- package/patterns/adia-chat/css/adia-chat.empty.css +0 -12
- package/patterns/adia-chat/css/adia-chat.layout.css +0 -60
- package/patterns/adia-chat/css/adia-chat.markdown.css +0 -74
- package/patterns/adia-chat/css/adia-chat.messages.css +0 -87
- package/patterns/adia-chat/css/adia-chat.streaming.css +0 -30
- package/patterns/adia-chat/css/adia-chat.tokens.css +0 -95
- package/patterns/adia-editor/adia-editor.a2ui.json +0 -73
- package/patterns/adia-editor/adia-editor.css +0 -6
- package/patterns/adia-editor/adia-editor.js +0 -56
- package/patterns/adia-editor/adia-editor.yaml +0 -59
- package/patterns/adia-editor/css/adia-editor.layout.css +0 -171
- package/patterns/adia-editor/css/adia-editor.tokens.css +0 -28
- package/patterns/app-nav/app-nav.a2ui.json +0 -89
- package/patterns/app-nav/app-nav.css +0 -92
- package/patterns/app-nav/app-nav.js +0 -112
- package/patterns/app-nav/app-nav.yaml +0 -54
- package/patterns/app-nav-group/app-nav-group.a2ui.json +0 -82
- package/patterns/app-nav-group/app-nav-group.css +0 -264
- package/patterns/app-nav-group/app-nav-group.js +0 -116
- package/patterns/app-nav-group/app-nav-group.yaml +0 -59
- package/patterns/app-nav-item/app-nav-item.a2ui.json +0 -83
- package/patterns/app-nav-item/app-nav-item.css +0 -162
- package/patterns/app-nav-item/app-nav-item.js +0 -42
- package/patterns/app-nav-item/app-nav-item.yaml +0 -62
- package/patterns/app-shell/app-shell.a2ui.json +0 -129
- package/patterns/app-shell/app-shell.css +0 -14
- package/patterns/app-shell/app-shell.js +0 -251
- package/patterns/app-shell/app-shell.yaml +0 -89
- package/patterns/app-shell/css/app-shell.collapsed.css +0 -86
- package/patterns/app-shell/css/app-shell.helpers.css +0 -42
- package/patterns/app-shell/css/app-shell.main.css +0 -172
- package/patterns/app-shell/css/app-shell.shell.css +0 -44
- package/patterns/app-shell/css/app-shell.sidebar.css +0 -161
- package/patterns/app-shell/css/app-shell.templates.css +0 -214
- package/patterns/app-shell/css/app-shell.tokens.css +0 -119
- package/patterns/gen-ui/gen-ui.a2ui.json +0 -72
- package/patterns/gen-ui/gen-ui.css +0 -83
- package/patterns/gen-ui/gen-ui.js +0 -136
- package/patterns/gen-ui/gen-ui.yaml +0 -43
- package/patterns/index.js +0 -11
- package/patterns/section-nav/section-nav.a2ui.json +0 -91
- package/patterns/section-nav/section-nav.css +0 -60
- package/patterns/section-nav/section-nav.js +0 -42
- package/patterns/section-nav/section-nav.yaml +0 -58
- package/patterns/section-nav-group/section-nav-group.a2ui.json +0 -95
- package/patterns/section-nav-group/section-nav-group.css +0 -74
- package/patterns/section-nav-group/section-nav-group.js +0 -84
- package/patterns/section-nav-group/section-nav-group.yaml +0 -66
- package/patterns/section-nav-item/section-nav-item.a2ui.json +0 -97
- package/patterns/section-nav-item/section-nav-item.css +0 -106
- package/patterns/section-nav-item/section-nav-item.js +0 -66
- package/patterns/section-nav-item/section-nav-item.yaml +0 -70
- 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
|
-
|
|
57
|
-
│
|
|
58
|
-
│
|
|
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-
|
|
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) {
|
package/components/feed/feed.css
CHANGED
|
@@ -131,6 +131,15 @@ feed-item-ui[data-closing] {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/* Phase 2 max-queue: items above [max] are appended with [data-queued]
|
|
135
|
+
and stay invisible until a visible sibling dismisses (at which point
|
|
136
|
+
`releaseContainerIfEmpty` strips the attribute). Display:none keeps
|
|
137
|
+
them out of the layout AND screen-reader-invisible — items not yet
|
|
138
|
+
shown shouldn't be announced. */
|
|
139
|
+
feed-item-ui[data-queued] {
|
|
140
|
+
display: none;
|
|
141
|
+
}
|
|
142
|
+
|
|
134
143
|
/* Reduced-motion: skip slide-in/out, keep opacity. */
|
|
135
144
|
@media (prefers-reduced-motion: reduce) {
|
|
136
145
|
feed-item-ui[data-open],
|
package/components/feed/feed.js
CHANGED
|
@@ -59,6 +59,11 @@ class AdiaFeedItem extends AdiaElement {
|
|
|
59
59
|
variant: { type: String, default: 'default', reflect: true },
|
|
60
60
|
duration: { type: Number, default: 4000, reflect: true },
|
|
61
61
|
dismissible: { type: Boolean, default: false, reflect: true },
|
|
62
|
+
// Phase 2: action-required policy. When set, the item carries an
|
|
63
|
+
// action button that the user MUST click (or dismiss) before the
|
|
64
|
+
// item goes away. Spec §2.3 — action-required role=alertdialog,
|
|
65
|
+
// focus is trapped until choice is made.
|
|
66
|
+
action: { type: String, default: '', reflect: true },
|
|
62
67
|
};
|
|
63
68
|
|
|
64
69
|
static parts = {
|
|
@@ -67,9 +72,21 @@ class AdiaFeedItem extends AdiaElement {
|
|
|
67
72
|
static template = () => html``;
|
|
68
73
|
|
|
69
74
|
#onPress = (e) => {
|
|
70
|
-
if (e.target.closest('[data-feed-close]')) this.dismiss();
|
|
75
|
+
if (e.target.closest('[data-feed-close]')) { this.dismiss(); return; }
|
|
76
|
+
// Phase 2: action button — fires the action callback (if registered
|
|
77
|
+
// via FeedHandle.update with onAction) then dismisses. The handle's
|
|
78
|
+
// .update({onAction: fn}) populates this.#onAction below.
|
|
79
|
+
if (e.target.closest('[data-feed-action]')) {
|
|
80
|
+
try { this.#onAction?.(); } catch (err) { console.warn('[feed-item] action handler threw:', err); }
|
|
81
|
+
this.dismiss();
|
|
82
|
+
}
|
|
71
83
|
};
|
|
72
84
|
|
|
85
|
+
// Phase 2 focus-trap state — only used when the policy is action-required.
|
|
86
|
+
#onAction = null;
|
|
87
|
+
#focusTrapPrev = null;
|
|
88
|
+
#onTrapKeydown = null;
|
|
89
|
+
|
|
73
90
|
connected() {
|
|
74
91
|
this.addEventListener('press', this.#onPress);
|
|
75
92
|
}
|
|
@@ -95,17 +112,22 @@ class AdiaFeedItem extends AdiaElement {
|
|
|
95
112
|
}
|
|
96
113
|
|
|
97
114
|
/* Inferred policy roles per spec §2.4. Auto-fade gets `status`;
|
|
98
|
-
sticky danger/warning gets `alert`; action-required
|
|
99
|
-
`alertdialog`
|
|
115
|
+
sticky danger/warning gets `alert`; action-required (sticky +
|
|
116
|
+
has [action]) gets `alertdialog` and a focus trap. */
|
|
100
117
|
const isSticky = !this.duration || this.duration <= 0;
|
|
101
118
|
const isLoud = this.variant === 'danger' || this.variant === 'warning';
|
|
119
|
+
const isActionRequired = isSticky && !!this.action;
|
|
102
120
|
let role = 'status';
|
|
103
|
-
if (
|
|
121
|
+
if (isActionRequired) role = 'alertdialog';
|
|
122
|
+
else if (isSticky && isLoud) role = 'alert';
|
|
104
123
|
this.setAttribute('role', role);
|
|
105
|
-
this.setAttribute('aria-live', role === 'alert' ? 'assertive' : 'polite');
|
|
124
|
+
this.setAttribute('aria-live', role === 'alert' || role === 'alertdialog' ? 'assertive' : 'polite');
|
|
125
|
+
if (isActionRequired) this.setAttribute('aria-modal', 'false'); // not page-blocking; focus-trap only
|
|
106
126
|
|
|
107
127
|
/* Render dismiss button for sticky items (spec §2.2 — default true
|
|
108
|
-
for sticky, false for auto-fade).
|
|
128
|
+
for sticky, false for auto-fade). Action-required items still
|
|
129
|
+
allow Escape to dismiss for safety; the action button is not the
|
|
130
|
+
only escape. */
|
|
109
131
|
const wantsClose = this.dismissible || isSticky;
|
|
110
132
|
let close = this.querySelector(':scope > [data-feed-close]');
|
|
111
133
|
if (wantsClose && !close) {
|
|
@@ -120,6 +142,27 @@ class AdiaFeedItem extends AdiaElement {
|
|
|
120
142
|
close.remove();
|
|
121
143
|
}
|
|
122
144
|
|
|
145
|
+
/* Phase 2 — render the action button when [action] is set. Sits
|
|
146
|
+
between the body and the close button. */
|
|
147
|
+
let actionBtn = this.querySelector(':scope > [data-feed-action]');
|
|
148
|
+
if (this.action) {
|
|
149
|
+
if (!actionBtn) {
|
|
150
|
+
actionBtn = document.createElement('button-ui');
|
|
151
|
+
actionBtn.setAttribute('data-feed-action', '');
|
|
152
|
+
actionBtn.setAttribute('variant', 'ghost');
|
|
153
|
+
actionBtn.setAttribute('size', 'sm');
|
|
154
|
+
if (close && close.parentElement === this) this.insertBefore(actionBtn, close);
|
|
155
|
+
else this.appendChild(actionBtn);
|
|
156
|
+
}
|
|
157
|
+
actionBtn.setAttribute('text', this.action);
|
|
158
|
+
} else if (actionBtn) {
|
|
159
|
+
actionBtn.remove();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Focus-trap activation for action-required. */
|
|
163
|
+
if (isActionRequired) this.#installFocusTrap();
|
|
164
|
+
else this.#uninstallFocusTrap();
|
|
165
|
+
|
|
123
166
|
if (!this.hasAttribute('data-open') && !this.#removing) {
|
|
124
167
|
this.#openRaf = requestAnimationFrame(() => {
|
|
125
168
|
this.#openRaf = null;
|
|
@@ -156,14 +199,55 @@ class AdiaFeedItem extends AdiaElement {
|
|
|
156
199
|
for (const k of Object.keys(patch)) {
|
|
157
200
|
if (k in AdiaFeedItem.properties) this[k] = patch[k];
|
|
158
201
|
}
|
|
202
|
+
// Phase 2: handle is set via the FeedHandle's update(); not a
|
|
203
|
+
// reflected attribute, so it lives outside the property loop.
|
|
204
|
+
if (typeof patch.onAction === 'function') this.#onAction = patch.onAction;
|
|
159
205
|
this.#scheduleAutoDismiss();
|
|
160
206
|
}
|
|
161
207
|
|
|
208
|
+
// Phase 2 focus-trap. Installs on action-required items; saves the
|
|
209
|
+
// previously-focused element so we can return focus on dismiss; cycles
|
|
210
|
+
// Tab/Shift-Tab between focusable descendants; Escape dismisses.
|
|
211
|
+
#installFocusTrap() {
|
|
212
|
+
if (this.#onTrapKeydown) return; // already installed
|
|
213
|
+
this.#focusTrapPrev = document.activeElement;
|
|
214
|
+
const focusables = () => [...this.querySelectorAll('button, button-ui, [tabindex]:not([tabindex="-1"]), input, [data-feed-action], [data-feed-close]')]
|
|
215
|
+
.filter((el) => !el.hasAttribute('disabled') && el.offsetParent !== null);
|
|
216
|
+
this.#onTrapKeydown = (e) => {
|
|
217
|
+
if (e.key === 'Escape') { e.stopPropagation(); this.dismiss(); return; }
|
|
218
|
+
if (e.key !== 'Tab') return;
|
|
219
|
+
const els = focusables();
|
|
220
|
+
if (!els.length) return;
|
|
221
|
+
const first = els[0];
|
|
222
|
+
const last = els[els.length - 1];
|
|
223
|
+
const active = document.activeElement;
|
|
224
|
+
if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
|
|
225
|
+
else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
|
|
226
|
+
};
|
|
227
|
+
this.addEventListener('keydown', this.#onTrapKeydown, true);
|
|
228
|
+
// Move focus into the item — prefer the action button.
|
|
229
|
+
queueMicrotask(() => {
|
|
230
|
+
const els = focusables();
|
|
231
|
+
const action = this.querySelector('[data-feed-action]');
|
|
232
|
+
(action || els[0])?.focus?.();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#uninstallFocusTrap() {
|
|
237
|
+
if (!this.#onTrapKeydown) return;
|
|
238
|
+
this.removeEventListener('keydown', this.#onTrapKeydown, true);
|
|
239
|
+
this.#onTrapKeydown = null;
|
|
240
|
+
// Restore focus to whatever had it before the trap engaged.
|
|
241
|
+
try { this.#focusTrapPrev?.focus?.(); } catch { /* element may have been removed */ }
|
|
242
|
+
this.#focusTrapPrev = null;
|
|
243
|
+
}
|
|
244
|
+
|
|
162
245
|
disconnected() {
|
|
163
246
|
this.removeEventListener('press', this.#onPress);
|
|
164
247
|
if (this.#timer) { clearTimeout(this.#timer); this.#timer = null; }
|
|
165
248
|
if (this.#closeTimer) { clearTimeout(this.#closeTimer); this.#closeTimer = null; }
|
|
166
249
|
if (this.#openRaf != null) { cancelAnimationFrame(this.#openRaf); this.#openRaf = null; }
|
|
250
|
+
this.#uninstallFocusTrap();
|
|
167
251
|
}
|
|
168
252
|
}
|
|
169
253
|
|
|
@@ -205,6 +289,12 @@ class AdiaFeed {
|
|
|
205
289
|
* @param {string} [opts.position='bottom-right']
|
|
206
290
|
* @param {boolean} [opts.dismissible] override default (true for sticky, false for auto)
|
|
207
291
|
* @param {string} [opts.id]
|
|
292
|
+
* @param {string} [opts.action] Phase 2 — action button label. When set,
|
|
293
|
+
* duration is forced to null (sticky); the
|
|
294
|
+
* item gets role=alertdialog + focus trap.
|
|
295
|
+
* @param {function} [opts.onAction] Phase 2 — callback invoked when the
|
|
296
|
+
* action button is pressed. Item dismisses
|
|
297
|
+
* after the callback returns.
|
|
208
298
|
*/
|
|
209
299
|
static post(opts = {}) {
|
|
210
300
|
const {
|
|
@@ -216,18 +306,30 @@ class AdiaFeed {
|
|
|
216
306
|
position = 'bottom-right',
|
|
217
307
|
dismissible,
|
|
218
308
|
id,
|
|
309
|
+
action = '',
|
|
310
|
+
onAction,
|
|
219
311
|
} = opts;
|
|
220
312
|
const v = variant === 'error' ? 'danger' : variant; // documented alias
|
|
221
313
|
const container = AdiaFeed.get(position);
|
|
314
|
+
// Phase 2 max-queue: items above [max] queue with [data-queued] and
|
|
315
|
+
// become visible as visible items dismiss.
|
|
316
|
+
const max = parseInt(container.getAttribute('max') || '5', 10);
|
|
317
|
+
const visibleCount = [...container.children].filter((c) =>
|
|
318
|
+
c.tagName === 'FEED-ITEM-UI' && !c.hasAttribute('data-queued')
|
|
319
|
+
).length;
|
|
222
320
|
const item = document.createElement('feed-item-ui');
|
|
223
321
|
if (id) item.setAttribute('data-id', id);
|
|
224
322
|
item.text = text;
|
|
225
323
|
item.heading = heading;
|
|
226
324
|
item.icon = icon;
|
|
227
325
|
item.variant = v;
|
|
228
|
-
|
|
326
|
+
// Action-required forces sticky.
|
|
327
|
+
item.duration = action ? 0 : duration;
|
|
229
328
|
if (dismissible != null) item.dismissible = !!dismissible;
|
|
329
|
+
if (action) item.action = action;
|
|
330
|
+
if (visibleCount >= max) item.setAttribute('data-queued', '');
|
|
230
331
|
container.appendChild(item);
|
|
332
|
+
if (typeof onAction === 'function') item.update({ onAction });
|
|
231
333
|
return {
|
|
232
334
|
id: id ?? null,
|
|
233
335
|
dismiss: () => item.dismiss(),
|
|
@@ -253,9 +355,16 @@ class AdiaFeed {
|
|
|
253
355
|
AdiaFeed.#containers.clear();
|
|
254
356
|
}
|
|
255
357
|
|
|
256
|
-
/** Internal: drop a lane when its last item exits.
|
|
358
|
+
/** Internal: drop a lane when its last item exits. Also rotates the
|
|
359
|
+
* Phase 2 max-queue: when a visible item leaves, promote the oldest
|
|
360
|
+
* queued sibling so the visible-count returns to [max]. */
|
|
257
361
|
static releaseContainerIfEmpty(container) {
|
|
258
|
-
if (!container
|
|
362
|
+
if (!container) return;
|
|
363
|
+
// Promote the oldest queued sibling (if any) before deciding whether
|
|
364
|
+
// to release. The queue is FIFO; queued items live at the tail.
|
|
365
|
+
const queued = container.querySelector('feed-item-ui[data-queued]');
|
|
366
|
+
if (queued) queued.removeAttribute('data-queued');
|
|
367
|
+
if (container.children.length > 0) return;
|
|
259
368
|
try { container.hidePopover?.(); } catch { /* noop */ }
|
|
260
369
|
container.remove();
|
|
261
370
|
for (const [pos, el] of AdiaFeed.#containers) {
|
|
@@ -1,39 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* <toast-ui> —
|
|
3
|
-
* messaging channel.
|
|
2
|
+
* <toast-ui> — Thin facade over `<feed-ui>` / `AdiaFeed.post()`.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Phase 4 of `docs/specs/feed-channel.md` (SPEC-FEED-CHANNEL-001) —
|
|
5
|
+
* toast-ui no longer owns its own per-position container. Both
|
|
6
|
+
* declarative `<toast-ui>` and imperative `AdiaToast.show()` paths
|
|
7
|
+
* route through `AdiaFeed`. The element exists for back-compat:
|
|
8
|
+
* authoring `<toast-ui text="…">` still produces a feed item.
|
|
9
9
|
*
|
|
10
|
-
* // Imperative API
|
|
10
|
+
* // Imperative API (delegates to AdiaFeed.post)
|
|
11
11
|
* AdiaToast.show({ text: 'Saved!', variant: 'success' });
|
|
12
12
|
*
|
|
13
|
-
* // Global event channel — same shape,
|
|
14
|
-
* // Any code (other components, integration scripts) can post
|
|
15
|
-
* // without importing AdiaToast.
|
|
13
|
+
* // Global event channel — same shape, same delegation.
|
|
16
14
|
* window.dispatchEvent(new CustomEvent('toast', {
|
|
17
15
|
* detail: { text: 'Saved!', variant: 'success' }
|
|
18
16
|
* }));
|
|
19
17
|
*
|
|
20
|
-
* // Declarative — auto-
|
|
21
|
-
* // connect. No need to author <toast-ui> inside the container.
|
|
18
|
+
* // Declarative — auto-posts and removes self on connect.
|
|
22
19
|
* <toast-ui text="Saved!" variant="success"></toast-ui>
|
|
23
20
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* The legacy `[data-toast-container]` per-position-Map plumbing was
|
|
22
|
+
* retired in this migration; the lane infrastructure now lives in
|
|
23
|
+
* `<feed-ui>` (see ../feed/feed.js).
|
|
26
24
|
*/
|
|
27
25
|
|
|
28
26
|
import { AdiaElement, html } from '../../core/element.js';
|
|
27
|
+
import { AdiaFeed } from '../feed/feed.js';
|
|
29
28
|
|
|
30
29
|
class AdiaToast extends AdiaElement {
|
|
31
|
-
#timer = null;
|
|
32
|
-
#removing = false;
|
|
33
|
-
#closeTimer = null;
|
|
34
|
-
#openRaf = null;
|
|
35
|
-
#routed = false;
|
|
36
|
-
|
|
37
30
|
static properties = {
|
|
38
31
|
text: { type: String, default: '', reflect: true },
|
|
39
32
|
variant: { type: String, default: 'info', reflect: true },
|
|
@@ -41,178 +34,55 @@ class AdiaToast extends AdiaElement {
|
|
|
41
34
|
position: { type: String, default: 'bottom-right', reflect: true },
|
|
42
35
|
};
|
|
43
36
|
|
|
44
|
-
static parts = {
|
|
45
|
-
message: '<div slot="message"></div>',
|
|
46
|
-
close: '<button-ui slot="close" icon="x" variant="ghost" size="sm" aria-label="Dismiss"></button-ui>',
|
|
47
|
-
};
|
|
48
|
-
|
|
49
37
|
static template = () => html``;
|
|
50
38
|
|
|
51
|
-
#onPress = (e) => {
|
|
52
|
-
if (e.target.closest('[slot="close"]')) this.dismiss();
|
|
53
|
-
};
|
|
54
|
-
|
|
55
39
|
connected() {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return parseFloat(raw) || 200;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
render() {
|
|
75
|
-
const message = this.ensure('message');
|
|
76
|
-
message.textContent = this.text;
|
|
77
|
-
|
|
78
|
-
const close = this.ensure('close');
|
|
79
|
-
if (close.parentElement !== this) this.appendChild(close);
|
|
80
|
-
|
|
81
|
-
this.setAttribute('role', 'status');
|
|
82
|
-
this.setAttribute('aria-live', 'polite');
|
|
83
|
-
|
|
84
|
-
// Schedule auto-open animation
|
|
85
|
-
if (!this.hasAttribute('data-open') && !this.#removing) {
|
|
86
|
-
this.#openRaf = requestAnimationFrame(() => {
|
|
87
|
-
this.#openRaf = null;
|
|
88
|
-
this.setAttribute('data-open', '');
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Schedule auto-dismiss
|
|
93
|
-
this.#scheduleAutoDismiss();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
#scheduleAutoDismiss() {
|
|
97
|
-
if (this.#timer) clearTimeout(this.#timer);
|
|
98
|
-
if (this.duration > 0) {
|
|
99
|
-
this.#timer = setTimeout(() => this.dismiss(), this.duration);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
dismiss() {
|
|
104
|
-
if (this.#removing) return;
|
|
105
|
-
this.#removing = true;
|
|
106
|
-
if (this.#timer) { clearTimeout(this.#timer); this.#timer = null; }
|
|
107
|
-
|
|
108
|
-
this.removeAttribute('data-open');
|
|
109
|
-
this.setAttribute('data-closing', '');
|
|
110
|
-
|
|
111
|
-
this.#closeTimer = setTimeout(() => {
|
|
112
|
-
this.#closeTimer = null;
|
|
113
|
-
this.removeAttribute('data-closing');
|
|
114
|
-
const container = this.parentElement;
|
|
115
|
-
this.dispatchEvent(new Event('close', { bubbles: true }));
|
|
116
|
-
this.remove();
|
|
117
|
-
/* If the lane is now empty, hide its popover and remove the
|
|
118
|
-
container — keeps document.body free of leaked containers. */
|
|
119
|
-
if (container?.matches?.('[data-toast-container]')) {
|
|
120
|
-
AdiaToast.#releaseContainerIfEmpty(container);
|
|
121
|
-
}
|
|
122
|
-
}, this.#getDuration());
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
disconnected() {
|
|
126
|
-
this.removeEventListener('press', this.#onPress);
|
|
127
|
-
if (this.#timer) { clearTimeout(this.#timer); this.#timer = null; }
|
|
128
|
-
if (this.#closeTimer) { clearTimeout(this.#closeTimer); this.#closeTimer = null; }
|
|
129
|
-
if (this.#openRaf != null) { cancelAnimationFrame(this.#openRaf); this.#openRaf = null; }
|
|
40
|
+
/* Declarative path: post into AdiaFeed and remove self. The
|
|
41
|
+
element is fire-and-forget — its only job is to forward the
|
|
42
|
+
authored attributes to the feed, then dissolve. Re-entrant
|
|
43
|
+
guard keeps this safe under HMR / dev-mode re-attachment. */
|
|
44
|
+
if (this.__routedToFeed) return;
|
|
45
|
+
this.__routedToFeed = true;
|
|
46
|
+
AdiaFeed.post({
|
|
47
|
+
text: this.text,
|
|
48
|
+
variant: this.variant === 'error' ? 'danger' : this.variant,
|
|
49
|
+
duration: this.duration,
|
|
50
|
+
position: this.position,
|
|
51
|
+
});
|
|
52
|
+
/* Schedule removal so the calling code can read the attributes
|
|
53
|
+
if it inspected this element on the same microtask. */
|
|
54
|
+
queueMicrotask(() => { try { this.remove(); } catch { /* noop */ } });
|
|
130
55
|
}
|
|
131
56
|
|
|
132
57
|
/**
|
|
133
|
-
* Static
|
|
58
|
+
* Static facade — delegates to AdiaFeed.post(). Kept for back-compat
|
|
59
|
+
* with consumer code shaped like `customElements.get('toast-ui').show(...)`
|
|
60
|
+
* or `AdiaToast.show(...)`. New code should call AdiaFeed.post()
|
|
61
|
+
* directly.
|
|
62
|
+
*
|
|
134
63
|
* @param {Object} opts
|
|
135
64
|
* @param {string} opts.text
|
|
136
|
-
* @param {string} [opts.variant='info']
|
|
65
|
+
* @param {string} [opts.variant='info'] — `error` aliases to `danger`.
|
|
137
66
|
* @param {number} [opts.duration=4000]
|
|
138
67
|
* @param {string} [opts.position='bottom-right']
|
|
139
|
-
* @returns {
|
|
140
|
-
*/
|
|
141
|
-
static #containers = new Map();
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Get (or lazily create) the per-position lane. Each lane is a manual
|
|
145
|
-
* Popover-API container — `[popover="manual"]` puts it in the browser's
|
|
146
|
-
* top-layer, above ALL page content with no z-index wars, and the
|
|
147
|
-
* native popover stack lets multiple lanes coexist (e.g. one toast in
|
|
148
|
-
* top-right + another in bottom-center) without collision.
|
|
149
|
-
*
|
|
150
|
-
* `popover="manual"` (not `auto`) — toasts must NOT light-dismiss on
|
|
151
|
-
* outside click; they auto-fade by their own duration timer.
|
|
152
|
-
*
|
|
153
|
-
* Falls back gracefully if the Popover API is unsupported (Safari < 17 /
|
|
154
|
-
* Firefox < 125): the lane still renders as a `position: fixed` div via
|
|
155
|
-
* the CSS `[data-toast-container]` rules. Browser baseline (Chromium
|
|
156
|
-
* 125+, Safari 18.0+, Firefox 129+) all support `[popover]`.
|
|
68
|
+
* @returns {{id:string|null, dismiss:function, update:function}} FeedHandle.
|
|
157
69
|
*/
|
|
158
|
-
static
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
el.setAttribute('popover', 'manual');
|
|
167
|
-
}
|
|
168
|
-
document.body.appendChild(el);
|
|
169
|
-
/* Show the popover so the lane lifts into the top-layer. Wrapped in
|
|
170
|
-
try/catch because some test rigs (happy-dom) don't ship the API. */
|
|
171
|
-
try { el.showPopover?.(); } catch { /* graceful fallback to fixed */ }
|
|
172
|
-
AdiaToast.#containers.set(position, el);
|
|
173
|
-
return el;
|
|
70
|
+
static show(opts = {}) {
|
|
71
|
+
const { text, variant = 'info', duration = 4000, position = 'bottom-right' } = opts;
|
|
72
|
+
return AdiaFeed.post({
|
|
73
|
+
text,
|
|
74
|
+
variant: variant === 'error' ? 'danger' : variant,
|
|
75
|
+
duration,
|
|
76
|
+
position,
|
|
77
|
+
});
|
|
174
78
|
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Tear down the per-position lane when its last toast has been
|
|
178
|
-
* dismissed. Keeps `document.body` clean — addresses the audit's L-B4
|
|
179
|
-
* "container leak" finding (toasts permanently leaving DIVs in body).
|
|
180
|
-
*/
|
|
181
|
-
static #releaseContainerIfEmpty(container) {
|
|
182
|
-
if (!container) return;
|
|
183
|
-
if (container.children.length > 0) return;
|
|
184
|
-
try { container.hidePopover?.(); } catch { /* noop */ }
|
|
185
|
-
container.remove();
|
|
186
|
-
/* Clear from the singleton map so the next post() rebuilds. */
|
|
187
|
-
for (const [pos, el] of AdiaToast.#containers) {
|
|
188
|
-
if (el === container) AdiaToast.#containers.delete(pos);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
static show({ text, variant = 'info', duration = 4000, position = 'bottom-right' } = {}) {
|
|
193
|
-
/* `error` is a documented alias of `danger` (Phase 6 variant table). */
|
|
194
|
-
if (variant === 'error') variant = 'danger';
|
|
195
|
-
const container = AdiaToast.#getContainer(position);
|
|
196
|
-
const toast = document.createElement('toast-ui');
|
|
197
|
-
toast.text = text;
|
|
198
|
-
toast.variant = variant;
|
|
199
|
-
toast.duration = duration;
|
|
200
|
-
toast.position = position;
|
|
201
|
-
container.appendChild(toast);
|
|
202
|
-
return toast;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
79
|
}
|
|
206
80
|
|
|
207
|
-
/*
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
detail: { text: 'Saved!', variant: 'success' }
|
|
213
|
-
}));
|
|
214
|
-
|
|
215
|
-
Idempotent — guarded by a window flag so HMR / re-imports are safe. */
|
|
81
|
+
/* Idempotent install of the global 'toast' CustomEvent listener.
|
|
82
|
+
Same shape as the historical channel — code that posts via
|
|
83
|
+
`window.dispatchEvent(new CustomEvent('toast', {detail:{…}}))`
|
|
84
|
+
keeps working. The listener delegates to AdiaToast.show(), which
|
|
85
|
+
delegates to AdiaFeed.post(). */
|
|
216
86
|
if (typeof window !== 'undefined' && !window.__adiaToastListenerInstalled) {
|
|
217
87
|
window.__adiaToastListenerInstalled = true;
|
|
218
88
|
window.addEventListener('toast', (e) => {
|
package/index.css
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* + typography — via the colors tree) → component styles → global
|
|
8
8
|
* resets.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Composite-element CSS (adia-chat, adia-editor, app-shell, etc.)
|
|
11
|
+
* lives in the sibling `@adia-ai/web-modules` package as of 0.0.29
|
|
12
|
+
* (see ADR-0012). Each page links its own cluster CSS explicitly.
|
|
12
13
|
* Opinionated theme overrides live in `./styles/themes.css`; link
|
|
13
14
|
* them separately when you want the 8 named themes.
|
|
14
15
|
*
|
package/index.js
CHANGED
|
@@ -2,17 +2,25 @@
|
|
|
2
2
|
* @adia-ai/web-components — main entry.
|
|
3
3
|
*
|
|
4
4
|
* import '@adia-ai/web-components';
|
|
5
|
-
* import { AdiaButton
|
|
5
|
+
* import { AdiaButton } from '@adia-ai/web-components';
|
|
6
6
|
*
|
|
7
|
-
* Loading this file registers every
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Loading this file registers every primitive (side effect of each
|
|
8
|
+
* module's `customElements.define(...)` call). Pair with
|
|
9
|
+
* `@adia-ai/web-components/css` (the all-in-one stylesheet) or link
|
|
10
|
+
* tokens/components/resets individually.
|
|
11
11
|
*
|
|
12
12
|
* If you only want a subset, use the subpath imports:
|
|
13
13
|
* import '@adia-ai/web-components/components/button';
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* Composite elements (app-shell, app-nav, adia-chat, adia-editor,
|
|
16
|
+
* gen-ui, a2ui-root) shipped here as `patterns/` until 0.0.28 — they
|
|
17
|
+
* now live in `@adia-ai/web-modules` (see ADR-0012).
|
|
18
|
+
*
|
|
19
|
+
* import '@adia-ai/web-modules'; // every cluster
|
|
20
|
+
* import '@adia-ai/web-modules/shell'; // app-shell, app-nav*, section-nav*
|
|
21
|
+
* import '@adia-ai/web-modules/chat'; // adia-chat
|
|
22
|
+
* import '@adia-ai/web-modules/editor'; // adia-editor
|
|
23
|
+
* import '@adia-ai/web-modules/runtime'; // gen-ui, a2ui-root
|
|
15
24
|
*/
|
|
16
25
|
|
|
17
26
|
export * from './components/index.js';
|
|
18
|
-
export * from './patterns/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/web-components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
4
4
|
"description": "AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-utils.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
"./core/*": "./core/*.js",
|
|
12
12
|
"./components": "./components/index.js",
|
|
13
13
|
"./components/*": "./components/*/*.js",
|
|
14
|
-
"./patterns": "./patterns/index.js",
|
|
15
|
-
"./patterns/*": "./patterns/*/*.js",
|
|
16
14
|
"./styles/*": "./styles/*",
|
|
17
15
|
"./traits": "./traits/index.js",
|
|
18
16
|
"./traits/*": "./traits/*.js",
|
|
@@ -23,7 +21,6 @@
|
|
|
23
21
|
"components/",
|
|
24
22
|
"styles/",
|
|
25
23
|
"traits/",
|
|
26
|
-
"patterns/",
|
|
27
24
|
"a2ui/",
|
|
28
25
|
"index.js",
|
|
29
26
|
"index.css"
|
|
@@ -32,7 +29,6 @@
|
|
|
32
29
|
"*.css",
|
|
33
30
|
"./index.js",
|
|
34
31
|
"./components/**/*.js",
|
|
35
|
-
"./patterns/**/*.js",
|
|
36
32
|
"./core/provider.js"
|
|
37
33
|
],
|
|
38
34
|
"dependencies": {
|