Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
155ba4b
UN-4301 [FEAT] Add the v1 option model, option row, chip and trigger …
praveen-formido Nov 9, 2026
a5bc316
UN-4301 [FIX] Guard disabled options in code and pin the row and type…
praveen-formido Nov 9, 2026
3fd71c3
UN-4301 [FEAT] Add the v1 Select
praveen-formido Nov 9, 2026
b9eb049
UN-4301 [FIX] Keep both list height caps, show an empty state, and pi…
praveen-formido Nov 9, 2026
b9b2cb5
UN-4301 [FEAT] Add the v1 listbox engine with async options and a vir…
praveen-formido Nov 9, 2026
b530b3b
UN-4301 [FIX] Land click-opens on the selection, pin the loader ident…
praveen-formido Nov 9, 2026
8994cfa
UN-4301 [FEAT] Add the v1 Combobox with server-loaded options
praveen-formido Nov 9, 2026
d39b235
UN-4301 [FIX] Surface load errors, remember every seen option, and se…
praveen-formido Nov 9, 2026
9238141
UN-4301 [MISC] Share the combobox frame helpers
praveen-formido Nov 9, 2026
781728c
UN-4301 [FEAT] Add the v1 MultiSelect with chips
praveen-formido Nov 9, 2026
09a36d7
UN-4301 [FIX] Keep the toggled option active, take chips out of the t…
praveen-formido Nov 9, 2026
1be17c1
UN-4301 [FEAT] Add the v1 SegmentedControl
praveen-formido Nov 9, 2026
2978c5e
UN-4301 [FEAT] Add the selection inputs gallery, markup snapshot and …
praveen-formido Nov 9, 2026
bbe0ed7
UN-4301 [FIX] Lay out the Select value, name the active option stably…
praveen-formido Nov 10, 2026
fa3d8a8
UN-4301 [FEAT] Share the list popover, chevron and loaded-options hoo…
praveen-formido Nov 10, 2026
ca025b3
UN-4301 [MISC] Drop the unused type-ahead helper and correct the READMEs
praveen-formido Nov 10, 2026
c58e0fe
UN-4301 [FIX] Drop a previous query's options at once, cap popover li…
praveen-formido Nov 10, 2026
44deb58
UN-4301 [FIX] Never let a disabled option become the active one
praveen-formido Nov 10, 2026
8aad7cc
Merge remote-tracking branch 'origin/main' into UN-4301-selection-inputs
praveen-formido Nov 11, 2026
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
17 changes: 17 additions & 0 deletions frontend/src/v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,23 @@ is convention.
- `CodeEditor` uses Monaco from its default CDN, as v0 does. Never call
`loader.config()` in v1: the loader is shared with v0.

## Selection inputs

- Dropdowns, searchable pickers, chip inputs and segmented switches come
from `@v1/components/ui/select` (`Select`, `Combobox`, `MultiSelect`,
`SegmentedControl`, `Chip`); its README lists every prop and the keyboard.
Do not build another dropdown, picker or chip input.
- Pick by list size: `Select` for a short, known list (under about 50
options; it renders every one), `Combobox` for a long or server-loaded
list (`loadOptions`), `MultiSelect` for several values, and
`SegmentedControl` for 2 to 5 always-visible options.
- `Select`, `Combobox` and `MultiSelect` take `SelectOption`, and
`SegmentedControl` takes `SegmentedOption`; the folder README lists their
fields. A provider or connector logo goes in a `SelectOption`'s `leading`
as `<ProviderLogo decorative … />` from `@v1/components/ui/BrandMark`.
- Until P1-T06's `Field` lands, name them with `aria-label` or
`aria-labelledby`; do not put them inside `Field`.

## Version switch and route registry

