Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions renderers/lit/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Unreleased

- (v0_9) Add portable Web Components for Select, Switch, Dialog, and Toast.


## 0.10.2

- (v0_9) Normalize Safari placeholder text color for `DateTimeInput` by updating CSS selectors for `.a2ui-date-time-input`.
Expand Down
7 changes: 6 additions & 1 deletion renderers/lit/src/v0_9/catalogs/basic/components/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,12 @@ export class A2uiBasicButtonElement extends BasicCatalogA2uiLitElement<typeof Bu
}
}

@customElement('a2ui-button')
export class A2uiButtonElement extends A2uiBasicButtonElement {}

export const A2uiButton = {
...ButtonApi,
tagName: 'a2ui-basic-button',
tagName: 'a2ui-button',
};


103 changes: 103 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {html, nothing, css} from 'lit';
import {customElement} from 'lit/decorators.js';
import {DialogApi} from '@a2ui/web_core/v0_9/basic_catalog';
import {BasicCatalogA2uiLitElement} from '../basic-catalog-a2ui-lit-element.js';
import {A2uiController} from '../../../a2ui-controller.js';

@customElement('a2ui-dialog')
export class A2uiDialogElement extends BasicCatalogA2uiLitElement<typeof DialogApi> {
static override styles = css`
:host {
display: block;
}
dialog {
border: var(--a2ui-border-width, 1px) solid var(--a2ui-color-border, #ccc);
border-radius: var(--a2ui-border-radius, 12px);
padding: var(--a2ui-spacing-l, 24px);
background-color: var(--a2ui-color-surface, #fff);
color: var(--a2ui-color-on-surface, #333);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
max-width: 500px;
width: 90%;
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
}
Comment on lines +40 to +43

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The dialog::backdrop pseudo-element is only rendered by browsers when the dialog is opened using the native .showModal() method. Since this component renders <dialog open> declaratively, the native backdrop will not be displayed. Consider rendering a custom backdrop element in the template or managing the dialog opening/closing imperatively using .showModal() and .close() in the Lit lifecycle.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Managed open/close imperatively using showModal() and close() in updated lifecycle to ensure native backdrop rendering.

.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--a2ui-spacing-m, 16px);
}
.dialog-title {
font-size: var(--a2ui-font-size-l, 1.25rem);
font-weight: 600;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.25rem;
cursor: pointer;
color: var(--a2ui-color-on-surface, #666);
}
`;

protected createController() {
return new A2uiController(this, DialogApi);
}

private closeDialog() {
this.dispatchEvent(
new CustomEvent('a2uiclose', {
bubbles: true,
composed: true,
}),
);
}

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

const isOpen = props.open !== false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using props.open !== false means that if props.open is undefined, isOpen will evaluate to true. However, the DialogApi schema defines open with a default of false. To respect this default and prevent the dialog from being open by default when the property is omitted, use Boolean(props.open) instead.

Suggested change
const isOpen = props.open !== false;
const isOpen = Boolean(props.open);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Used Boolean(props.open).


return html`
${isOpen
? html`
<dialog open>
<div class="dialog-header">
${props.title ? html`<h3 class="dialog-title">${props.title}</h3>` : nothing}
<button class="close-btn" @click=${this.closeDialog}>&times;</button>
</div>
<div class="dialog-body">
${props.child ? html`${this.renderNode(props.child)}` : nothing}
</div>
</dialog>
`
: nothing}
`;
}
}

export const A2uiDialog = {
...DialogApi,
tagName: 'a2ui-dialog',
};
97 changes: 97 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Select.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {html, nothing, css} from 'lit';
import {customElement} from 'lit/decorators.js';
import {SelectApi} from '@a2ui/web_core/v0_9/basic_catalog';
import {BasicCatalogA2uiLitElement} from '../basic-catalog-a2ui-lit-element.js';
import {A2uiController} from '../../../a2ui-controller.js';

