"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 && (
)}
>
)}
);
})}
)}
);
}