@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +1073 -0
  2. package/README.md +99 -0
  3. package/a2ui.schema.d.ts +192 -0
  4. package/controllers/accordion.js +73 -0
  5. package/controllers/base.js +68 -0
  6. package/controllers/data-stream.js +281 -0
  7. package/controllers/form.js +81 -0
  8. package/controllers/index.js +6 -0
  9. package/controllers/selection.js +82 -0
  10. package/controllers/state-machine.js +135 -0
  11. package/controllers/toggle.js +40 -0
  12. package/dockables/action.d.ts +55 -0
  13. package/dockables/action.js +152 -0
  14. package/dockables/base.d.ts +26 -0
  15. package/dockables/base.js +30 -0
  16. package/dockables/controller.d.ts +35 -0
  17. package/dockables/controller.js +97 -0
  18. package/dockables/data-source.d.ts +35 -0
  19. package/dockables/data-source.js +103 -0
  20. package/dockables/index.d.ts +21 -0
  21. package/dockables/index.js +6 -0
  22. package/dockables/lifecycle.d.ts +38 -0
  23. package/dockables/lifecycle.js +84 -0
  24. package/dockables/provider.d.ts +28 -0
  25. package/dockables/provider.js +59 -0
  26. package/index.d.ts +64 -0
  27. package/index.js +54 -0
  28. package/package.json +89 -0
  29. package/prop-apply.d.ts +13 -0
  30. package/prop-apply.js +113 -0
  31. package/registry.d.ts +17 -0
  32. package/registry.js +418 -0
  33. package/renderer.d.ts +67 -0
  34. package/renderer.js +715 -0
  35. package/stream.d.ts +62 -0
  36. package/stream.js +521 -0
  37. package/surface-manifest.d.ts +73 -0
  38. package/surface-manifest.js +294 -0
  39. package/surface.d.ts +72 -0
  40. package/surface.js +222 -0
  41. package/types.d.ts +26 -0
  42. package/validate/CHANGELOG.md +1005 -0
  43. package/validate/README.md +146 -0
  44. package/validate/index.d.ts +4 -0
  45. package/validate/index.js +12 -0
  46. package/validate/validator.d.ts +4 -0
  47. package/validate/validator.js +1232 -0
  48. package/wire-factory.d.ts +15 -0
  49. package/wire-factory.js +134 -0
  50. package/wiring-engine.d.ts +61 -0
  51. package/wiring-engine.js +209 -0
  52. package/wiring-registry.d.ts +80 -0
  53. package/wiring-registry.js +342 -0
