import React, { useState, useCallback } from 'react'; import { cn } from '@/lib/utils'; import FormLabel from './FormLabel'; import FormError from './FormError'; /** * FormSelect Component * Select dropdown with placeholder, multi-select support, and custom styling */ export interface SelectOption { value: string | number; label: string; disabled?: boolean; } export interface FormSelectProps extends Omit, 'multiple' | 'size'> { label?: string; error?: string | string[]; helpText?: string; required?: boolean; options: SelectOption[]; placeholder?: string; multiple?: boolean; showSearch?: boolean; containerClassName?: string; selectClassName?: string; onSearch?: (query: string) => void; } export const FormSelect: React.FC = ({ label, error, helpText, required = false, options, placeholder = 'Select an option', multiple = false, showSearch = false, containerClassName, selectClassName, onSearch, disabled = false, value, onChange, ...props }) => { const [isFocused, setIsFocused] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const hasError = !!error; const showError = hasError; const handleFocus = () => setIsFocused(true); const handleBlur = () => setIsFocused(false); const handleChange = useCallback((e: React.ChangeEvent) => { onChange?.(e); }, [onChange]); const handleSearchChange = useCallback((e: React.ChangeEvent) => { const query = e.target.value; setSearchQuery(query); if (onSearch) { onSearch(query); } }, [onSearch]); const filteredOptions = showSearch && searchQuery ? options.filter(option => option.label.toLowerCase().includes(searchQuery.toLowerCase()) ) : options; const inputId = props.id || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined); const baseSelectClasses = cn( 'w-full px-3 py-2 border rounded-md transition-all duration-200', 'bg-neutral-light text-text-primary', 'focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary', 'disabled:opacity-60 disabled:cursor-not-allowed', 'appearance-none cursor-pointer', 'bg-[length:1.5em_1.5em] bg-[position:right_0.5rem_center] bg-no-repeat', 'bg-[url("data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' fill=\'none\' viewBox=\'0 0 20 20\'%3e%3cpath stroke=\'%236c757d\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'1.5\' d=\'M6 8l4 4 4-4\'/%3e%3c/svg%3e")]', { 'border-neutral-dark hover:border-neutral-dark': !hasError && !isFocused, 'border-primary ring-2 ring-primary': isFocused && !hasError, 'border-danger ring-2 ring-danger/20': hasError, 'pr-10': !showSearch, }, selectClassName ); const containerClasses = cn( 'flex flex-col gap-1.5', containerClassName ); const searchInputClasses = cn( 'w-full px-3 py-2 border-b border-neutral-dark bg-transparent', 'focus:outline-none focus:border-primary', 'placeholder:text-text-light' ); // Custom dropdown arrow const dropdownArrow = ( ); const renderOptions = () => { // Add placeholder as first option if not multiple and no value const showPlaceholder = !multiple && !value && placeholder; return ( <> {showPlaceholder && ( )} {filteredOptions.map((option) => ( ))} ); }; return (
{label && ( {label} )}
{showSearch && ( )} {!showSearch && dropdownArrow}
{helpText && (

{helpText}

)} {showError && ( )}
); }; FormSelect.displayName = 'FormSelect'; export default FormSelect;