'use client' import * as React from 'react' import { Autocomplete as BaseAutocomplete } from '@base-ui/react/autocomplete' import { cn } from '../../internal/utils' import { type FloatingContentProps, splitFloatingProps } from '../../internal/floating' import { inputVariants } from '../input/input-variants' type AutocompleteSize = 'sm' | 'md' | 'lg' type AutocompleteVariant = 'outline' | 'soft' interface AutocompleteContextValue { size: AutocompleteSize variant: AutocompleteVariant clearable: boolean icon: boolean grid: boolean } const AutocompleteContext = React.createContext(null) function useAutocompleteContext() { const ctx = React.useContext(AutocompleteContext) if (!ctx) { throw new Error('Autocomplete sub-components must be rendered inside ') } return ctx } type BaseAutocompleteRootProps = React.ComponentProps interface AutocompleteProps extends BaseAutocompleteRootProps { /** * Input height, popup radius, and item sizing. * @default 'md' */ size?: AutocompleteSize /** * Input appearance - bordered or filled. * @default 'outline' */ variant?: AutocompleteVariant /** * Render a clear button inside the input when a value is present. * @default false */ clearable?: boolean /** * Render a chevron button that toggles the popup. * @default false */ icon?: boolean } function Autocomplete({ size = 'md', variant = 'outline', clearable = false, icon = false, grid = false, children, ...rest }: AutocompleteProps) { const ctx = React.useMemo( () => ({ size, variant, clearable, icon, grid }), [size, variant, clearable, icon, grid], ) return ( {children} ) } const ICON_SIZE: Record = { sm: 'size-4', md: 'size-4.5', lg: 'size-5', } interface AutocompleteInputProps extends Omit, 'size'> { /** Adornment rendered before the field, inside the input frame. */ startSlot?: React.ReactNode /** Adornment rendered after the field, inside the input frame. */ endSlot?: React.ReactNode } function AutocompleteInput({ className, startSlot, endSlot, placeholder, disabled, ...props }: AutocompleteInputProps) { const { size, variant, clearable, icon } = useAutocompleteContext() return ( {startSlot && (
{startSlot}
)} {clearable && } {endSlot && (
{endSlot}
)} {icon && }
) } function AutocompleteClearButton({ disabled }: { disabled?: boolean }) { return ( ) } function AutocompleteIconButton({ disabled }: { disabled?: boolean }) { const { size } = useAutocompleteContext() return ( ) } type AutocompleteTriggerProps = React.ComponentProps function AutocompleteTrigger({ className, ...props }: AutocompleteTriggerProps) { return ( ) } type AutocompleteValueProps = React.ComponentProps function AutocompleteValue(props: AutocompleteValueProps) { return } const POPUP_RADIUS: Record = { sm: 'rounded-md', md: 'rounded-lg', lg: 'rounded-xl', } type AutocompleteContentProps = React.ComponentProps & FloatingContentProps< React.ComponentProps, React.ComponentProps > function AutocompleteContent({ className, children, ...props }: AutocompleteContentProps) { const { size } = useAutocompleteContext() const { positioner, portal, popup } = splitFloatingProps(props) return ( {children} ) } interface AutocompleteListProps extends Omit, 'children'> { /** Items per row in grid mode (defaults to 2 when `grid` is set). */ cols?: number /** A render function over the filtered items, or static `AutocompleteItem`s. */ children?: React.ReactNode | ((item: T, index: number) => React.ReactNode) } const LIST_CLASSNAME = 'flex flex-col gap-0.5 px-2 min-h-0 flex-1 overflow-y-auto overscroll-contain' function cellKey(item: unknown, fallback: number): React.Key { if (item == null) return fallback if (typeof item === 'object') { const rec = item as Record const candidate = rec.value ?? rec.id ?? rec.key if (typeof candidate === 'string' || typeof candidate === 'number') return candidate return fallback } return item as React.Key } function AutocompleteList({ className, cols, children, ...props }: AutocompleteListProps) { const { grid } = useAutocompleteContext() const effectiveCols = cols ?? (grid ? 2 : undefined) if (effectiveCols && effectiveCols > 1 && typeof children === 'function') { return ( ) } return ( {children as React.ComponentProps['children']} ) } function AutocompleteGridRows({ cols, render, }: { cols: number render: (item: any, index: number) => React.ReactNode }) { const filtered = BaseAutocomplete.useFilteredItems() const rows = React.useMemo(() => { const out: unknown[][] = [] for (let i = 0; i < filtered.length; i += cols) out.push(filtered.slice(i, i + cols)) return out }, [filtered, cols]) return ( <> {rows.map((row, rowIndex) => ( {row.map((item, colIndex) => { const globalIndex = rowIndex * cols + colIndex return {render(item, globalIndex)} })} ))} ) } interface AutocompleteRowProps extends React.ComponentProps {} function AutocompleteRow({ className, ...props }: AutocompleteRowProps) { return ( ) } const ITEM_SIZE: Record = { sm: "gap-1 rounded-xs py-1.5 px-2.5 text-xs has-data-[icon=end]:pe-1.5 has-data-[icon=start]:ps-1.5 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='stroke-'])]:stroke-[1.75]", md: "gap-1.5 rounded-sm py-2 px-3 text-sm has-data-[icon=end]:pe-2 has-data-[icon=start]:ps-2 [&_svg:not([class*='size-'])]:size-4.5 [&_svg:not([class*='stroke-'])]:stroke-[1.65]", lg: "gap-1.5 rounded-md py-2.5 px-3.5 text-base has-data-[icon=end]:pe-2.5 has-data-[icon=start]:ps-2.5 [&_svg:not([class*='size-'])]:size-5 [&_svg:not([class*='stroke-'])]:stroke-[1.65]", } const ITEM_TEXT_SIZE: Record = { sm: 'gap-1', md: 'gap-1.5', lg: 'gap-1.5', } interface AutocompleteItemProps extends React.ComponentProps {} function AutocompleteItem({ className, children, ...props }: AutocompleteItemProps) { const { size } = useAutocompleteContext() return ( {children} ) } const EMPTY_SIZE: Record = { sm: 'px-2.5 py-2 text-xs', md: 'px-3 py-2.5 text-sm', lg: 'px-3.5 py-3 text-base', } interface AutocompleteEmptyProps extends React.ComponentProps {} function AutocompleteEmpty({ className, ...props }: AutocompleteEmptyProps) { const { size } = useAutocompleteContext() return (