@customElement('a2ui-select')
export class A2uiSelectElement extends BasicCatalogA2uiLitElement<typeof SelectApi> {
static override styles = css`
:host {
display: block;
margin: var(--a2ui-select-margin, var(--a2ui-spacing-m, 8px));
font-family: inherit;
}
.a2ui-select-container {
display: flex;
flex-direction: column;
gap: var(--a2ui-spacing-xs, 4px);
}
label {
font-size: var(--a2ui-font-size-s, 0.875rem);
color: var(--a2ui-color-on-surface, #333);
font-weight: 500;
}
select {
padding: var(--a2ui-select-padding, 8px 12px);
border: var(--a2ui-border-width, 1px) solid var(--a2ui-color-border, #ccc);
border-radius: var(--a2ui-border-radius, 6px);
background-color: var(--a2ui-color-surface, #fff);
color: var(--a2ui-color-on-surface, #333);
font-size: var(--a2ui-font-size-m, 1rem);
cursor: pointer;
}
select:focus {
outline: 2px solid var(--a2ui-color-primary, #17e);
}
`;

protected createController() {
return new A2uiController(this, SelectApi);
}

private onChange(e: Event) {
const target = e.target as HTMLSelectElement;
if (this.controller.props?.value) {
// Dispatch custom event for binding update
this.dispatchEvent(
new CustomEvent('a2uichange', {
detail: {value: target.value},
bubbles: true,
composed: true,
}),
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The if (this.controller.props?.value) check prevents the a2uichange event from firing if the initial value is empty ("") or undefined. Since we want to allow users to select an option even if no value was previously set, this check should be removed so the event is always dispatched on change.

    // Dispatch custom event for binding update
    this.dispatchEvent(
      new CustomEvent('a2uichange', {
        detail: {value: target.value},
        bubbles: true,
        composed: true,
      }),
    );

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Removed the check.

}

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

const options = props.options || [];

return html`
<div class="a2ui-select-container">
${props.label ? html`<label>${props.label}</label>` : nothing}
<select .value=${props.value || ''} @change=${this.onChange}>
${options.map(
(opt) => html`<option value=${opt.value} ?selected=${opt.value === props.value}>
${opt.label}
</option>`,
)}
</select>
</div>
`;
}
}

export const A2uiSelect = {
...SelectApi,
tagName: 'a2ui-select',
};
120 changes: 120 additions & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Switch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {html, nothing, css} from 'lit';
import {customElement} from 'lit/decorators.js';
import {SwitchApi} from '@a2ui/web_core/v0_9/basic_catalog';
import {BasicCatalogA2uiLitElement} from '../basic-catalog-a2ui-lit-element.js';
import {A2uiController} from '../../../a2ui-controller.js';

@customElement('a2ui-switch')
export class A2uiSwitchElement extends BasicCatalogA2uiLitElement<typeof SwitchApi> {
static override styles = css`
:host {
display: inline-block;
margin: var(--a2ui-switch-margin, var(--a2ui-spacing-m, 8px));
font-family: inherit;
}
.a2ui-switch-wrapper {
display: inline-flex;
align-items: center;
gap: var(--a2ui-spacing-s, 8px);
cursor: pointer;
}
.switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--a2ui-switch-off-bg, #ccc);
transition: 0.3s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: '';
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--a2ui-color-primary, #17e);
}
input:checked + .slider:before {
transform: translateX(20px);
}
.label-text {
font-size: var(--a2ui-font-size-m, 1rem);
color: var(--a2ui-color-on-surface, #333);
}
`;

protected createController() {
return new A2uiController(this, SwitchApi);
}

private onChange(e: Event) {
const target = e.target as HTMLInputElement;
this.dispatchEvent(
new CustomEvent('a2uichange', {
detail: {value: target.checked},
bubbles: true,
composed: true,
}),
);
}

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

return html`
<label class="a2ui-switch-wrapper">
<span class="switch">
<input
type="checkbox"
.checked=${Boolean(props.value)}
@change=${this.onChange}
/>
<span class="slider"></span>
</span>
${props.label ? html`<span class="label-text">${props.label}</span>` : nothing}
</label>
`;
}
}

export const A2uiSwitch = {
...SwitchApi,
tagName: 'a2ui-switch',
};
Loading
Loading