From e21e230a5971736273ee3932c02480dd77998ad8 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 17 Mar 2026 09:01:20 +1030 Subject: [PATCH 1/4] Patch in web core changes --- renderers/web_core/package-lock.json | 11 +- renderers/web_core/package.json | 6 +- renderers/web_core/src/v0_9/catalog/types.ts | 14 +- .../v0_9/processing/message-processor.test.ts | 225 ++++++++++++++++++ .../src/v0_9/processing/message-processor.ts | 132 ++++++++++ .../src/v0_9/schema/client-capabilities.ts | 52 ++++ .../web_core/src/v0_9/schema/common-types.ts | 26 +- renderers/web_core/src/v0_9/schema/index.ts | 1 + specification/v0_9/docs/renderer_guide.md | 42 +++- 9 files changed, 474 insertions(+), 35 deletions(-) create mode 100644 renderers/web_core/src/v0_9/schema/client-capabilities.ts diff --git a/renderers/web_core/package-lock.json b/renderers/web_core/package-lock.json index 721db3ea5..e7d509cc0 100644 --- a/renderers/web_core/package-lock.json +++ b/renderers/web_core/package-lock.json @@ -1,23 +1,23 @@ { "name": "@a2ui/web_core", - "version": "0.8.5", + "version": "0.8.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@a2ui/web_core", - "version": "0.8.5", + "version": "0.8.6", "license": "Apache-2.0", "dependencies": { "@preact/signals-core": "^1.13.0", "date-fns": "^4.1.0", - "zod": "^3.25.76" + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1" }, "devDependencies": { "@types/node": "^24.11.0", "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2", - "zod-to-json-schema": "^3.25.1" + "wireit": "^0.15.0-pre.2" } }, "node_modules/@nodelib/fs.scandir": { @@ -480,7 +480,6 @@ "node_modules/zod-to-json-schema": { "version": "3.25.1", "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.25 || ^4" diff --git a/renderers/web_core/package.json b/renderers/web_core/package.json index e18205d97..139c63869 100644 --- a/renderers/web_core/package.json +++ b/renderers/web_core/package.json @@ -88,12 +88,12 @@ "devDependencies": { "@types/node": "^24.11.0", "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2", - "zod-to-json-schema": "^3.25.1" + "wireit": "^0.15.0-pre.2" }, "dependencies": { "@preact/signals-core": "^1.13.0", "date-fns": "^4.1.0", - "zod": "^3.25.76" + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1" } } diff --git a/renderers/web_core/src/v0_9/catalog/types.ts b/renderers/web_core/src/v0_9/catalog/types.ts index 01c172fb7..466afd2ae 100644 --- a/renderers/web_core/src/v0_9/catalog/types.ts +++ b/renderers/web_core/src/v0_9/catalog/types.ts @@ -106,13 +106,23 @@ export class Catalog { */ readonly functions: ReadonlyMap; + /** + * The schema for theme parameters used by this catalog. + */ + readonly themeSchema?: z.ZodObject; + /** * A ready-to-use FunctionInvoker callback that delegates to this catalog's functions. * Can be passed directly to a DataContext. */ readonly invoker: FunctionInvoker; - constructor(id: string, components: T[], functions: FunctionImplementation[] = []) { + constructor( + id: string, + components: T[], + functions: FunctionImplementation[] = [], + themeSchema?: z.ZodObject, + ) { this.id = id; const compMap = new Map(); @@ -127,6 +137,8 @@ export class Catalog { } this.functions = funcMap; + this.themeSchema = themeSchema; + this.invoker = (name, rawArgs, ctx, abortSignal) => { const fn = this.functions.get(name); if (!fn) { diff --git a/renderers/web_core/src/v0_9/processing/message-processor.test.ts b/renderers/web_core/src/v0_9/processing/message-processor.test.ts index a4c4b0ad1..90fa87ad6 100644 --- a/renderers/web_core/src/v0_9/processing/message-processor.test.ts +++ b/renderers/web_core/src/v0_9/processing/message-processor.test.ts @@ -18,6 +18,7 @@ import assert from "node:assert"; import { describe, it, beforeEach } from "node:test"; import { MessageProcessor } from "./message-processor.js"; import { Catalog, ComponentApi } from "../catalog/types.js"; +import { z } from "zod"; describe("MessageProcessor", () => { let processor: MessageProcessor; @@ -32,6 +33,230 @@ describe("MessageProcessor", () => { }); }); + describe("getClientCapabilities", () => { + it("generates basic client capabilities with supportedCatalogIds", () => { + const caps: any = processor.getClientCapabilities(); + assert.strictEqual((caps["v0.9"] as any).inlineCatalogs, undefined); + assert.deepStrictEqual(caps, { + "v0.9": { + supportedCatalogIds: ["test-catalog"], + }, + }); + }); + + it("generates inline catalogs when requested", () => { + const buttonApi: ComponentApi = { + name: "Button", + schema: z.object({ + label: z.string().describe("The button label"), + }), + }; + const cat = new Catalog("cat-1", [buttonApi]); + const proc = new MessageProcessor([cat]); + + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + const inlineCat = caps["v0.9"].inlineCatalogs![0]; + + assert.strictEqual(inlineCat.catalogId, "cat-1"); + const buttonSchema = inlineCat.components!.Button; + + assert.ok(buttonSchema.allOf); + assert.strictEqual( + buttonSchema.allOf[0].$ref, + "common_types.json#/$defs/ComponentCommon", + ); + assert.strictEqual( + buttonSchema.allOf[1].properties.component.const, + "Button", + ); + assert.strictEqual( + buttonSchema.allOf[1].properties.label.description, + "The button label", + ); + assert.deepStrictEqual(buttonSchema.allOf[1].required, [ + "component", + "label", + ]); + }); + + it("transforms REF: descriptions into valid $ref nodes", () => { + const customApi: ComponentApi = { + name: "Custom", + schema: z.object({ + title: z + .string() + .describe("REF:common_types.json#/$defs/DynamicString|The title"), + }), + }; + const cat = new Catalog("cat-ref", [customApi]); + const proc = new MessageProcessor([cat]); + + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + const titleSchema = + caps["v0.9"].inlineCatalogs![0].components!.Custom.allOf[1].properties + .title; + + assert.strictEqual( + titleSchema.$ref, + "common_types.json#/$defs/DynamicString", + ); + assert.strictEqual(titleSchema.description, "The title"); + // Ensure Zod's 'type: string' was removed + assert.strictEqual(titleSchema.type, undefined); + }); + + it("generates inline catalogs with functions and theme schema", () => { + const buttonApi: ComponentApi = { + name: "Button", + schema: z.object({ + label: z.string(), + }), + }; + const addFn = { + name: "add", + returnType: "number" as const, + schema: z.object({ + a: z.number().describe("First number"), + b: z.number().describe("Second number"), + }), + execute: (args: any) => args.a + args.b, + }; + + const themeSchema = z.object({ + primaryColor: z + .string() + .describe("REF:common_types.json#/$defs/Color|The main color"), + }); + + const cat = new Catalog("cat-full", [buttonApi], [addFn], themeSchema); + const proc = new MessageProcessor([cat]); + + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + const inlineCat = caps["v0.9"].inlineCatalogs![0]; + + assert.strictEqual(inlineCat.catalogId, "cat-full"); + + // Verify Functions + assert.ok(inlineCat.functions); + assert.strictEqual(inlineCat.functions.length, 1); + const fn = inlineCat.functions[0]; + assert.strictEqual(fn.name, "add"); + assert.strictEqual(fn.returnType, "number"); + assert.strictEqual(fn.parameters.properties.a.description, "First number"); + + // Verify Theme + assert.ok(inlineCat.theme); + assert.ok(inlineCat.theme.primaryColor); + assert.strictEqual( + inlineCat.theme.primaryColor.$ref, + "common_types.json#/$defs/Color", + ); + assert.strictEqual( + inlineCat.theme.primaryColor.description, + "The main color", + ); + }); + + it("omits functions and theme when catalog has none", () => { + const compApi: ComponentApi = { name: "EmptyComp", schema: z.object({}) }; + const cat = new Catalog("cat-empty", [compApi]); + const proc = new MessageProcessor([cat]); + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + const inlineCat = caps["v0.9"].inlineCatalogs![0]; + + assert.strictEqual(inlineCat.catalogId, "cat-empty"); + assert.strictEqual(inlineCat.functions, undefined); + assert.strictEqual(inlineCat.theme, undefined); + }); + + it("processes REF: tags deeply nested in schema arrays and objects", () => { + const deepApi: ComponentApi = { + name: "DeepComp", + schema: z.object({ + items: z.array( + z.object({ + action: z + .string() + .describe( + "REF:common_types.json#/$defs/Action|The action to perform", + ), + }), + ), + }), + }; + const cat = new Catalog("cat-deep", [deepApi]); + const proc = new MessageProcessor([cat]); + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + + const properties = + caps["v0.9"].inlineCatalogs![0].components!.DeepComp.allOf[1].properties; + const actionSchema = properties.items.items.properties.action; + + assert.strictEqual(actionSchema.$ref, "common_types.json#/$defs/Action"); + assert.strictEqual(actionSchema.description, "The action to perform"); + assert.strictEqual(actionSchema.type, undefined); + }); + + it("handles REF: tags without pipes or with multiple pipes", () => { + const edgeApi: ComponentApi = { + name: "EdgeComp", + schema: z.object({ + noPipe: z.string().describe("REF:common_types.json#/$defs/NoPipe"), + multiPipe: z + .string() + .describe("REF:common_types.json#/$defs/MultiPipe|First|Second"), + }), + }; + const cat = new Catalog("cat-edge", [edgeApi]); + const proc = new MessageProcessor([cat]); + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + + const properties = + caps["v0.9"].inlineCatalogs![0].components!.EdgeComp.allOf[1].properties; + + assert.strictEqual( + properties.noPipe.$ref, + "common_types.json#/$defs/NoPipe", + ); + assert.strictEqual(properties.noPipe.description, undefined); + + assert.strictEqual( + properties.multiPipe.$ref, + "common_types.json#/$defs/MultiPipe", + ); + assert.strictEqual(properties.multiPipe.description, "First"); + }); + + it("handles multiple catalogs correctly", () => { + const compApi: ComponentApi = { name: "C1", schema: z.object({}) }; + const cat1 = new Catalog("cat-1", [compApi]); + + const addFn = { + name: "add", + returnType: "number" as const, + schema: z.object({}), + execute: () => 0, + }; + const themeSchema = z.object({ color: z.string() }); + const cat2 = new Catalog("cat-2", [], [addFn], themeSchema); + + const proc = new MessageProcessor([cat1, cat2]); + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + + assert.strictEqual(caps["v0.9"].inlineCatalogs!.length, 2); + + const inlineCat1 = caps["v0.9"].inlineCatalogs![0]; + assert.strictEqual(inlineCat1.catalogId, "cat-1"); + assert.strictEqual(inlineCat1.functions, undefined); + assert.strictEqual(inlineCat1.theme, undefined); + + const inlineCat2 = caps["v0.9"].inlineCatalogs![1]; + assert.strictEqual(inlineCat2.catalogId, "cat-2"); + assert.strictEqual(inlineCat2.functions!.length, 1); + assert.ok(inlineCat2.theme); + }); + }); + it("creates surface", () => { processor.processMessages([ { diff --git a/renderers/web_core/src/v0_9/processing/message-processor.ts b/renderers/web_core/src/v0_9/processing/message-processor.ts index df8c95c52..f3bd5030b 100644 --- a/renderers/web_core/src/v0_9/processing/message-processor.ts +++ b/renderers/web_core/src/v0_9/processing/message-processor.ts @@ -19,6 +19,7 @@ import { Catalog, ComponentApi } from "../catalog/types.js"; import { SurfaceGroupModel } from "../state/surface-group-model.js"; import { ComponentModel } from "../state/component-model.js"; import { Subscription } from "../common/events.js"; +import { zodToJsonSchema } from "zod-to-json-schema"; import { A2uiMessage, @@ -27,8 +28,20 @@ import { UpdateDataModelMessage, DeleteSurfaceMessage, } from "../schema/server-to-client.js"; +import { + A2uiClientCapabilities, + InlineCatalog, +} from "../schema/client-capabilities.js"; import { A2uiStateError, A2uiValidationError } from "../errors.js"; +/** + * Options for generating client capabilities. + */ +export interface CapabilitiesOptions { + /** If true, the full definition of all catalogs will be included. */ + includeInlineCatalogs?: boolean; +} + /** * The central processor for A2UI messages. * @template T The concrete type of the ComponentApi. @@ -59,6 +72,125 @@ export class MessageProcessor { return this.model.onSurfaceCreated.subscribe(handler); } + /** + * Generates the a2uiClientCapabilities object for the current processor. + * + * @param options Configuration for capability generation. + * @returns The capabilities object. + */ + getClientCapabilities(options?: CapabilitiesOptions): A2uiClientCapabilities { + const capabilities: A2uiClientCapabilities = { + "v0.9": { + supportedCatalogIds: this.catalogs.map((c) => c.id), + }, + }; + + if (options?.includeInlineCatalogs) { + capabilities["v0.9"].inlineCatalogs = this.catalogs.map((c) => + this.generateInlineCatalog(c), + ); + } + + return capabilities; + } + + private generateInlineCatalog(catalog: Catalog): InlineCatalog { + const components: Record = {}; + + for (const [name, api] of catalog.components.entries()) { + const zodSchema = zodToJsonSchema(api.schema, { + target: "jsonSchema2019-09", + }) as any; + + // Clean up Zod-specific artifacts and process REF: tags + this.processRefs(zodSchema); + + // Wrap in standard A2UI component envelope (ComponentCommon) + components[name] = { + allOf: [ + { $ref: "common_types.json#/$defs/ComponentCommon" }, + { + properties: { + component: { const: name }, + ...zodSchema.properties, + }, + required: ["component", ...(zodSchema.required || [])], + }, + ], + }; + } + + const functions: any[] = []; + for (const api of catalog.functions.values()) { + const zodSchema = zodToJsonSchema(api.schema, { + target: "jsonSchema2019-09", + }) as any; + + this.processRefs(zodSchema); + + functions.push({ + name: api.name, + description: api.schema.description, + returnType: api.returnType, + parameters: zodSchema, + }); + } + + let theme: Record | undefined; + if (catalog.themeSchema) { + const zodSchema = zodToJsonSchema(catalog.themeSchema, { + target: "jsonSchema2019-09", + }) as any; + + this.processRefs(zodSchema); + theme = zodSchema.properties; + } + + return { + catalogId: catalog.id, + components, + functions: functions.length > 0 ? functions : undefined, + theme, + }; + } + + private processRefs(node: any): void { + if (typeof node !== "object" || node === null) return; + + // If the node itself is a REF target, transform it and stop recursion. + if ( + typeof node.description === "string" && + node.description.startsWith("REF:") + ) { + const parts = node.description.substring(4).split("|"); + const ref = parts[0]; + const desc = parts[1] || ""; + + // Clear the node of all other properties. + for (const k of Object.keys(node)) { + delete node[k]; + } + + // Re-add only the $ref and an optional description. + node["$ref"] = ref; + if (desc) { + node["description"] = desc; + } + return; + } + + // If not a REF target, recurse into its children. + if (Array.isArray(node)) { + for (const item of node) { + this.processRefs(item); + } + } else { + for (const key of Object.keys(node)) { + this.processRefs(node[key]); + } + } + } + /** * Subscribes to surface deletion events. */ diff --git a/renderers/web_core/src/v0_9/schema/client-capabilities.ts b/renderers/web_core/src/v0_9/schema/client-capabilities.ts new file mode 100644 index 000000000..0bb3893a2 --- /dev/null +++ b/renderers/web_core/src/v0_9/schema/client-capabilities.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2026 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. + */ + +/** + * Represents a JSON Schema definition. + * Typed as Record to allow standard JSON schema properties + * without importing heavy schema types. + */ +export type JsonSchema = Record; + +/** + * Describes a function's interface within an inline catalog. + */ +export interface FunctionDefinition { + name: string; + description?: string; + parameters: JsonSchema; + returnType: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'any' | 'void'; +} + +/** + * Defines a catalog inline for the a2uiClientCapabilities object. + */ +export interface InlineCatalog { + catalogId: string; + components?: Record; + functions?: FunctionDefinition[]; + theme?: Record; +} + +/** + * The capabilities structure sent from the client to the server as part of transport metadata. + */ +export interface A2uiClientCapabilities { + "v0.9": { + supportedCatalogIds: string[]; + inlineCatalogs?: InlineCatalog[]; + }; +} diff --git a/renderers/web_core/src/v0_9/schema/common-types.ts b/renderers/web_core/src/v0_9/schema/common-types.ts index 632330a5d..1a3ee1cf9 100644 --- a/renderers/web_core/src/v0_9/schema/common-types.ts +++ b/renderers/web_core/src/v0_9/schema/common-types.ts @@ -20,7 +20,7 @@ export const DataBindingSchema = z.object({ path: z .string() .describe("A JSON Pointer path to a value in the data model."), -}); +}).describe("REF:common_types.json#/$defs/DataBinding|A JSON Pointer path to a value in the data model."); export const FunctionCallSchema = z.object({ call: z.string().describe("The name of the function to call."), @@ -28,7 +28,7 @@ export const FunctionCallSchema = z.object({ returnType: z .enum(["string", "number", "boolean", "array", "object", "any", "void"]) .default("boolean"), -}); +}).describe("REF:common_types.json#/$defs/FunctionCall|Invokes a named function on the client."); export const LogicExpressionSchema: z.ZodType = z.lazy(() => z.union([ @@ -42,32 +42,32 @@ export const LogicExpressionSchema: z.ZodType = z.lazy(() => z.object({ true: z.literal(true) }), z.object({ false: z.literal(false) }), ]), -); +).describe("REF:common_types.json#/$defs/LogicExpression|A logical expression representation."); export const DynamicStringSchema = z.union([ z.string(), DataBindingSchema, // FunctionCall returning string (simplified schema for Zod, stricter in JSON Schema) FunctionCallSchema, -]); +]).describe("REF:common_types.json#/$defs/DynamicString|Represents a string"); export const DynamicNumberSchema = z.union([ z.number(), DataBindingSchema, FunctionCallSchema, -]); +]).describe("REF:common_types.json#/$defs/DynamicNumber|Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number."); export const DynamicBooleanSchema = z.union([ z.boolean(), DataBindingSchema, LogicExpressionSchema, -]); +]).describe("REF:common_types.json#/$defs/DynamicBoolean|A boolean value that can be a literal, a path, or a function call returning a boolean."); export const DynamicStringListSchema = z.union([ z.array(z.string()), DataBindingSchema, FunctionCallSchema, -]); +]).describe("REF:common_types.json#/$defs/DynamicStringList|Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array."); export const DynamicValueSchema = z.union([ z.string(), @@ -76,7 +76,7 @@ export const DynamicValueSchema = z.union([ z.array(z.any()), DataBindingSchema, FunctionCallSchema, -]); +]).describe("REF:common_types.json#/$defs/DynamicValue|A value that can be a literal, a path, or a function call returning any type."); /** A JSON Pointer path to a value in the data model. */ export type DataBinding = z.infer; @@ -97,7 +97,7 @@ export type DynamicValue = z.infer; export const ComponentIdSchema = z .string() - .describe("The unique identifier for a component."); + .describe("REF:common_types.json#/$defs/ComponentId|The unique identifier for a component."); /** The unique identifier for a component. */ export type ComponentId = z.infer; @@ -113,7 +113,7 @@ export const ChildListSchema = z.union([ ), }) .describe("A template for generating a dynamic list of children."), -]); +]).describe("REF:common_types.json#/$defs/ChildList"); /** A static list of child component IDs or a dynamic list template. */ export type ChildList = z.infer; @@ -131,7 +131,7 @@ export const ActionSchema = z.union([ functionCall: FunctionCallSchema, }) .describe("Executes a local client-side function."), -]); +]).describe("REF:common_types.json#/$defs/Action"); /** Triggers a server-side event or a local client-side function. */ export type Action = z.infer; @@ -142,7 +142,7 @@ export const CheckRuleSchema = z.intersection( .string() .describe("The error message to display if the check fails."), }), -); +).describe("REF:common_types.json#/$defs/CheckRule|A check rule consisting of a condition and an error message."); /** A check rule consisting of a condition and an error message. */ export type CheckRule = z.infer; @@ -151,7 +151,7 @@ export const CheckableSchema = z.object({ .array(CheckRuleSchema) .optional() .describe("A list of checks to perform."), -}); +}).describe("REF:common_types.json#/$defs/Checkable|Properties for components that support client-side checks."); /** An object that contains checks. */ export type Checkable = z.infer; diff --git a/renderers/web_core/src/v0_9/schema/index.ts b/renderers/web_core/src/v0_9/schema/index.ts index d98dfd978..6e8434988 100644 --- a/renderers/web_core/src/v0_9/schema/index.ts +++ b/renderers/web_core/src/v0_9/schema/index.ts @@ -16,3 +16,4 @@ export * from "./common-types.js"; export * from "./server-to-client.js"; +export * from "./client-capabilities.js"; diff --git a/specification/v0_9/docs/renderer_guide.md b/specification/v0_9/docs/renderer_guide.md index 0eebda2ba..42dc79857 100644 --- a/specification/v0_9/docs/renderer_guide.md +++ b/specification/v0_9/docs/renderer_guide.md @@ -135,7 +135,22 @@ The model is designed to support high-performance rendering through granular upd * **Property Changes**: The `ComponentModel` notifies when its specific configuration changes. * **Data Changes**: The `DataModel` notifies only subscribers to the specific path that changed. -### The Models +### Protocol Models & Serialization + +The framework-agnostic layer is responsible for defining strict, native type representations of the A2UI JSON schemas. Renderers should not pass raw generic dictionaries (like `Map` or `Record`) directly into the state layer. + +Developers must create data classes, structs, or interfaces (e.g., `data class` in Kotlin, `Codable struct` in Swift, or Zod-validated `interface` in TypeScript) that perfectly mirror the official JSON specifications. This creates a safe boundary between the raw network stream and the internal state models. + +**Required Data Structures:** +* **Server-to-Client Messages:** `A2uiMessage` (a union/protocol type), `CreateSurfaceMessage`, `UpdateComponentsMessage`, `UpdateDataModelMessage`, `DeleteSurfaceMessage`. +* **Client-to-Server Events:** `ClientEvent` (a union/protocol type), `ActionMessage`, `ErrorMessage`. +* **Client Metadata:** `A2uiClientCapabilities`, `InlineCatalog`, `FunctionDefinition`, `ClientDataModel`. + +**JSON Serialization & Validation:** +* **Inbound (Parsing)**: The core library must provide a mechanism to deserialize a raw JSON string into a strongly-typed `A2uiMessage`. If the payload violates the A2UI JSON schema, this layer must throw an `A2uiValidationError` *before* the message reaches the state models. +* **Outbound (Stringifying)**: The core library must serialize client-to-server events and capabilities from their strict native types back into valid JSON strings to hand off to the transport layer. + +### The State Models #### SurfaceGroupModel & SurfaceModel The root containers for active surfaces and their catalogs, data, and components. @@ -267,9 +282,12 @@ class MessageProcessor { constructor(catalogs: Catalog[], actionHandler: ActionListener); - processMessages(messages: any[]): void; + // Accepts validated, strongly-typed message objects, not raw JSON + processMessages(messages: A2uiMessage[]): void; addLifecycleListener(l: SurfaceLifecycleListener): () => void; - getClientCapabilities(options?: CapabilitiesOptions): any; + + // Returns a strictly typed capabilities object ready for JSON serialization + getClientCapabilities(options?: CapabilitiesOptions): A2uiClientCapabilities; } ``` @@ -282,11 +300,11 @@ To dynamically generate the `a2uiClientCapabilities` payload (specifically `inli **Detectable Common Types**: Shared definitions (like `DynamicString`) must emit external JSON Schema `$ref` pointers. This is achieved by "tagging" the schemas using their `description` property (e.g., `REF:common_types.json#/$defs/DynamicString`). -When `getClientCapabilities()` converts internal schemas: -1. Translate the definition into a raw JSON Schema. -2. Traverse the tree looking for descriptions starting with `REF:`. -3. Strip the tag and replace the node with a valid JSON Schema `$ref` object. -4. Wrap property schemas in the standard A2UI component envelope (`allOf` containing `ComponentCommon`). +When `getClientCapabilities()` converts internal schemas to generate `inlineCatalogs`: +1. **Components**: Translate each component schema into a raw JSON Schema. Wrap it in the standard A2UI component envelope (`allOf` containing `ComponentCommon`). +2. **Functions**: Map each function in the catalog to a `FunctionDefinition` object, converting its argument schema to JSON Schema. +3. **Theme**: Convert the catalog's theme schema into a JSON Schema representation. +4. **Reference Processing**: For all generated schemas (components, functions, and themes), traverse the tree looking for descriptions starting with `REF:`. Strip the tag and replace the node with a valid JSON Schema `$ref` object. ## 4. The Catalog API & Functions @@ -313,9 +331,9 @@ class Catalog { readonly id: string; // Unique catalog URI readonly components: ReadonlyMap; readonly functions?: ReadonlyMap; - readonly theme?: Schema; + readonly themeSchema?: Schema; - constructor(id: string, components: T[], functions?: FunctionImplementation[], theme?: Schema) { + constructor(id: string, components: T[], functions?: FunctionImplementation[], themeSchema?: Schema) { // Initializes the properties } } @@ -341,7 +359,7 @@ myCustomCatalog = Catalog( id="https://mycompany.com/catalogs/custom_catalog.json", functions=basicCatalog.functions, components=basicCatalog.components + [MyCompanyLogoComponent()], - theme=basicCatalog.theme # Inherit theme schema + themeSchema=basicCatalog.themeSchema # Inherit theme schema ) ``` @@ -639,7 +657,7 @@ Implement the framework-agnostic Data Layer (Section 3). * Implement `ComponentModel`, `SurfaceComponentsModel`, `SurfaceModel`, and `SurfaceGroupModel`. * Implement `DataContext` and `ComponentContext`. * Implement `MessageProcessor` and ClientCapabilities generation. -* **Action**: Write unit tests for the `DataModel` (especially pointer resolution/cascade logic) and `MessageProcessor`. Ensure they pass before continuing. +* **Action**: Write unit tests for JSON validation, the `DataModel` (especially pointer resolution/cascade logic), and `MessageProcessor`. Ensure they pass before continuing. ### 4. Framework-Specific Layer Implement the bridge between models and native UI (Section 5 & 6). From e688cf2f74c5f5b0a5d7db385833aeab19dc845b Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 17 Mar 2026 09:59:19 +1030 Subject: [PATCH 2/4] Add prototype lit renderer --- renderers/lit/package-lock.json | 9 ++- renderers/lit/package.json | 4 ++ renderers/lit/src/index.ts | 1 + renderers/lit/src/v0_9/adapter.ts | 42 +++++++++++ .../catalogs/minimal/components/Button.ts | 30 ++++++++ .../catalogs/minimal/components/Column.ts | 49 +++++++++++++ .../v0_9/catalogs/minimal/components/Row.ts | 49 +++++++++++++ .../v0_9/catalogs/minimal/components/Text.ts | 32 +++++++++ .../catalogs/minimal/components/TextField.ts | 52 ++++++++++++++ .../catalogs/minimal/functions/capitalize.ts | 18 +++++ .../lit/src/v0_9/catalogs/minimal/index.ts | 14 ++++ renderers/lit/src/v0_9/index.ts | 5 ++ renderers/lit/src/v0_9/surface/A2uiSurface.ts | 60 ++++++++++++++++ renderers/lit/src/v0_9/surface/render-node.ts | 70 +++++++++++++++++++ .../lit/src/v0_9/tests/integration.test.ts | 48 +++++++++++++ renderers/lit/src/v0_9/types.ts | 11 +++ 16 files changed, 489 insertions(+), 5 deletions(-) create mode 100644 renderers/lit/src/v0_9/adapter.ts create mode 100644 renderers/lit/src/v0_9/catalogs/minimal/components/Button.ts create mode 100644 renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts create mode 100644 renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts create mode 100644 renderers/lit/src/v0_9/catalogs/minimal/components/Text.ts create mode 100644 renderers/lit/src/v0_9/catalogs/minimal/components/TextField.ts create mode 100644 renderers/lit/src/v0_9/catalogs/minimal/functions/capitalize.ts create mode 100644 renderers/lit/src/v0_9/catalogs/minimal/index.ts create mode 100644 renderers/lit/src/v0_9/index.ts create mode 100644 renderers/lit/src/v0_9/surface/A2uiSurface.ts create mode 100644 renderers/lit/src/v0_9/surface/render-node.ts create mode 100644 renderers/lit/src/v0_9/tests/integration.test.ts create mode 100644 renderers/lit/src/v0_9/types.ts diff --git a/renderers/lit/package-lock.json b/renderers/lit/package-lock.json index 77acb5c38..ba265d813 100644 --- a/renderers/lit/package-lock.json +++ b/renderers/lit/package-lock.json @@ -24,18 +24,18 @@ }, "../web_core": { "name": "@a2ui/web_core", - "version": "0.8.5", + "version": "0.8.6", "license": "Apache-2.0", "dependencies": { "@preact/signals-core": "^1.13.0", "date-fns": "^4.1.0", - "zod": "^3.25.76" + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1" }, "devDependencies": { "@types/node": "^24.11.0", "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2", - "zod-to-json-schema": "^3.25.1" + "wireit": "^0.15.0-pre.2" } }, "../web_core/node_modules/@nodelib/fs.scandir": { @@ -441,7 +441,6 @@ }, "../web_core/node_modules/zod-to-json-schema": { "version": "3.25.1", - "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.25 || ^4" diff --git a/renderers/lit/package.json b/renderers/lit/package.json index 99f8a4191..17dc696fe 100644 --- a/renderers/lit/package.json +++ b/renderers/lit/package.json @@ -13,6 +13,10 @@ "types": "./dist/src/0.8/core.d.ts", "default": "./dist/src/0.8/core.js" }, + "./v0_9": { + "types": "./dist/src/v0_9/index.d.ts", + "default": "./dist/src/v0_9/index.js" + }, "./ui": { "types": "./dist/src/0.8/ui/ui.d.ts", "default": "./dist/src/0.8/ui/ui.js" diff --git a/renderers/lit/src/index.ts b/renderers/lit/src/index.ts index b70862bd7..71307b5f4 100644 --- a/renderers/lit/src/index.ts +++ b/renderers/lit/src/index.ts @@ -15,3 +15,4 @@ */ export * as v0_8 from "./0.8/index.js"; +export * as v0_9 from "./v0_9/index.js"; diff --git a/renderers/lit/src/v0_9/adapter.ts b/renderers/lit/src/v0_9/adapter.ts new file mode 100644 index 000000000..d836510ff --- /dev/null +++ b/renderers/lit/src/v0_9/adapter.ts @@ -0,0 +1,42 @@ +import { ReactiveController, LitElement } from "lit"; +import { GenericBinder, ComponentContext, ComponentApi, ResolveA2uiProps } from "@a2ui/web_core/v0_9"; +import { ChildBuilder, LitComponentImplementation } from "./types.js"; + +export class A2uiController implements ReactiveController { + public props: ResolveA2uiProps; + private binder: GenericBinder; + private subscription?: { unsubscribe: () => void }; + + constructor(private host: LitElement & { context: ComponentContext }, api: ComponentApi) { + this.binder = new GenericBinder(this.host.context, api.schema); + this.props = this.binder.snapshot as ResolveA2uiProps; + this.host.addController(this); + } + + hostConnected() { + this.subscription = this.binder.subscribe((newProps) => { + this.props = newProps as ResolveA2uiProps; + this.host.requestUpdate(); + }); + } + + hostDisconnected() { + this.subscription?.unsubscribe(); + } + + dispose() { + this.binder.dispose(); + } +} + +import { z } from "zod"; + +export function createLitComponent( + api: ComponentApi, + renderFn: (args: { props: ResolveA2uiProps; buildChild: ChildBuilder; context: ComponentContext }) => unknown +): LitComponentImplementation { + return { + ...api, + render: renderFn, + }; +} \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/minimal/components/Button.ts b/renderers/lit/src/v0_9/catalogs/minimal/components/Button.ts new file mode 100644 index 000000000..820cf1aa4 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/minimal/components/Button.ts @@ -0,0 +1,30 @@ +import { html } from "lit"; +import { classMap } from "lit/directives/class-map.js"; +import { createLitComponent } from "../../../adapter.js"; +import { ButtonApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiButton = createLitComponent(ButtonApi, ({ props, buildChild }) => { + const isDisabled = props.isValid === false; + + const onClick = () => { + if (!isDisabled && props.action) { + props.action(); + } + }; + + const classes = { + "a2ui-button": true, + "a2ui-button-primary": props.variant === "primary", + "a2ui-button-borderless": props.variant === "borderless", + }; + + return html` + + `; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts b/renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts new file mode 100644 index 000000000..39f4c2288 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts @@ -0,0 +1,49 @@ +import { html } from "lit"; +import { map } from "lit/directives/map.js"; +import { styleMap } from "lit/directives/style-map.js"; +import { createLitComponent } from "../../../adapter.js"; +import { ColumnApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiColumn = createLitComponent(ColumnApi, ({ props, buildChild }) => { + const childrenArray = Array.isArray(props.children) ? props.children : []; + + const styles = { + display: "flex", + flexDirection: "column", + justifyContent: mapJustify(props.justify), + alignItems: mapAlign(props.align), + flex: props.weight !== undefined ? String(props.weight) : "initial", + }; + + return html` +
)}> + ${map(childrenArray, (child: any) => { + if (typeof child === 'string') return buildChild(child); + return buildChild(child.id, child.basePath); + })} +
+ `; +}); + +function mapJustify(justify: string | undefined): string { + switch (justify) { + case "start": return "flex-start"; + case "center": return "center"; + case "end": return "flex-end"; + case "spaceBetween": return "space-between"; + case "spaceAround": return "space-around"; + case "spaceEvenly": return "space-evenly"; + case "stretch": return "stretch"; + default: return "flex-start"; + } +} + +function mapAlign(align: string | undefined): string { + switch (align) { + case "start": return "flex-start"; + case "center": return "center"; + case "end": return "flex-end"; + case "stretch": return "stretch"; + default: return "stretch"; + } +} \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts b/renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts new file mode 100644 index 000000000..eba2314b6 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts @@ -0,0 +1,49 @@ +import { html } from "lit"; +import { map } from "lit/directives/map.js"; +import { styleMap } from "lit/directives/style-map.js"; +import { createLitComponent } from "../../../adapter.js"; +import { RowApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiRow = createLitComponent(RowApi, ({ props, buildChild }) => { + const childrenArray = Array.isArray(props.children) ? props.children : []; + + const styles = { + display: "flex", + flexDirection: "row", + justifyContent: mapJustify(props.justify), + alignItems: mapAlign(props.align), + flex: props.weight !== undefined ? String(props.weight) : "initial", + }; + + return html` +
)}> + ${map(childrenArray, (child: any) => { + if (typeof child === 'string') return buildChild(child); + return buildChild(child.id, child.basePath); + })} +
+ `; +}); + +function mapJustify(justify: string | undefined): string { + switch (justify) { + case "start": return "flex-start"; + case "center": return "center"; + case "end": return "flex-end"; + case "spaceBetween": return "space-between"; + case "spaceAround": return "space-around"; + case "spaceEvenly": return "space-evenly"; + case "stretch": return "stretch"; + default: return "flex-start"; + } +} + +function mapAlign(align: string | undefined): string { + switch (align) { + case "start": return "flex-start"; + case "center": return "center"; + case "end": return "flex-end"; + case "stretch": return "stretch"; + default: return "stretch"; + } +} \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/minimal/components/Text.ts b/renderers/lit/src/v0_9/catalogs/minimal/components/Text.ts new file mode 100644 index 000000000..dac7bbad9 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/minimal/components/Text.ts @@ -0,0 +1,32 @@ +import { html } from "lit"; +import { createLitComponent } from "../../../adapter.js"; +import { TextApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiText = createLitComponent(TextApi, ({ props }) => { + const variant = props.variant ?? "body"; + // Basic implementation without markdown parser for minimal catalog + // Basic catalog would add markdown support + + const tagMap: Record = { + h1: "h1", + h2: "h2", + h3: "h3", + h4: "h4", + h5: "h5", + caption: "span", + body: "p" + }; + const tag = tagMap[variant as string] || "p"; + + // Note: Lit html doesn't allow dynamic tag names directly like html`<${tag}>`, + // so we have to use static templates or unsafeHTML. Static templates are safer. + switch (variant) { + case "h1": return html`

${props.text}

`; + case "h2": return html`

${props.text}

`; + case "h3": return html`

${props.text}

`; + case "h4": return html`

${props.text}

`; + case "h5": return html`
${props.text}
`; + case "caption": return html`${props.text}`; + default: return html`

${props.text}

`; + } +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/minimal/components/TextField.ts b/renderers/lit/src/v0_9/catalogs/minimal/components/TextField.ts new file mode 100644 index 000000000..d55619314 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/minimal/components/TextField.ts @@ -0,0 +1,52 @@ +import { html } from "lit"; +import { classMap } from "lit/directives/class-map.js"; +import { createLitComponent } from "../../../adapter.js"; +import { TextFieldApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiTextField = createLitComponent(TextFieldApi, ({ props }) => { + const isInvalid = props.isValid === false; + + const onInput = (e: Event) => { + const target = e.target as HTMLInputElement; + if (props.setValue) { + props.setValue(target.value); + } + }; + + const classes = { + "a2ui-textfield": true, + "a2ui-textfield-invalid": isInvalid, + }; + + let type = "text"; + if (props.variant === "number") type = "number"; + if (props.variant === "obscured") type = "password"; + + return html` +
+ ${props.label ? html`` : ""} + + ${props.variant === "longText" + ? html` + ` + : html` + ` + } + + ${isInvalid && props.validationErrors && props.validationErrors.length > 0 + ? html`
${props.validationErrors[0]}
` + : ""} +
+ `; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/minimal/functions/capitalize.ts b/renderers/lit/src/v0_9/catalogs/minimal/functions/capitalize.ts new file mode 100644 index 000000000..48b2496ac --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/minimal/functions/capitalize.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; +import { createFunctionImplementation } from "@a2ui/web_core/v0_9"; + +export const CapitalizeApi = { + name: "capitalize" as const, + returnType: "string" as const, + schema: z.object({ + value: z.string() + }) as z.ZodType +}; + +export const CapitalizeImplementation = createFunctionImplementation( + CapitalizeApi as any, + (args) => { + if (!args.value) return ""; + return args.value.charAt(0).toUpperCase() + args.value.slice(1); + } +); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/minimal/index.ts b/renderers/lit/src/v0_9/catalogs/minimal/index.ts new file mode 100644 index 000000000..d311c324e --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/minimal/index.ts @@ -0,0 +1,14 @@ +import { Catalog } from "@a2ui/web_core/v0_9"; +import { LitComponentImplementation } from "../../types.js"; +import { A2uiText } from "./components/Text.js"; +import { A2uiButton } from "./components/Button.js"; +import { A2uiTextField } from "./components/TextField.js"; +import { A2uiRow } from "./components/Row.js"; +import { A2uiColumn } from "./components/Column.js"; +import { CapitalizeImplementation } from "./functions/capitalize.js"; + +export const minimalCatalog = new Catalog( + "https://a2ui.org/specification/v0_9/catalogs/minimal/minimal_catalog.json", + [A2uiText, A2uiButton, A2uiTextField, A2uiRow, A2uiColumn], + [CapitalizeImplementation] +); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/index.ts b/renderers/lit/src/v0_9/index.ts new file mode 100644 index 000000000..6f5103f83 --- /dev/null +++ b/renderers/lit/src/v0_9/index.ts @@ -0,0 +1,5 @@ +export * from "./types.js"; +export * from "./adapter.js"; +export * from "./surface/A2uiSurface.js"; +export * from "./surface/render-node.js"; +export * from "./catalogs/minimal/index.js"; \ No newline at end of file diff --git a/renderers/lit/src/v0_9/surface/A2uiSurface.ts b/renderers/lit/src/v0_9/surface/A2uiSurface.ts new file mode 100644 index 000000000..2101edb9c --- /dev/null +++ b/renderers/lit/src/v0_9/surface/A2uiSurface.ts @@ -0,0 +1,60 @@ +import { html, nothing, LitElement, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { SurfaceModel, ComponentContext } from "@a2ui/web_core/v0_9"; +import "./render-node.js"; + +@customElement("a2ui-surface") +export class A2uiSurface extends LitElement { + @property({ type: Object }) accessor surface: SurfaceModel | undefined = undefined; + + @state() accessor _hasRoot = false; + private unsub?: () => void; + + createRenderRoot() { + return this; + } + + protected willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has('surface')) { + if (this.unsub) { + this.unsub(); + this.unsub = undefined; + } + this._hasRoot = !!this.surface?.componentsModel.get("root"); + + if (this.surface && !this._hasRoot) { + const sub = this.surface.componentsModel.onCreated.subscribe((comp) => { + if (comp.id === "root") { + this._hasRoot = true; + this.unsub?.(); + this.unsub = undefined; + } + }); + this.unsub = () => sub.unsubscribe(); + } + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (this.unsub) { + this.unsub(); + this.unsub = undefined; + } + } + + render() { + if (!this.surface) return nothing; + if (!this._hasRoot) { + return html`
Loading surface...
`; + } + + try { + const rootContext = new ComponentContext(this.surface, "root", "/"); + return html``; + } catch (e) { + console.error("Error creating root context:", e); + return html`
Error rendering surface
`; + } + } +} \ No newline at end of file diff --git a/renderers/lit/src/v0_9/surface/render-node.ts b/renderers/lit/src/v0_9/surface/render-node.ts new file mode 100644 index 000000000..e75310897 --- /dev/null +++ b/renderers/lit/src/v0_9/surface/render-node.ts @@ -0,0 +1,70 @@ +import { html, nothing, LitElement, PropertyValues } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { ComponentContext } from "@a2ui/web_core/v0_9"; +import { A2uiController } from "../adapter.js"; +import { ChildBuilder, LitComponentImplementation } from "../types.js"; + +@customElement("a2ui-node") +export class A2uiNode extends LitElement { + @property({ type: Object }) accessor context!: ComponentContext; + + private controller?: A2uiController; + private implementation?: LitComponentImplementation; + + createRenderRoot() { + return this; + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (this.controller) { + this.controller.dispose(); + } + } + + protected willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + if (changedProperties.has('context')) { + if (this.controller) { + this.removeController(this.controller); + this.controller.dispose(); + this.controller = undefined; + } + + if (this.context) { + const type = this.context.componentModel.type; + const catalog = this.context.dataContext.surface.catalog; + this.implementation = catalog.components.get(type) as LitComponentImplementation | undefined; + + if (this.implementation) { + this.controller = new A2uiController(this, this.implementation); + } else { + console.warn(`Component implementation not found for type: ${type}`); + } + } + } + } + + render() { + if (!this.controller || !this.implementation || !this.context) return nothing; + + const buildChild: ChildBuilder = (id: string, overrideBasePath?: string) => { + const surface = this.context.dataContext.surface; + const basePath = overrideBasePath ?? this.context.dataContext.path; + + try { + const childContext = new ComponentContext(surface, id, basePath); + return html``; + } catch (e) { + console.error(`Error building child ${id}:`, e); + return nothing; + } + }; + + return this.implementation.render({ + props: this.controller.props, + buildChild, + context: this.context + }); + } +} \ No newline at end of file diff --git a/renderers/lit/src/v0_9/tests/integration.test.ts b/renderers/lit/src/v0_9/tests/integration.test.ts new file mode 100644 index 000000000..afd4fffb8 --- /dev/null +++ b/renderers/lit/src/v0_9/tests/integration.test.ts @@ -0,0 +1,48 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { MessageProcessor } from "@a2ui/web_core/v0_9"; +import { minimalCatalog } from "../catalogs/minimal/index.js"; +import fs from "fs"; +import path from "path"; + +describe("v0.9 Minimal Catalog Examples", () => { + const examplesDir = path.resolve( + process.cwd(), + "../specification/v0_9/json/catalogs/minimal/examples" + ); + + const files = fs.readdirSync(examplesDir).filter((f) => f.endsWith(".json")); + + for (const file of files) { + it(`should successfully process ${file}`, () => { + const content = fs.readFileSync(path.join(examplesDir, file), "utf-8"); + const data = JSON.parse(content); + const messages = Array.isArray(data) ? data : data.messages || []; + + let surfaceId = file.replace(".json", ""); + const createMsg = messages.find((m: any) => m.createSurface); + if (createMsg) { + surfaceId = createMsg.createSurface.surfaceId; + } else { + messages.unshift({ + version: "v0.9", + createSurface: { + surfaceId, + catalogId: minimalCatalog.id, + }, + }); + } + + const processor = new MessageProcessor([minimalCatalog]); + + // Should process without throwing validation or parsing errors + processor.processMessages(messages); + + const surface = processor.model.getSurface(surfaceId); + assert.ok(surface, `Surface ${surfaceId} should exist`); + + const rootNode = surface.componentsModel.get("root"); + assert.ok(rootNode, "Surface should have a root component"); + }); + } +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/types.ts b/renderers/lit/src/v0_9/types.ts new file mode 100644 index 000000000..c62b26f88 --- /dev/null +++ b/renderers/lit/src/v0_9/types.ts @@ -0,0 +1,11 @@ +import { ResolveA2uiProps, ComponentApi, ComponentContext } from "@a2ui/web_core/v0_9"; + +export type ChildBuilder = (id: string, overrideBasePath?: string) => unknown; + +export interface LitComponentImplementation extends ComponentApi { + render(args: { + props: ResolveA2uiProps; + buildChild: ChildBuilder; + context: ComponentContext; + }): unknown; +} \ No newline at end of file From 824489e5a0da8e523932b767d5017b6cce546f31 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 17 Mar 2026 10:14:04 +1030 Subject: [PATCH 3/4] Improvements to lit renderer --- .../catalogs/basic/components/AudioPlayer.ts | 11 + .../v0_9/catalogs/basic/components/Button.ts | 17 + .../v0_9/catalogs/basic/components/Card.ts | 7 + .../catalogs/basic/components/CheckBox.ts | 12 + .../catalogs/basic/components/ChoicePicker.ts | 32 + .../v0_9/catalogs/basic/components/Column.ts | 11 + .../basic/components/DateTimeInput.ts | 13 + .../v0_9/catalogs/basic/components/Divider.ts | 9 + .../v0_9/catalogs/basic/components/Icon.ts | 8 + .../v0_9/catalogs/basic/components/Image.ts | 9 + .../v0_9/catalogs/basic/components/List.ts | 11 + .../v0_9/catalogs/basic/components/Modal.ts | 33 + .../src/v0_9/catalogs/basic/components/Row.ts | 11 + .../v0_9/catalogs/basic/components/Slider.ts | 13 + .../v0_9/catalogs/basic/components/Tabs.ts | 39 + .../v0_9/catalogs/basic/components/Text.ts | 18 + .../catalogs/basic/components/TextField.ts | 23 + .../v0_9/catalogs/basic/components/Video.ts | 7 + .../lit/src/v0_9/catalogs/basic/index.ts | 32 + .../catalogs/minimal/functions/capitalize.ts | 2 +- renderers/lit/src/v0_9/surface/render-node.ts | 1 + .../lit/src/v0_9/tests/integration.test.ts | 30 +- samples/client/lit/gallery_v0_9/.gitignore | 3 + samples/client/lit/gallery_v0_9/README.md | 55 + samples/client/lit/gallery_v0_9/index.html | 41 + .../client/lit/gallery_v0_9/package-lock.json | 1200 +++++++++++++++++ samples/client/lit/gallery_v0_9/package.json | 25 + .../lit/gallery_v0_9/scripts/sync-specs.js | 52 + .../lit/gallery_v0_9/src/local-gallery.ts | 362 +++++ samples/client/lit/gallery_v0_9/src/main.ts | 17 + samples/client/lit/gallery_v0_9/src/theme.ts | 456 +++++++ samples/client/lit/gallery_v0_9/tsconfig.json | 16 + .../client/lit/gallery_v0_9/vite.config.ts | 26 + 33 files changed, 2597 insertions(+), 5 deletions(-) create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/AudioPlayer.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/Button.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/Card.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/CheckBox.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/ChoicePicker.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/Column.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/DateTimeInput.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/Divider.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/Icon.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/Image.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/List.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/Modal.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/Row.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/Slider.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/Tabs.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/Text.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/TextField.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/components/Video.ts create mode 100644 renderers/lit/src/v0_9/catalogs/basic/index.ts create mode 100644 samples/client/lit/gallery_v0_9/.gitignore create mode 100644 samples/client/lit/gallery_v0_9/README.md create mode 100644 samples/client/lit/gallery_v0_9/index.html create mode 100644 samples/client/lit/gallery_v0_9/package-lock.json create mode 100644 samples/client/lit/gallery_v0_9/package.json create mode 100644 samples/client/lit/gallery_v0_9/scripts/sync-specs.js create mode 100644 samples/client/lit/gallery_v0_9/src/local-gallery.ts create mode 100644 samples/client/lit/gallery_v0_9/src/main.ts create mode 100644 samples/client/lit/gallery_v0_9/src/theme.ts create mode 100644 samples/client/lit/gallery_v0_9/tsconfig.json create mode 100644 samples/client/lit/gallery_v0_9/vite.config.ts diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/AudioPlayer.ts b/renderers/lit/src/v0_9/catalogs/basic/components/AudioPlayer.ts new file mode 100644 index 000000000..388e68f31 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/AudioPlayer.ts @@ -0,0 +1,11 @@ +import { html } from "lit"; +import { createLitComponent } from "../../../adapter.js"; +import { AudioPlayerApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiAudioPlayer = createLitComponent(AudioPlayerApi, ({ props }) => { + return html` +
+ ${props.description ? html`

${props.description}

` : ""} + +
`; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/Button.ts b/renderers/lit/src/v0_9/catalogs/basic/components/Button.ts new file mode 100644 index 000000000..ca880739d --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/Button.ts @@ -0,0 +1,17 @@ +import { html } from "lit"; +import { classMap } from "lit/directives/class-map.js"; +import { createLitComponent } from "../../../adapter.js"; +import { ButtonApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiButton = createLitComponent(ButtonApi, ({ props, buildChild }) => { + const isDisabled = props.isValid === false; + return html` + + `; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/Card.ts b/renderers/lit/src/v0_9/catalogs/basic/components/Card.ts new file mode 100644 index 000000000..0d1f4f83b --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/Card.ts @@ -0,0 +1,7 @@ +import { html } from "lit"; +import { createLitComponent } from "../../../adapter.js"; +import { CardApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiCard = createLitComponent(CardApi, ({ props, buildChild }) => { + return html`
${props.child ? buildChild(props.child) : ""}
`; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/CheckBox.ts b/renderers/lit/src/v0_9/catalogs/basic/components/CheckBox.ts new file mode 100644 index 000000000..04e29e98d --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/CheckBox.ts @@ -0,0 +1,12 @@ +import { html } from "lit"; +import { createLitComponent } from "../../../adapter.js"; +import { CheckBoxApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiCheckBox = createLitComponent(CheckBoxApi, ({ props }) => { + return html` + + `; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/ChoicePicker.ts b/renderers/lit/src/v0_9/catalogs/basic/components/ChoicePicker.ts new file mode 100644 index 000000000..c838e97d4 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/ChoicePicker.ts @@ -0,0 +1,32 @@ +import { html } from "lit"; +import { createLitComponent } from "../../../adapter.js"; +import { ChoicePickerApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiChoicePicker = createLitComponent(ChoicePickerApi, ({ props }) => { + const selected = Array.isArray(props.value) ? props.value : []; + const isMulti = props.variant === "multipleSelection"; + + const toggle = (val: string) => { + if (!props.setValue) return; + if (isMulti) { + if (selected.includes(val)) props.setValue(selected.filter(v => v !== val)); + else props.setValue([...selected, val]); + } else { + props.setValue([val]); + } + }; + + return html` +
+ ${props.label ? html`` : ""} +
+ ${props.options?.map((opt: any) => html` + + `)} +
+
+ `; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/Column.ts b/renderers/lit/src/v0_9/catalogs/basic/components/Column.ts new file mode 100644 index 000000000..bb9531032 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/Column.ts @@ -0,0 +1,11 @@ +import { html } from "lit"; +import { map } from "lit/directives/map.js"; +import { styleMap } from "lit/directives/style-map.js"; +import { createLitComponent } from "../../../adapter.js"; +import { ColumnApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiColumn = createLitComponent(ColumnApi, ({ props, buildChild }) => { + const children = Array.isArray(props.children) ? props.children : []; + const styles = { display: "flex", flexDirection: "column", flex: props.weight !== undefined ? String(props.weight) : "initial", gap: "8px" }; + return html`
${map(children, (child: any) => typeof child === 'string' ? buildChild(child) : buildChild(child.id, child.basePath))}
`; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/DateTimeInput.ts b/renderers/lit/src/v0_9/catalogs/basic/components/DateTimeInput.ts new file mode 100644 index 000000000..ec18d2954 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/DateTimeInput.ts @@ -0,0 +1,13 @@ +import { html } from "lit"; +import { createLitComponent } from "../../../adapter.js"; +import { DateTimeInputApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiDateTimeInput = createLitComponent(DateTimeInputApi, ({ props }) => { + const type = (props.enableDate && props.enableTime) ? "datetime-local" : (props.enableDate ? "date" : "time"); + return html` +
+ ${props.label ? html`` : ""} + props.setValue?.((e.target as HTMLInputElement).value)} /> +
+ `; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/Divider.ts b/renderers/lit/src/v0_9/catalogs/basic/components/Divider.ts new file mode 100644 index 000000000..1bd543c25 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/Divider.ts @@ -0,0 +1,9 @@ +import { html } from "lit"; +import { createLitComponent } from "../../../adapter.js"; +import { DividerApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiDivider = createLitComponent(DividerApi, ({ props }) => { + return props.axis === "vertical" + ? html`
` + : html`
`; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/Icon.ts b/renderers/lit/src/v0_9/catalogs/basic/components/Icon.ts new file mode 100644 index 000000000..28e9ecc41 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/Icon.ts @@ -0,0 +1,8 @@ +import { html } from "lit"; +import { createLitComponent } from "../../../adapter.js"; +import { IconApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiIcon = createLitComponent(IconApi, ({ props }) => { + const name = typeof props.name === 'string' ? props.name : (props.name as any)?.path; + return html`${name}`; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/Image.ts b/renderers/lit/src/v0_9/catalogs/basic/components/Image.ts new file mode 100644 index 000000000..178b541fd --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/Image.ts @@ -0,0 +1,9 @@ +import { html } from "lit"; +import { styleMap } from "lit/directives/style-map.js"; +import { createLitComponent } from "../../../adapter.js"; +import { ImageApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiImage = createLitComponent(ImageApi, ({ props }) => { + const styles = { objectFit: props.fit || "fill", width: "100%" }; + return html``; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/List.ts b/renderers/lit/src/v0_9/catalogs/basic/components/List.ts new file mode 100644 index 000000000..c0758e098 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/List.ts @@ -0,0 +1,11 @@ +import { html } from "lit"; +import { map } from "lit/directives/map.js"; +import { styleMap } from "lit/directives/style-map.js"; +import { createLitComponent } from "../../../adapter.js"; +import { ListApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiList = createLitComponent(ListApi, ({ props, buildChild }) => { + const children = Array.isArray(props.children) ? props.children : []; + const styles = { display: "flex", flexDirection: props.direction === "horizontal" ? "row" : "column", overflow: "auto", gap: "8px" }; + return html`
${map(children, (child: any) => typeof child === 'string' ? buildChild(child) : buildChild(child.id, child.basePath))}
`; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/Modal.ts b/renderers/lit/src/v0_9/catalogs/basic/components/Modal.ts new file mode 100644 index 000000000..f537400b6 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/Modal.ts @@ -0,0 +1,33 @@ +import { LitElement, html } from "lit"; +import { customElement, property, query } from "lit/decorators.js"; +import { A2uiController } from "../../../adapter.js"; +import { ComponentContext } from "@a2ui/web_core/v0_9"; +import { ModalApi } from "@a2ui/web_core/v0_9/basic_catalog"; +import { ChildBuilder, LitComponentImplementation } from "../../../types.js"; + +@customElement("a2ui-lit-modal") +export class A2uiLitModal extends LitElement { + @property({ type: Object }) accessor context!: ComponentContext; + @property({ type: Function }) accessor buildChild!: ChildBuilder; + private a2ui = new A2uiController(this as any, ModalApi); + @query("dialog") accessor dialog!: HTMLDialogElement; + + render() { + const props = this.a2ui.props as any; + return html` +
this.dialog?.showModal()}> + ${props.trigger ? this.buildChild(props.trigger) : ''} +
+ +
+ ${props.content ? this.buildChild(props.content) : ''} +
+ `; + } +} + +export const A2uiModal: LitComponentImplementation = { + name: "Modal", + schema: ModalApi.schema, + render: ({ context, buildChild }) => html`` +}; \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/Row.ts b/renderers/lit/src/v0_9/catalogs/basic/components/Row.ts new file mode 100644 index 000000000..649e765e3 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/Row.ts @@ -0,0 +1,11 @@ +import { html } from "lit"; +import { map } from "lit/directives/map.js"; +import { styleMap } from "lit/directives/style-map.js"; +import { createLitComponent } from "../../../adapter.js"; +import { RowApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiRow = createLitComponent(RowApi, ({ props, buildChild }) => { + const children = Array.isArray(props.children) ? props.children : []; + const styles = { display: "flex", flexDirection: "row", flex: props.weight !== undefined ? String(props.weight) : "initial", gap: "8px" }; + return html`
${map(children, (child: any) => typeof child === 'string' ? buildChild(child) : buildChild(child.id, child.basePath))}
`; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/Slider.ts b/renderers/lit/src/v0_9/catalogs/basic/components/Slider.ts new file mode 100644 index 000000000..486e32cb7 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/Slider.ts @@ -0,0 +1,13 @@ +import { html } from "lit"; +import { createLitComponent } from "../../../adapter.js"; +import { SliderApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiSlider = createLitComponent(SliderApi, ({ props }) => { + return html` +
+ ${props.label ? html`` : ""} + props.setValue?.(Number((e.target as HTMLInputElement).value))} /> + ${props.value} +
+ `; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/Tabs.ts b/renderers/lit/src/v0_9/catalogs/basic/components/Tabs.ts new file mode 100644 index 000000000..348ef1bf5 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/Tabs.ts @@ -0,0 +1,39 @@ +import { LitElement, html } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { A2uiController } from "../../../adapter.js"; +import { ComponentContext } from "@a2ui/web_core/v0_9"; +import { TabsApi } from "@a2ui/web_core/v0_9/basic_catalog"; +import { ChildBuilder, LitComponentImplementation } from "../../../types.js"; + +@customElement("a2ui-lit-tabs") +export class A2uiLitTabs extends LitElement { + @property({ type: Object }) accessor context!: ComponentContext; + @property({ type: Function }) accessor buildChild!: ChildBuilder; + private a2ui = new A2uiController(this as any, TabsApi); + @state() accessor activeIndex = 0; + + render() { + const props = this.a2ui.props as any; + if (!props.tabs) return html``; + return html` +
+
+ ${props.tabs.map((tab: any, i: number) => html` + + `)} +
+
+ ${props.tabs[this.activeIndex] ? this.buildChild(props.tabs[this.activeIndex].child) : ''} +
+
+ `; + } +} + +export const A2uiTabs: LitComponentImplementation = { + name: "Tabs", + schema: TabsApi.schema, + render: ({ context, buildChild }) => html`` +}; \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/Text.ts b/renderers/lit/src/v0_9/catalogs/basic/components/Text.ts new file mode 100644 index 000000000..0a48b9eca --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/Text.ts @@ -0,0 +1,18 @@ +import { html } from "lit"; +import { createLitComponent } from "../../../adapter.js"; +import { TextApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiText = createLitComponent(TextApi, ({ props }) => { + const variant = props.variant ?? "body"; + const tagMap: Record = { h1: "h1", h2: "h2", h3: "h3", h4: "h4", h5: "h5", caption: "span", body: "p" }; + const tag = tagMap[variant as string] || "p"; + switch (variant) { + case "h1": return html`

${props.text}

`; + case "h2": return html`

${props.text}

`; + case "h3": return html`

${props.text}

`; + case "h4": return html`

${props.text}

`; + case "h5": return html`
${props.text}
`; + case "caption": return html`${props.text}`; + default: return html`

${props.text}

`; + } +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/TextField.ts b/renderers/lit/src/v0_9/catalogs/basic/components/TextField.ts new file mode 100644 index 000000000..04999a746 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/TextField.ts @@ -0,0 +1,23 @@ +import { html } from "lit"; +import { classMap } from "lit/directives/class-map.js"; +import { createLitComponent } from "../../../adapter.js"; +import { TextFieldApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiTextField = createLitComponent(TextFieldApi, ({ props }) => { + const isInvalid = props.isValid === false; + const onInput = (e: Event) => props.setValue?.((e.target as HTMLInputElement).value); + let type = "text"; + if (props.variant === "number") type = "number"; + if (props.variant === "obscured") type = "password"; + + return html` +
+ ${props.label ? html`` : ""} + ${props.variant === "longText" + ? html`` + : html`` + } + ${isInvalid && props.validationErrors?.length ? html`
${props.validationErrors[0]}
` : ""} +
+ `; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/Video.ts b/renderers/lit/src/v0_9/catalogs/basic/components/Video.ts new file mode 100644 index 000000000..6a5417ec8 --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/components/Video.ts @@ -0,0 +1,7 @@ +import { html } from "lit"; +import { createLitComponent } from "../../../adapter.js"; +import { VideoApi } from "@a2ui/web_core/v0_9/basic_catalog"; + +export const A2uiVideo = createLitComponent(VideoApi, ({ props }) => { + return html``; +}); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/basic/index.ts b/renderers/lit/src/v0_9/catalogs/basic/index.ts new file mode 100644 index 000000000..9e3717f1c --- /dev/null +++ b/renderers/lit/src/v0_9/catalogs/basic/index.ts @@ -0,0 +1,32 @@ +import { Catalog } from "@a2ui/web_core/v0_9"; +import { LitComponentImplementation } from "../../types.js"; +import { BASIC_FUNCTIONS } from "@a2ui/web_core/v0_9/basic_catalog"; + +import { A2uiText } from "./components/Text.js"; +import { A2uiButton } from "./components/Button.js"; +import { A2uiTextField } from "./components/TextField.js"; +import { A2uiRow } from "./components/Row.js"; +import { A2uiColumn } from "./components/Column.js"; +import { A2uiList } from "./components/List.js"; +import { A2uiImage } from "./components/Image.js"; +import { A2uiIcon } from "./components/Icon.js"; +import { A2uiVideo } from "./components/Video.js"; +import { A2uiAudioPlayer } from "./components/AudioPlayer.js"; +import { A2uiCard } from "./components/Card.js"; +import { A2uiDivider } from "./components/Divider.js"; +import { A2uiCheckBox } from "./components/CheckBox.js"; +import { A2uiSlider } from "./components/Slider.js"; +import { A2uiDateTimeInput } from "./components/DateTimeInput.js"; +import { A2uiChoicePicker } from "./components/ChoicePicker.js"; +import { A2uiTabs } from "./components/Tabs.js"; +import { A2uiModal } from "./components/Modal.js"; + +export const basicCatalog = new Catalog( + "https://a2ui.org/specification/v0_9/basic_catalog.json", + [ + A2uiText, A2uiButton, A2uiTextField, A2uiRow, A2uiColumn, A2uiList, A2uiImage, A2uiIcon, + A2uiVideo, A2uiAudioPlayer, A2uiCard, A2uiDivider, A2uiCheckBox, A2uiSlider, A2uiDateTimeInput, + A2uiChoicePicker, A2uiTabs, A2uiModal + ], + BASIC_FUNCTIONS as any +); \ No newline at end of file diff --git a/renderers/lit/src/v0_9/catalogs/minimal/functions/capitalize.ts b/renderers/lit/src/v0_9/catalogs/minimal/functions/capitalize.ts index 48b2496ac..0e70dca3b 100644 --- a/renderers/lit/src/v0_9/catalogs/minimal/functions/capitalize.ts +++ b/renderers/lit/src/v0_9/catalogs/minimal/functions/capitalize.ts @@ -5,7 +5,7 @@ export const CapitalizeApi = { name: "capitalize" as const, returnType: "string" as const, schema: z.object({ - value: z.string() + value: z.preprocess(v => v === undefined ? undefined : String(v), z.string()).optional() }) as z.ZodType }; diff --git a/renderers/lit/src/v0_9/surface/render-node.ts b/renderers/lit/src/v0_9/surface/render-node.ts index e75310897..fb12f8f37 100644 --- a/renderers/lit/src/v0_9/surface/render-node.ts +++ b/renderers/lit/src/v0_9/surface/render-node.ts @@ -49,6 +49,7 @@ export class A2uiNode extends LitElement { if (!this.controller || !this.implementation || !this.context) return nothing; const buildChild: ChildBuilder = (id: string, overrideBasePath?: string) => { + if (!id) return nothing; const surface = this.context.dataContext.surface; const basePath = overrideBasePath ?? this.context.dataContext.path; diff --git a/renderers/lit/src/v0_9/tests/integration.test.ts b/renderers/lit/src/v0_9/tests/integration.test.ts index afd4fffb8..365090d4c 100644 --- a/renderers/lit/src/v0_9/tests/integration.test.ts +++ b/renderers/lit/src/v0_9/tests/integration.test.ts @@ -1,20 +1,21 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { MessageProcessor } from "@a2ui/web_core/v0_9"; +import { MessageProcessor, GenericBinder, ComponentContext } from "@a2ui/web_core/v0_9"; import { minimalCatalog } from "../catalogs/minimal/index.js"; +import { TextApi } from "@a2ui/web_core/v0_9/basic_catalog"; import fs from "fs"; import path from "path"; describe("v0.9 Minimal Catalog Examples", () => { const examplesDir = path.resolve( process.cwd(), - "../specification/v0_9/json/catalogs/minimal/examples" + "../../specification/v0_9/json/catalogs/minimal/examples" ); const files = fs.readdirSync(examplesDir).filter((f) => f.endsWith(".json")); for (const file of files) { - it(`should successfully process ${file}`, () => { + it(`should successfully process ${file}`, async () => { const content = fs.readFileSync(path.join(examplesDir, file), "utf-8"); const data = JSON.parse(content); const messages = Array.isArray(data) ? data : data.messages || []; @@ -35,7 +36,6 @@ describe("v0.9 Minimal Catalog Examples", () => { const processor = new MessageProcessor([minimalCatalog]); - // Should process without throwing validation or parsing errors processor.processMessages(messages); const surface = processor.model.getSurface(surfaceId); @@ -43,6 +43,28 @@ describe("v0.9 Minimal Catalog Examples", () => { const rootNode = surface.componentsModel.get("root"); assert.ok(rootNode, "Surface should have a root component"); + + if (file.includes("capitalized_text")) { + const textNode = surface.componentsModel.get("result_text"); + assert.ok(textNode); + + const context = new ComponentContext(surface, "result_text"); + const binder = new GenericBinder(context, TextApi.schema); + const sub = binder.subscribe(() => {}); // Force connection + + // Wait a microtask to let initial resolution finish + await new Promise((r) => setTimeout(r, 0)); + assert.strictEqual(binder.snapshot.text, ""); + + // Set value in data model + surface.dataModel.set("/inputValue", "hello world"); + + await new Promise((r) => setTimeout(r, 0)); + assert.strictEqual(binder.snapshot.text, "Hello world"); + + sub.unsubscribe(); + binder.dispose(); + } }); } }); \ No newline at end of file diff --git a/samples/client/lit/gallery_v0_9/.gitignore b/samples/client/lit/gallery_v0_9/.gitignore new file mode 100644 index 000000000..f8ebe148f --- /dev/null +++ b/samples/client/lit/gallery_v0_9/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +public/specs diff --git a/samples/client/lit/gallery_v0_9/README.md b/samples/client/lit/gallery_v0_9/README.md new file mode 100644 index 000000000..cbdaf4ffc --- /dev/null +++ b/samples/client/lit/gallery_v0_9/README.md @@ -0,0 +1,55 @@ +# A2UI Local Gallery (Minimal v0.8) + +This is a standalone, agentless web application designed to render the A2UI v0.8 minimal examples directly from static JSON files. It serves as a focused environment for testing renderer subset compatibility and protocol compliance. + +## Prerequisites + +Before running this gallery, you **must** build the shared A2UI renderers. + +### 1. Build Shared Renderers + +Navigate to the project root and run: + +```bash +# Build Web Core +cd renderers/web_core +npm install +npm run build + +# Build Lit Renderer +cd ../lit +npm install +npm run build +``` + +For more details on building the renderers, see: +- [Web Core README](../../../../renderers/web_core/README.md) +- [Lit Renderer README](../../../../renderers/lit/README.md) + +## Getting Started + +1. **Navigate to this directory**: + ```bash + cd samples/client/lit/local_gallery + ``` + +2. **Install dependencies**: + ```bash + npm install + ``` + +3. **Run the development server**: + ```bash + npm run dev + ``` + This command will: + - Sync all JSON examples from `specification/v0_8/json/catalogs/minimal/examples/`. + - Generate a manifest file (`index.json`) for dynamic discovery. + - Start the Vite server at `http://localhost:5173`. + +## Architecture + +- **Agentless**: Unlike other samples, this does not require a running Python agent. It simulates agent responses locally for interactive components (like the Login Form). +- **Dynamic Loading**: The app automatically discovers and loads *all* `.json` files present in the v0.8 minimal specification folder at build time. To add a new test case, simply drop a JSON file into that specification folder and restart the dev server. +- **Surface Isolation**: Each example is rendered into its own independent `a2ui-surface` with a unique ID derived from the filename. +- **Mock Agent Console**: All user interactions (button clicks, form submissions) are intercepted and logged to a sidebar, demonstrating how the renderer resolves actions and contexts. diff --git a/samples/client/lit/gallery_v0_9/index.html b/samples/client/lit/gallery_v0_9/index.html new file mode 100644 index 000000000..9ac3b2c0e --- /dev/null +++ b/samples/client/lit/gallery_v0_9/index.html @@ -0,0 +1,41 @@ + + + + + + + + A2UI Local Gallery (Minimal v0.9) + + + + + + + + + + + diff --git a/samples/client/lit/gallery_v0_9/package-lock.json b/samples/client/lit/gallery_v0_9/package-lock.json new file mode 100644 index 000000000..cc343a2cf --- /dev/null +++ b/samples/client/lit/gallery_v0_9/package-lock.json @@ -0,0 +1,1200 @@ +{ + "name": "@a2ui/local-gallery-v09", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@a2ui/local-gallery-v09", + "version": "0.1.0", + "dependencies": { + "@a2ui/lit": "file:../../../../renderers/lit", + "@a2ui/markdown-it": "file:../../../../renderers/markdown/markdown-it", + "@a2ui/web_core": "file:../../../../renderers/web_core", + "@lit-labs/signals": "^0.1.3", + "@lit/context": "^1.1.4", + "lit": "^3.3.1" + }, + "devDependencies": { + "typescript": "^5.8.3", + "vite": "^7.1.11" + } + }, + "../../../../renderers/lit": { + "name": "@a2ui/lit", + "version": "0.8.2", + "license": "Apache-2.0", + "dependencies": { + "@a2ui/web_core": "file:../web_core", + "@lit-labs/signals": "^0.1.3", + "@lit/context": "^1.1.4", + "lit": "^3.3.1", + "signal-utils": "^0.21.1" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "google-artifactregistry-auth": "^3.5.0", + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + } + }, + "../../../../renderers/markdown/markdown-it": { + "name": "@a2ui/markdown-it", + "version": "0.0.2", + "license": "Apache-2.0", + "dependencies": { + "dompurify": "^3.3.1", + "markdown-it": "^14.1.0" + }, + "devDependencies": { + "@a2ui/web_core": "file:../../web_core", + "@types/dompurify": "^3.0.5", + "@types/jsdom": "^28.0.0", + "@types/markdown-it": "^14.1.2", + "@types/node": "^24.10.1", + "jsdom": "^28.1.0", + "prettier": "^3.4.2", + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + }, + "peerDependencies": { + "@a2ui/web_core": "file:../../web_core" + } + }, + "../../../../renderers/web_core": { + "name": "@a2ui/web_core", + "version": "0.8.6", + "license": "Apache-2.0", + "dependencies": { + "@preact/signals-core": "^1.13.0", + "date-fns": "^4.1.0", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1" + }, + "devDependencies": { + "@types/node": "^24.11.0", + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + } + }, + "node_modules/@a2ui/lit": { + "resolved": "../../../../renderers/lit", + "link": true + }, + "node_modules/@a2ui/markdown-it": { + "resolved": "../../../../renderers/markdown/markdown-it", + "link": true + }, + "node_modules/@a2ui/web_core": { + "resolved": "../../../../renderers/web_core", + "link": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lit-labs/signals": { + "version": "0.1.3", + "integrity": "sha512-P0yWgH5blwVyEwBg+WFspLzeu1i0ypJP1QB0l1Omr9qZLIPsUu0p4Fy2jshOg7oQyha5n163K3GJGeUhQQ682Q==", + "license": "BSD-3-Clause", + "dependencies": { + "lit": "^2.0.0 || ^3.0.0", + "signal-polyfill": "^0.2.0" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/context": { + "version": "1.1.6", + "integrity": "sha512-M26qDE6UkQbZA2mQ3RjJ3Gzd8TxP+/0obMgE5HfkfLhEEyYE3Bui4A5XHiGPjy0MUGAyxB3QgVuw2ciS0kHn6A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.6.2 || ^2.1.0" + } + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lit": { + "version": "3.3.2", + "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/signal-polyfill": { + "version": "0.2.2", + "integrity": "sha512-p63Y4Er5/eMQ9RHg0M0Y64NlsQKpiu6MDdhBXpyywRuWiPywhJTpKJ1iB5K2hJEbFZ0BnDS7ZkJ+0AfTuL37Rg==", + "license": "Apache-2.0" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/samples/client/lit/gallery_v0_9/package.json b/samples/client/lit/gallery_v0_9/package.json new file mode 100644 index 000000000..e012ef8b5 --- /dev/null +++ b/samples/client/lit/gallery_v0_9/package.json @@ -0,0 +1,25 @@ +{ + "name": "@a2ui/local-gallery-v09", + "private": true, + "version": "0.1.0", + "description": "A2UI Local Gallery (Agentless)", + "type": "module", + "scripts": { + "sync-specs": "node scripts/sync-specs.js", + "dev": "npm run sync-specs && vite", + "build": "npm run sync-specs && tsc && vite build", + "serve": "vite preview" + }, + "dependencies": { + "@a2ui/lit": "file:../../../../renderers/lit", + "@a2ui/web_core": "file:../../../../renderers/web_core", + "@a2ui/markdown-it": "file:../../../../renderers/markdown/markdown-it", + "@lit-labs/signals": "^0.1.3", + "@lit/context": "^1.1.4", + "lit": "^3.3.1" + }, + "devDependencies": { + "typescript": "^5.8.3", + "vite": "^7.1.11" + } +} diff --git a/samples/client/lit/gallery_v0_9/scripts/sync-specs.js b/samples/client/lit/gallery_v0_9/scripts/sync-specs.js new file mode 100644 index 000000000..a4fc19e0d --- /dev/null +++ b/samples/client/lit/gallery_v0_9/scripts/sync-specs.js @@ -0,0 +1,52 @@ +/** + * Copyright 2026 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 + * + * http://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. + */ + +/** + * @fileoverview Synchronizes minimal v0.8 examples from the specification folder + * into the local gallery's public assets and generates a manifest index. + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const TARGET_DIR = path.resolve(process.cwd(), 'public/specs/v0_9/minimal/examples'); +const SOURCE_DIR = path.resolve(process.cwd(), '../../../../specification/v0_9/json/catalogs/minimal/examples'); + +console.log(`Syncing specs from ${SOURCE_DIR} to ${TARGET_DIR}...`); + +// Ensure target directory exists +fs.mkdirSync(TARGET_DIR, { recursive: true }); + +// Read source files +const files = fs.readdirSync(SOURCE_DIR).filter(f => f.endsWith('.json')); + +// Copy files +files.forEach(f => { + const sourcePath = path.join(SOURCE_DIR, f); + const targetPath = path.join(TARGET_DIR, f); + fs.copyFileSync(sourcePath, targetPath); + console.log(` Copied ${f}`); +}); + +// Generate index.json (excluding itself) +const indexFiles = fs.readdirSync(TARGET_DIR).filter(f => f.endsWith('.json') && f !== 'index.json'); +fs.writeFileSync(path.join(TARGET_DIR, 'index.json'), JSON.stringify(indexFiles, null, 2)); + +console.log(`Generated manifest for ${indexFiles.length} files.`); diff --git a/samples/client/lit/gallery_v0_9/src/local-gallery.ts b/samples/client/lit/gallery_v0_9/src/local-gallery.ts new file mode 100644 index 000000000..644226b60 --- /dev/null +++ b/samples/client/lit/gallery_v0_9/src/local-gallery.ts @@ -0,0 +1,362 @@ +import { LitElement, html, css, nothing, PropertyValues } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { MessageProcessor } from "@a2ui/web_core/v0_9"; +import { minimalCatalog } from "@a2ui/lit/v0_9"; +// Try avoiding direct deep import if A2uiMessage is not exported at the top level, using any for now as this is just a type for the array of messages +interface DemoItem { + id: string; + title: string; + filename: string; + description: string; + messages: any[]; +} + +@customElement("local-gallery") +export class LocalGallery extends LitElement { + @state() accessor mockLogs: string[] = []; + @state() accessor demoItems: DemoItem[] = []; + @state() accessor activeItemIndex = 0; + @state() accessor processedMessageCount = 0; + @state() accessor currentDataModelText = "{}"; + + private processor = new MessageProcessor([minimalCatalog], (action: any) => { + this.log(`Action dispatched: ${action.surfaceId}`, action); + }); + + private dataModelSubscription?: { unsubscribe: () => void }; + + static styles = [ + css` + :host { + display: flex; + flex-direction: column; + height: 100vh; + width: 100vw; + overflow: hidden; + background: #0f172a; + color: #f1f5f9; + font-family: system-ui, sans-serif; + } + + header { + padding: 16px 24px; + background: rgba(15, 23, 42, 0.8); + border-bottom: 1px solid rgba(148, 163, 184, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; + } + + h1 { margin: 0; font-size: 1.5rem; } + p.subtitle { color: #94a3b8; margin: 4px 0 0 0; font-size: 0.9rem; } + + main { + flex: 1; + display: flex; + overflow: hidden; + } + + .nav-pane { + width: 250px; + background: #1e293b; + border-right: 1px solid rgba(148, 163, 184, 0.1); + display: flex; + flex-direction: column; + overflow-y: auto; + } + + .nav-item { + padding: 16px; + cursor: pointer; + border-bottom: 1px solid rgba(148, 163, 184, 0.05); + transition: background 0.2s; + } + + .nav-item:hover { background: rgba(255, 255, 255, 0.05); } + .nav-item.active { + background: rgba(56, 189, 248, 0.1); + border-left: 4px solid #38bdf8; + } + + .nav-title { margin: 0 0 4px 0; font-size: 0.95rem; font-weight: 500; } + .nav-desc { margin: 0; font-size: 0.8rem; color: #94a3b8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;} + + .gallery-pane { + flex: 1; + display: flex; + flex-direction: column; + background: #0f172a; + overflow: hidden; + } + + .preview-header { + padding: 16px; + background: #1e293b; + border-bottom: 1px solid rgba(148, 163, 184, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + } + + .stepper-controls { + display: flex; + gap: 8px; + align-items: center; + } + + button { + background: #38bdf8; + color: #0f172a; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 600; + cursor: pointer; + } + button:hover { background: #7dd3fc; } + button:disabled { background: #475569; color: #94a3b8; cursor: not-allowed; } + + .preview-content { + flex: 1; + padding: 24px; + overflow-y: auto; + display: flex; + justify-content: center; + } + + .surface-container { + width: 100%; + max-width: 600px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(148, 163, 184, 0.2); + border-radius: 8px; + padding: 24px; + } + + .inspector-pane { + width: 400px; + display: flex; + flex-direction: column; + border-left: 1px solid rgba(148, 163, 184, 0.1); + background: #020617; + } + + .inspector-section { + flex: 1; + display: flex; + flex-direction: column; + border-bottom: 1px solid rgba(148, 163, 184, 0.1); + overflow: hidden; + } + + .inspector-header { + padding: 12px 16px; + background: #1e293b; + font-weight: bold; + font-size: 0.8rem; + text-transform: uppercase; + color: #94a3b8; + } + + .inspector-body { + flex: 1; + overflow-y: auto; + padding: 16px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.8rem; + white-space: pre-wrap; + } + + .log-list { + display: flex; + flex-direction: column-reverse; + gap: 8px; + } + + .log-entry { + padding: 8px; + background: rgba(255, 255, 255, 0.03); + border-radius: 4px; + border-left: 2px solid #38bdf8; + } + ` + ]; + + async connectedCallback() { + super.connectedCallback(); + + this.processor.model.onSurfaceCreated.subscribe((surface: any) => { + surface.onError.subscribe((err: any) => { + this.log(`Error on surface ${surface.id}: ${err.message}`, err); + }); + }); + + await this.loadExamples(); + } + + async loadExamples() { + try { + const indexResp = await fetch('./specs/v0_9/minimal/examples/index.json'); + if (!indexResp.ok) throw new Error(`Could not load manifest (HTTP ${indexResp.status})`); + const filenames = await indexResp.json() as string[]; + + const items: DemoItem[] = []; + for (const filename of filenames) { + try { + const response = await fetch(`./specs/v0_9/minimal/examples/${filename}`); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); + const messages = Array.isArray(data) ? data : (data.messages || []); + + let surfaceId = filename.replace('.json', ''); + const createMsg = messages.find((m: any) => m.createSurface); + if (createMsg) { + surfaceId = createMsg.createSurface.surfaceId; + } else { + messages.unshift({ + version: "v0.9", + createSurface: { + surfaceId, + catalogId: minimalCatalog.id + } + }); + } + + items.push({ + id: surfaceId, + title: filename.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ').replace('.json', ''), + filename: filename, + description: data.description || `Source: ${filename}`, + messages: messages + }); + + } catch (err) { + console.error(`Error loading ${filename}:`, err); + } + } + + this.demoItems = items; + if (items.length > 0) { + this.selectItem(0); + } + } catch (err) { + console.error(`Failed to initiate gallery:`, err); + } + } + + selectItem(index: number) { + this.activeItemIndex = index; + this.resetSurface(); + this.advanceMessages(true); + } + + resetSurface() { + this.processedMessageCount = 0; + this.mockLogs = []; + this.currentDataModelText = "{}"; + + // Clear old surface and subscriptions + if (this.dataModelSubscription) { + this.dataModelSubscription.unsubscribe(); + this.dataModelSubscription = undefined; + } + + const item = this.demoItems[this.activeItemIndex]; + if (item && this.processor.model.getSurface(item.id)) { + this.processor.processMessages([{ version: "v0.9", deleteSurface: { surfaceId: item.id } }]); + } + } + + advanceMessages(all = false) { + const item = this.demoItems[this.activeItemIndex]; + if (!item) return; + + const toProcess = all + ? item.messages.slice(this.processedMessageCount) + : [item.messages[this.processedMessageCount]]; + + if (toProcess.length === 0) return; + + this.processor.processMessages(toProcess); + this.processedMessageCount += toProcess.length; + + // Subscribe to data model on first advance if not already subscribed + if (!this.dataModelSubscription) { + const surface = this.processor.model.getSurface(item.id); + if (surface) { + this.dataModelSubscription = surface.dataModel.subscribe("/", (val) => { + this.currentDataModelText = JSON.stringify(val || {}, null, 2); + }); + } + } + } + + log(msg: string, detail?: any) { + const time = new Date().toLocaleTimeString(); + const entry = detail ? `${msg}\n${JSON.stringify(detail, null, 2)}` : msg; + this.mockLogs = [...this.mockLogs, `[${time}] ${entry}`]; + } + + render() { + const activeItem = this.demoItems[this.activeItemIndex]; + const surface = activeItem ? this.processor.model.getSurface(activeItem.id) : undefined; + const canAdvance = activeItem && this.processedMessageCount < activeItem.messages.length; + + return html` +
+
+

A2UI Local Gallery

+

v0.9 Minimal Catalog

+
+
+
+ + + + + +
+ `; + } +} \ No newline at end of file diff --git a/samples/client/lit/gallery_v0_9/src/main.ts b/samples/client/lit/gallery_v0_9/src/main.ts new file mode 100644 index 000000000..30ea147ed --- /dev/null +++ b/samples/client/lit/gallery_v0_9/src/main.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2026 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 + * + * http://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 './local-gallery.js'; diff --git a/samples/client/lit/gallery_v0_9/src/theme.ts b/samples/client/lit/gallery_v0_9/src/theme.ts new file mode 100644 index 000000000..c7e91cd1d --- /dev/null +++ b/samples/client/lit/gallery_v0_9/src/theme.ts @@ -0,0 +1,456 @@ +/** + * Copyright 2026 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 + * + * http://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 { v0_8 } from "@a2ui/lit"; + +/** Elements */ + +const a = { + "typography-f-sf": true, + "typography-fs-n": true, + "typography-w-500": true, + "layout-as-n": true, + "layout-dis-iflx": true, + "layout-al-c": true, + "typography-td-none": true, + "color-c-p40": true, +}; + +const audio = { + "layout-w-100": true, +}; + +const body = { + "typography-f-s": true, + "typography-fs-n": true, + "typography-w-400": true, + "layout-mt-0": true, + "layout-mb-2": true, + "typography-sz-bm": true, + "color-c-n10": true, +}; + +const button = { + "typography-f-sf": true, + "typography-fs-n": true, + "typography-w-500": true, + "layout-pt-3": true, + "layout-pb-3": true, + "layout-pl-5": true, + "layout-pr-5": true, + "layout-mb-1": true, + "border-br-16": true, + "border-bw-0": true, + "border-c-n70": true, + "border-bs-s": true, + "color-bgc-s30": true, + "behavior-ho-80": true, +}; + +const heading = { + "typography-f-sf": true, + "typography-fs-n": true, + "typography-w-500": true, + "layout-mt-0": true, + "layout-mb-2": true, +}; + +const iframe = { + "behavior-sw-n": true, +}; + +const input = { + "typography-f-sf": true, + "typography-fs-n": true, + "typography-w-400": true, + "layout-pl-4": true, + "layout-pr-4": true, + "layout-pt-2": true, + "layout-pb-2": true, + "border-br-6": true, + "border-bw-1": true, + "color-bc-s70": true, + "border-bs-s": true, + "layout-as-n": true, + "color-c-n10": true, +}; + +const p = { + "typography-f-s": true, + "typography-fs-n": true, + "typography-w-400": true, + "layout-m-0": true, + "typography-sz-bm": true, + "layout-as-n": true, + "color-c-n10": true, +}; + +const orderedList = { + "typography-f-s": true, + "typography-fs-n": true, + "typography-w-400": true, + "layout-m-0": true, + "typography-sz-bm": true, + "layout-as-n": true, + "color-c-n10": true, +}; + +const unorderedList = { + "typography-f-s": true, + "typography-fs-n": true, + "typography-w-400": true, + "layout-m-0": true, + "typography-sz-bm": true, + "layout-as-n": true, + "color-c-n10": true, +}; + +const listItem = { + "typography-f-s": true, + "typography-fs-n": true, + "typography-w-400": true, + "layout-m-0": true, + "typography-sz-bm": true, + "layout-as-n": true, + "color-c-n10": true, +}; + +const pre = { + "typography-f-c": true, + "typography-fs-n": true, + "typography-w-400": true, + "typography-sz-bm": true, + "typography-ws-p": true, + "layout-as-n": true, +}; + +const textarea = { + ...input, + "layout-r-none": true, + "layout-fs-c": true, +}; + +const video = { + "layout-el-cv": true, +}; + +const aLight = v0_8.Styles.merge(a, {}); +const inputLight = v0_8.Styles.merge(input, {}); +const textareaLight = v0_8.Styles.merge(textarea, {}); +const buttonLight = v0_8.Styles.merge(button, {}); +const bodyLight = v0_8.Styles.merge(body, {}); +const pLight = v0_8.Styles.merge(p, {}); +const preLight = v0_8.Styles.merge(pre, {}); +const orderedListLight = v0_8.Styles.merge(orderedList, {}); +const unorderedListLight = v0_8.Styles.merge(unorderedList, {}); +const listItemLight = v0_8.Styles.merge(listItem, {}); + +export const theme: v0_8.Types.Theme = { + additionalStyles: { + Button: { + "--n-35": "var(--n-100)", + "--n-10": "var(--n-0)", + background: + "linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)", + boxShadow: "0 4px 15px rgba(102, 126, 234, 0.4)", + padding: "12px 28px", + textTransform: "uppercase", + }, + Text: { + h1: { + color: "transparent", + background: + "linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)", + "-webkit-background-clip": "text", + "background-clip": "text", + "-webkit-text-fill-color": "transparent", + }, + h2: { + color: "transparent", + background: + "linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)", + "-webkit-background-clip": "text", + "background-clip": "text", + "-webkit-text-fill-color": "transparent", + }, + h3: { + color: "transparent", + background: + "linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)", + "-webkit-background-clip": "text", + "background-clip": "text", + "-webkit-text-fill-color": "transparent", + }, + h4: {}, + h5: {}, + body: {}, + caption: {}, + }, + Card: { + background: + "radial-gradient(circle at top left, light-dark(transparent, rgba(6, 182, 212, 0.15)), transparent 40%), radial-gradient(circle at bottom right, light-dark(transparent, rgba(139, 92, 246, 0.15)), transparent 40%), linear-gradient(135deg, light-dark(rgba(255, 255, 255, 0.7), rgba(30, 41, 59, 0.7)), light-dark(rgba(255, 255, 255, 0.7), rgba(15, 23, 42, 0.8)))", + }, + TextField: { + "--p-0": "light-dark(var(--n-0), #1e293b)", + }, + Modal: { + background: + "linear-gradient(135deg, light-dark(rgba(255, 255, 255, 0.9), rgba(30, 41, 59, 1)), light-dark(rgba(255, 255, 255, 0.95), rgba(15, 23, 42, 1)))", + boxShadow: "0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.2)", + borderRadius: "8px", + padding: "16px", + minWidth: "300px", + maxWidth: "80vw", + display: "flex", + flexDirection: "column", + }, + MultipleChoice: { + "--md-sys-color-secondary-container-high": "#e8def8", + }, + }, + components: { + AudioPlayer: {}, + Button: { + "layout-pt-2": true, + "layout-pb-2": true, + "layout-pl-3": true, + "layout-pr-3": true, + "border-br-12": true, + "border-bw-0": true, + "border-bs-s": true, + "color-bgc-p30": true, + "behavior-ho-70": true, + "typography-w-400": true, + }, + Card: { "border-br-9": true, "layout-p-4": true, "color-bgc-n100": true }, + CheckBox: { + element: { + "layout-m-0": true, + "layout-mr-2": true, + "layout-p-2": true, + "border-br-12": true, + "border-bw-1": true, + "border-bs-s": true, + "color-bgc-p100": true, + "color-bc-p60": true, + "color-c-n30": true, + "color-c-p30": true, + }, + label: { + "color-c-p30": true, + "typography-f-sf": true, + "typography-v-r": true, + "typography-w-400": true, + "layout-flx-1": true, + "typography-sz-ll": true, + }, + container: { + "layout-dsp-iflex": true, + "layout-al-c": true, + }, + }, + Column: { + "layout-g-2": true, + }, + DateTimeInput: { + container: { + "typography-sz-bm": true, + "layout-w-100": true, + "layout-g-2": true, + "layout-dsp-flexhor": true, + "layout-al-c": true, + "typography-ws-nw": true, + }, + label: { + "color-c-p30": true, + "typography-sz-bm": true, + }, + element: { + "layout-pt-2": true, + "layout-pb-2": true, + "layout-pl-3": true, + "layout-pr-3": true, + "border-br-2": true, + "border-bw-1": true, + "border-bs-s": true, + "color-bgc-p100": true, + "color-bc-p60": true, + "color-c-n30": true, + "color-c-p30": true, + }, + }, + Divider: {}, + Image: { + all: { + "border-br-5": true, + "layout-el-cv": true, + "layout-w-100": true, + "layout-h-100": true, + }, + avatar: { "is-avatar": true }, + header: {}, + icon: {}, + largeFeature: {}, + mediumFeature: {}, + smallFeature: {}, + }, + Icon: {}, + List: { + "layout-g-4": true, + "layout-p-2": true, + }, + Modal: { + backdrop: { "color-bbgc-p60_20": true }, + element: { + "border-br-2": true, + "color-bgc-p100": true, + "layout-p-4": true, + "border-bw-1": true, + "border-bs-s": true, + "color-bc-p80": true, + }, + }, + MultipleChoice: { + container: {}, + label: {}, + element: {}, + }, + Row: { + "layout-g-4": true, + }, + Slider: { + container: {}, + label: {}, + element: {}, + }, + Tabs: { + container: {}, + controls: { all: {}, selected: {} }, + element: {}, + }, + Text: { + all: { + "layout-w-100": true, + "layout-g-2": true, + }, + h1: { + "typography-f-sf": true, + "typography-v-r": true, + "typography-w-400": true, + "layout-m-0": true, + "layout-p-0": true, + "typography-sz-hs": true, + }, + h2: { + "typography-f-sf": true, + "typography-v-r": true, + "typography-w-400": true, + "layout-m-0": true, + "layout-p-0": true, + "typography-sz-tl": true, + }, + h3: { + "typography-f-sf": true, + "typography-v-r": true, + "typography-w-400": true, + "layout-m-0": true, + "layout-p-0": true, + "typography-sz-tl": true, + }, + h4: { + "typography-f-sf": true, + "typography-v-r": true, + "typography-w-400": true, + "layout-m-0": true, + "layout-p-0": true, + "typography-sz-bl": true, + }, + h5: { + "typography-f-sf": true, + "typography-v-r": true, + "typography-w-400": true, + "layout-m-0": true, + "layout-p-0": true, + "typography-sz-bm": true, + }, + body: {}, + caption: {}, + }, + TextField: { + container: { + "typography-sz-bm": true, + "layout-w-100": true, + "layout-g-2": true, + "layout-dsp-flexhor": true, + "layout-al-c": true, + "typography-ws-nw": true, + }, + label: { + "layout-flx-0": true, + "color-c-p30": true, + }, + element: { + "typography-sz-bm": true, + "layout-pt-2": true, + "layout-pb-2": true, + "layout-pl-3": true, + "layout-pr-3": true, + "border-br-2": true, + "border-bw-1": true, + "border-bs-s": true, + "color-bgc-p100": true, + "color-bc-p60": true, + "color-c-n30": true, + "color-c-p30": true, + }, + }, + Video: { + "border-br-5": true, + "layout-el-cv": true, + }, + }, + elements: { + a: aLight, + audio, + body: bodyLight, + button: buttonLight, + h1: heading, + h2: heading, + h3: heading, + h4: heading, + h5: heading, + iframe, + input: inputLight, + p: pLight, + pre: preLight, + textarea: textareaLight, + video, + }, + markdown: { + p: [...Object.keys(pLight)], + h1: [...Object.keys(heading)], + h2: [...Object.keys(heading)], + h3: [...Object.keys(heading)], + h4: [...Object.keys(heading)], + h5: [...Object.keys(heading)], + ul: [...Object.keys(unorderedListLight)], + ol: [...Object.keys(orderedListLight)], + li: [...Object.keys(listItemLight)], + a: [...Object.keys(aLight)], + strong: [], + em: [], + }, +}; diff --git a/samples/client/lit/gallery_v0_9/tsconfig.json b/samples/client/lit/gallery_v0_9/tsconfig.json new file mode 100644 index 000000000..afabdb9d2 --- /dev/null +++ b/samples/client/lit/gallery_v0_9/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "strict": true, + "skipLibCheck": true, + "useDefineForClassFields": false, + "rootDir": ".", + "outDir": "dist", + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true + } +} diff --git a/samples/client/lit/gallery_v0_9/vite.config.ts b/samples/client/lit/gallery_v0_9/vite.config.ts new file mode 100644 index 000000000..57de5443f --- /dev/null +++ b/samples/client/lit/gallery_v0_9/vite.config.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2026 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 + * + * http://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 { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + target: 'esnext', + }, + resolve: { + dedupe: ['lit'], + }, +}); From d7e0e57543521c5ac1066d2233f0091c792df389 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 17 Mar 2026 10:20:37 +1030 Subject: [PATCH 4/4] Implement lit renderer basic catalog --- .../catalogs/basic/components/ChoicePicker.ts | 2 +- renderers/lit/src/v0_9/index.ts | 3 +- .../lit/gallery_v0_9/scripts/sync-specs.js | 45 +++++++++---------- .../lit/gallery_v0_9/src/local-gallery.ts | 42 ++++++++++------- 4 files changed, 52 insertions(+), 40 deletions(-) diff --git a/renderers/lit/src/v0_9/catalogs/basic/components/ChoicePicker.ts b/renderers/lit/src/v0_9/catalogs/basic/components/ChoicePicker.ts index c838e97d4..66fd5baa8 100644 --- a/renderers/lit/src/v0_9/catalogs/basic/components/ChoicePicker.ts +++ b/renderers/lit/src/v0_9/catalogs/basic/components/ChoicePicker.ts @@ -9,7 +9,7 @@ export const A2uiChoicePicker = createLitComponent(ChoicePickerApi, ({ props }) const toggle = (val: string) => { if (!props.setValue) return; if (isMulti) { - if (selected.includes(val)) props.setValue(selected.filter(v => v !== val)); + if (selected.includes(val)) props.setValue(selected.filter((v: string) => v !== val)); else props.setValue([...selected, val]); } else { props.setValue([val]); diff --git a/renderers/lit/src/v0_9/index.ts b/renderers/lit/src/v0_9/index.ts index 6f5103f83..d8f952f0c 100644 --- a/renderers/lit/src/v0_9/index.ts +++ b/renderers/lit/src/v0_9/index.ts @@ -2,4 +2,5 @@ export * from "./types.js"; export * from "./adapter.js"; export * from "./surface/A2uiSurface.js"; export * from "./surface/render-node.js"; -export * from "./catalogs/minimal/index.js"; \ No newline at end of file +export * from "./catalogs/minimal/index.js"; +export * from "./catalogs/basic/index.js"; \ No newline at end of file diff --git a/samples/client/lit/gallery_v0_9/scripts/sync-specs.js b/samples/client/lit/gallery_v0_9/scripts/sync-specs.js index a4fc19e0d..538635385 100644 --- a/samples/client/lit/gallery_v0_9/scripts/sync-specs.js +++ b/samples/client/lit/gallery_v0_9/scripts/sync-specs.js @@ -14,11 +14,6 @@ * limitations under the License. */ -/** - * @fileoverview Synchronizes minimal v0.8 examples from the specification folder - * into the local gallery's public assets and generates a manifest index. - */ - import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -26,27 +21,31 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const TARGET_DIR = path.resolve(process.cwd(), 'public/specs/v0_9/minimal/examples'); -const SOURCE_DIR = path.resolve(process.cwd(), '../../../../specification/v0_9/json/catalogs/minimal/examples'); +function syncDir(sourceDir, targetDir) { + console.log(`Syncing specs from ${sourceDir} to ${targetDir}...`); + fs.mkdirSync(targetDir, { recursive: true }); -console.log(`Syncing specs from ${SOURCE_DIR} to ${TARGET_DIR}...`); + const files = fs.readdirSync(sourceDir).filter(f => f.endsWith('.json')); -// Ensure target directory exists -fs.mkdirSync(TARGET_DIR, { recursive: true }); + files.forEach(f => { + const sourcePath = path.join(sourceDir, f); + const targetPath = path.join(targetDir, f); + fs.copyFileSync(sourcePath, targetPath); + console.log(` Copied ${f}`); + }); -// Read source files -const files = fs.readdirSync(SOURCE_DIR).filter(f => f.endsWith('.json')); + const indexFiles = fs.readdirSync(targetDir).filter(f => f.endsWith('.json') && f !== 'index.json'); + fs.writeFileSync(path.join(targetDir, 'index.json'), JSON.stringify(indexFiles, null, 2)); -// Copy files -files.forEach(f => { - const sourcePath = path.join(SOURCE_DIR, f); - const targetPath = path.join(TARGET_DIR, f); - fs.copyFileSync(sourcePath, targetPath); - console.log(` Copied ${f}`); -}); + console.log(`Generated manifest for ${indexFiles.length} files.`); +} -// Generate index.json (excluding itself) -const indexFiles = fs.readdirSync(TARGET_DIR).filter(f => f.endsWith('.json') && f !== 'index.json'); -fs.writeFileSync(path.join(TARGET_DIR, 'index.json'), JSON.stringify(indexFiles, null, 2)); +syncDir( + path.resolve(process.cwd(), '../../../../specification/v0_9/json/catalogs/minimal/examples'), + path.resolve(process.cwd(), 'public/specs/v0_9/minimal/examples') +); -console.log(`Generated manifest for ${indexFiles.length} files.`); +syncDir( + path.resolve(process.cwd(), '../../../../specification/v0_9/json/catalogs/basic/examples'), + path.resolve(process.cwd(), 'public/specs/v0_9/basic/examples') +); \ No newline at end of file diff --git a/samples/client/lit/gallery_v0_9/src/local-gallery.ts b/samples/client/lit/gallery_v0_9/src/local-gallery.ts index 644226b60..358536190 100644 --- a/samples/client/lit/gallery_v0_9/src/local-gallery.ts +++ b/samples/client/lit/gallery_v0_9/src/local-gallery.ts @@ -1,7 +1,7 @@ import { LitElement, html, css, nothing, PropertyValues } from "lit"; import { customElement, state } from "lit/decorators.js"; import { MessageProcessor } from "@a2ui/web_core/v0_9"; -import { minimalCatalog } from "@a2ui/lit/v0_9"; +import { minimalCatalog, basicCatalog } from "@a2ui/lit/v0_9"; // Try avoiding direct deep import if A2uiMessage is not exported at the top level, using any for now as this is just a type for the array of messages interface DemoItem { id: string; @@ -9,6 +9,7 @@ interface DemoItem { filename: string; description: string; messages: any[]; + isBasic?: boolean; } @customElement("local-gallery") @@ -19,9 +20,10 @@ export class LocalGallery extends LitElement { @state() accessor processedMessageCount = 0; @state() accessor currentDataModelText = "{}"; - private processor = new MessageProcessor([minimalCatalog], (action: any) => { + private processor = new MessageProcessor([minimalCatalog, basicCatalog], (action: any) => { this.log(`Action dispatched: ${action.surfaceId}`, action); }); + private dataModelSubscription?: { unsubscribe: () => void }; @@ -197,14 +199,28 @@ export class LocalGallery extends LitElement { async loadExamples() { try { - const indexResp = await fetch('./specs/v0_9/minimal/examples/index.json'); - if (!indexResp.ok) throw new Error(`Could not load manifest (HTTP ${indexResp.status})`); + const items: DemoItem[] = []; + await this.fetchExamplesFrom('./specs/v0_9/minimal/examples', items, false); + await this.fetchExamplesFrom('./specs/v0_9/basic/examples', items, true); + + this.demoItems = items; + if (items.length > 0) { + this.selectItem(0); + } + } catch (err) { + console.error(`Failed to initiate gallery:`, err); + } + } + + async fetchExamplesFrom(dir: string, items: DemoItem[], isBasic: boolean) { + try { + const indexResp = await fetch(`${dir}/index.json`); + if (!indexResp.ok) throw new Error(`Could not load manifest from ${dir}`); const filenames = await indexResp.json() as string[]; - const items: DemoItem[] = []; for (const filename of filenames) { try { - const response = await fetch(`./specs/v0_9/minimal/examples/${filename}`); + const response = await fetch(`${dir}/${filename}`); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); const messages = Array.isArray(data) ? data : (data.messages || []); @@ -218,7 +234,7 @@ export class LocalGallery extends LitElement { version: "v0.9", createSurface: { surfaceId, - catalogId: minimalCatalog.id + catalogId: isBasic ? basicCatalog.id : minimalCatalog.id } }); } @@ -228,20 +244,16 @@ export class LocalGallery extends LitElement { title: filename.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ').replace('.json', ''), filename: filename, description: data.description || `Source: ${filename}`, - messages: messages + messages: messages, + isBasic }); } catch (err) { console.error(`Error loading ${filename}:`, err); } } - - this.demoItems = items; - if (items.length > 0) { - this.selectItem(0); - } - } catch (err) { - console.error(`Failed to initiate gallery:`, err); + } catch (e) { + console.warn(`Could not load ${dir}`, e); } }