import React, { useState, useRef, useEffect } from 'react' import ReactDOM from 'react-dom' import { ChevronDown, Check } from 'lucide-react' interface SelectOption { value: string label: string icon?: React.ReactNode isHeader?: boolean searchLabel?: string groupLabel?: string } interface CustomSelectProps { value: string onChange: (value: string) => void options?: SelectOption[] placeholder?: string searchable?: boolean style?: React.CSSProperties size?: 'sm' | 'md' disabled?: boolean } export default function CustomSelect({ value, onChange, options = [], placeholder = '', searchable = false, style = {}, size = 'md', disabled = false, }: CustomSelectProps) { const [open, setOpen] = useState(false) const [search, setSearch] = useState('') const ref = useRef(null) const dropRef = useRef(null) const searchRef = useRef(null) useEffect(() => { if (open && searchable && searchRef.current) searchRef.current.focus() }, [open, searchable]) useEffect(() => { const handleClick = (e: MouseEvent) => { if (ref.current?.contains(e.target as Node)) return if (dropRef.current?.contains(e.target as Node)) return setOpen(false) } if (open) document.addEventListener('mousedown', handleClick) return () => document.removeEventListener('mousedown', handleClick) }, [open]) const selected = options.find(o => o.value === value) const filtered = searchable && search ? (() => { const q = search.toLowerCase() const result: SelectOption[] = [] let currentHeader: SelectOption | null = null let headerAdded = false for (const o of options) { if (o.isHeader) { currentHeader = o headerAdded = false continue } const haystack = [o.label, o.searchLabel, o.groupLabel].filter(Boolean).join(' ').toLowerCase() if (haystack.includes(q)) { if (currentHeader && !headerAdded) { result.push(currentHeader) headerAdded = true } result.push(o) } } return result })() : options const sm = size === 'sm' return (
{/* Trigger */} {/* Dropdown */} {open && ReactDOM.createPortal(
{ const r = ref.current?.getBoundingClientRect() if (!r) return { top: 0, left: 0, width: 200 } const spaceBelow = window.innerHeight - r.bottom const openUp = spaceBelow < 220 && r.top > spaceBelow return openUp ? { bottom: window.innerHeight - r.top + 4, left: r.left, width: r.width } : { top: r.bottom + 4, left: r.left, width: r.width } })(), zIndex: 99999, background: 'var(--bg-card)', backdropFilter: 'blur(24px) saturate(180%)', WebkitBackdropFilter: 'blur(24px) saturate(180%)', border: '1px solid var(--border-primary)', borderRadius: 10, boxShadow: '0 8px 32px rgba(0,0,0,0.12)', overflow: 'hidden', animation: 'trek-menu-enter 200ms cubic-bezier(0.23, 1, 0.32, 1)', transformOrigin: 'top center', willChange: 'transform, opacity', }}> {/* Search */} {searchable && (
setSearch(e.target.value)} placeholder="..." style={{ width: '100%', border: '1px solid var(--border-secondary)', borderRadius: 6, padding: '5px 8px', fontSize: 12, outline: 'none', fontFamily: 'inherit', background: 'var(--bg-secondary)', color: 'var(--text-primary)', boxSizing: 'border-box', }} />
)} {/* Options */}
{filtered.length === 0 ? (
) : ( filtered.map(option => { if (option.isHeader) { return (
{option.label}
) } const isSelected = option.value === value return ( ) }) )}
, document.body )}
) }