WEB-482High priority
Checkout button stays disabled after applying a promo code
Reported by 14 customers since the 3.12 release. Reproduces on Safari and Firefox.
Unassigned issues stay in the triage queue.
Free, copy-and-go Combobox components built on the SevenUI Combobox primitive.Read the primitive docs.
Reminders and digests are sent in this timezone.
"use client";
import { GlobeIcon } from "lucide-react";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
import { InputGroupAddon } from "@/components/ui/input-group";
const timezones = [
{ value: "America/Los_Angeles", label: "Pacific Time", offset: "UTC−07:00" },
{ value: "America/New_York", label: "Eastern Time", offset: "UTC−04:00" },
{ value: "America/Sao_Paulo", label: "Brasília Time", offset: "UTC−03:00" },
{ value: "Europe/London", label: "British Summer Time", offset: "UTC+01:00" },
{ value: "Europe/Berlin", label: "Central European Time", offset: "UTC+02:00" },
{ value: "Europe/Istanbul", label: "Turkey Time", offset: "UTC+03:00" },
{ value: "Asia/Kolkata", label: "India Standard Time", offset: "UTC+05:30" },
{ value: "Asia/Singapore", label: "Singapore Time", offset: "UTC+08:00" },
{ value: "Asia/Tokyo", label: "Japan Standard Time", offset: "UTC+09:00" },
{ value: "Australia/Sydney", label: "Australian Eastern Time", offset: "UTC+10:00" },
];
type Timezone = (typeof timezones)[number];
// Match the zone name, the city in its IANA id, and the offset, so "tokyo"
// finds Japan Standard Time and "+03" finds Turkey Time.
function matchesTimezone(item: Timezone, query: string) {
const needle = query.trim().toLowerCase();
return (
item.label.toLowerCase().includes(needle) ||
item.value.toLowerCase().replaceAll("_", " ").includes(needle) ||
item.offset.toLowerCase().replace("−", "-").includes(needle.replace("−", "-"))
);
}
export default function Combobox01() {
return (
<Field className="w-full max-w-xs">
<FieldLabel htmlFor="combobox-01-timezone">Timezone</FieldLabel>
<Combobox
items={timezones}
defaultValue={timezones[4]}
filter={matchesTimezone}
>
<ComboboxInput
id="combobox-01-timezone"
placeholder="Search timezones"
className="w-full"
showClear
>
<InputGroupAddon align="inline-start">
<GlobeIcon aria-hidden="true" />
</InputGroupAddon>
</ComboboxInput>
<ComboboxContent>
<ComboboxEmpty>No timezone matches that search.</ComboboxEmpty>
<ComboboxList>
{(item: Timezone) => (
<ComboboxItem key={item.value} value={item}>
<span className="truncate">{item.label}</span>
<span className="ml-auto text-xs text-muted-foreground tabular-nums">
{item.offset}
</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
<FieldDescription>
Reminders and digests are sent in this timezone.
</FieldDescription>
</Field>
);
}
npx shadcn@latest add @sevenui/component/combobox-01pnpm dlx shadcn@latest add @sevenui/component/combobox-01yarn dlx shadcn@latest add @sevenui/component/combobox-01bunx --bun shadcn@latest add @sevenui/component/combobox-01Stock verified 4 minutes ago.
Set by your workspace plan.
Available after you add a return address.
"use client";
import * as React from "react";
import { CircleCheckIcon, LockIcon } from "lucide-react";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { InputGroupAddon } from "@/components/ui/input-group";
type Option = { value: string; label: string };
const warehouses: Option[] = [
{ value: "rtm", label: "Rotterdam fulfillment center" },
{ value: "lej", label: "Leipzig cross-dock" },
{ value: "mad", label: "Madrid returns hub" },
];
const currencies: Option[] = [
{ value: "eur", label: "EUR — Euro" },
{ value: "usd", label: "USD — US Dollar" },
];
const carriers: Option[] = [
{ value: "dhl", label: "DHL Express" },
{ value: "ups", label: "UPS Standard" },
];
const regions: Option[] = [
{ value: "benelux", label: "Benelux" },
{ value: "dach", label: "DACH" },
{ value: "iberia", label: "Iberia" },
{ value: "nordics", label: "Nordics" },
];
function OptionList({ empty }: { empty: string }) {
return (
<ComboboxContent>
<ComboboxEmpty>{empty}</ComboboxEmpty>
<ComboboxList>
{(item: Option) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
);
}
export default function Combobox02() {
const [region, setRegion] = React.useState<Option | null>(null);
const regionMissing = region === null;
return (
<FieldGroup className="w-full max-w-xs gap-5">
<Field>
<FieldLabel htmlFor="combobox-02-warehouse">Ship from</FieldLabel>
<Combobox items={warehouses} defaultValue={warehouses[0]}>
<ComboboxInput id="combobox-02-warehouse" className="w-full">
<InputGroupAddon align="inline-start">
<CircleCheckIcon aria-hidden="true" className="text-success" />
</InputGroupAddon>
</ComboboxInput>
<OptionList empty="No warehouse found." />
</Combobox>
<FieldDescription>Stock verified 4 minutes ago.</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="combobox-02-currency">Billing currency</FieldLabel>
<Combobox items={currencies} defaultValue={currencies[0]} readOnly>
<ComboboxInput
id="combobox-02-currency"
className="w-full bg-muted/40"
showTrigger={false}
>
<InputGroupAddon align="inline-end">
<LockIcon aria-hidden="true" />
</InputGroupAddon>
</ComboboxInput>
<OptionList empty="No currency found." />
</Combobox>
<FieldDescription>Set by your workspace plan.</FieldDescription>
</Field>
<Field disabled>
<FieldLabel htmlFor="combobox-02-carrier">Carrier</FieldLabel>
<Combobox items={carriers} disabled>
<ComboboxInput
id="combobox-02-carrier"
placeholder="Choose a carrier"
className="w-full"
disabled
/>
<OptionList empty="No carrier found." />
</Combobox>
<FieldDescription>Available after you add a return address.</FieldDescription>
</Field>
<Field invalid={regionMissing}>
<FieldLabel htmlFor="combobox-02-region">Sales region</FieldLabel>
<Combobox items={regions} value={region} onValueChange={setRegion}>
<ComboboxInput
id="combobox-02-region"
placeholder="Choose a region"
className="w-full [&_[data-slot=input-group-button]]:border-transparent [&_[data-slot=input-group-button]]:ring-0"
aria-invalid={regionMissing || undefined}
/>
<OptionList empty="No region found." />
</Combobox>
{regionMissing ? (
<FieldError match>Select a region so we can calculate VAT.</FieldError>
) : (
<FieldDescription>VAT is calculated for {region.label}.</FieldDescription>
)}
</Field>
</FieldGroup>
);
}
npx shadcn@latest add @sevenui/component/combobox-02pnpm dlx shadcn@latest add @sevenui/component/combobox-02yarn dlx shadcn@latest add @sevenui/component/combobox-02bunx --bun shadcn@latest add @sevenui/component/combobox-02Beta translations may still show some English.
"use client";
import { LanguagesIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
import { InputGroupAddon } from "@/components/ui/input-group";
const languages = [
{ value: "en", native: "English", english: "English", beta: false },
{ value: "de", native: "Deutsch", english: "German", beta: false },
{ value: "es", native: "Español", english: "Spanish", beta: false },
{ value: "fr", native: "Français", english: "French", beta: false },
{ value: "ja", native: "日本語", english: "Japanese", beta: false },
{ value: "pt-BR", native: "Português (Brasil)", english: "Portuguese", beta: false },
{ value: "tr", native: "Türkçe", english: "Turkish", beta: true },
{ value: "ko", native: "한국어", english: "Korean", beta: true },
{ value: "uk", native: "Українська", english: "Ukrainian", beta: true },
];
type Language = (typeof languages)[number];
// Match the native name and the English name, so "german" finds Deutsch.
function matchesLanguage(item: Language, query: string) {
const needle = query.trim().toLowerCase();
return (
item.native.toLowerCase().includes(needle) ||
item.english.toLowerCase().includes(needle) ||
item.value.toLowerCase().startsWith(needle)
);
}
export default function Combobox03() {
return (
<Field className="w-full max-w-xs">
<FieldLabel htmlFor="combobox-03-language">Interface language</FieldLabel>
<Combobox
items={languages}
defaultValue={languages[1]}
itemToStringLabel={(item: Language) => item.native}
filter={matchesLanguage}
>
<ComboboxInput
id="combobox-03-language"
placeholder="Search in any language"
className="w-full"
>
<InputGroupAddon align="inline-start">
<LanguagesIcon aria-hidden="true" />
</InputGroupAddon>
</ComboboxInput>
<ComboboxContent>
<ComboboxEmpty>We don't support that language yet.</ComboboxEmpty>
<ComboboxList>
{(item: Language) => (
<ComboboxItem
key={item.value}
value={item}
lang={item.value}
className="py-1.5"
>
<span className="grid min-w-0 flex-1 leading-tight">
<span className="truncate font-medium">{item.native}</span>
{item.native !== item.english ? (
<span lang="en" className="truncate text-xs text-muted-foreground">
{item.english}
</span>
) : null}
</span>
{item.beta ? (
<Badge variant="outline" className="shrink-0">
Beta
</Badge>
) : null}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
<FieldDescription>
Beta translations may still show some English.
</FieldDescription>
</Field>
);
}
npx shadcn@latest add @sevenui/component/combobox-03pnpm dlx shadcn@latest add @sevenui/component/combobox-03yarn dlx shadcn@latest add @sevenui/component/combobox-03bunx --bun shadcn@latest add @sevenui/component/combobox-03"use client";
import {
Combobox,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxSeparator,
} from "@/components/ui/combobox";
import { Label } from "@/components/ui/label";
type Region = { value: string; label: string; city: string };
type RegionGroup = { value: string; items: Region[] };
const regionGroups: RegionGroup[] = [
{
value: "North America",
items: [
{ value: "us-east-1", label: "US East", city: "N. Virginia" },
{ value: "us-west-2", label: "US West", city: "Oregon" },
{ value: "ca-central-1", label: "Canada Central", city: "Montreal" },
],
},
{
value: "Europe",
items: [
{ value: "eu-west-1", label: "EU West", city: "Dublin" },
{ value: "eu-central-1", label: "EU Central", city: "Frankfurt" },
{ value: "eu-north-1", label: "EU North", city: "Stockholm" },
],
},
{
value: "Asia Pacific",
items: [
{ value: "ap-southeast-1", label: "AP Southeast", city: "Singapore" },
{ value: "ap-northeast-1", label: "AP Northeast", city: "Tokyo" },
{ value: "ap-south-1", label: "AP South", city: "Mumbai" },
],
},
];
export default function Combobox04() {
return (
<div className="grid w-full max-w-xs gap-2">
<Label htmlFor="combobox-04-region">Deployment region</Label>
<Combobox
items={regionGroups}
defaultValue={regionGroups[1].items[1]}
itemToStringLabel={(region: Region) => `${region.label} (${region.city})`}
>
<ComboboxInput
id="combobox-04-region"
placeholder="Search by region or city"
className="w-full"
/>
<ComboboxContent>
<ComboboxEmpty>No region in that location.</ComboboxEmpty>
<ComboboxList>
{(group: RegionGroup, index: number) => (
<ComboboxGroup key={group.value} items={group.items}>
{index > 0 ? <ComboboxSeparator /> : null}
<ComboboxLabel>{group.value}</ComboboxLabel>
<ComboboxCollection>
{(item: Region) => (
<ComboboxItem key={item.value} value={item}>
<span className="truncate">
{item.label}
<span className="text-muted-foreground"> · {item.city}</span>
</span>
<span className="ml-auto font-mono text-xs text-muted-foreground">
{item.value}
</span>
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
);
}
npx shadcn@latest add @sevenui/component/combobox-04pnpm dlx shadcn@latest add @sevenui/component/combobox-04yarn dlx shadcn@latest add @sevenui/component/combobox-04bunx --bun shadcn@latest add @sevenui/component/combobox-04"use client";
import * as React from "react";
import {
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxContent,
ComboboxEmpty,
ComboboxItem,
ComboboxList,
ComboboxValue,
useComboboxAnchor,
} from "@/components/ui/combobox";
import { Label } from "@/components/ui/label";
const MAX_LABELS = 3;
const labels = [
{ value: "bug", label: "Bug", dotClassName: "bg-destructive" },
{ value: "performance", label: "Performance", dotClassName: "bg-chart-1" },
{ value: "design", label: "Design", dotClassName: "bg-chart-2" },
{ value: "docs", label: "Documentation", dotClassName: "bg-chart-3" },
{ value: "security", label: "Security", dotClassName: "bg-chart-4" },
{ value: "good-first-issue", label: "Good first issue", dotClassName: "bg-chart-5" },
];
type IssueLabel = (typeof labels)[number];
export default function Combobox05() {
const anchor = useComboboxAnchor();
const [value, setValue] = React.useState<IssueLabel[]>([labels[0], labels[1]]);
const atLimit = value.length >= MAX_LABELS;
return (
<div className="grid w-full max-w-xs gap-2">
<div className="flex items-baseline justify-between gap-2">
<Label htmlFor="combobox-05-labels">Labels</Label>
<span
id="combobox-05-count"
aria-live="polite"
className="text-xs text-muted-foreground tabular-nums"
>
{value.length} of {MAX_LABELS}
</span>
</div>
<Combobox items={labels} multiple value={value} onValueChange={setValue}>
<ComboboxChips ref={anchor} className="w-full">
<ComboboxValue>
{(selected: IssueLabel[]) => (
<React.Fragment>
{selected.map((item) => (
<ComboboxChip
key={item.value}
aria-label={item.label}
className="gap-1.5 rounded-full bg-transparent pl-2 ring-1 ring-border"
>
<span
aria-hidden="true"
className={`size-1.5 rounded-full ${item.dotClassName}`}
/>
{item.label}
</ComboboxChip>
))}
<ComboboxChipsInput
id="combobox-05-labels"
aria-describedby="combobox-05-count"
placeholder={selected.length > 0 ? "" : "Add labels"}
/>
</React.Fragment>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxContent anchor={anchor}>
<ComboboxEmpty>No label with that name.</ComboboxEmpty>
<ComboboxList>
{(item: IssueLabel) => (
<ComboboxItem
key={item.value}
value={item}
disabled={
atLimit && !value.some((selected) => selected.value === item.value)
}
>
<span
aria-hidden="true"
className={`size-2 rounded-full ${item.dotClassName}`}
/>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
{atLimit ? (
<p className="border-t border-border px-2.5 py-2 text-xs text-muted-foreground">
Remove a label to add another.
</p>
) : null}
</ComboboxContent>
</Combobox>
</div>
);
}
npx shadcn@latest add @sevenui/component/combobox-05pnpm dlx shadcn@latest add @sevenui/component/combobox-05yarn dlx shadcn@latest add @sevenui/component/combobox-05bunx --bun shadcn@latest add @sevenui/component/combobox-05"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
import { Label } from "@/components/ui/label";
const currencies = [
{ value: "USD", label: "US Dollar", symbol: "$", rate: 1 },
{ value: "EUR", label: "Euro", symbol: "€", rate: 0.92 },
{ value: "GBP", label: "British Pound", symbol: "£", rate: 0.79 },
{ value: "JPY", label: "Japanese Yen", symbol: "¥", rate: 149.6 },
{ value: "CHF", label: "Swiss Franc", symbol: "CHF ", rate: 0.88 },
{ value: "CAD", label: "Canadian Dollar", symbol: "CA$", rate: 1.36 },
{ value: "AUD", label: "Australian Dollar", symbol: "A$", rate: 1.52 },
{ value: "TRY", label: "Turkish Lira", symbol: "₺", rate: 34.1 },
];
type Currency = (typeof currencies)[number];
const recent = ["EUR", "GBP", "JPY"];
const basePriceUsd = 48;
export default function Combobox06() {
const [currency, setCurrency] = React.useState<Currency | null>(currencies[0]);
const price = currency
? `${currency.symbol}${(basePriceUsd * currency.rate).toLocaleString("en-US", {
maximumFractionDigits: currency.value === "JPY" ? 0 : 2,
minimumFractionDigits: currency.value === "JPY" ? 0 : 2,
})}`
: "—";
return (
<div className="grid w-full max-w-xs gap-3">
<div className="grid gap-2">
<Label htmlFor="combobox-06-currency">Display currency</Label>
<Combobox
items={currencies}
value={currency}
onValueChange={setCurrency}
itemToStringLabel={(item: Currency) => `${item.value} — ${item.label}`}
isItemEqualToValue={(item: Currency, selected: Currency) =>
item.value === selected.value
}
>
<ComboboxInput
id="combobox-06-currency"
placeholder="Search currencies"
className="w-full"
showClear
/>
<ComboboxContent>
<ComboboxEmpty>No supported currency matches.</ComboboxEmpty>
<ComboboxList>
{(item: Currency) => (
<ComboboxItem key={item.value} value={item}>
<span className="w-9 font-medium tabular-nums">{item.value}</span>
<span className="truncate text-muted-foreground">{item.label}</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
<div className="flex flex-wrap items-center gap-1.5">
<span className="mr-0.5 text-xs text-muted-foreground">Recent</span>
{recent.map((code) => {
const item = currencies.find((entry) => entry.value === code);
if (!item) return null;
const active = currency?.value === code;
return (
<Button
key={code}
size="xs"
variant={active ? "secondary" : "outline"}
aria-pressed={active}
onClick={() => setCurrency(item)}
>
{code}
</Button>
);
})}
</div>
<div className="flex items-baseline justify-between rounded-lg bg-muted/50 px-3 py-2.5">
<span className="text-sm text-muted-foreground">Pro plan, monthly</span>
<output
htmlFor="combobox-06-currency"
aria-live="polite"
className="text-sm font-medium tabular-nums"
>
{price}
</output>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/combobox-06pnpm dlx shadcn@latest add @sevenui/component/combobox-06yarn dlx shadcn@latest add @sevenui/component/combobox-06bunx --bun shadcn@latest add @sevenui/component/combobox-06"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from "@/components/ui/combobox";
const statuses = [
{ value: "backlog", label: "Backlog", dotClassName: "border border-dashed border-muted-foreground" },
{ value: "todo", label: "Todo", dotClassName: "border-2 border-muted-foreground" },
{ value: "in-progress", label: "In progress", dotClassName: "bg-warning" },
{ value: "in-review", label: "In review", dotClassName: "bg-chart-2" },
{ value: "done", label: "Done", dotClassName: "bg-success" },
{ value: "canceled", label: "Canceled", dotClassName: "bg-muted-foreground/40" },
];
type Status = (typeof statuses)[number];
function StatusDot({ status }: { status: Status }) {
return (
<span
aria-hidden="true"
className={`size-2.5 shrink-0 rounded-full ${status.dotClassName}`}
/>
);
}
export default function Combobox07() {
const [status, setStatus] = React.useState<Status>(statuses[2]);
return (
<div className="flex w-full max-w-xs items-center justify-between gap-3 rounded-lg border border-border bg-card px-3 py-2.5 text-card-foreground">
<span id="combobox-07-label" className="text-sm text-muted-foreground">
Status
</span>
<Combobox
items={statuses}
value={status}
onValueChange={(next) => {
if (next) setStatus(next);
}}
>
<ComboboxTrigger
aria-labelledby="combobox-07-label combobox-07-value"
render={<Button variant="ghost" size="sm" className="-mr-1.5 gap-2 font-normal" />}
>
<StatusDot status={status} />
<span id="combobox-07-value">{status.label}</span>
</ComboboxTrigger>
<ComboboxContent align="end" className="w-56 min-w-56">
<ComboboxInput
placeholder="Change status…"
aria-label="Filter statuses"
showTrigger={false}
/>
<ComboboxEmpty>No status with that name.</ComboboxEmpty>
<ComboboxList>
{(item: Status) => (
<ComboboxItem key={item.value} value={item}>
<StatusDot status={item} />
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
);
}
npx shadcn@latest add @sevenui/component/combobox-07pnpm dlx shadcn@latest add @sevenui/component/combobox-07yarn dlx shadcn@latest add @sevenui/component/combobox-07bunx --bun shadcn@latest add @sevenui/component/combobox-07Showing popular packages
"use client";
import * as React from "react";
import { PackageIcon, SearchIcon } from "lucide-react";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
import { InputGroupAddon } from "@/components/ui/input-group";
import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/spinner";
type Package = {
value: string;
label: string;
description: string;
downloads: string;
};
// Stands in for a remote search endpoint.
const catalog: Package[] = [
{ value: "zod", label: "zod", description: "TypeScript-first schema validation", downloads: "31.2M" },
{ value: "date-fns", label: "date-fns", description: "Modern date utility library", downloads: "24.8M" },
{ value: "zustand", label: "zustand", description: "Small, fast state management", downloads: "6.1M" },
{ value: "react-hook-form", label: "react-hook-form", description: "Performant forms with easy validation", downloads: "9.4M" },
{ value: "recharts", label: "recharts", description: "Charts built on React and D3", downloads: "3.2M" },
{ value: "dayjs", label: "dayjs", description: "2kB immutable date library", downloads: "22.5M" },
{ value: "swr", label: "swr", description: "React hooks for data fetching", downloads: "3.9M" },
{ value: "valibot", label: "valibot", description: "Modular schema library", downloads: "1.7M" },
{ value: "embla-carousel", label: "embla-carousel", description: "Lightweight carousel engine", downloads: "2.4M" },
{ value: "clsx", label: "clsx", description: "Tiny utility for className strings", downloads: "38.9M" },
];
const popular = catalog.slice(0, 4);
function searchCatalog(query: string) {
const needle = query.trim().toLowerCase();
return catalog.filter(
(item) =>
item.label.includes(needle) ||
item.description.toLowerCase().includes(needle),
);
}
export default function Combobox08() {
const [query, setQuery] = React.useState("");
const [results, setResults] = React.useState<Package[]>(popular);
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
if (query.trim() === "") {
setResults(popular);
setLoading(false);
return;
}
setLoading(true);
const timeout = window.setTimeout(() => {
setResults(searchCatalog(query));
setLoading(false);
}, 450);
return () => window.clearTimeout(timeout);
}, [query]);
const status = loading
? "Searching packages"
: query.trim() === ""
? "Showing popular packages"
: `${results.length} ${results.length === 1 ? "package" : "packages"} found`;
return (
<div className="grid w-full max-w-sm gap-2">
<Label htmlFor="combobox-08-package">Add dependency</Label>
<Combobox
items={results}
filter={null}
onInputValueChange={setQuery}
>
<ComboboxInput
id="combobox-08-package"
placeholder="Search the registry"
className="w-full"
aria-busy={loading}
showTrigger={false}
showClear
>
<InputGroupAddon align="inline-start">
{loading ? (
<Spinner aria-hidden="true" />
) : (
<SearchIcon aria-hidden="true" />
)}
</InputGroupAddon>
</ComboboxInput>
<ComboboxContent>
{query.trim() === "" ? (
<p className="px-2.5 pt-2 pb-1 text-xs text-muted-foreground">
Popular this week
</p>
) : null}
<ComboboxEmpty className="flex-col items-center gap-1 py-6">
<span className="font-medium text-foreground">No packages for “{query}”</span>
<span>Check the spelling or search by what it does.</span>
</ComboboxEmpty>
<ComboboxList
className={loading ? "opacity-60 transition-opacity" : "transition-opacity"}
>
{(item: Package) => (
<ComboboxItem key={item.value} value={item} className="items-start gap-2.5 py-1.5">
<PackageIcon aria-hidden="true" className="mt-0.5 text-muted-foreground" />
<span className="grid min-w-0 flex-1 leading-tight">
<span className="truncate font-medium">{item.label}</span>
<span className="truncate text-xs text-muted-foreground">
{item.description}
</span>
</span>
<span className="text-xs text-muted-foreground tabular-nums">
{item.downloads}
</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
<p aria-live="polite" className="sr-only">
{status}
</p>
</div>
);
}
npx shadcn@latest add @sevenui/component/combobox-08pnpm dlx shadcn@latest add @sevenui/component/combobox-08yarn dlx shadcn@latest add @sevenui/component/combobox-08bunx --bun shadcn@latest add @sevenui/component/combobox-08Blue Bottle Coffee
Sep 22 · Visa ending 4417
−$14.50
Uncategorized expenses are flagged in the monthly report.
"use client";
import * as React from "react";
import { CoffeeIcon, PlusIcon, TagIcon } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
import { InputGroupAddon } from "@/components/ui/input-group";
import { Label } from "@/components/ui/label";
type Category = { value: string; label: string; created?: boolean };
const initialCategories: Category[] = [
{ value: "meals", label: "Meals & entertainment" },
{ value: "travel", label: "Travel" },
{ value: "software", label: "Software subscriptions" },
{ value: "office", label: "Office supplies" },
{ value: "hardware", label: "Hardware" },
{ value: "education", label: "Training & education" },
];
// Placeholder id for the "Create …" row; replaced with a real id on select.
const CREATE_ID = "__create__";
function slugify(label: string) {
return label
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-");
}
export default function Combobox09() {
const [categories, setCategories] = React.useState(initialCategories);
const [category, setCategory] = React.useState<Category | null>(null);
const [query, setQuery] = React.useState("");
const [remember, setRemember] = React.useState(true);
const trimmed = query.trim();
const exists = categories.some(
(item) => item.label.toLowerCase() === trimmed.toLowerCase(),
);
// Offer a create row whenever the typed text is not an existing category.
const items: Category[] =
trimmed && !exists
? [...categories, { value: CREATE_ID, label: trimmed }]
: categories;
const handleChange = (next: Category | null) => {
if (next?.value === CREATE_ID) {
const created = { value: slugify(next.label), label: next.label, created: true };
setCategories((current) => [...current, created]);
setCategory(created);
return;
}
setCategory(next);
};
return (
<div className="w-full max-w-sm rounded-xl border bg-card p-4 text-card-foreground">
<div className="flex items-center gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-full bg-muted">
<CoffeeIcon aria-hidden="true" className="size-4 text-muted-foreground" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">Blue Bottle Coffee</p>
<p className="text-xs text-muted-foreground">Sep 22 · Visa ending 4417</p>
</div>
<p className="text-sm font-medium tabular-nums">−$14.50</p>
</div>
<div className="mt-4 flex flex-col gap-2 border-t pt-4">
<Label htmlFor="combobox-09-category">Category</Label>
<Combobox
items={items}
value={category}
onValueChange={handleChange}
onInputValueChange={setQuery}
>
<ComboboxInput
id="combobox-09-category"
placeholder="Pick or create one"
className="w-full"
>
<InputGroupAddon align="inline-start">
<TagIcon aria-hidden="true" />
</InputGroupAddon>
</ComboboxInput>
<ComboboxContent>
<ComboboxEmpty>Start typing to name a new category.</ComboboxEmpty>
<ComboboxList>
{(item: Category) =>
item.value === CREATE_ID ? (
<ComboboxItem
key={item.value}
value={item}
className="border-t border-border text-muted-foreground"
>
<PlusIcon aria-hidden="true" />
<span className="truncate">
Create{" "}
<span className="font-medium text-foreground">
“{item.label}”
</span>
</span>
</ComboboxItem>
) : (
<ComboboxItem key={item.value} value={item}>
<span className="truncate">{item.label}</span>
{item.created ? (
<span className="ml-auto text-xs text-muted-foreground">
New
</span>
) : null}
</ComboboxItem>
)
}
</ComboboxList>
</ComboboxContent>
</Combobox>
<div className="mt-1 flex items-center gap-2">
<Checkbox
id="combobox-09-remember"
checked={remember}
onCheckedChange={setRemember}
/>
<Label htmlFor="combobox-09-remember" className="font-normal">
Always use this for Blue Bottle Coffee
</Label>
</div>
</div>
<p aria-live="polite" className="mt-4 text-xs text-muted-foreground">
{category
? category.created
? `Created “${category.label}” and filed this expense under it.`
: `Filed under ${category.label}.`
: "Uncategorized expenses are flagged in the monthly report."}
</p>
</div>
);
}
npx shadcn@latest add @sevenui/component/combobox-09pnpm dlx shadcn@latest add @sevenui/component/combobox-09yarn dlx shadcn@latest add @sevenui/component/combobox-09bunx --bun shadcn@latest add @sevenui/component/combobox-09Reported by 14 customers since the 3.12 release. Reproduces on Safari and Firefox.
Unassigned issues stay in the triage queue.
"use client";
import * as React from "react";
import { CircleDot } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
import { InputGroupAddon } from "@/components/ui/input-group";
import { Label } from "@/components/ui/label";
const people = [
{ value: "maya", label: "Maya Chen", initials: "MC", load: 2 },
{ value: "daniel", label: "Daniel Okafor", initials: "DO", load: 5 },
{ value: "lucia", label: "Lucía Fernández", initials: "LF", load: 1 },
{ value: "noah", label: "Noah Bennett", initials: "NB", load: 7 },
{ value: "priya", label: "Priya Raman", initials: "PR", load: 3 },
{ value: "tom", label: "Tom Lindqvist", initials: "TL", load: 0 },
];
type Person = (typeof people)[number];
const me = people[0];
export default function Combobox10() {
const [assignee, setAssignee] = React.useState<Person | null>(null);
return (
<article className="w-full max-w-md rounded-xl border bg-card p-5 text-card-foreground">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<CircleDot aria-hidden="true" className="size-3.5 text-chart-2" />
<span className="font-mono">WEB-482</span>
<Badge variant="outline" className="ml-auto">
High priority
</Badge>
</div>
<h3 className="mt-3 text-base font-medium text-balance">
Checkout button stays disabled after applying a promo code
</h3>
<p className="mt-1.5 text-sm text-muted-foreground">
Reported by 14 customers since the 3.12 release. Reproduces on Safari
and Firefox.
</p>
<div className="mt-5 flex flex-col gap-2">
<div className="flex items-center justify-between">
<Label htmlFor="combobox-10-assignee">Assignee</Label>
{assignee?.value !== me.value && (
<Button
variant="link"
size="xs"
className="h-auto px-0"
onClick={() => setAssignee(me)}
>
Assign to me
</Button>
)}
</div>
<Combobox items={people} value={assignee} onValueChange={setAssignee}>
<ComboboxInput
id="combobox-10-assignee"
placeholder="Search teammates"
showClear={assignee !== null}
className="w-full"
>
{assignee ? (
<InputGroupAddon align="inline-start" className="pl-2">
<Avatar aria-hidden="true" className="size-5">
<AvatarFallback className="text-[0.625rem]">
{assignee.initials}
</AvatarFallback>
</Avatar>
</InputGroupAddon>
) : null}
</ComboboxInput>
<ComboboxContent>
<ComboboxEmpty>No teammate with that name.</ComboboxEmpty>
<ComboboxList>
{(item: Person) => (
<ComboboxItem key={item.value} value={item} className="py-1.5">
<Avatar aria-hidden="true" className="size-6">
<AvatarFallback className="text-[0.625rem]">
{item.initials}
</AvatarFallback>
</Avatar>
<span className="flex-1 truncate">
{item.label}
{item.value === me.value && (
<span className="text-muted-foreground"> (you)</span>
)}
</span>
<span className="text-xs text-muted-foreground tabular-nums">
{item.load} open
</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
<p
aria-live="polite"
className="mt-4 border-t pt-3 text-xs text-muted-foreground"
>
{assignee
? `${assignee.label} will be notified and the issue moves to In progress.`
: "Unassigned issues stay in the triage queue."}
</p>
</article>
);
}
npx shadcn@latest add @sevenui/component/combobox-10pnpm dlx shadcn@latest add @sevenui/component/combobox-10yarn dlx shadcn@latest add @sevenui/component/combobox-10bunx --bun shadcn@latest add @sevenui/component/combobox-10"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxContent,
ComboboxEmpty,
ComboboxItem,
ComboboxList,
ComboboxValue,
useComboboxAnchor,
} from "@/components/ui/combobox";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const directory = [
{ value: "amara", label: "Amara Nwosu", email: "amara@northwind.io", member: false },
{ value: "ben", label: "Ben Hartley", email: "ben@northwind.io", member: true },
{ value: "chloe", label: "Chloé Martin", email: "chloe@northwind.io", member: false },
{ value: "diego", label: "Diego Alvarez", email: "diego@northwind.io", member: false },
{ value: "hana", label: "Hana Kobayashi", email: "hana@northwind.io", member: true },
{ value: "isaac", label: "Isaac Porter", email: "isaac@northwind.io", member: false },
{ value: "leila", label: "Leila Haddad", email: "leila@northwind.io", member: false },
{ value: "oscar", label: "Oscar Lindgren", email: "oscar@northwind.io", member: false },
];
type Person = (typeof directory)[number];
const roles = [
{ value: "viewer", label: "Viewer" },
{ value: "editor", label: "Editor" },
{ value: "admin", label: "Admin" },
];
const seatsLeft = 4;
export default function Combobox11() {
const anchor = useComboboxAnchor();
const [invitees, setInvitees] = React.useState<Person[]>([directory[2]]);
const [role, setRole] = React.useState<string | null>("editor");
const [sent, setSent] = React.useState<string | null>(null);
const overLimit = invitees.length > seatsLeft;
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
if (invitees.length === 0 || overLimit) return;
setSent(
`Invited ${invitees.length} ${invitees.length === 1 ? "person" : "people"} as ${role}.`,
);
setInvitees([]);
};
return (
<form
onSubmit={handleSubmit}
className="w-full max-w-md rounded-xl border bg-card p-5 text-card-foreground"
>
<h3 className="text-base font-medium">Invite to Northwind Design</h3>
<p className="mt-1 text-sm text-muted-foreground">
New members get access to every project in this workspace.
</p>
<div className="mt-5 flex flex-col gap-2">
<Label htmlFor="combobox-11-people">People</Label>
<Combobox
items={directory}
multiple
value={invitees}
onValueChange={(value) => {
setInvitees(value);
setSent(null);
}}
>
<ComboboxChips ref={anchor} className="w-full">
<ComboboxValue>
{(value: Person[]) => (
<React.Fragment>
{value.map((person) => (
<ComboboxChip key={person.value} aria-label={person.label}>
{person.label}
</ComboboxChip>
))}
<ComboboxChipsInput
id="combobox-11-people"
aria-describedby="combobox-11-seats"
aria-invalid={overLimit || undefined}
placeholder={value.length > 0 ? "" : "Name or email"}
/>
</React.Fragment>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxContent anchor={anchor}>
<ComboboxEmpty>No one in your directory matches.</ComboboxEmpty>
<ComboboxList>
{(item: Person) => (
<ComboboxItem
key={item.value}
value={item}
disabled={item.member}
className="py-1.5"
>
<span className="flex min-w-0 flex-1 flex-col">
<span className="truncate">{item.label}</span>
<span className="truncate text-xs text-muted-foreground">
{item.member ? "Already a member" : item.email}
</span>
</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
<p
id="combobox-11-seats"
className={
overLimit ? "text-xs text-destructive" : "text-xs text-muted-foreground"
}
>
{overLimit
? `Your plan has ${seatsLeft} seats left. Remove ${invitees.length - seatsLeft} to continue.`
: `${seatsLeft - invitees.length} of ${seatsLeft} open seats left after this invite.`}
</p>
</div>
<div className="mt-5 flex flex-col gap-3 border-t pt-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2">
<Label htmlFor="combobox-11-role" className="text-muted-foreground">
Role
</Label>
<Select items={roles} value={role} onValueChange={setRole}>
<SelectTrigger id="combobox-11-role" size="sm" className="w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
{roles.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button type="submit" disabled={invitees.length === 0 || overLimit}>
{invitees.length > 1 ? `Send ${invitees.length} invites` : "Send invite"}
</Button>
</div>
<p aria-live="polite" className="mt-3 text-xs text-muted-foreground empty:hidden">
{sent}
</p>
</form>
);
}
npx shadcn@latest add @sevenui/component/combobox-11pnpm dlx shadcn@latest add @sevenui/component/combobox-11yarn dlx shadcn@latest add @sevenui/component/combobox-11bunx --bun shadcn@latest add @sevenui/component/combobox-1145 min · You, Ava Brooks, Jonas Weber
Pick a new time
"use client";
import * as React from "react";
import { CalendarClock } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
import { Label } from "@/components/ui/label";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type Slot = {
value: string;
label: string;
conflict: string | null;
hostBusy: boolean;
};
const days = [
{ value: "tue", label: "Tue", long: "Tuesday, Oct 6" },
{ value: "wed", label: "Wed", long: "Wednesday, Oct 7" },
{ value: "thu", label: "Thu", long: "Thursday, Oct 8" },
];
const times = [
"9:00 AM",
"9:30 AM",
"10:00 AM",
"10:30 AM",
"11:00 AM",
"1:00 PM",
"1:30 PM",
"2:00 PM",
"3:00 PM",
"4:30 PM",
];
// Deterministic sample availability for each day.
const busy: Record<string, Record<string, string | "host">> = {
tue: { "9:00 AM": "host", "10:30 AM": "Jonas", "1:00 PM": "host", "2:00 PM": "Ava" },
wed: { "9:30 AM": "Ava", "11:00 AM": "host", "1:30 PM": "Jonas", "3:00 PM": "host" },
thu: { "10:00 AM": "host", "10:30 AM": "host", "2:00 PM": "Ava", "4:30 PM": "Jonas" },
};
function slotsFor(day: string): Slot[] {
return times.map((time) => {
const status = busy[day]?.[time];
return {
value: `${day}-${time}`,
label: time,
hostBusy: status === "host",
conflict: status && status !== "host" ? status : null,
};
});
}
export default function Combobox12() {
const [day, setDay] = React.useState("wed");
const [slot, setSlot] = React.useState<Slot | null>(null);
const [confirmed, setConfirmed] = React.useState<string | null>(null);
const slots = React.useMemo(() => slotsFor(day), [day]);
const dayLabel = days.find((item) => item.value === day)?.long ?? "";
return (
<div className="w-full max-w-sm rounded-xl border bg-card p-5 text-card-foreground">
<div className="flex items-start gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted">
<CalendarClock aria-hidden="true" className="size-4 text-muted-foreground" />
</div>
<div className="min-w-0">
<h3 className="text-sm font-medium">Reschedule: Q4 roadmap review</h3>
<p className="mt-0.5 text-sm text-muted-foreground">
45 min · You, Ava Brooks, Jonas Weber
</p>
</div>
</div>
<div className="mt-5 flex flex-col gap-2">
<span id="combobox-12-day-label" className="text-sm font-medium">
Day
</span>
<ToggleGroup
aria-labelledby="combobox-12-day-label"
variant="outline"
spacing={0}
value={[day]}
onValueChange={(next) => {
if (next.length === 0) return;
setDay(next[0]);
setSlot(null);
setConfirmed(null);
}}
className="w-full"
>
{days.map((item) => (
<ToggleGroupItem
key={item.value}
value={item.value}
className="flex-1 aria-pressed:bg-accent aria-pressed:text-accent-foreground"
>
{item.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<div className="mt-4 flex flex-col gap-2">
<Label htmlFor="combobox-12-time">Start time</Label>
<Combobox
items={slots}
value={slot}
onValueChange={(value) => {
setSlot(value);
setConfirmed(null);
}}
>
<ComboboxInput
id="combobox-12-time"
placeholder="Type a time, e.g. 2:00"
className="w-full"
/>
<ComboboxContent>
<ComboboxEmpty>No open slot at that time.</ComboboxEmpty>
<ComboboxList>
{(item: Slot) => (
<ComboboxItem key={item.value} value={item} disabled={item.hostBusy}>
<span className="w-18 tabular-nums">{item.label}</span>
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<span
aria-hidden="true"
className={
item.hostBusy
? "size-1.5 rounded-full bg-muted-foreground"
: item.conflict
? "size-1.5 rounded-full bg-warning"
: "size-1.5 rounded-full bg-success"
}
/>
{item.hostBusy
? "You're busy"
: item.conflict
? `${item.conflict} is busy`
: "Everyone free"}
</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{slot?.conflict && (
<p className="mt-3 rounded-lg bg-warning/10 px-3 py-2 text-xs text-foreground">
{slot.conflict} has another meeting then. They'll be asked to propose
a new time.
</p>
)}
<div className="mt-5 flex items-center justify-between gap-3 border-t pt-4">
<p aria-live="polite" className="min-w-0 text-xs text-muted-foreground">
{confirmed ?? (slot ? `${dayLabel} at ${slot.label}` : "Pick a new time")}
</p>
<Button
size="sm"
disabled={!slot}
onClick={() => slot && setConfirmed(`Update sent for ${dayLabel}, ${slot.label}.`)}
>
Send update
</Button>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/combobox-12pnpm dlx shadcn@latest add @sevenui/component/combobox-12yarn dlx shadcn@latest add @sevenui/component/combobox-12bunx --bun shadcn@latest add @sevenui/component/combobox-12"use client";
import * as React from "react";
import { GitBranch, Globe, Lock, Rocket } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field";
type Branch = { value: string; label: string; commit: string; message: string };
type Repo = {
value: string;
label: string;
private: boolean;
updated: string;
branches: Branch[];
};
const repos: Repo[] = [
{
value: "acme/storefront",
label: "acme/storefront",
private: true,
updated: "12m ago",
branches: [
{ value: "main", label: "main", commit: "a41f9c2", message: "Fix cart total rounding" },
{ value: "release/3.13", label: "release/3.13", commit: "7be02d1", message: "Bump version to 3.13.0" },
{ value: "feat/gift-cards", label: "feat/gift-cards", commit: "c90e4aa", message: "Add gift card balance check" },
],
},
{
value: "acme/marketing-site",
label: "acme/marketing-site",
private: false,
updated: "2h ago",
branches: [
{ value: "main", label: "main", commit: "3d18b77", message: "Update pricing page copy" },
{ value: "preview/launch-week", label: "preview/launch-week", commit: "e5c2f10", message: "Draft launch week banner" },
],
},
{
value: "acme/admin-dashboard",
label: "acme/admin-dashboard",
private: true,
updated: "yesterday",
branches: [
{ value: "main", label: "main", commit: "0f6ad3e", message: "Paginate order exports" },
{ value: "fix/csv-encoding", label: "fix/csv-encoding", commit: "b2291c4", message: "Write CSV with UTF-8 BOM" },
],
},
{
value: "acme/docs",
label: "acme/docs",
private: false,
updated: "3 days ago",
branches: [
{ value: "main", label: "main", commit: "91ac7e5", message: "Document webhooks retry policy" },
],
},
];
export default function Combobox13() {
const [repo, setRepo] = React.useState<Repo | null>(repos[0]);
const [branch, setBranch] = React.useState<Branch | null>(repos[0].branches[0]);
const [status, setStatus] = React.useState<string | null>(null);
return (
<form
className="w-full max-w-md rounded-xl border bg-card p-5 text-card-foreground"
onSubmit={(event) => {
event.preventDefault();
if (repo && branch) {
setStatus(`Queued deploy of ${repo.label}@${branch.commit} to Production.`);
}
}}
>
<h3 className="text-base font-medium">New deployment</h3>
<p className="mt-1 text-sm text-muted-foreground">
Pick the source to build and promote to production.
</p>
<FieldGroup className="mt-5 gap-4">
<Field>
<FieldLabel htmlFor="combobox-13-repo">Repository</FieldLabel>
<Combobox
items={repos}
value={repo}
onValueChange={(value) => {
setRepo(value);
// A branch only makes sense inside its repository; default to main.
setBranch(value ? value.branches[0] : null);
setStatus(null);
}}
>
<ComboboxInput
id="combobox-13-repo"
placeholder="Search repositories"
className="w-full"
/>
<ComboboxContent>
<ComboboxEmpty>No repository matches.</ComboboxEmpty>
<ComboboxList>
{(item: Repo) => (
<ComboboxItem key={item.value} value={item}>
{item.private ? (
<Lock aria-hidden="true" className="text-muted-foreground" />
) : (
<Globe aria-hidden="true" className="text-muted-foreground" />
)}
<span className="flex-1 truncate">{item.label}</span>
<span className="sr-only">{item.private ? "Private" : "Public"}</span>
<span className="text-xs text-muted-foreground">{item.updated}</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
<Field disabled={!repo}>
<FieldLabel htmlFor="combobox-13-branch">Branch</FieldLabel>
<Combobox
items={repo?.branches ?? []}
value={branch}
onValueChange={(value) => {
setBranch(value);
setStatus(null);
}}
disabled={!repo}
>
<ComboboxInput
id="combobox-13-branch"
placeholder={repo ? "Search branches" : "Choose a repository first"}
disabled={!repo}
className="w-full"
/>
<ComboboxContent>
<ComboboxEmpty>No branch with that name.</ComboboxEmpty>
<ComboboxList>
{(item: Branch) => (
<ComboboxItem key={item.value} value={item}>
<GitBranch aria-hidden="true" className="text-muted-foreground" />
<span className="flex-1 truncate font-mono text-xs">{item.label}</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
{branch && (
<FieldDescription className="truncate">
Latest commit{" "}
<span className="font-mono text-foreground">{branch.commit}</span> ·{" "}
{branch.message}
</FieldDescription>
)}
</Field>
</FieldGroup>
<div className="mt-5 flex flex-col gap-3 border-t pt-4 sm:flex-row sm:items-center sm:justify-between">
<p aria-live="polite" className="text-xs text-muted-foreground">
{status ?? "Build: pnpm build · Output: .next"}
</p>
<Button type="submit" disabled={!repo || !branch}>
<Rocket aria-hidden="true" data-icon="inline-start" />
Deploy
</Button>
</div>
</form>
);
}
npx shadcn@latest add @sevenui/component/combobox-13pnpm dlx shadcn@latest add @sevenui/component/combobox-13yarn dlx shadcn@latest add @sevenui/component/combobox-13bunx --bun shadcn@latest add @sevenui/component/combobox-13