@veil-runtime/ui 0.4.1 → 0.5.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.
@@ -1,4 +1,6 @@
1
- # Universal interface foundation (0.3.0-rc.2 Developer Preview candidate)
1
+ # Universal interface foundation (v0.5.0 Developer Preview candidate)
2
+
3
+ The `/interface` contract and optional `/dom` adapter work without Veil Core. The package declares Core as an optional peer with range `^0.4.0` for legacy capability integrations; no Core v0.6.0 compatibility is claimed here. The [v0.4.0 contract guide](v0.4.0.md) documents validation, input, and condition additions retained from the published v0.4.2 baseline.
2
4
 
3
5
  ## Smallest working interface
4
6
 
@@ -58,9 +60,11 @@ Every update follows one acceptance path: validate and copy the proposed definit
58
60
 
59
61
  ## Architecture and alternatives
60
62
 
61
- The RC's `ComposedApplication` is a good focused form controller: it validates field schemas, submits one action at a time, and protects late results. Its required `Form`/`Field`/`Submit` nesting cannot express arbitrary UI. `InterfaceDefinition` keeps the separation of plain data and trusted host callbacks, and adds a small node tree instead of extending the form schema with unrelated cases. This is a separate public API for broader experiences; composition and v0.1 capability graph APIs remain unchanged.
63
+ The separate `ComposedApplication` API is a focused form controller: it validates field schemas, submits one action at a time, and protects late results. Its required `Form`/`Field`/`Submit` nesting cannot express arbitrary UI. `InterfaceDefinition` keeps the separation of plain data and trusted host callbacks, and adds a small node tree instead of extending the form schema with unrelated cases. Composition and v0.1 capability graph APIs remain available.
64
+
65
+ Nodes have stable unique keys, a component name, and optional children. The built-ins are `Stack`, `Row`, `Grid`, `Text`, `Heading`, `Status`, `Input`, `Select`, `Textarea`, `Button`, `Table`, and `List`. `Stack` and wrapping `Row` nest layouts. `Heading` renders an `h2`; `Status` has a polite live region; `List` renders up to 100 scalar items. `Table` presents up to 100 rows and 20 columns. Text, list, and table values use one path binding from `data` or `state`. `Input`, `Select`, and `Textarea` write scalar state. Their optional declarative validation uses `required`, `minLength`, `maxLength`, `min`, and `max` where applicable. `getInterfaceValidation(snapshot)` reports errors for visible, enabled fields. Disabled fields retain state and skip validation. A Button with `validate: true` blocks dispatch when visible fields are invalid; other Buttons retain their existing behavior. `Button` emits `{ action, source, state, revision }` after an accepted dispatch. Table row actions add an opaque `rowId`; see [Interactive Tables](INTERACTIVE-COLLECTIONS-0.5.0.md).
62
66
 
63
- Nodes have stable unique keys, a component name, and optional children. The built-ins are `Stack`, `Row`, `Text`, `Heading`, `Status`, `Input`, `Button`, `Table`, and `List`. `Stack` and wrapping `Row` nest layouts. `Heading` renders an `h2`; `Status` has a polite live region; `List` renders up to 100 scalar items. `Table` presents up to 100 rows and 20 columns. Text, list, and table values use one path binding from `data` or `state`. An optional equality condition controls visibility. `Input` writes scalar state. `Button` emits `{ action, source, state, revision }`. Host code registers trusted DOM components by passing their names to the controller and implementations to `mountInterface`. Registrations are local to an instance.
67
+ All nodes accept a `visible` condition; Buttons and fields also accept `disabled`; text/number inputs and textareas support `readOnly`. Conditions support strict equality, `ne`, numeric `lt`/`lte`/`gt`/`gte`, and bounded `all`/`any`/`not` combinations. Hidden ancestors and disabled Buttons cannot dispatch actions. These conditions and validation rules do not authorize effects; the host still owns action policy and service-side checks. See [v0.4.0.md](v0.4.0.md) and [the validated example](../examples/universal/validated.mjs) for exact rules. Host code registers trusted DOM components by passing their names to the controller and implementations to `mountInterface`. Registrations are local to an instance.
64
68
 
