Implemented comprehensive loading and error handling components for the WellNuo web application with full test coverage. Components added: - LoadingSpinner: Configurable spinner with sizes, colors, and variants * LoadingOverlay: Full-screen loading with backdrop options * InlineLoader: Small inline spinner for buttons * PageLoader: Full-page centered loading state - ErrorMessage: Inline error messages with severity levels * FullScreenError: Full-page error states with retry/back actions * FieldError: Form field validation errors * ErrorBoundaryFallback: Error boundary fallback component * EmptyState: Empty data state component - Skeleton: Animated loading skeletons * SkeletonAvatar: Circular avatar skeleton * SkeletonText: Multi-line text skeleton * SkeletonCard: Card-style skeleton * SkeletonList: List of skeleton cards * SkeletonTable: Table skeleton with rows/columns * SkeletonDashboard: Dashboard-style skeleton layout * SkeletonForm: Form skeleton with fields Technical details: - Tailwind CSS styling with cn() utility function - Full accessibility support (ARIA labels, roles) - Comprehensive test coverage (57 tests, all passing) - TypeScript strict mode compliance - Added clsx and tailwind-merge dependencies Files: - web/components/ui/LoadingSpinner.tsx - web/components/ui/ErrorMessage.tsx - web/components/ui/Skeleton.tsx - web/components/ui/index.ts - web/lib/utils.ts - web/components/ui/__tests__/*.test.tsx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
181 lines
3.7 KiB
TypeScript
181 lines
3.7 KiB
TypeScript
/**
|
|
* LoadingSpinner - Animated loading spinner for web
|
|
*
|
|
* Features:
|
|
* - Multiple sizes (sm, md, lg, xl)
|
|
* - Optional loading message
|
|
* - Full-screen mode option
|
|
* - Tailwind CSS styling
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <LoadingSpinner size="md" />
|
|
* <LoadingSpinner size="lg" message="Loading data..." />
|
|
* <LoadingSpinner fullScreen message="Please wait..." />
|
|
* ```
|
|
*/
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface LoadingSpinnerProps {
|
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
color?: 'primary' | 'white' | 'gray';
|
|
message?: string;
|
|
fullScreen?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
// Size mapping for spinner
|
|
const sizeClasses = {
|
|
sm: 'h-4 w-4 border-2',
|
|
md: 'h-8 w-8 border-2',
|
|
lg: 'h-12 w-12 border-3',
|
|
xl: 'h-16 w-16 border-4',
|
|
};
|
|
|
|
// Color mapping for spinner
|
|
const colorClasses = {
|
|
primary: 'border-blue-600 border-t-transparent',
|
|
white: 'border-white border-t-transparent',
|
|
gray: 'border-gray-400 border-t-transparent',
|
|
};
|
|
|
|
// Text size mapping
|
|
const textSizeClasses = {
|
|
sm: 'text-sm',
|
|
md: 'text-base',
|
|
lg: 'text-lg',
|
|
xl: 'text-xl',
|
|
};
|
|
|
|
export function LoadingSpinner({
|
|
size = 'md',
|
|
color = 'primary',
|
|
message,
|
|
fullScreen = false,
|
|
className,
|
|
}: LoadingSpinnerProps) {
|
|
const spinner = (
|
|
<div
|
|
className={cn(
|
|
'animate-spin rounded-full',
|
|
sizeClasses[size],
|
|
colorClasses[color],
|
|
className
|
|
)}
|
|
role="status"
|
|
aria-label={message || 'Loading'}
|
|
/>
|
|
);
|
|
|
|
if (fullScreen) {
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-white">
|
|
{spinner}
|
|
{message && (
|
|
<p className={cn('mt-4 text-slate-600', textSizeClasses[size])}>
|
|
{message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (message) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center gap-3 p-6">
|
|
{spinner}
|
|
<p className={cn('text-slate-600', textSizeClasses[size])}>
|
|
{message}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return spinner;
|
|
}
|
|
|
|
/**
|
|
* LoadingOverlay - Full-screen loading overlay with backdrop
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* {isLoading && (
|
|
* <LoadingOverlay message="Saving changes..." />
|
|
* )}
|
|
* ```
|
|
*/
|
|
interface LoadingOverlayProps {
|
|
message?: string;
|
|
backdrop?: 'light' | 'dark' | 'blur';
|
|
}
|
|
|
|
export function LoadingOverlay({
|
|
message = 'Loading...',
|
|
backdrop = 'blur'
|
|
}: LoadingOverlayProps) {
|
|
const backdropClasses = {
|
|
light: 'bg-white/80',
|
|
dark: 'bg-slate-900/80',
|
|
blur: 'bg-white/80 backdrop-blur-sm',
|
|
};
|
|
|
|
return (
|
|
<div className={cn(
|
|
'fixed inset-0 z-50 flex flex-col items-center justify-center',
|
|
backdropClasses[backdrop]
|
|
)}>
|
|
<LoadingSpinner size="lg" />
|
|
<p className="mt-4 text-lg font-medium text-slate-700">
|
|
{message}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* InlineLoader - Small inline loading indicator
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <button disabled={isLoading}>
|
|
* {isLoading ? <InlineLoader /> : 'Save'}
|
|
* </button>
|
|
* ```
|
|
*/
|
|
interface InlineLoaderProps {
|
|
className?: string;
|
|
}
|
|
|
|
export function InlineLoader({ className }: InlineLoaderProps) {
|
|
return (
|
|
<LoadingSpinner
|
|
size="sm"
|
|
className={cn('inline-block', className)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* PageLoader - Full page loading state with centered spinner
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* if (isLoading) return <PageLoader message="Loading dashboard..." />;
|
|
* ```
|
|
*/
|
|
interface PageLoaderProps {
|
|
message?: string;
|
|
}
|
|
|
|
export function PageLoader({ message }: PageLoaderProps) {
|
|
return (
|
|
<div className="flex min-h-[400px] flex-col items-center justify-center">
|
|
<LoadingSpinner size="lg" />
|
|
{message && (
|
|
<p className="mt-4 text-lg text-slate-600">{message}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|