Skip to content

Commit 16af8e5

Browse files
1. Update A2UI spec to 0.8.2 and add support for filtering and types in multiselect (#604)
* fix contact sample * Fix dropdown and make it multi-choice * Add a new component_gallery sample agent and client that allows debugging on-the-wire a2UI JSON and actions * Update samples/client/lit/component_gallery/README.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * fix modal * Create new 0.8.2 version of the spec to support filtering and types for dropdown * address review fixes * Update readme * fix review comments --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 401409e commit 16af8e5

File tree

22 files changed

+1729
-1371
lines changed

22 files changed

+1729
-1371
lines changed

renderers/lit/src/0.8/ui/checkbox.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ export class Checkbox extends Root {
7171
return;
7272
}
7373

74-
75-
7674
this.processor.setData(
7775
this.component,
7876
this.value.path,
@@ -121,7 +119,6 @@ export class Checkbox extends Root {
121119
return this.#renderField(this.value.literal);
122120
} else if (this.value && "path" in this.value && this.value.path) {
123121
if (!this.processor || !this.component) {
124-
125122
return html`(no model)`;
126123
}
127124

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

134-
135-
136131
if (textValue === null) {
137132
return html`Invalid label`;
138133
}

renderers/lit/src/0.8/ui/icon.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ export class Icon extends Root {
3939
display: block;
4040
flex: var(--weight);
4141
min-height: 0;
42-
overflow: auto;
42+
4343
}
4444
4545
.g-icon {
46-
font-family: 'Material Icons';
46+
font-family: 'Material Symbols Outlined';
4747
font-weight: normal;
4848
font-style: normal;
4949
font-size: 24px;

renderers/lit/src/0.8/ui/multiple-choice.ts

Lines changed: 207 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
limitations under the License.
1515
*/
1616

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

39+
@property()
40+
accessor variant: "checkbox" | "chips" = "checkbox";
41+
42+
@property({ type: Boolean })
43+
accessor filterable = false;
44+
3845
@state()
3946
accessor isOpen = false;
4047

48+
@state()
49+
accessor filterText = "";
50+
4151
static styles = [
4252
structuralStyles,
4353
css`
@@ -95,25 +105,57 @@ export class MultipleChoice extends Root {
95105
transform: rotate(180deg);
96106
}
97107
98-
/* Dropdown List */
99-
.options-list {
108+
/* Dropdown Wrapper */
109+
.dropdown-wrapper {
100110
background: var(--md-sys-color-surface);
101111
border: 1px solid var(--md-sys-color-outline-variant);
102-
border-radius: 8px; /* Consistent rounding */
103-
box-shadow: none; /* Remove shadow for inline feel, or keep subtle */
104-
overflow-y: auto;
112+
border-radius: 8px;
113+
box-shadow: var(--md-sys-elevation-level2);
105114
padding: 0;
106115
display: none;
107116
flex-direction: column;
108-
margin-top: 4px; /* Small gap */
109-
max-height: 0; /* Animate height? */
110-
transition: max-height 0.2s ease-out;
117+
margin-top: 4px;
118+
max-height: 300px;
119+
transition: opacity 0.2s ease-out;
120+
overflow: hidden; /* contain children */
111121
}
112122
113-
.options-list.open {
123+
.dropdown-wrapper.open {
114124
display: flex;
115-
max-height: 300px; /* Limit height but allow scrolling */
116-
border: 1px solid var(--md-sys-color-outline-variant); /* efficient border */
125+
border: 1px solid var(--md-sys-color-outline-variant);
126+
}
127+
128+
/* Scrollable Area for Options */
129+
.options-scroll-container {
130+
overflow-y: auto;
131+
flex: 1; /* take remaining height */
132+
display: flex;
133+
flex-direction: column;
134+
}
135+
136+
/* Filter Input */
137+
.filter-container {
138+
padding: 8px;
139+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
140+
background: var(--md-sys-color-surface);
141+
z-index: 1; /* ensure top of stack */
142+
flex-shrink: 0; /* don't shrink */
143+
}
144+
145+
.filter-input {
146+
width: 100%;
147+
padding: 8px 12px;
148+
border: 1px solid var(--md-sys-color-outline);
149+
border-radius: 4px;
150+
font-family: inherit;
151+
font-size: 0.9rem;
152+
background: var(--md-sys-color-surface-container-low);
153+
color: var(--md-sys-color-on-surface);
154+
}
155+
156+
.filter-input:focus {
157+
outline: none;
158+
border-color: var(--md-sys-color-primary);
117159
}
118160
119161
/* Option Item (Checkbox style) */
@@ -164,6 +206,54 @@ export class MultipleChoice extends Root {
164206
transform: scale(1);
165207
}
166208
209+
/* Chips Layout */
210+
.chips-container {
211+
display: flex;
212+
flex-wrap: wrap;
213+
gap: 8px;
214+
padding: 4px 0;
215+
}
216+
217+
.chip {
218+
display: inline-flex;
219+
align-items: center;
220+
gap: 8px;
221+
padding: 6px 16px;
222+
border: 1px solid var(--md-sys-color-outline);
223+
border-radius: 16px;
224+
cursor: pointer;
225+
user-select: none;
226+
background: var(--md-sys-color-surface);
227+
color: var(--md-sys-color-on-surface);
228+
transition: all 0.2s ease;
229+
font-size: 0.9rem;
230+
}
231+
232+
.chip:hover {
233+
background: var(--md-sys-color-surface-container-high);
234+
}
235+
236+
.chip.selected {
237+
background: var(--md-sys-color-secondary-container);
238+
color: var(--md-sys-color-on-secondary-container);
239+
border-color: var(--md-sys-color-secondary-container);
240+
}
241+
242+
.chip.selected:hover {
243+
background: var(--md-sys-color-secondary-container-high, #e8def8);
244+
}
245+
246+
.chip-icon {
247+
display: none;
248+
width: 18px;
249+
height: 18px;
250+
}
251+
252+
.chip.selected .chip-icon {
253+
display: block;
254+
fill: currentColor;
255+
}
256+
167257
@keyframes fadeIn {
168258
from { opacity: 0; transform: translateY(-8px); }
169259
to { opacity: 1; transform: translateY(0); }
@@ -191,13 +281,14 @@ export class MultipleChoice extends Root {
191281
}
192282

193283
getCurrentSelections(): string[] {
194-
if (!this.processor || !this.component) {
195-
return Array.isArray(this.selections) ? this.selections : [];
196-
}
197284
if (Array.isArray(this.selections)) {
198285
return this.selections;
199286
}
200287

288+
if (!this.processor || !this.component) {
289+
return [];
290+
}
291+
201292
const selectionValue = this.processor.getData(
202293
this.component,
203294
this.selections.path!,
@@ -217,8 +308,82 @@ export class MultipleChoice extends Root {
217308
this.requestUpdate();
218309
}
219310

311+
#renderCheckIcon() {
312+
return html`
313+
<svg class="chip-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
314+
<path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z"/>
315+
</svg>
316+
`;
317+
}
318+
319+
#renderFilter() {
320+
return html`
321+
<div class="filter-container">
322+
<input
323+
type="text"
324+
class="filter-input"
325+
placeholder="Filter options..."
326+
.value=${this.filterText}
327+
@input=${(e: Event) => {
328+
const target = e.target as HTMLInputElement;
329+
this.filterText = target.value;
330+
}}
331+
@click=${(e: Event) => e.stopPropagation()}
332+
/>
333+
</div>
334+
`;
335+
}
336+
220337
render() {
221338
const currentSelections = this.getCurrentSelections();
339+
340+
// Filter options
341+
const filteredOptions = this.options.filter(option => {
342+
if (!this.filterText) return true;
343+
const label = extractStringValue(
344+
option.label,
345+
this.component,
346+
this.processor,
347+
this.surfaceId
348+
);
349+
return label.toLowerCase().includes(this.filterText.toLowerCase());
350+
});
351+
352+
// Chips Layout
353+
if (this.variant === "chips") {
354+
return html`
355+
<div class="container">
356+
${this.description ? html`<div class="header-text" style="margin-bottom: 8px;">${this.description}</div>` : nothing}
357+
${this.filterable ? this.#renderFilter() : nothing}
358+
<div class="chips-container">
359+
${filteredOptions.map((option) => {
360+
const label = extractStringValue(
361+
option.label,
362+
this.component,
363+
this.processor,
364+
this.surfaceId
365+
);
366+
const isSelected = currentSelections.includes(option.value);
367+
return html`
368+
<div
369+
class="chip ${isSelected ? "selected" : ""}"
370+
@click=${(e: Event) => {
371+
e.stopPropagation();
372+
this.toggleSelection(option.value);
373+
}}
374+
>
375+
${isSelected ? this.#renderCheckIcon() : nothing}
376+
<span>${label}</span>
377+
</div>
378+
`;
379+
})}
380+
</div>
381+
${filteredOptions.length === 0 ? html`<div style="padding: 8px; font-style: italic; color: var(--md-sys-color-outline);">No options found</div>` : nothing}
382+
</div>
383+
`;
384+
}
385+
386+
// Default Checkbox Dropdown Layout
222387
const count = currentSelections.length;
223388
const headerText = count > 0 ? `${count} Selected` : (this.description ?? "Select items");
224389

@@ -236,31 +401,35 @@ export class MultipleChoice extends Root {
236401
</span>
237402
</div>
238403
239-
<div class="options-list ${this.isOpen ? "open" : ""}">
240-
${this.options.map((option) => {
241-
const label = extractStringValue(
242-
option.label,
243-
this.component,
244-
this.processor,
245-
this.surfaceId
246-
);
247-
const isSelected = currentSelections.includes(option.value);
248-
249-
return html`
250-
<div
251-
class="option-item ${isSelected ? "selected" : ""}"
252-
@click=${(e: Event) => {
253-
e.stopPropagation();
254-
this.toggleSelection(option.value);
255-
}}
256-
>
257-
<div class="checkbox">
258-
<span class="checkbox-icon"></span>
404+
<div class="dropdown-wrapper ${this.isOpen ? "open" : ""}">
405+
${this.filterable ? this.#renderFilter() : nothing}
406+
<div class="options-scroll-container">
407+
${filteredOptions.map((option) => {
408+
const label = extractStringValue(
409+
option.label,
410+
this.component,
411+
this.processor,
412+
this.surfaceId
413+
);
414+
const isSelected = currentSelections.includes(option.value);
415+
416+
return html`
417+
<div
418+
class="option-item ${isSelected ? "selected" : ""}"
419+
@click=${(e: Event) => {
420+
e.stopPropagation();
421+
this.toggleSelection(option.value);
422+
}}
423+
>
424+
<div class="checkbox">
425+
<span class="checkbox-icon"></span>
426+
</div>
427+
<span>${label}</span>
259428
</div>
260-
<span>${label}</span>
261-
</div>
262-
`;
263-
})}
429+
`;
430+
})}
431+
${filteredOptions.length === 0 ? html`<div style="padding: 16px; text-align: center; color: var(--md-sys-color-outline);">No options found</div>` : nothing}
432+
</div>
264433
</div>
265434
</div>
266435
`;

renderers/lit/src/0.8/ui/root.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,8 @@ export class Root extends SignalWatcher(LitElement) {
385385
.options=${node.properties.options}
386386
.maxAllowedSelections=${node.properties.maxAllowedSelections}
387387
.selections=${node.properties.selections}
388+
.variant=${(node as any).properties.variant}
389+
.filterable=${node.properties.filterable}
388390
.enableCustomElements=${this.enableCustomElements}
389391
></a2ui-multiplechoice>`;
390392
}

renderers/web_core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@a2ui/web_core",
3-
"version": "0.8.0",
3+
"version": "0.8.2",
44
"description": "A2UI Core Library",
55
"main": "./dist/src/v0_8/index.js",
66
"types": "./dist/src/v0_8/index.d.ts",

0 commit comments

Comments
 (0)