chore: initial import of standalone agentscope project
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled

This commit is contained in:
2026-03-02 18:21:40 +08:00
commit a842f1861f
561 changed files with 91892 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
# A2UI Generator
This is a UI to generate and visualize A2UI responses.
## Prerequisites
1. [nodejs](https://nodejs.org/en)
## Running
This sample depends on the Lit renderer. Before running this sample, you need to build the renderer.
1. **Build the renderer:**
```bash
cd ../../../renderers/lit
npm install
npm run build
```
2. **Run this sample:**
```bash
cd - # back to the sample directory
npm install
```
3. **Run the servers:**
- Run the [A2A server](../../../agent/adk/contact_lookup/)
- Run the dev server: `npm run dev`
After starting the dev server, you can open http://localhost:5173/ to view the sample.

View File

@@ -0,0 +1,65 @@
/*
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 { v0_8 } from "@a2ui/lit";
import { registerContactComponents } from "./ui/custom-components/register-components.js";
type A2TextPayload = {
kind: "text";
text: string;
};
type A2DataPayload = {
kind: "data";
data: v0_8.Types.ServerToClientMessage;
};
type A2AServerPayload =
| Array<A2DataPayload | A2TextPayload>
| { error: string };
export class A2UIClient {
#ready: Promise<void> = Promise.resolve();
get ready() {
return this.#ready;
}
async send(
message: v0_8.Types.A2UIClientEventMessage
): Promise<v0_8.Types.ServerToClientMessage[]> {
const response = await fetch("/a2a", {
body: JSON.stringify(message),
method: "POST",
});
if (response.ok) {
const data = (await response.json()) as A2AServerPayload;
const messages: v0_8.Types.ServerToClientMessage[] = [];
if ("error" in data) {
throw new Error(data.error);
} else {
for (const item of data) {
if (item.kind === "text") continue;
messages.push(item.data);
}
}
return messages;
}
const error = (await response.json()) as { error: string };
throw new Error(error.error);
}
}
registerContactComponents();

View File

@@ -0,0 +1,376 @@
/*
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 { provide } from "@lit/context";
import {
LitElement,
html,
css,
nothing,
HTMLTemplateResult,
unsafeCSS,
} from "lit";
import { customElement, state } from "lit/decorators.js";
import { theme as uiTheme } from "./theme/theme.js";
import { A2UIClient } from "./client.js";
import {
SnackbarAction,
SnackbarMessage,
SnackbarUUID,
SnackType,
} from "./types/types.js";
import { type Snackbar } from "./ui/snackbar.js";
import { repeat } from "lit/directives/repeat.js";
import { v0_8 } from "@a2ui/lit";
import * as UI from "@a2ui/lit/ui";
// Demo elements.
import "./ui/ui.js";
import { registerContactComponents } from "./ui/custom-components/register-components.js";
// Register custom components for the contact app
registerContactComponents();
@customElement("a2ui-contact")
export class A2UIContactFinder extends SignalWatcher(LitElement) {
@provide({ context: UI.Context.themeContext })
accessor theme: v0_8.Types.Theme = uiTheme;
@state()
accessor #requesting = false;
@state()
accessor #error: string | null = null;
@state()
accessor #lastMessages: v0_8.Types.ServerToClientMessage[] = [];
static styles = [
unsafeCSS(v0_8.Styles.structuralStyles),
css`
:host {
display: block;
max-width: 640px;
margin: 0 auto;
min-height: 100%;
}
#surfaces {
display: flex;
flex-direction: column;
width: 100%;
padding: var(--bb-grid-size-3) 0;
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;
& a2ui-surface {
align-items: center;
}
}
form {
display: flex;
flex-direction: column;
flex: 1;
gap: 16px;
align-items: center;
padding: 16px 0;
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 1s backwards;
& > div {
display: flex;
flex: 1;
gap: 16px;
align-items: center;
width: 100%;
& > input {
display: block;
flex: 1;
border-radius: 32px;
padding: 16px 24px;
border: 1px solid var(--p-60);
font-size: 16px;
}
& > button {
display: flex;
align-items: center;
background: var(--p-40);
color: var(--n-100);
border: none;
padding: 8px 16px;
border-radius: 32px;
opacity: 0.5;
&:not([disabled]) {
cursor: pointer;
opacity: 1;
}
}
}
}
.rotate {
animation: rotate 1s linear infinite;
}
.pending {
width: 100%;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;
& .g-icon {
margin-right: 8px;
}
}
.error {
color: var(--e-40);
background-color: var(--e-95);
border: 1px solid var(--e-80);
padding: 16px;
border-radius: 8px;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes rotate {
from {
rotate: 0deg;
}
to {
rotate: 360deg;
}
}
`,
];
#processor = v0_8.Data.createSignalA2uiMessageProcessor();
#a2uiClient = new A2UIClient();
#snackbar: Snackbar | undefined = undefined;
#pendingSnackbarMessages: Array<{
message: SnackbarMessage;
replaceAll: boolean;
}> = [];
render() {
return [
this.#maybeRenderForm(),
this.#maybeRenderData(),
this.#maybeRenderError(),
];
}
#maybeRenderError() {
if (!this.#error) return nothing;
return html`<div class="error">${this.#error}</div>`;
}
#maybeRenderForm() {
if (this.#requesting) return nothing;
if (this.#lastMessages.length > 0) return nothing;
return html`<form
@submit=${async (evt: Event) => {
evt.preventDefault();
if (!(evt.target instanceof HTMLFormElement)) {
return;
}
const data = new FormData(evt.target);
const body = data.get("body") ?? null;
if (!body) {
return;
}
const message = body as v0_8.Types.A2UIClientEventMessage;
await this.#sendAndProcessMessage(message);
}}
>
<h1 class="typography-f-sf typography-v-r typography-w-400 color-c-p30">
Contact Finder
</h1>
<div>
<input
required
value="Casey Smith"
autocomplete="off"
id="body"
name="body"
type="text"
?disabled=${this.#requesting}
/>
<button type="submit" ?disabled=${this.#requesting}>
<span class="g-icon filled-heavy">send</span>
</button>
</div>
</form>`;
}
#maybeRenderData() {
if (this.#requesting) {
return html` <div class="pending">
<span class="g-icon filled-heavy rotate">progress_activity</span>
Awaiting an answer...
</div>`;
}
const surfaces = this.#processor.getSurfaces();
if (surfaces.size === 0) {
return nothing;
}
return html`<section id="surfaces">
${repeat(
this.#processor.getSurfaces(),
([surfaceId]) => surfaceId,
([surfaceId, surface]) => {
return html`<a2ui-surface
@a2uiaction=${async (
evt: v0_8.Events.StateEvent<"a2ui.action">
) => {
const [target] = evt.composedPath();
if (!(target instanceof HTMLElement)) {
return;
}
const context: v0_8.Types.A2UIClientEventMessage["userAction"]["context"] =
{};
if (evt.detail.action.context) {
const srcContext = evt.detail.action.context;
for (const item of srcContext) {
if (item.value.literalBoolean) {
context[item.key] = item.value.literalBoolean;
} else if (item.value.literalNumber) {
context[item.key] = item.value.literalNumber;
} else if (item.value.literalString) {
context[item.key] = item.value.literalString;
} else if (item.value.path) {
const path = this.#processor.resolvePath(
item.value.path,
evt.detail.dataContextPath
);
const value = this.#processor.getData(
evt.detail.sourceComponent,
path,
surfaceId
);
context[item.key] = value;
}
}
}
const message: v0_8.Types.A2UIClientEventMessage = {
userAction: {
surfaceId: surfaceId,
name: evt.detail.action.name,
sourceComponentId: target.id,
timestamp: new Date().toISOString(),
context,
},
};
await this.#sendAndProcessMessage(message);
}}
.surfaceId=${surfaceId}
.surface=${surface}
.processor=${this.#processor}
></a2-uisurface>`;
}
)}
</section>`;
}
async #sendAndProcessMessage(request) {
const messages = await this.#sendMessage(request);
this.#lastMessages = messages;
this.#processor.clearSurfaces();
this.#processor.processMessages(messages);
}
async #sendMessage(
message: v0_8.Types.A2UIClientEventMessage
): Promise<v0_8.Types.ServerToClientMessage[]> {
try {
this.#requesting = true;
const response = this.#a2uiClient.send(message);
await response;
this.#requesting = false;
return response;
} catch (err) {
this.snackbar(err as string, SnackType.ERROR);
} finally {
this.#requesting = false;
}
return [];
}
snackbar(
message: string | HTMLTemplateResult,
type: SnackType,
actions: SnackbarAction[] = [],
persistent = false,
id = globalThis.crypto.randomUUID(),
replaceAll = false
) {
if (!this.#snackbar) {
this.#pendingSnackbarMessages.push({
message: {
id,
message,
type,
persistent,
actions,
},
replaceAll,
});
return;
}
return this.#snackbar.show(
{
id,
message,
type,
persistent,
actions,
},
replaceAll
);
}
unsnackbar(id?: SnackbarUUID) {
if (!this.#snackbar) {
return;
}
this.#snackbar.hide(id);
}
}

View File

@@ -0,0 +1,35 @@
/*
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 { HTMLTemplateResult } from "lit";
const eventInit = {
bubbles: true,
cancelable: true,
composed: true,
};
export class SnackbarActionEvent extends Event {
static eventName = "snackbaraction";
constructor(
public readonly action: string,
public readonly value?: HTMLTemplateResult | string,
public readonly callback?: () => void
) {
super(SnackbarActionEvent.eventName, { ...eventInit });
}
}

View File

@@ -0,0 +1,196 @@
<!DOCTYPE html>
<!--
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.
-->
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contact Finder Agent</title>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=account_circle,add,arrow_back,arrow_drop_down,arrow_forward,attach_file,calendar_today,call,camera,check,check_circle,close,communication,content_copy,delete,download,draw,edit,error,event,favorite,favorite_off,folder,help,home,info,location_on,lock,lock_open,mail,menu,mobile_layout,more_horiz,more_vert,notifications,notifications_off,payment,pen_size_1,person,phone,photo,print,progress_activity,rectangle,refresh,search,send,settings,share,shopping_cart,star,star_half,star_off,upload,visibility,visibility_off,warning"
/>
<style>
:root {
--n-100: #ffffff;
--n-99: #fcfcfc;
--n-98: #f9f9f9;
--n-95: #f1f1f1;
--n-90: #e2e2e2;
--n-80: #c6c6c6;
--n-70: #ababab;
--n-60: #919191;
--n-50: #777777;
--n-40: #5e5e5e;
--n-35: #525252;
--n-30: #474747;
--n-25: #3b3b3b;
--n-20: #303030;
--n-15: #262626;
--n-10: #1b1b1b;
--n-5: #111111;
--n-0: #000000;
--p-100: #ffffff;
--p-99: #fffbff;
--p-98: #fcf8ff;
--p-95: #f2efff;
--p-90: #e1e0ff;
--p-80: #c0c1ff;
--p-70: #a0a3ff;
--p-60: #8487ea;
--p-50: #6a6dcd;
--p-40: #5154b3;
--p-35: #4447a6;
--p-30: #383b99;
--p-25: #2c2e8d;
--p-20: #202182;
--p-15: #131178;
--p-10: #06006c;
--p-5: #03004d;
--p-0: #000000;
--s-100: #ffffff;
--s-99: #fffbff;
--s-98: #fcf8ff;
--s-95: #f2efff;
--s-90: #e2e0f9;
--s-80: #c6c4dd;
--s-70: #aaa9c1;
--s-60: #8f8fa5;
--s-50: #75758b;
--s-40: #5d5c72;
--s-35: #515165;
--s-30: #454559;
--s-25: #393a4d;
--s-20: #2e2f42;
--s-15: #242437;
--s-10: #191a2c;
--s-5: #0f0f21;
--s-0: #000000;
--t-100: #ffffff;
--t-99: #fffbff;
--t-98: #fff8f9;
--t-95: #ffecf4;
--t-90: #ffd8ec;
--t-80: #e9b9d3;
--t-70: #cc9eb8;
--t-60: #af849d;
--t-50: #946b83;
--t-40: #79536a;
--t-35: #6c475d;
--t-30: #5f3c51;
--t-25: #523146;
--t-20: #46263a;
--t-15: #3a1b2f;
--t-10: #2e1125;
--t-5: #22071a;
--t-0: #000000;
--nv-100: #ffffff;
--nv-99: #fffbff;
--nv-98: #fcf8ff;
--nv-95: #f2effa;
--nv-90: #e4e1ec;
--nv-80: #c8c5d0;
--nv-70: #acaab4;
--nv-60: #918f9a;
--nv-50: #777680;
--nv-40: #5e5d67;
--nv-35: #52515b;
--nv-30: #46464f;
--nv-25: #3b3b43;
--nv-20: #303038;
--nv-15: #25252d;
--nv-10: #1b1b23;
--nv-5: #101018;
--nv-0: #000000;
--e-100: #ffffff;
--e-99: #fffbff;
--e-98: #fff8f7;
--e-95: #ffedea;
--e-90: #ffdad6;
--e-80: #ffb4ab;
--e-70: #ff897d;
--e-60: #ff5449;
--e-50: #de3730;
--e-40: #ba1a1a;
--e-35: #a80710;
--e-30: #93000a;
--e-25: #7e0007;
--e-20: #690005;
--e-15: #540003;
--e-10: #410002;
--e-5: #2d0001;
--e-0: #000000;
--primary: #137fec;
--text-color: #fff;
--background-light: #f6f7f8;
--background-dark: #101922;
--border-color: oklch(
from var(--background-light) l c h / calc(alpha * 0.15)
);
--elevated-background-light: oklch(
from var(--background-light) l c h / calc(alpha * 0.05)
);
--bb-grid-size: 4px;
--bb-grid-size-2: calc(var(--bb-grid-size) * 2);
--bb-grid-size-3: calc(var(--bb-grid-size) * 3);
--bb-grid-size-4: calc(var(--bb-grid-size) * 4);
--bb-grid-size-5: calc(var(--bb-grid-size) * 5);
--bb-grid-size-6: calc(var(--bb-grid-size) * 6);
--bb-grid-size-7: calc(var(--bb-grid-size) * 7);
--bb-grid-size-8: calc(var(--bb-grid-size) * 8);
--bb-grid-size-9: calc(var(--bb-grid-size) * 9);
--bb-grid-size-10: calc(var(--bb-grid-size) * 10);
--bb-grid-size-11: calc(var(--bb-grid-size) * 11);
--bb-grid-size-12: calc(var(--bb-grid-size) * 12);
--bb-grid-size-13: calc(var(--bb-grid-size) * 13);
--bb-grid-size-14: calc(var(--bb-grid-size) * 14);
--bb-grid-size-15: calc(var(--bb-grid-size) * 15);
--bb-grid-size-16: calc(var(--bb-grid-size) * 16);
}
* {
box-sizing: border-box;
}
html,
body {
--font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-family-flex: "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-family-mono: monospace;
background: var(--background-light);
font-family: var(--font-family);
margin: 0;
padding: 0;
width: 100svw;
height: 100svh;
}
</style>
</head>
<body>
<a2ui-contact></a2ui-contact>
<script src="./contact.ts" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,153 @@
/*
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 { IncomingMessage, ServerResponse } from "http";
import { Plugin, ViteDevServer } from "vite";
import { A2AClient } from "@a2a-js/sdk/client";
import {
MessageSendParams,
Part,
SendMessageSuccessResponse,
Task,
} from "@a2a-js/sdk";
import { v4 as uuidv4 } from "uuid";
const A2AUI_MIME_TYPE = "application/json+a2aui";
const fetchWithCustomHeader: typeof fetch = async (url, init) => {
const headers = new Headers(init?.headers);
headers.set("X-A2A-Extensions", "https://a2ui.org/a2a-extension/a2ui/v0.8");
const newInit = { ...init, headers };
return fetch(url, newInit);
};
const isJson = (str: string) => {
try {
const parsed = JSON.parse(str);
return (
typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
);
} catch (err) {
console.warn(err);
return false;
}
};
let client: A2AClient | null = null;
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",
{ fetchImpl: fetchWithCustomHeader }
);
}
return client;
};
export const plugin = (): Plugin => {
return {
name: "a2a-handler",
configureServer(server: ViteDevServer) {
server.middlewares.use(
"/a2a",
async (req: IncomingMessage, res: ServerResponse, next: () => void) => {
if (req.method === "POST") {
let originalBody = "";
req.on("data", (chunk) => {
originalBody += chunk.toString();
});
req.on("end", async () => {
let sendParams: MessageSendParams;
if (isJson(originalBody)) {
console.log(
"[a2a-middleware] Received JSON UI event:",
originalBody
);
const clientEvent = JSON.parse(originalBody);
sendParams = {
message: {
messageId: uuidv4(),
role: "user",
parts: [
{
kind: "data",
data: clientEvent,
metadata: { 'mimeType': A2AUI_MIME_TYPE },
} as Part,
],
kind: "message",
},
};
} else {
console.log(
"[a2a-middleware] Received text query:",
originalBody
);
sendParams = {
message: {
messageId: uuidv4(),
role: "user",
parts: [
{
kind: "text",
text: originalBody,
},
],
kind: "message",
},
};
}
const client = await createOrGetClient();
const response = await client.sendMessage(sendParams);
if ("error" in response) {
console.error("Error:", response.error.message);
res.statusCode = 500;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ error: response.error.message }));
return;
} else {
const result = (response as SendMessageSuccessResponse)
.result as Task;
if (result.kind === "task") {
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(result.status.message?.parts));
return;
}
}
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify([]));
});
return;
} else {
next();
}
}
);
},
};
};

View File

@@ -0,0 +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.
*/
export * as A2AMiddleware from "./a2a.js";

View File

@@ -0,0 +1,84 @@
{
"name": "@a2ui/contact",
"private": true,
"version": "0.8.1",
"description": "A2UI Contact Demo",
"main": "./dist/contact.js",
"types": "./dist/contact.d.ts",
"type": "module",
"scripts": {
"prepack": "npm run build",
"build": "wireit",
"build:tsc": "wireit",
"dev": "npm run serve --watch",
"test": "wireit",
"serve": "wireit"
},
"wireit": {
"serve": {
"command": "vite dev",
"dependencies": [
"build"
],
"service": true
},
"test": {
"command": "node --test --enable-source-maps --test-reporter spec dist/src/0.8/tests/**/*.test.js",
"dependencies": [
"build"
]
},
"build": {
"dependencies": [
"build:tsc"
]
},
"build:tsc": {
"command": "tsc -b --pretty",
"env": {
"FORCE_COLOR": "1"
},
"dependencies": [
"../../../../renderers/lit:build:tsc"
],
"files": [
"**/*.ts",
"tsconfig.json"
],
"output": [
"dist/",
"!dist/**/*.min.js{,.map}"
],
"clean": "if-file-deleted"
}
},
"repository": {
"directory": "samples/client/lit/contact",
"type": "git",
"url": "git+https://github.com/google/A2UI.git"
},
"files": [
"dist"
],
"keywords": [],
"author": "Google",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/google/A2UI/issues"
},
"homepage": "https://github.com/google/A2UI/tree/main/web#readme",
"devDependencies": {
"dotenv": "^17.2.3",
"typescript": "^5.8.3",
"uuid": "^13.0.0",
"vite": "^7.1.11",
"wireit": "^0.15.0-pre.2"
},
"dependencies": {
"@a2a-js/sdk": "^0.3.4",
"@a2ui/lit": "file:../../../../renderers/lit",
"@lit-labs/signals": "^0.1.3",
"@lit/context": "^1.1.4",
"lit": "^3.3.1"
}
}

