153 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
}
|