@adia-ai/web-components 0.0.6 → 0.0.8
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/modal/modal.js +5 -2
- package/components/popover/popover.js +6 -2
- package/core/icons.js +53 -10
- package/core/template.js +11 -2
- package/package.json +1 -1
- package/patterns/a2ui-root/a2ui-root.a2ui.json +7 -0
- package/patterns/a2ui-root/a2ui-root.js +27 -0
- package/patterns/a2ui-root/a2ui-root.yaml +11 -0
- package/traits/intersection-observer.js +4 -0
- package/traits/resize-observer.js +5 -0
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* close — fired after the modal finishes closing
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { AdiaElement
|
|
32
|
+
import { AdiaElement } from '../../core/element.js';
|
|
33
33
|
|
|
34
34
|
class AdiaModal extends AdiaElement {
|
|
35
35
|
#bound = false;
|
|
@@ -55,7 +55,10 @@ class AdiaModal extends AdiaElement {
|
|
|
55
55
|
footer: '<footer slot="footer"></footer>',
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
// No template — modal composes from authored light-DOM children. An empty
|
|
59
|
+
// html`` result would trigger stamp() → replaceChildren(), wiping authored
|
|
60
|
+
// [slot=body|footer] before render() can migrate them into the panel.
|
|
61
|
+
// (Same rationale as drawer.js — parallel pattern.)
|
|
59
62
|
|
|
60
63
|
#onPress = (e) => {
|
|
61
64
|
if (e.target.closest('[slot="close"]')) this.open = false;
|
|
@@ -79,7 +79,9 @@ class AdiaPopover extends AdiaElement {
|
|
|
79
79
|
const content = this.#content ?? this.querySelector('[slot="content"]');
|
|
80
80
|
if (!trigger || !content) return;
|
|
81
81
|
|
|
82
|
-
if (!content.matches(':popover-open'))
|
|
82
|
+
if (!content.matches(':popover-open')) {
|
|
83
|
+
try { content.showPopover(); } catch { /* popover API unavailable — anchor positioning still runs */ }
|
|
84
|
+
}
|
|
83
85
|
|
|
84
86
|
this.#anchorCleanup?.();
|
|
85
87
|
this.#anchorCleanup = anchorPopover(trigger, content, {
|
|
@@ -102,7 +104,9 @@ class AdiaPopover extends AdiaElement {
|
|
|
102
104
|
this.#anchorCleanup = null;
|
|
103
105
|
|
|
104
106
|
const content = this.#content ?? this.querySelector('[slot="content"]');
|
|
105
|
-
if (content?.matches(':popover-open'))
|
|
107
|
+
if (content?.matches(':popover-open')) {
|
|
108
|
+
try { content.hidePopover(); } catch { /* popover API unavailable */ }
|
|
109
|
+
}
|
|
106
110
|
|
|
107
111
|
if (this.#rafId != null) {
|
|
108
112
|
cancelAnimationFrame(this.#rafId);
|
package/core/icons.js
CHANGED
|
@@ -51,16 +51,59 @@ export function listIcons() {
|
|
|
51
51
|
|
|
52
52
|
// ── Async loader ──
|
|
53
53
|
|
|
54
|
-
// Vite
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
// Vite dev: `import.meta.glob(...)` is a build-time macro — Vite's AST
|
|
55
|
+
// transform replaces each call with a literal `{ path: loader }` object
|
|
56
|
+
// before the module reaches the browser. `import.meta.glob` itself is NOT
|
|
57
|
+
// exposed as a runtime property, so `typeof import.meta.glob` is always
|
|
58
|
+
// `'undefined'` at run time — don't try to feature-detect with it.
|
|
59
|
+
//
|
|
60
|
+
// Production static serving (no Vite): the glob calls remain in the
|
|
61
|
+
// source and `import.meta.glob` is undefined, so calling it throws a
|
|
62
|
+
// TypeError. We wrap the assignment in try/catch to handle both worlds:
|
|
63
|
+
// - Vite dev → Vite rewrote the calls to literals → try block succeeds.
|
|
64
|
+
// - Static → actual runtime call throws → catch block falls back to
|
|
65
|
+
// EMPTY_WEIGHTS; the manifest branch below fills it in.
|
|
66
|
+
const EMPTY_WEIGHTS = { regular: {}, thin: {}, light: {}, bold: {}, fill: {}, duotone: {} };
|
|
67
|
+
|
|
68
|
+
// `let` so the background manifest load (non-Vite branch) can swap in a
|
|
69
|
+
// real map once `icons-manifest.js` resolves. `resolveLoader` reads
|
|
70
|
+
// through this each call, so new icons become visible after manifest load.
|
|
71
|
+
let weightModules;
|
|
72
|
+
let hasViteGlob = false;
|
|
73
|
+
try {
|
|
74
|
+
weightModules = {
|
|
75
|
+
regular: import.meta.glob('/node_modules/@phosphor-icons/core/assets/regular/*.svg', { query: '?raw', import: 'default' }),
|
|
76
|
+
thin: import.meta.glob('/node_modules/@phosphor-icons/core/assets/thin/*.svg', { query: '?raw', import: 'default' }),
|
|
77
|
+
light: import.meta.glob('/node_modules/@phosphor-icons/core/assets/light/*.svg', { query: '?raw', import: 'default' }),
|
|
78
|
+
bold: import.meta.glob('/node_modules/@phosphor-icons/core/assets/bold/*.svg', { query: '?raw', import: 'default' }),
|
|
79
|
+
fill: import.meta.glob('/node_modules/@phosphor-icons/core/assets/fill/*.svg', { query: '?raw', import: 'default' }),
|
|
80
|
+
duotone: import.meta.glob('/node_modules/@phosphor-icons/core/assets/duotone/*.svg', { query: '?raw', import: 'default' }),
|
|
81
|
+
};
|
|
82
|
+
hasViteGlob = true;
|
|
83
|
+
} catch {
|
|
84
|
+
weightModules = EMPTY_WEIGHTS;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Non-Vite environments (plain static serving): fetch the build-time
|
|
88
|
+
// manifest in the background and rebuild `weightModules` with lazy
|
|
89
|
+
// fetch-based loaders. No top-level await — the module finishes loading
|
|
90
|
+
// immediately; icons appear once the one-shot manifest fetch resolves.
|
|
91
|
+
if (!hasViteGlob) {
|
|
92
|
+
// Specifier is hidden behind a variable so Vite's static analysis
|
|
93
|
+
// never tries to pre-resolve it in dev (where the file doesn't exist).
|
|
94
|
+
const manifestSpec = './icons-manifest.js';
|
|
95
|
+
import(/* @vite-ignore */ manifestSpec).then(({ default: manifest }) => {
|
|
96
|
+
weightModules = Object.fromEntries(Object.entries(manifest).map(([weight, names]) => {
|
|
97
|
+
const prefix = `/node_modules/@phosphor-icons/core/assets/${weight}/`;
|
|
98
|
+
const entries = names.map(name => {
|
|
99
|
+
const path = prefix + name;
|
|
100
|
+
const loader = () => fetch(path).then(r => r.ok ? r.text() : Promise.reject(new Error(`icon fetch failed: ${path}`)));
|
|
101
|
+
return [path, loader];
|
|
102
|
+
});
|
|
103
|
+
return [weight, Object.fromEntries(entries)];
|
|
104
|
+
}));
|
|
105
|
+
}).catch(() => { /* keep EMPTY_WEIGHTS */ });
|
|
106
|
+
}
|
|
64
107
|
|
|
65
108
|
/**
|
|
66
109
|
* Phosphor filename convention: `star.svg` for regular, `star-fill.svg` for
|
package/core/template.js
CHANGED
|
@@ -68,12 +68,21 @@ export function stamp(result, container) {
|
|
|
68
68
|
update(inst.p, result.values);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// Safari 14.0 lacks ChildNode.replaceChildren. Fallback manually removes
|
|
72
|
+
// existing children, then appends the new content. Runs once per call;
|
|
73
|
+
// cheap enough to be unconditional.
|
|
74
|
+
function replaceChildren(container, ...nodes) {
|
|
75
|
+
if (container.replaceChildren) { container.replaceChildren(...nodes); return; }
|
|
76
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
77
|
+
for (const n of nodes) container.appendChild(n);
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
function mount(result, container) {
|
|
72
81
|
const { strings } = result;
|
|
73
82
|
const tpl = getTemplate(strings);
|
|
74
83
|
const f = tpl.content.cloneNode(true);
|
|
75
84
|
const parts = scan(f, result.values.length);
|
|
76
|
-
|
|
85
|
+
replaceChildren(container, f);
|
|
77
86
|
return { s: strings, p: parts };
|
|
78
87
|
}
|
|
79
88
|
|
|
@@ -138,7 +147,7 @@ function applyValue(p, v) {
|
|
|
138
147
|
stamp(v, wrap(p));
|
|
139
148
|
} else if (Array.isArray(v)) {
|
|
140
149
|
const c = wrap(p);
|
|
141
|
-
|
|
150
|
+
replaceChildren(c);
|
|
142
151
|
for (const item of v) {
|
|
143
152
|
if (isResult(item)) {
|
|
144
153
|
const el = document.createElement('span');
|
package/package.json
CHANGED
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"component": {
|
|
27
27
|
"const": "A2UIRoot"
|
|
28
28
|
},
|
|
29
|
+
"doc": {
|
|
30
|
+
"description": "Author-driven mode — set to an array of A2UI messages and the renderer resets + replays them. No network/transport involvement. Setting to a new array triggers a full re-render. Use this for editors, previews, tests, and any static-doc authoring loop. When both `src` and `doc` are set, `doc` wins (the stream is not opened). Pass as a JS property; not reflected to an attribute.",
|
|
31
|
+
"type": "array"
|
|
32
|
+
},
|
|
29
33
|
"loading": {
|
|
30
34
|
"description": "True while the stream is connecting.",
|
|
31
35
|
"type": "boolean",
|
|
@@ -71,6 +75,9 @@
|
|
|
71
75
|
},
|
|
72
76
|
"a2ui-message": {
|
|
73
77
|
"description": "Fired for each A2UI message received. detail: { message }"
|
|
78
|
+
},
|
|
79
|
+
"doc-replaced": {
|
|
80
|
+
"description": "Fired after a full doc reset + replay in author-driven mode. detail: { count }"
|
|
74
81
|
}
|
|
75
82
|
},
|
|
76
83
|
"examples": [],
|
|
@@ -5,12 +5,24 @@
|
|
|
5
5
|
* <a2ui-root src="/api/agent" transport="sse"></a2ui-root>
|
|
6
6
|
* <a2ui-root src="ws://localhost:8080" transport="ws"></a2ui-root>
|
|
7
7
|
*
|
|
8
|
+
* Static / author-driven mode — set the `doc` property (array of A2UI messages)
|
|
9
|
+
* and the renderer resets + replays them. Editors and previews can drive the
|
|
10
|
+
* surface without opening a transport. Setting `doc` to a new array re-renders
|
|
11
|
+
* from scratch (reset() + processAll()).
|
|
12
|
+
*
|
|
13
|
+
* const root = document.querySelector('a2ui-root');
|
|
14
|
+
* root.doc = [
|
|
15
|
+
* { type: 'createSurface', surfaceId: 'root', root: 'c-1' },
|
|
16
|
+
* { type: 'updateComponents', components: [{ id: 'c-1', component: 'Heading', text: 'Hi' }] },
|
|
17
|
+
* ];
|
|
18
|
+
*
|
|
8
19
|
* Events:
|
|
9
20
|
* a2ui-connected — stream connected
|
|
10
21
|
* a2ui-message — each message received (detail: { message })
|
|
11
22
|
* a2ui-error — stream error (detail: { error })
|
|
12
23
|
* a2ui-closed — stream ended
|
|
13
24
|
* a2ui-action — user interaction (detail: { name, sourceComponentId, context })
|
|
25
|
+
* doc-replaced — fired after a full doc reset + replay (author-driven mode)
|
|
14
26
|
*/
|
|
15
27
|
|
|
16
28
|
import { AdiaElement } from '../../core/element.js';
|
|
@@ -36,6 +48,7 @@ class AdiaA2UIRoot extends AdiaElement {
|
|
|
36
48
|
|
|
37
49
|
#renderer = null;
|
|
38
50
|
#abortCtrl = null;
|
|
51
|
+
#doc = null;
|
|
39
52
|
|
|
40
53
|
connected() {
|
|
41
54
|
this.#renderer = new A2UIRenderer(this, registry, { batch: this.batch });
|
|
@@ -136,6 +149,20 @@ class AdiaA2UIRoot extends AdiaElement {
|
|
|
136
149
|
for (const msg of messages) this.process(msg);
|
|
137
150
|
}
|
|
138
151
|
|
|
152
|
+
get doc() { return this.#doc; }
|
|
153
|
+
set doc(messages) {
|
|
154
|
+
this.#doc = Array.isArray(messages) ? messages : [];
|
|
155
|
+
if (!this.#renderer) {
|
|
156
|
+
this.#renderer = new A2UIRenderer(this, registry);
|
|
157
|
+
}
|
|
158
|
+
this.#renderer.reset();
|
|
159
|
+
for (const msg of this.#doc) this.#renderer.process(msg);
|
|
160
|
+
this.dispatchEvent(new CustomEvent('doc-replaced', {
|
|
161
|
+
bubbles: true,
|
|
162
|
+
detail: { count: this.#doc.length },
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
|
|
139
166
|
reset() {
|
|
140
167
|
this.#renderer?.reset();
|
|
141
168
|
}
|
|
@@ -35,6 +35,15 @@ props:
|
|
|
35
35
|
description: Batch renderer updates via requestAnimationFrame for large fan-in.
|
|
36
36
|
type: boolean
|
|
37
37
|
default: false
|
|
38
|
+
doc:
|
|
39
|
+
description: >-
|
|
40
|
+
Author-driven mode — set to an array of A2UI messages and the renderer
|
|
41
|
+
resets + replays them. No network/transport involvement. Setting to a
|
|
42
|
+
new array triggers a full re-render. Use this for editors, previews,
|
|
43
|
+
tests, and any static-doc authoring loop. When both `src` and `doc` are
|
|
44
|
+
set, `doc` wins (the stream is not opened). Pass as a JS property; not
|
|
45
|
+
reflected to an attribute.
|
|
46
|
+
type: array
|
|
38
47
|
events:
|
|
39
48
|
a2ui-connected:
|
|
40
49
|
description: Fired when the stream is established.
|
|
@@ -46,6 +55,8 @@ events:
|
|
|
46
55
|
description: "Fired when the stream errors. detail: { error }"
|
|
47
56
|
a2ui-closed:
|
|
48
57
|
description: Fired when the stream ends.
|
|
58
|
+
doc-replaced:
|
|
59
|
+
description: "Fired after a full doc reset + replay in author-driven mode. detail: { count }"
|
|
49
60
|
slots:
|
|
50
61
|
default:
|
|
51
62
|
description: The rendered surface. Children are stamped by the A2UI renderer.
|
|
@@ -6,6 +6,10 @@ export const intersectionObserver = defineTrait({
|
|
|
6
6
|
events: ['element-visible', 'element-hidden'],
|
|
7
7
|
config: ['data-intersection-threshold'],
|
|
8
8
|
setup({ host }) {
|
|
9
|
+
// No-op in environments lacking IntersectionObserver (older Safari,
|
|
10
|
+
// happy-dom without polyfill). Trait becomes inert rather than throwing.
|
|
11
|
+
if (typeof IntersectionObserver === 'undefined') return () => {};
|
|
12
|
+
|
|
9
13
|
const threshold = parseFloat(host.getAttribute('data-intersection-threshold')) || 0;
|
|
10
14
|
|
|
11
15
|
const observer = new IntersectionObserver((entries) => {
|
|
@@ -6,6 +6,11 @@ export const resizeObserver = defineTrait({
|
|
|
6
6
|
events: ['element-resize'],
|
|
7
7
|
config: [],
|
|
8
8
|
setup({ host }) {
|
|
9
|
+
// Guard for older browsers + non-DOM test environments (happy-dom, jsdom
|
|
10
|
+
// without polyfill). The trait becomes a no-op rather than throwing on
|
|
11
|
+
// the `new ResizeObserver` call.
|
|
12
|
+
if (typeof ResizeObserver === 'undefined') return () => {};
|
|
13
|
+
|
|
9
14
|
const observer = new ResizeObserver((entries) => {
|
|
10
15
|
for (const entry of entries) {
|
|
11
16
|
const { width, height } = entry.contentRect;
|