@@ -0,0 +1,81 @@
1
+ import { BaseController } from './base.js';
2
+
3
+ /**
4
+ * Form controller — tracks field values, validation, and dirty state.
5
+ * Sets [data-form-valid], [data-form-invalid], [data-form-dirty] on host.
6
+ */
7
+ export class FormController extends BaseController {
8
+ static schema = Object.freeze({
9
+ name: 'form',
10
+ state: { values: 'object', errors: 'object', dirty: 'boolean', valid: 'boolean' },
11
+ commands: ['set', 'validate', 'reset', 'setInitial'],
12
+ attributes: ['data-form-valid', 'data-form-invalid', 'data-form-dirty'],
13
+ });
14
+
15
+ #initial = {};
16
+ #values = {};
17
+ #errors = {};
18
+ #validators = {};
19
+
20
+ constructor(initial = {}) {
21
+ super();
22
+ this.#initial = { ...initial };
23
+ this.#values = { ...initial };
24
+ }
25
+
26
+ onDisconnect(host) {
27
+ host.removeAttribute('data-form-valid');
28
+ host.removeAttribute('data-form-invalid');
29
+ host.removeAttribute('data-form-dirty');
30
+ }
31
+
32
+ getState() {
33
+ const dirty = Object.keys(this.#values).some(k => this.#values[k] !== this.#initial[k]);
34
+ const valid = Object.keys(this.#errors).length === 0;
35
+ return { values: { ...this.#values }, errors: { ...this.#errors }, dirty, valid };
36
+ }
37
+
38
+ reflect() {
39
+ if (!this.host) return;
40
+ const { dirty, valid } = this.getState();
41
+ if (valid) {
42
+ this.host.setAttribute('data-form-valid', '');
43
+ this.host.removeAttribute('data-form-invalid');
44
+ } else {
45
+ this.host.setAttribute('data-form-invalid', '');
46
+ this.host.removeAttribute('data-form-valid');
47
+ }
48
+ if (dirty) this.host.setAttribute('data-form-dirty', '');
49
+ else this.host.removeAttribute('data-form-dirty');
50
+ }
51
+
52
+ #runValidation(field) {
53
+ const fn = this.#validators[field];
54
+ if (!fn) { delete this.#errors[field]; return; }
55
+ const result = fn(this.#values[field]);
56
+ if (result === true || result == null) delete this.#errors[field];
57
+ else this.#errors[field] = typeof result === 'string' ? result : 'Invalid';
58
+ }
59
+
60
+ commands = {
61
+ set: (field, value) => {
62
+ this.#values[field] = value;
63
+ this.#runValidation(field);
64
+ this.notify();
65
+ },
66
+ validate: (field, fn) => {
67
+ this.#validators[field] = fn;
68
+ this.#runValidation(field);
69
+ this.notify();
70
+ },
71
+ reset: () => {
72
+ this.#values = { ...this.#initial };
73
+ this.#errors = {};
74
+ for (const field of Object.keys(this.#validators)) this.#runValidation(field);
75
+ this.notify();
76
+ },
77
+ setInitial: (values) => {
78
+ this.#initial = { ...values };
79
+ },
80
+ };
81
+ }
@@ -0,0 +1,6 @@
1
+ export { BaseController } from './base.js';
2
+ export { ToggleController } from './toggle.js';
3
+ export { SelectionController } from './selection.js';
4
+ export { FormController } from './form.js';
5
+ export { AccordionController } from './accordion.js';
6
+ export { DataStreamController } from './data-stream.js';
@@ -0,0 +1,82 @@
1
+ import { BaseController } from './base.js';
2
+
3
+ /**
4
+ * Selection controller — manages single or multi-select state.
5
+ * Sets [data-selection-active] on host, [data-selection-selected] on items.
6
+ */
7
+ export class SelectionController extends BaseController {
8
+ static schema = Object.freeze({
9
+ name: 'selection',
10
+ state: { selected: 'Set', multiple: 'boolean' },
11
+ commands: ['select', 'deselect', 'toggle', 'clear', 'selectAll'],
12
+ attributes: ['data-selection-active', 'data-selection-selected'],
13
+ });
14
+
15
+ #selected = new Set();
16
+ #multiple = false;
17
+
18
+ constructor({ multiple = false, initial = [] } = {}) {
19
+ super();
20
+ this.#multiple = multiple;
21
+ for (const key of initial) this.#selected.add(String(key));
22
+ }
23
+
24
+ onConnect(host) {
25
+ host.setAttribute('data-selection-active', '');
26
+ }
27
+
28
+ onDisconnect(host) {
29
+ this.#clearReflection(host);
30
+ host.removeAttribute('data-selection-active');
31
+ }
32
+
33
+ getState() {
34
+ return { selected: new Set(this.#selected), multiple: this.#multiple };
35
+ }
36
+
37
+ reflect() {
38
+ if (!this.host) return;
39
+ const items = this.host.querySelectorAll('[data-selection-key]');
40
+ for (const item of items) {
41
+ const key = item.getAttribute('data-selection-key');
42
+ if (this.#selected.has(key)) item.setAttribute('data-selection-selected', '');
43
+ else item.removeAttribute('data-selection-selected');
44
+ }
45
+ }
46
+
47
+ #clearReflection(host) {
48
+ const items = host.querySelectorAll('[data-selection-selected]');
49
+ for (const item of items) item.removeAttribute('data-selection-selected');
50
+ }
51
+
52
+ commands = {
53
+ select: (key) => {
54
+ key = String(key);
55
+ if (!this.#multiple) this.#selected.clear();
56
+ this.#selected.add(key);
57
+ this.notify();
58
+ },
59
+ deselect: (key) => {
60
+ this.#selected.delete(String(key));
61
+ this.notify();
62
+ },
63
+ toggle: (key) => {
64
+ key = String(key);
65
+ if (this.#selected.has(key)) this.#selected.delete(key);
66
+ else {
67
+ if (!this.#multiple) this.#selected.clear();
68
+ this.#selected.add(key);
69
+ }
70
+ this.notify();
71
+ },
72
+ clear: () => {
73
+ this.#selected.clear();
74
+ this.notify();
75
+ },
76
+ selectAll: (keys) => {
77
+ if (!this.#multiple) return;
78
+ for (const k of keys) this.#selected.add(String(k));
79
+ this.notify();
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * StateMachineController — a minimal XState-lite state chart for A2UI wiring.
3
+ *
4
+ * The LLM declares states + transitions in wireComponents. The runtime
5
+ * tracks current state, exposes it to CSS via `[data-state="…"]` on the
6
+ * host, and fires `stateEntered` / `stateExited` events. Transitions are
7
+ * triggered via commands (`send` named events or `transition` to a state).
8
+ *
9
+ * Goals (vs. the rest of the wiring system):
10
+ * - Expands the LLM's output surface beyond pure structure: it can emit a
11
+ * multi-step wizard, a validation flow, a loading/ready/error trio, etc.,
12
+ * without writing behavior code.
13
+ * - Declarative only — the catalog schema bounds what a state machine can
14
+ * express. No arbitrary JS. Side effects are wiring actions (like every
15
+ * other AdiaUI interactive surface).
16
+ *
17
+ * What's supported in this first pass (subset of statechart semantics):
18
+ * - Finite states with named transitions (`on: { NEXT: "review" }`)
19
+ * - Initial state (`initial: "welcome"`)
20
+ * - `send(event)` to fire a transition
21
+ * - `transition(state)` to jump to a state unconditionally
22
+ * - CSS reflection via `data-state` on the host
23
+ * - Entry/exit custom events
24
+ *
25
+ * Not yet (deliberately): guards, actions on transitions, hierarchical
26
+ * states, parallel states, history. Add when real exemplars demand them.
27
+ */
28
+
29
+ import { BaseController } from './base.js';
30
+
31
+ export class StateMachineController extends BaseController {
32
+ static schema = {
33
+ name: 'state-machine',
34
+ state: {
35
+ current: 'string',
36
+ history: 'string[]',
37
+ },
38
+ commands: ['send', 'transition', 'reset'],
39
+ attributes: ['data-state'],
40
+ config: {
41
+ initial: 'string',
42
+ states: 'object', // { [name]: { on: { [event]: targetState } } }
43
+ },
44
+ };
45
+
46
+ #current = '';
47
+ #initial = '';
48
+ #states = {};
49
+ #history = [];
50
+
51
+ onConnect(host) {
52
+ // Config read from the controller declaration. Falls back to any
53
+ // data-initial/data-states on the host for hand-authored surfaces.
54
+ const config = this.config || {};
55
+ this.#initial = config.initial
56
+ || host.dataset?.initial
57
+ || Object.keys(config.states || {})[0]
58
+ || '';
59
+ this.#states = this._validateStates(config.states || {});
60
+ this.#current = this.#initial;
61
+ this.#history = this.#current ? [this.#current] : [];
62
+ if (this.#current) this._emit('stateEntered', { state: this.#current, initial: true });
63
+ }
64
+
65
+ /** Public state. */
66
+ getState() {
67
+ return { current: this.#current, history: [...this.#history] };
68
+ }
69
+
70
+ reflect() {
71
+ if (!this.host) return;
72
+ if (this.#current) this.host.setAttribute('data-state', this.#current);
73
+ else this.host.removeAttribute('data-state');
74
+ }
75
+
76
+ commands = {
77
+ /** Fire a named event; if current state defines a transition for it, move. */
78
+ send: (eventName) => {
79
+ if (!eventName || !this.#current) return;
80
+ const transitions = this.#states[this.#current]?.on || {};
81
+ const target = transitions[eventName];
82
+ if (!target) return; // unknown event in this state — silent no-op
83
+ this._moveTo(target, { event: eventName });
84
+ },
85
+
86
+ /** Unconditional jump to a declared state. */
87
+ transition: (target) => {
88
+ if (!target || !(target in this.#states)) return;
89
+ this._moveTo(target, { forced: true });
90
+ },
91
+
92
+ /** Return to initial state. Fires exit + enter events. */
93
+ reset: () => {
94
+ if (!this.#initial) return;
95
+ this._moveTo(this.#initial, { reset: true });
96
+ },
97
+ };
98
+
99
+ // ── internals ──
100
+
101
+ _moveTo(target, detail) {
102
+ if (target === this.#current) return;
103
+ const prev = this.#current;
104
+ if (prev) this._emit('stateExited', { state: prev, target, ...detail });
105
+ this.#current = target;
106
+ this.#history.push(target);
107
+ this.notify(); // triggers reflect() + subscribers
108
+ this._emit('stateEntered', { state: target, from: prev, ...detail });
109
+ }
110
+
111
+ _emit(name, detail) {
112
+ if (!this.host) return;
113
+ this.host.dispatchEvent(new CustomEvent(name, { bubbles: true, detail }));
114
+ }
115
+
116
+ _validateStates(states) {
117
+ // Shallow schema check: each state entry is an object; `on` is an object
118
+ // mapping event names → target state strings that exist in the map.
119
+ const out = {};
120
+ const names = Object.keys(states);
121
+ for (const name of names) {
122
+ const def = states[name] || {};
123
+ const on = {};
124
+ for (const [ev, target] of Object.entries(def.on || {})) {
125
+ if (typeof target !== 'string' || !(target in states)) {
126
+ console.warn(`[state-machine] state "${name}" transition "${ev}" → "${target}" — target not declared`);
127
+ continue;
128
+ }
129
+ on[ev] = target;
130
+ }
131
+ out[name] = { on };
132
+ }
133
+ return out;
134
+ }
135
+ }
@@ -0,0 +1,40 @@
1
+ import { BaseController } from './base.js';
2
+
3
+ /**
4
+ * Toggle controller — manages on/off state.
5
+ * Sets [data-toggle-on] on host when active.
6
+ */
7
+ export class ToggleController extends BaseController {
8
+ static schema = Object.freeze({
9
+ name: 'toggle',
10
+ state: { on: 'boolean' },
11
+ commands: ['toggle', 'set'],
12
+ attributes: ['data-toggle-on'],
13
+ });
14
+
15
+ #on = false;
16
+
17
+ constructor(initial = false) {
18
+ super();
19
+ this.#on = initial;
20
+ }
21
+
22
+ getState() {
23
+ return { on: this.#on };
24
+ }
25
+
26
+ onDisconnect(host) {
27
+ host.removeAttribute('data-toggle-on');
28
+ }
29
+
30
+ reflect() {
31
+ if (!this.host) return;
32
+ if (this.#on) this.host.setAttribute('data-toggle-on', '');
33
+ else this.host.removeAttribute('data-toggle-on');
34
+ }
35
+
36
+ commands = {
37
+ toggle: () => { this.#on = !this.#on; this.notify(); },
38
+ set: (v) => { this.#on = !!v; this.notify(); },
39
+ };
40
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * ActionDock — Binds a UIEvent on a component to a handler function.
3
+ */
4
+
5
+ import type { Dockable } from './base.js';
6
+ import type { SurfaceContext } from '../surface.js';
7
+
8
+ /** Mapping of A2UI semantic event names to DOM event names. */
9
+ export declare const A2UI_EVENT_TO_DOM: Record<string, string>;
10
+
11
+ export interface UIEventDecl {
12
+ /** A2UI semantic event name (press, submit, input, change, …). */
13
+ event: string;
14
+ /** Component ID to listen on. Defaults to the surface root. */
15
+ target?: string;
16
+ /** Debounce delay in ms. */
17
+ debounce?: number;
18
+ /** Throttle interval in ms. */
19
+ throttle?: number;
20
+ /** Optional condition guard. */
21
+ condition?: {
22
+ path: string;
23
+ equals?: unknown;
24
+ notEquals?: unknown;
25
+ exists?: boolean;
26
+ };
27
+ }
28
+
29
+ export interface ActionDecl {
30
+ /** DOM event descriptor. */
31
+ event: UIEventDecl;
32
+ /** Handler name from the wiring registry. */
33
+ handler: string;
34
+ config?: Record<string, unknown>;
35
+ onSuccess?: Array<{ handler: string; config?: Record<string, unknown> }> | null;
36
+ onError?: Array<{ handler: string; config?: Record<string, unknown> }> | null;
37
+ }
38
+
39
+ export declare class ActionDock extends Dockable {
40
+ readonly kind: 'action';
41
+ readonly id: string;
42
+ readonly event: UIEventDecl;
43
+ readonly handler: string;
44
+ readonly config: Record<string, unknown>;
45
+ readonly onSuccess: Array<{ handler: string; config?: Record<string, unknown> }> | null;
46
+ readonly onError: Array<{ handler: string; config?: Record<string, unknown> }> | null;
47
+
48
+ constructor(
49
+ decl: ActionDecl,
50
+ resolveHandler: (name: string) => ((config: unknown, ctx: SurfaceContext, event: unknown) => Promise<unknown>) | null,
51
+ );
52
+
53
+ dock(ctx: SurfaceContext): (() => void) | void;
54
+ undock(): void;
55
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * ActionDock — Binds a UIEvent on a component to a handler.
3
+ *
4
+ * The UIEvent is a typed object: { event, target?, debounce?, throttle?, condition? }
5
+ * When the DOM event fires, the handler runs with the surface context.
6
+ * onSuccess/onError chains run follow-up handlers based on the outcome.
7
+ */
8
+ import { Dockable } from './base.js';
9
+
10
+ /**
11
+ * UIEvent type → DOM event name.
12
+ * AdiaUI components emit semantic events; these map to the actual DOM event to listen on.
13
+ */
14
+ const A2UI_EVENT_TO_DOM = {
15
+ press: 'click',
16
+ submit: 'submit',
17
+ input: 'input',
18
+ change: 'change',
19
+ select: 'select',
20
+ toggle: 'change',
21
+ dismiss: 'close',
22
+ navigate: 'click',
23
+ mount: 'connectedCallback',
24
+ unmount: 'disconnectedCallback',
25
+ focus: 'focusin',
26
+ blur: 'focusout',
27
+ drag: 'dragstart',
28
+ drop: 'drop',
29
+ };
30
+
31
+ export { A2UI_EVENT_TO_DOM };
32
+
33
+ export class ActionDock extends Dockable {
34
+ kind = 'action';
35
+
36
+ /** @type {string} */
37
+ id;
38
+
39
+ /** @type {{ event: string, target?: string, debounce?: number, throttle?: number, condition?: object }} */
40
+ event;
41
+
42
+ /** @type {string} handler name */
43
+ handler;
44
+
45
+ /** @type {object} handler config */
46
+ config;
47
+
48
+ /** @type {Array|null} follow-up actions on success */
49
+ onSuccess;
50
+
51
+ /** @type {Array|null} follow-up actions on error */
52
+ onError;
53
+
54
+ /** @type {Function} (handlerName) => handlerFn */
55
+ #resolveHandler;
56
+
57
+ /**
58
+ * @param {object} decl — { event, handler, config?, onSuccess?, onError? }
59
+ * @param {Function} resolveHandler — (name) => async (config, ctx) => result
60
+ */
61
+ constructor(decl, resolveHandler) {
62
+ super();
63
+ this.event = decl.event;
64
+ this.handler = decl.handler;
65
+ this.config = decl.config || {};
66
+ this.onSuccess = decl.onSuccess || null;
67
+ this.onError = decl.onError || null;
68
+ this.#resolveHandler = resolveHandler;
69
+
70
+ // Auto-generate id from event + target
71
+ this.id = `action:${this.event.event}:${this.event.target || 'root'}:${this.handler}`;
72
+ }
73
+
74
+ dock(ctx) {
75
+ const target = this.event.target
76
+ ? ctx.getElement(this.event.target)
77
+ : ctx.getRootElement();
78
+
79
+ if (!target) {
80
+ console.warn(`ActionDock: target "${this.event.target}" not found`);
81
+ return;
82
+ }
83
+
84
+ const domEventName = A2UI_EVENT_TO_DOM[this.event.event] || this.event.event;
85
+ const handlerFn = this.#resolveHandler(this.handler);
86
+
87
+ if (!handlerFn) {
88
+ console.warn(`ActionDock: unknown handler "${this.handler}"`);
89
+ return;
90
+ }
91
+
92
+ let listener = async (domEvent) => {
93
+ // Condition guard
94
+ if (this.event.condition) {
95
+ const val = ctx.getModel(this.event.condition.path);
96
+ if ('equals' in this.event.condition && val !== this.event.condition.equals) return;
97
+ if ('notEquals' in this.event.condition && val === this.event.condition.notEquals) return;
98
+ if ('exists' in this.event.condition && this.event.condition.exists && val == null) return;
99
+ }
100
+
101
+ try {
102
+ const result = await handlerFn(this.config, ctx, domEvent);
103
+
104
+ // Run onSuccess chain
105
+ if (this.onSuccess) {
106
+ for (const follow of this.onSuccess) {
107
+ const followFn = this.#resolveHandler(follow.handler);
108
+ if (followFn) await followFn(follow.config || {}, ctx, result);
109
+ }
110
+ }
111
+ } catch (err) {
112
+ // Run onError chain
113
+ if (this.onError) {
114
+ for (const follow of this.onError) {
115
+ const followFn = this.#resolveHandler(follow.handler);
116
+ if (followFn) await followFn(follow.config || {}, ctx, err);
117
+ }
118
+ } else {
119
+ console.error(`ActionDock: handler "${this.handler}" failed`, err);
120
+ }
121
+ }
122
+ };
123
+
124
+ // Debounce
125
+ if (this.event.debounce > 0) {
126
+ const origListener = listener;
127
+ let timer;
128
+ listener = (...args) => {
129
+ clearTimeout(timer);
130
+ timer = setTimeout(() => origListener(...args), this.event.debounce);
131
+ };
132
+ }
133
+
134
+ // Throttle
135
+ if (this.event.throttle > 0) {
136
+ const origListener = listener;
137
+ let last = 0;
138
+ listener = (...args) => {
139
+ const now = Date.now();
140
+ if (now - last >= this.event.throttle) {
141
+ last = now;
142
+ origListener(...args);
143
+ }
144
+ };
145
+ }
146
+
147
+ target.addEventListener(domEventName, listener);
148
+ return () => target.removeEventListener(domEventName, listener);
149
+ }
150
+
151
+ undock() {}
152
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Dockable — Base class for objects that attach to a Surface.
3
+ */
4
+
5
+ import type { SurfaceContext } from '../surface.js';
6
+
7
+ /** The kinds of dockable objects. */
8
+ export type DockableKind = 'controller' | 'source' | 'action' | 'provider' | 'lifecycle';
9
+
10
+ export declare abstract class Dockable {
11
+ /** Port type — determines dock ordering. */
12
+ abstract readonly kind: DockableKind;
13
+
14
+ /** Unique identifier within the surface. */
15
+ abstract readonly id: string;
16
+
17
+ /**
18
+ * Attach to a surface. Return a cleanup function, or void.
19
+ */
20
+ dock(ctx: SurfaceContext): (() => void) | void;
21
+
22
+ /**
23
+ * Detach from the surface. The cleanup fn from dock() is called first.
24
+ */
25
+ undock(): void;
26
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Dockable — Base class for objects that attach to a Surface.
3
+ *
4
+ * Every dockable has:
5
+ * kind — which port type it uses (controller, source, action, provider, lifecycle)
6
+ * id — unique identity within the surface
7
+ * dock(context) — attach and return cleanup function
8
+ * undock() — teardown
9
+ */
10
+ export class Dockable {
11
+ /** @type {'controller'|'source'|'action'|'provider'|'lifecycle'} */
12
+ get kind() { throw new Error('Dockable subclass must define kind'); }
13
+
14
+ /** @type {string} */
15
+ get id() { throw new Error('Dockable subclass must define id'); }
16
+
17
+ /**
18
+ * Attach to a surface. Receives the surface context.
19
+ * Return a cleanup function, or void.
20
+ * @param {import('../surface.js').SurfaceContext} ctx
21
+ * @returns {(() => void)|void}
22
+ */
23
+ dock(ctx) { throw new Error('Dockable subclass must implement dock()'); }
24
+
25
+ /**
26
+ * Detach from the surface. Cleanup fn from dock() is called
27
+ * automatically before this.
28
+ */
29
+ undock() {}
30
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ControllerDock — Wraps an AdiaUI controller as a dockable.
3
+ */
4
+
5
+ import type { Dockable } from './base.js';
6
+ import type { SurfaceContext } from '../surface.js';
7
+
8
+ export interface ControllerDecl {
9
+ id: string;
10
+ /** Controller type name, e.g. 'FormController'. */
11
+ type: string;
12
+ /** Component ID of the host element to attach to. */
13
+ host: string;
14
+ config?: Record<string, unknown>;
15
+ /** Model path bindings: { stateKey: '/model/path' } */
16
+ bind?: Record<string, string> | null;
17
+ }
18
+
19
+ export declare class ControllerDock extends Dockable {
20
+ readonly kind: 'controller';
21
+ readonly id: string;
22
+ readonly type: string;
23
+ readonly hostId: string;
24
+ readonly config: Record<string, unknown>;
25
+ readonly bind: Record<string, string> | null;
26
+ controller: unknown | null;
27
+
28
+ constructor(
29
+ decl: ControllerDecl,
30
+ resolveClass: (type: string) => Promise<(new (...args: unknown[]) => unknown) | null>,
31
+ );
32
+
33
+ dock(ctx: SurfaceContext): Promise<(() => void) | void>;
34
+ undock(): void;
35
+ }