@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.
Files changed (69) hide show
  1. package/README.md +4 -8
  2. package/a2ui/index.js +1 -1
  3. package/components/canvas/canvas.js +1 -1
  4. package/components/feed/feed.css +9 -0
  5. package/components/feed/feed.js +118 -9
  6. package/components/toast/toast.js +48 -178
  7. package/index.css +3 -2
  8. package/index.js +15 -7
  9. package/package.json +1 -5
  10. package/patterns/a2ui-root/a2ui-root.a2ui.json +0 -125
  11. package/patterns/a2ui-root/a2ui-root.js +0 -191
  12. package/patterns/a2ui-root/a2ui-root.yaml +0 -87
  13. package/patterns/adia-chat/adia-chat.a2ui.json +0 -149
  14. package/patterns/adia-chat/adia-chat.css +0 -10
  15. package/patterns/adia-chat/adia-chat.js +0 -297
  16. package/patterns/adia-chat/adia-chat.yaml +0 -118
  17. package/patterns/adia-chat/css/adia-chat.empty.css +0 -12
  18. package/patterns/adia-chat/css/adia-chat.layout.css +0 -60
  19. package/patterns/adia-chat/css/adia-chat.markdown.css +0 -74
  20. package/patterns/adia-chat/css/adia-chat.messages.css +0 -87
  21. package/patterns/adia-chat/css/adia-chat.streaming.css +0 -30
  22. package/patterns/adia-chat/css/adia-chat.tokens.css +0 -95
  23. package/patterns/adia-editor/adia-editor.a2ui.json +0 -73
  24. package/patterns/adia-editor/adia-editor.css +0 -6
  25. package/patterns/adia-editor/adia-editor.js +0 -56
  26. package/patterns/adia-editor/adia-editor.yaml +0 -59
  27. package/patterns/adia-editor/css/adia-editor.layout.css +0 -171
  28. package/patterns/adia-editor/css/adia-editor.tokens.css +0 -28
  29. package/patterns/app-nav/app-nav.a2ui.json +0 -89
  30. package/patterns/app-nav/app-nav.css +0 -92
  31. package/patterns/app-nav/app-nav.js +0 -112
  32. package/patterns/app-nav/app-nav.yaml +0 -54
  33. package/patterns/app-nav-group/app-nav-group.a2ui.json +0 -82
  34. package/patterns/app-nav-group/app-nav-group.css +0 -264
  35. package/patterns/app-nav-group/app-nav-group.js +0 -116
  36. package/patterns/app-nav-group/app-nav-group.yaml +0 -59
  37. package/patterns/app-nav-item/app-nav-item.a2ui.json +0 -83
  38. package/patterns/app-nav-item/app-nav-item.css +0 -162
  39. package/patterns/app-nav-item/app-nav-item.js +0 -42
  40. package/patterns/app-nav-item/app-nav-item.yaml +0 -62
  41. package/patterns/app-shell/app-shell.a2ui.json +0 -129
  42. package/patterns/app-shell/app-shell.css +0 -14
  43. package/patterns/app-shell/app-shell.js +0 -251
  44. package/patterns/app-shell/app-shell.yaml +0 -89
  45. package/patterns/app-shell/css/app-shell.collapsed.css +0 -86
  46. package/patterns/app-shell/css/app-shell.helpers.css +0 -42
  47. package/patterns/app-shell/css/app-shell.main.css +0 -172
  48. package/patterns/app-shell/css/app-shell.shell.css +0 -44
  49. package/patterns/app-shell/css/app-shell.sidebar.css +0 -161
  50. package/patterns/app-shell/css/app-shell.templates.css +0 -214
  51. package/patterns/app-shell/css/app-shell.tokens.css +0 -119
  52. package/patterns/gen-ui/gen-ui.a2ui.json +0 -72
  53. package/patterns/gen-ui/gen-ui.css +0 -83
  54. package/patterns/gen-ui/gen-ui.js +0 -136
  55. package/patterns/gen-ui/gen-ui.yaml +0 -43
  56. package/patterns/index.js +0 -11
  57. package/patterns/section-nav/section-nav.a2ui.json +0 -91
  58. package/patterns/section-nav/section-nav.css +0 -60
  59. package/patterns/section-nav/section-nav.js +0 -42
  60. package/patterns/section-nav/section-nav.yaml +0 -58
  61. package/patterns/section-nav-group/section-nav-group.a2ui.json +0 -95
  62. package/patterns/section-nav-group/section-nav-group.css +0 -74
  63. package/patterns/section-nav-group/section-nav-group.js +0 -84
  64. package/patterns/section-nav-group/section-nav-group.yaml +0 -66
  65. package/patterns/section-nav-item/section-nav-item.a2ui.json +0 -97
  66. package/patterns/section-nav-item/section-nav-item.css +0 -106
  67. package/patterns/section-nav-item/section-nav-item.js +0 -66
  68. package/patterns/section-nav-item/section-nav-item.yaml +0 -70
  69. package/styles/layouts/admin.css +0 -7
package/README.md CHANGED
@@ -46,20 +46,16 @@ web-components/
46
46
  │ └── data-stream.js `data-stream-*` attribute trait (HTTP/SSE/WS,
47
47
  │ signal-backed, refcounted shared transports)
