@adia-ai/a2ui 0.8.37
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 +1073 -0
- package/README.md +99 -0
- package/a2ui.schema.d.ts +192 -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.d.ts +55 -0
- package/dockables/action.js +152 -0
- package/dockables/base.d.ts +26 -0
- package/dockables/base.js +30 -0
- package/dockables/controller.d.ts +35 -0
- package/dockables/controller.js +97 -0
- package/dockables/data-source.d.ts +35 -0
- package/dockables/data-source.js +103 -0
- package/dockables/index.d.ts +21 -0
- package/dockables/index.js +6 -0
- package/dockables/lifecycle.d.ts +38 -0
- package/dockables/lifecycle.js +84 -0
- package/dockables/provider.d.ts +28 -0
- package/dockables/provider.js +59 -0
- package/index.d.ts +64 -0
- package/index.js +54 -0
- package/package.json +89 -0
- package/prop-apply.d.ts +13 -0
- package/prop-apply.js +113 -0
- package/registry.d.ts +17 -0
- package/registry.js +418 -0
- package/renderer.d.ts +67 -0
- package/renderer.js +715 -0
- package/stream.d.ts +62 -0
- package/stream.js +521 -0
- package/surface-manifest.d.ts +73 -0
- package/surface-manifest.js +294 -0
- package/surface.d.ts +72 -0
- package/surface.js +222 -0
- package/types.d.ts +26 -0
- package/validate/CHANGELOG.md +1005 -0
- package/validate/README.md +146 -0
- package/validate/index.d.ts +4 -0
- package/validate/index.js +12 -0
- package/validate/validator.d.ts +4 -0
- package/validate/validator.js +1232 -0
- package/wire-factory.d.ts +15 -0
- package/wire-factory.js +134 -0
- package/wiring-engine.d.ts +61 -0
- package/wiring-engine.js +209 -0
- package/wiring-registry.d.ts +80 -0
- package/wiring-registry.js +342 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface Manifest — Multi-surface relationship document (A008).
|
|
3
|
+
*
|
|
4
|
+
* Manages a graph of surfaces and their associations:
|
|
5
|
+
* routes-to, feeds, shares-context, depends-on, triggers, contains, slots-into
|
|
6
|
+
*
|
|
7
|
+
* The manifest is the design-time document that describes application topology.
|
|
8
|
+
* The runtime reads it to set up navigation, pre-fetch data, and manage
|
|
9
|
+
* cross-surface context.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} SurfaceDescriptor
|
|
14
|
+
* @property {string} name — Human-readable name
|
|
15
|
+
* @property {string} [route] — URL pattern with :param placeholders
|
|
16
|
+
* @property {boolean} [entryPoint] — Can user navigate here directly?
|
|
17
|
+
* @property {string[]} [requiredParams] — Params needed to render
|
|
18
|
+
* @property {Record<string, object>} [produces] — Data this surface outputs
|
|
19
|
+
* @property {Record<string, { keys: string[] }>} [consumes] — Named contexts consumed
|
|
20
|
+
* @property {string} [status] — generated | manual | template | placeholder
|
|
21
|
+
* @property {string} [generatedBy] — Execution ID from gen-ui pipeline
|
|
22
|
+
* @property {string[]} [tags] — Freeform tags
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} Association
|
|
27
|
+
* @property {string} type — routes-to | feeds | shares-context | depends-on | triggers | contains | slots-into
|
|
28
|
+
* @property {string} from — Source surface ID
|
|
29
|
+
* @property {string} to — Target surface ID
|
|
30
|
+
* @property {string} [trigger] — What activates this association
|
|
31
|
+
* @property {Record<string, object>} [params] — Data passed from source to target
|
|
32
|
+
* @property {object} [mapping] — Data flow mapping (for feeds)
|
|
33
|
+
* @property {string} [context] — Shared context name (for shares-context)
|
|
34
|
+
* @property {string} [condition] — Dependency condition (for depends-on)
|
|
35
|
+
* @property {string} [fallback] — Fallback action (for depends-on)
|
|
36
|
+
* @property {string} [effect] — Side effect (for triggers)
|
|
37
|
+
* @property {string} [slot] — Composition slot (for contains/slots-into)
|
|
38
|
+
* @property {number} [position] — Order within slot
|
|
39
|
+
* @property {object} [meta] — Freeform metadata
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export class SurfaceManifest {
|
|
43
|
+
#id;
|
|
44
|
+
#name;
|
|
45
|
+
#version;
|
|
46
|
+
/** @type {Map<string, SurfaceDescriptor>} */
|
|
47
|
+
#surfaces = new Map();
|
|
48
|
+
/** @type {Association[]} */
|
|
49
|
+
#associations = [];
|
|
50
|
+
/** @type {Map<string, object>} */
|
|
51
|
+
#sharedContexts = new Map();
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {object} opts
|
|
55
|
+
* @param {string} opts.id — Manifest ID
|
|
56
|
+
* @param {string} opts.name — Human-readable name
|
|
57
|
+
* @param {string} [opts.version]
|
|
58
|
+
*/
|
|
59
|
+
constructor({ id, name, version = '1.0.0' }) {
|
|
60
|
+
this.#id = id;
|
|
61
|
+
this.#name = name;
|
|
62
|
+
this.#version = version;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Surface CRUD ──
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Add or update a surface descriptor.
|
|
69
|
+
* @param {string} surfaceId
|
|
70
|
+
* @param {SurfaceDescriptor} descriptor
|
|
71
|
+
*/
|
|
72
|
+
addSurface(surfaceId, descriptor) {
|
|
73
|
+
this.#surfaces.set(surfaceId, { ...descriptor });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Remove a surface and all its associations.
|
|
78
|
+
* @param {string} surfaceId
|
|
79
|
+
*/
|
|
80
|
+
removeSurface(surfaceId) {
|
|
81
|
+
this.#surfaces.delete(surfaceId);
|
|
82
|
+
this.#associations = this.#associations.filter(
|
|
83
|
+
a => a.from !== surfaceId && a.to !== surfaceId
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get a surface descriptor.
|
|
89
|
+
* @param {string} surfaceId
|
|
90
|
+
* @returns {SurfaceDescriptor | null}
|
|
91
|
+
*/
|
|
92
|
+
getSurface(surfaceId) {
|
|
93
|
+
return this.#surfaces.get(surfaceId) ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @returns {string[]} */
|
|
97
|
+
get surfaceIds() {
|
|
98
|
+
return [...this.#surfaces.keys()];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Associations ──
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Add an association between surfaces.
|
|
105
|
+
* @param {Association} association
|
|
106
|
+
*/
|
|
107
|
+
addAssociation(association) {
|
|
108
|
+
// Validate surfaces exist
|
|
109
|
+
if (!this.#surfaces.has(association.from)) {
|
|
110
|
+
console.warn(`Manifest: surface "${association.from}" not found`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!this.#surfaces.has(association.to)) {
|
|
114
|
+
console.warn(`Manifest: surface "${association.to}" not found`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Deduplicate: same type + from + to replaces existing
|
|
119
|
+
this.#associations = this.#associations.filter(
|
|
120
|
+
a => !(a.type === association.type && a.from === association.from && a.to === association.to)
|
|
121
|
+
);
|
|
122
|
+
this.#associations.push({ ...association });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get associations for a surface (outgoing).
|
|
127
|
+
* @param {string} surfaceId
|
|
128
|
+
* @param {string} [type] — Filter by association type
|
|
129
|
+
* @returns {Association[]}
|
|
130
|
+
*/
|
|
131
|
+
getAssociationsFrom(surfaceId, type) {
|
|
132
|
+
return this.#associations.filter(
|
|
133
|
+
a => a.from === surfaceId && (!type || a.type === type)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get associations targeting a surface (incoming).
|
|
139
|
+
* @param {string} surfaceId
|
|
140
|
+
* @param {string} [type]
|
|
141
|
+
* @returns {Association[]}
|
|
142
|
+
*/
|
|
143
|
+
getAssociationsTo(surfaceId, type) {
|
|
144
|
+
return this.#associations.filter(
|
|
145
|
+
a => a.to === surfaceId && (!type || a.type === type)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get all surfaces sharing a context with the given surface.
|
|
151
|
+
* @param {string} surfaceId
|
|
152
|
+
* @returns {{ surfaceId: string, context: string }[]}
|
|
153
|
+
*/
|
|
154
|
+
getSharedContextPeers(surfaceId) {
|
|
155
|
+
const peers = [];
|
|
156
|
+
for (const a of this.#associations) {
|
|
157
|
+
if (a.type !== 'shares-context') continue;
|
|
158
|
+
if (a.from === surfaceId) peers.push({ surfaceId: a.to, context: a.context });
|
|
159
|
+
if (a.to === surfaceId) peers.push({ surfaceId: a.from, context: a.context });
|
|
160
|
+
}
|
|
161
|
+
return peers;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Shared Contexts ──
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Define a shared context.
|
|
168
|
+
* @param {string} name
|
|
169
|
+
* @param {object} config — { shape, source, params }
|
|
170
|
+
*/
|
|
171
|
+
defineSharedContext(name, config) {
|
|
172
|
+
this.#sharedContexts.set(name, { ...config });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get a shared context definition.
|
|
177
|
+
* @param {string} name
|
|
178
|
+
* @returns {object | null}
|
|
179
|
+
*/
|
|
180
|
+
getSharedContext(name) {
|
|
181
|
+
return this.#sharedContexts.get(name) ?? null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Validation ──
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Validate the manifest for common issues.
|
|
188
|
+
* @returns {{ valid: boolean, issues: { severity: string, message: string }[] }}
|
|
189
|
+
*/
|
|
190
|
+
validate() {
|
|
191
|
+
const issues = [];
|
|
192
|
+
|
|
193
|
+
// Check for orphan surfaces (no associations)
|
|
194
|
+
for (const id of this.#surfaces.keys()) {
|
|
195
|
+
const hasAssoc = this.#associations.some(a => a.from === id || a.to === id);
|
|
196
|
+
if (!hasAssoc && this.#surfaces.size > 1) {
|
|
197
|
+
issues.push({ severity: 'warning', message: `Surface "${id}" has no associations` });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check for missing entry points
|
|
202
|
+
const entryPoints = [...this.#surfaces.entries()].filter(([, d]) => d.entryPoint);
|
|
203
|
+
if (entryPoints.length === 0 && this.#surfaces.size > 0) {
|
|
204
|
+
issues.push({ severity: 'warning', message: 'No surface marked as entryPoint' });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check depends-on has fallback
|
|
208
|
+
for (const a of this.#associations) {
|
|
209
|
+
if (a.type === 'depends-on' && !a.fallback) {
|
|
210
|
+
issues.push({ severity: 'warning', message: `depends-on from "${a.from}" to "${a.to}" has no fallback` });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check shares-context references defined contexts
|
|
215
|
+
for (const a of this.#associations) {
|
|
216
|
+
if (a.type === 'shares-context' && a.context && !this.#sharedContexts.has(a.context)) {
|
|
217
|
+
issues.push({ severity: 'error', message: `Shared context "${a.context}" referenced but not defined` });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check feeds associations have trigger
|
|
222
|
+
for (const a of this.#associations) {
|
|
223
|
+
if (a.type === 'feeds' && !a.trigger) {
|
|
224
|
+
issues.push({ severity: 'warning', message: `feeds from "${a.from}" to "${a.to}" has no trigger` });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check routes-to targets have routes
|
|
229
|
+
for (const a of this.#associations) {
|
|
230
|
+
if (a.type === 'routes-to') {
|
|
231
|
+
const target = this.#surfaces.get(a.to);
|
|
232
|
+
if (target && !target.route) {
|
|
233
|
+
issues.push({ severity: 'error', message: `routes-to target "${a.to}" has no route defined` });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
valid: !issues.some(i => i.severity === 'error'),
|
|
240
|
+
issues,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Serialization ──
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Export as a JSON-serializable object.
|
|
248
|
+
* @returns {object}
|
|
249
|
+
*/
|
|
250
|
+
toJSON() {
|
|
251
|
+
return {
|
|
252
|
+
$schema: 'https://a2ui.dev/schema/relationships/v1',
|
|
253
|
+
id: this.#id,
|
|
254
|
+
name: this.#name,
|
|
255
|
+
version: this.#version,
|
|
256
|
+
surfaces: Object.fromEntries(this.#surfaces),
|
|
257
|
+
associations: [...this.#associations],
|
|
258
|
+
sharedContexts: Object.fromEntries(this.#sharedContexts),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Import from a JSON object.
|
|
264
|
+
* @param {object} json
|
|
265
|
+
* @returns {SurfaceManifest}
|
|
266
|
+
*/
|
|
267
|
+
static fromJSON(json) {
|
|
268
|
+
const manifest = new SurfaceManifest({
|
|
269
|
+
id: json.id,
|
|
270
|
+
name: json.name,
|
|
271
|
+
version: json.version,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (json.surfaces) {
|
|
275
|
+
for (const [id, desc] of Object.entries(json.surfaces)) {
|
|
276
|
+
manifest.addSurface(id, desc);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (json.sharedContexts) {
|
|
281
|
+
for (const [name, config] of Object.entries(json.sharedContexts)) {
|
|
282
|
+
manifest.defineSharedContext(name, config);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (json.associations) {
|
|
287
|
+
for (const assoc of json.associations) {
|
|
288
|
+
manifest.addAssociation(assoc);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return manifest;
|
|
293
|
+
}
|
|
294
|
+
}
|
package/surface.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface — A dock host where typed objects connect and disconnect cleanly.
|
|
3
|
+
*
|
|
4
|
+
* A Surface is the runtime representation of a rendered UI region.
|
|
5
|
+
* It holds a data model, resolved params, and a registry of docked objects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Dockable } from './dockables/base.js';
|
|
9
|
+
|
|
10
|
+
/** The context object passed to every Dockable.dock() call. */
|
|
11
|
+
export interface SurfaceContext {
|
|
12
|
+
readonly surfaceId: string;
|
|
13
|
+
getElement(id: string): HTMLElement | null;
|
|
14
|
+
getRootElement(): HTMLElement;
|
|
15
|
+
getModel(path?: string): unknown;
|
|
16
|
+
setModel(path: string, value: unknown): void;
|
|
17
|
+
getDockable(kind: string, id: string): Dockable | null;
|
|
18
|
+
listDockables(kind?: string): Dockable[];
|
|
19
|
+
emit(adiaEvent: AdiaEvent): void;
|
|
20
|
+
getParam(key: string): unknown;
|
|
21
|
+
watchModel(path: string, fn: (value: unknown) => void): () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Event descriptor passed to Surface.context.emit(). */
|
|
25
|
+
export interface AdiaEvent {
|
|
26
|
+
event: string;
|
|
27
|
+
target?: string;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export declare class Surface {
|
|
32
|
+
/** The unique surface ID. */
|
|
33
|
+
readonly surfaceId: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param surfaceId - Unique surface identifier.
|
|
37
|
+
* @param rootElement - The surface's root DOM node.
|
|
38
|
+
* @param elements - Map of componentId → DOM element.
|
|
39
|
+
*/
|
|
40
|
+
constructor(
|
|
41
|
+
surfaceId: string,
|
|
42
|
+
rootElement: HTMLElement,
|
|
43
|
+
elements: Map<string, HTMLElement>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/** The surface context object passed to dockables. */
|
|
47
|
+
readonly context: SurfaceContext;
|
|
48
|
+
|
|
49
|
+
/** Dock a dockable, calling its dock(context) method. */
|
|
50
|
+
dock(dockable: Dockable): void;
|
|
51
|
+
|
|
52
|
+
/** Dock multiple dockables in dependency order (provider → controller → source → action → lifecycle). */
|
|
53
|
+
dockAll(dockables: Dockable[]): void;
|
|
54
|
+
|
|
55
|
+
/** Undock by id, calling cleanup then dockable.undock(). */
|
|
56
|
+
undock(id: string): void;
|
|
57
|
+
|
|
58
|
+
/** Undock everything in reverse dependency order. */
|
|
59
|
+
undockAll(): void;
|
|
60
|
+
|
|
61
|
+
/** Undock specific ids. */
|
|
62
|
+
undockMany(ids: string[]): void;
|
|
63
|
+
|
|
64
|
+
/** Set initial model state (before docking). */
|
|
65
|
+
setInitialModel(model: Record<string, unknown>): void;
|
|
66
|
+
|
|
67
|
+
/** Set resolved params. */
|
|
68
|
+
setParams(params: Record<string, unknown>): void;
|
|
69
|
+
|
|
70
|
+
/** Update the element map (after re-render). */
|
|
71
|
+
updateElements(elements: Map<string, HTMLElement>): void;
|
|
72
|
+
}
|
package/surface.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface — A dock host where typed objects connect and disconnect cleanly.
|
|
3
|
+
*
|
|
4
|
+
* A Surface is the runtime representation of a rendered UI region.
|
|
5
|
+
* It holds a data model, resolved params, and a registry of docked objects
|
|
6
|
+
* (controllers, data sources, actions, providers, lifecycle hooks).
|
|
7
|
+
*
|
|
8
|
+
* Dockables attach via dock(surface) and clean up via undock().
|
|
9
|
+
* The Surface doesn't know what dockables do internally — it just manages
|
|
10
|
+
* their lifecycle and provides a shared context.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ── Dock order (lowest docks first, undocks last) ──
|
|
14
|
+
const DOCK_ORDER = { provider: 0, controller: 1, source: 2, action: 3, lifecycle: 4 };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} DockEntry
|
|
18
|
+
* @property {import('./dockables/base.js').Dockable} dockable
|
|
19
|
+
* @property {Function|null} cleanup — returned from dock()
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export class Surface {
|
|
23
|
+
/** @type {string} */
|
|
24
|
+
surfaceId;
|
|
25
|
+
|
|
26
|
+
/** @type {Map<string, DockEntry>} keyed by dockable.id */
|
|
27
|
+
#docked = new Map();
|
|
28
|
+
|
|
29
|
+
/** @type {object} reactive-ish data model */
|
|
30
|
+
#model = {};
|
|
31
|
+
|
|
32
|
+
/** @type {object} resolved params (route, store, literal) */
|
|
33
|
+
#params = {};
|
|
34
|
+
|
|
35
|
+
/** @type {Map<string, Set<Function>>} model watchers keyed by path */
|
|
36
|
+
#watchers = new Map();
|
|
37
|
+
|
|
38
|
+
/** @type {HTMLElement} surface root element */
|
|
39
|
+
#rootElement;
|
|
40
|
+
|
|
41
|
+
/** @type {Map<string, HTMLElement>} componentId → DOM element */
|
|
42
|
+
#elements;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} surfaceId
|
|
46
|
+
* @param {HTMLElement} rootElement — the surface's root DOM node
|
|
47
|
+
* @param {Map<string, HTMLElement>} elements — componentId → element map
|
|
48
|
+
*/
|
|
49
|
+
constructor(surfaceId, rootElement, elements) {
|
|
50
|
+
this.surfaceId = surfaceId;
|
|
51
|
+
this.#rootElement = rootElement;
|
|
52
|
+
this.#elements = elements;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Context (passed to dockables) ─────────────────────────
|
|
56
|
+
|
|
57
|
+
/** @returns {SurfaceContext} */
|
|
58
|
+
get context() {
|
|
59
|
+
return {
|
|
60
|
+
surfaceId: this.surfaceId,
|
|
61
|
+
getElement: (id) => this.#elements.get(id) || null,
|
|
62
|
+
getRootElement: () => this.#rootElement,
|
|
63
|
+
getModel: (path) => path ? getPath(this.#model, path) : this.#model,
|
|
64
|
+
setModel: (path, value) => this.#setModel(path, value),
|
|
65
|
+
getDockable: (kind, id) => this.#getDockable(kind, id),
|
|
66
|
+
listDockables: (kind) => this.#listDockables(kind),
|
|
67
|
+
emit: (adiaEvent) => this.#emit(adiaEvent),
|
|
68
|
+
getParam: (key) => this.#params[key],
|
|
69
|
+
watchModel: (path, fn) => this.#watchModel(path, fn),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Dock Protocol ─────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Dock a new object. Calls dockable.dock(context).
|
|
77
|
+
* @param {import('./dockables/base.js').Dockable} dockable
|
|
78
|
+
*/
|
|
79
|
+
dock(dockable) {
|
|
80
|
+
// If same id already docked, redock (hot-swap)
|
|
81
|
+
if (this.#docked.has(dockable.id)) {
|
|
82
|
+
this.undock(dockable.id);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const cleanup = dockable.dock(this.context);
|
|
86
|
+
this.#docked.set(dockable.id, {
|
|
87
|
+
dockable,
|
|
88
|
+
cleanup: typeof cleanup === 'function' ? cleanup : null,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Dock multiple objects in dependency order.
|
|
94
|
+
* @param {import('./dockables/base.js').Dockable[]} dockables
|
|
95
|
+
*/
|
|
96
|
+
dockAll(dockables) {
|
|
97
|
+
const sorted = [...dockables].sort(
|
|
98
|
+
(a, b) => (DOCK_ORDER[a.kind] ?? 9) - (DOCK_ORDER[b.kind] ?? 9)
|
|
99
|
+
);
|
|
100
|
+
for (const d of sorted) this.dock(d);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Undock by id. Calls cleanup then dockable.undock().
|
|
105
|
+
* @param {string} id
|
|
106
|
+
*/
|
|
107
|
+
undock(id) {
|
|
108
|
+
const entry = this.#docked.get(id);
|
|
109
|
+
if (!entry) return;
|
|
110
|
+
entry.cleanup?.();
|
|
111
|
+
entry.dockable.undock();
|
|
112
|
+
this.#docked.delete(id);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Undock everything in reverse dependency order.
|
|
117
|
+
*/
|
|
118
|
+
undockAll() {
|
|
119
|
+
const entries = [...this.#docked.entries()].sort(
|
|
120
|
+
(a, b) => (DOCK_ORDER[b[1].dockable.kind] ?? 9) - (DOCK_ORDER[a[1].dockable.kind] ?? 9)
|
|
121
|
+
);
|
|
122
|
+
for (const [id] of entries) this.undock(id);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Undock specific ids.
|
|
127
|
+
* @param {string[]} ids
|
|
128
|
+
*/
|
|
129
|
+
undockMany(ids) {
|
|
130
|
+
for (const id of ids) this.undock(id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Model ─────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
/** Set initial model state (before docking). */
|
|
136
|
+
setInitialModel(model) {
|
|
137
|
+
Object.assign(this.#model, model);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Set resolved params. */
|
|
141
|
+
setParams(params) {
|
|
142
|
+
Object.assign(this.#params, params);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Update element map (after re-render). */
|
|
146
|
+
updateElements(elements) {
|
|
147
|
+
this.#elements = elements;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Private ───────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
#setModel(path, value) {
|
|
153
|
+
setPath(this.#model, path, value);
|
|
154
|
+
// Notify watchers for this path and any parent paths
|
|
155
|
+
for (const [watchPath, fns] of this.#watchers) {
|
|
156
|
+
if (path === watchPath || path.startsWith(watchPath + '/')) {
|
|
157
|
+
for (const fn of fns) fn(getPath(this.#model, watchPath));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#watchModel(path, fn) {
|
|
163
|
+
if (!this.#watchers.has(path)) this.#watchers.set(path, new Set());
|
|
164
|
+
this.#watchers.get(path).add(fn);
|
|
165
|
+
return () => {
|
|
166
|
+
const set = this.#watchers.get(path);
|
|
167
|
+
set?.delete(fn);
|
|
168
|
+
if (set?.size === 0) this.#watchers.delete(path);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#getDockable(kind, id) {
|
|
173
|
+
const entry = this.#docked.get(id);
|
|
174
|
+
if (entry && entry.dockable.kind === kind) return entry.dockable;
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#listDockables(kind) {
|
|
179
|
+
const result = [];
|
|
180
|
+
for (const { dockable } of this.#docked.values()) {
|
|
181
|
+
if (!kind || dockable.kind === kind) result.push(dockable);
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#emit(adiaEvent) {
|
|
187
|
+
const target = adiaEvent.target
|
|
188
|
+
? this.#elements.get(adiaEvent.target)
|
|
189
|
+
: this.#rootElement;
|
|
190
|
+
if (!target) return;
|
|
191
|
+
|
|
192
|
+
target.dispatchEvent(new CustomEvent(adiaEvent.event, {
|
|
193
|
+
bubbles: true,
|
|
194
|
+
detail: adiaEvent,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── JSON Pointer helpers (simplified, "/" delimited) ────────
|
|
200
|
+
|
|
201
|
+
function getPath(obj, path) {
|
|
202
|
+
if (!path || path === '/') return obj;
|
|
203
|
+
const keys = path.replace(/^\//, '').split('/');
|
|
204
|
+
let current = obj;
|
|
205
|
+
for (const key of keys) {
|
|
206
|
+
if (current == null) return undefined;
|
|
207
|
+
current = current[key];
|
|
208
|
+
}
|
|
209
|
+
return current;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function setPath(obj, path, value) {
|
|
213
|
+
if (!path || path === '/') return;
|
|
214
|
+
const keys = path.replace(/^\//, '').split('/');
|
|
215
|
+
let current = obj;
|
|
216
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
217
|
+
const key = keys[i];
|
|
218
|
+
if (current[key] == null) current[key] = {};
|
|
219
|
+
current = current[key];
|
|
220
|
+
}
|
|
221
|
+
current[keys[keys.length - 1]] = value;
|
|
222
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// AUTO-GENERATED re-export: runtime/types.d.ts now delegates to the schema codegen.
|
|
2
|
+
// The schema (a2ui.schema.json) is the authoritative source for A2UIMessage shapes.
|
|
3
|
+
// Regenerate with: node scripts/build/a2ui-schema-types.mjs
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
A2UIMessage,
|
|
7
|
+
A2UIComponent,
|
|
8
|
+
CreateSurfaceMessage,
|
|
9
|
+
UpdateComponentsMessage,
|
|
10
|
+
UpdateDataModelMessage,
|
|
11
|
+
WireComponentsMessage,
|
|
12
|
+
WireAction,
|
|
13
|
+
WireDataSource,
|
|
14
|
+
WireState,
|
|
15
|
+
DeleteSurfaceMessage,
|
|
16
|
+
MetaMessage,
|
|
17
|
+
} from './a2ui.schema.js';
|
|
18
|
+
|
|
19
|
+
/** String literal union of all valid A2UI message type discriminators. */
|
|
20
|
+
export type A2UIMessageType =
|
|
21
|
+
| 'createSurface'
|
|
22
|
+
| 'updateComponents'
|
|
23
|
+
| 'updateDataModel'
|
|
24
|
+
| 'wireComponents'
|
|
25
|
+
| 'deleteSurface'
|
|
26
|
+
| 'meta';
|