Skip to content
Open
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.

4 changes: 4 additions & 0 deletions renderers/lit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
"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
1 change: 1 addition & 0 deletions renderers/lit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
*/

export * as v0_8 from "./0.8/index.js";
export * as v0_9 from "./v0_9/index.js";
42 changes: 42 additions & 0 deletions renderers/lit/src/v0_9/adapter.ts
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,
};
}
11 changes: 11 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,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>`;
});
17 changes: 17 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,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>
`;
});
7 changes: 7 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,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>`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 styles block 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).

});
12 changes: 12 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/CheckBox.ts
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>
`;
});
32 changes: 32 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/ChoicePicker.ts
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`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type for the opt parameter in the map function is any. This weakens type safety. The type for the options should be inferred from the ChoicePickerApi.schema. Please define a specific type for the options and use it here to leverage TypeScript's static analysis.

<label>
<input type=${isMulti ? "checkbox" : "radio"} .checked=${selected.includes(opt.value)} @change=${() => toggle(opt.value)} />
${opt.label}
</label>
`)}
</div>
</div>
`;
});
11 changes: 11 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Column.ts
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>`;
});
13 changes: 13 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/DateTimeInput.ts
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>
`;
});
9 changes: 9 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Divider.ts
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;" />`;
});
8 changes: 8 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Icon.ts
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>`;
});
9 changes: 9 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Image.ts
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)} />`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using string concatenation for CSS classes is less robust than using the classMap directive from Lit. classMap handles conditional classes more cleanly and avoids potential issues with extra spaces or empty class attributes.

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)} />`;
});

});
11 changes: 11 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/List.ts
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>`;
});
33 changes: 33 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Modal.ts
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Casting this to any when creating the A2uiController bypasses TypeScript's type safety. This is often a symptom of the component's type not being fully inferred at the point of property initialization. To improve type safety, consider initializing the controller in the constructor after super() has been called, where this is fully typed. This same issue is also present in A2uiLitTabs.

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>`
};
11 changes: 11 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Row.ts
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>`;
});
13 changes: 13 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Slider.ts
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>
`;
});
39 changes: 39 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Tabs.ts
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>`
};
18 changes: 18 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Text.ts
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The tagMap and tag variables are declared, but tag is never used. The component's logic relies on a switch statement instead, making the tagMap and tag declarations redundant. You can remove them to simplify the code.

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>`;
}
});
Loading
Loading