erp-mecanicas-diesel-fronte.../src/components/ui/LoadingSpinner.tsx
rckrdmrd abff318db4 Migración desde erp-mecanicas-diesel/frontend - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:11:27 -06:00

153 lines
3.7 KiB
TypeScript

import { Loader2 } from 'lucide-react';
export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export type SpinnerVariant = 'default' | 'primary' | 'white';
interface LoadingSpinnerProps {
size?: SpinnerSize;
variant?: SpinnerVariant;
className?: string;
}
const SIZE_CLASSES: Record<SpinnerSize, string> = {
xs: 'h-3 w-3',
sm: 'h-4 w-4',
md: 'h-6 w-6',
lg: 'h-8 w-8',
xl: 'h-12 w-12',
};
const VARIANT_CLASSES: Record<SpinnerVariant, string> = {
default: 'text-gray-500',
primary: 'text-diesel-600',
white: 'text-white',
};
export function LoadingSpinner({
size = 'md',
variant = 'primary',
className = '',
}: LoadingSpinnerProps) {
return (
<Loader2
className={`animate-spin ${SIZE_CLASSES[size]} ${VARIANT_CLASSES[variant]} ${className}`}
/>
);
}
// Full page loading state
interface PageLoaderProps {
message?: string;
}
export function PageLoader({ message = 'Cargando...' }: PageLoaderProps) {
return (
<div className="flex h-64 flex-col items-center justify-center">
<LoadingSpinner size="lg" />
<p className="mt-3 text-sm text-gray-500">{message}</p>
</div>
);
}
// Inline loading state (for buttons, etc.)
interface InlineLoaderProps {
size?: SpinnerSize;
variant?: SpinnerVariant;
}
export function InlineLoader({ size = 'sm', variant = 'primary' }: InlineLoaderProps) {
return (
<span className="inline-flex items-center">
<LoadingSpinner size={size} variant={variant} />
</span>
);
}
// Overlay loading state
interface LoadingOverlayProps {
isLoading: boolean;
message?: string;
children: React.ReactNode;
}
export function LoadingOverlay({ isLoading, message, children }: LoadingOverlayProps) {
return (
<div className="relative">
{children}
{isLoading && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-white/80">
<LoadingSpinner size="lg" />
{message && <p className="mt-3 text-sm text-gray-500">{message}</p>}
</div>
)}
</div>
);
}
// Skeleton loader for content placeholders
interface SkeletonProps {
className?: string;
variant?: 'text' | 'circular' | 'rectangular';
width?: string | number;
height?: string | number;
}
export function Skeleton({
className = '',
variant = 'text',
width,
height,
}: SkeletonProps) {
const baseClasses = 'animate-pulse bg-gray-200';
const variantClasses = {
text: 'rounded',
circular: 'rounded-full',
rectangular: 'rounded-lg',
};
const style: React.CSSProperties = {};
if (width) style.width = typeof width === 'number' ? `${width}px` : width;
if (height) style.height = typeof height === 'number' ? `${height}px` : height;
return (
<div
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
style={style}
/>
);
}
// Pre-configured skeleton rows
export function SkeletonText({ lines = 3 }: { lines?: number }) {
return (
<div className="space-y-2">
{Array.from({ length: lines }).map((_, i) => (
<Skeleton
key={i}
variant="text"
height={16}
width={i === lines - 1 ? '60%' : '100%'}
/>
))}
</div>
);
}
export function SkeletonCard() {
return (
<div className="rounded-xl border border-gray-200 bg-white p-6">
<div className="flex items-center gap-4">
<Skeleton variant="circular" width={48} height={48} />
<div className="flex-1 space-y-2">
<Skeleton variant="text" height={20} width="40%" />
<Skeleton variant="text" height={16} width="60%" />
</div>
</div>
<div className="mt-4">
<SkeletonText lines={2} />
</div>
</div>
);
}