diff --git a/renderers/lit/fixing.md b/renderers/lit/fixing.md
new file mode 100644
index 00000000..dac2f185
--- /dev/null
+++ b/renderers/lit/fixing.md
@@ -0,0 +1,52 @@
+# Debugging Report
+
+## Issue 1: Nothing Renders (Fixed)
+
+**Symptoms:**
+The sample app starts, but the screen is blank. Console logs (added during debugging) showed:
+`[Surface] #renderSurface. Renderer: false, Node: true`
+Followed by:
+`Surface cannot render content: {renderer: false, node: 'root-column'}`
+
+**Root Cause:**
+The `` component requires a `renderer` property (an instance of `LitRenderer`) to convert the abstract component tree into Lit templates. The consumer (the sample shell app) was not providing this property, leaving it `null`.
+
+**Solution:**
+I modified `src/0.8/lit/components/surface.ts` to initialize the `renderer` property with a default instance using the `standardLitCatalogImplementation`. This ensures the Surface can render even if the parent application doesn't explicitly inject a renderer.
+
+```typescript
+// src/0.8/lit/components/surface.ts
+
+// ... imports
+import { standardLitCatalogImplementation } from "../standard_catalog_implementation/standard_catalog_lit.js";
+
+// ... inside class Surface
+ @property({ attribute: false })
+ accessor renderer: LitRenderer | null = new LitRenderer(
+ standardLitCatalogImplementation
+ );
+```
+
+---
+
+## Issue 2: "(no model)" Output (Pending)
+
+**Symptoms:**
+Components that use data bindings (like `Text`, `Image`, etc.) render the string `(no model)` instead of the actual data.
+Logs added to `Text` component confirm:
+`Text component missing dependencies: { processor: false, node: true }`
+
+**Root Cause:**
+It appears that the `LitRenderer` (and specifically component renderers like `litTextRenderer`) creates the Lit components (e.g., ``) but does not pass the `processor` property to them.
+The rendering chain is `Surface` -> `LitRenderer` -> `ComponentRenderer` -> `LitComponent`.
+The `Surface` has the `processor`, but it only calls `renderer.renderNode(node)`. It does not pass the `processor`. Consequently, the leaf components (which inherit from `Root`) have a `null` processor and cannot resolve paths like `text.path`.
+
+**Solution:**
+The robust solution is to use **Lit Context** to provide the `MessageProcessor` from the `Surface` (the provider) to all descendant components (consumers). This avoids needing to thread the `processor` argument through the generic `FrameworkRenderer` interfaces which are not aware of the processor.
+
+**Steps to implement:**
+1. Create a `processorContext` key.
+2. Update `Surface` to provide this context.
+3. Update `Root` (the base class for all A2UI Lit components) to `@consume` this context.
+
+This will automatically make `this.processor` available in all components, resolving the `(no model)` issue.
diff --git a/renderers/lit/package.json b/renderers/lit/package.json
index 6cc360cb..b8e6652a 100644
--- a/renderers/lit/package.json
+++ b/renderers/lit/package.json
@@ -14,8 +14,8 @@
"default": "./dist/src/0.8/core.js"
},
"./ui": {
- "types": "./dist/src/0.8/ui/ui.d.ts",
- "default": "./dist/src/0.8/ui/ui.js"
+ "types": "./dist/src/0.8/lit/components/ui.d.ts",
+ "default": "./dist/src/0.8/lit/components/ui.js"
}
},
"type": "module",
diff --git a/renderers/lit/src/0.8/core.ts b/renderers/lit/src/0.8/core.ts
index 9c16e747..f19fbbfe 100644
--- a/renderers/lit/src/0.8/core.ts
+++ b/renderers/lit/src/0.8/core.ts
@@ -15,21 +15,19 @@
*/
export * as Events from "./events/events.js";
-export * as Types from "./types/types.js";
-export * as Primitives from "./types/primitives.js";
+export * as Types from "./core/types/types.js";
+export * as Primitives from "./core/types/primitives.js";
export * as Styles from "./styles/index.js";
-import * as Guards from "./data/guards.js";
+export { CatalogApi } from "./core/types/types.js";
+export { type ComponentApi } from "./core/types/types.js";
+export { standardCatalogApi } from "./core/standard_catalog_api/standard_catalog.js";
+import * as Guards from "./core/data/guards.js";
-import { create as createSignalA2uiMessageProcessor } from "./data/signal-model-processor.js";
-import { A2uiMessageProcessor } from "./data/model-processor.js";
-import A2UIClientEventMessage from "./schemas/server_to_client_with_standard_catalog.json" with { type: "json" };
+import { create as createSignalA2uiMessageProcessor } from "./core/data/signal-model-processor.js";
+import { A2uiMessageProcessor } from "./core/a2ui_message_processor.js";
export const Data = {
createSignalA2uiMessageProcessor,
A2uiMessageProcessor,
Guards,
};
-
-export const Schemas = {
- A2UIClientEventMessage,
-};
diff --git a/renderers/lit/src/0.8/data/model-processor.ts b/renderers/lit/src/0.8/core/a2ui_message_processor.ts
similarity index 69%
rename from renderers/lit/src/0.8/data/model-processor.ts
rename to renderers/lit/src/0.8/core/a2ui_message_processor.ts
index 3b08f699..aa873a2b 100644
--- a/renderers/lit/src/0.8/data/model-processor.ts
+++ b/renderers/lit/src/0.8/core/a2ui_message_processor.ts
@@ -31,31 +31,15 @@ import {
MessageProcessor,
ValueMap,
DataObject,
-} from "../types/types";
+ CatalogApi,
+ AnyResolvedNode,
+ ComponentArrayReference,
+} from "./types/types.js";
import {
isComponentArrayReference,
isObject,
isPath,
- isResolvedAudioPlayer,
- isResolvedButton,
- isResolvedCard,
- isResolvedCheckbox,
- isResolvedColumn,
- isResolvedDateTimeInput,
- isResolvedDivider,
- isResolvedIcon,
- isResolvedImage,
- isResolvedList,
- isResolvedModal,
- isResolvedMultipleChoice,
- isResolvedRow,
- isResolvedSlider,
- isResolvedTabs,
- isResolvedText,
- isResolvedTextField,
- isResolvedVideo,
- isValueMap,
-} from "./guards.js";
+} from "./data/guards.js";
/**
* Processes and consolidates A2UIProtocolMessage objects into a structured,
@@ -69,21 +53,23 @@ export class A2uiMessageProcessor implements MessageProcessor {
private setCtor: SetConstructor = Set;
private objCtor: ObjectConstructor = Object;
private surfaces: Map;
+ private readonly catalog: CatalogApi;
constructor(
readonly opts: {
- mapCtor: MapConstructor;
- arrayCtor: ArrayConstructor;
- setCtor: SetConstructor;
- objCtor: ObjectConstructor;
- } = { mapCtor: Map, arrayCtor: Array, setCtor: Set, objCtor: Object }
+ mapCtor?: MapConstructor;
+ arrayCtor?: ArrayConstructor;
+ setCtor?: SetConstructor;
+ objCtor?: ObjectConstructor;
+ catalog: CatalogApi;
+ }
) {
- this.arrayCtor = opts.arrayCtor;
- this.mapCtor = opts.mapCtor;
- this.setCtor = opts.setCtor;
- this.objCtor = opts.objCtor;
-
- this.surfaces = new opts.mapCtor();
+ this.arrayCtor = opts.arrayCtor ?? Array;
+ this.mapCtor = opts.mapCtor ?? Map;
+ this.setCtor = opts.setCtor ?? Set;
+ this.objCtor = opts.objCtor ?? Object;
+ this.surfaces = new (this.mapCtor)();
+ this.catalog = opts.catalog;
}
getSurfaces(): ReadonlyMap {
@@ -129,11 +115,11 @@ export class A2uiMessageProcessor implements MessageProcessor {
* own data context.
*/
getData(
- node: AnyComponentNode,
+ node: AnyResolvedNode,
relativePath: string,
surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID
): DataValue | null {
- const surface = this.getOrCreateSurface(surfaceId);
+ const surface = this.surfaces.get(surfaceId);
if (!surface) return null;
let finalPath: string;
@@ -151,7 +137,7 @@ export class A2uiMessageProcessor implements MessageProcessor {
}
setData(
- node: AnyComponentNode | null,
+ node: AnyResolvedNode | null,
relativePath: string,
value: DataValue,
surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID
@@ -161,7 +147,7 @@ export class A2uiMessageProcessor implements MessageProcessor {
return;
}
- const surface = this.getOrCreateSurface(surfaceId);
+ const surface = this.surfaces.get(surfaceId);
if (!surface) return;
let finalPath: string;
@@ -294,9 +280,9 @@ export class A2uiMessageProcessor implements MessageProcessor {
if (segments.length === 0) {
// Root data can either be a Map or an Object. If we receive an Object,
// however, we will normalize it to a proper Map.
- if (value instanceof Map || isObject(value)) {
+ if (this.isMap(value) || isObject(value)) {
// Normalize an Object to a Map.
- if (!(value instanceof Map) && isObject(value)) {
+ if (!this.isMap(value) && isObject(value)) {
value = new this.mapCtor(Object.entries(value));
}
@@ -315,7 +301,7 @@ export class A2uiMessageProcessor implements MessageProcessor {
const segment = segments[i];
let target: DataValue | undefined;
- if (current instanceof Map) {
+ if (this.isMap(current)) {
target = current.get(segment);
} else if (Array.isArray(current) && /^\d+$/.test(segment)) {
target = current[parseInt(segment, 10)];
@@ -327,7 +313,7 @@ export class A2uiMessageProcessor implements MessageProcessor {
target === null
) {
target = new this.mapCtor();
- if (current instanceof this.mapCtor) {
+ if (this.isMap(current)) {
current.set(segment, target);
} else if (Array.isArray(current)) {
current[parseInt(segment, 10)] = target;
@@ -338,7 +324,7 @@ export class A2uiMessageProcessor implements MessageProcessor {
const finalSegment = segments[segments.length - 1];
const storedValue = value;
- if (current instanceof this.mapCtor) {
+ if (this.isMap(current)) {
current.set(finalSegment, storedValue);
} else if (Array.isArray(current) && /^\d+$/.test(finalSegment)) {
current[parseInt(finalSegment, 10)] = storedValue;
@@ -362,6 +348,16 @@ export class A2uiMessageProcessor implements MessageProcessor {
return "/" + segments.filter((s) => s.length > 0).join("/");
}
+ private isMap(value: unknown): value is DataMap {
+ return (
+ value instanceof this.mapCtor ||
+ (isObject(value) &&
+ typeof (value as any).get === "function" &&
+ typeof (value as any).set === "function" &&
+ typeof (value as any).entries === "function")
+ );
+ }
+
private getDataByPath(root: DataMap, path: string): DataValue | null {
const segments = this.normalizePath(path)
.split("/")
@@ -371,7 +367,7 @@ export class A2uiMessageProcessor implements MessageProcessor {
for (const segment of segments) {
if (current === undefined || current === null) return null;
- if (current instanceof Map) {
+ if (this.isMap(current)) {
current = current.get(segment) as DataMap;
} else if (Array.isArray(current) && /^\d+$/.test(segment)) {
current = current[parseInt(segment, 10)];
@@ -497,222 +493,41 @@ export class A2uiMessageProcessor implements MessageProcessor {
const unresolvedProperties =
componentProps[componentType as keyof typeof componentProps];
- // Manually build the resolvedProperties object by resolving each value in
- // the component's properties.
- const resolvedProperties: ResolvedMap = new this.objCtor() as ResolvedMap;
- if (isObject(unresolvedProperties)) {
- for (const [key, value] of Object.entries(unresolvedProperties)) {
- resolvedProperties[key] = this.resolvePropertyValue(
- value,
- surface,
- visited,
- dataContextPath,
- idSuffix
- );
- }
+ const resolver = (value: unknown) => this.resolvePropertyValue(
+ value,
+ surface,
+ visited,
+ dataContextPath,
+ idSuffix
+ );
+
+ const catalogItem = this.catalog.get(componentType);
+ if (!catalogItem) {
+ // Fallback for unknown (custom) components.
+ return new this.objCtor({
+ id: fullId,
+ dataContextPath,
+ weight: componentData.weight ?? "initial",
+ type: componentType,
+ properties: isObject(unresolvedProperties) ? Object.fromEntries(Object.entries(unresolvedProperties).map(([key, value]) => [key, resolver(value)])) : {},
+ }) as AnyComponentNode;
}
+
+ const resolvedProperties = catalogItem.resolveProperties(
+ isObject(unresolvedProperties) ? unresolvedProperties : {},
+ resolver
+ );
+
visited.delete(fullId);
- // Now that we have the resolved properties in place we can go ahead and
- // ensure that they meet expectations in terms of types and so forth,
- // casting them into the specific shape for usage.
- const baseNode = {
+ return new this.objCtor({
id: fullId,
dataContextPath,
weight: componentData.weight ?? "initial",
- };
- switch (componentType) {
- case "Text":
- if (!isResolvedText(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "Text",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "Image":
- if (!isResolvedImage(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "Image",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "Icon":
- if (!isResolvedIcon(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "Icon",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "Video":
- if (!isResolvedVideo(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "Video",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "AudioPlayer":
- if (!isResolvedAudioPlayer(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "AudioPlayer",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "Row":
- if (!isResolvedRow(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
-
- return new this.objCtor({
- ...baseNode,
- type: "Row",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "Column":
- if (!isResolvedColumn(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
-
- return new this.objCtor({
- ...baseNode,
- type: "Column",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "List":
- if (!isResolvedList(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "List",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "Card":
- if (!isResolvedCard(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "Card",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "Tabs":
- if (!isResolvedTabs(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "Tabs",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "Divider":
- if (!isResolvedDivider(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "Divider",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "Modal":
- if (!isResolvedModal(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "Modal",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "Button":
- if (!isResolvedButton(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "Button",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "CheckBox":
- if (!isResolvedCheckbox(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "CheckBox",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "TextField":
- if (!isResolvedTextField(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "TextField",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "DateTimeInput":
- if (!isResolvedDateTimeInput(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "DateTimeInput",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "MultipleChoice":
- if (!isResolvedMultipleChoice(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "MultipleChoice",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- case "Slider":
- if (!isResolvedSlider(resolvedProperties)) {
- throw new Error(`Invalid data; expected ${componentType}`);
- }
- return new this.objCtor({
- ...baseNode,
- type: "Slider",
- properties: resolvedProperties,
- }) as AnyComponentNode;
-
- default:
- // Catch-all for other custom component types.
- return new this.objCtor({
- ...baseNode,
- type: componentType,
- properties: resolvedProperties,
- }) as AnyComponentNode;
- }
+ type: componentType,
+ ...resolvedProperties,
+ }) as AnyComponentNode;
}
/**
@@ -742,7 +557,7 @@ export class A2uiMessageProcessor implements MessageProcessor {
// resolve the list and return an array of nodes.
if (isComponentArrayReference(value)) {
if (value.explicitList) {
- return value.explicitList.map((id) =>
+ return value.explicitList.map((id: string) =>
this.buildNodeRecursive(
id,
surface,
@@ -852,4 +667,4 @@ export class A2uiMessageProcessor implements MessageProcessor {
// 5. Otherwise, it's a primitive value.
return value as ResolvedValue;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/data/guards.ts b/renderers/lit/src/0.8/core/data/guards.ts
new file mode 100644
index 00000000..5a29a98a
--- /dev/null
+++ b/renderers/lit/src/0.8/core/data/guards.ts
@@ -0,0 +1,76 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { StringValue, NumberValue, BooleanValue } from "../types/primitives.js";
+import {
+ AnyComponentNode,
+ ComponentArrayReference,
+ ValueMap,
+} from "../types/types.js";
+
+export function isValueMap(value: unknown): value is ValueMap {
+ return isObject(value) && "key" in value;
+}
+
+export function isPath(key: string, value: unknown): value is string {
+ return key === "path" && typeof value === "string";
+}
+
+export function isObject(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+export function isComponentArrayReference(
+ value: unknown
+): value is ComponentArrayReference {
+ if (!isObject(value)) return false;
+ return "explicitList" in value || "template" in value;
+}
+
+function isStringValue(value: unknown): value is StringValue {
+ return (
+ isObject(value) &&
+ ("path" in value ||
+ ("literal" in value && typeof value.literal === "string") ||
+ "literalString" in value)
+ );
+}
+
+function isNumberValue(value: unknown): value is NumberValue {
+ return (
+ isObject(value) &&
+ ("path" in value ||
+ ("literal" in value && typeof value.literal === "number") ||
+ "literalNumber" in value)
+ );
+}
+
+function isBooleanValue(value: unknown): value is BooleanValue {
+ return (
+ isObject(value) &&
+ ("path" in value ||
+ ("literal" in value && typeof value.literal === "boolean") ||
+ "literalBoolean" in value)
+ );
+}
+
+function isAnyComponentNode(value: unknown): value is AnyComponentNode {
+ if (!isObject(value)) return false;
+ const hasBaseKeys = "id" in value && "type" in value && "properties" in value;
+ if (!hasBaseKeys) return false;
+
+ return true;
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/data/signal-model-processor.ts b/renderers/lit/src/0.8/core/data/signal-model-processor.ts
similarity index 85%
rename from renderers/lit/src/0.8/data/signal-model-processor.ts
rename to renderers/lit/src/0.8/core/data/signal-model-processor.ts
index e821c152..d7e2ab37 100644
--- a/renderers/lit/src/0.8/data/signal-model-processor.ts
+++ b/renderers/lit/src/0.8/core/data/signal-model-processor.ts
@@ -14,18 +14,20 @@
limitations under the License.
*/
-import { A2uiMessageProcessor } from "./model-processor.js";
+import { A2uiMessageProcessor } from "../a2ui_message_processor.js";
+import { CatalogApi } from "../types/types.js";
import { SignalArray } from "signal-utils/array";
import { SignalMap } from "signal-utils/map";
import { SignalObject } from "signal-utils/object";
import { SignalSet } from "signal-utils/set";
-export function create() {
+export function create(catalog: CatalogApi) {
return new A2uiMessageProcessor({
arrayCtor: SignalArray as unknown as ArrayConstructor,
mapCtor: SignalMap as unknown as MapConstructor,
objCtor: SignalObject as unknown as ObjectConstructor,
setCtor: SignalSet as unknown as SetConstructor,
+ catalog,
});
}
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/audio_player.ts b/renderers/lit/src/0.8/core/standard_catalog_api/audio_player.ts
new file mode 100644
index 00000000..467347ee
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/audio_player.ts
@@ -0,0 +1,48 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ BaseResolvedNode,
+} from '../types/types.js';
+import { StringValue } from '../types/primitives.js';
+
+export interface AudioPlayerNode extends BaseResolvedNode<'AudioPlayer'> {
+ properties: {
+ url: StringValue;
+ /**
+ * A label, title, or placeholder text.
+ */
+ description?: StringValue;
+ }
+}
+
+export const audioPlayerApi: ComponentApi<'AudioPlayer', AudioPlayerNode> = {
+ name: 'AudioPlayer',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || typeof unresolved.url !== 'object') {
+ throw new Error('Invalid properties for AudioPlayer: missing url.');
+ }
+
+ return {
+ properties: {
+ url: resolver(unresolved.url) as StringValue,
+ description: resolver(unresolved.description) as StringValue,
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/button.ts b/renderers/lit/src/0.8/core/standard_catalog_api/button.ts
new file mode 100644
index 00000000..c204be0a
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/button.ts
@@ -0,0 +1,46 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ AnyResolvedNode,
+ BaseResolvedNode,
+ Action,
+} from '../types/types.js';
+
+export interface ButtonNode extends BaseResolvedNode<'Button'> {
+ properties: {
+ child: AnyResolvedNode;
+ action: Action;
+ }
+}
+
+export const buttonApi: ComponentApi<'Button', ButtonNode> = {
+ name: 'Button',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || typeof unresolved.child !== 'string' || !unresolved.action) {
+ throw new Error('Invalid properties for Button: missing child or action.');
+ }
+
+ return {
+ properties: {
+ child: resolver(unresolved.child) as AnyResolvedNode,
+ action: unresolved.action as Action,
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/card.ts b/renderers/lit/src/0.8/core/standard_catalog_api/card.ts
new file mode 100644
index 00000000..2cb28dcd
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/card.ts
@@ -0,0 +1,48 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ AnyResolvedNode,
+ BaseResolvedNode,
+} from '../types/types.js';
+
+export interface CardNode extends BaseResolvedNode<'Card'> {
+ properties: {
+ child: AnyResolvedNode;
+ children: AnyResolvedNode[];
+ }
+}
+
+export const cardApi: ComponentApi<'Card', CardNode> = {
+ name: 'Card',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || (typeof unresolved.child !== 'string' && !Array.isArray(unresolved.children))) {
+ throw new Error('Invalid properties for Card: missing child or children.');
+ }
+
+ const child = resolver(unresolved.child) as AnyResolvedNode;
+ const children = Array.isArray(unresolved.children) ? resolver(unresolved.children) as AnyResolvedNode[] : [];
+
+ return {
+ properties: {
+ child,
+ children
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/checkbox.ts b/renderers/lit/src/0.8/core/standard_catalog_api/checkbox.ts
new file mode 100644
index 00000000..17fa931b
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/checkbox.ts
@@ -0,0 +1,45 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ BaseResolvedNode,
+} from '../types/types.js';
+import { StringValue, BooleanValue } from '../types/primitives.js';
+
+export interface CheckboxNode extends BaseResolvedNode<'CheckBox'> {
+ properties: {
+ label: StringValue;
+ value: BooleanValue;
+ }
+}
+
+export const checkboxApi: ComponentApi<'CheckBox', CheckboxNode> = {
+ name: 'CheckBox',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || typeof unresolved.label !== 'object' || typeof unresolved.value !== 'object') {
+ throw new Error('Invalid properties for CheckBox: missing label or value.');
+ }
+
+ return {
+ properties: {
+ label: resolver(unresolved.label) as StringValue,
+ value: resolver(unresolved.value) as BooleanValue,
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/column.ts b/renderers/lit/src/0.8/core/standard_catalog_api/column.ts
new file mode 100644
index 00000000..38f6aeae
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/column.ts
@@ -0,0 +1,53 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ AnyResolvedNode,
+ BaseResolvedNode,
+} from '../types/types.js';
+
+export interface ColumnNode extends BaseResolvedNode<'Column'> {
+ properties: {
+ children: AnyResolvedNode[];
+ distribution?:
+ | "start"
+ | "center"
+ | "end"
+ | "spaceBetween"
+ | "spaceAround"
+ | "spaceEvenly";
+ alignment?: "start" | "center" | "end" | "stretch";
+ }
+}
+
+export const columnApi: ComponentApi<'Column', ColumnNode> = {
+ name: 'Column',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || !unresolved.children) {
+ throw new Error('Invalid properties for Column: missing children.');
+ }
+
+ return {
+ properties: {
+ children: resolver(unresolved.children) as AnyResolvedNode[],
+ distribution: unresolved.distribution as ColumnNode['properties']['distribution'],
+ alignment: unresolved.alignment as ColumnNode['properties']['alignment'],
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/datetime_input.ts b/renderers/lit/src/0.8/core/standard_catalog_api/datetime_input.ts
new file mode 100644
index 00000000..535cfe2d
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/datetime_input.ts
@@ -0,0 +1,49 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ BaseResolvedNode,
+} from '../types/types.js';
+import { StringValue } from '../types/primitives.js';
+
+export interface DateTimeInputNode extends BaseResolvedNode<'DateTimeInput'> {
+ properties: {
+ value: StringValue;
+ enableDate?: boolean;
+ enableTime?: boolean;
+ outputFormat?: string;
+ }
+}
+
+export const dateTimeInputApi: ComponentApi<'DateTimeInput', DateTimeInputNode> = {
+ name: 'DateTimeInput',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || typeof unresolved.value !== 'object') {
+ throw new Error('Invalid properties for DateTimeInput: missing value.');
+ }
+
+ return {
+ properties: {
+ value: resolver(unresolved.value) as StringValue,
+ enableDate: unresolved.enableDate as boolean,
+ enableTime: unresolved.enableTime as boolean,
+ outputFormat: unresolved.outputFormat as string,
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/divider.ts b/renderers/lit/src/0.8/core/standard_catalog_api/divider.ts
new file mode 100644
index 00000000..915569f6
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/divider.ts
@@ -0,0 +1,42 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ BaseResolvedNode,
+} from '../types/types.js';
+
+export interface DividerNode extends BaseResolvedNode<'Divider'> {
+ properties: {
+ axis?: "horizontal" | "vertical";
+ color?: string;
+ thickness?: number;
+ }
+}
+
+export const dividerApi: ComponentApi<'Divider', DividerNode> = {
+ name: 'Divider',
+
+ resolveProperties(unresolved, _resolver) {
+ return {
+ properties: {
+ axis: unresolved.axis as DividerNode['properties']['axis'],
+ color: unresolved.color as DividerNode['properties']['color'],
+ thickness: unresolved.thickness as DividerNode['properties']['thickness'],
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/icon.ts b/renderers/lit/src/0.8/core/standard_catalog_api/icon.ts
new file mode 100644
index 00000000..14798539
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/icon.ts
@@ -0,0 +1,43 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ BaseResolvedNode,
+} from '../types/types.js';
+import { StringValue } from '../types/primitives.js';
+
+export interface IconNode extends BaseResolvedNode<'Icon'> {
+ properties: {
+ name: StringValue;
+ }
+}
+
+export const iconApi: ComponentApi<'Icon', IconNode> = {
+ name: 'Icon',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || typeof unresolved.name !== 'object') {
+ throw new Error('Invalid properties for Icon: missing name.');
+ }
+
+ return {
+ properties: {
+ name: resolver(unresolved.name) as StringValue,
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/image.ts b/renderers/lit/src/0.8/core/standard_catalog_api/image.ts
new file mode 100644
index 00000000..be902d83
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/image.ts
@@ -0,0 +1,47 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ BaseResolvedNode,
+} from '../types/types.js';
+import { StringValue } from '../types/primitives.js';
+
+export interface ImageNode extends BaseResolvedNode<'Image'> {
+ properties: {
+ url: StringValue;
+ usageHint: "icon" | "avatar" | "smallFeature" | "mediumFeature" | "largeFeature" | "header";
+ fit?: "contain" | "cover" | "fill" | "none" | "scale-down";
+ }
+}
+
+export const imageApi: ComponentApi<'Image', ImageNode> = {
+ name: 'Image',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || typeof unresolved.url !== 'object') {
+ throw new Error('Invalid properties for Image: missing url.');
+ }
+
+ return {
+ properties: {
+ url: resolver(unresolved.url) as StringValue,
+ usageHint: unresolved.usageHint as ImageNode['properties']['usageHint'],
+ fit: unresolved.fit as ImageNode['properties']['fit'],
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/list.ts b/renderers/lit/src/0.8/core/standard_catalog_api/list.ts
new file mode 100644
index 00000000..295a2338
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/list.ts
@@ -0,0 +1,47 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ AnyResolvedNode,
+ BaseResolvedNode,
+} from '../types/types.js';
+
+export interface ListNode extends BaseResolvedNode<'List'> {
+ properties: {
+ children: AnyResolvedNode[];
+ direction?: "vertical" | "horizontal";
+ alignment?: "start" | "center" | "end" | "stretch";
+ }
+}
+
+export const listApi: ComponentApi<'List', ListNode> = {
+ name: 'List',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || !unresolved.children) {
+ throw new Error('Invalid properties for List: missing children.');
+ }
+
+ return {
+ properties: {
+ children: resolver(unresolved.children) as AnyResolvedNode[],
+ direction: unresolved.direction as ListNode['properties']['direction'],
+ alignment: unresolved.alignment as ListNode['properties']['alignment'],
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/modal.ts b/renderers/lit/src/0.8/core/standard_catalog_api/modal.ts
new file mode 100644
index 00000000..2cc18eda
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/modal.ts
@@ -0,0 +1,45 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ AnyResolvedNode,
+ BaseResolvedNode,
+} from '../types/types.js';
+
+export interface ModalNode extends BaseResolvedNode<'Modal'> {
+ properties: {
+ entryPointChild: AnyResolvedNode;
+ contentChild: AnyResolvedNode;
+ }
+}
+
+export const modalApi: ComponentApi<'Modal', ModalNode> = {
+ name: 'Modal',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || typeof unresolved.entryPointChild !== 'string' || typeof unresolved.contentChild !== 'string') {
+ throw new Error('Invalid properties for Modal: missing entryPointChild or contentChild.');
+ }
+
+ return {
+ properties: {
+ entryPointChild: resolver(unresolved.entryPointChild) as AnyResolvedNode,
+ contentChild: resolver(unresolved.contentChild) as AnyResolvedNode,
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/multiple_choice.ts b/renderers/lit/src/0.8/core/standard_catalog_api/multiple_choice.ts
new file mode 100644
index 00000000..290bacfc
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/multiple_choice.ts
@@ -0,0 +1,56 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ BaseResolvedNode,
+} from '../types/types.js';
+import { StringValue } from '../types/primitives.js';
+
+export interface MultipleChoiceNode extends BaseResolvedNode<'MultipleChoice'> {
+ properties: {
+ selections: {
+ path?: string;
+ literalArray?: string[];
+ };
+ options?: {
+ label: StringValue;
+ value: string;
+ }[];
+ maxAllowedSelections?: number;
+ }
+}
+
+export const multipleChoiceApi: ComponentApi<'MultipleChoice', MultipleChoiceNode> = {
+ name: 'MultipleChoice',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || typeof unresolved.selections !== 'object' || !Array.isArray(unresolved.options)) {
+ throw new Error('Invalid properties for MultipleChoice: missing selections or options.');
+ }
+
+ return {
+ properties: {
+ selections: resolver(unresolved.selections) as { path?: string; literalArray?: string[] },
+ options: (unresolved.options as { label: StringValue, value: string }[]).map(option => ({
+ label: resolver(option.label) as StringValue,
+ value: option.value,
+ })),
+ maxAllowedSelections: unresolved.maxAllowedSelections as number,
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/row.ts b/renderers/lit/src/0.8/core/standard_catalog_api/row.ts
new file mode 100644
index 00000000..03a7e31e
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/row.ts
@@ -0,0 +1,53 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ AnyResolvedNode,
+ BaseResolvedNode,
+} from '../types/types.js';
+
+export interface RowNode extends BaseResolvedNode<'Row'> {
+ properties: {
+ children: AnyResolvedNode[];
+ distribution?:
+ | "start"
+ | "center"
+ | "end"
+ | "spaceBetween"
+ | "spaceAround"
+ | "spaceEvenly";
+ alignment?: "start" | "center" | "end" | "stretch";
+ }
+}
+
+export const rowApi: ComponentApi<'Row', RowNode> = {
+ name: 'Row',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || !unresolved.children) {
+ throw new Error('Invalid properties for Row: missing children.');
+ }
+
+ return {
+ properties: {
+ children: resolver(unresolved.children) as AnyResolvedNode[],
+ distribution: unresolved.distribution as RowNode['properties']['distribution'],
+ alignment: unresolved.alignment as RowNode['properties']['alignment'],
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/slider.ts b/renderers/lit/src/0.8/core/standard_catalog_api/slider.ts
new file mode 100644
index 00000000..8f67383c
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/slider.ts
@@ -0,0 +1,47 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ BaseResolvedNode,
+} from '../types/types.js';
+import { NumberValue } from '../types/primitives.js';
+
+export interface SliderNode extends BaseResolvedNode<'Slider'> {
+ properties: {
+ value: NumberValue;
+ minValue?: number;
+ maxValue?: number;
+ }
+}
+
+export const sliderApi: ComponentApi<'Slider', SliderNode> = {
+ name: 'Slider',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || typeof unresolved.value !== 'object') {
+ throw new Error('Invalid properties for Slider: missing value.');
+ }
+
+ return {
+ properties: {
+ value: resolver(unresolved.value) as NumberValue,
+ minValue: unresolved.minValue as number,
+ maxValue: unresolved.maxValue as number,
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/standard_catalog.ts b/renderers/lit/src/0.8/core/standard_catalog_api/standard_catalog.ts
new file mode 100644
index 00000000..dc4483cf
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/standard_catalog.ts
@@ -0,0 +1,56 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { CatalogApi } from '../types/types.js';
+import { audioPlayerApi } from './audio_player.js';
+import { buttonApi } from './button.js';
+import { cardApi } from './card.js';
+import { checkboxApi } from './checkbox.js';
+import { columnApi } from './column.js';
+import { dateTimeInputApi } from './datetime_input.js';
+import { dividerApi } from './divider.js';
+import { iconApi } from './icon.js';
+import { imageApi } from './image.js';
+import { listApi } from './list.js';
+import { modalApi } from './modal.js';
+import { multipleChoiceApi } from './multiple_choice.js';
+import { rowApi } from './row.js';
+import { sliderApi } from './slider.js';
+import { tabsApi } from './tabs.js';
+import { textFieldApi } from './text_field.js';
+import { textApi } from './text.js';
+import { videoApi } from './video.js';
+
+export const standardCatalogApi = new CatalogApi([
+ audioPlayerApi,
+ buttonApi,
+ cardApi,
+ checkboxApi,
+ columnApi,
+ dateTimeInputApi,
+ dividerApi,
+ iconApi,
+ imageApi,
+ listApi,
+ modalApi,
+ multipleChoiceApi,
+ rowApi,
+ sliderApi,
+ tabsApi,
+ textFieldApi,
+ textApi,
+ videoApi,
+]);
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/tabs.ts b/renderers/lit/src/0.8/core/standard_catalog_api/tabs.ts
new file mode 100644
index 00000000..6def8324
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/tabs.ts
@@ -0,0 +1,49 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ AnyResolvedNode,
+ BaseResolvedNode,
+} from '../types/types.js';
+import { StringValue } from '../types/primitives.js';
+
+export interface ResolvedTabItem {
+ title: StringValue;
+ child: AnyResolvedNode;
+}
+
+export interface TabsNode extends BaseResolvedNode<'Tabs'> {
+ properties: {
+ tabItems: ResolvedTabItem[];
+ }
+}
+
+export const tabsApi: ComponentApi<'Tabs', TabsNode> = {
+ name: 'Tabs',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || !Array.isArray(unresolved.tabItems)) {
+ throw new Error('Invalid properties for Tabs: missing tabItems.');
+ }
+
+ return {
+ properties: {
+ tabItems: resolver(unresolved.tabItems) as ResolvedTabItem[],
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/text.ts b/renderers/lit/src/0.8/core/standard_catalog_api/text.ts
new file mode 100644
index 00000000..852b6505
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/text.ts
@@ -0,0 +1,45 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ BaseResolvedNode,
+} from '../types/types.js';
+import { StringValue } from '../types/primitives.js';
+
+export interface TextNode extends BaseResolvedNode<'Text'> {
+ properties: {
+ text: StringValue;
+ usageHint: "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body";
+ }
+}
+
+export const textApi: ComponentApi<'Text', TextNode> = {
+ name: 'Text',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || typeof unresolved.text !== 'object') {
+ throw new Error('Invalid properties for Text: missing text.');
+ }
+
+ return {
+ properties: {
+ text: resolver(unresolved.text) as StringValue,
+ usageHint: unresolved.usageHint as TextNode['properties']['usageHint'],
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/text_field.ts b/renderers/lit/src/0.8/core/standard_catalog_api/text_field.ts
new file mode 100644
index 00000000..a64d5c5a
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/text_field.ts
@@ -0,0 +1,49 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ BaseResolvedNode,
+} from '../types/types.js';
+import { StringValue } from '../types/primitives.js';
+
+export interface TextFieldNode extends BaseResolvedNode<'TextField'> {
+ properties: {
+ text?: StringValue;
+ label: StringValue;
+ type?: "shortText" | "number" | "date" | "longText";
+ validationRegexp?: string;
+ }
+}
+
+export const textFieldApi: ComponentApi<'TextField', TextFieldNode> = {
+ name: 'TextField',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || typeof unresolved.label !== 'object') {
+ throw new Error('Invalid properties for TextField: missing label.');
+ }
+
+ return {
+ properties: {
+ label: resolver(unresolved.label) as StringValue,
+ text: resolver(unresolved.text) as StringValue,
+ type: unresolved.type as TextFieldNode['properties']['type'],
+ validationRegexp: unresolved.validationRegexp as string,
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/core/standard_catalog_api/video.ts b/renderers/lit/src/0.8/core/standard_catalog_api/video.ts
new file mode 100644
index 00000000..a7759881
--- /dev/null
+++ b/renderers/lit/src/0.8/core/standard_catalog_api/video.ts
@@ -0,0 +1,43 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import {
+ ComponentApi,
+ BaseResolvedNode,
+} from '../types/types.js';
+import { StringValue } from '../types/primitives.js';
+
+export interface VideoNode extends BaseResolvedNode<'Video'> {
+ properties: {
+ url: StringValue;
+ }
+}
+
+export const videoApi: ComponentApi<'Video', VideoNode> = {
+ name: 'Video',
+
+ resolveProperties(unresolved, resolver) {
+ if (!unresolved || typeof unresolved.url !== 'object') {
+ throw new Error('Invalid properties for Video: missing url.');
+ }
+
+ return {
+ properties: {
+ url: resolver(unresolved.url) as StringValue,
+ }
+ };
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/types/client-event.ts b/renderers/lit/src/0.8/core/types/client-event.ts
similarity index 100%
rename from renderers/lit/src/0.8/types/client-event.ts
rename to renderers/lit/src/0.8/core/types/client-event.ts
diff --git a/renderers/lit/src/0.8/types/colors.ts b/renderers/lit/src/0.8/core/types/colors.ts
similarity index 100%
rename from renderers/lit/src/0.8/types/colors.ts
rename to renderers/lit/src/0.8/core/types/colors.ts
diff --git a/renderers/lit/src/0.8/types/primitives.ts b/renderers/lit/src/0.8/core/types/primitives.ts
similarity index 100%
rename from renderers/lit/src/0.8/types/primitives.ts
rename to renderers/lit/src/0.8/core/types/primitives.ts
diff --git a/renderers/lit/src/0.8/types/types.ts b/renderers/lit/src/0.8/core/types/types.ts
similarity index 61%
rename from renderers/lit/src/0.8/types/types.ts
rename to renderers/lit/src/0.8/core/types/types.ts
index 1e1f6686..80d938dc 100644
--- a/renderers/lit/src/0.8/types/types.ts
+++ b/renderers/lit/src/0.8/core/types/types.ts
@@ -14,27 +14,166 @@
limitations under the License.
*/
+import { StringValue, NumberValue, BooleanValue } from "./primitives.js";
export {
type ClientToServerMessage as A2UIClientEventMessage,
type ClientCapabilitiesDynamic,
+ type UserAction,
} from "./client-event.js";
-export { type Action } from "./components.js";
-
-import {
- AudioPlayer,
- Button,
- Checkbox,
- DateTimeInput,
- Divider,
- Icon,
- Image,
- MultipleChoice,
- Slider,
- Text,
- TextField,
- Video,
-} from "./components";
-import { StringValue } from "./primitives";
+
+// --- Core Action Type ---
+
+export interface Action {
+ /**
+ * A unique name identifying the action (e.g., 'submitForm').
+ */
+ name: string;
+ /**
+ * A key-value map of data bindings to be resolved when the action is triggered.
+ */
+ context?: {
+ key: string;
+ /**
+ * The dynamic value. Define EXACTLY ONE of the nested properties.
+ */
+ value: {
+ /**
+ * A data binding reference to a location in the data model (e.g., '/user/name').
+ */
+ path?: string;
+ /**
+ * A fixed, hardcoded string value.
+ */
+ literalString?: string;
+ literalNumber?: number;
+ literalBoolean?: boolean;
+ };
+ }[];
+}
+
+// --- Core Node Types ---
+
+// Base interface for any resolved component node.
+export interface BaseResolvedNode {
+ id: string;
+ type: TName;
+ weight: number | 'initial';
+ dataContextPath?: string;
+ slotName?: string;
+}
+
+// Union type for any possible resolved node.
+// Now strictly generic to decouple from specific components.
+export type AnyResolvedNode = BaseResolvedNode & {
+ properties: Record;
+};
+
+// Interface for a component's definition.
+export interface ComponentApi<
+ TName extends string,
+ RNode extends BaseResolvedNode
+> {
+ readonly name: TName;
+
+ /**
+ * Resolves the raw properties from the A2UI message into the typed
+ * properties required by the final resolved node. This logic is specific
+ * to each component.
+ * @param unresolvedProperties The raw properties from the message.
+ * @param resolver A callback provided by the A2uiMessageProcessor to recursively
+ * resolve values (e.g., data bindings, child component IDs).
+ * @returns The resolved properties for the node.
+ */
+ resolveProperties(
+ unresolvedProperties: Record,
+ resolver: (value: unknown) => unknown
+ ): Omit>;
+}
+
+export type AnyComponentApi = ComponentApi;
+
+export class CatalogApi {
+ private readonly components: Map;
+
+ constructor(components: AnyComponentApi[]) {
+ this.components = new Map(components.map(c => [c.name, c]));
+ }
+
+ public get(componentName: string): AnyComponentApi | undefined {
+ return this.components.get(componentName);
+ }
+}
+
+/**
+ * @template RNode The specific resolved node type this component can render.
+ * @template RenderOutput The output type of the rendering framework (e.g., TemplateResult for Lit, JSX.Element for React).
+ */
+export interface ComponentRenderer<
+ RNode extends AnyResolvedNode,
+ RenderOutput
+> {
+ readonly componentName: RNode['type'];
+
+ /**
+ * Renders the resolved component node.
+ * @param node The fully resolved, typed component node to render.
+ * @param renderChild A function provided by the framework renderer to
+ * recursively render child nodes. Container components MUST use this.
+ * @returns The framework-specific, renderable output.
+ */
+ render(
+ node: RNode,
+ renderChild: (child: AnyResolvedNode) => RenderOutput | null
+ ): RenderOutput;
+}
+export type AnyComponentRenderer = ComponentRenderer;
+
+export class CatalogImplementation {
+ private readonly renderers: Map>;
+
+ /**
+ * @param catalogApi The API definition for the catalog.
+ * @param renderers A list of framework-specific renderers.
+ */
+ constructor(catalogApi: CatalogApi, renderers: AnyComponentRenderer[]) {
+ this.renderers = new Map(renderers.map(r => [r.componentName, r]));
+
+ for (const api of (catalogApi as any)['components'].values()) {
+ if (!this.renderers.has(api.name)) {
+ throw new Error(`Missing renderer implementation for component: ${api.name}`);
+ }
+ }
+ }
+
+ public getRenderer(componentName: string): AnyComponentRenderer | undefined {
+ return this.renderers.get(componentName);
+ }
+}
+
+export class FrameworkRenderer {
+ protected readonly catalogImplementation: CatalogImplementation;
+
+ constructor(catalogImplementation: CatalogImplementation) {
+ this.catalogImplementation = catalogImplementation;
+ }
+
+ /**
+ * Renders a resolved node from the A2uiMessageProcessor into the final output.
+ * This is the entry point for rendering a component tree.
+ */
+ public renderNode(node: AnyResolvedNode): RenderOutput | null {
+ const renderer = this.catalogImplementation.getRenderer(node.type);
+ if (!renderer) {
+ console.warn(`No renderer found for component type: ${node.type}`);
+ return null;
+ }
+
+ // The `renderChild` function passed to the component renderer is a bound
+ // version of this same `renderNode` method, enabling recursion.
+ return renderer.render(node, this.renderNode.bind(this));
+ }
+}
+
export type MessageProcessor = {
getSurfaces(): ReadonlyMap;
@@ -47,13 +186,13 @@ export type MessageProcessor = {
* own data context.
*/
getData(
- node: AnyComponentNode,
+ node: AnyResolvedNode,
relativePath: string,
surfaceId: string
): DataValue | null;
setData(
- node: AnyComponentNode | null,
+ node: AnyResolvedNode | null,
relativePath: string,
value: DataValue,
surfaceId: string
@@ -194,32 +333,6 @@ export type Theme = {
};
};
-/**
- * Represents a user-initiated action, sent from the client to the server.
- */
-export interface UserAction {
- /**
- * The name of the action, taken from the component's `action.action`
- * property.
- */
- actionName: string;
- /**
- * The `id` of the component that triggered the event.
- */
- sourceComponentId: string;
- /**
- * An ISO 8601 timestamp of when the event occurred.
- */
- timestamp: string;
- /**
- * A JSON object containing the key-value pairs from the component's
- * `action.context`, after resolving all data bindings.
- */
- context?: {
- [k: string]: unknown;
- };
-}
-
/** A recursive type for any valid JSON-like value in the data model. */
export type DataValue =
| string
@@ -307,7 +420,7 @@ export type ResolvedValue =
| number
| boolean
| null
- | AnyComponentNode
+ | AnyResolvedNode
| ResolvedMap
| ResolvedArray;
@@ -317,204 +430,13 @@ export type ResolvedMap = { [key: string]: ResolvedValue };
/** A generic array where each item has been recursively resolved. */
export type ResolvedArray = ResolvedValue[];
-/**
- * A base interface that all component nodes share.
- */
-interface BaseComponentNode {
- id: string;
- weight?: number;
- dataContextPath?: string;
- slotName?: string;
-}
-
-export interface TextNode extends BaseComponentNode {
- type: "Text";
- properties: ResolvedText;
-}
-
-export interface ImageNode extends BaseComponentNode {
- type: "Image";
- properties: ResolvedImage;
-}
-
-export interface IconNode extends BaseComponentNode {
- type: "Icon";
- properties: ResolvedIcon;
-}
-
-export interface VideoNode extends BaseComponentNode {
- type: "Video";
- properties: ResolvedVideo;
-}
-
-export interface AudioPlayerNode extends BaseComponentNode {
- type: "AudioPlayer";
- properties: ResolvedAudioPlayer;
-}
-
-export interface RowNode extends BaseComponentNode {
- type: "Row";
- properties: ResolvedRow;
-}
-
-export interface ColumnNode extends BaseComponentNode {
- type: "Column";
- properties: ResolvedColumn;
-}
-
-export interface ListNode extends BaseComponentNode {
- type: "List";
- properties: ResolvedList;
-}
-
-export interface CardNode extends BaseComponentNode {
- type: "Card";
- properties: ResolvedCard;
-}
-
-export interface TabsNode extends BaseComponentNode {
- type: "Tabs";
- properties: ResolvedTabs;
-}
-
-export interface DividerNode extends BaseComponentNode {
- type: "Divider";
- properties: ResolvedDivider;
-}
-
-export interface ModalNode extends BaseComponentNode {
- type: "Modal";
- properties: ResolvedModal;
-}
-
-export interface ButtonNode extends BaseComponentNode {
- type: "Button";
- properties: ResolvedButton;
-}
-
-export interface CheckboxNode extends BaseComponentNode {
- type: "CheckBox";
- properties: ResolvedCheckbox;
-}
-
-export interface TextFieldNode extends BaseComponentNode {
- type: "TextField";
- properties: ResolvedTextField;
-}
-
-export interface DateTimeInputNode extends BaseComponentNode {
- type: "DateTimeInput";
- properties: ResolvedDateTimeInput;
-}
-
-export interface MultipleChoiceNode extends BaseComponentNode {
- type: "MultipleChoice";
- properties: ResolvedMultipleChoice;
-}
-
-export interface SliderNode extends BaseComponentNode {
- type: "Slider";
- properties: ResolvedSlider;
-}
-
-export interface CustomNode extends BaseComponentNode {
+export interface CustomNode extends BaseResolvedNode {
type: string;
// For custom nodes, properties are just a map of string keys to any resolved value.
properties: CustomNodeProperties;
}
-/**
- * The complete discriminated union of all possible resolved component nodes.
- * A renderer would use this type for any given node in the component tree.
- */
-export type AnyComponentNode =
- | TextNode
- | IconNode
- | ImageNode
- | VideoNode
- | AudioPlayerNode
- | RowNode
- | ColumnNode
- | ListNode
- | CardNode
- | TabsNode
- | DividerNode
- | ModalNode
- | ButtonNode
- | CheckboxNode
- | TextFieldNode
- | DateTimeInputNode
- | MultipleChoiceNode
- | SliderNode
- | CustomNode;
-
-// These components do not contain other components can reuse their
-// original interfaces.
-export type ResolvedText = Text;
-export type ResolvedIcon = Icon;
-export type ResolvedImage = Image;
-export type ResolvedVideo = Video;
-export type ResolvedAudioPlayer = AudioPlayer;
-export type ResolvedDivider = Divider;
-export type ResolvedCheckbox = Checkbox;
-export type ResolvedTextField = TextField;
-export type ResolvedDateTimeInput = DateTimeInput;
-export type ResolvedMultipleChoice = MultipleChoice;
-export type ResolvedSlider = Slider;
-
-export interface ResolvedRow {
- children: AnyComponentNode[];
- distribution?:
- | "start"
- | "center"
- | "end"
- | "spaceBetween"
- | "spaceAround"
- | "spaceEvenly";
- alignment?: "start" | "center" | "end" | "stretch";
-}
-
-export interface ResolvedColumn {
- children: AnyComponentNode[];
- distribution?:
- | "start"
- | "center"
- | "end"
- | "spaceBetween"
- | "spaceAround"
- | "spaceEvenly";
- alignment?: "start" | "center" | "end" | "stretch";
-}
-
-export interface ResolvedButton {
- child: AnyComponentNode;
- action: Button["action"];
-}
-
-export interface ResolvedList {
- children: AnyComponentNode[];
- direction?: "vertical" | "horizontal";
- alignment?: "start" | "center" | "end" | "stretch";
-}
-
-export interface ResolvedCard {
- child: AnyComponentNode;
- children: AnyComponentNode[];
-}
-
-export interface ResolvedTabItem {
- title: StringValue;
- child: AnyComponentNode;
-}
-
-export interface ResolvedTabs {
- tabItems: ResolvedTabItem[];
-}
-
-export interface ResolvedModal {
- entryPointChild: AnyComponentNode;
- contentChild: AnyComponentNode;
-}
+export type AnyComponentNode = AnyResolvedNode;
export interface CustomNodeProperties {
[k: string]: ResolvedValue;
@@ -525,8 +447,8 @@ export type SurfaceID = string;
/** The complete state of a single UI surface. */
export interface Surface {
rootComponentId: string | null;
- componentTree: AnyComponentNode | null;
+ componentTree: AnyResolvedNode | null;
dataModel: DataMap;
components: Map;
styles: Record;
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/data/guards.ts b/renderers/lit/src/0.8/data/guards.ts
deleted file mode 100644
index 624e8f5e..00000000
--- a/renderers/lit/src/0.8/data/guards.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- Copyright 2025 Google LLC
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
-
-import { BooleanValue, NumberValue, StringValue } from "../types/primitives";
-import {
- AnyComponentNode,
- ComponentArrayReference,
- ResolvedAudioPlayer,
- ResolvedButton,
- ResolvedCard,
- ResolvedCheckbox,
- ResolvedColumn,
- ResolvedDateTimeInput,
- ResolvedDivider,
- ResolvedIcon,
- ResolvedImage,
- ResolvedList,
- ResolvedModal,
- ResolvedMultipleChoice,
- ResolvedRow,
- ResolvedSlider,
- ResolvedTabItem,
- ResolvedTabs,
- ResolvedText,
- ResolvedTextField,
- ResolvedVideo,
- ValueMap,
-} from "../types/types";
-
-export function isValueMap(value: unknown): value is ValueMap {
- return isObject(value) && "key" in value;
-}
-
-export function isPath(key: string, value: unknown): value is string {
- return key === "path" && typeof value === "string";
-}
-
-export function isObject(value: unknown): value is Record {
- return typeof value === "object" && value !== null && !Array.isArray(value);
-}
-
-export function isComponentArrayReference(
- value: unknown
-): value is ComponentArrayReference {
- if (!isObject(value)) return false;
- return "explicitList" in value || "template" in value;
-}
-
-function isStringValue(value: unknown): value is StringValue {
- return (
- isObject(value) &&
- ("path" in value ||
- ("literal" in value && typeof value.literal === "string") ||
- "literalString" in value)
- );
-}
-
-function isNumberValue(value: unknown): value is NumberValue {
- return (
- isObject(value) &&
- ("path" in value ||
- ("literal" in value && typeof value.literal === "number") ||
- "literalNumber" in value)
- );
-}
-
-function isBooleanValue(value: unknown): value is BooleanValue {
- return (
- isObject(value) &&
- ("path" in value ||
- ("literal" in value && typeof value.literal === "boolean") ||
- "literalBoolean" in value)
- );
-}
-
-function isAnyComponentNode(value: unknown): value is AnyComponentNode {
- if (!isObject(value)) return false;
- const hasBaseKeys = "id" in value && "type" in value && "properties" in value;
- if (!hasBaseKeys) return false;
-
- return true;
-}
-
-export function isResolvedAudioPlayer(
- props: unknown
-): props is ResolvedAudioPlayer {
- return isObject(props) && "url" in props && isStringValue(props.url);
-}
-
-export function isResolvedButton(props: unknown): props is ResolvedButton {
- return (
- isObject(props) &&
- "child" in props &&
- isAnyComponentNode(props.child) &&
- "action" in props
- );
-}
-
-export function isResolvedCard(props: unknown): props is ResolvedCard {
- if (!isObject(props)) return false;
- if (!("child" in props)) {
- if (!("children" in props)) {
- return false;
- } else {
- return (
- Array.isArray(props.children) &&
- props.children.every(isAnyComponentNode)
- );
- }
- }
-
- return isAnyComponentNode(props.child);
-}
-
-export function isResolvedCheckbox(props: unknown): props is ResolvedCheckbox {
- return (
- isObject(props) &&
- "label" in props &&
- isStringValue(props.label) &&
- "value" in props &&
- isBooleanValue(props.value)
- );
-}
-
-export function isResolvedColumn(props: unknown): props is ResolvedColumn {
- return (
- isObject(props) &&
- "children" in props &&
- Array.isArray(props.children) &&
- props.children.every(isAnyComponentNode)
- );
-}
-
-export function isResolvedDateTimeInput(
- props: unknown
-): props is ResolvedDateTimeInput {
- return isObject(props) && "value" in props && isStringValue(props.value);
-}
-
-export function isResolvedDivider(props: unknown): props is ResolvedDivider {
- // Dividers can have all optional properties, so just checking if
- // it's an object is enough.
- return isObject(props);
-}
-
-export function isResolvedImage(props: unknown): props is ResolvedImage {
- return isObject(props) && "url" in props && isStringValue(props.url);
-}
-
-export function isResolvedIcon(props: unknown): props is ResolvedIcon {
- return isObject(props) && "name" in props && isStringValue(props.name);
-}
-
-export function isResolvedList(props: unknown): props is ResolvedList {
- return (
- isObject(props) &&
- "children" in props &&
- Array.isArray(props.children) &&
- props.children.every(isAnyComponentNode)
- );
-}
-
-export function isResolvedModal(props: unknown): props is ResolvedModal {
- return (
- isObject(props) &&
- "entryPointChild" in props &&
- isAnyComponentNode(props.entryPointChild) &&
- "contentChild" in props &&
- isAnyComponentNode(props.contentChild)
- );
-}
-
-export function isResolvedMultipleChoice(
- props: unknown
-): props is ResolvedMultipleChoice {
- return isObject(props) && "selections" in props;
-}
-
-export function isResolvedRow(props: unknown): props is ResolvedRow {
- return (
- isObject(props) &&
- "children" in props &&
- Array.isArray(props.children) &&
- props.children.every(isAnyComponentNode)
- );
-}
-
-export function isResolvedSlider(props: unknown): props is ResolvedSlider {
- return isObject(props) && "value" in props && isNumberValue(props.value);
-}
-
-function isResolvedTabItem(item: unknown): item is ResolvedTabItem {
- return (
- isObject(item) &&
- "title" in item &&
- isStringValue(item.title) &&
- "child" in item &&
- isAnyComponentNode(item.child)
- );
-}
-
-export function isResolvedTabs(props: unknown): props is ResolvedTabs {
- return (
- isObject(props) &&
- "tabItems" in props &&
- Array.isArray(props.tabItems) &&
- props.tabItems.every(isResolvedTabItem)
- );
-}
-
-export function isResolvedText(props: unknown): props is ResolvedText {
- return isObject(props) && "text" in props && isStringValue(props.text);
-}
-
-export function isResolvedTextField(
- props: unknown
-): props is ResolvedTextField {
- return isObject(props) && "label" in props && isStringValue(props.label);
-}
-
-export function isResolvedVideo(props: unknown): props is ResolvedVideo {
- return isObject(props) && "url" in props && isStringValue(props.url);
-}
diff --git a/renderers/lit/src/0.8/events/a2ui.ts b/renderers/lit/src/0.8/events/a2ui.ts
index 88a41ea7..7e094f26 100644
--- a/renderers/lit/src/0.8/events/a2ui.ts
+++ b/renderers/lit/src/0.8/events/a2ui.ts
@@ -14,8 +14,8 @@
limitations under the License.
*/
-import { Action } from "../types/components.js";
-import { AnyComponentNode } from "../types/types.js";
+import { Action } from "../core/types/types.js";
+import { AnyComponentNode } from "../core/types/types.js";
import { BaseEventDetail } from "./base.js";
type Namespace = "a2ui";
diff --git a/renderers/lit/src/0.8/index.ts b/renderers/lit/src/0.8/index.ts
index ab41af4b..71f1e0ed 100644
--- a/renderers/lit/src/0.8/index.ts
+++ b/renderers/lit/src/0.8/index.ts
@@ -15,4 +15,6 @@
*/
export * from "./core.js";
-export * as UI from "./ui/ui.js";
+export * as UI from "./lit/components/ui.js";
+export { LitRenderer } from "./lit/lit_renderer.js";
+export { standardLitCatalogImplementation } from "./lit/standard_catalog_implementation/standard_catalog_lit.js";
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/audio.ts b/renderers/lit/src/0.8/lit/components/audio.ts
similarity index 72%
rename from renderers/lit/src/0.8/ui/audio.ts
rename to renderers/lit/src/0.8/lit/components/audio.ts
index 3465687b..0b5703c0 100644
--- a/renderers/lit/src/0.8/ui/audio.ts
+++ b/renderers/lit/src/0.8/lit/components/audio.ts
@@ -15,18 +15,16 @@
*/
import { html, css, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
-import { StringValue } from "../types/primitives.js";
import { classMap } from "lit/directives/class-map.js";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
+import { AudioPlayerNode } from "../../core/standard_catalog_api/audio_player.js";
@customElement("a2ui-audioplayer")
-export class Audio extends Root {
- @property()
- accessor url: StringValue | null = null;
+export class Audio extends Root {
static styles = [
structuralStyles,
@@ -50,23 +48,24 @@ export class Audio extends Root {
];
#renderAudio() {
- if (!this.url) {
+ const url = this.node.properties.url;
+ if (!url) {
return nothing;
}
- if (this.url && typeof this.url === "object") {
- if ("literalString" in this.url) {
- return html``;
- } else if ("literal" in this.url) {
- return html``;
- } else if (this.url && "path" in this.url && this.url.path) {
- if (!this.processor || !this.component) {
+ if (url && typeof url === "object") {
+ if ("literalString" in url) {
+ return html``;
+ } else if ("literal" in url) {
+ return html``;
+ } else if (url && "path" in url && url.path) {
+ if (!this.processor || !this.node) {
return html`(no processor)`;
}
const audioUrl = this.processor.getData(
- this.component,
- this.url.path,
+ this.node,
+ url.path,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
if (!audioUrl) {
@@ -93,4 +92,4 @@ export class Audio extends Root {
${this.#renderAudio()}
`;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/button.ts b/renderers/lit/src/0.8/lit/components/button.ts
similarity index 79%
rename from renderers/lit/src/0.8/ui/button.ts
rename to renderers/lit/src/0.8/lit/components/button.ts
index b4f728be..dd61083a 100644
--- a/renderers/lit/src/0.8/ui/button.ts
+++ b/renderers/lit/src/0.8/lit/components/button.ts
@@ -15,18 +15,16 @@
*/
import { html, css, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
-import { StateEvent } from "../events/events.js";
+import { StateEvent } from "../../events/events.js";
import { classMap } from "lit/directives/class-map.js";
-import { Action } from "../types/components.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
+import { ButtonNode } from "../../core/standard_catalog_api/button.js";
@customElement("a2ui-button")
-export class Button extends Root {
- @property()
- accessor action: Action | null = null;
+export class Button extends Root {
static styles = [
structuralStyles,
@@ -40,26 +38,27 @@ export class Button extends Root {
];
render() {
+ const { action, child } = this.node.properties;
return html``;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/card.ts b/renderers/lit/src/0.8/lit/components/card.ts
similarity index 85%
rename from renderers/lit/src/0.8/ui/card.ts
rename to renderers/lit/src/0.8/lit/components/card.ts
index ac72d61c..04e1db70 100644
--- a/renderers/lit/src/0.8/ui/card.ts
+++ b/renderers/lit/src/0.8/lit/components/card.ts
@@ -20,9 +20,11 @@ import { Root } from "./root.js";
import { classMap } from "lit/directives/class-map.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
+import { CardNode } from "../../core/standard_catalog_api/card.js";
@customElement("a2ui-card")
-export class Card extends Root {
+export class Card extends Root {
+
static styles = [
structuralStyles,
css`
@@ -52,13 +54,15 @@ export class Card extends Root {
];
render() {
+ const { child, children } = this.node.properties;
return html`
-
+ ${children.map(c => this.renderChild(c))}
+ ${child ? this.renderChild(child) : nothing}
`;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/checkbox.ts b/renderers/lit/src/0.8/lit/components/checkbox.ts
similarity index 71%
rename from renderers/lit/src/0.8/ui/checkbox.ts
rename to renderers/lit/src/0.8/lit/components/checkbox.ts
index 6f858c3c..2e805a8c 100644
--- a/renderers/lit/src/0.8/ui/checkbox.ts
+++ b/renderers/lit/src/0.8/lit/components/checkbox.ts
@@ -15,21 +15,16 @@
*/
import { html, css, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
-import { StringValue, BooleanValue } from "../types/primitives";
import { classMap } from "lit/directives/class-map.js";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
+import { CheckboxNode } from "../../core/standard_catalog_api/checkbox.js";
@customElement("a2ui-checkbox")
-export class Checkbox extends Root {
- @property()
- accessor value: BooleanValue | null = null;
-
- @property()
- accessor label: StringValue | null = null;
+export class Checkbox extends Root {
static styles = [
structuralStyles,
@@ -58,27 +53,29 @@ export class Checkbox extends Root {
];
#setBoundValue(value: string) {
- if (!this.value || !this.processor) {
+ const { value: valueProp } = this.node.properties;
+ if (!valueProp || !this.processor) {
return;
}
- if (!("path" in this.value)) {
+ if (!("path" in valueProp)) {
return;
}
- if (!this.value.path) {
+ if (!valueProp.path) {
return;
}
this.processor.setData(
- this.component,
- this.value.path,
+ this.node,
+ valueProp.path,
value,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
}
#renderField(value: boolean | number) {
+ const { label } = this.node.properties;
return html`
${label?.literalString}
`;
}
render() {
- if (this.value && typeof this.value === "object") {
- if ("literalBoolean" in this.value && this.value.literalBoolean) {
- return this.#renderField(this.value.literalBoolean);
- } else if ("literal" in this.value && this.value.literal !== undefined) {
- return this.#renderField(this.value.literal);
- } else if (this.value && "path" in this.value && this.value.path) {
- if (!this.processor || !this.component) {
+ const { value } = this.node.properties;
+ if (value && typeof value === "object") {
+ if ("literalBoolean" in value && value.literalBoolean !== undefined) {
+ return this.#renderField(value.literalBoolean);
+ } else if ("literal" in value && value.literal !== undefined) {
+ return this.#renderField(value.literal as boolean);
+ } else if (value && "path" in value && value.path) {
+ if (!this.processor || !this.node) {
return html`(no model)`;
}
const textValue = this.processor.getData(
- this.component,
- this.value.path,
+ this.node,
+ value.path,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
@@ -136,4 +134,4 @@ export class Checkbox extends Root {
return nothing;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/column.ts b/renderers/lit/src/0.8/lit/components/column.ts
similarity index 85%
rename from renderers/lit/src/0.8/ui/column.ts
rename to renderers/lit/src/0.8/lit/components/column.ts
index 8f7f34f7..46b800a6 100644
--- a/renderers/lit/src/0.8/ui/column.ts
+++ b/renderers/lit/src/0.8/lit/components/column.ts
@@ -15,20 +15,15 @@
*/
import { html, css, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
import { classMap } from "lit/directives/class-map.js";
-import { ResolvedColumn } from "../types/types";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
+import { ColumnNode } from "../../core/standard_catalog_api/column.js";
@customElement("a2ui-column")
-export class Column extends Root {
- @property({ reflect: true, type: String })
- accessor alignment: ResolvedColumn["alignment"] = "stretch";
-
- @property({ reflect: true, type: String })
- accessor distribution: ResolvedColumn["distribution"] = "start";
+export class Column extends Root {
static styles = [
structuralStyles,
@@ -92,13 +87,14 @@ export class Column extends Root {
];
render() {
+ const { children } = this.node.properties;
return html`
-
+ ${children.map(c => this.renderChild(c))}
`;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/component-registry.ts b/renderers/lit/src/0.8/lit/components/component-registry.ts
similarity index 100%
rename from renderers/lit/src/0.8/ui/component-registry.ts
rename to renderers/lit/src/0.8/lit/components/component-registry.ts
diff --git a/renderers/lit/src/0.8/ui/datetime-input.ts b/renderers/lit/src/0.8/lit/components/datetime-input.ts
similarity index 71%
rename from renderers/lit/src/0.8/ui/datetime-input.ts
rename to renderers/lit/src/0.8/lit/components/datetime-input.ts
index c77acd8d..895195b6 100644
--- a/renderers/lit/src/0.8/ui/datetime-input.ts
+++ b/renderers/lit/src/0.8/lit/components/datetime-input.ts
@@ -15,27 +15,16 @@
*/
import { html, css, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
-import { StringValue } from "../types/primitives.js";
import { classMap } from "lit/directives/class-map.js";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
+import { DateTimeInputNode } from "../../core/standard_catalog_api/datetime_input.js";
@customElement("a2ui-datetimeinput")
-export class DateTimeInput extends Root {
- @property()
- accessor value: StringValue | null = null;
-
- @property()
- accessor label: StringValue | null = null;
-
- @property({ reflect: false, type: Boolean })
- accessor enableDate = true;
-
- @property({ reflect: false, type: Boolean })
- accessor enableTime = true;
+export class DateTimeInput extends Root {
static styles = [
structuralStyles,
@@ -62,21 +51,22 @@ export class DateTimeInput extends Root {
];
#setBoundValue(value: string) {
- if (!this.value || !this.processor) {
+ const { value: valueProp } = this.node.properties;
+ if (!valueProp || !this.processor) {
return;
}
- if (!("path" in this.value)) {
+ if (!("path" in valueProp)) {
return;
}
- if (!this.value.path) {
+ if (!valueProp.path) {
return;
}
this.processor.setData(
- this.component,
- this.value.path,
+ this.node,
+ valueProp.path,
value,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
@@ -114,11 +104,12 @@ export class DateTimeInput extends Root {
}
#getInputType() {
- if (this.enableDate && this.enableTime) {
+ const { enableDate, enableTime } = this.node.properties;
+ if (enableDate && enableTime) {
return "datetime-local";
- } else if (this.enableDate) {
+ } else if (enableDate) {
return "date";
- } else if (this.enableTime) {
+ } else if (enableTime) {
return "time";
}
@@ -139,9 +130,6 @@ export class DateTimeInput extends Root {
const hours = this.#padNumber(date.getHours());
const minutes = this.#padNumber(date.getMinutes());
- // Browsers are picky with what format they allow for the `value` attribute of date/time inputs.
- // We need to parse it out of the provided value. Note that we don't use `toISOString`,
- // because the resulting value is relative to UTC.
if (inputType === "date") {
return `${year}-${month}-${day}`;
} else if (inputType === "time") {
@@ -156,7 +144,6 @@ export class DateTimeInput extends Root {
}
#getPlaceholderText() {
- // TODO: this should likely be passed from the model.
const inputType = this.#getInputType();
if (inputType === "date") {
@@ -169,19 +156,24 @@ export class DateTimeInput extends Root {
}
render() {
- if (this.value && typeof this.value === "object") {
- if ("literalString" in this.value && this.value.literalString) {
- return this.#renderField(this.value.literalString);
- } else if ("literal" in this.value && this.value.literal !== undefined) {
- return this.#renderField(this.value.literal);
- } else if (this.value && "path" in this.value && this.value.path) {
- if (!this.processor || !this.component) {
+ const { value } = this.node.properties;
+ if (value && typeof value === "object") {
+ if ("literalString" in value && value.literalString) {
+ return this.#renderField(value.literalString);
+ } else if ("literal" in value && value.literal !== undefined) {
+ return this.#renderField(value.literal);
+ } else if (value && "path" in value && value.path) {
+ if (!this.processor || !this.node) {
+ console.warn("DateTimeInput component missing dependencies:", {
+ processor: !!this.processor,
+ node: !!this.node,
+ });
return html`(no model)`;
}
const textValue = this.processor.getData(
- this.component,
- this.value.path,
+ this.node,
+ value.path,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
if (typeof textValue !== "string") {
diff --git a/renderers/lit/src/0.8/ui/divider.ts b/renderers/lit/src/0.8/lit/components/divider.ts
similarity index 88%
rename from renderers/lit/src/0.8/ui/divider.ts
rename to renderers/lit/src/0.8/lit/components/divider.ts
index f4088ee2..3e489a01 100644
--- a/renderers/lit/src/0.8/ui/divider.ts
+++ b/renderers/lit/src/0.8/lit/components/divider.ts
@@ -9,7 +9,7 @@
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.
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
See the License for the specific language governing permissions and
limitations under the License.
*/
@@ -17,12 +17,14 @@
import { html, css, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
-import { styleMap } from "lit/directives/style-map.js";
import { classMap } from "lit/directives/class-map.js";
+import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
+import { DividerNode } from "../../core/standard_catalog_api/divider.js";
@customElement("a2ui-divider")
-export class Divider extends Root {
+export class Divider extends Root {
+
static styles = [
structuralStyles,
css`
@@ -48,4 +50,4 @@ export class Divider extends Root {
: nothing}
/>`;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/icon.ts b/renderers/lit/src/0.8/lit/components/icon.ts
similarity index 71%
rename from renderers/lit/src/0.8/ui/icon.ts
rename to renderers/lit/src/0.8/lit/components/icon.ts
index a160f22d..69f86a68 100644
--- a/renderers/lit/src/0.8/ui/icon.ts
+++ b/renderers/lit/src/0.8/lit/components/icon.ts
@@ -15,18 +15,16 @@
*/
import { html, css, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
-import { StringValue } from "../types/primitives.js";
import { classMap } from "lit/directives/class-map.js";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
+import { IconNode } from "../../core/standard_catalog_api/icon.js";
@customElement("a2ui-icon")
-export class Icon extends Root {
- @property()
- accessor name: StringValue | null = null;
+export class Icon extends Root {
static styles = [
structuralStyles,
@@ -45,7 +43,8 @@ export class Icon extends Root {
];
#renderIcon() {
- if (!this.name) {
+ const { name } = this.node.properties;
+ if (!name) {
return nothing;
}
@@ -54,21 +53,25 @@ export class Icon extends Root {
return html`${url}`;
};
- if (this.name && typeof this.name === "object") {
- if ("literalString" in this.name) {
- const iconName = this.name.literalString ?? "";
+ if (name && typeof name === "object") {
+ if ("literalString" in name) {
+ const iconName = name.literalString ?? "";
return render(iconName);
- } else if ("literal" in this.name) {
- const iconName = this.name.literal ?? "";
+ } else if ("literal" in name) {
+ const iconName = name.literal ?? "";
return render(iconName);
- } else if (this.name && "path" in this.name && this.name.path) {
- if (!this.processor || !this.component) {
+ } else if (name && "path" in name && name.path) {
+ if (!this.processor || !this.node) {
+ console.warn("Icon component missing dependencies:", {
+ processor: !!this.processor,
+ node: !!this.node,
+ });
return html`(no model)`;
}
const iconName = this.processor.getData(
- this.component,
- this.name.path,
+ this.node,
+ name.path,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
if (!iconName) {
@@ -95,4 +98,4 @@ export class Icon extends Root {
${this.#renderIcon()}
`;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/image.ts b/renderers/lit/src/0.8/lit/components/image.ts
similarity index 66%
rename from renderers/lit/src/0.8/ui/image.ts
rename to renderers/lit/src/0.8/lit/components/image.ts
index f13b1f4a..d532702c 100644
--- a/renderers/lit/src/0.8/ui/image.ts
+++ b/renderers/lit/src/0.8/lit/components/image.ts
@@ -15,26 +15,17 @@
*/
import { html, css, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
-import { StringValue } from "../types/primitives.js";
import { classMap } from "lit/directives/class-map.js";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
-import { ResolvedImage } from "../types/types.js";
-import { Styles } from "../index.js";
+import { Styles } from "../../core.js";
+import { ImageNode } from "../../core/standard_catalog_api/image.js";
@customElement("a2ui-image")
-export class Image extends Root {
- @property()
- accessor url: StringValue | null = null;
-
- @property()
- accessor usageHint: ResolvedImage["usageHint"] | null = null;
-
- @property()
- accessor fit: "contain" | "cover" | "fill" | "none" | "scale-down" | null = null;
+export class Image extends Root {
static styles = [
structuralStyles,
@@ -60,7 +51,8 @@ export class Image extends Root {
];
#renderImage() {
- if (!this.url) {
+ const { url } = this.node.properties;
+ if (!url) {
return nothing;
}
@@ -68,21 +60,25 @@ export class Image extends Root {
return html``;
};
- if (this.url && typeof this.url === "object") {
- if ("literalString" in this.url) {
- const imageUrl = this.url.literalString ?? "";
+ if (url && typeof url === "object") {
+ if ("literalString" in url) {
+ const imageUrl = url.literalString ?? "";
return render(imageUrl);
- } else if ("literal" in this.url) {
- const imageUrl = this.url.literal ?? "";
+ } else if ("literal" in url) {
+ const imageUrl = url.literal ?? "";
return render(imageUrl);
- } else if (this.url && "path" in this.url && this.url.path) {
- if (!this.processor || !this.component) {
+ } else if (url && "path" in url && url.path) {
+ if (!this.processor || !this.node) {
+ console.warn("Image component missing dependencies:", {
+ processor: !!this.processor,
+ node: !!this.node,
+ });
return html`(no model)`;
}
const imageUrl = this.processor.getData(
- this.component,
- this.url.path,
+ this.node,
+ url.path,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
if (!imageUrl) {
@@ -100,19 +96,20 @@ export class Image extends Root {
}
render() {
+ const { usageHint, fit } = this.node.properties;
const classes = Styles.merge(
this.theme.components.Image.all,
- this.usageHint ? this.theme.components.Image[this.usageHint] : {}
+ usageHint ? this.theme.components.Image[usageHint] : {}
);
return html`
${this.#renderImage()}
`;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/list.ts b/renderers/lit/src/0.8/lit/components/list.ts
similarity index 87%
rename from renderers/lit/src/0.8/ui/list.ts
rename to renderers/lit/src/0.8/lit/components/list.ts
index 7b1f09b7..29f1f4fc 100644
--- a/renderers/lit/src/0.8/ui/list.ts
+++ b/renderers/lit/src/0.8/lit/components/list.ts
@@ -15,16 +15,15 @@
*/
import { html, css, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
import { classMap } from "lit/directives/class-map.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
+import { ListNode } from "../../core/standard_catalog_api/list.js";
@customElement("a2ui-list")
-export class List extends Root {
- @property({ reflect: true, type: String })
- accessor direction: "vertical" | "horizontal" = "vertical";
+export class List extends Root {
static styles = [
structuralStyles,
@@ -66,7 +65,7 @@ export class List extends Root {
? styleMap(this.theme.additionalStyles?.List)
: nothing}
>
-
+ ${this.node.properties.children.map(c => this.renderChild(c))}
`;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/modal.ts b/renderers/lit/src/0.8/lit/components/modal.ts
similarity index 92%
rename from renderers/lit/src/0.8/ui/modal.ts
rename to renderers/lit/src/0.8/lit/components/modal.ts
index 685b5588..5a7de5b1 100644
--- a/renderers/lit/src/0.8/ui/modal.ts
+++ b/renderers/lit/src/0.8/lit/components/modal.ts
@@ -21,9 +21,11 @@ import { classMap } from "lit/directives/class-map.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
import { ref } from "lit/directives/ref.js";
+import { ModalNode } from "../../core/standard_catalog_api/modal.js";
@customElement("a2ui-modal")
-export class Modal extends Root {
+export class Modal extends Root {
+
static styles = [
structuralStyles,
css`
@@ -76,13 +78,14 @@ export class Modal extends Root {
}
render() {
+ const { entryPointChild, contentChild } = this.node.properties;
if (!this.#showModal) {
return html` {
this.#showModal = true;
}}
>
-
+ ${this.renderChild(entryPointChild)}
`;
}
@@ -124,8 +127,8 @@ export class Modal extends Root {
close
-
+ ${this.renderChild(contentChild)}
`;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/multiple-choice.ts b/renderers/lit/src/0.8/lit/components/multiple-choice.ts
similarity index 70%
rename from renderers/lit/src/0.8/ui/multiple-choice.ts
rename to renderers/lit/src/0.8/lit/components/multiple-choice.ts
index a4fba38e..ec8e0464 100644
--- a/renderers/lit/src/0.8/ui/multiple-choice.ts
+++ b/renderers/lit/src/0.8/lit/components/multiple-choice.ts
@@ -15,25 +15,17 @@
*/
import { html, css, PropertyValues, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
-import { StringValue } from "../types/primitives.js";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
import { classMap } from "lit/directives/class-map.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
-import { extractStringValue } from "./utils/utils.js";
+import { extractStringValue } from "../utils/utils.js";
+import { MultipleChoiceNode } from "../../core/standard_catalog_api/multiple_choice.js";
@customElement("a2ui-multiplechoice")
-export class MultipleChoice extends Root {
- @property()
- accessor description: string | null = null;
-
- @property()
- accessor options: { label: StringValue; value: string }[] = [];
-
- @property()
- accessor selections: StringValue | string[] = [];
+export class MultipleChoice extends Root {
static styles = [
structuralStyles,
@@ -59,40 +51,39 @@ export class MultipleChoice extends Root {
];
#setBoundValue(value: string[]) {
- console.log(value);
- if (!this.selections || !this.processor) {
+ const { selections } = this.node.properties;
+ if (!selections || !this.processor) {
return;
}
- if (!("path" in this.selections)) {
+ if (!("path" in selections)) {
return;
}
- if (!this.selections.path) {
+ if (!selections.path) {
return;
}
this.processor.setData(
- this.component,
- this.selections.path,
+ this.node,
+ selections.path,
value,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
}
protected willUpdate(changedProperties: PropertyValues): void {
- const shouldUpdate = changedProperties.has("options");
+ const { selections } = this.node.properties;
+ const shouldUpdate = changedProperties.has("node");
if (!shouldUpdate) {
return;
}
- if (!this.processor || !this.component || Array.isArray(this.selections)) {
+ if (!this.processor || !this.node || Array.isArray(selections)) {
return;
}
- this.selections;
-
const selectionValue = this.processor.getData(
- this.component,
- this.selections.path!,
+ this.node,
+ selections.path!,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
@@ -104,12 +95,13 @@ export class MultipleChoice extends Root {
}
render() {
+ const { options } = this.node.properties;
return html``;
diff --git a/renderers/lit/src/0.8/lit/components/root.ts b/renderers/lit/src/0.8/lit/components/root.ts
new file mode 100644
index 00000000..3190f85d
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/components/root.ts
@@ -0,0 +1,92 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { SignalWatcher } from "@lit-labs/signals";
+import { consume } from "@lit/context";
+import {
+ css,
+ html,
+ LitElement,
+ nothing,
+ TemplateResult,
+} from "lit";
+import { customElement, property } from "lit/decorators.js";
+import {
+ Theme,
+ AnyResolvedNode,
+ SurfaceID,
+ MessageProcessor,
+} from "../../core/types/types.js";
+import { themeContext } from "../context/theme.js";
+import { processorContext } from "../context/processor.js";
+import { surfaceIdContext } from "../context/surfaceId.js";
+import { structuralStyles } from "./styles.js";
+
+// This is the base class all the components will inherit
+@customElement("a2ui-root")
+export class Root extends SignalWatcher(LitElement) {
+ @property({ attribute: false })
+ set node(node: T) {
+ this.#node = node;
+ if (node) {
+ this.style.setProperty("--weight", `${node.weight}`);
+ }
+ }
+
+ get node(): T {
+ return this.#node;
+ }
+
+ #node!: T;
+
+ get id() {
+ return this.node?.id ?? "";
+ }
+
+ @property({ attribute: false })
+ accessor renderChild!: (child: AnyResolvedNode) => TemplateResult | null;
+
+ @consume({ context: surfaceIdContext })
+ @property()
+ accessor surfaceId: SurfaceID | undefined = undefined;
+
+ @consume({ context: themeContext })
+ accessor theme!: Theme;
+
+ @consume({ context: processorContext })
+ @property({ attribute: false })
+ accessor processor: MessageProcessor | undefined = undefined;
+
+ get dataContextPath() {
+ return this.node?.dataContextPath ?? "";
+ }
+
+ static styles = [
+ structuralStyles,
+ css`
+ :host {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ max-height: 80%;
+ }
+ `,
+ ];
+
+ render(): TemplateResult | typeof nothing {
+ return html``;
+ }
+}
diff --git a/renderers/lit/src/0.8/ui/row.ts b/renderers/lit/src/0.8/lit/components/row.ts
similarity index 88%
rename from renderers/lit/src/0.8/ui/row.ts
rename to renderers/lit/src/0.8/lit/components/row.ts
index bdab6284..facf0289 100644
--- a/renderers/lit/src/0.8/ui/row.ts
+++ b/renderers/lit/src/0.8/lit/components/row.ts
@@ -18,17 +18,12 @@ import { html, css, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Root } from "./root.js";
import { classMap } from "lit/directives/class-map.js";
-import { ResolvedRow } from "../types/types";
+import { RowNode } from "../../core/standard_catalog_api/row.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
@customElement("a2ui-row")
-export class Row extends Root {
- @property({ reflect: true, type: String })
- accessor alignment: ResolvedRow["alignment"] = "stretch";
-
- @property({ reflect: true, type: String })
- accessor distribution: ResolvedRow["distribution"] = "start";
+export class Row extends Root {
static styles = [
structuralStyles,
@@ -92,13 +87,14 @@ export class Row extends Root {
];
render() {
+ const { children } = this.node.properties;
return html`
-
+ ${children.map(c => this.renderChild(c))}
`;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/slider.ts b/renderers/lit/src/0.8/lit/components/slider.ts
similarity index 65%
rename from renderers/lit/src/0.8/ui/slider.ts
rename to renderers/lit/src/0.8/lit/components/slider.ts
index 17adcff2..e81871e6 100644
--- a/renderers/lit/src/0.8/ui/slider.ts
+++ b/renderers/lit/src/0.8/lit/components/slider.ts
@@ -15,32 +15,17 @@
*/
import { html, css, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
-import { NumberValue, StringValue } from "../types/primitives";
-import { ResolvedTextField } from "../types/types.js";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
import { classMap } from "lit/directives/class-map.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
-import { extractNumberValue, extractStringValue } from "./utils/utils.js";
+import { SliderNode } from "../../core/standard_catalog_api/slider.js";
+import { extractNumberValue } from "../utils/utils.js";
@customElement("a2ui-slider")
-export class Slider extends Root {
- @property()
- accessor value: NumberValue | null = null;
-
- @property()
- accessor minValue = 0;
-
- @property()
- accessor maxValue = 0;
-
- @property()
- accessor label: StringValue | null = null;
-
- @property()
- accessor inputType: ResolvedTextField["type"] | null = null;
+export class Slider extends Root {
static styles = [
structuralStyles,
@@ -65,21 +50,22 @@ export class Slider extends Root {
];
#setBoundValue(value: string) {
- if (!this.value || !this.processor) {
+ const { value: valueProp } = this.node.properties;
+ if (!valueProp || !this.processor) {
return;
}
- if (!("path" in this.value)) {
+ if (!("path" in valueProp)) {
return;
}
- if (!this.value.path) {
+ if (!valueProp.path) {
return;
}
this.processor.setData(
- this.component,
- this.value.path,
+ this.node,
+ valueProp.path,
value,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
@@ -90,7 +76,7 @@ export class Slider extends Root {
class=${classMap(this.theme.components.Slider.container)}
>
${this.value
+ >${this.node.properties.value
? extractNumberValue(
- this.value,
- this.component,
+ this.node.properties.value,
+ this.node,
this.processor,
this.surfaceId
)
@@ -126,19 +112,20 @@ export class Slider extends Root {
}
render() {
- if (this.value && typeof this.value === "object") {
- if ("literalNumber" in this.value && this.value.literalNumber) {
- return this.#renderField(this.value.literalNumber);
- } else if ("literal" in this.value && this.value.literal !== undefined) {
- return this.#renderField(this.value.literal);
- } else if (this.value && "path" in this.value && this.value.path) {
- if (!this.processor || !this.component) {
+ const { value } = this.node.properties;
+ if (value && typeof value === "object") {
+ if ("literalNumber" in value && value.literalNumber !== undefined) {
+ return this.#renderField(value.literalNumber);
+ } else if ("literal" in value && value.literal !== undefined) {
+ return this.#renderField(value.literal as number);
+ } else if (value && "path" in value && value.path) {
+ if (!this.processor || !this.node) {
return html`(no processor)`;
}
const textValue = this.processor.getData(
- this.component,
- this.value.path,
+ this.node,
+ value.path,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
@@ -156,4 +143,4 @@ export class Slider extends Root {
return nothing;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/styles.ts b/renderers/lit/src/0.8/lit/components/styles.ts
similarity index 88%
rename from renderers/lit/src/0.8/ui/styles.ts
rename to renderers/lit/src/0.8/lit/components/styles.ts
index f5135c12..a79f0883 100644
--- a/renderers/lit/src/0.8/ui/styles.ts
+++ b/renderers/lit/src/0.8/lit/components/styles.ts
@@ -15,6 +15,6 @@
*/
import { unsafeCSS } from "lit";
-import { structuralStyles as unsafeStructuralStyles } from "../styles/index.js";
+import { structuralStyles as unsafeStructuralStyles } from "../../styles/index.js";
export const structuralStyles = unsafeCSS(unsafeStructuralStyles);
diff --git a/renderers/lit/src/0.8/ui/surface.ts b/renderers/lit/src/0.8/lit/components/surface.ts
similarity index 58%
rename from renderers/lit/src/0.8/ui/surface.ts
rename to renderers/lit/src/0.8/lit/components/surface.ts
index f11af658..6be6df1a 100644
--- a/renderers/lit/src/0.8/ui/surface.ts
+++ b/renderers/lit/src/0.8/lit/components/surface.ts
@@ -14,24 +14,33 @@
limitations under the License.
*/
-import { html, css, nothing } from "lit";
+import { html, css, nothing, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
-import { SurfaceID, Surface as SurfaceState } from "../types/types";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
+import { provide } from "@lit/context";
+import {
+ SurfaceID,
+ Surface as SurfaceState,
+} from "../../core/types/types.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
import { Root } from "./root.js";
import { styleMap } from "lit/directives/style-map.js";
+import { processorContext } from "../context/processor.js";
+import { surfaceIdContext } from "../context/surfaceId.js";
+import { LitRenderer } from "../lit_renderer.js";
@customElement("a2ui-surface")
export class Surface extends Root {
+ @provide({ context: surfaceIdContext })
@property()
- accessor surfaceId: SurfaceID | null = null;
+ accessor surfaceId: SurfaceID | undefined = undefined;
- @property()
- accessor surface: SurfaceState | null = null;
-
- @property()
- accessor processor: A2uiMessageProcessor | null = null;
+ @provide({ context: processorContext })
+ @property({ attribute: false })
+ accessor processor: A2uiMessageProcessor | undefined = undefined;
+ @property({ attribute: false })
+ accessor renderer: LitRenderer | null = null;
+
static styles = [
css`
:host {
@@ -58,31 +67,21 @@ export class Surface extends Root {
`,
];
- #renderLogo() {
- if (!this.surface?.styles.logoUrl) {
+ #renderLogo(surface: SurfaceState) {
+ if (!surface.styles.logoUrl) {
return nothing;
}
return html`
-
+
`;
}
- #renderSurface() {
+ #renderSurface(surface: SurfaceState): TemplateResult | typeof nothing {
const styles: Record = {};
- if (this.surface?.styles) {
- for (const [key, value] of Object.entries(this.surface.styles)) {
+ if (surface.styles) {
+ for (const [key, value] of Object.entries(surface.styles)) {
switch (key) {
- // Here we generate a palette from the singular primary color received
- // from the surface data. We will want the values to range from
- // 0 <= x <= 100, where 0 = back, 100 = white, and 50 = the primary
- // color itself. As such we use a color-mix to create the intermediate
- // values.
- //
- // Note: since we use half the range for black to the primary color,
- // and half the range for primary color to white the mixed values have
- // to go up double the amount, i.e., a range from black to primary
- // color needs to fit in 0 -> 50 rather than 0 -> 100.
case "primaryColor": {
styles["--p-100"] = "#ffffff";
styles["--p-99"] = `color-mix(in srgb, ${value} 2%, white 98%)`;
@@ -114,21 +113,38 @@ export class Surface extends Root {
}
}
- return html``;
+ const node = surface.componentTree;
+
+ console.log(`[Surface] #renderSurface. Renderer: ${!!this.renderer}, Node: ${!!node}`);
+ if (!this.renderer || !node) {
+ console.warn("Surface cannot render content:", {
+ renderer: !!this.renderer,
+ node: !!node ? node.id : "null",
+ });
+ return nothing;
+ }
+
+ return html`
${this.renderer.renderNode(node)}
`;
}
render() {
- if (!this.surface) {
+ console.log(`[Surface] render called. SurfaceID: ${this.surfaceId}, Processor: ${!!this.processor}`);
+ if (!this.processor || !this.surfaceId) {
+ console.warn("Surface missing dependencies:", {
+ processor: !!this.processor,
+ surfaceId: this.surfaceId,
+ });
+ return nothing;
+ }
+ const surface = this.processor.getSurfaces().get(this.surfaceId) as
+ | SurfaceState
+ | undefined;
+
+ if (!surface) {
+ console.warn("Surface state not found for id:", this.surfaceId);
return nothing;
}
- return html`${[this.#renderLogo(), this.#renderSurface()]}`;
+ return html`${[this.#renderLogo(surface), this.#renderSurface(surface)]}`;
}
}
diff --git a/renderers/lit/src/0.8/ui/tabs.ts b/renderers/lit/src/0.8/lit/components/tabs.ts
similarity index 88%
rename from renderers/lit/src/0.8/ui/tabs.ts
rename to renderers/lit/src/0.8/lit/components/tabs.ts
index 7485063a..7f79ebc5 100644
--- a/renderers/lit/src/0.8/ui/tabs.ts
+++ b/renderers/lit/src/0.8/lit/components/tabs.ts
@@ -18,17 +18,15 @@ import { html, css, PropertyValues, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Root } from "./root.js";
import { repeat } from "lit/directives/repeat.js";
-import { StringValue } from "../types/primitives.js";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
import { classMap } from "lit/directives/class-map.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
-import { Styles } from "../index.js";
+import { Styles } from "../../core.js";
+import { TabsNode } from "../../core/standard_catalog_api/tabs.js";
@customElement("a2ui-tabs")
-export class Tabs extends Root {
- @property()
- accessor titles: StringValue[] | null = null;
+export class Tabs extends Root {
@property()
accessor selected = 0;
@@ -60,7 +58,8 @@ export class Tabs extends Root {
}
#renderTabs() {
- if (!this.titles) {
+ const { tabItems } = this.node.properties;
+ if (!tabItems) {
return nothing;
}
@@ -68,19 +67,20 @@ export class Tabs extends Root {
id="buttons"
class=${classMap(this.theme.components.Tabs.element)}
>
- ${repeat(this.titles, (title, idx) => {
+ ${repeat(tabItems, (item, idx) => {
let titleString = "";
+ const title = item.title;
if ("literalString" in title && title.literalString) {
titleString = title.literalString;
} else if ("literal" in title && title.literal !== undefined) {
titleString = title.literal;
} else if (title && "path" in title && title.path) {
- if (!this.processor || !this.component) {
+ if (!this.processor || !this.node) {
return html`(no model)`;
}
const textValue = this.processor.getData(
- this.component,
+ this.node,
title.path,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
@@ -129,4 +129,4 @@ export class Tabs extends Root {
${[this.#renderTabs(), this.#renderSlot()]}
`;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/text-field.ts b/renderers/lit/src/0.8/lit/components/text-field.ts
similarity index 75%
rename from renderers/lit/src/0.8/ui/text-field.ts
rename to renderers/lit/src/0.8/lit/components/text-field.ts
index 4d695751..faae5c0f 100644
--- a/renderers/lit/src/0.8/ui/text-field.ts
+++ b/renderers/lit/src/0.8/lit/components/text-field.ts
@@ -15,26 +15,17 @@
*/
import { html, css, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
-import { StringValue } from "../types/primitives.js";
import { classMap } from "lit/directives/class-map.js";
-import { ResolvedTextField } from "../types/types";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
import { styleMap } from "lit/directives/style-map.js";
-import { extractStringValue } from "./utils/utils.js";
+import { extractStringValue } from "../utils/utils.js";
import { structuralStyles } from "./styles.js";
+import { TextFieldNode } from "../../core/standard_catalog_api/text_field.js";
@customElement("a2ui-textfield")
-export class TextField extends Root {
- @property()
- accessor text: StringValue | null = null;
-
- @property()
- accessor label: StringValue | null = null;
-
- @property()
- accessor inputType: ResolvedTextField["type"] | null = null;
+export class TextField extends Root {
static styles = [
structuralStyles,
@@ -61,19 +52,20 @@ export class TextField extends Root {
];
#setBoundValue(value: string) {
- if (!this.text || !this.processor) {
+ const { text } = this.node.properties;
+ if (!text || !this.processor) {
return;
}
- if (!("path" in this.text)) {
+ if (!("path" in text)) {
return;
}
- if (!this.text.path) {
+ if (!text.path) {
return;
}
this.processor.setData(
- this.component,
- this.text.path,
+ this.node,
+ text.path,
value,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
@@ -107,21 +99,21 @@ export class TextField extends Root {
id="data"
.value=${value}
.placeholder=${"Please enter a value"}
- type=${this.inputType === "number" ? "number" : "text"}
+ type=${this.node.properties.type === "number" ? "number" : "text"}
/>
`;
}
render() {
const label = extractStringValue(
- this.label,
- this.component,
+ this.node.properties.label,
+ this.node,
this.processor,
this.surfaceId
);
const value = extractStringValue(
- this.text,
- this.component,
+ this.node.properties.text,
+ this.node,
this.processor,
this.surfaceId
);
diff --git a/renderers/lit/src/0.8/ui/text.ts b/renderers/lit/src/0.8/lit/components/text.ts
similarity index 73%
rename from renderers/lit/src/0.8/ui/text.ts
rename to renderers/lit/src/0.8/lit/components/text.ts
index 02cb32a6..b53aa585 100644
--- a/renderers/lit/src/0.8/ui/text.ts
+++ b/renderers/lit/src/0.8/lit/components/text.ts
@@ -15,16 +15,16 @@
*/
import { html, css, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
-import { markdown } from "./directives/directives.js";
+import { customElement } from "lit/decorators.js";
+import { markdown } from "../directives/directives.js";
import { Root } from "./root.js";
-import { StringValue } from "../types/primitives.js";
import { classMap } from "lit/directives/class-map.js";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
-import { Styles } from "../index.js";
-import { ResolvedText, Theme } from "../types/types.js";
+import { Styles } from "../../core.js";
+import { Theme } from "../../core/types/types.js";
+import { TextNode } from "../../core/standard_catalog_api/text.js";
interface HintedStyles {
h1: Record;
@@ -37,12 +37,7 @@ interface HintedStyles {
}
@customElement("a2ui-text")
-export class Text extends Root {
- @property()
- accessor text: StringValue | null = null;
-
- @property({ reflect: true, attribute: "usage-hint" })
- accessor usageHint: ResolvedText["usageHint"] | null = null;
+export class Text extends Root {
static styles = [
structuralStyles,
@@ -65,20 +60,25 @@ export class Text extends Root {
#renderText() {
let textValue: string | null | undefined = null;
-
- if (this.text && typeof this.text === "object") {
- if ("literalString" in this.text && this.text.literalString) {
- textValue = this.text.literalString;
- } else if ("literal" in this.text && this.text.literal !== undefined) {
- textValue = this.text.literal;
- } else if (this.text && "path" in this.text && this.text.path) {
- if (!this.processor || !this.component) {
+ const { text } = this.node.properties;
+
+ if (text && typeof text === "object") {
+ if ("literalString" in text && text.literalString) {
+ textValue = text.literalString;
+ } else if ("literal" in text && text.literal !== undefined) {
+ textValue = text.literal;
+ } else if (text && "path" in text && text.path) {
+ if (!this.processor || !this.node) {
+ console.warn("Text component missing dependencies:", {
+ processor: !!this.processor,
+ node: !!this.node,
+ });
return html`(no model)`;
}
const value = this.processor.getData(
- this.component,
- this.text.path,
+ this.node,
+ text.path,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
@@ -93,7 +93,7 @@ export class Text extends Root {
}
let markdownText = textValue;
- switch (this.usageHint) {
+ switch (this.node.properties.usageHint) {
case "h1":
markdownText = `# ${markdownText}`;
break;
@@ -137,7 +137,7 @@ export class Text extends Root {
if (!styles) return additionalStyles;
if (this.#areHintedStyles(styles)) {
- const hint = this.usageHint ?? "body";
+ const hint = this.node.properties.usageHint ?? "body";
additionalStyles = styles[hint] as Record;
} else {
additionalStyles = styles;
@@ -147,9 +147,11 @@ export class Text extends Root {
}
render() {
+ console.log(`[TextComponent] render called for node: ${this.node?.id}`);
+ const { usageHint } = this.node.properties;
const classes = Styles.merge(
this.theme.components.Text.all,
- this.usageHint ? this.theme.components.Text[this.usageHint] : {}
+ usageHint ? this.theme.components.Text[usageHint] : {}
);
return html``;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/ui.ts b/renderers/lit/src/0.8/lit/components/ui.ts
similarity index 91%
rename from renderers/lit/src/0.8/ui/ui.ts
rename to renderers/lit/src/0.8/lit/components/ui.ts
index 476d4d56..b718fefa 100644
--- a/renderers/lit/src/0.8/ui/ui.ts
+++ b/renderers/lit/src/0.8/lit/components/ui.ts
@@ -43,10 +43,16 @@ import { TextField } from "./text-field.js";
import { Text } from "./text.js";
import { Video } from "./video.js";
-export * as Context from "./context/theme.js";
-export * as Utils from "./utils/utils.js";
+import { themeContext } from "../context/theme.js";
+import { processorContext } from "../context/processor.js";
+
+export const Context = {
+ themeContext,
+ processorContext,
+};
+export * as Utils from "../utils/utils.js";
export { ComponentRegistry, componentRegistry } from "./component-registry.js";
-export { registerCustomComponents } from "./custom-components/index.js";
+export { registerCustomComponents } from "../custom-components/index.js";
export {
Audio,
@@ -116,4 +122,4 @@ export function instanceOf(tagName: T) {
}
return new ctor();
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/video.ts b/renderers/lit/src/0.8/lit/components/video.ts
similarity index 72%
rename from renderers/lit/src/0.8/ui/video.ts
rename to renderers/lit/src/0.8/lit/components/video.ts
index cf7613e0..3cb7496d 100644
--- a/renderers/lit/src/0.8/ui/video.ts
+++ b/renderers/lit/src/0.8/lit/components/video.ts
@@ -15,18 +15,16 @@
*/
import { html, css, nothing } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
import { Root } from "./root.js";
-import { StringValue } from "../types/primitives.js";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
import { classMap } from "lit/directives/class-map.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
+import { VideoNode } from "../../core/standard_catalog_api/video.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
@customElement("a2ui-video")
-export class Video extends Root {
- @property()
- accessor url: StringValue | null = null;
+export class Video extends Root {
static styles = [
structuralStyles,
@@ -50,23 +48,24 @@ export class Video extends Root {
];
#renderVideo() {
- if (!this.url) {
+ const { url } = this.node.properties;
+ if (!url) {
return nothing;
}
- if (this.url && typeof this.url === "object") {
- if ("literalString" in this.url) {
- return html``;
- } else if ("literal" in this.url) {
- return html``;
- } else if (this.url && "path" in this.url && this.url.path) {
- if (!this.processor || !this.component) {
+ if (url && typeof url === "object") {
+ if ("literalString" in url) {
+ return html``;
+ } else if ("literal" in url) {
+ return html``;
+ } else if (url && "path" in url && url.path) {
+ if (!this.processor || !this.node) {
return html`(no processor)`;
}
const videoUrl = this.processor.getData(
- this.component,
- this.url.path,
+ this.node,
+ url.path,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
if (!videoUrl) {
@@ -93,4 +92,4 @@ export class Video extends Root {
${this.#renderVideo()}
`;
}
-}
+}
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/context/processor.ts b/renderers/lit/src/0.8/lit/context/processor.ts
new file mode 100644
index 00000000..189dab8e
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/context/processor.ts
@@ -0,0 +1,20 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { createContext } from "@lit/context";
+import { MessageProcessor } from "../../core/types/types.js";
+
+export const processorContext = createContext("A2UIProcessor");
diff --git a/renderers/lit/src/0.8/lit/context/surfaceId.ts b/renderers/lit/src/0.8/lit/context/surfaceId.ts
new file mode 100644
index 00000000..126b23c0
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/context/surfaceId.ts
@@ -0,0 +1,20 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { createContext } from "@lit/context";
+import { SurfaceID } from "../../core/types/types.js";
+
+export const surfaceIdContext = createContext("A2UISurfaceID");
diff --git a/renderers/lit/src/0.8/ui/context/theme.ts b/renderers/lit/src/0.8/lit/context/theme.ts
similarity index 92%
rename from renderers/lit/src/0.8/ui/context/theme.ts
rename to renderers/lit/src/0.8/lit/context/theme.ts
index 117db779..242c709e 100644
--- a/renderers/lit/src/0.8/ui/context/theme.ts
+++ b/renderers/lit/src/0.8/lit/context/theme.ts
@@ -15,6 +15,6 @@
*/
import { createContext } from "@lit/context";
-import { type Theme } from "../../types/types.js";
+import { type Theme } from "../../core/types/types.js";
export const themeContext = createContext("A2UITheme");
diff --git a/renderers/lit/src/0.8/ui/custom-components/index.ts b/renderers/lit/src/0.8/lit/custom-components/index.ts
similarity index 90%
rename from renderers/lit/src/0.8/ui/custom-components/index.ts
rename to renderers/lit/src/0.8/lit/custom-components/index.ts
index 64ffcf90..5e97c7d7 100644
--- a/renderers/lit/src/0.8/ui/custom-components/index.ts
+++ b/renderers/lit/src/0.8/lit/custom-components/index.ts
@@ -14,7 +14,7 @@
limitations under the License.
*/
-import { ComponentRegistry } from '../component-registry.js';
+import { ComponentRegistry } from '../components/component-registry.js';
export function registerCustomComponents() {
// No default custom components in the core library.
diff --git a/renderers/lit/src/0.8/ui/directives/directives.ts b/renderers/lit/src/0.8/lit/directives/directives.ts
similarity index 100%
rename from renderers/lit/src/0.8/ui/directives/directives.ts
rename to renderers/lit/src/0.8/lit/directives/directives.ts
diff --git a/renderers/lit/src/0.8/ui/directives/markdown.ts b/renderers/lit/src/0.8/lit/directives/markdown.ts
similarity index 100%
rename from renderers/lit/src/0.8/ui/directives/markdown.ts
rename to renderers/lit/src/0.8/lit/directives/markdown.ts
diff --git a/renderers/lit/src/0.8/ui/directives/sanitizer.ts b/renderers/lit/src/0.8/lit/directives/sanitizer.ts
similarity index 100%
rename from renderers/lit/src/0.8/ui/directives/sanitizer.ts
rename to renderers/lit/src/0.8/lit/directives/sanitizer.ts
diff --git a/renderers/lit/src/0.8/lit/lit_renderer.ts b/renderers/lit/src/0.8/lit/lit_renderer.ts
new file mode 100644
index 00000000..ace5e974
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/lit_renderer.ts
@@ -0,0 +1,56 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { unsafeStatic } from 'lit/static-html.js';
+import {
+ FrameworkRenderer,
+ CatalogImplementation,
+ AnyResolvedNode,
+} from '../core/types/types.js';
+import { componentRegistry } from './components/component-registry.js';
+
+export class LitRenderer extends FrameworkRenderer {
+ constructor(catalogImplementation: CatalogImplementation) {
+ super(catalogImplementation);
+ }
+
+ override renderNode(node: AnyResolvedNode): TemplateResult | null {
+ console.log(
+ `[LitRenderer] renderNode visiting: ${node.type} (id: ${node.id})`
+ );
+
+ // 1. Check componentRegistry for overrides or custom components
+ const constructor = componentRegistry.get(node.type);
+ if (constructor) {
+ const tagName = customElements.getName(constructor);
+ if (tagName) {
+ return html`<${unsafeStatic(tagName)}
+ .node=${node}
+ .renderChild=${this.renderNode.bind(this)}
+ >${unsafeStatic(tagName)}>`;
+ }
+ }
+
+ // 2. Fallback to catalog
+ const renderer = this.catalogImplementation.getRenderer(node.type);
+ if (renderer) {
+ return renderer.render(node, this.renderNode.bind(this));
+ }
+
+ return super.renderNode(node);
+ }
+}
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/audio_player.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/audio_player.ts
new file mode 100644
index 00000000..6c23c464
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/audio_player.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { AudioPlayerNode } from '../../core/standard_catalog_api/audio_player.js';
+import '../components/audio.js';
+
+export const litAudioPlayerRenderer: ComponentRenderer = {
+ componentName: 'AudioPlayer',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/button.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/button.ts
new file mode 100644
index 00000000..c8728dcc
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/button.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { ButtonNode } from '../../core/standard_catalog_api/button.js';
+import '../components/button.js';
+
+export const litButtonRenderer: ComponentRenderer = {
+ componentName: 'Button',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/card.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/card.ts
new file mode 100644
index 00000000..014fd74a
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/card.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { CardNode } from '../../core/standard_catalog_api/card.js';
+import '../components/card.js';
+
+export const litCardRenderer: ComponentRenderer = {
+ componentName: 'Card',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/checkbox.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/checkbox.ts
new file mode 100644
index 00000000..4f281599
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/checkbox.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { CheckboxNode } from '../../core/standard_catalog_api/checkbox.js';
+import '../components/checkbox.js';
+
+export const litCheckboxRenderer: ComponentRenderer = {
+ componentName: 'CheckBox',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/column.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/column.ts
new file mode 100644
index 00000000..2ffe696c
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/column.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { ColumnNode } from '../../core/standard_catalog_api/column.js';
+import '../components/column.js';
+
+export const litColumnRenderer: ComponentRenderer = {
+ componentName: 'Column',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/datetime_input.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/datetime_input.ts
new file mode 100644
index 00000000..b6bb5ae9
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/datetime_input.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { DateTimeInputNode } from '../../core/standard_catalog_api/datetime_input.js';
+import '../components/datetime-input.js';
+
+export const litDateTimeInputRenderer: ComponentRenderer = {
+ componentName: 'DateTimeInput',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/divider.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/divider.ts
new file mode 100644
index 00000000..b1334145
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/divider.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { DividerNode } from '../../core/standard_catalog_api/divider.js';
+import '../components/divider.js';
+
+export const litDividerRenderer: ComponentRenderer = {
+ componentName: 'Divider',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/icon.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/icon.ts
new file mode 100644
index 00000000..1bde9201
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/icon.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { IconNode } from '../../core/standard_catalog_api/icon.js';
+import '../components/icon.js';
+
+export const litIconRenderer: ComponentRenderer = {
+ componentName: 'Icon',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/image.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/image.ts
new file mode 100644
index 00000000..c6485786
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/image.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { ImageNode } from '../../core/standard_catalog_api/image.js';
+import '../components/image.js';
+
+export const litImageRenderer: ComponentRenderer = {
+ componentName: 'Image',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/list.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/list.ts
new file mode 100644
index 00000000..1099149d
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/list.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { ListNode } from '../../core/standard_catalog_api/list.js';
+import '../components/list.js';
+
+export const litListRenderer: ComponentRenderer = {
+ componentName: 'List',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/modal.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/modal.ts
new file mode 100644
index 00000000..082d306b
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/modal.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { ModalNode } from '../../core/standard_catalog_api/modal.js';
+import '../components/modal.js';
+
+export const litModalRenderer: ComponentRenderer = {
+ componentName: 'Modal',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/multiple_choice.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/multiple_choice.ts
new file mode 100644
index 00000000..018c2d19
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/multiple_choice.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { MultipleChoiceNode } from '../../core/standard_catalog_api/multiple_choice.js';
+import '../components/multiple-choice.js';
+
+export const litMultipleChoiceRenderer: ComponentRenderer = {
+ componentName: 'MultipleChoice',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/row.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/row.ts
new file mode 100644
index 00000000..50ecd324
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/row.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { RowNode } from '../../core/standard_catalog_api/row.js';
+import '../components/row.js';
+
+export const litRowRenderer: ComponentRenderer = {
+ componentName: 'Row',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/slider.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/slider.ts
new file mode 100644
index 00000000..310407ed
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/slider.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { SliderNode } from '../../core/standard_catalog_api/slider.js';
+import '../components/slider.js';
+
+export const litSliderRenderer: ComponentRenderer = {
+ componentName: 'Slider',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/standard_catalog_lit.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/standard_catalog_lit.ts
new file mode 100644
index 00000000..11d80af4
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/standard_catalog_lit.ts
@@ -0,0 +1,60 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { CatalogImplementation } from '../../core/types/types.js';
+import { standardCatalogApi } from '../../core/standard_catalog_api/standard_catalog.js';
+import { litAudioPlayerRenderer } from './audio_player.js';
+import { litButtonRenderer } from './button.js';
+import { litCardRenderer } from './card.js';
+import { litCheckboxRenderer } from './checkbox.js';
+import { litColumnRenderer } from './column.js';
+import { litDateTimeInputRenderer } from './datetime_input.js';
+import { litDividerRenderer } from './divider.js';
+import { litIconRenderer } from './icon.js';
+import { litImageRenderer } from './image.js';
+import { litListRenderer } from './list.js';
+import { litModalRenderer } from './modal.js';
+import { litMultipleChoiceRenderer } from './multiple_choice.js';
+import { litRowRenderer } from './row.js';
+import { litSliderRenderer } from './slider.js';
+import { litTabsRenderer } from './tabs.js';
+import { litTextFieldRenderer } from './text_field.js';
+import { litTextRenderer } from './text.js';
+import { litVideoRenderer } from './video.js';
+
+export const standardLitCatalogImplementation = new CatalogImplementation(
+ standardCatalogApi,
+ [
+ litAudioPlayerRenderer,
+ litButtonRenderer,
+ litCardRenderer,
+ litCheckboxRenderer,
+ litColumnRenderer,
+ litDateTimeInputRenderer,
+ litDividerRenderer,
+ litIconRenderer,
+ litImageRenderer,
+ litListRenderer,
+ litModalRenderer,
+ litMultipleChoiceRenderer,
+ litRowRenderer,
+ litSliderRenderer,
+ litTabsRenderer,
+ litTextFieldRenderer,
+ litTextRenderer,
+ litVideoRenderer,
+ ]
+);
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/tabs.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/tabs.ts
new file mode 100644
index 00000000..5acb7c45
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/tabs.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { TabsNode } from '../../core/standard_catalog_api/tabs.js';
+import '../components/tabs.js';
+
+export const litTabsRenderer: ComponentRenderer = {
+ componentName: 'Tabs',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/text.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/text.ts
new file mode 100644
index 00000000..27ff94e6
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/text.ts
@@ -0,0 +1,34 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { TextNode } from '../../core/standard_catalog_api/text.js';
+import '../components/text.js';
+
+export const litTextRenderer: ComponentRenderer = {
+ componentName: 'Text',
+
+ render(node, renderChild) {
+ console.log(`[litTextRenderer] render called for node: ${node.id}`);
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/text_field.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/text_field.ts
new file mode 100644
index 00000000..7ab49171
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/text_field.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { TextFieldNode } from '../../core/standard_catalog_api/text_field.js';
+import '../components/text-field.js';
+
+export const litTextFieldRenderer: ComponentRenderer = {
+ componentName: 'TextField',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/lit/standard_catalog_implementation/video.ts b/renderers/lit/src/0.8/lit/standard_catalog_implementation/video.ts
new file mode 100644
index 00000000..1830e25a
--- /dev/null
+++ b/renderers/lit/src/0.8/lit/standard_catalog_implementation/video.ts
@@ -0,0 +1,33 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import { html, TemplateResult } from 'lit';
+import { ComponentRenderer } from '../../core/types/types.js';
+import { VideoNode } from '../../core/standard_catalog_api/video.js';
+import '../components/video.js';
+
+export const litVideoRenderer: ComponentRenderer = {
+ componentName: 'Video',
+
+ render(node, renderChild) {
+ return html`
+
+ `;
+ },
+};
\ No newline at end of file
diff --git a/renderers/lit/src/0.8/ui/utils/utils.ts b/renderers/lit/src/0.8/lit/utils/utils.ts
similarity index 77%
rename from renderers/lit/src/0.8/ui/utils/utils.ts
rename to renderers/lit/src/0.8/lit/utils/utils.ts
index ca882ad3..461bd26e 100644
--- a/renderers/lit/src/0.8/ui/utils/utils.ts
+++ b/renderers/lit/src/0.8/lit/utils/utils.ts
@@ -14,15 +14,16 @@
limitations under the License.
*/
-import { A2uiMessageProcessor } from "../../data/model-processor.js";
-import { NumberValue, type StringValue } from "../../types/primitives.js";
-import { type AnyComponentNode } from "../../types/types.js";
+import { MessageProcessor } from "../../core/types/types.js";
+import { A2uiMessageProcessor } from "../../core/a2ui_message_processor.js";
+import { NumberValue, type StringValue } from "../../core/types/primitives.js";
+import { type AnyComponentNode } from "../../core/types/types.js";
export function extractStringValue(
- val: StringValue | null,
+ val: StringValue | null | undefined,
component: AnyComponentNode | null,
- processor: A2uiMessageProcessor | null,
- surfaceId: string | null
+ processor: MessageProcessor | null | undefined,
+ surfaceId: string | null | undefined
): string {
if (val !== null && typeof val === "object") {
if ("literalString" in val) {
@@ -31,6 +32,10 @@ export function extractStringValue(
return val.literal ?? "";
} else if (val && "path" in val && val.path) {
if (!processor || !component) {
+ console.warn("extractStringValue missing dependencies:", {
+ processor: !!processor,
+ component: !!component,
+ });
return "(no model)";
}
@@ -54,8 +59,8 @@ export function extractStringValue(
export function extractNumberValue(
val: NumberValue | null,
component: AnyComponentNode | null,
- processor: A2uiMessageProcessor | null,
- surfaceId: string | null
+ processor: MessageProcessor | null | undefined,
+ surfaceId: string | null | undefined
): number {
if (val !== null && typeof val === "object") {
if ("literalNumber" in val) {
diff --git a/renderers/lit/src/0.8/ui/utils/youtube.ts b/renderers/lit/src/0.8/lit/utils/youtube.ts
similarity index 100%
rename from renderers/lit/src/0.8/ui/utils/youtube.ts
rename to renderers/lit/src/0.8/lit/utils/youtube.ts
diff --git a/renderers/lit/src/0.8/model.test.ts b/renderers/lit/src/0.8/model.test.ts
index 892964e9..7b58ee88 100644
--- a/renderers/lit/src/0.8/model.test.ts
+++ b/renderers/lit/src/0.8/model.test.ts
@@ -17,7 +17,8 @@
import assert from "node:assert";
import { describe, it, beforeEach } from "node:test";
import { v0_8 } from "@a2ui/lit";
-import { DataMap, DataValue } from "./types/types";
+import { DataMap, DataValue } from "./core/types/types";
+import { standardCatalogApi } from "./core/standard_catalog_api/standard_catalog.js";
// Helper function to strip reactivity for clean comparisons.
const toPlainObject = (value: unknown): ReturnType => {
@@ -46,10 +47,10 @@ const toPlainObject = (value: unknown): ReturnType => {
};
describe("A2uiMessageProcessor", () => {
- let processor = new v0_8.Data.A2uiMessageProcessor();
+ let processor = new v0_8.Data.A2uiMessageProcessor({ catalog: standardCatalogApi });
beforeEach(() => {
- processor = new v0_8.Data.A2uiMessageProcessor();
+ processor = new v0_8.Data.A2uiMessageProcessor({ catalog: standardCatalogApi });
});
describe("Basic Initialization and State", () => {
diff --git a/renderers/lit/src/0.8/styles/colors.ts b/renderers/lit/src/0.8/styles/colors.ts
index 74334fdd..0af780a3 100644
--- a/renderers/lit/src/0.8/styles/colors.ts
+++ b/renderers/lit/src/0.8/styles/colors.ts
@@ -14,7 +14,7 @@
limitations under the License.
*/
-import { PaletteKey, PaletteKeyVals, shades } from "../types/colors.js";
+import { PaletteKey, PaletteKeyVals, shades } from "../core/types/colors.js";
import { toProp } from "./utils.js";
const color = (src: PaletteKey) =>
diff --git a/renderers/lit/src/0.8/styles/utils.ts b/renderers/lit/src/0.8/styles/utils.ts
index 05003f83..1f5833f2 100644
--- a/renderers/lit/src/0.8/styles/utils.ts
+++ b/renderers/lit/src/0.8/styles/utils.ts
@@ -14,7 +14,7 @@
limitations under the License.
*/
-import { ColorPalettes } from "../types/colors.js";
+import { ColorPalettes } from "../core/types/colors.js";
export function merge(...classes: Array>) {
const styles: Record = {};
diff --git a/renderers/lit/src/0.8/types/components.ts b/renderers/lit/src/0.8/types/components.ts
deleted file mode 100644
index 0e1765f5..00000000
--- a/renderers/lit/src/0.8/types/components.ts
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- Copyright 2025 Google LLC
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
-
-import { StringValue } from "./primitives";
-
-export interface Action {
- /**
- * A unique name identifying the action (e.g., 'submitForm').
- */
- name: string;
- /**
- * A key-value map of data bindings to be resolved when the action is triggered.
- */
- context?: {
- key: string;
- /**
- * The dynamic value. Define EXACTLY ONE of the nested properties.
- */
- value: {
- /**
- * A data binding reference to a location in the data model (e.g., '/user/name').
- */
- path?: string;
- /**
- * A fixed, hardcoded string value.
- */
- literalString?: string;
- literalNumber?: number;
- literalBoolean?: boolean;
- };
- }[];
-}
-
-export interface Text {
- text: StringValue;
- usageHint: "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body";
-}
-
-export interface Image {
- url: StringValue;
- usageHint:
- | "icon"
- | "avatar"
- | "smallFeature"
- | "mediumFeature"
- | "largeFeature"
- | "header";
- fit?: "contain" | "cover" | "fill" | "none" | "scale-down";
-}
-
-export interface Icon {
- name: StringValue;
-}
-
-export interface Video {
- url: StringValue;
-}
-
-export interface AudioPlayer {
- url: StringValue;
- /**
- * A label, title, or placeholder text.
- */
- description?: StringValue;
-}
-
-export interface Tabs {
- /**
- * A list of tabs, each with a title and a child component ID.
- */
- tabItems: {
- /**
- * The title of the tab.
- */
- title: {
- /**
- * A data binding reference to a location in the data model (e.g., '/user/name').
- */
- path?: string;
- /**
- * A fixed, hardcoded string value.
- */
- literalString?: string;
- };
- /**
- * A reference to a component instance by its unique ID.
- */
- child: string;
- }[];
-}
-
-export interface Divider {
- /**
- * The orientation.
- */
- axis?: "horizontal" | "vertical";
- /**
- * The color of the divider (e.g., hex code or semantic name).
- */
- color?: string;
- /**
- * The thickness of the divider.
- */
- thickness?: number;
-}
-
-export interface Modal {
- /**
- * The ID of the component (e.g., a button) that triggers the modal.
- */
- entryPointChild: string;
- /**
- * The ID of the component to display as the modal's content.
- */
- contentChild: string;
-}
-
-export interface Button {
- /**
- * The ID of the component to display as the button's content.
- */
- child: string;
-
- /**
- * Represents a user-initiated action.
- */
- action: Action;
-}
-
-export interface Checkbox {
- label: StringValue;
- value: {
- /**
- * A data binding reference to a location in the data model (e.g., '/user/name').
- */
- path?: string;
- literalBoolean?: boolean;
- };
-}
-
-export interface TextField {
- text?: StringValue;
- /**
- * A label, title, or placeholder text.
- */
- label: StringValue;
- type?: "shortText" | "number" | "date" | "longText";
- /**
- * A regex string to validate the input.
- */
- validationRegexp?: string;
-}
-
-export interface DateTimeInput {
- value: StringValue;
- enableDate?: boolean;
- enableTime?: boolean;
- /**
- * The string format for the output (e.g., 'YYYY-MM-DD').
- */
- outputFormat?: string;
-}
-
-export interface MultipleChoice {
- selections: {
- /**
- * A data binding reference to a location in the data model (e.g., '/user/name').
- */
- path?: string;
- literalArray?: string[];
- };
- options?: {
- label: {
- /**
- * A data binding reference to a location in the data model (e.g., '/user/name').
- */
- path?: string;
- /**
- * A fixed, hardcoded string value.
- */
- literalString?: string;
- };
- value: string;
- }[];
- maxAllowedSelections?: number;
-}
-
-export interface Slider {
- value: {
- /**
- * A data binding reference to a location in the data model (e.g., '/user/name').
- */
- path?: string;
- literalNumber?: number;
- };
- minValue?: number;
- maxValue?: number;
-}
diff --git a/renderers/lit/src/0.8/ui/root.ts b/renderers/lit/src/0.8/ui/root.ts
deleted file mode 100644
index 93ba9cb6..00000000
--- a/renderers/lit/src/0.8/ui/root.ts
+++ /dev/null
@@ -1,531 +0,0 @@
-/*
- Copyright 2025 Google LLC
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
-
-import { SignalWatcher } from "@lit-labs/signals";
-import { consume } from "@lit/context";
-import {
- css,
- html,
- LitElement,
- nothing,
- PropertyValues,
- render,
- TemplateResult,
-} from "lit";
-import { customElement, property } from "lit/decorators.js";
-import { map } from "lit/directives/map.js";
-import { effect } from "signal-utils/subtle/microtask-effect";
-import { A2uiMessageProcessor } from "../data/model-processor.js";
-import { StringValue } from "../types/primitives.js";
-import { Theme, AnyComponentNode, SurfaceID } from "../types/types.js";
-import { themeContext } from "./context/theme.js";
-import { structuralStyles } from "./styles.js";
-import { componentRegistry } from "./component-registry.js";
-
-type NodeOfType = Extract<
- AnyComponentNode,
- { type: T }
->;
-
-// This is the base class all the components will inherit
-@customElement("a2ui-root")
-export class Root extends SignalWatcher(LitElement) {
- @property()
- accessor surfaceId: SurfaceID | null = null;
-
- @property()
- accessor component: AnyComponentNode | null = null;
-
- @consume({ context: themeContext })
- accessor theme!: Theme;
-
- @property({ attribute: false })
- accessor childComponents: AnyComponentNode[] | null = null;
-
- @property({ attribute: false })
- accessor processor: A2uiMessageProcessor | null = null;
-
- @property()
- accessor dataContextPath: string = "";
-
- @property()
- accessor enableCustomElements = false;
-
- @property()
- set weight(weight: string | number) {
- this.#weight = weight;
- this.style.setProperty("--weight", `${weight}`);
- }
-
- get weight() {
- return this.#weight;
- }
-
- #weight: string | number = 1;
-
- static styles = [
- structuralStyles,
- css`
- :host {
- display: flex;
- flex-direction: column;
- gap: 8px;
- max-height: 80%;
- }
- `,
- ];
-
- /**
- * Holds the cleanup function for our effect.
- * We need this to stop the effect when the component is disconnected.
- */
- #lightDomEffectDisposer: null | (() => void) = null;
-
- protected willUpdate(changedProperties: PropertyValues): void {
- if (changedProperties.has("childComponents")) {
- if (this.#lightDomEffectDisposer) {
- this.#lightDomEffectDisposer();
- }
-
- // This effect watches the A2UI Children signal and updates the Light DOM.
- this.#lightDomEffectDisposer = effect(() => {
- // 1. Read the signal to create the subscription.
- const allChildren = this.childComponents ?? null;
-
- // 2. Generate the template for the children.
- const lightDomTemplate = this.renderComponentTree(allChildren);
-
- // 3. Imperatively render that template into the component itself.
- render(lightDomTemplate, this, { host: this });
- });
- }
- }
-
- /**
- * Clean up the effect when the component is removed from the DOM.
- */
- disconnectedCallback(): void {
- super.disconnectedCallback();
-
- if (this.#lightDomEffectDisposer) {
- this.#lightDomEffectDisposer();
- }
- }
-
- /**
- * Turns the SignalMap into a renderable TemplateResult for Lit.
- */
- private renderComponentTree(
- components: AnyComponentNode[] | null
- ): TemplateResult | typeof nothing {
- if (!components) {
- return nothing;
- }
-
- if (!Array.isArray(components)) {
- return nothing;
- }
-
- return html` ${map(components, (component) => {
- // 1. Check if there is a registered custom component or override.
- if (this.enableCustomElements) {
- const registeredCtor = componentRegistry.get(component.type);
- // We also check customElements.get for non-registered but defined elements
- const elCtor = registeredCtor || customElements.get(component.type);
-
- if (elCtor) {
- const node = component as AnyComponentNode;
- const el = new elCtor() as Root;
- el.id = node.id;
- if (node.slotName) {
- el.slot = node.slotName;
- }
- el.component = node;
- el.weight = node.weight ?? "initial";
- el.processor = this.processor;
- el.surfaceId = this.surfaceId;
- el.dataContextPath = node.dataContextPath ?? "/";
-
- for (const [prop, val] of Object.entries(component.properties)) {
- // @ts-expect-error We're off the books.
- el[prop] = val;
- }
- return html`${el}`;
- }
- }
-
- // 2. Fallback to standard components.
- switch (component.type) {
- case "List": {
- const node = component as NodeOfType<"List">;
- const childComponents = node.properties.children;
- return html``;
- }
-
- case "Card": {
- const node = component as NodeOfType<"Card">;
- let childComponents: AnyComponentNode[] | null =
- node.properties.children;
- if (!childComponents && node.properties.child) {
- childComponents = [node.properties.child];
- }
-
- return html``;
- }
-
- case "Column": {
- const node = component as NodeOfType<"Column">;
- return html``;
- }
-
- case "Row": {
- const node = component as NodeOfType<"Row">;
- return html``;
- }
-
- case "Image": {
- const node = component as NodeOfType<"Image">;
- return html``;
- }
-
- case "Icon": {
- const node = component as NodeOfType<"Icon">;
- return html``;
- }
-
- case "AudioPlayer": {
- const node = component as NodeOfType<"AudioPlayer">;
- return html``;
- }
-
- case "Button": {
- const node = component as NodeOfType<"Button">;
- return html``;
- }
-
- case "Text": {
- const node = component as NodeOfType<"Text">;
- return html``;
- }
-
- case "CheckBox": {
- const node = component as NodeOfType<"CheckBox">;
- return html``;
- }
-
- case "DateTimeInput": {
- const node = component as NodeOfType<"DateTimeInput">;
- return html``;
- }
-
- case "Divider": {
- // TODO: thickness, axis and color.
- const node = component as NodeOfType<"Divider">;
- return html``;
- }
-
- case "MultipleChoice": {
- // TODO: maxAllowedSelections and selections.
- const node = component as NodeOfType<"MultipleChoice">;
- return html``;
- }
-
- case "Slider": {
- const node = component as NodeOfType<"Slider">;
- return html``;
- }
-
- case "TextField": {
- // TODO: type and validationRegexp.
- const node = component as NodeOfType<"TextField">;
- return html``;
- }
-
- case "Video": {
- const node = component as NodeOfType<"Video">;
- return html``;
- }
-
- case "Tabs": {
- const node = component as NodeOfType<"Tabs">;
- const titles: StringValue[] = [];
- const childComponents: AnyComponentNode[] = [];
- if (node.properties.tabItems) {
- for (const item of node.properties.tabItems) {
- titles.push(item.title);
- childComponents.push(item.child);
- }
- }
-
- return html``;
- }
-
- case "Modal": {
- const node = component as NodeOfType<"Modal">;
- const childComponents: AnyComponentNode[] = [
- node.properties.entryPointChild,
- node.properties.contentChild,
- ];
-
- node.properties.entryPointChild.slotName = "entry";
-
- return html``;
- }
-
- default: {
- return this.renderCustomComponent(component);
- }
- }
- })}`;
- }
-
- private renderCustomComponent(component: AnyComponentNode) {
- if (!this.enableCustomElements) {
- return;
- }
-
- const node = component as AnyComponentNode;
- const registeredCtor = componentRegistry.get(component.type);
- const elCtor = registeredCtor || customElements.get(component.type);
-
- if (!elCtor) {
- return html`Unknown element ${component.type}`;
- }
-
- const el = new elCtor() as Root;
- el.id = node.id;
- if (node.slotName) {
- el.slot = node.slotName;
- }
- el.component = node;
- el.weight = node.weight ?? "initial";
- el.processor = this.processor;
- el.surfaceId = this.surfaceId;
- el.dataContextPath = node.dataContextPath ?? "/";
-
- for (const [prop, val] of Object.entries(component.properties)) {
- // @ts-expect-error We're off the books.
- el[prop] = val;
- }
- return html`${el}`;
- }
-
- render(): TemplateResult | typeof nothing {
- return html``;
- }
-}
diff --git a/samples/client/lit/contact/contact.ts b/samples/client/lit/contact/contact.ts
index 8db10286..a5d61936 100644
--- a/samples/client/lit/contact/contact.ts
+++ b/samples/client/lit/contact/contact.ts
@@ -171,7 +171,8 @@ export class A2UIContactFinder extends SignalWatcher(LitElement) {
`,
];
- #processor = v0_8.Data.createSignalA2uiMessageProcessor();
+ #processor = v0_8.Data.createSignalA2uiMessageProcessor(v0_8.standardCatalogApi);
+ #renderer = new v0_8.LitRenderer(v0_8.standardLitCatalogImplementation);
#a2uiClient = new A2UIClient();
#snackbar: Snackbar | undefined = undefined;
#pendingSnackbarMessages: Array<{
@@ -263,11 +264,11 @@ export class A2UIContactFinder extends SignalWatcher(LitElement) {
if (evt.detail.action.context) {
const srcContext = evt.detail.action.context;
for (const item of srcContext) {
- if (item.value.literalBoolean) {
+ if (item.value.literalBoolean !== undefined) {
context[item.key] = item.value.literalBoolean;
- } else if (item.value.literalNumber) {
+ } else if (item.value.literalNumber !== undefined) {
context[item.key] = item.value.literalNumber;
- } else if (item.value.literalString) {
+ } else if (item.value.literalString !== undefined) {
context[item.key] = item.value.literalString;
} else if (item.value.path) {
const path = this.#processor.resolvePath(
@@ -279,7 +280,12 @@ export class A2UIContactFinder extends SignalWatcher(LitElement) {
path,
surfaceId
);
- context[item.key] = value;
+ if (value !== undefined && value !== null) {
+ context[item.key] = value;
+ } else {
+ console.warn(`[App] Context path ${path} resolved to null/undefined`);
+ context[item.key] = null;
+ }
}
}
}
@@ -299,7 +305,8 @@ export class A2UIContactFinder extends SignalWatcher(LitElement) {
.surfaceId=${surfaceId}
.surface=${surface}
.processor=${this.#processor}
- >`;
+ .renderer=${this.#renderer}
+ >`;
}
)}
`;
diff --git a/samples/client/lit/contact/middleware/a2a.ts b/samples/client/lit/contact/middleware/a2a.ts
index 74813d93..99e583b5 100644
--- a/samples/client/lit/contact/middleware/a2a.ts
+++ b/samples/client/lit/contact/middleware/a2a.ts
@@ -52,7 +52,7 @@ const createOrGetClient = async () => {
if (!client) {
// Create a client pointing to the agent's Agent Card URL.
client = await A2AClient.fromCardUrl(
- "http://localhost:10002/.well-known/agent-card.json",
+ "http://localhost:10003/.well-known/agent-card.json",
{ fetchImpl: fetchWithCustomHeader }
);
}
diff --git a/samples/client/lit/contact/ui/custom-components/org-chart.ts b/samples/client/lit/contact/ui/custom-components/org-chart.ts
index 4895ca7d..2ad28b7d 100644
--- a/samples/client/lit/contact/ui/custom-components/org-chart.ts
+++ b/samples/client/lit/contact/ui/custom-components/org-chart.ts
@@ -16,8 +16,8 @@
import { Root } from '@a2ui/lit/ui';
import { v0_8 } from '@a2ui/lit';
-import { html, css, TemplateResult } from 'lit';
-import { customElement, property } from 'lit/decorators.js';
+import { html, css } from 'lit';
+import { customElement } from 'lit/decorators.js';
import { map } from 'lit/directives/map.js';
// Use aliases for convenience
@@ -31,9 +31,6 @@ export interface OrgChartNode {
@customElement('org-chart')
export class OrgChart extends Root {
- @property({ type: Array }) accessor chain: OrgChartNode[] = [];
- @property({ type: Object }) accessor action: Action | null = null;
-
static styles = [
...Root.styles,
css`
@@ -103,14 +100,15 @@ export class OrgChart extends Root {
`];
render() {
- if (!this.chain || this.chain.length === 0) {
+ const chain = (this.node.properties.chain as OrgChartNode[]) || [];
+ if (chain.length === 0) {
return html`