435 lines · 15.5 KB · TypeScript
RawDownload
"use client"; import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox";import { ChevronsUpDownIcon, SearchIcon, XIcon } from "lucide-react";import * as React from "react"; import { cn } from "~/lib/utils";import { Input } from "~/components/ui/input";import { ScrollArea } from "~/components/ui/scroll-area"; const ComboboxContext = React.createContext<{  chipsRef: React.RefObject<Element | null> | null;  multiple: boolean;}>({  chipsRef: null,  multiple: false,}); function Combobox<Value, Multiple extends boolean | undefined = false>(  props: ComboboxPrimitive.Root.Props<Value, Multiple>,) {  const chipsRef = React.useRef<Element | null>(null);  const value = React.useMemo(() => ({ chipsRef, multiple: !!props.multiple }), [props.multiple]);  return (    <ComboboxContext value={value}>      <ComboboxPrimitive.Root {...props} />    </ComboboxContext>  );} function ComboboxChipsInput({  className,  size,  ...props}: Omit<ComboboxPrimitive.Input.Props, "size"> & {  size?: "sm" | "default" | "lg" | number;  ref?: React.Ref<HTMLInputElement>;}) {  const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;   return (    <ComboboxPrimitive.Input      className={cn(        "min-w-12 flex-1 text-base outline-none sm:text-sm [[data-slot=combobox-chip]+&]:ps-0.5",        sizeValue === "sm" ? "ps-1.5" : "ps-2",        className,      )}      data-size={typeof sizeValue === "string" ? sizeValue : undefined}      data-slot="combobox-chips-input"      size={typeof sizeValue === "number" ? sizeValue : undefined}      {...props}    />  );} function ComboboxInput({  className,  inputClassName,  showTrigger = true,  showClear = false,  startAddon,  size,  unstyled = false,  ...props}: Omit<ComboboxPrimitive.Input.Props, "size"> & {  inputClassName?: string;  showTrigger?: boolean;  showClear?: boolean;  startAddon?: React.ReactNode;  size?: "sm" | "default" | "lg" | number;  unstyled?: boolean;  ref?: React.Ref<HTMLInputElement>;}) {  const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;   return (    <div className="relative not-has-[>*.w-full]:w-fit w-full text-foreground has-disabled:opacity-64">      {startAddon && (        <div          aria-hidden="true"          className="[&_svg]:-mx-0.5 pointer-events-none absolute inset-y-0 start-px z-10 flex items-center ps-[calc(--spacing(3)-1px)] opacity-80 has-[+[data-size=sm]]:ps-[calc(--spacing(2.5)-1px)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4"          data-slot="combobox-start-addon"        >          {startAddon}        </div>      )}      <ComboboxPrimitive.Input        className={cn(          startAddon &&            "data-[size=sm]:*:data-[slot=combobox-input]:ps-[calc(--spacing(7.5)-1px)] *:data-[slot=combobox-input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=combobox-input]:ps-[calc(--spacing(7)-1px)] sm:*:data-[slot=combobox-input]:ps-[calc(--spacing(8)-1px)]",          sizeValue === "sm"            ? "has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=combobox-input]:pe-6.5"            : "has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=combobox-input]:pe-7",          className,        )}        data-slot="combobox-input"        render={          <Input            className={cn("has-disabled:opacity-100", inputClassName)}            nativeInput            size={sizeValue}            unstyled={unstyled}          />        }        {...props}      />      {showTrigger && (        <ComboboxTrigger          className={cn(            "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-opacity pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=combobox-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",            sizeValue === "sm" ? "end-0" : "end-0.5",          )}        >          <ComboboxPrimitive.Icon data-slot="combobox-icon">            <ChevronsUpDownIcon />          </ComboboxPrimitive.Icon>        </ComboboxTrigger>      )}      {showClear && (        <ComboboxClear          className={cn(            "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-opacity pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=combobox-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",            sizeValue === "sm" ? "end-0" : "end-0.5",          )}        >          <XIcon />        </ComboboxClear>      )}    </div>  );} function ComboboxSearchInput(props: React.ComponentProps<typeof ComboboxInput>) {  return (    <div className="shrink-0 px-3 pt-2.5">      <div className="relative -translate-y-px border-b border-border/70 pb-1.5 transition-colors focus-within:border-ring">        <SearchIcon          aria-hidden="true"          className="pointer-events-none absolute top-1.5 left-0 size-4 shrink-0 text-muted-foreground/55"        />        <ComboboxInput          {...props}          className="[&_input]:h-6.5 [&_input]:ps-5 [&_input]:font-sans [&_input]:leading-6.5"          inputClassName="rounded-none bg-transparent text-sm"          showTrigger={false}          size="sm"          unstyled        />      </div>    </div>  );} function ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) {  return (    <ComboboxPrimitive.Trigger className={className} data-slot="combobox-trigger" {...props}>      {children}    </ComboboxPrimitive.Trigger>  );} function ComboboxPopup({  className,  children,  side = "bottom",  sideOffset = 4,  alignOffset,  align = "start",  anchor: anchorProp,  ...props}: ComboboxPrimitive.Popup.Props & {  align?: ComboboxPrimitive.Positioner.Props["align"];  sideOffset?: ComboboxPrimitive.Positioner.Props["sideOffset"];  alignOffset?: ComboboxPrimitive.Positioner.Props["alignOffset"];  side?: ComboboxPrimitive.Positioner.Props["side"];  anchor?: ComboboxPrimitive.Positioner.Props["anchor"];}) {  const { chipsRef, multiple } = React.use(ComboboxContext);  const anchor = anchorProp ?? (multiple ? chipsRef : undefined);   return (    <ComboboxPrimitive.Portal>      <ComboboxPrimitive.Positioner        align={align}        alignOffset={alignOffset}        anchor={anchor}        className="z-[130] select-none"        data-slot="combobox-positioner"        side={side}        sideOffset={sideOffset}      >        <span          className={cn(            "dropdown-glass relative flex max-h-full min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) rounded-lg shadow-[0_16px_40px_-18px_rgb(0_0_0/55%)] transition-[scale,opacity] dark:shadow-[0_18px_44px_-18px_rgb(0_0_0/80%)]",            className,          )}        >          <ComboboxPrimitive.Popup            className="flex min-w-0 max-h-[min(var(--available-height),23rem)] flex-1 flex-col overflow-hidden text-foreground"            data-slot="combobox-popup"            {...props}          >            {children}          </ComboboxPrimitive.Popup>        </span>      </ComboboxPrimitive.Positioner>    </ComboboxPrimitive.Portal>  );} function ComboboxItem({  className,  contentClassName,  children,  hideIndicator: _hideIndicator = false,  ...props}: ComboboxPrimitive.Item.Props & {  contentClassName?: string;  hideIndicator?: boolean;}) {  return (    <ComboboxPrimitive.Item      className={cn(        "flex min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-pointer items-center rounded-sm px-2 py-1 text-base outline-none hover:bg-accent data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-selected:bg-foreground/[0.08] data-selected:text-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground [&[data-highlighted][data-selected]]:bg-accent [&[data-highlighted][data-selected]]:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",        className,      )}      data-slot="combobox-item"      {...props}    >      <div        className={cn(          "min-w-0 flex-1 [&_svg:not([class*='text-'])]:text-muted-foreground",          contentClassName,        )}        data-slot="combobox-item-content"      >        {children}      </div>    </ComboboxPrimitive.Item>  );} function ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) {  return (    <ComboboxPrimitive.Separator      className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}      data-slot="combobox-separator"      {...props}    />  );} function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {  return (    <ComboboxPrimitive.Group      className={cn("[[role=group]+&]:mt-1.5", className)}      data-slot="combobox-group"      {...props}    />  );} function ComboboxGroupLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) {  return (    <ComboboxPrimitive.GroupLabel      className={cn("px-2 py-1.5 font-medium text-muted-foreground text-xs", className)}      data-slot="combobox-group-label"      {...props}    />  );} function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {  return (    <ComboboxPrimitive.Empty      className={cn(        "not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm",        className,      )}      data-slot="combobox-empty"      {...props}    />  );} function ComboboxRow({ className, ...props }: ComboboxPrimitive.Row.Props) {  return <ComboboxPrimitive.Row className={className} data-slot="combobox-row" {...props} />;} function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {  return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;} function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {  return (    <ScrollArea scrollbarGutter scrollFade>      <ComboboxPrimitive.List        className={cn("not-empty:scroll-py-1 not-empty:px-1 not-empty:py-1", className)}        data-slot="combobox-list"        {...props}      />    </ScrollArea>  );} /** * A variant of `ComboboxList` without `ScrollArea`, for use when * an external virtualizer (e.g. LegendList) owns the scroll container. */function ComboboxListVirtualized({ className, ...props }: ComboboxPrimitive.List.Props) {  return (    <ComboboxPrimitive.List      className={cn("not-empty:px-1 not-empty:py-1", className)}      data-slot="combobox-list"      {...props}    />  );} function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {  return <ComboboxPrimitive.Clear className={className} data-slot="combobox-clear" {...props} />;} function ComboboxStatus({ className, ...props }: ComboboxPrimitive.Status.Props) {  return (    <ComboboxPrimitive.Status      className={cn(        "px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0",        className,      )}      data-slot="combobox-status"      {...props}    />  );} function ComboboxCollection(props: ComboboxPrimitive.Collection.Props) {  return <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />;} function ComboboxChips({  className,  children,  startAddon,  ...props}: ComboboxPrimitive.Chips.Props & {  startAddon?: React.ReactNode;}) {  const { chipsRef } = React.use(ComboboxContext);   return (    <ComboboxPrimitive.Chips      className={cn(        "relative inline-flex min-h-9 w-full flex-wrap gap-1 rounded-lg border border-input bg-background not-dark:bg-clip-padding p-[calc(--spacing(1)-1px)] text-base shadow-xs/5 outline-none ring-ring/24 transition-shadow *:min-h-7 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-within:border-ring focus-within:ring-[3px] has-disabled:pointer-events-none has-data-[size=lg]:min-h-10 has-data-[size=sm]:min-h-8 has-aria-invalid:border-destructive/36 has-autofill:bg-foreground/4 has-disabled:opacity-64 has-[:disabled,:focus-within,[aria-invalid]]:shadow-none focus-within:has-aria-invalid:border-destructive/64 focus-within:has-aria-invalid:ring-destructive/16 has-data-[size=lg]:*:min-h-8 has-data-[size=sm]:*:min-h-6 sm:min-h-8 sm:text-sm sm:has-data-[size=lg]:min-h-9 sm:has-data-[size=sm]:min-h-7 sm:*:min-h-6 sm:has-data-[size=lg]:*:min-h-7 sm:has-data-[size=sm]:*:min-h-5 dark:not-has-disabled:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",        className,      )}      data-slot="combobox-chips"      ref={chipsRef as React.Ref<HTMLDivElement> | null}      {...props}    >      {startAddon && (        <div          aria-hidden="true"          className="[&_svg]:-ms-0.5 [&_svg]:-me-1.5 flex shrink-0 items-center ps-2 opacity-80 has-[~[data-size=sm]]:has-[+[data-slot=combobox-chip]]:pe-1.5 has-[~[data-size=sm]]:ps-1.5 has-[+[data-slot=combobox-chip]]:pe-2 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none"          data-slot="combobox-start-addon"        >          {startAddon}        </div>      )}      {children}    </ComboboxPrimitive.Chips>  );} function ComboboxChip({ children, ...props }: ComboboxPrimitive.Chip.Props) {  const labelId = React.useId();   return (    <ComboboxPrimitive.Chip      className="flex items-center rounded-[calc(var(--radius-md)-1px)] bg-accent ps-2 font-medium text-accent-foreground text-sm outline-none sm:text-xs/(--text-xs--line-height) [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5"      data-slot="combobox-chip"      {...props}    >      <span id={labelId}>{children}</span>      <ComboboxChipRemove labelId={labelId} />    </ComboboxPrimitive.Chip>  );} function ComboboxChipRemove({  labelId,  ...props}: ComboboxPrimitive.ChipRemove.Props & { labelId: string }) {  const removeLabelId = `${labelId}-remove`;   return (    <ComboboxPrimitive.ChipRemove      aria-labelledby={`${removeLabelId} ${labelId}`}      className="h-full shrink-0 cursor-pointer px-1.5 opacity-80 hover:opacity-100 [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5"      data-slot="combobox-chip-remove"      {...props}    >      <span id={removeLabelId} className="sr-only">        Remove      </span>      <XIcon aria-hidden="true" />    </ComboboxPrimitive.ChipRemove>  );} const useComboboxFilter = ComboboxPrimitive.useFilter; export {  Combobox,  ComboboxChipsInput,  ComboboxInput,  ComboboxSearchInput,  ComboboxTrigger,  ComboboxPopup,  ComboboxItem,  ComboboxSeparator,  ComboboxGroup,  ComboboxGroupLabel,  ComboboxEmpty,  ComboboxValue,  ComboboxList,  ComboboxListVirtualized,  ComboboxClear,  ComboboxStatus,  ComboboxRow,  ComboboxCollection,  ComboboxChips,  ComboboxChip,  useComboboxFilter,};