Sergei 4b60a92777 Add basic UI components for web application
- 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>
2026-01-31 18:11:31 -08:00

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';