Setup project structure, dependencies, and basic configuration for the ComFi application. Includes initial setup for Vite, React, TypeScript, Tailwind CSS, and essential development tools. Defines core types and provides a basic README for local development.
86 lines
3.1 KiB
TypeScript
86 lines
3.1 KiB
TypeScript
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
import { ChevronDown, Check } from 'lucide-react';
|
|
|
|
export interface Option {
|
|
value: string | number;
|
|
label: string;
|
|
}
|
|
|
|
interface CustomSelectProps {
|
|
value: string | number;
|
|
onChange: (value: any) => void;
|
|
options: Option[];
|
|
placeholder?: string;
|
|
icon?: React.ReactNode;
|
|
className?: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export const CustomSelect: React.FC<CustomSelectProps> = ({
|
|
value,
|
|
onChange,
|
|
options,
|
|
placeholder = 'Selecione...',
|
|
icon,
|
|
className = '',
|
|
disabled = false
|
|
}) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const selectedOption = options.find(o => o.value === value);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
return (
|
|
<div className={`relative ${className}`} ref={containerRef}>
|
|
<div
|
|
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
className={`w-full p-3 bg-white border rounded-xl flex justify-between items-center cursor-pointer text-sm transition-all shadow-sm
|
|
${isOpen ? 'border-primary-500 ring-2 ring-primary-100' : 'border-slate-200 hover:border-primary-300'}
|
|
${disabled ? 'opacity-50 cursor-not-allowed bg-slate-50' : ''}
|
|
`}
|
|
>
|
|
<div className="flex items-center gap-2 truncate pr-2">
|
|
{icon && <span className="text-slate-400 shrink-0">{icon}</span>}
|
|
<span className={`truncate ${!selectedOption ? 'text-slate-400' : 'font-medium text-slate-700'}`}>
|
|
{selectedOption ? selectedOption.label : placeholder}
|
|
</span>
|
|
</div>
|
|
<ChevronDown size={18} className={`text-slate-400 shrink-0 transition-transform duration-200 ${isOpen ? 'rotate-180 text-primary-500' : ''}`} />
|
|
</div>
|
|
|
|
{isOpen && !disabled && (
|
|
<div className="absolute top-full left-0 w-full mt-2 bg-white border border-slate-100 rounded-xl shadow-2xl z-50 max-h-60 overflow-y-auto animate-fade-in">
|
|
{options.map((option) => (
|
|
<div
|
|
key={option.value}
|
|
onClick={() => { onChange(option.value); setIsOpen(false); }}
|
|
className={`p-3 text-sm cursor-pointer flex justify-between items-center transition-colors border-b border-slate-50 last:border-0
|
|
${option.value === value
|
|
? 'bg-primary-50 text-primary-700 font-bold'
|
|
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}
|
|
`}
|
|
>
|
|
<span className="truncate">{option.label}</span>
|
|
{option.value === value && <Check size={16} className="text-primary-500 shrink-0 ml-2" />}
|
|
</div>
|
|
))}
|
|
{options.length === 0 && (
|
|
<div className="p-3 text-sm text-slate-400 text-center italic">Sem opções</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|