View File

@@ -0,0 +1,463 @@
/*
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 { v0_8 } from "@a2ui/lit";
/** Elements */
const a = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-as-n": true,
"layout-dis-iflx": true,
"layout-al-c": true,
};
const audio = {
"layout-w-100": true,
};
const body = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-mt-0": true,
"layout-mb-2": true,
"typography-sz-bm": true,
"color-c-n10": true,
};
const button = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-pt-3": true,
"layout-pb-3": true,
"layout-pl-5": true,
"layout-pr-5": true,
"layout-mb-1": true,
"border-br-16": true,
"border-bw-0": true,
"border-c-n70": true,
"border-bs-s": true,
"color-bgc-s30": true,
"color-c-n100": true,
"behavior-ho-80": true,
};
const heading = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mb-2": true,
"color-c-n10": true,
};
const h1 = {
...heading,
"typography-sz-tl": true,
};
const h2 = {
...heading,
"typography-sz-tm": true,
};
const h3 = {
...heading,
"typography-sz-ts": true,
};
const iframe = {
"behavior-sw-n": true,
};
const input = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-pl-4": true,
"layout-pr-4": true,
"layout-pt-2": true,
"layout-pb-2": true,
"border-br-6": true,
"border-bw-1": true,
"color-bc-s70": true,
"border-bs-s": true,
"layout-as-n": true,
"color-c-n10": true,
};
const p = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
"color-c-n10": true,
};
const orderedList = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
};
const unorderedList = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
};
const listItem = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
};
const pre = {
"typography-f-c": true,
"typography-fs-n": true,
"typography-w-400": true,
"typography-sz-bm": true,
"typography-ws-p": true,
"layout-as-n": true,
};
const textarea = {
...input,
"layout-r-none": true,
"layout-fs-c": true,
};
const video = {
"layout-el-cv": true,
};
const aLight = v0_8.Styles.merge(a, { "color-c-p30": true });
const inputLight = v0_8.Styles.merge(input, { "color-c-n5": true });
const textareaLight = v0_8.Styles.merge(textarea, { "color-c-n5": true });
const buttonLight = v0_8.Styles.merge(button, { "color-c-n100": true });
const h1Light = v0_8.Styles.merge(h1, { "color-c-n5": true });
const h2Light = v0_8.Styles.merge(h2, { "color-c-n5": true });
const h3Light = v0_8.Styles.merge(h3, { "color-c-n5": true });
const bodyLight = v0_8.Styles.merge(body, { "color-c-n5": true });
const pLight = v0_8.Styles.merge(p, { "color-c-n60": true });
const preLight = v0_8.Styles.merge(pre, { "color-c-n35": true });
const orderedListLight = v0_8.Styles.merge(orderedList, {
"color-c-n35": true,
});
const unorderedListLight = v0_8.Styles.merge(unorderedList, {
"color-c-n35": true,
});
const listItemLight = v0_8.Styles.merge(listItem, {
"color-c-n35": true,
});
export const theme: v0_8.Types.Theme = {
additionalStyles: {
Card: {
"min-width": "320px",
},
Button: {
"--n-60": "var(--n-100)",
},
Image: {
"max-width": "120px",
"max-height": "120px",
marginLeft: "auto",
marginRight: "auto",
},
},
components: {
AudioPlayer: {},
Button: {
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-5": true,
"layout-pr-5": true,
"border-br-2": true,
"border-bw-0": true,
"border-bs-s": true,
"color-bgc-p30": true,
"color-c-n100": true,
"behavior-ho-70": true,
},
Card: {
"border-br-4": true,
"color-bgc-p100": true,
"color-bc-n90": true,
"border-bw-1": true,
"border-bs-s": true,
"layout-pt-10": true,
"layout-pb-10": true,
"layout-pl-4": true,
"layout-pr-4": true,
},
CheckBox: {
element: {
"layout-m-0": true,
"layout-mr-2": true,
"layout-p-2": true,
"border-br-12": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true,
"color-c-p30": true,
},
label: {
"color-c-p30": true,
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-flx-1": true,
"typography-sz-ll": true,
},
container: {
"layout-dsp-iflex": true,
"layout-al-c": true,
},
},
Column: {},
DateTimeInput: {
container: {},
label: {},
element: {
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-br-12": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true,
},
},
Divider: {
"color-bgc-n90": true,
"layout-mt-6": true,
"layout-mb-6": true,
},
Image: {
all: {
"border-br-50pc": true,
"layout-el-cv": true,
"layout-w-100": true,
"layout-h-100": true,
"layout-dsp-flexhor": true,
"layout-al-c": true,
"layout-sp-c": true,
"layout-mb-3": true,
},
avatar: {},
header: {},
icon: {},
largeFeature: {},
mediumFeature: {},
smallFeature: {},
},
Icon: {
"border-br-1": true,
"layout-p-2": true,
"color-bgc-n98": true,
"layout-dsp-flexhor": true,
"layout-al-c": true,
"layout-sp-c": true,
},
List: {
"layout-g-4": true,
"layout-p-2": true,
},
Modal: {
backdrop: { "color-bbgc-p60_20": true },
element: {
"border-br-2": true,
"color-bgc-p100": true,
"layout-p-4": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bc-p80": true,
},
},
MultipleChoice: {
container: {},
label: {},
element: {},
},
Row: {
"layout-g-4": true,
"layout-mb-3": true,
},
Slider: {
container: {},
label: {},
element: {},
},
Tabs: {
container: {},
controls: { all: {}, selected: {} },
element: {},
},
Text: {
all: {
"layout-w-100": true,
"layout-g-2": true,
"color-c-p30": true,
},
h1: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-2": true,
"layout-p-0": true,
"typography-sz-tl": true,
},
h2: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-2": true,
"layout-p-0": true,
"typography-sz-tl": true,
},
h3: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-0": true,
"layout-p-0": true,
"typography-sz-ts": true,
},
h4: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-0": true,
"layout-p-0": true,
"typography-sz-bl": true,
},
h5: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-0": true,
"layout-p-0": true,
"color-c-n30": true,
"typography-sz-bm": true,
"layout-mb-1": true,
},
body: {},
caption: {},
},
TextField: {
container: {
"typography-sz-bm": true,
"layout-w-100": true,
"layout-g-2": true,
"layout-dsp-flexhor": true,
"layout-al-c": true,
},
label: {
"layout-flx-0": true,
},
element: {
"typography-sz-bm": true,
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-br-12": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true,
"color-c-p30": true,
},
},
Video: {
"border-br-5": true,
"layout-el-cv": true,
},
},
elements: {
a: aLight,
audio,
body: bodyLight,
button: buttonLight,
h1: h1Light,
h2: h2Light,
h3: h3Light,
h4: {},
h5: {},
iframe,
input: inputLight,
p: pLight,
pre: preLight,
textarea: textareaLight,
video,
},
markdown: {
p: [...Object.keys(pLight)],
h1: [...Object.keys(h1Light)],
h2: [...Object.keys(h2Light)],
h3: [...Object.keys(h3Light)],
h4: [],
h5: [],
ul: [...Object.keys(unorderedListLight)],
ol: [...Object.keys(orderedListLight)],
li: [...Object.keys(listItemLight)],
a: [...Object.keys(aLight)],
strong: [],
em: [],
},
};

