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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions renderers/lit/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion renderers/lit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
"types": "./dist/src/0.8/core.d.ts",
"default": "./dist/src/0.8/core.js"
},
"./v0_8": {
"types": "./dist/src/0.8/core.d.ts",
"default": "./dist/src/0.8/core.js"
},
"./v0_9": {
"types": "./dist/src/v0_9/index.d.ts",
"default": "./dist/src/v0_9/index.js"
},
"./ui": {
"types": "./dist/src/0.8/ui/ui.d.ts",
"default": "./dist/src/0.8/ui/ui.js"
Expand Down Expand Up @@ -89,6 +97,7 @@
"@lit/context": "^1.1.4",
"lit": "^3.3.1",
"signal-utils": "^0.21.1",
"@a2ui/web_core": "file:../web_core"
"@a2ui/web_core": "file:../web_core",
"zod": "^3.25.76"
}
}
4 changes: 2 additions & 2 deletions renderers/lit/src/0.8/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

import assert from "node:assert";
import { describe, it, beforeEach } from "node:test";
import { v0_8 } from "@a2ui/lit";
import * as v0_8 from "@a2ui/lit/v0_8";
import * as Types from "@a2ui/web_core/types/types";
import { A2uiStateError } from "@a2ui/web_core/v0_9";
import { A2uiStateError } from "@a2ui/web_core/v0_8";

// Helper function to strip reactivity for clean comparisons.
const toPlainObject = (value: unknown): ReturnType<typeof JSON.parse> => {
Expand Down
4 changes: 4 additions & 0 deletions renderers/lit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@
* limitations under the License.
*/

/**
* @deprecated Import v0.8 from '@a2ui/lit/v0_8'.
* This entrypoint is maintained for backwards compatibility.
*/
export * as v0_8 from "./0.8/index.js";
79 changes: 79 additions & 0 deletions renderers/lit/src/v0_9/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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 { ReactiveController, LitElement } from "lit";
import { GenericBinder, ComponentContext, ComponentApi, ResolveA2uiProps, InferredComponentApiSchemaType } from "@a2ui/web_core/v0_9";

/**
* A Lit ReactiveController that binds an A2UI component context to its API schema.
*
* This controller manages the subscription to the GenericBinder, updating the
* component props and requesting a host update whenever the underlying layer data changes.
*
* @template Api The specific A2UI component API interface this controller is bound to.
*/
export class A2uiController<Api extends ComponentApi> implements ReactiveController {
/**
* The current reactive properties of the A2UI component, matching the expected output schema.
*/
public props: ResolveA2uiProps<InferredComponentApiSchemaType<Api>>;
private binder: GenericBinder<InferredComponentApiSchemaType<Api>>;
private subscription?: { unsubscribe: () => void };

/**
* Initializes the controller, binding it to the given Lit element and API schema.
*
* @param host The LitElement acting as the component host. Must provide a component `context`.
* @param api The A2UI component API defining the schema for this element.
*/
constructor(private host: LitElement & { context: ComponentContext }, api: Api) {
this.binder = new GenericBinder(this.host.context, api.schema);
this.props = this.binder.snapshot as ResolveA2uiProps<InferredComponentApiSchemaType<Api>>;
this.host.addController(this);
if (this.host.isConnected) {
this.hostConnected();
}
}

/**
* Subscribes to the GenericBinder updates when the host connects.
*
* Triggers a request update on the host element when new props are received.
*/
hostConnected() {
if (!this.subscription) {
this.subscription = this.binder.subscribe((newProps) => {
this.props = newProps as ResolveA2uiProps<InferredComponentApiSchemaType<Api>>;
this.host.requestUpdate();
});
}
}

/**
* Unsubscribes from the GenericBinder updates when the host disconnects.
*/
hostDisconnected() {
this.subscription?.unsubscribe();
this.subscription = undefined;
}

/**
* Disposes the underlying GenericBinder to clean up resources from the context.
*/
dispose() {
this.binder.dispose();
}
}
63 changes: 63 additions & 0 deletions renderers/lit/src/v0_9/base-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 { LitElement } from "lit";
import { property } from "lit/decorators.js";
import { ComponentContext, ComponentApi } from "@a2ui/web_core/v0_9";
import { A2uiController } from "./adapter.js";

/**
* A base class for A2UI Lit elements that manages the A2uiController lifecycle.
*
* This element handles the reactive attachment and detachment of the `A2uiController`
* whenever the component's `context` changes. Subclasses only need to implement
* `createController` to provide their specific schema-bound controller, and `render`
* to define the template based on the controller's reactive props.
*
* @template Api The specific A2UI component API defining the schema for this element.
*/
export abstract class A2uiLitElement<Api extends ComponentApi> extends LitElement {
@property({ type: Object }) accessor context!: ComponentContext;
protected controller!: A2uiController<Api>;

/**
* Instantiates the unique controller for this element's specific bound API.
*
* Subclasses must implement this method to return an `A2uiController` tied to
* their specific component `Api` definition.
*
* @returns A new instance of `A2uiController` matching the component API.
*/
protected abstract createController(): A2uiController<Api>;

/**
* Reacts to changes in the component's properties.
*
* Specifically, when the `context` property changes or is initialized, this method
* cleans up any existing controller and invokes `createController()` to bind to
* the new context.
*/
willUpdate(changedProperties: Map<string, any>) {
super.willUpdate(changedProperties);
if (changedProperties.has('context') && this.context) {
if (this.controller) {
this.removeController(this.controller);
this.controller.dispose();
}
this.controller = this.createController();
}
}
}
42 changes: 42 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/AudioPlayer.ts
Original file line number Diff line number Diff line change
@@ -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 { html, nothing} from "lit";
import { customElement } from "lit/decorators.js";
import { A2uiLitElement } from "../../../base-element.js";
import { A2uiController } from "../../../adapter.js";
import { AudioPlayerApi } from "@a2ui/web_core/v0_9/basic_catalog";

@customElement("a2ui-audioplayer")
export class A2uiAudioPlayerElement extends A2uiLitElement<typeof AudioPlayerApi> {
protected createController() { return new A2uiController(this, AudioPlayerApi); }

render() {
const props = this.controller.props;
if (!props) return nothing;

return html`
<div class="a2ui-audioplayer">
${props.description ? html`<p>${props.description}</p>` : ""}
<audio src=${props.url} controls></audio>
</div>`;
}
}

export const A2uiAudioPlayer = {
...AudioPlayerApi,
tagName: "a2ui-audioplayer"
};
56 changes: 56 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Button.ts
Original file line number Diff line number Diff line change
@@ -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, nothing} from "lit";
import { customElement } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { A2uiLitElement } from "../../../base-element.js";
import { A2uiController } from "../../../adapter.js";
import { ButtonApi } from "@a2ui/web_core/v0_9/basic_catalog";
import { ComponentContext } from "@a2ui/web_core/v0_9";
import { renderA2uiNode } from "../../../surface/render-node.js";

