WellNuo/web/components/ui/LoadingSpinner.tsx
Sergei 71f194cc4d Add Loading & Error UI components for web application
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>
2026-01-31 18:26:28 -08:00

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>
);
}