Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 36 additions & 23 deletions apps/website/content/docs/render/a2ui/catalog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Renders a span of text.
| `text` | `string` | The text content to display |

```json
{"id": "greeting", "component": "Text", "text": "Hello, world!"}
{"id": "greeting", "component": {"Text": {"text": {"literalString": "Hello, world!"}}}}
```

### Image
Expand Down Expand Up @@ -103,15 +103,14 @@ Arranges children vertically with a flex column layout.

### Card

Renders children inside a rounded bordered card container with an optional title.
Renders children inside a rounded bordered card container.

| A2UI type | Angular component | Selector |
|-----------|-------------------|----------|
| `Card` | `A2uiCardComponent` | `a2ui-card` |

| Prop | Type | Description |
|------|------|-------------|
| `title` | `string` | Optional card heading |
| `childKeys` | `string[]` | Ordered list of child component IDs |
| `spec` | `Spec` | Injected automatically by the render engine |

Expand Down Expand Up @@ -149,8 +148,8 @@ Renders a button that dispatches an action when clicked.

| Prop | Type | Description |
|------|------|-------------|
| `label` | `string` | Button label text |
| `variant` | `'primary' \| 'borderless'` | Visual style. Defaults to `'primary'` |
| `childKeys` | `string[]` | Child component IDs whose rendered output is the button's content (e.g., a `Text` label) |
| `primary` | `boolean` | Renders the primary visual style. Defaults to `true` |
| `disabled` | `boolean` | Disables the button when `true` |
| `action` | `A2uiAction` | Action to execute on click (event or function call) |
| `validationResult` | `A2uiValidationResult` | Pre-computed validation result — button is disabled if `valid` is `false` |
Expand Down Expand Up @@ -183,7 +182,7 @@ A single-line text input with optional label and placeholder.
| Prop | Type | Description |
|------|------|-------------|
| `label` | `string` | Input label |
| `value` | `string` | Current value (resolved from path reference) |
| `text` | `string` | Current value (resolved from path reference). `value` is a read-only computed alias |
| `placeholder` | `string` | Placeholder text |
| `validationResult` | `A2uiValidationResult` | Validation state — shows errors below input when invalid |
| `_bindings` | `Record<string, string>` | Auto-populated by `surfaceToSpec()` from path references |
Expand All @@ -192,9 +191,12 @@ A single-line text input with optional label and placeholder.
```json
{
"id": "name-field",
"component": "TextField",
"label": "Your name",
"value": {"path": "/name"}
"component": {
"TextField": {
"label": {"literalString": "Your name"},
"text": {"path": "/name"}
}
}
}
```

Expand All @@ -211,7 +213,8 @@ A labeled checkbox with two-way binding for its checked state.
| Prop | Type | Description |
|------|------|-------------|
| `label` | `string` | Checkbox label |
| `checked` | `boolean` | Current checked state (resolved from path reference) |
| `value` | `boolean` | Current checked state (resolved from path reference) |
| `checked` | `boolean` | Deprecated back-compat alias for `value` |
| `validationResult` | `A2uiValidationResult` | Validation state — shows errors below checkbox when invalid |
| `_bindings` | `Record<string, string>` | Auto-populated by `surfaceToSpec()` from path references |
| `emit` | injected | Event emitter provided by the render engine |
Expand Down Expand Up @@ -245,20 +248,27 @@ A date, time, or datetime input with two-way binding.
|------|------|-------------|
| `label` | `string` | Input label |
| `value` | `string` | Current value (resolved from path reference) |
| `inputType` | `'date' \| 'time' \| 'datetime-local'` | HTML input type. Defaults to `'date'` |
| `enableDate` | `boolean` | Include the date portion. Defaults to `true` |
| `enableTime` | `boolean` | Include the time portion. Defaults to `false` |
| `min` | `string` | Minimum allowed value |
| `max` | `string` | Maximum allowed value |
| `validationResult` | `A2uiValidationResult` | Validation state — shows errors below input when invalid |
| `_bindings` | `Record<string, string>` | Auto-populated by `surfaceToSpec()` from path references |
| `emit` | injected | Event emitter provided by the render engine |

The HTML input type (`date`, `time`, or `datetime-local`) is derived internally from `enableDate` and `enableTime`.

```json
{
"id": "date-field",
"component": "DateTimeInput",
"label": "Appointment date",
"value": {"path": "/appointmentDate"},
"inputType": "date"
"component": {
"DateTimeInput": {
"label": {"literalString": "Appointment date"},
"value": {"path": "/appointmentDate"},
"enableDate": true,
"enableTime": false
}
}
}
```