@customElement("a2ui-basic-button")
export class A2uiBasicButtonElement extends A2uiLitElement<typeof ButtonApi> {
protected createController() { return new A2uiController(this, ButtonApi); }

render() {
const props = this.controller.props;
if (!props) return nothing;

const isDisabled = props.isValid === false;

const classes = {
"a2ui-button": true,
["a2ui-button-" + (props.variant || "default")]: true
};

return html`
<button
class=${classMap(classes)}
@click=${() => !isDisabled && props.action && props.action()}
?disabled=${isDisabled}
>
${props.child ? html`${renderA2uiNode(new ComponentContext(this.context.dataContext.surface, props.child, this.context.dataContext.path))}` : ""}
</button>
`;
}
}

export const A2uiButton = {
...ButtonApi,
tagName: "a2ui-basic-button"
};
44 changes: 44 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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, nothing} from "lit";
import { customElement } from "lit/decorators.js";
import { A2uiLitElement } from "../../../base-element.js";
import { A2uiController } from "../../../adapter.js";
import { CardApi } from "@a2ui/web_core/v0_9/basic_catalog";
import { ComponentContext } from "@a2ui/web_core/v0_9";
import { renderA2uiNode } from "../../../surface/render-node.js";

@customElement("a2ui-card")
export class A2uiCardElement extends A2uiLitElement<typeof CardApi> {
protected createController() { return new A2uiController(this, CardApi); }

render() {
const props = this.controller.props;
if (!props) return nothing;

return html`
<div class="a2ui-card" style="border: 1px solid #ccc; border-radius: 8px; padding: 16px;">
${props.child ? html`${renderA2uiNode(new ComponentContext(this.context.dataContext.surface, props.child, this.context.dataContext.path))}` : ""}
</div>
`;
}
}

export const A2uiCard = {
...CardApi,
tagName: "a2ui-card"
};
Loading
Loading