224 lines
5.5 KiB
TypeScript
224 lines
5.5 KiB
TypeScript
import React, { forwardRef, HTMLAttributes } from 'react';
|
|
import { cn } from '../../lib/utils';
|
|
|
|
// Loading sizes
|
|
type LoadingSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
|
|
// Loading variants
|
|
type LoadingVariant = 'primary' | 'secondary' | 'neutral' | 'contrast';
|
|
|
|
// Loading props interface
|
|
interface LoadingProps extends HTMLAttributes<HTMLDivElement> {
|
|
size?: LoadingSize;
|
|
variant?: LoadingVariant;
|
|
overlay?: boolean;
|
|
text?: string;
|
|
fullscreen?: boolean;
|
|
}
|
|
|
|
// Helper function to get size styles
|
|
const getSizeStyles = (size: LoadingSize) => {
|
|
switch (size) {
|
|
case 'sm':
|
|
return 'w-4 h-4 border-2';
|
|
case 'md':
|
|
return 'w-8 h-8 border-4';
|
|
case 'lg':
|
|
return 'w-12 h-12 border-4';
|
|
case 'xl':
|
|
return 'w-16 h-16 border-4';
|
|
default:
|
|
return 'w-8 h-8 border-4';
|
|
}
|
|
};
|
|
|
|
// Helper function to get variant styles
|
|
const getVariantStyles = (variant: LoadingVariant) => {
|
|
switch (variant) {
|
|
case 'primary':
|
|
return 'border-primary';
|
|
case 'secondary':
|
|
return 'border-secondary';
|
|
case 'neutral':
|
|
return 'border-gray-300';
|
|
case 'contrast':
|
|
return 'border-white';
|
|
default:
|
|
return 'border-primary';
|
|
}
|
|
};
|
|
|
|
// Helper function to get text size
|
|
const getTextSize = (size: LoadingSize) => {
|
|
switch (size) {
|
|
case 'sm':
|
|
return 'text-sm';
|
|
case 'md':
|
|
return 'text-base';
|
|
case 'lg':
|
|
return 'text-lg';
|
|
case 'xl':
|
|
return 'text-xl';
|
|
default:
|
|
return 'text-base';
|
|
}
|
|
};
|
|
|
|
// Main Loading Component
|
|
export const Loading = forwardRef<HTMLDivElement, LoadingProps>(
|
|
({
|
|
size = 'md',
|
|
variant = 'primary',
|
|
overlay = false,
|
|
text,
|
|
fullscreen = false,
|
|
className = '',
|
|
...props
|
|
}, ref) => {
|
|
const spinner = (
|
|
<div
|
|
className={cn(
|
|
'animate-spin rounded-full',
|
|
'border-t-transparent',
|
|
getSizeStyles(size),
|
|
getVariantStyles(variant),
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
|
|
if (overlay) {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'fixed inset-0 z-50 flex items-center justify-center',
|
|
'bg-black/50 backdrop-blur-sm',
|
|
fullscreen && 'w-screen h-screen'
|
|
)}
|
|
>
|
|
<div className="flex flex-col items-center gap-3">
|
|
{spinner}
|
|
{text && (
|
|
<span className={cn('text-white font-medium', getTextSize(size))}>
|
|
{text}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'flex flex-col items-center justify-center gap-3',
|
|
fullscreen && 'w-screen h-screen'
|
|
)}
|
|
>
|
|
{spinner}
|
|
{text && (
|
|
<span className={cn('text-gray-700 font-medium', getTextSize(size))}>
|
|
{text}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
Loading.displayName = 'Loading';
|
|
|
|
// Loading Button Component
|
|
interface LoadingButtonProps extends HTMLAttributes<HTMLDivElement> {
|
|
size?: LoadingSize;
|
|
variant?: LoadingVariant;
|
|
text?: string;
|
|
}
|
|
|
|
export const LoadingButton = forwardRef<HTMLDivElement, LoadingButtonProps>(
|
|
({ size = 'md', variant = 'primary', text = 'Loading...', className = '', ...props }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'inline-flex items-center gap-2 px-4 py-2 rounded-lg',
|
|
'bg-gray-100 text-gray-700',
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'animate-spin rounded-full border-t-transparent',
|
|
getSizeStyles(size === 'sm' ? 'sm' : 'md'),
|
|
getVariantStyles(variant)
|
|
)}
|
|
/>
|
|
<span className="font-medium">{text}</span>
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
LoadingButton.displayName = 'LoadingButton';
|
|
|
|
// Loading Skeleton Component
|
|
interface LoadingSkeletonProps extends HTMLAttributes<HTMLDivElement> {
|
|
width?: string | number;
|
|
height?: string | number;
|
|
rounded?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
export const LoadingSkeleton = forwardRef<HTMLDivElement, LoadingSkeletonProps>(
|
|
({ width = '100%', height = '1rem', rounded = false, className = '', ...props }, ref) => {
|
|
// Convert numeric values to Tailwind width classes
|
|
const getWidthClass = (width: string | number) => {
|
|
if (typeof width === 'number') {
|
|
if (width <= 32) return 'w-8';
|
|
if (width <= 64) return 'w-16';
|
|
if (width <= 128) return 'w-32';
|
|
if (width <= 192) return 'w-48';
|
|
if (width <= 256) return 'w-64';
|
|
return 'w-full';
|
|
}
|
|
return width === '100%' ? 'w-full' : width;
|
|
};
|
|
|
|
// Convert numeric values to Tailwind height classes
|
|
const getHeightClass = (height: string | number) => {
|
|
if (typeof height === 'number') {
|
|
if (height <= 8) return 'h-2';
|
|
if (height <= 16) return 'h-4';
|
|
if (height <= 24) return 'h-6';
|
|
if (height <= 32) return 'h-8';
|
|
if (height <= 48) return 'h-12';
|
|
if (height <= 64) return 'h-16';
|
|
return 'h-auto';
|
|
}
|
|
return height === '1rem' ? 'h-4' : height;
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'animate-pulse bg-gray-200',
|
|
rounded && 'rounded-md',
|
|
getWidthClass(width),
|
|
getHeightClass(height),
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
|
|
LoadingSkeleton.displayName = 'LoadingSkeleton';
|
|
|
|
// Export types for external use
|
|
export type { LoadingProps, LoadingSize, LoadingVariant, LoadingButtonProps, LoadingSkeletonProps }; |