48
48
 
49
- ├── components/ — 80 *-ui custom elements
49
+ ├── components/ — 80 *-ui custom elements (primitives)
50
50
  │ └── <tag>/
51
51
  │ ├── <tag>.js class definition (extends AdiaElement)
52
52
  │ ├── <tag>.css @scope(tag-ui) two-block: tokens + styles
53
53
  │ ├── <tag>.yaml authoring contract (props, slots, events, examples)
54
54
  │ └── <tag>.a2ui.json generated — do NOT edit
55
55
 
56
- ├── patterns/ — Higher-level compositions
57
- ├── app-shell, app-nav*, section-nav* — admin layout scaffolding
58
- ├── adia-chat, adia-editor, gen-ui LLM + editor + gen-UI patterns
59
- │ ├── a2ui-root — A2UI declarative host (moved
60
- │ │ from a2ui/ in 0.0.4; pairs
61
- │ │ with @adia-ai/a2ui-utils)
62
- │ └── index.js — registers all patterns
56
+ │ Composite elements (app-shell, app-nav*, adia-chat, adia-editor,
57
+ gen-ui, a2ui-root) ship in the sibling `@adia-ai/web-modules`
58
+ package as of 0.0.29 see ADR-0012 for the three-tier rationale.
63
59
 
64
60
  ├── traits/ — 42 composable behaviors via defineTrait()
65
61
  │ (pressable, focusTrap, confetti, resizable, …)
package/a2ui/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * The declarative `<a2ui-root>` custom element also moved — it now lives
13
13
  * in the patterns directory. Import it via:
14
- * import '@adia-ai/web-components/patterns/a2ui-root/a2ui-root.js';
14
+ * import '@adia-ai/web-modules/runtime/a2ui-root/a2ui-root.js';
15
15
  */
16
16
 
17
17
  if (typeof console !== 'undefined' && !globalThis.__a2ui_subpath_warned) {
@@ -1,5 +1,5 @@
1
1
  import { AdiaElement } from '../../core/element.js';
2
- import '../../patterns/a2ui-root/a2ui-root.js';
2
+ import '../../../web-modules/runtime/a2ui-root/a2ui-root.js';
3
3
 
4
4
  /**
5
5
  * <canvas-ui> — A2UI rendering surface.
@@ -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],
@@ -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 gets
99
- `alertdialog` (a future phase wires the focus trap). */
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 (isSticky && isLoud) role = 'alert';
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
- item.duration = duration;
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 || container.children.length > 0) return;
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> — Notification popup wired through a shared top-layer
3
- * messaging channel.
2
+ * <toast-ui> — Thin facade over `<feed-ui>` / `AdiaFeed.post()`.
4
3
  *
5
- * All toasts declarative AND imperative route through one
6
- * lazily-mounted `[data-toast-container][data-toast-position]` per
7
- * position. Per-position singletons; consumers can post from anywhere
8
- * without holding a reference to the toast component:
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, dispatched on `window`.
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-routes to the per-position container on
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
- * Events:
25
- * close fired after the toast finishes its exit animation
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
- this.addEventListener('press', this.#onPress);
57
-
58
- // Route declarative <toast-ui> instances into the shared per-position
59
- // container so authored toasts share the same lane as imperatively-
60
- // posted ones (no overlap, consistent stacking). Skipped if we're
61
- // already inside a container (re-entrant connect after reparent).
62
- if (!this.#routed && !this.parentElement?.matches?.('[data-toast-container]')) {
63
- this.#routed = true;
64
- const container = AdiaToast.#getContainer(this.position || 'bottom-right');
65
- if (this.parentElement !== container) container.appendChild(this);
66
- }
67
- }
68
-
69
- #getDuration() {
70
- const raw = getComputedStyle(this).getPropertyValue('--toast-duration').trim();
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 conveniencecreates a toast, appends to body, auto-removes.
58
+ * Static facadedelegates 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 {AdiaToast}
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 #getContainer(position) {
159
- let el = AdiaToast.#containers.get(position);
160
- if (el && el.isConnected) return el;
161
- el = document.createElement('div');
162
- el.setAttribute('data-toast-container', position);
163
- /* `manual` = no light-dismiss; container stays open until we
164
- explicitly hidePopover() it when its last toast leaves. */
165
- if ('popover' in HTMLElement.prototype) {
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
- /* Install the global 'toast' CustomEvent listener once, at module load,
208
- so any code can post into the channel without importing AdiaToast
209
- directly:
210
-
211
- window.dispatchEvent(new CustomEvent('toast', {
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
- * Pattern CSS (adia-chat, adia-editor, app-shell, etc.) is NOT
11
- * bundled here each page links its own patterns explicitly.
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, AdiaAppShell } from '@adia-ai/web-components';
5
+ * import { AdiaButton } from '@adia-ai/web-components';
6
6
  *
7
- * Loading this file registers every component + pattern custom
8
- * element (side effect of each module's `customElements.define(...)`
9
- * call). Pair with `@adia-ai/web-components/css` (the all-in-one
10
- * stylesheet) or link tokens/components/resets individually.
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
- * import { AdiaAppShell } from '@adia-ai/web-components/patterns';
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.28",
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": {