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 | 'use client'; import { TextareaHTMLAttributes, forwardRef } from 'react'; import { cn } from '@/lib/core'; import { adminTokens } from '@/lib/admin/design-tokens'; interface AdminTextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> { /** Textarea label */ label: string; /** Error message */ error?: string; /** Helper text */ hint?: string; /** Hide label visually (still accessible) */ hideLabel?: boolean; /** Show character count */ showCharCount?: boolean; } /** * Styled textarea component for admin forms. * * @example * ```tsx * <AdminTextarea * label="Description" * maxLength={500} * showCharCount * {...register('description')} * /> * ``` */ export const AdminTextarea = forwardRef<HTMLTextAreaElement, AdminTextareaProps>( ({ label, error, hint, hideLabel, showCharCount, className, id, value, maxLength, ...props }, ref) => { const textareaId = id || `textarea-${label.toLowerCase().replace(/\s+/g, '-')}`; const charCount = typeof value === 'string' ? value.length : 0; return ( <div className="space-y-1"> <div className="flex items-center justify-between"> <label htmlFor={textareaId} 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> {showCharCount && maxLength && ( <span className={cn( 'text-xs', charCount > maxLength * 0.9 ? charCount >= maxLength ? 'text-red-500' : 'text-yellow-500' : 'text-gray-400' )} > {charCount}/{maxLength} </span> )} </div> <textarea ref={ref} id={textareaId} value={value} maxLength={maxLength} className={cn( 'w-full px-3 py-2 rounded-md border min-h-[100px] resize-y', adminTokens.colors.form.input.bg, adminTokens.colors.form.input.text, adminTokens.colors.form.input.placeholder, '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 ? `${textareaId}-error` : hint ? `${textareaId}-hint` : undefined } {...props} /> {error && ( <p id={`${textareaId}-error`} className={cn('text-sm', adminTokens.colors.form.error)}> {error} </p> )} {hint && !error && ( <p id={`${textareaId}-hint`} className={cn('text-sm', adminTokens.colors.form.hint)}> {hint} </p> )} </div> ); } ); AdminTextarea.displayName = 'AdminTextarea'; export default AdminTextarea; |