@veil-runtime/ui 0.4.2 → 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.
- package/CHANGELOG.md +23 -2
- package/README.md +35 -17
- package/dist/build-manifest.json +10 -10
- package/dist/dom.js +270 -23
- package/dist/interface.d.ts +207 -22
- package/dist/interface.js +151 -32
- package/dist/validation-safety.d.ts +10 -0
- package/dist/validation-safety.js +3 -1
- package/docs/API.md +15 -3
- package/docs/ASYNC-PRESENTATION-0.5.0.md +109 -0
- package/docs/INTERACTIVE-COLLECTIONS-0.5.0.md +139 -0
- package/docs/INTERFACE-CAPABILITIES-0.5.0.md +105 -0
- package/docs/RELEASE-0.5.0.md +47 -0
- package/docs/RELEASE-QUALIFICATION-0.5.0.md +157 -0
- package/docs/RICHER-FORMS-0.5.0.md +69 -0
- package/docs/UNIVERSAL-INTERFACE.md +19 -11
- package/docs/V0.5.0-HANDOVER.md +144 -0
- package/examples/universal/async-presentation.mjs +32 -0
- package/examples/universal/collections.mjs +22 -0
- package/examples/universal/forms.mjs +21 -0
- package/package.json +5 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
# Universal interface foundation (
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|