From 555c9a8fdcacbc457c6a9bc3d818218a304e644c Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 14 Jan 2026 11:06:03 +1030 Subject: [PATCH 01/19] add types design initial version --- types_design.md | 319 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 types_design.md diff --git a/types_design.md b/types_design.md new file mode 100644 index 00000000..b07aba21 --- /dev/null +++ b/types_design.md @@ -0,0 +1,319 @@ +# A2UI Renderer Type System Design + +This document outlines a TypeScript-based type system for A2UI web renderers. The primary goal is to create a decoupled, extensible, and type-safe architecture that separates the core A2UI message processing logic from the framework-specific rendering implementations (e.g., Lit, Angular, React). + +This design enables an ecosystem where: +- A central, framework-agnostic `A2uiWebRenderer` handles all protocol logic. +- Framework-specific renderers (`@a2ui/angular`, `@a2ui/lit`) consume the output of the core renderer. +- Component libraries (e.g., `@a2ui/material-catalog`) can provide framework-agnostic API definitions and framework-specific rendering implementations. + +## 1. Core Concepts & Interfaces + +The system is built around a few key interfaces that clearly define the responsibilities of each part of the architecture. + +### `A2uiWebRenderer` + +The central, framework-agnostic engine. Its role is to process raw A2UI messages, manage the state of each UI surface (data models, component definitions), and resolve the incoming data into a structured, typed, and fully-resolved node tree. It has no knowledge of how these nodes will be rendered. + +```typescript +import { Signal } from '@lit-labs/preact-signals'; + +// Represents the state of a single UI surface, with a fully resolved node tree. +interface SurfaceState { + surfaceId: string; + componentTree: Signal; + styles: Record; +} + +// The core, framework-agnostic processor. +interface A2uiWebRenderer { + /** + * Processes an array of raw A2UI messages from the server. + * This updates the internal state and triggers changes in the resolved node trees. + */ + processMessages(messages: ServerToClientMessage[]): void; + + /** + * Returns a map of all current UI surfaces. + * The componentTree for each surface is a signal, allowing renderers + * to subscribe to changes reactively. + */ + getSurfaces(): ReadonlyMap; + + /** + * Dispatches a user action to the server. + */ + dispatchAction(action: UserAction): Promise; + + /** + * Allows UI components to update data in the data model. + */ + setData(dataContext: BaseResolvedNode, relativePath: string, value: unknown): void; + + /** + + * Allows UI components to get data from the data model. + */ + getData(dataContext: BaseResolvedNode, relativePath: string): unknown; +} +``` + +### `ComponentApi` + +Defines the contract for a single component, independent of any rendering framework. It is responsible for parsing and resolving its own properties. + +```typescript +// Base interface for any resolved component node. +interface BaseResolvedNode { + id: string; + type: TName; + weight: number | 'initial'; + dataContextPath?: string; +} + +// Union type for any possible resolved node. +type AnyResolvedNode = BaseResolvedNode & { + properties: Record; +}; + +// Interface for a component's definition. +interface ComponentApi< + TName extends string, + RNode extends BaseResolvedNode +> { + readonly name: TName; + + /** + * Resolves the raw properties from the A2UI message into the typed + * properties required by the final resolved node. This logic is specific + * to each component. + * @param unresolvedProperties The raw properties from the message. + * @param resolver A callback provided by the A2uiWebRenderer to recursively + * resolve values (e.g., data bindings, child component IDs). + * @returns The resolved properties for the node. + */ + resolveProperties( + unresolvedProperties: Record, + resolver: (value: unknown) => unknown + ): Omit>['properties']; +} +``` + +### `CatalogApi` + +A collection of `ComponentApi` definitions. This represents a complete set of components that can be used together, like a design system (e.g., Material Design). + +```typescript +type AnyComponentApi = ComponentApi; + +class CatalogApi { + private readonly components: Map; + + constructor(components: AnyComponentApi[]) { + this.components = new Map(components.map(c => [c.name, c])); + } + + public get(componentName: string): AnyComponentApi | undefined { + return this.components.get(componentName); + } +} +``` + +### `ComponentRenderer` + +The framework-specific implementation for rendering a single component. + +```typescript +/** + * @template RNode The specific resolved node type this component can render. + * @template RenderOutput The output type of the rendering framework (e.g., TemplateResult for Lit, JSX.Element for React). + */ +interface ComponentRenderer< + RNode extends AnyResolvedNode, + RenderOutput +> { + readonly componentName: RNode['type']; + + /** + * Renders the resolved component node. + * @param node The fully resolved, typed component node to render. + * @param renderChild A function provided by the framework renderer to + * recursively render child nodes. Container components MUST use this. + * @returns The framework-specific, renderable output. + */ + render( + node: RNode, + renderChild: (child: AnyResolvedNode) => RenderOutput | null + ): RenderOutput; +} +``` + +### `CatalogImplementation` + +A framework-specific implementation of a `CatalogApi`. It maps each `ComponentApi` to its corresponding `ComponentRenderer`. + +```typescript +type AnyComponentRenderer = ComponentRenderer; + +class CatalogImplementation { + private readonly renderers: Map>; + + /** + * @param catalogApi The API definition for the catalog. + * @param renderers A list of framework-specific renderers. + */ + constructor(catalogApi: CatalogApi, renderers: AnyComponentRenderer[]) { + // The constructor verifies that every component in `catalogApi` has a + // corresponding renderer provided in the `renderers` array. + // It will throw an error if a component implementation is missing. + this.renderers = new Map(renderers.map(r => [r.componentName, r])); + + for (const api of catalogApi['components'].values()) { + if (!this.renderers.has(api.name)) { + throw new Error(`Missing renderer implementation for component: ${api.name}`); + } + } + } + + public getRenderer(componentName: string): AnyComponentRenderer | undefined { + return this.renderers.get(componentName); + } +} +``` + +### `FrameworkRenderer` + +The top-level, framework-specific renderer that orchestrates the rendering of a complete node tree. + +```typescript +class FrameworkRenderer { + private readonly catalogImplementation: CatalogImplementation; + + constructor(catalogImplementation: CatalogImplementation) { + this.catalogImplementation = catalogImplementation; + } + + /** + * Renders a resolved node from the A2uiWebRenderer into the final output. + * This is the entry point for rendering a component tree. + */ + public renderNode(node: AnyResolvedNode): RenderOutput | null { + const renderer = this.catalogImplementation.getRenderer(node.type); + if (!renderer) { + console.warn(`No renderer found for component type: ${node.type}`); + return null; + } + + // The `renderChild` function passed to the component renderer is a bound + // version of this same `renderNode` method, enabling recursion. + return renderer.render(node, this.renderNode.bind(this)); + } +} +``` + +## 2. Workflow and Data Flow + +1. **Initialization**: + * An application creates an instance of a `CatalogApi` (e.g., `StandardCatalogApi`). + * It creates an instance of the central `A2uiWebRenderer`, passing it the `catalogApi`. + * It creates a framework-specific `CatalogImplementation` (e.g., `StandardLitCatalogImplementation`), passing it the same `catalogApi` and a list of `ComponentRenderer`s for Lit. + * It creates a `FrameworkRenderer` (e.g., `LitRenderer`), passing it the `catalogImplementation`. + +2. **Message Processing**: + * Raw A2UI messages are fed into `a2uiWebRenderer.processMessages()`. + * The `A2uiWebRenderer` processes the messages, updating its internal data models. + * When building the component tree, it uses the `ComponentApi` from the `CatalogApi` to correctly `resolveProperties` for each node. + * This updates the `componentTree` signal for the relevant surface. + +3. **Rendering**: + * A top-level UI component (e.g., a Lit `` or an Angular ``) listens to the `componentTree` signal from the `A2uiWebRenderer`. + * When the signal changes, it passes the new resolved node tree to the `frameworkRenderer.renderNode()`. + * The `FrameworkRenderer` recursively walks the tree, using the `CatalogImplementation` to find the right `ComponentRenderer` for each node, until the entire tree is converted into the framework's renderable format. + +![Data Flow Diagram](https://i.imgur.com/your-diagram-image.png) + +## 3. Satisfying Use Cases + +#### Developer of a `CatalogApi` +The developer creates a new class `MyCatalogApi extends CatalogApi`, passing an array of `ComponentApi` instances to the constructor. This is a clean, framework-agnostic definition of a component library. + +#### Developer of a `ComponentApi` +A developer defines a component by implementing the `ComponentApi` interface. This includes: +- `name`: A string literal type (e.g., `'Card'`). +- A `ResolvedNode` interface (e.g., `CardResolvedNode`). +- `resolveProperties` method: Contains the logic to transform raw properties into the strongly-typed `CardResolvedNode.properties`, using the provided `resolver` to handle children and data bindings. This encapsulates the component's structural logic. + +#### Developer of a `CatalogImplementation` +The developer gathers the required `ComponentRenderer`s for their target framework and creates an instance of `CatalogImplementation`. +- They pass the `CatalogApi` and the array of renderers to the constructor. +- The constructor automatically validates that an implementation exists for every component in the API, satisfying the type-safety and verification requirement. +- Because the `FrameworkRenderer` uses a `Map` (`this.renderers.get(...)`), there is no need for a large `switch` statement. + +#### Developer of a `ComponentRenderer` +An Angular developer, for example, would implement `ComponentRenderer`. +- The `componentName` would be `'Card'`. +- The `render` method receives a `CardResolvedNode` object, which is fully typed and resolved. +- To render the card's child, it calls the `renderChild(node.properties.child)` function, which recursively invokes the main `FrameworkRenderer`. +- The method returns an Angular component, fulfilling the contract. + +#### Developer of a renderer for a new rendering framework (e.g., React) +1. **Reuse `A2uiWebRenderer`**: No changes are needed here. The developer's React application would include `@a2ui/web-renderer` as a dependency. +2. **Create React `FrameworkRenderer`**: A `ReactRenderer` class would be created. Its `renderNode` method would return `JSX.Element`. +3. **Create React `CatalogImplementation`**: The developer would create `ComponentRenderer` implementations for React (e.g., `ReactCardRenderer`). These would be collected into a `CatalogImplementation`. +4. **Create React Surface Component**: A top-level `` React component would be created. It would hold the `A2uiWebRenderer` and `ReactRenderer` instances and use a hook (e.g., `useEffect` or a signal-based hook) to listen for changes to the `componentTree` signal and trigger re-renders. + +## 4. Implementation in Existing and New Frameworks + +### Refactoring the Lit Renderer + +The current Lit renderer (`@renderers/lit/src/0.8/ui/root.ts`) has a large `renderComponentTree` method with a `switch` statement. This would be replaced: +1. The `A2uiMessageProcessor` would evolve into the `A2uiWebRenderer`. Its `buildNodeRecursive` logic would be adapted to use the new `ComponentApi.resolveProperties` pattern. +2. A new `LitRenderer` class (`FrameworkRenderer`) would be created. +3. Each Lit component (`a2ui-text`, `a2ui-card`, etc.) would be wrapped in a `ComponentRenderer` implementation. For example, `LitTextRenderer`'s `render` method would return `html```. +4. The `Root` element's `renderComponentTree` method would be removed. Instead, the top-level `` component would use the `LitRenderer` to render its tree. + +### Adapting the Angular Renderer + +The Angular renderer is already close to this design. +1. The `MessageProcessor` becomes the `A2uiWebRenderer`. +2. The `Catalog` (`@renderers/angular/src/lib/rendering/catalog.ts`) concept maps directly to the `CatalogImplementation`. The existing `DEFAULT_CATALOG` would be used to create `new CatalogImplementation(standardCatalogApi, defaultRenderers)`. +3. The `Renderer` directive (`renderer.ts`) is the `FrameworkRenderer`. Its logic for dynamically creating components remains the same. +4. The `DynamicComponent` base class remains, providing the bridge between the A2UI world and Angular's DI and component model. + +### Sketch of a React Renderer + +```typescript +// --- ReactRenderer.tsx (The top-level surface component) --- +import { useSignalEffect } from '@preact/signals-react'; +import { a2uiWebRenderer, reactRenderer } from './config'; // Assume these are configured + +export function A2UISurface({ surfaceId }: { surfaceId: string }) { + const [node, setNode] = useState(null); + + useSignalEffect(() => { + const surface = a2uiWebRenderer.getSurfaces().get(surfaceId); + setNode(surface?.componentTree.value ?? null); + }); + + if (!node) { + return
Loading...
; + } + + return reactRenderer.renderNode(node); +} + +// --- react-card-renderer.ts --- +import { CardResolvedNode } from '@a2ui/standard-catalog'; + +const ReactCardRenderer: ComponentRenderer = { + componentName: 'Card', + render: (node, renderChild) => { + // A simple div wrapper for the Card component + return ( +
+ {renderChild(node.properties.child)} +
+ ); + } +}; +``` From 6f3debe942061b6d67f1dd46af29c1b9125e5727 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 14 Jan 2026 11:17:53 +1030 Subject: [PATCH 02/19] update plan --- types_design.md | 200 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 182 insertions(+), 18 deletions(-) diff --git a/types_design.md b/types_design.md index b07aba21..830ad1ad 100644 --- a/types_design.md +++ b/types_design.md @@ -3,7 +3,7 @@ This document outlines a TypeScript-based type system for A2UI web renderers. The primary goal is to create a decoupled, extensible, and type-safe architecture that separates the core A2UI message processing logic from the framework-specific rendering implementations (e.g., Lit, Angular, React). This design enables an ecosystem where: -- A central, framework-agnostic `A2uiWebRenderer` handles all protocol logic. +- A central, framework-agnostic `A2uiMessageProcessor` handles all protocol logic. - Framework-specific renderers (`@a2ui/angular`, `@a2ui/lit`) consume the output of the core renderer. - Component libraries (e.g., `@a2ui/material-catalog`) can provide framework-agnostic API definitions and framework-specific rendering implementations. @@ -11,7 +11,7 @@ This design enables an ecosystem where: The system is built around a few key interfaces that clearly define the responsibilities of each part of the architecture. -### `A2uiWebRenderer` +### `A2uiMessageProcessor` The central, framework-agnostic engine. Its role is to process raw A2UI messages, manage the state of each UI surface (data models, component definitions), and resolve the incoming data into a structured, typed, and fully-resolved node tree. It has no knowledge of how these nodes will be rendered. @@ -26,7 +26,7 @@ interface SurfaceState { } // The core, framework-agnostic processor. -interface A2uiWebRenderer { +interface A2uiMessageProcessor { /** * Processes an array of raw A2UI messages from the server. * This updates the internal state and triggers changes in the resolved node trees. @@ -88,7 +88,7 @@ interface ComponentApi< * properties required by the final resolved node. This logic is specific * to each component. * @param unresolvedProperties The raw properties from the message. - * @param resolver A callback provided by the A2uiWebRenderer to recursively + * @param resolver A callback provided by the A2uiMessageProcessor to recursively * resolve values (e.g., data bindings, child component IDs). * @returns The resolved properties for the node. */ @@ -194,7 +194,7 @@ class FrameworkRenderer { } /** - * Renders a resolved node from the A2uiWebRenderer into the final output. + * Renders a resolved node from the A2uiMessageProcessor into the final output. * This is the entry point for rendering a component tree. */ public renderNode(node: AnyResolvedNode): RenderOutput | null { @@ -211,28 +211,192 @@ class FrameworkRenderer { } ``` -## 2. Workflow and Data Flow +## 2. Codebase Structure and File Examples + +To make this architecture concrete, here is a proposed file structure and examples for a `Card` component. + +### Directory Structure + +The proposed structure separates framework-agnostic core logic from framework-specific implementations. This allows for maximum code reuse and clarity. + +``` +renderers/ +└── lit/ + └── src/ + └── 0.8/ + ├── core/ + │ ├── a2ui_message_processor.ts # Core processor logic + │ ├── types.ts # Core type definitions + │ └── standard_catalog_api/ + │ ├── standard_catalog.ts # The main CatalogApi instance + │ ├── card.ts # Card ComponentApi definition + │ └── text.ts # Text ComponentApi definition + │ + └── lit/ + ├── lit_renderer.ts # The FrameworkRenderer for Lit + ├── components/ + │ ├── card.ts # Lit component + │ └── text.ts # Lit component + └── standard_catalog_implementation/ + ├── standard_lit_catalog.ts # The CatalogImplementation for Lit + ├── card.ts # Card ComponentRenderer for Lit + └── text.ts # Text ComponentRenderer for Lit +``` + +### Example: `Card` Component Files + +#### 1. Core API Definition (`core/standard_catalog_api/card.ts`) + +This file is framework-agnostic. It defines the `Card`'s data structure and its property resolution logic. + +```typescript +// core/standard_catalog_api/card.ts + +import { + ComponentApi, + BaseResolvedNode, + AnyResolvedNode, +} from '../types'; + +// 1. Define the final, resolved shape of the Card node. +export interface CardResolvedNode extends BaseResolvedNode<'Card'> { + properties: { + child: AnyResolvedNode; + }; +} + +// 2. Implement the ComponentApi for 'Card'. +export const cardApi: ComponentApi<'Card', CardResolvedNode> = { + name: 'Card', + + resolveProperties(unresolved, resolver) { + if (!unresolved || typeof unresolved.child !== 'string') { + throw new Error('Invalid properties for Card: missing child ID.'); + } + // Use the resolver provided by A2uiMessageProcessor to turn the child ID + // into a fully resolved node. + const resolvedChild = resolver(unresolved.child) as AnyResolvedNode; + + return { + child: resolvedChild, + }; + }, +}; +``` + +#### 2. Lit Component Implementation (`lit/components/card.ts`) + +This is the standard Lit web component that will be rendered to the DOM. It receives a fully resolved node. + +```typescript +// lit/components/card.ts + +import { LitElement, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { CardResolvedNode } from '../../core/standard_catalog_api/card'; + +@customElement('a2ui-card') +export class A2UICard extends LitElement { + @property({ attribute: false }) + node!: CardResolvedNode; + + @property({ attribute: false }) + renderChild!: (child: AnyResolvedNode) => TemplateResult | null; + + render() { + return html` +
+ ${this.renderChild(this.node.properties.child)} +
+ `; + } +} +``` + +#### 3. Lit Renderer Implementation (`lit/standard_catalog_implementation/card.ts`) + +This file acts as the glue, mapping the `Card` component API to its Lit rendering logic. + +```typescript +// lit/standard_catalog_implementation/card.ts + +import { html, TemplateResult } from 'lit'; +import { ComponentRenderer } from '../../core/types'; +import { CardResolvedNode } from '../../core/standard_catalog_api/card'; + +export const litCardRenderer: ComponentRenderer = { + componentName: 'Card', + + render(node, renderChild) { + // The renderer is responsible for creating the Lit component (``) + // and passing it the resolved node and the `renderChild` callback. + return html` + + `; + }, +}; +``` + +#### 4. Angular Implementation + +The same pattern applies to Angular. + +```typescript +// in an angular-specific folder... + +// 1. The Angular Component (`card.component.ts`) +@Component({ + selector: 'a2ui-card', + template: `
+ +
`, +}) +export class CardComponent { + @Input() node!: CardResolvedNode; + @Input() renderChild!: (child: AnyResolvedNode) => Type | null; +} + +// 2. The Angular Renderer (`angular-card-renderer.ts`) +export const angularCardRenderer: ComponentRenderer> = { + componentName: 'Card', + render(node, renderChild) { + // In Angular, the "render output" can be the Component Type itself. + // The FrameworkRenderer would then use a dynamic outlet to render it. + // We would need a way to pass the inputs (`node` and `renderChild`) dynamically. + // For simplicity, this example assumes the FrameworkRenderer handles that. + return CardComponent; + }, +}; +``` + + +## 3. Workflow and Data Flow 1. **Initialization**: * An application creates an instance of a `CatalogApi` (e.g., `StandardCatalogApi`). - * It creates an instance of the central `A2uiWebRenderer`, passing it the `catalogApi`. + * It creates an instance of the central `A2uiMessageProcessor`, passing it the `catalogApi`. * It creates a framework-specific `CatalogImplementation` (e.g., `StandardLitCatalogImplementation`), passing it the same `catalogApi` and a list of `ComponentRenderer`s for Lit. * It creates a `FrameworkRenderer` (e.g., `LitRenderer`), passing it the `catalogImplementation`. 2. **Message Processing**: - * Raw A2UI messages are fed into `a2uiWebRenderer.processMessages()`. - * The `A2uiWebRenderer` processes the messages, updating its internal data models. + * Raw A2UI messages are fed into `a2uiMessageProcessor.processMessages()`. + * The `A2uiMessageProcessor` processes the messages, updating its internal data models. * When building the component tree, it uses the `ComponentApi` from the `CatalogApi` to correctly `resolveProperties` for each node. * This updates the `componentTree` signal for the relevant surface. 3. **Rendering**: - * A top-level UI component (e.g., a Lit `` or an Angular ``) listens to the `componentTree` signal from the `A2uiWebRenderer`. + * A top-level UI component (e.g., a Lit `` or an Angular ``) listens to the `componentTree` signal from the `A2uiMessageProcessor`. * When the signal changes, it passes the new resolved node tree to the `frameworkRenderer.renderNode()`. * The `FrameworkRenderer` recursively walks the tree, using the `CatalogImplementation` to find the right `ComponentRenderer` for each node, until the entire tree is converted into the framework's renderable format. ![Data Flow Diagram](https://i.imgur.com/your-diagram-image.png) -## 3. Satisfying Use Cases +## 4. Satisfying Use Cases #### Developer of a `CatalogApi` The developer creates a new class `MyCatalogApi extends CatalogApi`, passing an array of `ComponentApi` instances to the constructor. This is a clean, framework-agnostic definition of a component library. @@ -257,17 +421,17 @@ An Angular developer, for example, would implement `ComponentRenderer`. -4. **Create React Surface Component**: A top-level `` React component would be created. It would hold the `A2uiWebRenderer` and `ReactRenderer` instances and use a hook (e.g., `useEffect` or a signal-based hook) to listen for changes to the `componentTree` signal and trigger re-renders. +4. **Create React Surface Component**: A top-level `` React component would be created. It would hold the `A2uiMessageProcessor` and `ReactRenderer` instances and use a hook (e.g., `useEffect` or a signal-based hook) to listen for changes to the `componentTree` signal and trigger re-renders. -## 4. Implementation in Existing and New Frameworks +## 5. Implementation in Existing and New Frameworks ### Refactoring the Lit Renderer The current Lit renderer (`@renderers/lit/src/0.8/ui/root.ts`) has a large `renderComponentTree` method with a `switch` statement. This would be replaced: -1. The `A2uiMessageProcessor` would evolve into the `A2uiWebRenderer`. Its `buildNodeRecursive` logic would be adapted to use the new `ComponentApi.resolveProperties` pattern. +1. The existing `A2uiMessageProcessor` would be updated to use the new `ComponentApi.resolveProperties` pattern during its `buildNodeRecursive` logic. 2. A new `LitRenderer` class (`FrameworkRenderer`) would be created. 3. Each Lit component (`a2ui-text`, `a2ui-card`, etc.) would be wrapped in a `ComponentRenderer` implementation. For example, `LitTextRenderer`'s `render` method would return `html```. 4. The `Root` element's `renderComponentTree` method would be removed. Instead, the top-level `` component would use the `LitRenderer` to render its tree. @@ -275,7 +439,7 @@ The current Lit renderer (`@renderers/lit/src/0.8/ui/root.ts`) has a large `rend ### Adapting the Angular Renderer The Angular renderer is already close to this design. -1. The `MessageProcessor` becomes the `A2uiWebRenderer`. +1. The current `MessageProcessor` would be updated to align with the new framework-agnostic `A2uiMessageProcessor` design. 2. The `Catalog` (`@renderers/angular/src/lib/rendering/catalog.ts`) concept maps directly to the `CatalogImplementation`. The existing `DEFAULT_CATALOG` would be used to create `new CatalogImplementation(standardCatalogApi, defaultRenderers)`. 3. The `Renderer` directive (`renderer.ts`) is the `FrameworkRenderer`. Its logic for dynamically creating components remains the same. 4. The `DynamicComponent` base class remains, providing the bridge between the A2UI world and Angular's DI and component model. @@ -285,13 +449,13 @@ The Angular renderer is already close to this design. ```typescript // --- ReactRenderer.tsx (The top-level surface component) --- import { useSignalEffect } from '@preact/signals-react'; -import { a2uiWebRenderer, reactRenderer } from './config'; // Assume these are configured +import { a2uiMessageProcessor, reactRenderer } from './config'; // Assume these are configured export function A2UISurface({ surfaceId }: { surfaceId: string }) { const [node, setNode] = useState(null); useSignalEffect(() => { - const surface = a2uiWebRenderer.getSurfaces().get(surfaceId); + const surface = a2uiMessageProcessor.getSurfaces().get(surfaceId); setNode(surface?.componentTree.value ?? null); }); From 06ca73adf83b002d4c3f5bd7b2b3b264f1e29516 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 14 Jan 2026 11:23:05 +1030 Subject: [PATCH 03/19] Update design --- types_design.md | 53 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/types_design.md b/types_design.md index 830ad1ad..edeec785 100644 --- a/types_design.md +++ b/types_design.md @@ -213,11 +213,11 @@ class FrameworkRenderer { ## 2. Codebase Structure and File Examples -To make this architecture concrete, here is a proposed file structure and examples for a `Card` component. +To make this architecture concrete, here is a proposed file structure and examples for a `Card` component. This structure separates the view-layer component (e.g., the Lit `LitElement`) from its A2UI integration logic (the `ComponentRenderer`), promoting a clear separation of concerns. ### Directory Structure -The proposed structure separates framework-agnostic core logic from framework-specific implementations. This allows for maximum code reuse and clarity. +The proposed structure separates framework-agnostic core logic from framework-specific implementations. ``` renderers/ @@ -235,12 +235,12 @@ renderers/ └── lit/ ├── lit_renderer.ts # The FrameworkRenderer for Lit ├── components/ - │ ├── card.ts # Lit component - │ └── text.ts # Lit component + │ ├── card.ts # Defines the LitElement + │ └── text.ts # Defines the LitElement └── standard_catalog_implementation/ - ├── standard_lit_catalog.ts # The CatalogImplementation for Lit - ├── card.ts # Card ComponentRenderer for Lit - └── text.ts # Text ComponentRenderer for Lit + ├── standard_catalog_lit.ts # Assembles the final CatalogImplementation + ├── card.ts # Defines the ComponentRenderer for Card + └── text.ts # Defines the ComponentRenderer for Text ``` ### Example: `Card` Component Files @@ -284,16 +284,17 @@ export const cardApi: ComponentApi<'Card', CardResolvedNode> = { }; ``` -#### 2. Lit Component Implementation (`lit/components/card.ts`) +#### 2. Lit Component (`lit/components/card.ts`) -This is the standard Lit web component that will be rendered to the DOM. It receives a fully resolved node. +This is the pure, presentational web component for the card. It has no direct knowledge of the `ComponentRenderer`. ```typescript // lit/components/card.ts -import { LitElement, html } from 'lit'; +import { LitElement, html, TemplateResult } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { CardResolvedNode } from '../../core/standard_catalog_api/card'; +import { AnyResolvedNode } from '../../core/types'; @customElement('a2ui-card') export class A2UICard extends LitElement { @@ -313,9 +314,9 @@ export class A2UICard extends LitElement { } ``` -#### 3. Lit Renderer Implementation (`lit/standard_catalog_implementation/card.ts`) +#### 3. Lit Renderer (`lit/standard_catalog_implementation/card.ts`) -This file acts as the glue, mapping the `Card` component API to its Lit rendering logic. +This file contains the A2UI integration logic, mapping the `Card` API to its Lit component. ```typescript // lit/standard_catalog_implementation/card.ts @@ -323,6 +324,7 @@ This file acts as the glue, mapping the `Card` component API to its Lit renderin import { html, TemplateResult } from 'lit'; import { ComponentRenderer } from '../../core/types'; import { CardResolvedNode } from '../../core/standard_catalog_api/card'; +import '../components/card.js'; // Ensure the component is defined export const litCardRenderer: ComponentRenderer = { componentName: 'Card', @@ -340,9 +342,32 @@ export const litCardRenderer: ComponentRenderer Date: Wed, 14 Jan 2026 11:55:47 +1030 Subject: [PATCH 04/19] Update lit renderer almost working --- renderers/lit/src/0.8/core.ts | 18 +- .../a2ui_message_processor.ts} | 292 ++-------- .../lit/src/0.8/{ => core}/data/guards.ts | 0 .../{ => core}/data/signal-model-processor.ts | 6 +- .../core/standard_catalog_api/audio_player.ts | 38 ++ .../0.8/core/standard_catalog_api/button.ts | 39 ++ .../src/0.8/core/standard_catalog_api/card.ts | 41 ++ .../0.8/core/standard_catalog_api/checkbox.ts | 38 ++ .../0.8/core/standard_catalog_api/column.ts | 39 ++ .../standard_catalog_api/datetime_input.ts | 40 ++ .../0.8/core/standard_catalog_api/divider.ts | 34 ++ .../src/0.8/core/standard_catalog_api/icon.ts | 37 ++ .../0.8/core/standard_catalog_api/image.ts | 39 ++ .../src/0.8/core/standard_catalog_api/list.ts | 39 ++ .../0.8/core/standard_catalog_api/modal.ts | 38 ++ .../standard_catalog_api/multiple_choice.ts | 39 ++ .../src/0.8/core/standard_catalog_api/row.ts | 39 ++ .../0.8/core/standard_catalog_api/slider.ts | 39 ++ .../standard_catalog_api/standard_catalog.ts | 56 ++ .../src/0.8/core/standard_catalog_api/tabs.ts | 37 ++ .../src/0.8/core/standard_catalog_api/text.ts | 37 ++ .../core/standard_catalog_api/text_field.ts | 40 ++ .../0.8/core/standard_catalog_api/video.ts | 37 ++ .../src/0.8/{ => core}/types/client-event.ts | 0 .../lit/src/0.8/{ => core}/types/colors.ts | 0 .../src/0.8/{ => core}/types/components.ts | 0 .../src/0.8/{ => core}/types/primitives.ts | 0 .../lit/src/0.8/{ => core}/types/types.ts | 277 +++++---- renderers/lit/src/0.8/events/a2ui.ts | 4 +- renderers/lit/src/0.8/index.ts | 2 +- .../src/0.8/{ui => lit/components}/audio.ts | 33 +- .../src/0.8/{ui => lit/components}/button.ts | 21 +- .../src/0.8/{ui => lit/components}/card.ts | 10 +- .../0.8/{ui => lit/components}/checkbox.ts | 48 +- .../src/0.8/{ui => lit/components}/column.ts | 16 +- .../components}/component-registry.ts | 0 .../{ui => lit/components}/datetime-input.ts | 60 +- .../src/0.8/{ui => lit/components}/divider.ts | 10 +- .../src/0.8/{ui => lit/components}/icon.ts | 33 +- .../src/0.8/{ui => lit/components}/image.ts | 47 +- .../src/0.8/{ui => lit/components}/list.ts | 11 +- .../src/0.8/{ui => lit/components}/modal.ts | 11 +- .../{ui => lit/components}/multiple-choice.ts | 50 +- renderers/lit/src/0.8/lit/components/root.ts | 84 +++ .../lit/src/0.8/{ui => lit/components}/row.ts | 14 +- .../src/0.8/{ui => lit/components}/slider.ts | 69 +-- .../src/0.8/{ui => lit/components}/styles.ts | 2 +- .../src/0.8/{ui => lit/components}/surface.ts | 67 ++- .../src/0.8/{ui => lit/components}/tabs.ts | 22 +- .../0.8/{ui => lit/components}/text-field.ts | 40 +- .../src/0.8/{ui => lit/components}/text.ts | 48 +- .../lit/src/0.8/{ui => lit/components}/ui.ts | 8 +- .../src/0.8/{ui => lit/components}/video.ts | 33 +- .../lit/src/0.8/{ui => lit}/context/theme.ts | 2 +- .../{ui => lit}/custom-components/index.ts | 2 +- .../0.8/{ui => lit}/directives/directives.ts | 0 .../0.8/{ui => lit}/directives/markdown.ts | 0 .../0.8/{ui => lit}/directives/sanitizer.ts | 0 renderers/lit/src/0.8/lit/lit_renderer.ts | 27 + .../audio_player.ts | 33 ++ .../standard_catalog_implementation/button.ts | 33 ++ .../standard_catalog_implementation/card.ts | 33 ++ .../checkbox.ts | 33 ++ .../standard_catalog_implementation/column.ts | 33 ++ .../datetime_input.ts | 33 ++ .../divider.ts | 33 ++ .../standard_catalog_implementation/icon.ts | 33 ++ .../standard_catalog_implementation/image.ts | 33 ++ .../standard_catalog_implementation/list.ts | 33 ++ .../standard_catalog_implementation/modal.ts | 33 ++ .../multiple_choice.ts | 33 ++ .../standard_catalog_implementation/row.ts | 33 ++ .../standard_catalog_implementation/slider.ts | 33 ++ .../standard_catalog_lit.ts | 60 ++ .../standard_catalog_implementation/tabs.ts | 33 ++ .../standard_catalog_implementation/text.ts | 33 ++ .../text_field.ts | 33 ++ .../standard_catalog_implementation/video.ts | 33 ++ .../lit/src/0.8/{ui => lit}/utils/utils.ts | 13 +- .../lit/src/0.8/{ui => lit}/utils/youtube.ts | 0 renderers/lit/src/0.8/model.test.ts | 7 +- renderers/lit/src/0.8/styles/colors.ts | 2 +- renderers/lit/src/0.8/styles/utils.ts | 2 +- renderers/lit/src/0.8/ui/root.ts | 531 ------------------ 84 files changed, 2065 insertions(+), 1257 deletions(-) rename renderers/lit/src/0.8/{data/model-processor.ts => core/a2ui_message_processor.ts} (71%) rename renderers/lit/src/0.8/{ => core}/data/guards.ts (100%) rename renderers/lit/src/0.8/{ => core}/data/signal-model-processor.ts (85%) create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/audio_player.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/button.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/card.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/checkbox.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/column.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/datetime_input.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/divider.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/icon.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/image.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/list.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/modal.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/multiple_choice.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/row.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/slider.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/standard_catalog.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/tabs.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/text.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/text_field.ts create mode 100644 renderers/lit/src/0.8/core/standard_catalog_api/video.ts rename renderers/lit/src/0.8/{ => core}/types/client-event.ts (100%) rename renderers/lit/src/0.8/{ => core}/types/colors.ts (100%) rename renderers/lit/src/0.8/{ => core}/types/components.ts (100%) rename renderers/lit/src/0.8/{ => core}/types/primitives.ts (100%) rename renderers/lit/src/0.8/{ => core}/types/types.ts (63%) rename renderers/lit/src/0.8/{ui => lit/components}/audio.ts (72%) rename renderers/lit/src/0.8/{ui => lit/components}/button.ts (80%) rename renderers/lit/src/0.8/{ui => lit/components}/card.ts (86%) rename renderers/lit/src/0.8/{ui => lit/components}/checkbox.ts (71%) rename renderers/lit/src/0.8/{ui => lit/components}/column.ts (85%) rename renderers/lit/src/0.8/{ui => lit/components}/component-registry.ts (100%) rename renderers/lit/src/0.8/{ui => lit/components}/datetime-input.ts (71%) rename renderers/lit/src/0.8/{ui => lit/components}/divider.ts (89%) rename renderers/lit/src/0.8/{ui => lit/components}/icon.ts (74%) rename renderers/lit/src/0.8/{ui => lit/components}/image.ts (66%) rename renderers/lit/src/0.8/{ui => lit/components}/list.ts (88%) rename renderers/lit/src/0.8/{ui => lit/components}/modal.ts (92%) rename renderers/lit/src/0.8/{ui => lit/components}/multiple-choice.ts (70%) create mode 100644 renderers/lit/src/0.8/lit/components/root.ts rename renderers/lit/src/0.8/{ui => lit/components}/row.ts (88%) rename renderers/lit/src/0.8/{ui => lit/components}/slider.ts (65%) rename renderers/lit/src/0.8/{ui => lit/components}/styles.ts (88%) rename renderers/lit/src/0.8/{ui => lit/components}/surface.ts (67%) rename renderers/lit/src/0.8/{ui => lit/components}/tabs.ts (88%) rename renderers/lit/src/0.8/{ui => lit/components}/text-field.ts (75%) rename renderers/lit/src/0.8/{ui => lit/components}/text.ts (75%) rename renderers/lit/src/0.8/{ui => lit/components}/ui.ts (95%) rename renderers/lit/src/0.8/{ui => lit/components}/video.ts (72%) rename renderers/lit/src/0.8/{ui => lit}/context/theme.ts (92%) rename renderers/lit/src/0.8/{ui => lit}/custom-components/index.ts (90%) rename renderers/lit/src/0.8/{ui => lit}/directives/directives.ts (100%) rename renderers/lit/src/0.8/{ui => lit}/directives/markdown.ts (100%) rename renderers/lit/src/0.8/{ui => lit}/directives/sanitizer.ts (100%) create mode 100644 renderers/lit/src/0.8/lit/lit_renderer.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/audio_player.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/button.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/card.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/checkbox.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/column.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/datetime_input.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/divider.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/icon.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/image.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/list.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/modal.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/multiple_choice.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/row.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/slider.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/standard_catalog_lit.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/tabs.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/text.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/text_field.ts create mode 100644 renderers/lit/src/0.8/lit/standard_catalog_implementation/video.ts rename renderers/lit/src/0.8/{ui => lit}/utils/utils.ts (85%) rename renderers/lit/src/0.8/{ui => lit}/utils/youtube.ts (100%) delete mode 100644 renderers/lit/src/0.8/ui/root.ts diff --git a/renderers/lit/src/0.8/core.ts b/renderers/lit/src/0.8/core.ts index 9c16e747..f19fbbfe 100644 --- a/renderers/lit/src/0.8/core.ts +++ b/renderers/lit/src/0.8/core.ts @@ -15,21 +15,19 @@ */ export * as Events from "./events/events.js"; -export * as Types from "./types/types.js"; -export * as Primitives from "./types/primitives.js"; +export * as Types from "./core/types/types.js"; +export * as Primitives from "./core/types/primitives.js"; export * as Styles from "./styles/index.js"; -import * as Guards from "./data/guards.js"; +export { CatalogApi } from "./core/types/types.js"; +export { type ComponentApi } from "./core/types/types.js"; +export { standardCatalogApi } from "./core/standard_catalog_api/standard_catalog.js"; +import * as Guards from "./core/data/guards.js"; -import { create as createSignalA2uiMessageProcessor } from "./data/signal-model-processor.js"; -import { A2uiMessageProcessor } from "./data/model-processor.js"; -import A2UIClientEventMessage from "./schemas/server_to_client_with_standard_catalog.json" with { type: "json" }; +import { create as createSignalA2uiMessageProcessor } from "./core/data/signal-model-processor.js"; +import { A2uiMessageProcessor } from "./core/a2ui_message_processor.js"; export const Data = { createSignalA2uiMessageProcessor, A2uiMessageProcessor, Guards, }; - -export const Schemas = { - A2UIClientEventMessage, -}; diff --git a/renderers/lit/src/0.8/data/model-processor.ts b/renderers/lit/src/0.8/core/a2ui_message_processor.ts similarity index 71% rename from renderers/lit/src/0.8/data/model-processor.ts rename to renderers/lit/src/0.8/core/a2ui_message_processor.ts index 3b08f699..b7925ddd 100644 --- a/renderers/lit/src/0.8/data/model-processor.ts +++ b/renderers/lit/src/0.8/core/a2ui_message_processor.ts @@ -31,31 +31,14 @@ import { MessageProcessor, ValueMap, DataObject, -} from "../types/types"; + CatalogApi, + AnyResolvedNode, +} from "./types/types.js"; import { isComponentArrayReference, isObject, isPath, - isResolvedAudioPlayer, - isResolvedButton, - isResolvedCard, - isResolvedCheckbox, - isResolvedColumn, - isResolvedDateTimeInput, - isResolvedDivider, - isResolvedIcon, - isResolvedImage, - isResolvedList, - isResolvedModal, - isResolvedMultipleChoice, - isResolvedRow, - isResolvedSlider, - isResolvedTabs, - isResolvedText, - isResolvedTextField, - isResolvedVideo, - isValueMap, -} from "./guards.js"; +} from "./data/guards.js"; /** * Processes and consolidates A2UIProtocolMessage objects into a structured, @@ -69,21 +52,23 @@ export class A2uiMessageProcessor implements MessageProcessor { private setCtor: SetConstructor = Set; private objCtor: ObjectConstructor = Object; private surfaces: Map; + private readonly catalog: CatalogApi; constructor( readonly opts: { - mapCtor: MapConstructor; - arrayCtor: ArrayConstructor; - setCtor: SetConstructor; - objCtor: ObjectConstructor; - } = { mapCtor: Map, arrayCtor: Array, setCtor: Set, objCtor: Object } + mapCtor?: MapConstructor; + arrayCtor?: ArrayConstructor; + setCtor?: SetConstructor; + objCtor?: ObjectConstructor; + catalog: CatalogApi; + } ) { - this.arrayCtor = opts.arrayCtor; - this.mapCtor = opts.mapCtor; - this.setCtor = opts.setCtor; - this.objCtor = opts.objCtor; - - this.surfaces = new opts.mapCtor(); + this.arrayCtor = opts.arrayCtor ?? Array; + this.mapCtor = opts.mapCtor ?? Map; + this.setCtor = opts.setCtor ?? Set; + this.objCtor = opts.objCtor ?? Object; + this.surfaces = new (this.mapCtor)(); + this.catalog = opts.catalog; } getSurfaces(): ReadonlyMap { @@ -129,7 +114,7 @@ export class A2uiMessageProcessor implements MessageProcessor { * own data context. */ getData( - node: AnyComponentNode, + node: AnyResolvedNode, relativePath: string, surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID ): DataValue | null { @@ -151,7 +136,7 @@ export class A2uiMessageProcessor implements MessageProcessor { } setData( - node: AnyComponentNode | null, + node: AnyResolvedNode | null, relativePath: string, value: DataValue, surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID @@ -497,222 +482,41 @@ export class A2uiMessageProcessor implements MessageProcessor { const unresolvedProperties = componentProps[componentType as keyof typeof componentProps]; - // Manually build the resolvedProperties object by resolving each value in - // the component's properties. - const resolvedProperties: ResolvedMap = new this.objCtor() as ResolvedMap; - if (isObject(unresolvedProperties)) { - for (const [key, value] of Object.entries(unresolvedProperties)) { - resolvedProperties[key] = this.resolvePropertyValue( - value, - surface, - visited, - dataContextPath, - idSuffix - ); - } + const resolver = (value: unknown) => this.resolvePropertyValue( + value, + surface, + visited, + dataContextPath, + idSuffix + ); + + const catalogItem = this.catalog.get(componentType); + if (!catalogItem) { + // Fallback for unknown (custom) components. + return new this.objCtor({ + id: fullId, + dataContextPath, + weight: componentData.weight ?? "initial", + type: componentType, + properties: isObject(unresolvedProperties) ? Object.fromEntries(Object.entries(unresolvedProperties).map(([key, value]) => [key, resolver(value)])) : {}, + }) as AnyComponentNode; } + + const resolvedProperties = catalogItem.resolveProperties( + isObject(unresolvedProperties) ? unresolvedProperties : {}, + resolver + ); + visited.delete(fullId); - // Now that we have the resolved properties in place we can go ahead and - // ensure that they meet expectations in terms of types and so forth, - // casting them into the specific shape for usage. - const baseNode = { + return new this.objCtor({ id: fullId, dataContextPath, weight: componentData.weight ?? "initial", - }; - switch (componentType) { - case "Text": - if (!isResolvedText(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "Text", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "Image": - if (!isResolvedImage(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "Image", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "Icon": - if (!isResolvedIcon(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "Icon", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "Video": - if (!isResolvedVideo(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "Video", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "AudioPlayer": - if (!isResolvedAudioPlayer(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "AudioPlayer", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "Row": - if (!isResolvedRow(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - - return new this.objCtor({ - ...baseNode, - type: "Row", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "Column": - if (!isResolvedColumn(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - - return new this.objCtor({ - ...baseNode, - type: "Column", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "List": - if (!isResolvedList(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "List", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "Card": - if (!isResolvedCard(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "Card", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "Tabs": - if (!isResolvedTabs(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "Tabs", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "Divider": - if (!isResolvedDivider(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "Divider", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "Modal": - if (!isResolvedModal(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "Modal", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "Button": - if (!isResolvedButton(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "Button", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "CheckBox": - if (!isResolvedCheckbox(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "CheckBox", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "TextField": - if (!isResolvedTextField(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "TextField", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "DateTimeInput": - if (!isResolvedDateTimeInput(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "DateTimeInput", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "MultipleChoice": - if (!isResolvedMultipleChoice(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "MultipleChoice", - properties: resolvedProperties, - }) as AnyComponentNode; - - case "Slider": - if (!isResolvedSlider(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.objCtor({ - ...baseNode, - type: "Slider", - properties: resolvedProperties, - }) as AnyComponentNode; - - default: - // Catch-all for other custom component types. - return new this.objCtor({ - ...baseNode, - type: componentType, - properties: resolvedProperties, - }) as AnyComponentNode; - } + type: componentType, + ...resolvedProperties, + }) as AnyComponentNode; } /** @@ -852,4 +656,4 @@ export class A2uiMessageProcessor implements MessageProcessor { // 5. Otherwise, it's a primitive value. return value as ResolvedValue; } -} +} \ No newline at end of file diff --git a/renderers/lit/src/0.8/data/guards.ts b/renderers/lit/src/0.8/core/data/guards.ts similarity index 100% rename from renderers/lit/src/0.8/data/guards.ts rename to renderers/lit/src/0.8/core/data/guards.ts diff --git a/renderers/lit/src/0.8/data/signal-model-processor.ts b/renderers/lit/src/0.8/core/data/signal-model-processor.ts similarity index 85% rename from renderers/lit/src/0.8/data/signal-model-processor.ts rename to renderers/lit/src/0.8/core/data/signal-model-processor.ts index e821c152..d7e2ab37 100644 --- a/renderers/lit/src/0.8/data/signal-model-processor.ts +++ b/renderers/lit/src/0.8/core/data/signal-model-processor.ts @@ -14,18 +14,20 @@ limitations under the License. */ -import { A2uiMessageProcessor } from "./model-processor.js"; +import { A2uiMessageProcessor } from "../a2ui_message_processor.js"; +import { CatalogApi } from "../types/types.js"; import { SignalArray } from "signal-utils/array"; import { SignalMap } from "signal-utils/map"; import { SignalObject } from "signal-utils/object"; import { SignalSet } from "signal-utils/set"; -export function create() { +export function create(catalog: CatalogApi) { return new A2uiMessageProcessor({ arrayCtor: SignalArray as unknown as ArrayConstructor, mapCtor: SignalMap as unknown as MapConstructor, objCtor: SignalObject as unknown as ObjectConstructor, setCtor: SignalSet as unknown as SetConstructor, + catalog, }); } diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/audio_player.ts b/renderers/lit/src/0.8/core/standard_catalog_api/audio_player.ts new file mode 100644 index 00000000..61b057a4 --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/audio_player.ts @@ -0,0 +1,38 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + AudioPlayerNode, +} from '../types/types'; +import { StringValue } from '../types/primitives'; + +export const audioPlayerApi: ComponentApi<'AudioPlayer', AudioPlayerNode> = { + name: 'AudioPlayer', + + resolveProperties(unresolved) { + if (!unresolved || typeof unresolved.url !== 'object') { + throw new Error('Invalid properties for AudioPlayer: missing url.'); + } + + return { + properties: { + url: unresolved.url as StringValue, + description: unresolved.description as StringValue, + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/button.ts b/renderers/lit/src/0.8/core/standard_catalog_api/button.ts new file mode 100644 index 00000000..298a934f --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/button.ts @@ -0,0 +1,39 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + AnyResolvedNode, + ButtonNode, + Action, +} from '../types/types'; + +export const buttonApi: ComponentApi<'Button', ButtonNode> = { + name: 'Button', + + resolveProperties(unresolved, resolver) { + if (!unresolved || typeof unresolved.child !== 'string' || !unresolved.action) { + throw new Error('Invalid properties for Button: missing child or action.'); + } + + return { + properties: { + child: resolver(unresolved.child) as AnyResolvedNode, + action: unresolved.action as Action, + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/card.ts b/renderers/lit/src/0.8/core/standard_catalog_api/card.ts new file mode 100644 index 00000000..129e2798 --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/card.ts @@ -0,0 +1,41 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + AnyResolvedNode, + CardNode, +} from '../types/types'; + +export const cardApi: ComponentApi<'Card', CardNode> = { + name: 'Card', + + resolveProperties(unresolved, resolver) { + if (!unresolved || (typeof unresolved.child !== 'string' && !Array.isArray(unresolved.children))) { + throw new Error('Invalid properties for Card: missing child or children.'); + } + + const child = resolver(unresolved.child) as AnyResolvedNode; + const children = Array.isArray(unresolved.children) ? resolver(unresolved.children) as AnyResolvedNode[] : []; + + return { + properties: { + child, + children + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/checkbox.ts b/renderers/lit/src/0.8/core/standard_catalog_api/checkbox.ts new file mode 100644 index 00000000..5e473ac6 --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/checkbox.ts @@ -0,0 +1,38 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + CheckboxNode, +} from '../types/types'; +import { StringValue, BooleanValue } from '../types/primitives'; + +export const checkboxApi: ComponentApi<'CheckBox', CheckboxNode> = { + name: 'CheckBox', + + resolveProperties(unresolved) { + if (!unresolved || typeof unresolved.label !== 'object' || typeof unresolved.value !== 'object') { + throw new Error('Invalid properties for CheckBox: missing label or value.'); + } + + return { + properties: { + label: unresolved.label as StringValue, + value: unresolved.value as BooleanValue, + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/column.ts b/renderers/lit/src/0.8/core/standard_catalog_api/column.ts new file mode 100644 index 00000000..551c953b --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/column.ts @@ -0,0 +1,39 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + AnyResolvedNode, + ColumnNode, +} from '../types/types'; + +export const columnApi: ComponentApi<'Column', ColumnNode> = { + name: 'Column', + + resolveProperties(unresolved, resolver) { + if (!unresolved || !unresolved.children) { + throw new Error('Invalid properties for Column: missing children.'); + } + + return { + properties: { + children: resolver(unresolved.children) as AnyResolvedNode[], + distribution: unresolved.distribution as ColumnNode['properties']['distribution'], + alignment: unresolved.alignment as ColumnNode['properties']['alignment'], + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/datetime_input.ts b/renderers/lit/src/0.8/core/standard_catalog_api/datetime_input.ts new file mode 100644 index 00000000..60ad3bf8 --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/datetime_input.ts @@ -0,0 +1,40 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + DateTimeInputNode, +} from '../types/types'; +import { StringValue } from '../types/primitives'; + +export const dateTimeInputApi: ComponentApi<'DateTimeInput', DateTimeInputNode> = { + name: 'DateTimeInput', + + resolveProperties(unresolved) { + if (!unresolved || typeof unresolved.value !== 'object') { + throw new Error('Invalid properties for DateTimeInput: missing value.'); + } + + return { + properties: { + value: unresolved.value as StringValue, + enableDate: unresolved.enableDate as boolean, + enableTime: unresolved.enableTime as boolean, + outputFormat: unresolved.outputFormat as string, + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/divider.ts b/renderers/lit/src/0.8/core/standard_catalog_api/divider.ts new file mode 100644 index 00000000..d2ab333c --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/divider.ts @@ -0,0 +1,34 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + DividerNode, +} from '../types/types'; + +export const dividerApi: ComponentApi<'Divider', DividerNode> = { + name: 'Divider', + + resolveProperties(unresolved) { + return { + properties: { + axis: unresolved.axis as DividerNode['properties']['axis'], + color: unresolved.color as DividerNode['properties']['color'], + thickness: unresolved.thickness as DividerNode['properties']['thickness'], + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/icon.ts b/renderers/lit/src/0.8/core/standard_catalog_api/icon.ts new file mode 100644 index 00000000..dca8875f --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/icon.ts @@ -0,0 +1,37 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + IconNode, +} from '../types/types'; +import { StringValue } from '../types/primitives'; + +export const iconApi: ComponentApi<'Icon', IconNode> = { + name: 'Icon', + + resolveProperties(unresolved) { + if (!unresolved || typeof unresolved.name !== 'object') { + throw new Error('Invalid properties for Icon: missing name.'); + } + + return { + properties: { + name: unresolved.name as StringValue, + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/image.ts b/renderers/lit/src/0.8/core/standard_catalog_api/image.ts new file mode 100644 index 00000000..f3c0993e --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/image.ts @@ -0,0 +1,39 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + ImageNode, +} from '../types/types'; +import { StringValue } from '../types/primitives'; + +export const imageApi: ComponentApi<'Image', ImageNode> = { + name: 'Image', + + resolveProperties(unresolved) { + if (!unresolved || typeof unresolved.url !== 'object') { + throw new Error('Invalid properties for Image: missing url.'); + } + + return { + properties: { + url: unresolved.url as StringValue, + usageHint: unresolved.usageHint as ImageNode['properties']['usageHint'], + fit: unresolved.fit as ImageNode['properties']['fit'], + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/list.ts b/renderers/lit/src/0.8/core/standard_catalog_api/list.ts new file mode 100644 index 00000000..975b131e --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/list.ts @@ -0,0 +1,39 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + AnyResolvedNode, + ListNode, +} from '../types/types'; + +export const listApi: ComponentApi<'List', ListNode> = { + name: 'List', + + resolveProperties(unresolved, resolver) { + if (!unresolved || !unresolved.children) { + throw new Error('Invalid properties for List: missing children.'); + } + + return { + properties: { + children: resolver(unresolved.children) as AnyResolvedNode[], + direction: unresolved.direction as ListNode['properties']['direction'], + alignment: unresolved.alignment as ListNode['properties']['alignment'], + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/modal.ts b/renderers/lit/src/0.8/core/standard_catalog_api/modal.ts new file mode 100644 index 00000000..80f44c23 --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/modal.ts @@ -0,0 +1,38 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + AnyResolvedNode, + ModalNode, +} from '../types/types'; + +export const modalApi: ComponentApi<'Modal', ModalNode> = { + name: 'Modal', + + resolveProperties(unresolved, resolver) { + if (!unresolved || typeof unresolved.entryPointChild !== 'string' || typeof unresolved.contentChild !== 'string') { + throw new Error('Invalid properties for Modal: missing entryPointChild or contentChild.'); + } + + return { + properties: { + entryPointChild: resolver(unresolved.entryPointChild) as AnyResolvedNode, + contentChild: resolver(unresolved.contentChild) as AnyResolvedNode, + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/multiple_choice.ts b/renderers/lit/src/0.8/core/standard_catalog_api/multiple_choice.ts new file mode 100644 index 00000000..7c807366 --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/multiple_choice.ts @@ -0,0 +1,39 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + MultipleChoiceNode, +} from '../types/types'; +import { StringValue } from '../types/primitives'; + +export const multipleChoiceApi: ComponentApi<'MultipleChoice', MultipleChoiceNode> = { + name: 'MultipleChoice', + + resolveProperties(unresolved) { + if (!unresolved || typeof unresolved.selections !== 'object' || !Array.isArray(unresolved.options)) { + throw new Error('Invalid properties for MultipleChoice: missing selections or options.'); + } + + return { + properties: { + selections: unresolved.selections as { path?: string; literalArray?: string[] }, + options: unresolved.options as { label: StringValue, value: string }[], + maxAllowedSelections: unresolved.maxAllowedSelections as number, + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/row.ts b/renderers/lit/src/0.8/core/standard_catalog_api/row.ts new file mode 100644 index 00000000..459d7920 --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/row.ts @@ -0,0 +1,39 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + AnyResolvedNode, + RowNode, +} from '../types/types'; + +export const rowApi: ComponentApi<'Row', RowNode> = { + name: 'Row', + + resolveProperties(unresolved, resolver) { + if (!unresolved || !unresolved.children) { + throw new Error('Invalid properties for Row: missing children.'); + } + + return { + properties: { + children: resolver(unresolved.children) as AnyResolvedNode[], + distribution: unresolved.distribution as RowNode['properties']['distribution'], + alignment: unresolved.alignment as RowNode['properties']['alignment'], + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/slider.ts b/renderers/lit/src/0.8/core/standard_catalog_api/slider.ts new file mode 100644 index 00000000..fabed83e --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/slider.ts @@ -0,0 +1,39 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + SliderNode, +} from '../types/types'; +import { NumberValue } from '../types/primitives'; + +export const sliderApi: ComponentApi<'Slider', SliderNode> = { + name: 'Slider', + + resolveProperties(unresolved) { + if (!unresolved || typeof unresolved.value !== 'object') { + throw new Error('Invalid properties for Slider: missing value.'); + } + + return { + properties: { + value: unresolved.value as NumberValue, + minValue: unresolved.minValue as number, + maxValue: unresolved.maxValue as number, + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/standard_catalog.ts b/renderers/lit/src/0.8/core/standard_catalog_api/standard_catalog.ts new file mode 100644 index 00000000..111a8550 --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/standard_catalog.ts @@ -0,0 +1,56 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { CatalogApi } from '../types/types'; +import { audioPlayerApi } from './audio_player'; +import { buttonApi } from './button'; +import { cardApi } from './card'; +import { checkboxApi } from './checkbox'; +import { columnApi } from './column'; +import { dateTimeInputApi } from './datetime_input'; +import { dividerApi } from './divider'; +import { iconApi } from './icon'; +import { imageApi } from './image'; +import { listApi } from './list'; +import { modalApi } from './modal'; +import { multipleChoiceApi } from './multiple_choice'; +import { rowApi } from './row'; +import { sliderApi } from './slider'; +import { tabsApi } from './tabs'; +import { textFieldApi } from './text_field'; +import { textApi } from './text'; +import { videoApi } from './video'; + +export const standardCatalogApi = new CatalogApi([ + audioPlayerApi, + buttonApi, + cardApi, + checkboxApi, + columnApi, + dateTimeInputApi, + dividerApi, + iconApi, + imageApi, + listApi, + modalApi, + multipleChoiceApi, + rowApi, + sliderApi, + tabsApi, + textFieldApi, + textApi, + videoApi, +]); diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/tabs.ts b/renderers/lit/src/0.8/core/standard_catalog_api/tabs.ts new file mode 100644 index 00000000..96416225 --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/tabs.ts @@ -0,0 +1,37 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + TabsNode, + ResolvedTabItem, +} from '../types/types'; + +export const tabsApi: ComponentApi<'Tabs', TabsNode> = { + name: 'Tabs', + + resolveProperties(unresolved, resolver) { + if (!unresolved || !Array.isArray(unresolved.tabItems)) { + throw new Error('Invalid properties for Tabs: missing tabItems.'); + } + + return { + properties: { + tabItems: resolver(unresolved.tabItems) as ResolvedTabItem[], + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/text.ts b/renderers/lit/src/0.8/core/standard_catalog_api/text.ts new file mode 100644 index 00000000..388b75a8 --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/text.ts @@ -0,0 +1,37 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law of an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + TextNode, +} from '../types/types'; +import { StringValue } from '../types/primitives'; + +export const textApi: ComponentApi<'Text', TextNode> = { + name: 'Text', + + resolveProperties(unresolved) { + if (!unresolved || typeof unresolved.text !== 'object') { + throw new Error('Invalid properties for Text: missing text.'); + } + + return { + properties: { + text: unresolved.text as StringValue, + usageHint: unresolved.usageHint as TextNode['properties']['usageHint'], + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/text_field.ts b/renderers/lit/src/0.8/core/standard_catalog_api/text_field.ts new file mode 100644 index 00000000..dd496849 --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/text_field.ts @@ -0,0 +1,40 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + TextFieldNode, +} from '../types/types'; +import { StringValue } from '../types/primitives'; + +export const textFieldApi: ComponentApi<'TextField', TextFieldNode> = { + name: 'TextField', + + resolveProperties(unresolved) { + if (!unresolved || typeof unresolved.label !== 'object') { + throw new Error('Invalid properties for TextField: missing label.'); + } + + return { + properties: { + label: unresolved.label as StringValue, + text: unresolved.text as StringValue, + type: unresolved.type as TextFieldNode['properties']['type'], + validationRegexp: unresolved.validationRegexp as string, + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/video.ts b/renderers/lit/src/0.8/core/standard_catalog_api/video.ts new file mode 100644 index 00000000..654186c3 --- /dev/null +++ b/renderers/lit/src/0.8/core/standard_catalog_api/video.ts @@ -0,0 +1,37 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { + ComponentApi, + VideoNode, +} from '../types/types'; +import { StringValue } from '../types/primitives'; + +export const videoApi: ComponentApi<'Video', VideoNode> = { + name: 'Video', + + resolveProperties(unresolved) { + if (!unresolved || typeof unresolved.url !== 'object') { + throw new Error('Invalid properties for Video: missing url.'); + } + + return { + properties: { + url: unresolved.url as StringValue, + } + }; + }, +}; diff --git a/renderers/lit/src/0.8/types/client-event.ts b/renderers/lit/src/0.8/core/types/client-event.ts similarity index 100% rename from renderers/lit/src/0.8/types/client-event.ts rename to renderers/lit/src/0.8/core/types/client-event.ts diff --git a/renderers/lit/src/0.8/types/colors.ts b/renderers/lit/src/0.8/core/types/colors.ts similarity index 100% rename from renderers/lit/src/0.8/types/colors.ts rename to renderers/lit/src/0.8/core/types/colors.ts diff --git a/renderers/lit/src/0.8/types/components.ts b/renderers/lit/src/0.8/core/types/components.ts similarity index 100% rename from renderers/lit/src/0.8/types/components.ts rename to renderers/lit/src/0.8/core/types/components.ts diff --git a/renderers/lit/src/0.8/types/primitives.ts b/renderers/lit/src/0.8/core/types/primitives.ts similarity index 100% rename from renderers/lit/src/0.8/types/primitives.ts rename to renderers/lit/src/0.8/core/types/primitives.ts diff --git a/renderers/lit/src/0.8/types/types.ts b/renderers/lit/src/0.8/core/types/types.ts similarity index 63% rename from renderers/lit/src/0.8/types/types.ts rename to renderers/lit/src/0.8/core/types/types.ts index 1e1f6686..1c8ae024 100644 --- a/renderers/lit/src/0.8/types/types.ts +++ b/renderers/lit/src/0.8/core/types/types.ts @@ -14,12 +14,6 @@ limitations under the License. */ -export { - type ClientToServerMessage as A2UIClientEventMessage, - type ClientCapabilitiesDynamic, -} from "./client-event.js"; -export { type Action } from "./components.js"; - import { AudioPlayer, Button, @@ -29,12 +23,133 @@ import { Icon, Image, MultipleChoice, - Slider, Text, TextField, Video, -} from "./components"; -import { StringValue } from "./primitives"; +} from "./components.js"; +import { StringValue, NumberValue, BooleanValue } from "./primitives.js"; +export { + type ClientToServerMessage as A2UIClientEventMessage, + type ClientCapabilitiesDynamic, + type UserAction, +} from "./client-event.js"; +export { type Action } from "./components.js"; + +// Base interface for any resolved component node. +export interface BaseResolvedNode { + id: string; + type: TName; + weight: number | 'initial'; + dataContextPath?: string; + slotName?: string; +} + +// Interface for a component's definition. +export interface ComponentApi< + TName extends string, + RNode extends BaseResolvedNode +> { + readonly name: TName; + + /** + * Resolves the raw properties from the A2UI message into the typed + * properties required by the final resolved node. This logic is specific + * to each component. + * @param unresolvedProperties The raw properties from the message. + * @param resolver A callback provided by the A2uiMessageProcessor to recursively + * resolve values (e.g., data bindings, child component IDs). + * @returns The resolved properties for the node. + */ + resolveProperties( + unresolvedProperties: Record, + resolver: (value: unknown) => unknown + ): Omit>; +} + +export type AnyComponentApi = ComponentApi; + +export class CatalogApi { + private readonly components: Map; + + constructor(components: AnyComponentApi[]) { + this.components = new Map(components.map(c => [c.name, c])); + } + + public get(componentName: string): AnyComponentApi | undefined { + return this.components.get(componentName); + } +} + +/** + * @template RNode The specific resolved node type this component can render. + * @template RenderOutput The output type of the rendering framework (e.g., TemplateResult for Lit, JSX.Element for React). + */ +export interface ComponentRenderer< + RNode extends AnyResolvedNode, + RenderOutput +> { + readonly componentName: RNode['type']; + + /** + * Renders the resolved component node. + * @param node The fully resolved, typed component node to render. + * @param renderChild A function provided by the framework renderer to + * recursively render child nodes. Container components MUST use this. + * @returns The framework-specific, renderable output. + */ + render( + node: RNode, + renderChild: (child: AnyResolvedNode) => RenderOutput | null + ): RenderOutput; +} +export type AnyComponentRenderer = ComponentRenderer; + +export class CatalogImplementation { + private readonly renderers: Map>; + + /** + * @param catalogApi The API definition for the catalog. + * @param renderers A list of framework-specific renderers. + */ + constructor(catalogApi: CatalogApi, renderers: AnyComponentRenderer[]) { + this.renderers = new Map(renderers.map(r => [r.componentName, r])); + + for (const api of (catalogApi as any)['components'].values()) { + if (!this.renderers.has(api.name)) { + throw new Error(`Missing renderer implementation for component: ${api.name}`); + } + } + } + + public getRenderer(componentName: string): AnyComponentRenderer | undefined { + return this.renderers.get(componentName); + } +} + +export class FrameworkRenderer { + private readonly catalogImplementation: CatalogImplementation; + + constructor(catalogImplementation: CatalogImplementation) { + this.catalogImplementation = catalogImplementation; + } + + /** + * Renders a resolved node from the A2uiMessageProcessor into the final output. + * This is the entry point for rendering a component tree. + */ + public renderNode(node: AnyResolvedNode): RenderOutput | null { + const renderer = this.catalogImplementation.getRenderer(node.type); + if (!renderer) { + console.warn(`No renderer found for component type: ${node.type}`); + return null; + } + + // The `renderChild` function passed to the component renderer is a bound + // version of this same `renderNode` method, enabling recursion. + return renderer.render(node, this.renderNode.bind(this)); + } +} + export type MessageProcessor = { getSurfaces(): ReadonlyMap; @@ -47,13 +162,13 @@ export type MessageProcessor = { * own data context. */ getData( - node: AnyComponentNode, + node: AnyResolvedNode, relativePath: string, surfaceId: string ): DataValue | null; setData( - node: AnyComponentNode | null, + node: AnyResolvedNode | null, relativePath: string, value: DataValue, surfaceId: string @@ -194,32 +309,6 @@ export type Theme = { }; }; -/** - * Represents a user-initiated action, sent from the client to the server. - */ -export interface UserAction { - /** - * The name of the action, taken from the component's `action.action` - * property. - */ - actionName: string; - /** - * The `id` of the component that triggered the event. - */ - sourceComponentId: string; - /** - * An ISO 8601 timestamp of when the event occurred. - */ - timestamp: string; - /** - * A JSON object containing the key-value pairs from the component's - * `action.context`, after resolving all data bindings. - */ - context?: { - [k: string]: unknown; - }; -} - /** A recursive type for any valid JSON-like value in the data model. */ export type DataValue = | string @@ -307,7 +396,7 @@ export type ResolvedValue = | number | boolean | null - | AnyComponentNode + | AnyResolvedNode | ResolvedMap | ResolvedArray; @@ -317,107 +406,80 @@ export type ResolvedMap = { [key: string]: ResolvedValue }; /** A generic array where each item has been recursively resolved. */ export type ResolvedArray = ResolvedValue[]; -/** - * A base interface that all component nodes share. - */ -interface BaseComponentNode { - id: string; - weight?: number; - dataContextPath?: string; - slotName?: string; -} -export interface TextNode extends BaseComponentNode { - type: "Text"; +export interface TextNode extends BaseResolvedNode<'Text'> { properties: ResolvedText; } -export interface ImageNode extends BaseComponentNode { - type: "Image"; +export interface ImageNode extends BaseResolvedNode<'Image'> { properties: ResolvedImage; } -export interface IconNode extends BaseComponentNode { - type: "Icon"; +export interface IconNode extends BaseResolvedNode<'Icon'> { properties: ResolvedIcon; } -export interface VideoNode extends BaseComponentNode { - type: "Video"; +export interface VideoNode extends BaseResolvedNode<'Video'> { properties: ResolvedVideo; } -export interface AudioPlayerNode extends BaseComponentNode { - type: "AudioPlayer"; +export interface AudioPlayerNode extends BaseResolvedNode<'AudioPlayer'> { properties: ResolvedAudioPlayer; } -export interface RowNode extends BaseComponentNode { - type: "Row"; +export interface RowNode extends BaseResolvedNode<'Row'> { properties: ResolvedRow; } -export interface ColumnNode extends BaseComponentNode { - type: "Column"; +export interface ColumnNode extends BaseResolvedNode<'Column'> { properties: ResolvedColumn; } -export interface ListNode extends BaseComponentNode { - type: "List"; +export interface ListNode extends BaseResolvedNode<'List'> { properties: ResolvedList; } -export interface CardNode extends BaseComponentNode { - type: "Card"; +export interface CardNode extends BaseResolvedNode<'Card'> { properties: ResolvedCard; } -export interface TabsNode extends BaseComponentNode { - type: "Tabs"; +export interface TabsNode extends BaseResolvedNode<'Tabs'> { properties: ResolvedTabs; } -export interface DividerNode extends BaseComponentNode { - type: "Divider"; +export interface DividerNode extends BaseResolvedNode<'Divider'> { properties: ResolvedDivider; } -export interface ModalNode extends BaseComponentNode { - type: "Modal"; +export interface ModalNode extends BaseResolvedNode<'Modal'> { properties: ResolvedModal; } -export interface ButtonNode extends BaseComponentNode { - type: "Button"; +export interface ButtonNode extends BaseResolvedNode<'Button'> { properties: ResolvedButton; } -export interface CheckboxNode extends BaseComponentNode { - type: "CheckBox"; +export interface CheckboxNode extends BaseResolvedNode<'CheckBox'> { properties: ResolvedCheckbox; } -export interface TextFieldNode extends BaseComponentNode { - type: "TextField"; +export interface TextFieldNode extends BaseResolvedNode<'TextField'> { properties: ResolvedTextField; } -export interface DateTimeInputNode extends BaseComponentNode { - type: "DateTimeInput"; +export interface DateTimeInputNode extends BaseResolvedNode<'DateTimeInput'> { properties: ResolvedDateTimeInput; } -export interface MultipleChoiceNode extends BaseComponentNode { - type: "MultipleChoice"; +export interface MultipleChoiceNode extends BaseResolvedNode<'MultipleChoice'> { properties: ResolvedMultipleChoice; } -export interface SliderNode extends BaseComponentNode { - type: "Slider"; +export interface SliderNode extends BaseResolvedNode<'Slider'> { properties: ResolvedSlider; } -export interface CustomNode extends BaseComponentNode { +export interface CustomNode extends BaseResolvedNode { type: string; // For custom nodes, properties are just a map of string keys to any resolved value. properties: CustomNodeProperties; @@ -427,7 +489,7 @@ export interface CustomNode extends BaseComponentNode { * The complete discriminated union of all possible resolved component nodes. * A renderer would use this type for any given node in the component tree. */ -export type AnyComponentNode = +export type AnyResolvedNode = | TextNode | IconNode | ImageNode @@ -448,6 +510,8 @@ export type AnyComponentNode = | SliderNode | CustomNode; +export type AnyComponentNode = AnyResolvedNode; + // These components do not contain other components can reuse their // original interfaces. export type ResolvedText = Text; @@ -456,14 +520,31 @@ export type ResolvedImage = Image; export type ResolvedVideo = Video; export type ResolvedAudioPlayer = AudioPlayer; export type ResolvedDivider = Divider; -export type ResolvedCheckbox = Checkbox; +export interface ResolvedCheckbox { + label: StringValue; + value: BooleanValue; +} export type ResolvedTextField = TextField; export type ResolvedDateTimeInput = DateTimeInput; -export type ResolvedMultipleChoice = MultipleChoice; -export type ResolvedSlider = Slider; +export interface ResolvedMultipleChoice { + selections: { + path?: string; + literalArray?: string[]; + }; + options?: { + label: StringValue; + value: string; + }[]; + maxAllowedSelections?: number; +} +export interface ResolvedSlider { + value: NumberValue; + minValue?: number; + maxValue?: number; +} export interface ResolvedRow { - children: AnyComponentNode[]; + children: AnyResolvedNode[]; distribution?: | "start" | "center" @@ -475,7 +556,7 @@ export interface ResolvedRow { } export interface ResolvedColumn { - children: AnyComponentNode[]; + children: AnyResolvedNode[]; distribution?: | "start" | "center" @@ -487,24 +568,24 @@ export interface ResolvedColumn { } export interface ResolvedButton { - child: AnyComponentNode; + child: AnyResolvedNode; action: Button["action"]; } export interface ResolvedList { - children: AnyComponentNode[]; + children: AnyResolvedNode[]; direction?: "vertical" | "horizontal"; alignment?: "start" | "center" | "end" | "stretch"; } export interface ResolvedCard { - child: AnyComponentNode; - children: AnyComponentNode[]; + child: AnyResolvedNode; + children: AnyResolvedNode[]; } export interface ResolvedTabItem { title: StringValue; - child: AnyComponentNode; + child: AnyResolvedNode; } export interface ResolvedTabs { @@ -512,8 +593,8 @@ export interface ResolvedTabs { } export interface ResolvedModal { - entryPointChild: AnyComponentNode; - contentChild: AnyComponentNode; + entryPointChild: AnyResolvedNode; + contentChild: AnyResolvedNode; } export interface CustomNodeProperties { @@ -525,7 +606,7 @@ export type SurfaceID = string; /** The complete state of a single UI surface. */ export interface Surface { rootComponentId: string | null; - componentTree: AnyComponentNode | null; + componentTree: AnyResolvedNode | null; dataModel: DataMap; components: Map; styles: Record; diff --git a/renderers/lit/src/0.8/events/a2ui.ts b/renderers/lit/src/0.8/events/a2ui.ts index 88a41ea7..722577b2 100644 --- a/renderers/lit/src/0.8/events/a2ui.ts +++ b/renderers/lit/src/0.8/events/a2ui.ts @@ -14,8 +14,8 @@ limitations under the License. */ -import { Action } from "../types/components.js"; -import { AnyComponentNode } from "../types/types.js"; +import { Action } from "../core/types/components.js"; +import { AnyComponentNode } from "../core/types/types.js"; import { BaseEventDetail } from "./base.js"; type Namespace = "a2ui"; diff --git a/renderers/lit/src/0.8/index.ts b/renderers/lit/src/0.8/index.ts index ab41af4b..5413dcf6 100644 --- a/renderers/lit/src/0.8/index.ts +++ b/renderers/lit/src/0.8/index.ts @@ -15,4 +15,4 @@ */ export * from "./core.js"; -export * as UI from "./ui/ui.js"; +export * as UI from "./lit/components/ui.js"; diff --git a/renderers/lit/src/0.8/ui/audio.ts b/renderers/lit/src/0.8/lit/components/audio.ts similarity index 72% rename from renderers/lit/src/0.8/ui/audio.ts rename to renderers/lit/src/0.8/lit/components/audio.ts index 3465687b..f788313d 100644 --- a/renderers/lit/src/0.8/ui/audio.ts +++ b/renderers/lit/src/0.8/lit/components/audio.ts @@ -15,18 +15,16 @@ */ import { html, css, nothing } from "lit"; -import { customElement, property } from "lit/decorators.js"; +import { customElement } from "lit/decorators.js"; import { Root } from "./root.js"; -import { StringValue } from "../types/primitives.js"; import { classMap } from "lit/directives/class-map.js"; -import { A2uiMessageProcessor } from "../data/model-processor.js"; +import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js"; import { styleMap } from "lit/directives/style-map.js"; import { structuralStyles } from "./styles.js"; +import { AudioPlayerNode } from "../../core/types/types.js"; @customElement("a2ui-audioplayer") -export class Audio extends Root { - @property() - accessor url: StringValue | null = null; +export class Audio extends Root { static styles = [ structuralStyles, @@ -50,23 +48,24 @@ export class Audio extends Root { ]; #renderAudio() { - if (!this.url) { + const url = this.node.properties.url; + if (!url) { return nothing; } - if (this.url && typeof this.url === "object") { - if ("literalString" in this.url) { - return html`