sadded by commitizen

This commit is contained in:
rmoren97 2026-02-13 15:58:06 -08:00
parent 7edef35d7f
commit 39766a0994
2 changed files with 115 additions and 54 deletions

View File

@ -59,20 +59,6 @@ body {
color: rgba(255, 255, 255, 0.3);
}
select.glass-input {
appearance: none;
-webkit-appearance: none;
color-scheme: dark;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 36px;
}
select option {
background-color: #0d0d1a;
color: #f3f4f6;
}
/* ─── Glass Shine (top border gradient) ───────────────────────── */

View File

@ -1,49 +1,124 @@
import { forwardRef } from 'react'
'use client'
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
import { useState, useRef, useEffect } from 'react'
import { ChevronDown } from 'lucide-react'
interface SelectProps {
label?: string
error?: string
required?: boolean
options: { label: string; value: string }[]
placeholder?: string
value?: string
defaultValue?: string
onChange?: (e: { target: { value: string; name?: string } }) => void
disabled?: boolean
className?: string
name?: string
id?: string
}
const Select = forwardRef<HTMLSelectElement, SelectProps>(
({ label, error, required, options, placeholder, className = '', ...props }, ref) => {
return (
<div className="space-y-1.5">
{label && (
<label className="block text-sm font-medium text-gray-300">
{label}
{required && <span className="text-red-400 ml-1">*</span>}
</label>
)}
<select
ref={ref}
className={`w-full px-4 py-2 glass-input text-gray-100 rounded-lg
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent
focus:bg-white/[0.06] focus:shadow-[0_0_20px_rgba(6,182,212,0.08)]
disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 ${
error ? 'border-red-500 focus:ring-red-500' : ''
} ${className}`}
{...props}
>
{placeholder && (
<option value="" className="text-gray-500">
{placeholder}
</option>
)}
{options.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
{error && <p className="text-xs text-red-400">{error}</p>}
</div>
)
}
)
export default function Select({
label,
error,
required,
options,
placeholder,
value,
defaultValue,
onChange,
disabled,
className = '',
name,
id,
}: SelectProps) {
const isControlled = value !== undefined
const [internalValue, setInternalValue] = useState(defaultValue ?? '')
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
Select.displayName = 'Select'
export default Select
const currentValue = isControlled ? value : internalValue
const selected = options.find(o => o.value === currentValue)
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const handleSelect = (optValue: string) => {
if (!isControlled) setInternalValue(optValue)
onChange?.({ target: { value: optValue, name } })
setOpen(false)
}
return (
<div className={`space-y-1.5 ${className}`}>
{label && (
<label htmlFor={id} className="block text-sm font-medium text-gray-300">
{label}
{required && <span className="text-red-400 ml-1">*</span>}
</label>
)}
{/* Hidden input for FormData compatibility */}
{name && <input type="hidden" name={name} value={currentValue} />}
<div ref={ref} className="relative">
<button
id={id}
type="button"
disabled={disabled}
onClick={() => !disabled && setOpen(prev => !prev)}
className={`w-full px-4 py-2 glass-input text-left flex items-center justify-between rounded-lg
focus:outline-none focus:ring-2 focus:ring-cyan-400/40
disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200
${error ? 'border-red-500' : ''}
`}
>
<span className={selected ? 'text-gray-100 text-sm' : 'text-gray-500 text-sm'}>
{selected ? selected.label : (placeholder ?? 'Select...')}
</span>
<ChevronDown
size={16}
className={`text-gray-400 shrink-0 ml-2 transition-transform duration-200 ${open ? 'rotate-180' : ''}`}
/>
</button>
{open && (
<div className="absolute z-50 w-full mt-1 glass rounded-lg overflow-hidden shadow-xl shadow-black/50 border border-white/[0.1]">
{placeholder && (
<button
type="button"
onClick={() => handleSelect('')}
className="w-full px-4 py-2.5 text-left text-sm text-gray-500 hover:bg-white/[0.06] transition-colors"
>
{placeholder}
</button>
)}
{options.map(opt => (
<button
key={opt.value}
type="button"
onClick={() => handleSelect(opt.value)}
className={`w-full px-4 py-2.5 text-left text-sm transition-colors
${opt.value === currentValue
? 'bg-cyan-500/20 text-cyan-400'
: 'text-gray-200 hover:bg-white/[0.06]'
}`}
>
{opt.label}
</button>
))}
</div>
)}
</div>
{error && <p className="text-xs text-red-400">{error}</p>}
</div>
)
}