All files / src/components/admin/forms AdminTextarea.tsx

0% Statements 0/107
100% Branches 0/0
0% Functions 0/1
0% Lines 0/107

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;