import { html, css, LitElement } from "lit"; import { repeat } from "lit/directives/repeat.js"; import { ContextProvider } from "@lit/context"; import { v0_8 } from "@a2ui/lit"; import "@a2ui/lit/ui"; import { themeContext } from "@clawdis/a2ui-theme-context"; const empty = Object.freeze({}); const emptyClasses = () => ({}); const textHintStyles = () => ({ h1: {}, h2: {}, h3: {}, h4: {}, h5: {}, body: {}, caption: {} }); const clawdisTheme = { components: { AudioPlayer: emptyClasses(), Button: emptyClasses(), Card: emptyClasses(), Column: emptyClasses(), CheckBox: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() }, DateTimeInput: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() }, Divider: emptyClasses(), Image: { all: emptyClasses(), icon: emptyClasses(), avatar: emptyClasses(), smallFeature: emptyClasses(), mediumFeature: emptyClasses(), largeFeature: emptyClasses(), header: emptyClasses(), }, Icon: emptyClasses(), List: emptyClasses(), Modal: { backdrop: emptyClasses(), element: emptyClasses() }, MultipleChoice: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() }, Row: emptyClasses(), Slider: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() }, Tabs: { container: emptyClasses(), element: emptyClasses(), controls: { all: emptyClasses(), selected: emptyClasses() } }, Text: { all: emptyClasses(), h1: emptyClasses(), h2: emptyClasses(), h3: emptyClasses(), h4: emptyClasses(), h5: emptyClasses(), caption: emptyClasses(), body: emptyClasses(), }, TextField: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() }, Video: emptyClasses(), }, elements: { a: emptyClasses(), audio: emptyClasses(), body: emptyClasses(), button: emptyClasses(), h1: emptyClasses(), h2: emptyClasses(), h3: emptyClasses(), h4: emptyClasses(), h5: emptyClasses(), iframe: emptyClasses(), input: emptyClasses(), p: emptyClasses(), pre: emptyClasses(), textarea: emptyClasses(), video: emptyClasses(), }, markdown: { p: [], h1: [], h2: [], h3: [], h4: [], h5: [], ul: [], ol: [], li: [], a: [], strong: [], em: [], }, additionalStyles: { Card: { background: "linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03))", border: "1px solid rgba(255,255,255,.09)", borderRadius: "14px", padding: "14px", boxShadow: "0 10px 30px rgba(0,0,0,.35)", }, Column: { gap: "10px" }, Row: { gap: "10px", alignItems: "center" }, Divider: { opacity: "0.25" }, Button: { background: "linear-gradient(135deg, #22c55e 0%, #06b6d4 100%)", border: "0", borderRadius: "12px", padding: "10px 14px", color: "#071016", fontWeight: "650", cursor: "pointer", boxShadow: "0 10px 25px rgba(6, 182, 212, 0.18)", }, Text: { ...textHintStyles(), h1: { fontSize: "20px", fontWeight: "750", margin: "0 0 6px 0" }, h2: { fontSize: "16px", fontWeight: "700", margin: "0 0 6px 0" }, body: { fontSize: "13px", lineHeight: "1.4" }, caption: { opacity: "0.8" }, }, TextField: { display: "grid", gap: "6px" }, Image: { borderRadius: "12px" }, }, }; class ClawdisA2UIHost extends LitElement { static properties = { surfaces: { state: true }, }; #processor = v0_8.Data.createSignalA2uiMessageProcessor(); #themeProvider = new ContextProvider(this, { context: themeContext, initialValue: clawdisTheme, }); surfaces = []; static styles = css` :host { display: block; height: 100%; box-sizing: border-box; padding: 12px; } #surfaces { display: grid; grid-template-columns: 1fr; gap: 12px; height: 100%; overflow: auto; padding-bottom: 24px; } `; connectedCallback() { super.connectedCallback(); globalThis.clawdisA2UI = { applyMessages: (messages) => this.applyMessages(messages), reset: () => this.reset(), getSurfaces: () => Array.from(this.#processor.getSurfaces().keys()), }; this.addEventListener("a2uiaction", (evt) => this.#handleA2UIAction(evt)); this.#syncSurfaces(); } #handleA2UIAction(evt) { const payload = evt?.detail ?? evt?.payload ?? null; if (!payload || payload.eventType !== "a2ui.action") { return; } const action = payload.action; const name = action?.name; if (!name) { return; } const sourceComponentId = payload.sourceComponentId ?? ""; const surfaces = this.#processor.getSurfaces(); let surfaceId = null; let sourceNode = null; for (const [sid, surface] of surfaces.entries()) { const node = surface?.components?.get?.(sourceComponentId) ?? null; if (node) { surfaceId = sid; sourceNode = node; break; } } const context = {}; const ctxItems = Array.isArray(action?.context) ? action.context : []; for (const item of ctxItems) { const key = item?.key; const value = item?.value ?? null; if (!key || !value) continue; if (typeof value.path === "string") { const resolved = sourceNode ? this.#processor.getData(sourceNode, value.path, surfaceId ?? undefined) : null; context[key] = resolved; continue; } if (Object.prototype.hasOwnProperty.call(value, "literalString")) { context[key] = value.literalString ?? ""; continue; } if (Object.prototype.hasOwnProperty.call(value, "literalNumber")) { context[key] = value.literalNumber ?? 0; continue; } if (Object.prototype.hasOwnProperty.call(value, "literalBoolean")) { context[key] = value.literalBoolean ?? false; continue; } } const userAction = { name, surfaceId: surfaceId ?? "main", sourceComponentId, timestamp: new Date().toISOString(), ...(Object.keys(context).length ? { context } : {}), }; globalThis.__clawdisLastA2UIAction = userAction; const handler = globalThis.webkit?.messageHandlers?.clawdisCanvasA2UIAction; if (handler?.postMessage) { handler.postMessage({ userAction }); } } applyMessages(messages) { if (!Array.isArray(messages)) { throw new Error("A2UI: expected messages array"); } this.#processor.processMessages(messages); this.#syncSurfaces(); this.requestUpdate(); return { ok: true, surfaces: this.surfaces.map(([id]) => id) }; } reset() { this.#processor.clearSurfaces(); this.#syncSurfaces(); this.requestUpdate(); return { ok: true }; } #syncSurfaces() { this.surfaces = Array.from(this.#processor.getSurfaces().entries()); } render() { if (this.surfaces.length === 0) { return html`