"use client"; import React, { Fragment, useState } from "react"; import { Listbox, ListboxButton, ListboxOptions, ListboxOption, Transition, Combobox, ComboboxInput, ComboboxOptions, ComboboxOption } from "@headlessui/react"; import { FiChevronDown, FiCheck } from "react-icons/fi"; export interface SelectOption { value: string; label: string; disabled?: boolean; icon?: React.ComponentType<{ className?: string }>; description?: string; } export interface SelectDivider { type: "divider"; label?: string; } interface SelectProps { value: string; onChange: (value: string) => void; options: (SelectOption | SelectDivider)[]; placeholder?: string; disabled?: boolean; className?: string; dropdownClassName?: string; dropdownAlign?: "left" | "right"; id?: string; name?: string; enableFilter?: boolean; } function DividerContent({ divider, index }: { divider: SelectDivider; index: number }) { return (
{divider.label && (
{divider.label}
)}
); } export default function Select({ value, onChange, options, placeholder = "Select an option", disabled = false, className = "", dropdownClassName = "", dropdownAlign = "left", id, name, enableFilter = false, }: SelectProps) { const [query, setQuery] = useState(""); // Separate options and dividers const actualOptions = options.filter((opt): opt is SelectOption => "value" in opt); const selectedOption = actualOptions.find((opt) => opt.value === value); // Filter function for Combobox const filterOptions = (query: string) => { if (!query) return options; // When filtering, exclude dividers and return only filtered options return actualOptions.filter((opt) => opt.label.toLowerCase().includes(query.toLowerCase()) ); }; if (enableFilter) { // Use Combobox for filterable select return ( { onChange(newValue || ""); setQuery(""); }} disabled={disabled} immediate > {({ open }) => (
selectedOption?.label ?? ""} onChange={(event) => setQuery(event.target.value)} placeholder={placeholder} disabled={disabled} className={`relative h-11 w-full rounded-md bg-[var(--surface-2)] px-3 pr-10 text-left text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)] disabled:cursor-not-allowed disabled:opacity-50 ${ !selectedOption ? "text-foreground/60" : "" } ${className}`} /> {name && } {(() => { const displayItems = filterOptions(query); if (displayItems.length === 0) { return (
No results found
); } return displayItems.map((item, index) => { if ("type" in item && item.type === "divider") { return ; } const option = item as SelectOption; const Icon = option.icon; return ( `relative cursor-pointer select-none py-2 pl-10 pr-4 ${ focus ? "bg-black/5 dark:bg-white/10" : "" } ${option.disabled ? "opacity-50 cursor-not-allowed" : ""}` } > {({ selected }) => ( <> {Icon && } {option.label} {option.description && ( {option.description} )} {selected && ( )} )} ); }); })()}
)}
); } // Use Listbox for non-filterable select return ( {({ open }) => (
{selectedOption?.label || placeholder} {name && } {options.map((item, index) => { if ("type" in item && item.type === "divider") { return ; } const option = item as SelectOption; const Icon = option.icon; return ( `relative cursor-pointer select-none py-2 pl-10 pr-4 ${ focus ? "bg-black/5 dark:bg-white/10" : "" } ${option.disabled ? "opacity-50 cursor-not-allowed" : ""}` } > {({ selected }) => ( <> {Icon && } {option.label} {option.description && ( {option.description} )} {selected && ( )} )} ); })}
)}
); }