v0's router (`routes/Router.jsx`) wraps its routes in `VersionSwitch`, which
Expand Down
45 changes: 45 additions & 0 deletions frontend/src/v1/components/ui/select/Chip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { screen } from "@testing-library/react";
import { renderV1 } from "@v1/test";
import { expect, it, vi } from "vitest";
import { Chip } from "./Chip";

it("renders the label with a remove button named after it", async () => {
const onRemove = vi.fn();
const { user } = renderV1(<Chip label="GPT-4o" onRemove={onRemove} />);
const chip = document.querySelector('[data-slot="chip"]');
expect(chip?.textContent).toBe("GPT-4o");
const remove = screen.getByRole("button", { name: "Remove GPT-4o" });
expect(remove.getAttribute("type")).toBe("button");
await user.click(remove);
expect(onRemove).toHaveBeenCalledTimes(1);
});

it("disables the remove button when the chip is disabled", async () => {
const onRemove = vi.fn();
const { user } = renderV1(
<Chip label="GPT-4o" onRemove={onRemove} disabled />,
);
const remove = screen.getByRole("button", { name: "Remove GPT-4o" });
expect((remove as HTMLButtonElement).disabled).toBe(true);
await user.click(remove);
expect(onRemove).not.toHaveBeenCalled();
});

it("has no remove button without onRemove", () => {
renderV1(<Chip label="GPT-4o" />);
expect(screen.queryByRole("button")).toBeNull();
expect(screen.getByText("GPT-4o")).toBeTruthy();
});

it("takes the remove button out of the tab order when not removeFocusable", async () => {
const onRemove = vi.fn();
const { user, rerender } = renderV1(
<Chip label="GPT-4o" onRemove={onRemove} />,
);
const remove = () => screen.getByRole("button", { name: "Remove GPT-4o" });
expect(remove().tabIndex).toBe(0);
rerender(<Chip label="GPT-4o" onRemove={onRemove} removeFocusable={false} />);
expect(remove().tabIndex).toBe(-1);
await user.click(remove());
expect(onRemove).toHaveBeenCalledTimes(1);
});
55 changes: 55 additions & 0 deletions frontend/src/v1/components/ui/select/Chip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { cn } from "@v1/lib/cn";
import { ACTION_ICONS } from "@v1/lib/icons";
import type { JSX } from "react";

const RemoveIcon = ACTION_ICONS.close;

export type ChipProps = Readonly<{
label: string;
/** Shows a remove button named "Remove {label}". */
onRemove?: () => void;
/**
* Whether the remove button is a tab stop. Default true; false keeps it
* clickable and named but out of the tab order, for a field whose keyboard
* removes chips another way (MultiSelect: Backspace).
*/
removeFocusable?: boolean;
disabled?: boolean;
className?: string;
}>;

/**
* A selected value in a MultiSelect, with an optional remove button, which
* `removeFocusable={false}` takes out of the tab order.
*/
export function Chip({
label,
onRemove,
removeFocusable = true,
disabled,
className,
}: ChipProps): JSX.Element {
return (
<span
data-slot="chip"
className={cn(
"ui:inline-flex ui:h-5 ui:max-w-full ui:items-center ui:gap-1 ui:rounded-sm ui:bg-muted ui:pl-1.75 ui:pr-0.5 ui:text-caption ui:font-medium ui:text-muted-foreground",
className,
)}
>
<span className="ui:truncate">{label}</span>
{onRemove ? (
<button
type="button"
aria-label={`Remove ${label}`}
tabIndex={removeFocusable ? undefined : -1}
disabled={disabled}
onClick={onRemove}
className="ui:flex ui:size-4 ui:shrink-0 ui:cursor-pointer ui:items-center ui:justify-center ui:rounded-sm ui:border-0 ui:bg-transparent ui:p-0 ui:text-muted-foreground ui:hover:bg-accent ui:hover:text-foreground ui:focus-visible:focus-ring"
>
<RemoveIcon aria-hidden className="ui:size-3" />
</button>
) : null}
</span>
);
}
Loading
Loading