import React, { useMemo } from 'react'; import Select, { StylesConfig, GroupBase } from 'react-select'; import { useUiStore } from '../../stores/uiStore'; export interface SelectOption { value: string | number; label: string; } interface Props { options: SelectOption[]; value?: string | number | null; onChange?: (value: string | number | null) => void; placeholder?: string; isLoading?: boolean; isDisabled?: boolean; isClearable?: boolean; noOptionsMessage?: string; inputId?: string; height?: number; /** * نام دسترس‌پذیر فیلد. react-select ورودی داخلی خودش را بدون label رندر می‌کند و * placeholder را هم به‌صورت div می‌گذارد نه attribute، پس بدون این، فیلد برای * screen reader بی‌نام می‌ماند. پیش‌فرض روی placeholder می‌افتد. */ ariaLabel?: string; /** اگر label قابل‌مشاهده‌ای وجود دارد، id آن را بده (بر ariaLabel اولویت دارد). */ ariaLabelledBy?: string; } export default function SearchableSelect({ options, value, onChange, placeholder = 'انتخاب کنید...', isLoading, isDisabled, isClearable, noOptionsMessage = 'موردی یافت نشد', inputId, height = 42, ariaLabel, ariaLabelledBy, }: Props) { const darkMode = useUiStore((s) => s.darkMode); // مقایسهٔ نرم (string↔number): مقدار پیش‌فرض ممکن است number باشد ولی value گزینه String // (مثلاً id بیمه). تطبیق سخت‌گیرانه در این حالت گزینه را خالی نشان می‌داد. const selected = useMemo( () => (value == null || value === '' ? null : options.find((o) => String(o.value) === String(value)) ?? null), [options, value], ); const styles: StylesConfig> = { control: (base, state) => ({ ...base, background: state.isFocused ? 'var(--surface)' : 'var(--surface-2)', borderColor: state.isFocused ? 'var(--primary)' : 'var(--border)', boxShadow: state.isFocused ? '0 0 0 4px var(--ring)' : 'none', borderRadius: 'var(--r-sm)', minHeight: height, fontSize: 14, cursor: 'pointer', '&:hover': { borderColor: state.isFocused ? 'var(--primary)' : 'var(--border-2)', }, }), valueContainer: (base) => ({ ...base, padding: '0 14px' }), menu: (base) => ({ ...base, background: 'var(--surface)', border: '1px solid var(--border)', boxShadow: 'var(--shadow-lg)', borderRadius: 'var(--r)', overflow: 'hidden', marginTop: 4, }), menuPortal: (base) => ({ ...base, zIndex: 9999 }), option: (base, state) => ({ ...base, background: state.isSelected ? 'var(--primary-soft2)' : state.isFocused ? 'var(--surface-2)' : 'transparent', color: state.isSelected ? 'var(--primary-700)' : 'var(--text)', fontWeight: state.isSelected ? 700 : 400, fontSize: 14, cursor: 'pointer', padding: '9px 14px', }), singleValue: (base) => ({ ...base, color: 'var(--text)' }), input: (base) => ({ ...base, color: 'var(--text)', margin: 0, padding: 0 }), placeholder: (base) => ({ ...base, color: 'var(--text-3)' }), indicatorSeparator: (base) => ({ ...base, background: 'var(--border)' }), dropdownIndicator: (base) => ({ ...base, color: 'var(--text-3)', padding: '0 10px', '&:hover': { color: 'var(--text-2)' }, }), clearIndicator: (base) => ({ ...base, color: 'var(--text-3)', padding: '0 6px', '&:hover': { color: 'var(--danger)' }, }), loadingIndicator: (base) => ({ ...base, color: 'var(--primary)' }), noOptionsMessage: (base) => ({ ...base, color: 'var(--text-3)', fontSize: 13 }), loadingMessage: (base) => ({ ...base, color: 'var(--text-3)', fontSize: 13 }), }; return ( options={options} value={selected} onChange={(opt) => onChange?.(opt ? opt.value : null)} placeholder={placeholder} isLoading={isLoading} isDisabled={isDisabled} isClearable={isClearable} styles={styles} isRtl menuPortalTarget={typeof document !== 'undefined' ? document.body : undefined} menuPosition="fixed" noOptionsMessage={() => noOptionsMessage} loadingMessage={() => 'در حال بارگذاری...'} inputId={inputId} aria-labelledby={ariaLabelledBy} aria-label={ariaLabelledBy ? undefined : (ariaLabel ?? placeholder)} /> ); }