mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-08 21:09:23 +08:00
refactor(vendor): align a2ui renderer typings
This commit is contained in:
58
vendor/a2ui/renderers/lit/src/0.8/types/types.ts
vendored
58
vendor/a2ui/renderers/lit/src/0.8/types/types.ts
vendored
@@ -1,25 +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.
|
||||
*/
|
||||
|
||||
export {
|
||||
type ClientToServerMessage as A2UIClientEventMessage,
|
||||
type ClientCapabilitiesDynamic,
|
||||
} from "./client-event.js";
|
||||
export { type Action } from "./components.js";
|
||||
|
||||
import {
|
||||
AudioPlayer,
|
||||
Button,
|
||||
@@ -35,12 +30,10 @@ import {
|
||||
Video,
|
||||
} from "./components";
|
||||
import { StringValue } from "./primitives";
|
||||
|
||||
export type MessageProcessor = {
|
||||
getSurfaces(): ReadonlyMap<string, Surface>;
|
||||
clearSurfaces(): void;
|
||||
processMessages(messages: ServerToClientMessage[]): void;
|
||||
|
||||
/**
|
||||
* Retrieves the data for a given component node and a relative path string.
|
||||
* This correctly handles the special `.` path, which refers to the node's
|
||||
@@ -51,17 +44,14 @@ export type MessageProcessor = {
|
||||
relativePath: string,
|
||||
surfaceId: string
|
||||
): DataValue | null;
|
||||
|
||||
setData(
|
||||
node: AnyComponentNode | null,
|
||||
relativePath: string,
|
||||
value: DataValue,
|
||||
surfaceId: string
|
||||
): void;
|
||||
|
||||
resolvePath(path: string, dataContextPath?: string): string;
|
||||
};
|
||||
|
||||
export type Theme = {
|
||||
components: {
|
||||
AudioPlayer: Record<string, boolean>;
|
||||
@@ -193,7 +183,6 @@ export type Theme = {
|
||||
Video?: Record<string, string>;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a user-initiated action, sent from the client to the server.
|
||||
*/
|
||||
@@ -219,7 +208,6 @@ export interface UserAction {
|
||||
[k: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
/** A recursive type for any valid JSON-like value in the data model. */
|
||||
export type DataValue =
|
||||
| string
|
||||
@@ -232,19 +220,16 @@ export type DataValue =
|
||||
export type DataObject = { [key: string]: DataValue };
|
||||
export type DataMap = Map<string, DataValue>;
|
||||
export type DataArray = DataValue[];
|
||||
|
||||
/** A template for creating components from a list in the data model. */
|
||||
export interface ComponentArrayTemplate {
|
||||
componentId: string;
|
||||
dataBinding: string;
|
||||
}
|
||||
|
||||
/** Defines a list of child components, either explicitly or via a template. */
|
||||
export interface ComponentArrayReference {
|
||||
explicitList?: string[];
|
||||
template?: ComponentArrayTemplate;
|
||||
}
|
||||
|
||||
/** Represents the general shape of a component's properties. */
|
||||
export type ComponentProperties = {
|
||||
// Allow any property, but define known structural ones for type safety.
|
||||
@@ -252,31 +237,26 @@ export type ComponentProperties = {
|
||||
child?: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
|
||||
/** A raw component instance from a SurfaceUpdate message. */
|
||||
export interface ComponentInstance {
|
||||
id: string;
|
||||
weight?: number;
|
||||
component?: ComponentProperties;
|
||||
}
|
||||
|
||||
export interface BeginRenderingMessage {
|
||||
surfaceId: string;
|
||||
root: string;
|
||||
styles?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface SurfaceUpdateMessage {
|
||||
surfaceId: string;
|
||||
components: ComponentInstance[];
|
||||
}
|
||||
|
||||
export interface DataModelUpdate {
|
||||
surfaceId: string;
|
||||
path?: string;
|
||||
contents: ValueMap[];
|
||||
}
|
||||
|
||||
// ValueMap is a type of DataObject for passing to the data model.
|
||||
export type ValueMap = DataObject & {
|
||||
key: string;
|
||||
@@ -286,18 +266,15 @@ export type ValueMap = DataObject & {
|
||||
valueBoolean?: boolean;
|
||||
valueMap?: ValueMap[];
|
||||
};
|
||||
|
||||
export interface DeleteSurfaceMessage {
|
||||
surfaceId: string;
|
||||
}
|
||||
|
||||
export interface ServerToClientMessage {
|
||||
beginRendering?: BeginRenderingMessage;
|
||||
surfaceUpdate?: SurfaceUpdateMessage;
|
||||
dataModelUpdate?: DataModelUpdate;
|
||||
deleteSurface?: DeleteSurfaceMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* A recursive type for any value that can appear within a resolved component
|
||||
* tree. This is the main type that makes the recursive resolution possible.
|
||||
@@ -310,13 +287,10 @@ export type ResolvedValue =
|
||||
| AnyComponentNode
|
||||
| ResolvedMap
|
||||
| ResolvedArray;
|
||||
|
||||
/** A generic map where each value has been recursively resolved. */
|
||||
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.
|
||||
*/
|
||||
@@ -326,103 +300,83 @@ interface BaseComponentNode {
|
||||
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 {
|
||||
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.
|
||||
@@ -447,7 +401,6 @@ export type AnyComponentNode =
|
||||
| MultipleChoiceNode
|
||||
| SliderNode
|
||||
| CustomNode;
|
||||
|
||||
// These components do not contain other components can reuse their
|
||||
// original interfaces.
|
||||
export type ResolvedText = Text;
|
||||
@@ -461,7 +414,6 @@ export type ResolvedTextField = TextField;
|
||||
export type ResolvedDateTimeInput = DateTimeInput;
|
||||
export type ResolvedMultipleChoice = MultipleChoice;
|
||||
export type ResolvedSlider = Slider;
|
||||
|
||||
export interface ResolvedRow {
|
||||
children: AnyComponentNode[];
|
||||
distribution?:
|
||||
@@ -473,7 +425,6 @@ export interface ResolvedRow {
|
||||
| "spaceEvenly";
|
||||
alignment?: "start" | "center" | "end" | "stretch";
|
||||
}
|
||||
|
||||
export interface ResolvedColumn {
|
||||
children: AnyComponentNode[];
|
||||
distribution?:
|
||||
@@ -485,43 +436,34 @@ export interface ResolvedColumn {
|
||||
| "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 interface CustomNodeProperties {
|
||||
[k: string]: ResolvedValue;
|
||||
}
|
||||
|
||||
export type SurfaceID = string;
|
||||
|
||||
/** The complete state of a single UI surface. */
|
||||
export interface Surface {
|
||||
rootComponentId: string | null;
|
||||
|
||||
57
vendor/a2ui/renderers/lit/src/0.8/ui/root.ts
vendored
57
vendor/a2ui/renderers/lit/src/0.8/ui/root.ts
vendored
@@ -1,19 +1,15 @@
|
||||
/*
|
||||
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 {
|
||||
@@ -34,48 +30,36 @@ 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<T extends AnyComponentNode["type"]> = Extract<
|
||||
AnyComponentNode,
|
||||
{ type: T }
|
||||
>;
|
||||
|
||||
// This is the base class all the components will inherit
|
||||
@customElement("a2ui-root")
|
||||
export class Root extends (SignalWatcher(LitElement) as typeof 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`
|
||||
@@ -87,44 +71,36 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
/**
|
||||
* 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<this>): 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.
|
||||
*/
|
||||
@@ -134,18 +110,15 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
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;
|
||||
@@ -158,7 +131,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
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;
|
||||
@@ -166,7 +138,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
return html`${el}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to standard components.
|
||||
switch (component.type) {
|
||||
case "List": {
|
||||
@@ -184,7 +155,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-list>`;
|
||||
}
|
||||
|
||||
case "Card": {
|
||||
const node = component as NodeOfType<"Card">;
|
||||
let childComponents: AnyComponentNode[] | null =
|
||||
@@ -192,7 +162,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
if (!childComponents && node.properties.child) {
|
||||
childComponents = [node.properties.child];
|
||||
}
|
||||
|
||||
return html`<a2ui-card
|
||||
id=${node.id}
|
||||
slot=${node.slotName ? node.slotName : nothing}
|
||||
@@ -205,7 +174,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-card>`;
|
||||
}
|
||||
|
||||
case "Column": {
|
||||
const node = component as NodeOfType<"Column">;
|
||||
return html`<a2ui-column
|
||||
@@ -222,7 +190,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-column>`;
|
||||
}
|
||||
|
||||
case "Row": {
|
||||
const node = component as NodeOfType<"Row">;
|
||||
return html`<a2ui-row
|
||||
@@ -239,7 +206,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-row>`;
|
||||
}
|
||||
|
||||
case "Image": {
|
||||
const node = component as NodeOfType<"Image">;
|
||||
return html`<a2ui-image
|
||||
@@ -256,7 +222,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-image>`;
|
||||
}
|
||||
|
||||
case "Icon": {
|
||||
const node = component as NodeOfType<"Icon">;
|
||||
return html`<a2ui-icon
|
||||
@@ -271,7 +236,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-icon>`;
|
||||
}
|
||||
|
||||
case "AudioPlayer": {
|
||||
const node = component as NodeOfType<"AudioPlayer">;
|
||||
return html`<a2ui-audioplayer
|
||||
@@ -286,7 +250,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-audioplayer>`;
|
||||
}
|
||||
|
||||
case "Button": {
|
||||
const node = component as NodeOfType<"Button">;
|
||||
return html`<a2ui-button
|
||||
@@ -302,7 +265,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-button>`;
|
||||
}
|
||||
|
||||
case "Text": {
|
||||
const node = component as NodeOfType<"Text">;
|
||||
return html`<a2ui-text
|
||||
@@ -319,7 +281,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-text>`;
|
||||
}
|
||||
|
||||
case "CheckBox": {
|
||||
const node = component as NodeOfType<"CheckBox">;
|
||||
return html`<a2ui-checkbox
|
||||
@@ -335,7 +296,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-checkbox>`;
|
||||
}
|
||||
|
||||
case "DateTimeInput": {
|
||||
const node = component as NodeOfType<"DateTimeInput">;
|
||||
return html`<a2ui-datetimeinput
|
||||
@@ -353,7 +313,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-datetimeinput>`;
|
||||
}
|
||||
|
||||
case "Divider": {
|
||||
// TODO: thickness, axis and color.
|
||||
const node = component as NodeOfType<"Divider">;
|
||||
@@ -371,7 +330,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-divider>`;
|
||||
}
|
||||
|
||||
case "MultipleChoice": {
|
||||
// TODO: maxAllowedSelections and selections.
|
||||
const node = component as NodeOfType<"MultipleChoice">;
|
||||
@@ -389,7 +347,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-multiplechoice>`;
|
||||
}
|
||||
|
||||
case "Slider": {
|
||||
const node = component as NodeOfType<"Slider">;
|
||||
return html`<a2ui-slider
|
||||
@@ -406,7 +363,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-slider>`;
|
||||
}
|
||||
|
||||
case "TextField": {
|
||||
// TODO: type and validationRegexp.
|
||||
const node = component as NodeOfType<"TextField">;
|
||||
@@ -425,7 +381,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-textfield>`;
|
||||
}
|
||||
|
||||
case "Video": {
|
||||
const node = component as NodeOfType<"Video">;
|
||||
return html`<a2ui-video
|
||||
@@ -440,7 +395,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-video>`;
|
||||
}
|
||||
|
||||
case "Tabs": {
|
||||
const node = component as NodeOfType<"Tabs">;
|
||||
const titles: StringValue[] = [];
|
||||
@@ -451,7 +405,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
childComponents.push(item.child);
|
||||
}
|
||||
}
|
||||
|
||||
return html`<a2ui-tabs
|
||||
id=${node.id}
|
||||
slot=${node.slotName ? node.slotName : nothing}
|
||||
@@ -465,16 +418,13 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-tabs>`;
|
||||
}
|
||||
|
||||
case "Modal": {
|
||||
const node = component as NodeOfType<"Modal">;
|
||||
const childComponents: AnyComponentNode[] = [
|
||||
node.properties.entryPointChild,
|
||||
node.properties.contentChild,
|
||||
];
|
||||
|
||||
node.properties.entryPointChild.slotName = "entry";
|
||||
|
||||
return html`<a2ui-modal
|
||||
id=${node.id}
|
||||
slot=${node.slotName ? node.slotName : nothing}
|
||||
@@ -487,27 +437,22 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
.enableCustomElements=${this.enableCustomElements}
|
||||
></a2ui-modal>`;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -518,14 +463,12 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
|
||||
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`<slot></slot>`;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
/*
|
||||
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 { SurfaceUpdateSchemaMatcher } from "./surface_update_schema_matcher";
|
||||
import { SchemaMatcher } from "./schema_matcher";
|
||||
|
||||
export function validateSchema(
|
||||
data: any,
|
||||
schemaName: string,
|
||||
@@ -36,7 +31,6 @@ export function validateSchema(
|
||||
"A2UI Protocol message must have one of: surfaceUpdate, dataModelUpdate, beginRendering, deleteSurface.",
|
||||
);
|
||||
}
|
||||
|
||||
if (matchers) {
|
||||
for (const matcher of matchers) {
|
||||
const result = matcher.validate(data);
|
||||
@@ -45,10 +39,8 @@ export function validateSchema(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateDeleteSurface(data: any, errors: string[]) {
|
||||
if (data.surfaceId === undefined) {
|
||||
errors.push("DeleteSurface must have a 'surfaceId' property.");
|
||||
@@ -60,7 +52,6 @@ function validateDeleteSurface(data: any, errors: string[]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateSurfaceUpdate(data: any, errors: string[]) {
|
||||
if (data.surfaceId === undefined) {
|
||||
errors.push("SurfaceUpdate must have a 'surfaceId' property.");
|
||||
@@ -69,7 +60,6 @@ function validateSurfaceUpdate(data: any, errors: string[]) {
|
||||
errors.push("SurfaceUpdate must have a 'components' array.");
|
||||
return;
|
||||
}
|
||||
|
||||
const componentIds = new Set<string>();
|
||||
for (const c of data.components) {
|
||||
if (c.id) {
|
||||
@@ -79,29 +69,24 @@ function validateSurfaceUpdate(data: any, errors: string[]) {
|
||||
componentIds.add(c.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const component of data.components) {
|
||||
validateComponent(component, componentIds, errors);
|
||||
}
|
||||
}
|
||||
|
||||
function validateDataModelUpdate(data: any, errors: string[]) {
|
||||
if (data.surfaceId === undefined) {
|
||||
errors.push("DataModelUpdate must have a 'surfaceId' property.");
|
||||
}
|
||||
|
||||
const allowedTopLevel = ["surfaceId", "path", "contents"];
|
||||
for (const key in data) {
|
||||
if (!allowedTopLevel.includes(key)) {
|
||||
errors.push(`DataModelUpdate has unexpected property: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.contents)) {
|
||||
errors.push("DataModelUpdate must have a 'contents' array.");
|
||||
return;
|
||||
}
|
||||
|
||||
const validateValueProperty = (
|
||||
item: any,
|
||||
itemErrors: string[],
|
||||
@@ -127,7 +112,6 @@ function validateDataModelUpdate(data: any, errors: string[]) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (foundValueProp === "valueMap") {
|
||||
if (!Array.isArray(item.valueMap)) {
|
||||
itemErrors.push(`${prefix} 'valueMap' must be an array.`);
|
||||
@@ -162,7 +146,6 @@ function validateDataModelUpdate(data: any, errors: string[]) {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
data.contents.forEach((item: any, index: number) => {
|
||||
if (!item.key) {
|
||||
errors.push(
|
||||
@@ -190,7 +173,6 @@ function validateDataModelUpdate(data: any, errors: string[]) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function validateBeginRendering(data: any, errors: string[]) {
|
||||
if (data.surfaceId === undefined) {
|
||||
errors.push("BeginRendering message must have a 'surfaceId' property.");
|
||||
@@ -199,7 +181,6 @@ function validateBeginRendering(data: any, errors: string[]) {
|
||||
errors.push("BeginRendering message must have a 'root' property.");
|
||||
}
|
||||
}
|
||||
|
||||
function validateBoundValue(
|
||||
prop: any,
|
||||
propName: string,
|
||||
@@ -232,7 +213,6 @@ function validateBoundValue(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateComponent(
|
||||
component: any,
|
||||
allIds: Set<string>,
|
||||
@@ -246,7 +226,6 @@ function validateComponent(
|
||||
errors.push(`Component '${component.id}' is missing 'component'.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const componentTypes = Object.keys(component.component);
|
||||
if (componentTypes.length !== 1) {
|
||||
errors.push(
|
||||
@@ -254,10 +233,8 @@ function validateComponent(
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const componentType = componentTypes[0];
|
||||
const properties = component.component[componentType];
|
||||
|
||||
const checkRequired = (props: string[]) => {
|
||||
for (const prop of props) {
|
||||
if (properties[prop] === undefined) {
|
||||
@@ -267,7 +244,6 @@ function validateComponent(
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkRefs = (ids: (string | undefined)[]) => {
|
||||
for (const id of ids) {
|
||||
if (id && !allIds.has(id)) {
|
||||
@@ -277,7 +253,6 @@ function validateComponent(
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
switch (componentType) {
|
||||
case "Heading":
|
||||
checkRequired(["text"]);
|
||||
|
||||
Reference in New Issue
Block a user