View File

@@ -0,0 +1,30 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"incremental": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"preserveWatchOutput": true,
"sourceMap": true,
"target": "es2022",
"module": "es2022",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"useDefineForClassFields": false,
"rootDir": ".",
"outDir": "dist",
"tsBuildInfoFile": "dist/.tsbuildinfo",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"references": [{ "path": "../../../../renderers/lit" }]
}

View File

@@ -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 { HTMLTemplateResult } from "lit";
export enum SnackType {
NONE = "none",
INFORMATION = "information",
WARNING = "warning",
ERROR = "error",
PENDING = "pending",
}
export type SnackbarUUID = ReturnType<typeof globalThis.crypto.randomUUID>;
export type SnackbarAction = {
title: string;
action: string;
value?: HTMLTemplateResult | string;
callback?: () => void;
};
export type SnackbarMessage = {
id: SnackbarUUID;
type: SnackType;
persistent: boolean;
message: string | HTMLTemplateResult;
actions?: SnackbarAction[];
};

View File

@@ -0,0 +1,167 @@
# A2UI custom component integration guide
This guide details how to create, register, and use a custom component in the A2UI client.
## Create the component
Create a new Lit component file in `lib/src/0.8/ui/custom-components/`.
Example: `my-component.ts`
```typescript
import { html, css } from "lit";
import { property } from "lit/decorators.js";
import { Root } from "../root.js";
export class MyComponent extends Root {
@property() accessor myProp: string = "Default";
static styles = [
...Root.styles, // Inherit base styles
css`
:host {
display: block;
padding: 16px;
border: 1px solid #ccc;
}
`,
];
render() {
return html`
<div>
<h2>My Custom Component</h2>
<p>Prop value: ${this.myProp}</p>
</div>
`;
}
}
```
## Register the component
Update `lib/src/0.8/ui/custom-components/index.ts` to register your new component.
You must pass the desired tag name as the third argument.
```typescript
import { componentRegistry } from "../component-registry.js";
import { MyComponent } from "./my-component.js"; // Import your component
export function registerCustomComponents() {
// Register with explicit tag name
componentRegistry.register("MyComponent", MyComponent, "my-component");
}
export { MyComponent }; // Export for type usage if needed
```
## Define the schema (server-side)
Create a JSON schema for your component properties. This will be used by the server to validate messages.
Example: `lib/my_component_schema.json`
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"properties": {
"type": { "const": "object" },
"properties": {
"type": "object",
"additionalProperties": false,
"properties": {
"myProp": {
"type": "string",
"description": "A sample property."
}
},
"required": ["myProp"]
}
},
"required": ["type", "properties"]
}
```
## Use in client application
In your client application (e.g., `contact` sample), ensure you import and call the registration function.
```typescript
import { registerCustomComponents } from "@a2ui/lit/ui";
// Call this once at startup
registerCustomComponents();
```
## Overriding standard components
You can replace standard A2UI components (like `TextField`, `Video`, `Button`) with your own custom implementations.
### Steps to override
1. **Create your component** extending `Root` (just like a custom component).
2. **Ensure it accepts the standard properties** for that component type (e.g., `label` and `text` for `TextField`).
3. **Register it** using the **standard type name** (e.g., `"TextField"`).
```typescript
// 1. Define your override
class MyPremiumTextField extends Root {
@property() accessor label = "";
@property() accessor text = "";
static styles = [
...Root.styles,
css`
/* your premium styles */
`,
];
render() {
return html`
<div class="premium-field">
<label>${this.label}</label>
<input .value="${this.text}" />
</div>
`;
}
}
// 2. Register with the STANDARD type name
import { componentRegistry } from "@a2ui/lit/ui";
componentRegistry.register(
"TextField",
MyPremiumTextField,
"my-premium-textfield"
);
```
**Result:**
When the server sends a `TextField` component, the client will now render `<my-premium-textfield>` instead of the default `<a2ui-textfield>`.
## Verify
You can verify the component by creating a simple HTML test file or by sending a server message with the new component type.
**Server message example:**
```json
{
"surfaceId": "main",
"component": {
"type": "MyComponent",
"id": "comp-1",
"properties": {
"myProp": "Hello World"
}
}
}
```
## Troubleshooting
- **`NotSupportedError`**: If you see "constructor has already been used", ensure you **removed** the `@customElement` decorator from your component class.
- **Component not rendering**: Check if `registerCustomComponents()` is actually called. Verify the tag name in the DOM matches what you registered (e.g., `<my-component>` vs `<a2ui-custom-mycomponent>`).
- **Styles missing**: Ensure `static styles` includes `...Root.styles`.

View File

@@ -0,0 +1,160 @@
/*
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 { 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 { map } from 'lit/directives/map.js';
// Use aliases for convenience
const StateEvent = v0_8.Events.StateEvent;
type Action = v0_8.Types.Action;
export interface OrgChartNode {
title: string;
name: string;
}
@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`
:host {
display: block;
padding: 16px;
font-family: 'Roboto', sans-serif;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.node {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 24px;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
min-width: 200px;
position: relative;
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.node:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.node:focus {
outline: 2px solid #1a73e8;
outline-offset: 2px;
}
.node.current {
background: #e8f0fe;
border-color: #1a73e8;
border-width: 2px;
}
.title {
font-size: 0.85rem;
color: #5f6368;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.name {
font-size: 1.1rem;
font-weight: 500;
color: #202124;
}
.arrow {
color: #9aa0a6;
font-size: 24px;
line-height: 1;
}
`];
render() {
if (!this.chain || this.chain.length === 0) {
return html`<div class="empty">No hierarchy data</div>`;
}
return html`
<div class="container">
${map(this.chain, (node, index) => {
const isLast = index === this.chain.length - 1;
return html`
<button
class="node ${isLast ? 'current' : ''}"
@click=${() => this.handleNodeClick(node)}
aria-label="Select ${node.name} (${node.title})"
>
<span class="title">${node.title}</span>
<span class="name">${node.name}</span>
</button>
${!isLast ? html`<div class="arrow">↓</div>` : ''}
`;
})}
</div>
`;
}
private handleNodeClick(node: OrgChartNode) {
if (!this.action) return;
// Create a new action with the node's context merged in
const newContext = [
...(this.action.context || []),
{
key: 'clickedNodeTitle',
value: { literalString: node.title }
},
{
key: 'clickedNodeName',
value: { literalString: node.name }
}
];
const actionWithContext: Action = {
...this.action,
context: newContext as Action['context']
};
const evt = new StateEvent<"a2ui.action">({
eventType: "a2ui.action",
action: actionWithContext,
dataContextPath: this.dataContextPath,
sourceComponentId: this.id,
sourceComponent: this.component,
});
this.dispatchEvent(evt);
}
}

View File

@@ -0,0 +1,100 @@
/*
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 { Root } from '@a2ui/lit/ui';
import { html, css } from 'lit';
import { property } from 'lit/decorators.js';
export class PremiumTextField extends Root {
@property() accessor label = '';
@property() accessor text = '';
static styles = [
...Root.styles,
css`
:host {
display: block;
padding: 16px;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
border: 1px solid #e0e0e0;
transition: all 0.2s ease;
font-family: 'Inter', sans-serif;
}
:host(:hover) {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
}
.input-container {
position: relative;
margin-top: 8px;
}
input {
width: 100%;
padding: 12px 16px;
font-size: 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
outline: none;
transition: border-color 0.2s;
box-sizing: border-box;
background: #fafafa;
}
input:focus {
border-color: #6200ee;
background: #fff;
}
label {
display: block;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.hint {
margin-top: 8px;
font-size: 12px;
color: #666;
display: flex;
align-items: center;
gap: 4px;
}
.badge {
background: #6200ee;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: bold;
text-transform: uppercase;
}
`
];
render() {
return html`
<label>${this.label}</label>
<div class="input-container">
<input type="text" .value="${this.text}" placeholder="Type here...">
</div>
<div class="hint">
<span class="badge">Custom</span>
<span>This is a premium override of the standard TextField.</span>
</div>
`;
}
}

View File

@@ -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 { componentRegistry } from "@a2ui/lit/ui";
import { OrgChart } from "./org-chart.js";
import { PremiumTextField } from "./premium-text-field.js";
export function registerContactComponents() {
// Register OrgChart
componentRegistry.register("OrgChart", OrgChart, "org-chart");
// Register PremiumTextField as an override for TextField
componentRegistry.register(
"TextField",
PremiumTextField,
"premium-text-field"
);
console.log("Registered Contact App Custom Components");
}

View File

@@ -0,0 +1,30 @@
# Contact sample verification tests
This directory contains tests to verify custom component integration specifically within the `contact` sample application environment.
## How to run
These tests run via the Vite development server used by the contact sample.
### 1. Start the dev server
From the `web/lit/samples/contact` directory, run:
```bash
npm run dev
```
### 2. Access the tests
Open your browser and navigate to the local server (usually port 5173):
- **Component override test**:
[http://localhost:5173/ui/custom-components/test/override-test.html](http://localhost:5173/ui/custom-components/test/override-test.html)
*Verifies that a standard component (TextField) can be overridden by a custom implementation.*
- **Hierarchy graph integration test**:
[http://localhost:5173/ui/custom-components/test/hierarchy-test.html](http://localhost:5173/ui/custom-components/test/hierarchy-test.html)
*Verifies that the HierarchyGraph component renders correctly within the contact app's build setup.*
## Files
- `override-test.html` & `override-test.ts`: Implements and tests a custom `TextField` override.
- `hierarchy-test.html`: Tests the `HierarchyGraph` component.

View File

@@ -0,0 +1,86 @@
<!--
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.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A2UI Org Chart Test (Contact Sample)</title>
<style>
body {
font-family: sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container {
margin-top: 20px;
display: flex;
justify-content: center;
}
</style>
<script type="module">
import { registerContactComponents } from '../register-components.js';
// Register custom components
registerContactComponents();
console.log('Custom components registered in Contact Sample');
</script>
</head>
<body>
<h1>A2UI Org Chart Test (Contact Sample)</h1>
<div class="container">
<org-chart id="graph"></org-chart>
</div>
<script>
customElements.whenDefined('org-chart').then(() => {
const graph = document.getElementById('graph');
graph.chain = [
{ title: 'CEO', name: 'Alice Johnson' },
{ title: 'SVP', name: 'Bob Smith' },
{ title: 'VP', name: 'Charlie Brown' },
{ title: 'Director', name: 'Diana Prince' },
{ title: 'Software Engineer', name: 'Evan Wright' }
];
graph.action = {
name: 'showContactCard',
context: []
};
graph.addEventListener('a2uiaction', (e) => {
const status = document.createElement('div');
const clickedNodeTitle = e.detail.action.context.find(c => c.key === 'clickedNodeTitle')?.value.literalString;
const clickedNodeName = e.detail.action.context.find(c => c.key === 'clickedNodeName')?.value.literalString;
status.textContent = `Clicked: ${clickedNodeTitle} - ${clickedNodeName}`;
status.style.marginTop = '20px';
status.style.padding = '10px';
status.style.background = '#e0f7fa';
status.style.border = '1px solid #006064';
status.id = 'action-status';
document.body.appendChild(status);
});
});
</script>
</body>
</html>

View File

@@ -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.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>A2UI Component Override Test</title>
<style>
body {
padding: 20px;
font-family: sans-serif;
}
.container {
margin-top: 20px;
border: 1px dashed #ccc;
padding: 10px;
}
</style>
<script type="module" src="./override-test.ts"></script>
</head>
<body>
<h1>Component Override Test</h1>
<div id="app" class="container"></div>
</body>
</html>

View File

@@ -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 { componentRegistry, Root } from "@a2ui/lit/ui";
import { html, css } from "lit";
import { property } from "lit/decorators.js";
// 1. Define the override
import { PremiumTextField } from "../premium-text-field.js";
// 2. Register it as "TextField"
componentRegistry.register("TextField", PremiumTextField, "premium-text-field");
console.log("Registered PremiumTextField override");
// 3. Render a standard TextField component node
const container = document.getElementById("app");
if (container) {
const root = document.createElement("a2ui-root") as Root;
const textFieldComponent = {
type: "TextField",
id: "tf-1",
properties: {
label: "Enter your name",
text: "John Doe",
},
};
// Root renders its *children*, so we must pass the component as a child.
root.childComponents = [textFieldComponent];
root.enableCustomElements = true; // Enable the feature
container.appendChild(root);
}

View File

@@ -0,0 +1,299 @@
/*
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, html, css, nothing, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import { SnackbarMessage, SnackbarUUID, SnackType } from "../types/types";
import { repeat } from "lit/directives/repeat.js";
import { SnackbarActionEvent } from "../events/events";
import { classMap } from "lit/directives/class-map.js";
import { v0_8 } from "@a2ui/lit";
const DEFAULT_TIMEOUT = 8000;
@customElement("ui-snackbar")
export class Snackbar extends LitElement {
@property({ reflect: true, type: Boolean })
accessor active = false;
@property({ reflect: true, type: Boolean })
accessor error = false;
@property()
accessor timeout = DEFAULT_TIMEOUT;
#messages: SnackbarMessage[] = [];
#timeout = 0;
static styles = [
unsafeCSS(v0_8.Styles.structuralStyles),
css`
:host {
--text-color: var(--n-0);
--bb-body-medium: 16px;
--bb-body-line-height-medium: 24px;
display: flex;
align-items: center;
position: fixed;
bottom: var(--bb-grid-size-7);
left: 50%;
translate: -50% 0;
opacity: 0;
pointer-events: none;
border-radius: var(--bb-grid-size-2);
background: var(--n-90);
padding: var(--bb-grid-size-3) var(--bb-grid-size-6);
width: 60svw;
max-width: 720px;
z-index: 1800;
scrollbar-width: none;
overflow-x: scroll;
font: 400 var(--bb-body-medium) / var(--bb-body-line-height-medium)
var(--bb-font-family);
}
:host([active]) {
transition: opacity 0.3s cubic-bezier(0, 0, 0.3, 1) 0.2s;
opacity: 1;
pointer-events: auto;
}
:host([error]) {
background: var(--e-90);
--text-color: var(--e-40);
}
.g-icon {
flex: 0 0 auto;
color: var(--text-color);
margin-right: var(--bb-grid-size-4);
&.rotate {
animation: 1s linear 0s infinite normal forwards running rotate;
}
}
#messages {
color: var(--text-color);
flex: 1 1 auto;
margin-right: var(--bb-grid-size-11);
a,
a:visited {
color: var(--bb-ui-600);
text-decoration: none;
&:hover {
color: var(--bb-ui-500);
text-decoration: underline;
}
}
}
#actions {
flex: 0 1 auto;
width: fit-content;
margin-right: var(--bb-grid-size-3);
& button {
font: 500 var(--bb-body-medium) / var(--bb-body-line-height-medium)
var(--bb-font-family);
padding: 0;
background: transparent;
border: none;
margin: 0 var(--bb-grid-size-4);
color: var(--text-color);
opacity: 0.7;
transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);
&:not([disabled]) {
cursor: pointer;
&:hover,
&:focus {
opacity: 1;
}
}
}
}
#close {
display: flex;
align-items: center;
padding: 0;
color: var(--text-color);
background: transparent;
border: none;
margin: 0 0 0 var(--bb-grid-size-2);
opacity: 0.7;
transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);
.g-icon {
margin-right: 0;
}
&:not([disabled]) {
cursor: pointer;
&:hover,
&:focus {
opacity: 1;
}
}
}
@keyframes rotate {
from {
rotate: 0deg;
}
to {
rotate: 360deg;
}
}
`,
];
show(message: SnackbarMessage, replaceAll = false) {
const existingMessage = this.#messages.findIndex(
(msg) => msg.id === message.id
);
if (existingMessage === -1) {
if (replaceAll) {
this.#messages.length = 0;
}
this.#messages.push(message);
} else {
this.#messages[existingMessage] = message;
}
window.clearTimeout(this.#timeout);
if (!this.#messages.every((msg) => msg.persistent)) {
this.#timeout = window.setTimeout(() => {
this.hide();
}, this.timeout);
}
this.error = this.#messages.some((msg) => msg.type === SnackType.ERROR);
this.active = true;
this.requestUpdate();
return message.id;
}
hide(id?: SnackbarUUID) {
if (id) {
const idx = this.#messages.findIndex((msg) => msg.id === id);
if (idx !== -1) {
this.#messages.splice(idx, 1);
}
} else {
this.#messages.length = 0;
}
this.active = this.#messages.length !== 0;
this.updateComplete.then((avoidedUpdate) => {
if (!avoidedUpdate) {
return;
}
this.requestUpdate();
});
}
render() {
let rotate = false;
let icon = "";
for (let i = this.#messages.length - 1; i >= 0; i--) {
if (
!this.#messages[i].type ||
this.#messages[i].type === SnackType.NONE
) {
continue;
}
icon = this.#messages[i].type;
if (this.#messages[i].type === SnackType.PENDING) {
icon = "progress_activity";
rotate = true;
}
break;
}
return html` ${icon
? html`<span
class=${classMap({
"g-icon": true,
round: true,
filled: true,
rotate,
})}
>${icon}</span
>`
: nothing}
<div id="messages">
${repeat(
this.#messages,
(message) => message.id,
(message) => {
return html`<div>${message.message}</div>`;
}
)}
</div>
<div id="actions">
${repeat(
this.#messages,
(message) => message.id,
(message) => {
if (!message.actions) {
return nothing;
}
return html`${repeat(
message.actions,
(action) => action.value,
(action) => {
return html`<button
@click=${() => {
this.hide();
this.dispatchEvent(
new SnackbarActionEvent(
action.action,
action.value,
action.callback
)
);
}}
>
${action.title}
</button>`;
}
)}`;
}
)}
</div>
<button
id="close"
@click=${() => {
this.hide();
this.dispatchEvent(new SnackbarActionEvent("dismiss"));
}}
>
<span class="g-icon">close</span>
</button>`;
}
}

View File

@@ -0,0 +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.
*/
export { Snackbar } from "./snackbar";

View File

@@ -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 { config } from "dotenv";
import { UserConfig } from "vite";
import * as Middleware from "./middleware";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default async () => {
config();
const entry: Record<string, string> = {
contact: resolve(__dirname, "index.html"),
};
return {
plugins: [Middleware.A2AMiddleware.plugin()],
build: {
rollupOptions: {
input: entry,
},
target: "esnext",
},
define: {},
resolve: {
dedupe: ["lit"],
},
} satisfies UserConfig;
};