Spaces:
Sleeping
Sleeping
| import React, { forwardRef } from 'react'; | |
| import { ChevronDown } from 'lucide-react'; | |
| import { classNames } from '@/utils/helpers'; | |
| interface SelectOption { | |
| value: string; | |
| label: string; | |
| disabled?: boolean; | |
| } | |
| interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'children'> { | |
| label?: string; | |
| error?: string; | |
| hint?: string; | |
| options: SelectOption[]; | |
| placeholder?: string; | |
| } | |
| export const Select = forwardRef<HTMLSelectElement, SelectProps>( | |
| ({ label, error, hint, options, placeholder, className, id, ...props }, ref) => { | |
| const selectId = id || `select-${Math.random().toString(36).slice(2, 9)}`; | |
| return ( | |
| <div className="w-full"> | |
| {label && ( | |
| <label | |
| htmlFor={selectId} | |
| className="block text-sm font-medium text-dark-300 mb-1.5" | |
| > | |
| {label} | |
| </label> | |
| )} | |
| <div className="relative"> | |
| <select | |
| ref={ref} | |
| id={selectId} | |
| className={classNames( | |
| 'w-full px-3 py-2 pr-10 bg-dark-900 border border-dark-600 rounded-lg', | |
| 'text-dark-100 appearance-none cursor-pointer', | |
| 'focus:outline-none focus:border-accent-primary focus:ring-1 focus:ring-accent-primary/50', | |
| 'transition-colors duration-200', | |
| error && 'border-red-500 focus:border-red-500 focus:ring-red-500/50', | |
| className | |
| )} | |
| {...props} | |
| > | |
| {placeholder && ( | |
| <option value="" disabled> | |
| {placeholder} | |
| </option> | |
| )} | |
| {options.map((option) => ( | |
| <option | |
| key={option.value} | |
| value={option.value} | |
| disabled={option.disabled} | |
| > | |
| {option.label} | |
| </option> | |
| ))} | |
| </select> | |
| <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-dark-500"> | |
| <ChevronDown className="w-4 h-4" /> | |
| </div> | |
| </div> | |
| {(error || hint) && ( | |
| <p | |
| className={classNames( | |
| 'mt-1.5 text-sm', | |
| error ? 'text-red-400' : 'text-dark-500' | |
| )} | |
| > | |
| {error || hint} | |
| </p> | |
| )} | |
| </div> | |
| ); | |
| } | |
| ); | |
| Select.displayName = 'Select'; | |
| interface ToggleProps { | |
| label?: string; | |
| description?: string; | |
| checked: boolean; | |
| onChange: (checked: boolean) => void; | |
| disabled?: boolean; | |
| } | |
| export const Toggle: React.FC<ToggleProps> = ({ | |
| label, | |
| description, | |
| checked, | |
| onChange, | |
| disabled = false, | |
| }) => { | |
| return ( | |
| <label | |
| className={classNames( | |
| 'flex items-center justify-between gap-4 cursor-pointer', | |
| disabled && 'opacity-50 cursor-not-allowed' | |
| )} | |
| > | |
| <div> | |
| {label && ( | |
| <span className="block text-sm font-medium text-dark-200"> | |
| {label} | |
| </span> | |
| )} | |
| {description && ( | |
| <span className="block text-sm text-dark-500 mt-0.5"> | |
| {description} | |
| </span> | |
| )} | |
| </div> | |
| <button | |
| type="button" | |
| role="switch" | |
| aria-checked={checked} | |
| disabled={disabled} | |
| onClick={() => !disabled && onChange(!checked)} | |
| className={classNames( | |
| 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors', | |
| 'focus:outline-none focus:ring-2 focus:ring-accent-primary/50', | |
| checked ? 'bg-accent-primary' : 'bg-dark-600' | |
| )} | |
| > | |
| <span | |
| className={classNames( | |
| 'inline-block h-4 w-4 transform rounded-full bg-white transition-transform', | |
| checked ? 'translate-x-6' : 'translate-x-1' | |
| )} | |
| /> | |
| </button> | |
| </label> | |
| ); | |
| }; | |
| export default Select; | |