Expand All @@ -274,8 +284,8 @@ A range slider input with two-way binding.
|------|------|-------------|
| `label` | `string` | Slider label |
| `value` | `number` | Current value (bind via `_bindings`) |
| `min` | `number` | Minimum value |
| `max` | `number` | Maximum value |
| `minValue` | `number` | Minimum value |
| `maxValue` | `number` | Maximum value |
| `step` | `number` | Step increment |
| `validationResult` | `A2uiValidationResult` | Validation state — shows errors below slider when invalid |
| `_bindings` | `Record<string, string>` | Auto-populated by `surfaceToSpec()` from path references |
Expand All @@ -284,12 +294,15 @@ A range slider input with two-way binding.
```json
{
"id": "volume",
"component": "Slider",
"label": "Volume",
"value": {"path": "/volume"},
"min": 0,
"max": 100,
"step": 1
"component": {
"Slider": {
"label": {"literalString": "Volume"},
"value": {"path": "/volume"},
"minValue": 0,
"maxValue": 100,
"step": 1
}
}
}
```

Expand Down
22 changes: 11 additions & 11 deletions apps/website/content/docs/render/a2ui/surface-store.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,20 @@ Processes one `A2uiMessage` and updates the internal surfaces signal. All four m

| Message type | Behavior |
|--------------|----------|
| `createSurface` | Creates a new `A2uiSurface` entry with an empty component map and data model |
| `updateComponents` | Merges the provided components into the surface's component map by `id` — existing components are replaced, others are kept |
| `updateDataModel` | Applies a JSON Pointer patch to the surface's data model (see below) |
| `surfaceUpdate` | Delivers a surface's components — merges the provided components into the surface's component map by `id` so existing components are replaced and others are kept |
| `dataModelUpdate` | Applies a JSON Pointer patch to the surface's data model (see below) |
| `beginRendering` | Marks the surface root and commits the buffered components and data model into a live surface |
| `deleteSurface` | Removes the surface from the map entirely |

Messages for unknown surface IDs are silently ignored (except `createSurface`, which registers the surface).
Messages for unknown surface IDs are silently ignored until a `surfaceUpdate` introduces the surface and a `beginRendering` commits it.

### `surfaces`

A readonly `Signal<Map<string, A2uiSurface>>` containing all active surfaces. Each map operation produces a new `Map` reference so that Angular's change detection triggers correctly.

### `surface(surfaceId)`

Returns a `computed` signal for a single surface. The signal emits `undefined` until a `createSurface` message registers it, and `undefined` again after `deleteSurface` removes it.
Returns a `computed` signal for a single surface. The signal emits `undefined` until a `beginRendering` message commits it, and `undefined` again after `deleteSurface` removes it.

```typescript
const dashboard = store.surface('dashboard');
Expand All @@ -61,9 +61,9 @@ const dashboard = store.surface('dashboard');

The surface's `dataModel` is synchronized into the render-lib `StateStore` when `surfaceToSpec()` converts the surface to a spec. The conversion sets `state: surface.dataModel` on the produced `Spec`, which initializes the render-lib's internal `StateStore` with the surface data. When components with `_bindings` update values (e.g., a text field changing), those updates flow through the render-lib `StateStore`, and each mutation emits a `RenderStateChangeEvent` through the render-lib event system. This means consumers observing the `events` output on `A2uiSurfaceComponent` see all data model changes as typed `RenderStateChangeEvent` objects with `path`, `value`, and `snapshot` fields.

## updateDataModel Semantics
## dataModelUpdate Semantics

The `updateDataModel` message uses JSON Pointer (RFC 6901) paths to address values in the data model.
The `dataModelUpdate` message uses JSON Pointer (RFC 6901) paths to address values in the data model.

| `path` | `value` | Effect |
|--------|---------|--------|
Expand All @@ -73,13 +73,13 @@ The `updateDataModel` message uses JSON Pointer (RFC 6901) paths to address valu

```json
// Replace entire model
{"updateDataModel": {"surfaceId": "s1", "value": {"name": "Alice", "score": 42}}}
{"dataModelUpdate": {"surfaceId": "s1", "value": {"name": "Alice", "score": 42}}}

// Set a single field
{"updateDataModel": {"surfaceId": "s1", "path": "/score", "value": 99}}
{"dataModelUpdate": {"surfaceId": "s1", "path": "/score", "value": 99}}

// Delete a field
{"updateDataModel": {"surfaceId": "s1", "path": "/score"}}
{"dataModelUpdate": {"surfaceId": "s1", "path": "/score"}}
```

## Usage with createA2uiMessageParser
Expand Down Expand Up @@ -113,7 +113,7 @@ effect(() => {
```

<Callout type="info" title="JSONL envelope format">
The parser expects each line to be wrapped in an envelope object: `{"createSurface": {...}}`, `{"updateComponents": {...}}`, etc. The envelope key determines the message type; its value is the message payload.
The parser expects each line to be wrapped in an envelope object: `{"surfaceUpdate": {...}}`, `{"dataModelUpdate": {...}}`, etc. The envelope key determines the message type; its value is the message payload.
</Callout>

