344 lines · 11.0 KB · TypeScript
RawDownload
"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 <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;} function ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) {  return (    <ComboboxPrimitive.Trigger      data-slot="combobox-trigger"      className={cn("[&_svg:not([class*='size-'])]:size-4", className)}      {...props}    >      {children}      <ChevronDownIcon        data-slot="combobox-trigger-icon"        className="pointer-events-none size-4 text-muted-foreground"      />    </ComboboxPrimitive.Trigger>  );} function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {  return (    <ComboboxPrimitive.Clear      data-slot="combobox-clear"      render={<InputGroupButton variant="ghost" size="icon-xs" />}      className={cn(className)}      {...props}    >      <XIcon className="pointer-events-none" />    </ComboboxPrimitive.Clear>  );} function ComboboxInput({  className,  inputClassName,  children,  disabled = false,  showTrigger = true,  showClear = false,  ...props}: ComboboxPrimitive.Input.Props & {  inputClassName?: string;  showTrigger?: boolean;  showClear?: boolean;}) {  return (    <InputGroup className={cn("w-auto", className)}>      <ComboboxPrimitive.Input        render={<InputGroupInput disabled={disabled} className={inputClassName} />}        {...props}      />      <InputGroupAddon align="inline-end">        {showTrigger && (          <InputGroupButton            size="icon-xs"            variant="ghost"            asChild            data-slot="input-group-button"            className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"            disabled={disabled}          >            <ComboboxTrigger />          </InputGroupButton>        )}        {showClear && <ComboboxClear disabled={disabled} />}      </InputGroupAddon>      {children}    </InputGroup>  );} 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 (    <ComboboxPrimitive.Portal>      <ComboboxPrimitive.Positioner        side={side}        sideOffset={sideOffset}        align={align}        alignOffset={alignOffset}        anchor={anchor}        className="isolate z-50"      >        <ComboboxPrimitive.Popup          data-slot="combobox-content"          data-chips={!!anchor}          className={cn(            "group/combobox-content relative max-h-96 w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",            className,          )}          {...props}        />      </ComboboxPrimitive.Positioner>    </ComboboxPrimitive.Portal>  );} function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {  return (    <ComboboxPrimitive.List      data-slot="combobox-list"      className={cn(        "max-h-[min(calc(--spacing(96)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto p-1 data-empty:p-0",        className,      )}      {...props}    />  );} function ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) {  return (    <ComboboxPrimitive.Item      data-slot="combobox-item"      className={cn(        "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",        className,      )}      {...props}    >      {children}      <ComboboxPrimitive.ItemIndicator        data-slot="combobox-item-indicator"        render={          <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />        }      >        <CheckIcon className="pointer-events-none size-4 pointer-coarse:size-5" />      </ComboboxPrimitive.ItemIndicator>    </ComboboxPrimitive.Item>  );} function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {  return (    <ComboboxPrimitive.Group data-slot="combobox-group" className={cn(className)} {...props} />  );} function ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) {  return (    <ComboboxPrimitive.GroupLabel      data-slot="combobox-label"      className={cn(        "px-2 py-1.5 text-xs text-muted-foreground pointer-coarse:px-3 pointer-coarse:py-2 pointer-coarse:text-sm",        className,      )}      {...props}    />  );} function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {  return <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />;} function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {  return (    <ComboboxPrimitive.Empty      data-slot="combobox-empty"      className={cn(        "hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex",        className,      )}      {...props}    />  );} function ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) {  return (    <ComboboxPrimitive.Separator      data-slot="combobox-separator"      className={cn("-mx-1 my-1 h-px bg-border", className)}      {...props}    />  );} function ComboboxChips({  className,  ...props}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> & ComboboxPrimitive.Chips.Props) {  return (    <ComboboxPrimitive.Chips      data-slot="combobox-chips"      className={cn(        "flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border border-input bg-transparent bg-clip-padding px-2.5 py-1.5 text-sm shadow-xs transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-[3px] has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1.5 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",        className,      )}      {...props}    />  );} function ComboboxChip({  className,  children,  showRemove = true,  ...props}: ComboboxPrimitive.Chip.Props & {  showRemove?: boolean;}) {  return (    <ComboboxPrimitive.Chip      data-slot="combobox-chip"      className={cn(        "flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",        className,      )}      {...props}    >      {children}      {showRemove && (        <ComboboxPrimitive.ChipRemove          render={<Button variant="ghost" size="icon-xs" />}          className="-ml-1 opacity-50 hover:opacity-100"          data-slot="combobox-chip-remove"        >          <XIcon className="pointer-events-none" />        </ComboboxPrimitive.ChipRemove>      )}    </ComboboxPrimitive.Chip>  );} function ComboboxChipsInput({ className, ...props }: ComboboxPrimitive.Input.Props) {  return (    <ComboboxPrimitive.Input      data-slot="combobox-chip-input"      className={cn("min-w-16 flex-1 outline-none", className)}      {...props}    />  );} function useComboboxAnchor() {  return React.useRef<HTMLDivElement | null>(null);} export interface FreeformComboboxOption {  readonly value: string;  readonly label?: React.ReactNode;  readonly description?: React.ReactNode;} 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;}) {  const valueOption = props.value.trim();  const options =    valueOption.length > 0 && !props.options.some((option) => option.value === valueOption)      ? [{ value: valueOption, label: valueOption }, ...props.options]      : props.options;  const selectedValue = options.some((option) => option.value === props.value) ? props.value : null;   return (    <Combobox      items={options.map((option) => option.value)}      inputValue={props.value}      value={selectedValue}      onInputValueChange={props.onValueChange}      onValueChange={(value) => {        if (value !== null) props.onValueChange(value);      }}    >      <ComboboxInput        placeholder={props.placeholder}        className={props.className}        inputClassName={props.inputClassName}        disabled={props.disabled}        showClear={props.value.length > 0}      />      <ComboboxContent>        <ComboboxEmpty>{props.emptyLabel ?? "No options"}</ComboboxEmpty>        <ComboboxList>          {options.map((option) => (            <ComboboxItem key={option.value} value={option.value}>              <div className="min-w-0 flex-1">                <div className="truncate">{option.label ?? option.value}</div>                {option.description && (                  <div className="mt-0.5 truncate text-[10px] text-muted-foreground">                    {option.description}                  </div>                )}              </div>            </ComboboxItem>          ))}        </ComboboxList>      </ComboboxContent>    </Combobox>  );} export {  Combobox,  ComboboxInput,  ComboboxContent,  ComboboxList,  ComboboxItem,  ComboboxGroup,  ComboboxLabel,  ComboboxCollection,  ComboboxEmpty,  ComboboxSeparator,  ComboboxChips,  ComboboxChip,  ComboboxChipsInput,  ComboboxTrigger,  ComboboxValue,  FreeformCombobox,  useComboboxAnchor,};