@adia-ai/a2ui-runtime 0.3.0
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/CHANGELOG.md +215 -0
- package/README.md +87 -0
- package/controllers/accordion.js +73 -0
- package/controllers/base.js +68 -0
- package/controllers/data-stream.js +281 -0
- package/controllers/form.js +81 -0
- package/controllers/index.js +6 -0
- package/controllers/selection.js +82 -0
- package/controllers/state-machine.js +135 -0
- package/controllers/toggle.js +40 -0
- package/dockables/action.js +152 -0
- package/dockables/base.js +30 -0
- package/dockables/controller.js +97 -0
- package/dockables/data-source.js +103 -0
- package/dockables/index.js +6 -0
- package/dockables/lifecycle.js +84 -0
- package/dockables/provider.js +59 -0
- package/index.js +45 -0
- package/package.json +31 -0
- package/registry.js +205 -0
- package/renderer.js +395 -0
- package/stream.js +243 -0
- package/surface-manifest.js +294 -0
- package/surface.js +222 -0
- package/wire-factory.js +134 -0
- package/wiring-engine.js +209 -0
- package/wiring-registry.js +342 -0
package/registry.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2UI Registry — maps A2UI type names to AdiaUI custom element tag names.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { registry, resolveTag } from './registry.js';
|
|
6
|
+
* resolveTag('Button') // → 'button-ui'
|
|
7
|
+
* resolveTag('ChoicePicker') // → 'select-ui'
|
|
8
|
+
* resolveTag('Toggle') // → 'switch-ui'
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const registry = new Map([
|
|
12
|
+
|
|
13
|
+
// ══════════════════════════════════════════════════════
|
|
14
|
+
// A2UI Protocol Types (standard catalog)
|
|
15
|
+
// ══════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
// Layout
|
|
18
|
+
['Row', 'row-ui'],
|
|
19
|
+
['Column', 'col-ui'],
|
|
20
|
+
['List', 'list-ui'],
|
|
21
|
+
['ListItem', 'list-item-ui'],
|
|
22
|
+
['Grid', 'grid-ui'],
|
|
23
|
+
['Stack', 'stack-ui'],
|
|
24
|
+
['Block', 'block-ui'],
|
|
25
|
+
|
|
26
|
+
// Display
|
|
27
|
+
['Text', 'text-ui'],
|
|
28
|
+
['Image', 'image-ui'],
|
|
29
|
+
['Icon', 'icon-ui'],
|
|
30
|
+
['Divider', 'divider-ui'],
|
|
31
|
+
['Badge', 'badge-ui'],
|
|
32
|
+
['Avatar', 'avatar-ui'],
|
|
33
|
+
['AvatarGroup', 'avatar-group-ui'],
|
|
34
|
+
['Progress', 'progress-ui'],
|
|
35
|
+
['Skeleton', 'skeleton-ui'],
|
|
36
|
+
['Code', 'code-ui'],
|
|
37
|
+
['Stat', 'stat-ui'],
|
|
38
|
+
['EmptyState', 'empty-state-ui'],
|
|
39
|
+
|
|
40
|
+
// Input
|
|
41
|
+
['Input', 'input-ui'],
|
|
42
|
+
['TextField', 'input-ui'],
|
|
43
|
+
['TextArea', 'textarea-ui'],
|
|
44
|
+
['Field', 'field-ui'],
|
|
45
|
+
['CheckBox', 'check-ui'],
|
|
46
|
+
['Toggle', 'switch-ui'],
|
|
47
|
+
['Switch', 'switch-ui'],
|
|
48
|
+
['Slider', 'slider-ui'],
|
|
49
|
+
['Range', 'range-ui'],
|
|
50
|
+
['Rating', 'rating-ui'],
|
|
51
|
+
['ChoicePicker', 'select-ui'],
|
|
52
|
+
['Select', 'select-ui'],
|
|
53
|
+
['Radio', 'radio-ui'],
|
|
54
|
+
['DateTimeInput', 'calendar-picker-ui'],
|
|
55
|
+
['CalendarPicker', 'calendar-picker-ui'],
|
|
56
|
+
['ColorPicker', 'color-picker-ui'],
|
|
57
|
+
// Search deprecated — use Input type="search" prefix="magnifying-glass"
|
|
58
|
+
['Upload', 'upload-ui'],
|
|
59
|
+
['OtpInput', 'otp-input-ui'],
|
|
60
|
+
|
|
61
|
+
// Action
|
|
62
|
+
['Button', 'button-ui'],
|
|
63
|
+
|
|
64
|
+
// System State
|
|
65
|
+
['LoadingIndicator', 'progress-ui'],
|
|
66
|
+
['ErrorContainer', 'card-ui'],
|
|
67
|
+
|
|
68
|
+
// Container
|
|
69
|
+
['Card', 'card-ui'],
|
|
70
|
+
['Tabs', 'tabs-ui'],
|
|
71
|
+
['Tab', 'tab-ui'],
|
|
72
|
+
['Panel', 'pane-ui'],
|
|
73
|
+
['Pane', 'pane-ui'],
|
|
74
|
+
['Modal', 'modal-ui'],
|
|
75
|
+
['Dialog', 'modal-ui'],
|
|
76
|
+
['Drawer', 'drawer-ui'],
|
|
77
|
+
['Toast', 'toast-ui'],
|
|
78
|
+
['Popover', 'popover-ui'],
|
|
79
|
+
['Accordion', 'accordion-ui'],
|
|
80
|
+
['AccordionItem', 'accordion-item-ui'],
|
|
81
|
+
['Alert', 'alert-ui'],
|
|
82
|
+
['Tooltip', 'tooltip-ui'],
|
|
83
|
+
['Menu', 'menu-ui'],
|
|
84
|
+
|
|
85
|
+
// Card children (native HTML elements styled by card.css)
|
|
86
|
+
['Section', 'section'],
|
|
87
|
+
['Header', 'header'],
|
|
88
|
+
['Footer', 'footer'],
|
|
89
|
+
|
|
90
|
+
// Agent / Specialized
|
|
91
|
+
['Stream', 'stream-ui'],
|
|
92
|
+
['Table', 'table-ui'],
|
|
93
|
+
['Chart', 'chart-ui'],
|
|
94
|
+
['Embed', 'embed-ui'],
|
|
95
|
+
['Swiper', 'swiper-ui'],
|
|
96
|
+
['Slideshow', 'swiper-ui'],
|
|
97
|
+
['Carousel', 'swiper-ui'],
|
|
98
|
+
|
|
99
|
+
// Navigation
|
|
100
|
+
['Breadcrumb', 'breadcrumb-ui'],
|
|
101
|
+
['Nav', 'nav-ui'],
|
|
102
|
+
['NavGroup', 'nav-group-ui'],
|
|
103
|
+
['NavItem', 'nav-item-ui'],
|
|
104
|
+
['Noodles', 'noodles-ui'],
|
|
105
|
+
['Pagination', 'pagination-ui'],
|
|
106
|
+
['SegmentedControl', 'segmented-ui'],
|
|
107
|
+
['Segment', 'segment-ui'],
|
|
108
|
+
['ToggleGroup', 'toggle-group-ui'],
|
|
109
|
+
|
|
110
|
+
// Utility
|
|
111
|
+
['Command', 'command-ui'],
|
|
112
|
+
['Kbd', 'kbd-ui'],
|
|
113
|
+
['Toolbar', 'toolbar-ui'],
|
|
114
|
+
['Tag', 'tag-ui'],
|
|
115
|
+
['Timeline', 'timeline-ui'],
|
|
116
|
+
['TimelineItem', 'timeline-item-ui'],
|
|
117
|
+
|
|
118
|
+
// ══════════════════════════════════════════════════════
|
|
119
|
+
// AgentUI Aliases (backwards compat with -ui tags)
|
|
120
|
+
// ══════════════════════════════════════════════════════
|
|
121
|
+
['button-ui', 'button-ui'],
|
|
122
|
+
['card-ui', 'card-ui'],
|
|
123
|
+
['text-ui', 'text-ui'],
|
|
124
|
+
['input-ui', 'input-ui'],
|
|
125
|
+
['text-field-ui', 'input-ui'],
|
|
126
|
+
['select-ui', 'select-ui'],
|
|
127
|
+
['toggle-ui', 'switch-ui'],
|
|
128
|
+
['check-ui', 'check-ui'],
|
|
129
|
+
['slider-ui', 'slider-ui'],
|
|
130
|
+
['badge-ui', 'badge-ui'],
|
|
131
|
+
['avatar-ui', 'avatar-ui'],
|
|
132
|
+
['icon-ui', 'icon-ui'],
|
|
133
|
+
['image-ui', 'image-ui'],
|
|
134
|
+
['divider-ui', 'divider-ui'],
|
|
135
|
+
['progress-ui', 'progress-ui'],
|
|
136
|
+
['skeleton-ui', 'skeleton-ui'],
|
|
137
|
+
['tabs-ui', 'tabs-ui'],
|
|
138
|
+
['tab-ui', 'tab-ui'],
|
|
139
|
+
['modal-ui', 'modal-ui'],
|
|
140
|
+
['dialog-ui', 'modal-ui'],
|
|
141
|
+
['drawer-ui', 'drawer-ui'],
|
|
142
|
+
['toast-ui', 'toast-ui'],
|
|
143
|
+
['popover-ui', 'popover-ui'],
|
|
144
|
+
['panel-ui', 'pane-ui'],
|
|
145
|
+
['accordion-ui', 'accordion-ui'],
|
|
146
|
+
['alert-ui', 'alert-ui'],
|
|
147
|
+
['tooltip-ui', 'tooltip-ui'],
|
|
148
|
+
['menu-ui', 'menu-ui'],
|
|
149
|
+
['table-ui', 'table-ui'],
|
|
150
|
+
['chart-ui', 'chart-ui'],
|
|
151
|
+
['code-ui', 'code-ui'],
|
|
152
|
+
['textarea-ui', 'textarea-ui'],
|
|
153
|
+
['radio-ui', 'radio-ui'],
|
|
154
|
+
['tag-ui', 'tag-ui'],
|
|
155
|
+
['search-ui', 'search-ui'],
|
|
156
|
+
['upload-ui', 'upload-ui'],
|
|
157
|
+
['breadcrumb-ui', 'breadcrumb-ui'],
|
|
158
|
+
['nav-ui', 'nav-ui'],
|
|
159
|
+
['noodles-ui', 'noodles-ui'],
|
|
160
|
+
['pagination-ui', 'pagination-ui'],
|
|
161
|
+
['segmented-control-ui', 'segmented-ui'],
|
|
162
|
+
['segment-ui', 'segment-ui'],
|
|
163
|
+
['command-ui', 'command-ui'],
|
|
164
|
+
['calendar-picker-ui', 'calendar-picker-ui'],
|
|
165
|
+
['color-picker-ui', 'color-picker-ui'],
|
|
166
|
+
['kbd-ui', 'kbd-ui'],
|
|
167
|
+
['toolbar-ui', 'toolbar-ui'],
|
|
168
|
+
['otp-input-ui', 'otp-input-ui'],
|
|
169
|
+
['embed-ui', 'embed-ui'],
|
|
170
|
+
['stream-ui', 'stream-ui'],
|
|
171
|
+
['row-ui', 'row-ui'],
|
|
172
|
+
['col-ui', 'col-ui'],
|
|
173
|
+
['grid-ui', 'grid-ui'],
|
|
174
|
+
['stack-ui', 'stack-ui'],
|
|
175
|
+
['block-ui', 'block-ui'],
|
|
176
|
+
['list-ui', 'list-ui'],
|
|
177
|
+
['range-ui', 'range-ui'],
|
|
178
|
+
['datetime-ui', 'calendar-picker-ui'],
|
|
179
|
+
['timeline-ui', 'timeline-ui'],
|
|
180
|
+
['timeline-item-ui', 'timeline-item-ui'],
|
|
181
|
+
['avatar-group-ui', 'avatar-group-ui'],
|
|
182
|
+
|
|
183
|
+
// Aliases (alternate names)
|
|
184
|
+
['Keyboard', 'kbd-ui'],
|
|
185
|
+
['DatePicker', 'calendar-picker-ui'],
|
|
186
|
+
['CommandPalette', 'command-ui'],
|
|
187
|
+
['Segmented', 'segmented-ui'],
|
|
188
|
+
['OTP', 'otp-input-ui'],
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resolve an A2UI component type to a AdiaUI tag name.
|
|
193
|
+
* @param {string} type — A2UI type name or AgentUI tag
|
|
194
|
+
* @returns {string|null} — AdiaUI tag name or null
|
|
195
|
+
*/
|
|
196
|
+
export function resolveTag(type) {
|
|
197
|
+
return registry.get(type) || null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Register a custom component type.
|
|
202
|
+
*/
|
|
203
|
+
export function registerType(type, tagName) {
|
|
204
|
+
registry.set(type, tagName);
|
|
205
|
+
}
|
package/renderer.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2UI Renderer — processes A2UI messages and renders AdiaUI components.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { A2UIRenderer } from './renderer.js';
|
|
6
|
+
* const renderer = new A2UIRenderer(container);
|
|
7
|
+
* renderer.process(message);
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { resolveTag, registry } from './registry.js';
|
|
11
|
+
|
|
12
|
+
export class A2UIRenderer {
|
|
13
|
+
#container;
|
|
14
|
+
#registry;
|
|
15
|
+
#surfaces = new Map();
|
|
16
|
+
#elements = new Map();
|
|
17
|
+
#prevProps = new Map();
|
|
18
|
+
#queue = [];
|
|
19
|
+
#rafId = null;
|
|
20
|
+
#batching = false;
|
|
21
|
+
|
|
22
|
+
constructor(container, reg = registry, { batch = false } = {}) {
|
|
23
|
+
this.#container = container;
|
|
24
|
+
this.#registry = reg;
|
|
25
|
+
this.#batching = batch;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
process(message) {
|
|
29
|
+
if (!message) return;
|
|
30
|
+
if (this.#batching) {
|
|
31
|
+
this.#queue.push(message);
|
|
32
|
+
if (this.#rafId === null) {
|
|
33
|
+
this.#rafId = requestAnimationFrame(() => this.#flush());
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
this.#processOne(message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#flush() {
|
|
41
|
+
this.#rafId = null;
|
|
42
|
+
const batch = this.#queue.splice(0);
|
|
43
|
+
for (const msg of batch) this.#processOne(msg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#processOne(message) {
|
|
47
|
+
switch (message.type || message.messageType) {
|
|
48
|
+
case 'createSurface': this.#createSurface(message); break;
|
|
49
|
+
case 'updateComponents': this.#updateComponents(message); break;
|
|
50
|
+
case 'updateDataModel': this.#updateDataModel(message); break;
|
|
51
|
+
case 'wireComponents': this.#wireComponents(message); break;
|
|
52
|
+
case 'deleteSurface': this.#deleteSurface(message); break;
|
|
53
|
+
case 'meta': break; // LLM self-critique — not renderable
|
|
54
|
+
default: console.warn('A2UI: unknown message type', message.type);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── wireComponents (lazy-loaded) ──
|
|
59
|
+
|
|
60
|
+
#wiringEngine = null;
|
|
61
|
+
|
|
62
|
+
async #wireComponents(message) {
|
|
63
|
+
if (!this.#wiringEngine) {
|
|
64
|
+
const { WiringEngine } = await import('./wiring-engine.js');
|
|
65
|
+
this.#wiringEngine = new WiringEngine({
|
|
66
|
+
updateDataModel: (surfaceId, path, data) => {
|
|
67
|
+
this.#updateDataModel({ surfaceId, path, value: data });
|
|
68
|
+
},
|
|
69
|
+
getElement: (surfaceId, componentId) => {
|
|
70
|
+
const surface = this.#surfaces.get(surfaceId);
|
|
71
|
+
return surface?.elements.get(componentId) || null;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
await this.#wiringEngine.process(message);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async processStream(stream) {
|
|
79
|
+
for await (const message of stream) this.process(message);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── createSurface ──
|
|
83
|
+
|
|
84
|
+
#createSurface({ surfaceId, catalogId, root: rootId }) {
|
|
85
|
+
if (this.#surfaces.has(surfaceId)) return;
|
|
86
|
+
|
|
87
|
+
const root = document.createElement('div');
|
|
88
|
+
root.setAttribute('data-a2ui-surface', surfaceId);
|
|
89
|
+
if (catalogId) root.setAttribute('data-catalog', catalogId);
|
|
90
|
+
this.#container.appendChild(root);
|
|
91
|
+
|
|
92
|
+
this.#surfaces.set(surfaceId, {
|
|
93
|
+
root,
|
|
94
|
+
// `rootId` = the id of the component to attach as the surface's
|
|
95
|
+
// rendered root. Honors the A2UI protocol's `createSurface.root`
|
|
96
|
+
// field when present; falls back to the legacy `'root'` convention
|
|
97
|
+
// that earlier docs baked in. See #updateComponents attach step.
|
|
98
|
+
rootId: typeof rootId === 'string' && rootId ? rootId : 'root',
|
|
99
|
+
elements: new Map(),
|
|
100
|
+
dataModel: {},
|
|
101
|
+
bindings: new Map(),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── updateComponents ──
|
|
106
|
+
|
|
107
|
+
#updateComponents({ surfaceId, components }) {
|
|
108
|
+
let surface = this.#surfaces.get(surfaceId);
|
|
109
|
+
|
|
110
|
+
if (!surface) {
|
|
111
|
+
surface = {
|
|
112
|
+
root: this.#container,
|
|
113
|
+
// Synthetic surfaces (updateComponents without a prior
|
|
114
|
+
// createSurface) default to the legacy 'root' convention.
|
|
115
|
+
rootId: 'root',
|
|
116
|
+
elements: new Map(),
|
|
117
|
+
dataModel: {},
|
|
118
|
+
bindings: new Map(),
|
|
119
|
+
};
|
|
120
|
+
this.#surfaces.set(surfaceId, surface);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// First pass: create/update elements
|
|
124
|
+
for (const comp of components) {
|
|
125
|
+
if (!comp.id && comp.id !== 0) continue;
|
|
126
|
+
if (comp.component === 'ContextBindings') {
|
|
127
|
+
surface.contextBindings = comp;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
let el = surface.elements.get(comp.id);
|
|
133
|
+
const tagName = comp.component === 'Text'
|
|
134
|
+
? this.#resolveTextTag(comp.variant, comp)
|
|
135
|
+
: resolveTag(comp.component, this.#registry);
|
|
136
|
+
|
|
137
|
+
if (!tagName) {
|
|
138
|
+
if (!el) {
|
|
139
|
+
el = document.createElement('div');
|
|
140
|
+
el.setAttribute('data-a2ui-id', comp.id);
|
|
141
|
+
el.setAttribute('data-a2ui-unknown', comp.component);
|
|
142
|
+
el.textContent = `[unknown: ${comp.component}]`;
|
|
143
|
+
surface.elements.set(comp.id, el);
|
|
144
|
+
this.#elements.set(comp.id, el);
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!el) {
|
|
150
|
+
el = document.createElement(tagName);
|
|
151
|
+
el.setAttribute('data-a2ui-id', comp.id);
|
|
152
|
+
surface.elements.set(comp.id, el);
|
|
153
|
+
this.#elements.set(comp.id, el);
|
|
154
|
+
} else if (el.localName !== tagName) {
|
|
155
|
+
const newEl = document.createElement(tagName);
|
|
156
|
+
newEl.setAttribute('data-a2ui-id', comp.id);
|
|
157
|
+
el.replaceWith(newEl);
|
|
158
|
+
el = newEl;
|
|
159
|
+
surface.elements.set(comp.id, el);
|
|
160
|
+
this.#elements.set(comp.id, el);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
comp._surfaceId = surfaceId;
|
|
164
|
+
|
|
165
|
+
const hasBindings = Object.values(comp).some(v => v && typeof v === 'object' && v.path);
|
|
166
|
+
if (hasBindings) surface.bindings.set(comp.id, comp);
|
|
167
|
+
|
|
168
|
+
this.#applyProps(el, comp);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.warn(`A2UI: component "${comp.id}" (${comp.component}) failed:`, err.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Build parent map for cycle detection
|
|
175
|
+
const parentMap = new Map();
|
|
176
|
+
for (const comp of components) {
|
|
177
|
+
for (const childId of (Array.isArray(comp.children) ? comp.children : [])) {
|
|
178
|
+
parentMap.set(childId, comp.id);
|
|
179
|
+
}
|
|
180
|
+
if (comp.child) parentMap.set(comp.child, comp.id);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const wouldCycle = (parentId, childId) => {
|
|
184
|
+
const visited = new Set();
|
|
185
|
+
let cursor = parentId;
|
|
186
|
+
while (cursor != null) {
|
|
187
|
+
if (cursor === childId) return true;
|
|
188
|
+
if (visited.has(cursor)) return true;
|
|
189
|
+
visited.add(cursor);
|
|
190
|
+
cursor = parentMap.get(cursor);
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Second pass: build tree
|
|
196
|
+
for (const comp of components) {
|
|
197
|
+
const el = surface.elements.get(comp.id);
|
|
198
|
+
if (!el) continue;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const childIds = Array.isArray(comp.children) ? comp.children : [];
|
|
202
|
+
for (const childId of childIds) {
|
|
203
|
+
if (childId === comp.id || wouldCycle(comp.id, childId)) continue;
|
|
204
|
+
const childEl = surface.elements.get(childId);
|
|
205
|
+
if (childEl && childEl.parentElement !== el) el.appendChild(childEl);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (comp.child && comp.child !== comp.id && !wouldCycle(comp.id, comp.child)) {
|
|
209
|
+
const childEl = surface.elements.get(comp.child);
|
|
210
|
+
if (childEl && childEl.parentElement !== el) el.appendChild(childEl);
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.warn(`A2UI: tree build failed for "${comp.id}":`, err.message);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Attach root — use the surface's declared rootId (from
|
|
218
|
+
// createSurface.root) if present, else fall back to the legacy
|
|
219
|
+
// 'root' convention. Both surface.rootId and the fallback are
|
|
220
|
+
// looked up in the same elements Map.
|
|
221
|
+
const attachId = surface.rootId ?? 'root';
|
|
222
|
+
const rootComp = components.find(c => c.id === attachId);
|
|
223
|
+
if (rootComp) {
|
|
224
|
+
const rootEl = surface.elements.get(attachId);
|
|
225
|
+
if (rootEl && rootEl.parentElement !== surface.root) {
|
|
226
|
+
surface.root.appendChild(rootEl);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Apply props ──
|
|
232
|
+
|
|
233
|
+
static #JS_PROPS = new Set(['data', 'columns', 'options', 'itemRenderer', 'textContent']);
|
|
234
|
+
|
|
235
|
+
// Tags where setting `textContent` is safe — pure text-bearing leaves.
|
|
236
|
+
// On a container (alert-ui, card-ui, button-ui, …) `el.textContent = value`
|
|
237
|
+
// would wipe slotted children + trailing-pass appended children, leaving a
|
|
238
|
+
// bare text node sandwiched between attributes and children. When an LLM
|
|
239
|
+
// emits `{ component: "Alert", textContent: "…" }` we instead want to set
|
|
240
|
+
// the `text=` attribute, which the component's own connected()/render()
|
|
241
|
+
// routes into its `<span slot="content">` slot.
|
|
242
|
+
static #TEXT_TAG_OK = new Set([
|
|
243
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
244
|
+
'p', 'small', 'span', 'em', 'strong', 'code',
|
|
245
|
+
'text-ui',
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
// Semantic HTML variants — these render as native tags (h1, p, small)
|
|
249
|
+
// Everything else renders as <text-ui> which is now display:inline
|
|
250
|
+
static #TEXT_TAG_MAP = {
|
|
251
|
+
h1: 'h1', h2: 'h2', h3: 'h3', h4: 'h4', h5: 'h5', h6: 'h6',
|
|
252
|
+
body: 'p', caption: 'small',
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
#resolveTextTag(variant, comp) {
|
|
256
|
+
// Semantic HTML variants get native tags (accessibility)
|
|
257
|
+
if (!comp?.slot && A2UIRenderer.#TEXT_TAG_MAP[variant]) {
|
|
258
|
+
return A2UIRenderer.#TEXT_TAG_MAP[variant];
|
|
259
|
+
}
|
|
260
|
+
// Everything else → text-ui (inline, supports truncate/lines/variant)
|
|
261
|
+
return 'text-ui';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#applyProps(el, comp) {
|
|
265
|
+
const skip = new Set(['id', 'component', 'children', 'child', '_surfaceId']);
|
|
266
|
+
// Skip variant attr when the tag IS the variant (h1 for h1, p for body, small for caption)
|
|
267
|
+
if (comp.component === 'Text' && comp.variant && !comp.slot && A2UIRenderer.#TEXT_TAG_MAP[comp.variant]) {
|
|
268
|
+
skip.add('variant');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const prev = this.#prevProps.get(comp.id);
|
|
272
|
+
const next = {};
|
|
273
|
+
|
|
274
|
+
for (const [key, value] of Object.entries(comp)) {
|
|
275
|
+
if (skip.has(key)) continue;
|
|
276
|
+
|
|
277
|
+
const isBinding = value && typeof value === 'object' && value.path;
|
|
278
|
+
const resolved = this.#resolveValue(value, comp._surfaceId);
|
|
279
|
+
next[key] = resolved;
|
|
280
|
+
|
|
281
|
+
if (prev && Object.is(prev[key], resolved)) continue;
|
|
282
|
+
|
|
283
|
+
if (key === 'style' && typeof resolved === 'string') {
|
|
284
|
+
el.style.cssText = resolved;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (A2UIRenderer.#JS_PROPS.has(key) && resolved != null) {
|
|
289
|
+
// Guard against destructive `el.textContent = …` on container
|
|
290
|
+
// elements: it wipes slotted children + the trailing-pass tree.
|
|
291
|
+
// For non-text-bearing tags, route the value to the `text=`
|
|
292
|
+
// attribute instead (which Alert/Button/Badge/etc. read in
|
|
293
|
+
// their own render loop and slot correctly).
|
|
294
|
+
if (key === 'textContent' && !A2UIRenderer.#TEXT_TAG_OK.has(el.localName)) {
|
|
295
|
+
el.setAttribute('text', String(resolved));
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
el[key] = resolved;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (typeof resolved === 'boolean') {
|
|
303
|
+
if (resolved) el.setAttribute(key, '');
|
|
304
|
+
else el.removeAttribute(key);
|
|
305
|
+
} else if (resolved != null) {
|
|
306
|
+
el.setAttribute(this.#toAttr(key), String(resolved));
|
|
307
|
+
} else if (isBinding) {
|
|
308
|
+
el.removeAttribute(this.#toAttr(key));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (prev) {
|
|
313
|
+
for (const key of Object.keys(prev)) {
|
|
314
|
+
if (!(key in next) && !skip.has(key)) el.removeAttribute(this.#toAttr(key));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this.#prevProps.set(comp.id, next);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#toAttr(name) {
|
|
322
|
+
return name.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#resolveValue(value, surfaceId) {
|
|
326
|
+
if (value == null) return null;
|
|
327
|
+
if (typeof value !== 'object') return value;
|
|
328
|
+
if (value.path) {
|
|
329
|
+
const surface = surfaceId ? this.#surfaces.get(surfaceId) : null;
|
|
330
|
+
return surface ? this.#getByPath(surface.dataModel, value.path) : value.path;
|
|
331
|
+
}
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#getByPath(obj, path) {
|
|
336
|
+
if (!path || path === '/') return obj;
|
|
337
|
+
return path.split('/').filter(Boolean).reduce((o, k) => o?.[k], obj);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── updateDataModel ──
|
|
341
|
+
|
|
342
|
+
#updateDataModel({ surfaceId, path, value }) {
|
|
343
|
+
const surface = this.#surfaces.get(surfaceId);
|
|
344
|
+
if (!surface) return;
|
|
345
|
+
|
|
346
|
+
if (!path || path === '/') {
|
|
347
|
+
surface.dataModel = value ?? {};
|
|
348
|
+
} else {
|
|
349
|
+
const parts = path.split('/').filter(Boolean);
|
|
350
|
+
let cur = surface.dataModel;
|
|
351
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
352
|
+
if (cur[parts[i]] == null) cur[parts[i]] = {};
|
|
353
|
+
cur = cur[parts[i]];
|
|
354
|
+
}
|
|
355
|
+
cur[parts[parts.length - 1]] = value;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const [compId, comp] of surface.bindings) {
|
|
359
|
+
const el = surface.elements.get(compId);
|
|
360
|
+
if (el) this.#applyProps(el, comp);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── deleteSurface ──
|
|
365
|
+
|
|
366
|
+
#deleteSurface({ surfaceId }) {
|
|
367
|
+
const surface = this.#surfaces.get(surfaceId);
|
|
368
|
+
if (!surface) return;
|
|
369
|
+
this.#wiringEngine?.teardown(surfaceId);
|
|
370
|
+
for (const [id] of surface.elements) this.#elements.delete(id);
|
|
371
|
+
surface.root.remove();
|
|
372
|
+
this.#surfaces.delete(surfaceId);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── Public ──
|
|
376
|
+
|
|
377
|
+
getSurface(id) { return this.#surfaces.get(id); }
|
|
378
|
+
getElement(id) { return this.#elements.get(id); }
|
|
379
|
+
get surfaces() { return [...this.#surfaces.keys()]; }
|
|
380
|
+
|
|
381
|
+
reset() {
|
|
382
|
+
if (this.#rafId !== null) { cancelAnimationFrame(this.#rafId); this.#rafId = null; }
|
|
383
|
+
this.#queue.length = 0;
|
|
384
|
+
for (const [, s] of this.#surfaces) {
|
|
385
|
+
if (s.root === this.#container) s.root.innerHTML = '';
|
|
386
|
+
else s.root.remove();
|
|
387
|
+
}
|
|
388
|
+
this.#surfaces.clear();
|
|
389
|
+
this.#elements.clear();
|
|
390
|
+
this.#prevProps.clear();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
set batching(v) { this.#batching = !!v; }
|
|
394
|
+
get batching() { return this.#batching; }
|
|
395
|
+
}
|