Skip to content

Commit 3586b8d

Browse files
committed
Create new 0.8.2 version of the spec to support filtering and types for dropdown
1 parent eae7f16 commit 3586b8d

File tree

12 files changed

+1544
-1295
lines changed

12 files changed

+1544
-1295
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class Icon extends Root {
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: 198 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,18 @@ export class MultipleChoice extends Root {
3535
@property()
3636
accessor selections: Primitives.StringValue | string[] = [];
3737

38+
@property()
39+
accessor type: "checkbox" | "chips" = "checkbox";
40+
41+
@property({ type: Boolean })
42+
accessor filterable = false;
43+
3844
@state()
3945
accessor isOpen = false;
4046

47+
@state()
48+
accessor filterText = "";
49+
4150
static styles = [
4251
structuralStyles,
4352
css`
@@ -95,25 +104,57 @@ export class MultipleChoice extends Root {
95104
transform: rotate(180deg);
96105
}
97106
98-
/* Dropdown List */
99-
.options-list {
107+
/* Dropdown Wrapper */
108+
.dropdown-wrapper {
100109
background: var(--md-sys-color-surface);
101110
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;
111+
border-radius: 8px;
112+
box-shadow: var(--md-sys-elevation-level2);
105113
padding: 0;
106114
display: none;
107115
flex-direction: column;
108-
margin-top: 4px; /* Small gap */
109-
max-height: 0; /* Animate height? */
110-
transition: max-height 0.2s ease-out;
116+
margin-top: 4px;
117+
max-height: 300px;
118+
transition: opacity 0.2s ease-out;
119+
overflow: hidden; /* contain children */
111120
}
112121
113-
.options-list.open {
122+
.dropdown-wrapper.open {
114123
display: flex;
115-
max-height: 300px; /* Limit height but allow scrolling */
116-
border: 1px solid var(--md-sys-color-outline-variant); /* efficient border */
124+
border: 1px solid var(--md-sys-color-outline-variant);
125+
}
126+
127+
/* Scrollable Area for Options */
128+
.options-scroll-container {
129+
overflow-y: auto;
130+
flex: 1; /* take remaining height */
131+
display: flex;
132+
flex-direction: column;
133+
}
134+
135+
/* Filter Input */
136+
.filter-container {
137+
padding: 8px;
138+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
139+
background: var(--md-sys-color-surface);
140+
z-index: 1; /* ensure top of stack */
141+
flex-shrink: 0; /* don't shrink */
142+
}
143+
144+
.filter-input {
145+
width: 100%;
146+
padding: 8px 12px;
147+
border: 1px solid var(--md-sys-color-outline);
148+
border-radius: 4px;
149+
font-family: inherit;
150+
font-size: 0.9rem;
151+
background: var(--md-sys-color-surface-container-low);
152+
color: var(--md-sys-color-on-surface);
153+
}
154+
155+
.filter-input:focus {
156+
outline: none;
157+
border-color: var(--md-sys-color-primary);
117158
}
118159
119160
/* Option Item (Checkbox style) */
@@ -164,6 +205,54 @@ export class MultipleChoice extends Root {
164205
transform: scale(1);
165206
}
166207
208+
/* Chips Layout */
209+
.chips-container {
210+
display: flex;
211+
flex-wrap: wrap;
212+
gap: 8px;
213+
padding: 4px 0;
214+
}
215+
216+
.chip {
217+
display: inline-flex;
218+
align-items: center;
219+
gap: 8px;
220+
padding: 6px 16px;
221+
border: 1px solid var(--md-sys-color-outline);
222+
border-radius: 16px;
223+
cursor: pointer;
224+
user-select: none;
225+
background: var(--md-sys-color-surface);
226+
color: var(--md-sys-color-on-surface);
227+
transition: all 0.2s ease;
228+
font-size: 0.9rem;
229+
}
230+
231+
.chip:hover {
232+
background: var(--md-sys-color-surface-container-high);
233+
}
234+
235+
.chip.selected {
236+
background: var(--md-sys-color-secondary-container);
237+
color: var(--md-sys-color-on-secondary-container);
238+
border-color: var(--md-sys-color-secondary-container);
239+
}
240+
241+
.chip.selected:hover {
242+
background: var(--md-sys-color-secondary-container-high, #e8def8);
243+
}
244+
245+
.chip-icon {
246+
display: none;
247+
width: 18px;
248+
height: 18px;
249+
}
250+
251+
.chip.selected .chip-icon {
252+
display: block;
253+
fill: currentColor;
254+
}
255+
167256
@keyframes fadeIn {
168257
from { opacity: 0; transform: translateY(-8px); }
169258
to { opacity: 1; transform: translateY(0); }
@@ -217,8 +306,78 @@ export class MultipleChoice extends Root {
217306
this.requestUpdate();
218307
}
219308

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

@@ -236,31 +395,35 @@ export class MultipleChoice extends Root {
236395
</span>
237396
</div>
238397
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>
398+
<div class="dropdown-wrapper ${this.isOpen ? "open" : ""}">
399+
${this.filterable ? this.#renderFilter() : nothing}
400+
<div class="options-scroll-container">
401+
${filteredOptions.map((option) => {
402+
const label = extractStringValue(
403+
option.label,
404+
this.component,
405+
this.processor,
406+
this.surfaceId
407+
);
408+
const isSelected = currentSelections.includes(option.value);
409+
410+
return html`
411+
<div
412+
class="option-item ${isSelected ? "selected" : ""}"
413+
@click=${(e: Event) => {
414+
e.stopPropagation();
415+
this.toggleSelection(option.value);
416+
}}
417+
>
418+
<div class="checkbox">
419+
<span class="checkbox-icon"></span>
420+
</div>
421+
<span>${label}</span>
259422
</div>
260-
<span>${label}</span>
261-
</div>
262-
`;
263-
})}
423+
`;
424+
})}
425+
${filteredOptions.length === 0 ? html`<div style="padding: 16px; text-align: center; color: var(--md-sys-color-outline);">No options found</div>` : nothing}
426+
</div>
264427
</div>
265428
</div>
266429
`;

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+
.type=${node.properties.type}
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",

renderers/web_core/src/v0_8/types/components.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ export interface MultipleChoice {
196196
value: string;
197197
}[];
198198
maxAllowedSelections?: number;
199+
type?: "checkbox" | "chips";
200+
filterable?: boolean;
199201
}
200202

201203
export interface Slider {

samples/agent/adk/component_gallery/gallery_examples.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ def get_gallery_json() -> str:
2121
{ "key": "date", "valueString": "2025-10-26" },
2222
{ "key": "favorites", "valueMap": [
2323
{ "key": "0", "valueString": "A" }
24-
]}
24+
]},
25+
{ "key": "favoritesChips", "valueMap": [] },
26+
{ "key": "favoritesFilter", "valueMap": [] }
2527
]
2628
}
2729

@@ -79,7 +81,7 @@ def add_demo_surface(surface_id, component_def):
7981
}
8082
})
8183

82-
# 5. MultipleChoice
84+
# 5. MultipleChoice (Default)
8385
add_demo_surface("demo-multichoice", {
8486
"MultipleChoice": {
8587
"selections": { "path": "galleryData/favorites" },
@@ -91,6 +93,39 @@ def add_demo_surface(surface_id, component_def):
9193
}
9294
})
9395

96+
# 5b. MultipleChoice (Chips)
97+
add_demo_surface("demo-multichoice-chips", {
98+
"MultipleChoice": {
99+
"selections": { "path": "galleryData/favoritesChips" },
100+
"description": "Select tags (Chips)",
101+
"type": "chips",
102+
"options": [
103+
{ "label": { "literalString": "Work" }, "value": "work" },
104+
{ "label": { "literalString": "Home" }, "value": "home" },
105+
{ "label": { "literalString": "Urgent" }, "value": "urgent" },
106+
{ "label": { "literalString": "Later" }, "value": "later" }
107+
]
108+
}
109+
})
110+
111+
# 5c. MultipleChoice (Filterable)
112+
add_demo_surface("demo-multichoice-filter", {
113+
"MultipleChoice": {
114+
"selections": { "path": "galleryData/favoritesFilter" },
115+
"description": "Select countries (Filterable)",
116+
"filterable": True,
117+
"options": [
118+
{ "label": { "literalString": "United States" }, "value": "US" },
119+
{ "label": { "literalString": "Canada" }, "value": "CA" },
120+
{ "label": { "literalString": "United Kingdom" }, "value": "UK" },
121+
{ "label": { "literalString": "Australia" }, "value": "AU" },
122+
{ "label": { "literalString": "Germany" }, "value": "DE" },
123+
{ "label": { "literalString": "France" }, "value": "FR" },
124+
{ "label": { "literalString": "Japan" }, "value": "JP" }
125+
]
126+
}
127+
})
128+
94129
# 6. Image
95130
add_demo_surface("demo-image", {
96131
"Image": {
@@ -319,7 +354,7 @@ def add_demo_surface(surface_id, component_def):
319354
{
320355
"id": "response-text",
321356
"component": {
322-
"Text": { "text": { "literalString": "Interact with the gallery to see responses." } }
357+
"Text": { "text": { "literalString": "Interact with the gallery to see responses. This view is updated by the agent by relaying the raw action commands it received from the client" } }
323358
}
324359
}
325360
]

0 commit comments

Comments
 (0)