65
69
  `getInterfaceCapabilities()` returns an isolated JSON-safe manifest: component names, allowed and required properties, child composition, events, property types, binding semantics, and limits. The built-in validator uses the same component property lists. An agent host can serialize the manifest alongside its own allowed action IDs, ask a provider-neutral agent for JSON, cap bytes before parsing, and call `validateInterfaceDefinition` before `createMountedInterface` or `updateDefinition`. Component properties that do not belong to a built-in are rejected. `BuiltinInterfaceDefinition` supports TypeScript `satisfies` checks for built-in-only authored screens; the runtime validator remains mandatory for received JSON. The manifest intentionally excludes trusted custom factories and privileged actions. A host may add a trusted component after receiving the generated definition, as the agent demonstration does with `Activity`.
66
70
 
@@ -82,24 +86,28 @@ const unmount = mountInterface(target, controller, components);
82
86
  // Teardown: unmount(); controller.dispose();
83
87
  ```
84
88
 
85
- Alternatives considered: extending the form schema in place would weaken its focused validation and compatibility; accepting HTML or generated framework code would grant definitions executable behavior; a full virtual DOM or application framework adds scope before use cases justify it. This foundation chooses a bounded tree and explicit event boundary. More complex layout, row actions, repeat templates, validation vocabulary, and navigation state need later designs.
89
+ Alternatives considered: extending the form schema in place would weaken its focused validation and compatibility; accepting HTML or generated framework code would grant definitions executable behavior; a full virtual DOM or application framework adds scope before use cases justify it. This foundation chooses a bounded tree and explicit event boundary. Responsive Grid, row actions, richer forms and phased Status are specified in the [v0.5.0 release notes](RELEASE-0.5.0.md). Repeat templates, asynchronous validation and navigation state remain outside this contract.
86
90
 
87
91
  ## Trust boundary and limits
88
92
 
89
- Treat model responses as untrusted bytes. The host must cap bytes before `JSON.parse` (the example uses 64 KiB). The runtime rejects cyclic data, accessors, unsafe property names, unsupported values, unknown node properties and components, undeclared actions, excessive depth and size, malformed bindings, table rows over 100, and invalid input state. Interface trees are limited to 512 nodes and depth 16; the underlying transport limit is 1 MiB. Text is assigned with `textContent`, never `innerHTML`. `allowedActions` narrows action IDs accepted from a definition. These checks do not grant authority: the host must authenticate, authorize, validate inputs on the server, and execute external effects. Trusted component code is outside the untrusted definition boundary.
93
+ Treat model responses as untrusted bytes. The host must cap bytes before `JSON.parse` (the example uses 64 KiB). The runtime rejects cyclic data, accessors, unsafe property names, unsupported values, unknown node properties and components, undeclared actions, excessive depth and size, malformed bindings, table rows over 100, and invalid input state. Interface trees are limited to 512 nodes and 15 child edges; aggregate traversal may reject shallower definitions. The conservative accounting budget is 1 MiB, independently applied to definition, data and state. See [capability limits](INTERFACE-CAPABILITIES-0.5.0.md); this does not bound raw JSON bytes before parsing. Text is assigned with `textContent`, never `innerHTML`. `allowedActions` narrows action IDs accepted from a definition. These checks do not grant authority: the host must authenticate, authorize, validate inputs on the server, and execute external effects. Trusted component code is outside the untrusted definition boundary.
90
94
 
91
- Subscriptions and DOM listeners are removed on disposal. Disposed controllers ignore later data updates; revision checks help hosts reject stale asynchronous responses. Events are delivered synchronously in interaction order with a copy of the current state. Reentrant dispatch is ignored, but repeated later clicks are separate events. The host must deduplicate or serialize effects, check revisions when applying asynchronous results, and authorize every capability independently. Hidden actions cannot be dispatched. Listener feedback is capped at 32 passes per update. Failed DOM preparation calls the mount's `onError` and causes the initiating update to throw. Errors from ordinary subscribers are reported through the controller's `onError` after acceptance and do not reject the update. An input edit rejected during rendering restores its displayed value from accepted state.
95
+ Controller disposal clears subscriptions and blocks dispatch and updates. The lower-level mount disposer removes DOM listeners, component resources and renderer styles; call both disposers, or use `createMountedInterface().dispose()` to own both. Disposed controllers ignore later data updates; revision checks help hosts reject stale asynchronous responses. Events are delivered synchronously in interaction order with a copy of the current state. Reentrant dispatch is ignored, but repeated later clicks are separate events. The host must deduplicate or serialize effects, check revisions when applying asynchronous results, and authorize every capability independently. Hidden actions cannot be dispatched. Listener feedback is capped at 32 passes per update. Failed DOM preparation calls the mount's `onError` and causes the initiating update to throw. Errors from ordinary subscribers are reported through the controller's `onError` after acceptance and do not reject the update. An input edit rejected during rendering restores its displayed value from accepted state.
92
96
 
93
97
  The transaction covers Veil-owned controller data and DOM operations under normal DOM ownership. It cannot reverse effects that arbitrary trusted component code performs outside its candidate element. Factories should attach cleanup to their abort signal as soon as they acquire a resource; if a factory throws before returning an instance, Veil can abort the signal but cannot call a `dispose` it never received. Components must not mutate the mounted tree or perform privileged effects during preparation. External code that removes or monkey-patches Veil-owned DOM nodes can break commit assumptions. Report such failures as integration defects.
94
98
 
95
- ## Three public API demonstrations
99
+ ## Public API demonstrations
96
100
 
97
- `examples/universal/human.mjs` defines an interactive interface with the single-call mount helper. `agent.mjs` reads the built-in manifest, parses simulated JSON responses, rejects an unknown component, filters data, continues to a summary, and starts over. The host adds a trusted Activity component after parsing; the agent never supplies its implementation. It retains the query and focus, and disposes Activity on removal. `application.mjs` switches between reusable home and detail definitions and displays a list. Build with `node packages/ui/examples/universal/build.mjs`; open `examples/universal/index.html`. `npm --workspace @veil-runtime/ui run test:browser` exercises all three in Chromium at mobile and desktop widths.
101
+ `examples/universal/human.mjs` defines an interactive interface with the single-call mount helper. `agent.mjs` reads the built-in manifest, parses simulated JSON responses, rejects an unknown component, filters data, continues to a summary, and starts over. The host adds a trusted Activity component after parsing; the agent never supplies its implementation. It retains the query and focus, and disposes Activity on removal. `application.mjs` switches between reusable home and detail definitions and displays a list. The [validated example](../examples/universal/validated.mjs) covers multi-input validation and conditions. Build with `node packages/ui/examples/universal/build.mjs`; open `examples/universal/index.html`. `npm --workspace @veil-runtime/ui run test:browser` exercises the browser examples in Chromium at mobile and desktop widths.
98
102
 
99
103
  ## External consumer assessment
100
104
 
101
- `npm --workspace @veil-runtime/ui run verify:consumer` packs twice, compares tarball hashes, installs the packed artifact into separate TypeScript and JavaScript projects offline, and runs six headless scenarios: information, form, structured data, JSON acceptance, event update, and disposal. Core is not installed; the only production dependency is Zod. Before this increment, a one-mount app called `createInterfaceController`, `mountInterface`, then both `unmount()` and `controller.dispose()`; custom component names had to be listed separately from the factory map. The helper reduces these to one creation call, one `dispose()`, and one registry map. The host still supplies an action allowlist, event handling, pre-parse byte limit, and domain authorization. There is no bundled AI provider or framework.
105
+ `npm --workspace @veil-runtime/ui run verify:consumer` packs twice, compares tarball hashes, installs the packed artifact into separate TypeScript and JavaScript projects offline, and runs headless scenarios covering information, forms, structured data, responsive Grid, interactive collections, field descriptions and disabled validation, asynchronous presentation and stale completion, JSON acceptance, event updates, and disposal. Core is not installed; the only production dependency is Zod. The single-call mount helper creates a controller and mount together with one `dispose()` and derives trusted component names from its registry map. The host still supplies an action allowlist, event handling, pre-parse byte limit, and domain authorization. There is no bundled AI provider or framework.
106
+
107
+ ## Candidate qualification and historical release assessment
108
+
109
+ This is an unpublished v0.5.0 Developer Preview candidate. Existing v0.4.2 definitions remain accepted, with source compatibility changes for controller implementers and broad Table-column readers. See [release notes and migration](RELEASE-0.5.0.md) and the [qualification report](RELEASE-QUALIFICATION-0.5.0.md).
102
110
 
103
- ## Release assessment
111
+ Chromium covers the original composition/universal demonstrations and Grid breakpoint edges. Chromium, Firefox and WebKit cover collections, forms, asynchronous presentation and text stability; manual assistive-technology certification remains unverified. Heading remains fixed at h2. Sorting, pagination, list templates and implicit field submission remain outside scope. Publication requires separate authorization of the exact qualified artifact.
104
112
 
105
- This is a 0.3.0 release candidate that preserves the qualified 0.2.0 RC APIs. Chromium coverage is available; Firefox, WebKit, screen readers, and framework-specific adapters remain unverified. The `Heading` primitive has a fixed h2 level; richer heading hierarchy, row controls, table sorting, list item templates, and form-level validation are outside this increment. The old RC commit and its tarball remain the reproducible 0.2.0 checkpoint. Publication requires maintainer approval of the exact qualified artifact.
113
+ At the v0.3.0-rc.2 checkpoint, this interface preserved the qualified v0.2.0 RC APIs. The old RC commit and tarball remain a reproducible historical checkpoint. Its candidate-time limitations and publication gate do not describe the current v0.4.2 package: declarative form-field validation, Select, Textarea, compound conditions, and conditional Button disabling were added by v0.4.0. The v0.4.2 renderer stability patch was qualified in Chromium, Firefox, and WebKit; physical Safari, screen readers, and framework-specific adapters are not established by that browser-engine evidence.
@@ -0,0 +1,144 @@
1
+ # UI v0.5.0 development handover
2
+
3
+ Qualification follow-up: see [release notes](RELEASE-0.5.0.md) and [audit report](RELEASE-QUALIFICATION-0.5.0.md). The historical increment snapshot below used `0.5.0-dev.0`; qualification prepares `0.5.0` locally and does not publish.
4
+
5
+ Snapshot: 2026-11-14, `/home/mustapha/dev/ai/veil-ui-v050`.
6
+ Unpublished candidate `0.5.0-dev.0`; publication hold remains active.
7
+ **All planned v0.5.0 feature increments are complete and ready for release
8
+ qualification. This is not a release.**
9
+
10
+ ## Git checkpoint
11
+
12
+ Branch: `feature/ui-v0.5-contract-metadata`.
13
+ Verified starting HEAD: `e622054260b1d508aacd6a92baa1fe86dafa5185`.
14
+ Starting working tree was clean. The Host-Driven Asynchronous Presentation States
15
+ increment is one local checkpoint with message
16
+ `feat(ui): add host-driven asynchronous presentation states`.
17
+ Its SHA is available with `git log -1 --format=%H` after creation; a commit cannot
18
+ embed its own SHA. The common Git directory is `/home/mustapha/dev/ai/veil-ui-lab/.git`.
19
+
20
+ The user authorized design, implementation, verification, exact staging and one
21
+ local checkpoint commit. No merge, push, tag, publication, release or deployment
22
+ is authorized. Veil Core, the separate showcase and published v0.4.2 artifacts
23
+ remain untouched. Generated distribution, bundles, logs and tarballs are not
24
+ commit inputs.
25
+
26
+ ## Completed feature scope
27
+
28
+ - Capability metadata describes aggregate and effective validation bounds.
29
+ - Responsive Grid provides bounded 1–4 columns, viewport breakpoints at
30
+ 600px/1024px, finite gaps, keyed children and stable DOM/keyboard order.
31
+ - Interactive Tables provide keyed rows, neutral badges and host-allowlisted,
32
+ revision-checked row actions.
33
+ - Richer Forms provides bounded descriptions, disabled conditions, text/number
34
+ readOnly flags, accessible eager feedback and first-invalid submission focus.
35
+ - Host-driven asynchronous presentation adds optional Status `phase` literals or
36
+ bindings for `loading`, `ready`, `empty`, `error`, safe defaults, strict bounded
37
+ string messages, polite atomic semantics and focus recovery.
38
+
39
+ Specifications: [`INTERFACE-CAPABILITIES-0.5.0.md`](INTERFACE-CAPABILITIES-0.5.0.md),
40
+ [`INTERACTIVE-COLLECTIONS-0.5.0.md`](INTERACTIVE-COLLECTIONS-0.5.0.md),
41
+ [`RICHER-FORMS-0.5.0.md`](RICHER-FORMS-0.5.0.md), and
42
+ [`ASYNC-PRESENTATION-0.5.0.md`](ASYNC-PRESENTATION-0.5.0.md).
43
+ The asynchronous presentation document records the design before implementation,
44
+ including the alternatives considered and the exact JSON/TypeScript contract.
45
+
46
+ ## Final increment behavior
47
+
48
+ Manifest v5 adds Status `phase` and `behavior.presentation`. Existing definitions,
49
+ controller signatures and event fields remain compatible. Exact manifest-version
50
+ consumers must accept v5. The separate composition API's Status is unchanged.
51
+
52
+ A phased Status keeps its keyed paragraph, `role=status`, `aria-live=polite`,
53
+ `aria-atomic=true`, `data-phase` and `tabindex=-1`. Loading messages are not marked
54
+ aria-busy, so their announcements are not deferred. Optional text uses existing
55
+ string/binding syntax but must resolve to a string of at most 4096 UTF-16 units
56
+ when phase is present. Invalid or missing bound phase/text rejects the candidate
57
+ atomically, including when hidden. Absent text uses a safe phase default; explicit
58
+ empty strings remain permitted. Supplied messages render only as textContent.
59
+ Hosts must sanitize public messages: Veil cannot recognize secrets in strings.
60
+ Exceptions, callback errors and diagnostics are never automatically displayed.
61
+
62
+ Existing conditions select content and ordinary Buttons request retry. Host action
63
+ allowlists and current visibility/disabled checks remain authoritative. Phase
64
+ alone does not authorize, disable, hide or validate anything. Hosts own operation
65
+ execution, cancellation, deduplication, business authorization and result handling.
66
+ Veil introduces no fetching, timers, retries, jobs, queues, promise ownership or
67
+ second state engine. The example includes host-side revision protocol helpers.
68
+
69
+ Use existing revision-protected updates: capture the revision after accepting
70
+ loading and supply it on completion. Any intervening accepted edit or update
71
+ invalidates it; stale updates return false. Hosts decide whether to discard or
72
+ rebase. Compatible controls retain state, identity, focus and selection. If an
73
+ accepted update removes/disables/hides the focused control, focus moves to the
74
+ first visible phased Status in screen order. Focus outside the interface remains
75
+ untouched. Hidden children retain their existing mounted lifecycle and field
76
+ state; hidden controls remain ineligible. Disposal remains authoritative.
77
+
78
+ ## Increment files
79
+
80
+ Paths relative to `packages/ui/`:
81
+
82
+ - `src/interface.ts`: public phase type, strict definition/data validation,
83
+ manifest v5 and behavioral metadata. No controller API changes.
84
+ - `src/dom.ts`: phased messages/semantics and focus recovery during reconciliation.
85
+ - `test/interface-async-presentation.test.ts`,
86
+ `test/browser-async-presentation.mjs`: contract/state and three-browser coverage.
87
+ - Existing manifest unit tests: v5 expectations, with existing assertions retained.
88
+ - `scripts/verify-universal-consumer.mjs`: installed strict TypeScript phase types
89
+ and independent JavaScript host transition/retry/stale-result scenarios.
90
+ - `examples/universal/async-presentation.mjs`: JSON-only definition and explicit
91
+ host protocol helpers, composing Grid, Table, List and fields.
92
+ - `package.json`: dedicated `test:browser:async` command.
93
+ - README, CHANGELOG, capability documentation, async contract and this handover.
94
+
95
+ ## Verification
96
+
97
+ All required gates pass:
98
+
99
+ - `npm --workspace @veil-runtime/ui run check`: build, test typecheck, **110/110 UI
100
+ tests**, distribution integrity (54 files), source/dist boundary and circular
101
+ import checks.
102
+ - `npm --workspace @veil-runtime/ui run test:browser`: existing composition and
103
+ universal suites, including accessibility tree, mobile/desktop/narrow layout,
104
+ Grid, input behavior, reconciliation, focus, escaping and teardown.
105
+ - With `VEIL_PLAYWRIGHT_MODULE=/tmp/veil-v050-publication-main/node_modules/playwright/index.mjs`:
106
+ `test:browser:async`, `test:browser:forms`, `test:browser:collections`, and
107
+ `node packages/ui/test/browser-text-stability.mjs` pass on Chromium
108
+ 151.0.7922.108, Firefox 157.0 and WebKit 27.2.
109
+ - Async tests cover accessible loading/ready/empty/error messages and defaults,
110
+ native retry Enter/Space, Tab order, state/identity/focus/selection retention,
111
+ hidden-ancestor/removal/disabled focus recovery and hidden destination exclusion,
112
+ rapid transitions, out-of-order results, intervening edits, revision/disposal,
113
+ atomic rejection, escaping and Table/List/Grid/form composition.
114
+ - `npm --workspace @veil-runtime/ui run verify:consumer`: repeat npm packs have
115
+ identical hashes; independent installed strict TypeScript and JavaScript
116
+ consumers pass without optional Core. Only zod is a runtime dependency.
117
+ - `git diff --check` and exact staged inventory/diff review before checkpoint.
118
+
119
+ Local logs: `/tmp/veil-async-{check,browser,regressions,forms,collections,text,consumer}.log`.
120
+ Browser launch and npm packaging failed under the sandbox and passed with
121
+ authorized host execution. No validation was weakened or failing test skipped.
122
+
123
+ ## Limits and release qualification
124
+
125
+ Host revisions are snapshot-wide, not per-operation tokens. Multi-region focus
126
+ recovery uses the first visible phased Status; hosts should order destinations or
127
+ manage focus explicitly. No automatic return to a formerly hidden control is
128
+ provided. Automated accessibility tests do not certify assistive-technology
129
+ announcements. No progress percentage, urgent alert mode, async validator,
130
+ server-error field mapping, implicit submit or asynchronous orchestration is added.
131
+
132
+ Forms retain eager validation and existing five rules. Native number controls
133
+ normalize incomplete spelling. Hidden/disabled values remain in event.state;
134
+ field flags and phase are not confidentiality/authorization controls.
135
+
136
+ Collections still omit sorting/filtering/pagination/selection, interactive Lists,
137
+ rich cell objects, URLs, custom formatters and arrow-key grid mode. Row IDs remain
138
+ opaque host-authorized references. Table gestures retain their existing revision
139
+ protection; ordinary Buttons keep their existing eligibility semantics. Grid uses
140
+ viewport breakpoints; host CSP must permit finite renderer styles.
141
+
142
+ Next: separately authorized release qualification of this completed feature scope.
143
+ Release, publication, merge, push, tagging and deployment require separate
144
+ instructions. The candidate remains unpublished and publication hold stays active.
@@ -0,0 +1,32 @@
1
+ // JSON-only definition. The host supplies approved public messages and valid
2
+ // collections in every snapshot, including while their views are hidden.
3
+ const isPhase = equals => ({ binding: { source: 'data', path: 'phase' }, equals });
4
+ export const asyncPresentation = {
5
+ id: 'async-results', actions: ['retry'],
6
+ data: { phase: 'loading', message: 'Loading results…', rows: [], items: [] },
7
+ state: { notes: '' },
8
+ screen: { key: 'root', component: 'Grid', columnsAt: { sm: 2 }, children: [
9
+ { key: 'status', component: 'Status', phase: { source: 'data', path: 'phase' }, text: { source: 'data', path: 'message' } },
10
+ { key: 'retry', component: 'Button', text: 'Retry', action: 'retry', visible: isPhase('error') },
11
+ { key: 'results', component: 'Stack', visible: isPhase('ready'), children: [
12
+ { key: 'table', component: 'Table', binding: { source: 'data', path: 'rows' }, rowKey: 'id', columns: [{ key: 'name', label: 'Name' }] },
13
+ { key: 'list', component: 'List', binding: { source: 'data', path: 'items' } },
14
+ ] },
15
+ { key: 'notes', component: 'Textarea', label: 'Notes', binding: { source: 'state', path: 'notes' } },
16
+ ] },
17
+ };
18
+
19
+ // Host-side protocol helpers, not an asynchronous engine. The host authorizes
20
+ // retry, starts/cancels its operation, and retains the returned revision token.
21
+ export function beginLoading(controller, expectedRevision = controller.snapshot().revision) {
22
+ const data = controller.snapshot().data;
23
+ if (!controller.updateData({ ...data, phase: 'loading', message: 'Loading results…' }, expectedRevision)) return null;
24
+ return controller.snapshot().revision;
25
+ }
26
+ export function presentResult(controller, revision, rows, items) {
27
+ return controller.updateData({ phase: rows.length || items.length ? 'ready' : 'empty', message: rows.length || items.length ? 'Results ready.' : 'No results.', rows, items }, revision);
28
+ }
29
+ export function presentError(controller, revision) {
30
+ // Never pass an exception or its message through to the screen.
31
+ return controller.updateData({ phase: 'error', message: 'Results are unavailable. You can try again.', rows: [], items: [] }, revision);
32
+ }
@@ -0,0 +1,22 @@
1
+ import { createMountedInterface } from '../../dist/dom.js';
2
+
3
+ // Trusted host code supplies openRecord. It must resolve and authorize the actual
4
+ // record for the current principal before opening it. An ID is only a reference.
5
+ export function mountCollections(target, rows, openRecord) {
6
+ const controller = createMountedInterface(target, {
7
+ id: 'collection-example', actions: ['record.open'], data: { rows },
8
+ screen: {
9
+ key: 'records', component: 'Table', ariaLabel: 'Records',
10
+ binding: { source: 'data', path: 'rows' }, rowKey: 'id',
11
+ columns: [{ key: 'name', label: 'Name' }, { key: 'status', label: 'Status', presentation: 'badge' }],
12
+ rowActions: [{ action: 'record.open', label: 'Open record' }],
13
+ },
14
+ }, {
15
+ allowedActions: ['record.open'],
16
+ async onEvent(event) {
17
+ if (event.action !== 'record.open' || event.source !== 'records' || event.rowId === undefined) return;
18
+ await openRecord(event.rowId); // Host owns lookup, record authorization and execution.
19
+ },
20
+ });
21
+ return controller;
22
+ }
@@ -0,0 +1,21 @@
1
+ // JSON-only definition. The host handles the declared action and owns authorization
2
+ // and persistence. Hidden/disabled values remain in event.state.
3
+ export const requestForm = {
4
+ id: 'request', actions: ['submit'],
5
+ state: { reference: 'REQ-42', summary: '', priority: 'normal', expedited: false, reason: '', units: 1 },
6
+ screen: { key: 'request', component: 'Stack', children: [
7
+ { key: 'reference', component: 'Input', label: 'Reference', readOnly: true,
8
+ binding: { source: 'state', path: 'reference' } },
9
+ { key: 'summary', component: 'Input', label: 'Summary', description: 'Describe the operational request in 5–100 characters.',
10
+ binding: { source: 'state', path: 'summary' }, validation: { required: true, minLength: 5, maxLength: 100 } },
11
+ { key: 'priority', component: 'Select', label: 'Priority', description: 'The host reviews all urgent requests.',
12
+ binding: { source: 'state', path: 'priority' }, options: [{ value: 'normal', label: 'Normal' }, { value: 'urgent', label: 'Urgent' }] },
13
+ { key: 'expedited', component: 'Input', inputType: 'checkbox', label: 'Request expedited handling', binding: { source: 'state', path: 'expedited' } },
14
+ { key: 'reason', component: 'Textarea', label: 'Expedited reason', description: 'Required when expedited handling is selected.',
15
+ binding: { source: 'state', path: 'reason' }, visible: { binding: { source: 'state', path: 'expedited' }, equals: true },
16
+ validation: { required: true, maxLength: 500 } },
17
+ { key: 'units', component: 'Input', inputType: 'number', label: 'Units', description: 'Choose between 1 and 20 units.',
18
+ binding: { source: 'state', path: 'units' }, disabled: { binding: { source: 'state', path: 'expedited' }, equals: true }, validation: { required: true, min: 1, max: 20 } },
19
+ { key: 'submit', component: 'Button', text: 'Submit request', action: 'submit', validate: true },
20
+ ] },
21
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veil-runtime/ui",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/",
@@ -50,13 +50,16 @@
50
50
  "build:no-dom": "node scripts/build.mjs",
51
51
  "test": "node --import tsx --test --test-isolation=none test/*.test.ts test/*.test.mjs",
52
52
  "test:browser": "node examples/composed/build.mjs && node test/browser-composition.mjs && node examples/universal/build.mjs && node test/browser-interface.mjs",
53
+ "test:browser:collections": "cd ../.. && node packages/ui/test/browser-collections.mjs",
53
54
  "check:boundary": "node scripts/check-boundary.mjs",
54
55
  "typecheck:test": "tsc -p tsconfig.test.json",
55
56
  "verify:dist": "node scripts/verify-dist.mjs",
56
57
  "verify:consumer": "node scripts/verify-universal-consumer.mjs",
57
58
  "prepack": "npm run build && npm run verify:dist && npm run check:boundary",
58
59
  "prepublishOnly": "node scripts/publication-hold.mjs",
59
- "check": "npm run build && npm run typecheck:test && npm test && npm run verify:dist && npm run check:boundary"
60
+ "check": "npm run build && npm run typecheck:test && npm test && npm run verify:dist && npm run check:boundary",
61
+ "test:browser:forms": "cd ../.. && node packages/ui/test/browser-forms.mjs",
62
+ "test:browser:async": "cd ../.. && node packages/ui/test/browser-async-presentation.mjs"
60
63
  },
61
64
  "peerDependencies": {
62
65
  "@veil-runtime/core": "^0.4.0"