Skip to content
Merged
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
5 changes: 0 additions & 5 deletions renderers/lit/src/0.8/ui/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ export class Checkbox extends Root {
return;
}



this.processor.setData(
this.component,
this.value.path,
Expand Down Expand Up @@ -121,7 +119,6 @@ export class Checkbox extends Root {
return this.#renderField(this.value.literal);
} else if (this.value && "path" in this.value && this.value.path) {
if (!this.processor || !this.component) {

return html`(no model)`;
}

Expand All @@ -131,8 +128,6 @@ export class Checkbox extends Root {
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);



if (textValue === null) {
return html`Invalid label`;
}
Expand Down
4 changes: 2 additions & 2 deletions renderers/lit/src/0.8/ui/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ export class Icon extends Root {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
.g-icon {
font-family: 'Material Icons';
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 24px;
Expand Down
245 changes: 207 additions & 38 deletions renderers/lit/src/0.8/ui/multiple-choice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
limitations under the License.
*/


import { html, css, PropertyValues, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { Root } from "./root.js";
Expand All @@ -35,9 +36,18 @@ export class MultipleChoice extends Root {
@property()
accessor selections: Primitives.StringValue | string[] = [];

@property()
accessor variant: "checkbox" | "chips" = "checkbox";

@property({ type: Boolean })
accessor filterable = false;

@state()
accessor isOpen = false;

@state()
accessor filterText = "";

static styles = [
structuralStyles,
css`
Expand Down Expand Up @@ -95,25 +105,57 @@ export class MultipleChoice extends Root {
transform: rotate(180deg);
}

/* Dropdown List */
.options-list {
/* Dropdown Wrapper */
.dropdown-wrapper {
background: var(--md-sys-color-surface);
border: 1px solid var(--md-sys-color-outline-variant);
border-radius: 8px; /* Consistent rounding */
box-shadow: none; /* Remove shadow for inline feel, or keep subtle */
overflow-y: auto;
border-radius: 8px;
box-shadow: var(--md-sys-elevation-level2);
padding: 0;
display: none;
flex-direction: column;
margin-top: 4px; /* Small gap */
max-height: 0; /* Animate height? */
transition: max-height 0.2s ease-out;
margin-top: 4px;
max-height: 300px;
transition: opacity 0.2s ease-out;
overflow: hidden; /* contain children */
}

.options-list.open {
.dropdown-wrapper.open {
display: flex;
max-height: 300px; /* Limit height but allow scrolling */
border: 1px solid var(--md-sys-color-outline-variant); /* efficient border */
border: 1px solid var(--md-sys-color-outline-variant);
}

/* Scrollable Area for Options */
.options-scroll-container {
overflow-y: auto;
flex: 1; /* take remaining height */
display: flex;
flex-direction: column;
}

/* Filter Input */
.filter-container {
padding: 8px;
border-bottom: 1px solid var(--md-sys-color-outline-variant);
background: var(--md-sys-color-surface);
z-index: 1; /* ensure top of stack */
flex-shrink: 0; /* don't shrink */
}

.filter-input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--md-sys-color-outline);
border-radius: 4px;
font-family: inherit;
font-size: 0.9rem;
background: var(--md-sys-color-surface-container-low);
color: var(--md-sys-color-on-surface);
}

.filter-input:focus {
outline: none;
border-color: var(--md-sys-color-primary);
}

/* Option Item (Checkbox style) */
Expand Down Expand Up @@ -164,6 +206,54 @@ export class MultipleChoice extends Root {
transform: scale(1);
}

/* Chips Layout */
.chips-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 4px 0;
}

.chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
border: 1px solid var(--md-sys-color-outline);
border-radius: 16px;
cursor: pointer;
user-select: none;
background: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
transition: all 0.2s ease;
font-size: 0.9rem;
}

.chip:hover {
background: var(--md-sys-color-surface-container-high);
}

.chip.selected {
background: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
border-color: var(--md-sys-color-secondary-container);
}

.chip.selected:hover {
background: var(--md-sys-color-secondary-container-high, #e8def8);
}

.chip-icon {
display: none;
width: 18px;
height: 18px;
}

.chip.selected .chip-icon {
display: block;
fill: currentColor;
}

@keyframes fadeIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
Expand Down Expand Up @@ -191,13 +281,14 @@ export class MultipleChoice extends Root {
}

getCurrentSelections(): string[] {
if (!this.processor || !this.component) {
return Array.isArray(this.selections) ? this.selections : [];
}
if (Array.isArray(this.selections)) {
return this.selections;
}

if (!this.processor || !this.component) {
return [];
}

const selectionValue = this.processor.getData(
this.component,
this.selections.path!,
Expand All @@ -217,8 +308,82 @@ export class MultipleChoice extends Root {
this.requestUpdate();
}

#renderCheckIcon() {
return html`
<svg class="chip-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
<path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z"/>
</svg>
`;
}

#renderFilter() {
return html`
<div class="filter-container">
<input
type="text"
class="filter-input"
placeholder="Filter options..."
.value=${this.filterText}
@input=${(e: Event) => {
const target = e.target as HTMLInputElement;
this.filterText = target.value;
}}
@click=${(e: Event) => e.stopPropagation()}
/>
</div>
`;
}

render() {
const currentSelections = this.getCurrentSelections();

// Filter options
const filteredOptions = this.options.filter(option => {
if (!this.filterText) return true;
const label = extractStringValue(
option.label,
this.component,
this.processor,
this.surfaceId
);
return label.toLowerCase().includes(this.filterText.toLowerCase());
});

// Chips Layout
if (this.variant === "chips") {
return html`
<div class="container">
${this.description ? html`<div class="header-text" style="margin-bottom: 8px;">${this.description}</div>` : nothing}
${this.filterable ? this.#renderFilter() : nothing}
<div class="chips-container">
${filteredOptions.map((option) => {
const label = extractStringValue(
option.label,
this.component,
this.processor,
this.surfaceId
);
const isSelected = currentSelections.includes(option.value);
return html`
<div
class="chip ${isSelected ? "selected" : ""}"
@click=${(e: Event) => {
e.stopPropagation();
this.toggleSelection(option.value);
}}
>
${isSelected ? this.#renderCheckIcon() : nothing}
<span>${label}</span>
</div>
`;
})}
</div>
${filteredOptions.length === 0 ? html`<div style="padding: 8px; font-style: italic; color: var(--md-sys-color-outline);">No options found</div>` : nothing}
</div>
`;
}

// Default Checkbox Dropdown Layout
const count = currentSelections.length;
const headerText = count > 0 ? `${count} Selected` : (this.description ?? "Select items");

Expand All @@ -236,31 +401,35 @@ export class MultipleChoice extends Root {
</span>
</div>

<div class="options-list ${this.isOpen ? "open" : ""}">
${this.options.map((option) => {
const label = extractStringValue(
option.label,
this.component,
this.processor,
this.surfaceId
);
const isSelected = currentSelections.includes(option.value);

return html`
<div
class="option-item ${isSelected ? "selected" : ""}"
@click=${(e: Event) => {
e.stopPropagation();
this.toggleSelection(option.value);
}}
>
<div class="checkbox">
<span class="checkbox-icon">✓</span>
<div class="dropdown-wrapper ${this.isOpen ? "open" : ""}">
${this.filterable ? this.#renderFilter() : nothing}
<div class="options-scroll-container">
${filteredOptions.map((option) => {
const label = extractStringValue(
option.label,
this.component,
this.processor,
this.surfaceId
);
const isSelected = currentSelections.includes(option.value);

return html`
<div
class="option-item ${isSelected ? "selected" : ""}"
@click=${(e: Event) => {
e.stopPropagation();
this.toggleSelection(option.value);
}}
>
<div class="checkbox">
<span class="checkbox-icon">✓</span>
</div>
<span>${label}</span>
</div>
<span>${label}</span>
</div>
`;
})}
`;
})}
${filteredOptions.length === 0 ? html`<div style="padding: 16px; text-align: center; color: var(--md-sys-color-outline);">No options found</div>` : nothing}
</div>
</div>
</div>
`;
Expand Down
2 changes: 2 additions & 0 deletions renderers/lit/src/0.8/ui/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,8 @@ export class Root extends SignalWatcher(LitElement) {
.options=${node.properties.options}
.maxAllowedSelections=${node.properties.maxAllowedSelections}
.selections=${node.properties.selections}
.variant=${(node as any).properties.variant}
.filterable=${node.properties.filterable}
.enableCustomElements=${this.enableCustomElements}
></a2ui-multiplechoice>`;
}
Expand Down
2 changes: 1 addition & 1 deletion renderers/web_core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@a2ui/web_core",
"version": "0.8.0",
"version": "0.8.2",
"description": "A2UI Core Library",
"main": "./dist/src/v0_8/index.js",
"types": "./dist/src/v0_8/index.d.ts",
Expand Down
Loading
Loading