- Set up Tailwind CSS configuration for styling - Create Button component with variants (primary, secondary, outline, ghost, danger) - Create Input component with label, error, and helper text support - Create Card component with composable subcomponents (Header, Title, Description, Content, Footer) - Create LoadingSpinner component with size and fullscreen options - Create ErrorMessage component with retry and dismiss actions - Add comprehensive test suite using Jest and React Testing Library - Configure ESLint and Jest for quality assurance All components follow consistent design patterns from mobile app and include proper TypeScript types and accessibility features. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
88 lines
2.4 KiB
TypeScript
88 lines
2.4 KiB
TypeScript
import React, { forwardRef } from 'react';
|
|
|
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
label?: string;
|
|
error?: string;
|
|
helperText?: string;
|
|
fullWidth?: boolean;
|
|
leftIcon?: React.ReactNode;
|
|
rightIcon?: React.ReactNode;
|
|
}
|
|
|
|
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
(
|
|
{
|
|
label,
|
|
error,
|
|
helperText,
|
|
fullWidth = false,
|
|
leftIcon,
|
|
rightIcon,
|
|
className = '',
|
|
disabled,
|
|
...props
|
|
},
|
|
ref
|
|
) => {
|
|
const baseClasses =
|
|
'block px-4 py-2 text-base border rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed';
|
|
|
|
const borderClasses = error
|
|
? 'border-error focus:ring-error focus:border-error'
|
|
: 'border-gray-300 focus:ring-primary focus:border-primary';
|
|
|
|
const widthClass = fullWidth ? 'w-full' : '';
|
|
|
|
const inputClasses = `${baseClasses} ${borderClasses} ${widthClass} ${leftIcon ? 'pl-10' : ''} ${rightIcon ? 'pr-10' : ''} ${className}`;
|
|
|
|
return (
|
|
<div className={fullWidth ? 'w-full' : ''}>
|
|
{label && (
|
|
<label className="block text-sm font-medium text-textPrimary mb-1">
|
|
{label}
|
|
</label>
|
|
)}
|
|
|
|
<div className="relative">
|
|
{leftIcon && (
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-textMuted">
|
|
{leftIcon}
|
|
</div>
|
|
)}
|
|
|
|
<input
|
|
ref={ref}
|
|
className={inputClasses}
|
|
disabled={disabled}
|
|
aria-invalid={error ? 'true' : 'false'}
|
|
aria-describedby={
|
|
error ? `${props.id}-error` : helperText ? `${props.id}-helper` : undefined
|
|
}
|
|
{...props}
|
|
/>
|
|
|
|
{rightIcon && (
|
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-textMuted">
|
|
{rightIcon}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="mt-1 text-sm text-error" id={`${props.id}-error`}>
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
{helperText && !error && (
|
|
<p className="mt-1 text-sm text-textMuted" id={`${props.id}-helper`}>
|
|
{helperText}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
Input.displayName = 'Input';
|