## A2uiSurface Shape
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ An `AngularRegistry` object with two methods:
```typescript
interface AngularRegistry {
get(name: string): AngularComponentRenderer | undefined;
getFallback(name: string): AngularComponentRenderer | undefined;
names(): string[];
}
```

| Method | Description |
|--------|-------------|
| `get(name)` | Returns the component class for the given type name, or `undefined` if not registered |
| `getFallback(name)` | Returns the configured fallback renderer for a registered name -- the entry's own `fallback`, the library's default fallback if the entry omits one, or `undefined` if the name isn't registered. |
| `names()` | Returns an array of all registered type name strings |

## Usage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ interface RenderContext {
store: StateStore;
functions?: Record<string, ComputedFunction>;
handlers?: Record<string, (params: Record<string, unknown>) => unknown | Promise<unknown>>;
emitEvent?: (event: RenderEvent) => void;
loading?: boolean;
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { signalStateStore } from '@threadplane/render';
## Signature

```typescript
function signalStateStore(initialState?: StateModel): StateStore;
function signalStateStore(initialState: StateModel = {}): StateStore;
```

### Parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ A surface has an id, catalog id, component map, data model, optional theme, opti
interface A2uiSurface {
surfaceId: string;
catalogId: string;
theme?: A2uiTheme;
sendDataModel?: boolean;
components: Map<string, A2uiComponent>;
dataModel: Record<string, unknown>;
styles?: { font?: string; primaryColor?: string };
Expand Down Expand Up @@ -173,6 +175,10 @@ interface A2uiActionMessage {
sourceComponentId: string;
timestamp: string;
context: Record<string, unknown>;
label?: string;
};
metadata?: {
a2uiClientDataModel: A2uiClientDataModel;
};
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const appConfig: ApplicationConfig = {

```typescript
import { Component, ChangeDetectionStrategy, input } from '@angular/core';
import type { Spec } from '@json-render/core';

@Component({
selector: 'app-text',
Expand All @@ -98,7 +99,7 @@ import { Component, ChangeDetectionStrategy, input } from '@angular/core';
export class TextComponent {
readonly label = input<string>('');
readonly childKeys = input<string[]>([]);
readonly spec = input<unknown>(null);
readonly spec = input.required<Spec>();
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Let's start with a simple Angular component for the spec to render. Every render
```typescript
// text.component.ts
import { Component, ChangeDetectionStrategy, input } from '@angular/core';
import type { Spec } from '@json-render/core';

@Component({
selector: 'app-text',
Expand All @@ -24,7 +25,7 @@ import { Component, ChangeDetectionStrategy, input } from '@angular/core';
export class TextComponent {
readonly label = input<string>('');
readonly childKeys = input<string[]>([]);
readonly spec = input<unknown>(null);
readonly spec = input.required<Spec>();
}
```

Expand Down
2 changes: 1 addition & 1 deletion apps/website/content/docs/render/guides/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ const handlers = {
};
```

This works for handlers passed via `[handlers]` on `<render-spec>`, `provideRender()`, or `ChatComponent`.
This works for handlers passed via `[handlers]` on `<render-spec>`, `provideRender()`, or other render-enabled components like `ChatComponent` (from `@threadplane/chat`).

### Resolution Priority

Expand Down
14 changes: 10 additions & 4 deletions apps/website/content/docs/render/guides/registry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,23 @@ export const uiRegistry = defineAngularRegistry({
});
```

The returned `AngularRegistry` object has two methods:
The returned `AngularRegistry` object has three methods:

- `get(name: string)` -- returns the component class for the given type name, or `undefined` if not registered
- `getFallback(name: string)` -- returns the configured fallback renderer for a registered name -- the entry's own `fallback`, or the library's default fallback if the entry omits one, or `undefined` if the name isn't registered
- `names()` -- returns an array of all registered type names

```typescript
uiRegistry.get('Text'); // TextComponent
uiRegistry.get('Unknown'); // undefined
uiRegistry.names(); // ['Text', 'Card', 'Button', 'Container']
uiRegistry.get('Text'); // TextComponent
uiRegistry.get('Unknown'); // undefined
uiRegistry.getFallback('Text'); // fallback renderer (or default)
uiRegistry.names(); // ['Text', 'Card', 'Button', 'Container']
```

### Fallback Rendering

When an element's type is not registered, it renders that type's configured fallback if one exists, and otherwise renders nothing. Fallbacks also fill a transient gap during rendering: while an element's state-bound props are still resolving, the library can render a fallback to give visual feedback until the real component is ready. Once the real component mounts, it stays mounted -- later re-renders never revert to the fallback.

## The Component Input Contract

Every component rendered by `@threadplane/render` receives inputs conforming to the `AngularComponentInputs` interface. Your custom props from the spec are spread as additional inputs alongside the standard ones.
Expand Down
Loading
Loading