initial: standalone repo from monorepo split
This commit is contained in:
130
src/components/ui/select.tsx
Normal file
130
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
'use client';
|
||||
|
||||
import { useState, forwardRef, useRef, useEffect } from 'react';
|
||||
|
||||
interface SelectProps {
|
||||
value?: string | string[];
|
||||
onChange: (value: string | string[]) => void;
|
||||
options: { label: string; value: string }[];
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
const Select = forwardRef<HTMLDivElement, SelectProps>(
|
||||
({ value, onChange, options, placeholder, disabled = false, multiple = false }, ref) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const selectRef = useRef<HTMLDivElement>(null);
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isMulti = multiple;
|
||||
const currentValue = value;
|
||||
|
||||
const selectedLabels = isMulti
|
||||
? (Array.isArray(currentValue) ? currentValue : [])
|
||||
: options.find((o) => o.value === currentValue)?.label || '';
|
||||
|
||||
function handleClick() {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
|
||||
function handleSelect(optionValue: string) {
|
||||
if (isMulti) {
|
||||
const newValues = Array.isArray(currentValue) ? [...currentValue] : [];
|
||||
if (newValues.includes(optionValue)) {
|
||||
onChange(newValues.filter((v) => v !== optionValue));
|
||||
} else {
|
||||
onChange([...newValues, optionValue]);
|
||||
}
|
||||
} else {
|
||||
onChange(optionValue);
|
||||
}
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (triggerRef.current && !triggerRef.current.contains(e.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<div
|
||||
ref={triggerRef}
|
||||
onClick={handleClick}
|
||||
className={`
|
||||
flex items-center justify-between px-3 py-2 rounded-lg border
|
||||
${disabled ? 'bg-surface-50 dark:bg-surface-800 border-surface-200 dark:border-surface-700 text-surface-300' : 'bg-white dark:bg-surface-800 border-surface-300 dark:border-surface-600 cursor-pointer hover:border-surface-400 dark:hover:border-surface-500'}
|
||||
transition-all duration-200
|
||||
min-w-[200px] max-w-full
|
||||
`}
|
||||
>
|
||||
<span className={selectedLabels.length === 0 ? 'text-surface-400' : 'text-surface-900 dark:text-surface-200 truncate'}>
|
||||
{placeholder || 'Select...'}
|
||||
{isMulti && Array.isArray(selectedLabels) && selectedLabels.length > 0 && <span className="text-surface-400"> ({selectedLabels.length})</span>}
|
||||
{!isMulti && selectedLabels && <span className="text-surface-900 dark:text-surface-200"> {selectedLabels}</span>}
|
||||
</span>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="4 8 8 12" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute z-50 mt-1 w-full bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-lg max-h-60 overflow-y-auto">
|
||||
{options.map((option) => {
|
||||
const isSelected = isMulti
|
||||
? Array.isArray(currentValue) && currentValue.includes(option.value)
|
||||
: currentValue === option.value;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
onClick={() => handleSelect(option.value)}
|
||||
className={`
|
||||
px-3 py-2 cursor-pointer hover:bg-surface-50 dark:hover:bg-surface-700
|
||||
${isSelected ? 'bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400' : 'text-surface-800 dark:text-surface-300'}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isMulti && (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => {}}
|
||||
className="w-4 h-4"
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
<span className="flex-1">{option.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Select.displayName = 'Select';
|
||||
|
||||
export { Select };
|
||||
Reference in New Issue
Block a user