-
Notifications
You must be signed in to change notification settings - Fork 1k
feat(lit): Initial Draft of v0.9 Lit Renderer and Local Gallery #869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,3 +15,4 @@ | |
| */ | ||
|
|
||
| export * as v0_8 from "./0.8/index.js"; | ||
| export * as v0_9 from "./v0_9/index.js"; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { ReactiveController, LitElement } from "lit"; | ||
| import { GenericBinder, ComponentContext, ComponentApi, ResolveA2uiProps } from "@a2ui/web_core/v0_9"; | ||
| import { ChildBuilder, LitComponentImplementation } from "./types.js"; | ||
|
|
||
| export class A2uiController<T> implements ReactiveController { | ||
| public props: ResolveA2uiProps<T>; | ||
| private binder: GenericBinder<T>; | ||
| private subscription?: { unsubscribe: () => void }; | ||
|
|
||
| constructor(private host: LitElement & { context: ComponentContext }, api: ComponentApi) { | ||
| this.binder = new GenericBinder(this.host.context, api.schema); | ||
| this.props = this.binder.snapshot as ResolveA2uiProps<T>; | ||
| this.host.addController(this); | ||
| } | ||
|
|
||
| hostConnected() { | ||
| this.subscription = this.binder.subscribe((newProps) => { | ||
| this.props = newProps as ResolveA2uiProps<T>; | ||
| this.host.requestUpdate(); | ||
| }); | ||
| } | ||
|
|
||
| hostDisconnected() { | ||
| this.subscription?.unsubscribe(); | ||
| } | ||
|
|
||
| dispose() { | ||
| this.binder.dispose(); | ||
| } | ||
| } | ||
|
|
||
| import { z } from "zod"; | ||
|
|
||
| export function createLitComponent<T = any>( | ||
| api: ComponentApi, | ||
| renderFn: (args: { props: ResolveA2uiProps<T>; buildChild: ChildBuilder; context: ComponentContext }) => unknown | ||
| ): LitComponentImplementation<T> { | ||
| return { | ||
| ...api, | ||
| render: renderFn, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { html } from "lit"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { AudioPlayerApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiAudioPlayer = createLitComponent(AudioPlayerApi, ({ props }) => { | ||
| return html` | ||
| <div class="a2ui-audioplayer"> | ||
| ${props.description ? html`<p>${props.description}</p>` : ""} | ||
| <audio src=${props.url} controls></audio> | ||
| </div>`; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { html } from "lit"; | ||
| import { classMap } from "lit/directives/class-map.js"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { ButtonApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiButton = createLitComponent(ButtonApi, ({ props, buildChild }) => { | ||
| const isDisabled = props.isValid === false; | ||
| return html` | ||
| <button | ||
| class=${classMap({"a2ui-button": true, ["a2ui-button-" + (props.variant || "default")]: true})} | ||
| @click=${() => !isDisabled && props.action && props.action()} | ||
| ?disabled=${isDisabled} | ||
| > | ||
| ${props.child ? buildChild(props.child) : ""} | ||
| </button> | ||
| `; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { html } from "lit"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { CardApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiCard = createLitComponent(CardApi, ({ props, buildChild }) => { | ||
| return html`<div class="a2ui-card" style="border: 1px solid #ccc; border-radius: 8px; padding: 16px;">${props.child ? buildChild(props.child) : ""}</div>`; | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { html } from "lit"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { CheckBoxApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiCheckBox = createLitComponent(CheckBoxApi, ({ props }) => { | ||
| return html` | ||
| <label class="a2ui-checkbox"> | ||
| <input type="checkbox" .checked=${props.value || false} @change=${(e: Event) => props.setValue?.((e.target as HTMLInputElement).checked)} /> | ||
| ${props.label} | ||
| </label> | ||
| `; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { html } from "lit"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { ChoicePickerApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiChoicePicker = createLitComponent(ChoicePickerApi, ({ props }) => { | ||
| const selected = Array.isArray(props.value) ? props.value : []; | ||
| const isMulti = props.variant === "multipleSelection"; | ||
|
|
||
| const toggle = (val: string) => { | ||
| if (!props.setValue) return; | ||
| if (isMulti) { | ||
| if (selected.includes(val)) props.setValue(selected.filter((v: string) => v !== val)); | ||
| else props.setValue([...selected, val]); | ||
| } else { | ||
| props.setValue([val]); | ||
| } | ||
| }; | ||
|
|
||
| return html` | ||
| <div class="a2ui-choicepicker"> | ||
| ${props.label ? html`<label>${props.label}</label>` : ""} | ||
| <div class="options"> | ||
| ${props.options?.map((opt: any) => html` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| <label> | ||
| <input type=${isMulti ? "checkbox" : "radio"} .checked=${selected.includes(opt.value)} @change=${() => toggle(opt.value)} /> | ||
| ${opt.label} | ||
| </label> | ||
| `)} | ||
| </div> | ||
| </div> | ||
| `; | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { html } from "lit"; | ||
| import { map } from "lit/directives/map.js"; | ||
| import { styleMap } from "lit/directives/style-map.js"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { ColumnApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiColumn = createLitComponent(ColumnApi, ({ props, buildChild }) => { | ||
| const children = Array.isArray(props.children) ? props.children : []; | ||
| const styles = { display: "flex", flexDirection: "column", flex: props.weight !== undefined ? String(props.weight) : "initial", gap: "8px" }; | ||
| return html`<div class="a2ui-column" style=${styleMap(styles)}>${map(children, (child: any) => typeof child === 'string' ? buildChild(child) : buildChild(child.id, child.basePath))}</div>`; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { html } from "lit"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { DateTimeInputApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiDateTimeInput = createLitComponent(DateTimeInputApi, ({ props }) => { | ||
| const type = (props.enableDate && props.enableTime) ? "datetime-local" : (props.enableDate ? "date" : "time"); | ||
| return html` | ||
| <div class="a2ui-datetime"> | ||
| ${props.label ? html`<label>${props.label}</label>` : ""} | ||
| <input type=${type} .value=${props.value || ""} @input=${(e: Event) => props.setValue?.((e.target as HTMLInputElement).value)} /> | ||
| </div> | ||
| `; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { html } from "lit"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { DividerApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiDivider = createLitComponent(DividerApi, ({ props }) => { | ||
| return props.axis === "vertical" | ||
| ? html`<div class="a2ui-divider-vertical" style="width: 1px; background: #ccc; height: 100%;"></div>` | ||
| : html`<hr class="a2ui-divider" style="border: none; border-top: 1px solid #ccc; margin: 16px 0;" />`; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { html } from "lit"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { IconApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiIcon = createLitComponent(IconApi, ({ props }) => { | ||
| const name = typeof props.name === 'string' ? props.name : (props.name as any)?.path; | ||
| return html`<span class="material-symbols-outlined a2ui-icon">${name}</span>`; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { html } from "lit"; | ||
| import { styleMap } from "lit/directives/style-map.js"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { ImageApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiImage = createLitComponent(ImageApi, ({ props }) => { | ||
| const styles = { objectFit: props.fit || "fill", width: "100%" }; | ||
| return html`<img src=${props.url} class=${"a2ui-image " + (props.variant || "")} style=${styleMap(styles)} />`; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using string concatenation for CSS classes is less robust than using the import { classMap } from "lit/directives/class-map.js";
// ...
export const A2uiImage = createLitComponent(ImageApi, ({ props }) => {
const styles = { objectFit: props.fit || "fill", width: "100%" };
const classes = {
'a2ui-image': true,
[props.variant || '']: !!props.variant
};
return html`<img src=${props.url} class=${classMap(classes)} style=${styleMap(styles)} />`;
}); |
||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { html } from "lit"; | ||
| import { map } from "lit/directives/map.js"; | ||
| import { styleMap } from "lit/directives/style-map.js"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { ListApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiList = createLitComponent(ListApi, ({ props, buildChild }) => { | ||
| const children = Array.isArray(props.children) ? props.children : []; | ||
| const styles = { display: "flex", flexDirection: props.direction === "horizontal" ? "row" : "column", overflow: "auto", gap: "8px" }; | ||
| return html`<div class="a2ui-list" style=${styleMap(styles)}>${map(children, (child: any) => typeof child === 'string' ? buildChild(child) : buildChild(child.id, child.basePath))}</div>`; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { LitElement, html } from "lit"; | ||
| import { customElement, property, query } from "lit/decorators.js"; | ||
| import { A2uiController } from "../../../adapter.js"; | ||
| import { ComponentContext } from "@a2ui/web_core/v0_9"; | ||
| import { ModalApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
| import { ChildBuilder, LitComponentImplementation } from "../../../types.js"; | ||
|
|
||
| @customElement("a2ui-lit-modal") | ||
| export class A2uiLitModal extends LitElement { | ||
| @property({ type: Object }) accessor context!: ComponentContext; | ||
| @property({ type: Function }) accessor buildChild!: ChildBuilder; | ||
| private a2ui = new A2uiController(this as any, ModalApi); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Casting For example: export class A2uiLitModal extends LitElement {
// ... properties
private a2ui: A2uiController<any>;
constructor() {
super();
this.a2ui = new A2uiController(this, ModalApi);
}
// ... rest of the class
} |
||
| @query("dialog") accessor dialog!: HTMLDialogElement; | ||
|
|
||
| render() { | ||
| const props = this.a2ui.props as any; | ||
| return html` | ||
| <div @click=${() => this.dialog?.showModal()}> | ||
| ${props.trigger ? this.buildChild(props.trigger) : ''} | ||
| </div> | ||
| <dialog class="a2ui-modal" style="border: 1px solid #ccc; border-radius: 8px; padding: 24px; min-width: 300px;"> | ||
| <form method="dialog" style="text-align: right;"><button>×</button></form> | ||
| ${props.content ? this.buildChild(props.content) : ''} | ||
| </dialog> | ||
| `; | ||
| } | ||
| } | ||
|
|
||
| export const A2uiModal: LitComponentImplementation = { | ||
| name: "Modal", | ||
| schema: ModalApi.schema, | ||
| render: ({ context, buildChild }) => html`<a2ui-lit-modal .context=${context} .buildChild=${buildChild}></a2ui-lit-modal>` | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { html } from "lit"; | ||
| import { map } from "lit/directives/map.js"; | ||
| import { styleMap } from "lit/directives/style-map.js"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { RowApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiRow = createLitComponent(RowApi, ({ props, buildChild }) => { | ||
| const children = Array.isArray(props.children) ? props.children : []; | ||
| const styles = { display: "flex", flexDirection: "row", flex: props.weight !== undefined ? String(props.weight) : "initial", gap: "8px" }; | ||
| return html`<div class="a2ui-row" style=${styleMap(styles)}>${map(children, (child: any) => typeof child === 'string' ? buildChild(child) : buildChild(child.id, child.basePath))}</div>`; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { html } from "lit"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { SliderApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiSlider = createLitComponent(SliderApi, ({ props }) => { | ||
| return html` | ||
| <div class="a2ui-slider"> | ||
| ${props.label ? html`<label>${props.label}</label>` : ""} | ||
| <input type="range" min=${props.min ?? 0} max=${props.max ?? 100} .value=${props.value?.toString() || "0"} @input=${(e: Event) => props.setValue?.(Number((e.target as HTMLInputElement).value))} /> | ||
| <span>${props.value}</span> | ||
| </div> | ||
| `; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { LitElement, html } from "lit"; | ||
| import { customElement, property, state } from "lit/decorators.js"; | ||
| import { A2uiController } from "../../../adapter.js"; | ||
| import { ComponentContext } from "@a2ui/web_core/v0_9"; | ||
| import { TabsApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
| import { ChildBuilder, LitComponentImplementation } from "../../../types.js"; | ||
|
|
||
| @customElement("a2ui-lit-tabs") | ||
| export class A2uiLitTabs extends LitElement { | ||
| @property({ type: Object }) accessor context!: ComponentContext; | ||
| @property({ type: Function }) accessor buildChild!: ChildBuilder; | ||
| private a2ui = new A2uiController(this as any, TabsApi); | ||
| @state() accessor activeIndex = 0; | ||
|
|
||
| render() { | ||
| const props = this.a2ui.props as any; | ||
| if (!props.tabs) return html``; | ||
| return html` | ||
| <div class="a2ui-tabs"> | ||
| <div class="a2ui-tab-headers" style="display:flex; gap: 8px; border-bottom: 1px solid #ccc; margin-bottom: 16px;"> | ||
| ${props.tabs.map((tab: any, i: number) => html` | ||
| <button @click=${() => this.activeIndex = i} style="padding: 8px; background: ${i === this.activeIndex ? '#eee' : 'transparent'}; border: none;"> | ||
| ${tab.title} | ||
| </button> | ||
| `)} | ||
| </div> | ||
| <div class="a2ui-tab-content"> | ||
| ${props.tabs[this.activeIndex] ? this.buildChild(props.tabs[this.activeIndex].child) : ''} | ||
| </div> | ||
| </div> | ||
| `; | ||
| } | ||
| } | ||
|
|
||
| export const A2uiTabs: LitComponentImplementation = { | ||
| name: "Tabs", | ||
| schema: TabsApi.schema, | ||
| render: ({ context, buildChild }) => html`<a2ui-lit-tabs .context=${context} .buildChild=${buildChild}></a2ui-lit-tabs>` | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { html } from "lit"; | ||
| import { createLitComponent } from "../../../adapter.js"; | ||
| import { TextApi } from "@a2ui/web_core/v0_9/basic_catalog"; | ||
|
|
||
| export const A2uiText = createLitComponent(TextApi, ({ props }) => { | ||
| const variant = props.variant ?? "body"; | ||
| const tagMap: Record<string, string> = { h1: "h1", h2: "h2", h3: "h3", h4: "h4", h5: "h5", caption: "span", body: "p" }; | ||
| const tag = tagMap[variant as string] || "p"; | ||
|
Comment on lines
+7
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| switch (variant) { | ||
| case "h1": return html`<h1>${props.text}</h1>`; | ||
| case "h2": return html`<h2>${props.text}</h2>`; | ||
| case "h3": return html`<h3>${props.text}</h3>`; | ||
| case "h4": return html`<h4>${props.text}</h4>`; | ||
| case "h5": return html`<h5>${props.text}</h5>`; | ||
| case "caption": return html`<span class="a2ui-caption">${props.text}</span>`; | ||
| default: return html`<p>${props.text}</p>`; | ||
| } | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using inline styles is generally discouraged as it makes theming and maintenance difficult, and can violate Content Security Policy (CSP) in some environments. It would be better to move these styles to a dedicated CSS file or a
static stylesblock within the component, and apply them via classes. This issue is present in several other new components as well (e.g.,Column,Row,Divider,Modal,Tabs).