"use client"; import * as React from "react"; import { Combobox as ComboboxPrimitive } from "@base-ui/react"; import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"; import { cn } from "../lib/utils"; import { Button } from "./button"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "./input-group"; const Combobox = ComboboxPrimitive.Root; function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { return ; } function ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) { return ( {children} ); } function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { return ( } className={cn(className)} {...props} > ); } function ComboboxInput({ className, inputClassName, children, disabled = false, showTrigger = true, showClear = false, ...props }: ComboboxPrimitive.Input.Props & { inputClassName?: string; showTrigger?: boolean; showClear?: boolean; }) { return ( } {...props} /> {showTrigger && ( )} {showClear && } {children} ); } function ComboboxContent({ className, side = "bottom", sideOffset = 6, align = "start", alignOffset = 0, anchor, ...props }: ComboboxPrimitive.Popup.Props & Pick< ComboboxPrimitive.Positioner.Props, "side" | "align" | "sideOffset" | "alignOffset" | "anchor" >) { return ( ); } function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { return ( ); } function ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) { return ( {children} } > ); } function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { return ( ); } function ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) { return ( ); } function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { return ; } function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { return ( ); } function ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) { return ( ); } function ComboboxChips({ className, ...props }: React.ComponentPropsWithRef & ComboboxPrimitive.Chips.Props) { return ( ); } function ComboboxChip({ className, children, showRemove = true, ...props }: ComboboxPrimitive.Chip.Props & { showRemove?: boolean; }) { return ( {children} {showRemove && ( } className="-ml-1 opacity-50 hover:opacity-100" data-slot="combobox-chip-remove" > )} ); } function ComboboxChipsInput({ className, ...props }: ComboboxPrimitive.Input.Props) { return ( ); } function useComboboxAnchor() { return React.useRef(null); } export interface FreeformComboboxOption { readonly value: string; readonly label?: React.ReactNode; readonly description?: React.ReactNode; } /** How many options the popup renders at once. base-ui slices the filtered list * to this, so a multi-thousand-operation spec stays cheap: the empty-query open * shows the top slice, and typing narrows to the top matches. */ const FREEFORM_COMBOBOX_LIMIT = 100; function FreeformCombobox(props: { readonly value: string; readonly onValueChange: (value: string) => void; readonly options: readonly FreeformComboboxOption[]; readonly placeholder?: string; readonly emptyLabel?: React.ReactNode; readonly className?: string; readonly inputClassName?: string; readonly disabled?: boolean; /** Forwarded to the underlying text input so a `` can target it * (and tests get a stable selector). */ readonly id?: string; }) { const valueOption = props.value.trim(); const propsOptions = props.options; const options = React.useMemo( () => valueOption.length > 0 && !propsOptions.some((option) => option.value === valueOption) ? [{ value: valueOption, label: valueOption }, ...propsOptions] : propsOptions, [valueOption, propsOptions], ); const selectedValue = options.some((option) => option.value === props.value) ? props.value : null; const byValue = React.useMemo(() => { const map = new Map(); for (const option of options) map.set(option.value, option); return map; }, [options]); const items = React.useMemo(() => options.map((option) => option.value), [options]); // base-ui only filters the popup when the list renders from `items` (the // function-child / closed-template path below); a statically-mapped list never // narrows as you type. Match the query against the value AND its string // label/description, so typing a summary word or the human label finds the // option, not just its raw id. const filter = React.useCallback( (item: string, query: string) => { const needle = query.trim().toLowerCase(); if (needle === "") return true; const option = byValue.get(item); const haystacks = [ item, typeof option?.label === "string" ? option.label : "", typeof option?.description === "string" ? option.description : "", ]; return haystacks.some((part) => part.toLowerCase().includes(needle)); }, [byValue], ); return ( { if (value !== null) props.onValueChange(value); }} > 0} /> {props.emptyLabel ?? "No options"} {(item: string) => { const option = byValue.get(item); return ( {option?.label ?? item} {option?.description && ( {option.description} )} ); }} ); } export { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxGroup, ComboboxLabel, ComboboxCollection, ComboboxEmpty, ComboboxSeparator, ComboboxChips, ComboboxChip, ComboboxChipsInput, ComboboxTrigger, ComboboxValue, FreeformCombobox, useComboboxAnchor, };