Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | 'use client'; import { SelectHTMLAttributes, forwardRef } from 'react'; import { cn } from '@/lib/core'; import { adminTokens } from '@/lib/admin/design-tokens'; interface SelectOption { value: string; label: string; disabled?: boolean; } interface AdminSelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'children'> { /** Select label */ label: string; /** Options to display */ options: SelectOption[]; /** Error message */ error?: string; /** Helper text */ hint?: string; /** Hide label visually (still accessible) */ hideLabel?: boolean; /** Placeholder option text */ placeholder?: string; } /** * Styled select component for admin forms. * * @example * ```tsx * <AdminSelect * label="Category" * options={[ * { value: 'electronics', label: 'Electronics' }, * { value: 'clothing', label: 'Clothing' }, * ]} * {...register('category')} * /> * ``` */ export const AdminSelect = forwardRef<HTMLSelectElement, AdminSelectProps>( ({ label, options, error, hint, hideLabel, placeholder, className, id, ...props }, ref) => { const selectId = id || `select-${label.toLowerCase().replace(/\s+/g, '-')}`; return ( <div className="space-y-1"> <label htmlFor={selectId} className={cn( 'block text-sm font-medium', adminTokens.colors.form.label, hideLabel && 'sr-only' )} > {label} {props.required && <span className="text-red-500 ml-1">*</span>} </label> <select ref={ref} id={selectId} className={cn( 'w-full px-3 py-2 rounded-md border', adminTokens.colors.form.input.bg, adminTokens.colors.form.input.text, 'focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900', error ? adminTokens.colors.form.input.error : cn(adminTokens.colors.form.input.border, adminTokens.colors.form.input.focus), 'disabled:opacity-50 disabled:cursor-not-allowed', className )} aria-invalid={error ? 'true' : 'false'} aria-describedby={ error ? `${selectId}-error` : hint ? `${selectId}-hint` : undefined } {...props} > {placeholder && ( <option value="" disabled> {placeholder} </option> )} {options.map((option) => ( <option key={option.value} value={option.value} disabled={option.disabled}> {option.label} </option> ))} </select> {error && ( <p id={`${selectId}-error`} className={cn('text-sm', adminTokens.colors.form.error)}> {error} </p> )} {hint && !error && ( <p id={`${selectId}-hint`} className={cn('text-sm', adminTokens.colors.form.hint)}> {hint} </p> )} </div> ); } ); AdminSelect.displayName = 'AdminSelect'; export default AdminSelect; |