229 lines
5.9 KiB
TypeScript
229 lines
5.9 KiB
TypeScript
|
|
|
|
import { Box } from './Box';
|
|
import { Stack } from './Stack';
|
|
import { LoadingWrapperProps } from './state-types';
|
|
import { Text } from './Text';
|
|
|
|
/**
|
|
* LoadingWrapper Component
|
|
*
|
|
* Provides consistent loading states with multiple variants:
|
|
* - spinner: Traditional loading spinner (default)
|
|
* - skeleton: Skeleton screens for better UX
|
|
* - full-screen: Centered in viewport
|
|
* - inline: Compact inline loading
|
|
* - card: Loading card placeholders
|
|
*
|
|
* All variants are fully accessible with ARIA labels and keyboard support.
|
|
*/
|
|
export function LoadingWrapper({
|
|
variant = 'spinner',
|
|
message = 'Loading...',
|
|
className = '',
|
|
size = 'md',
|
|
skeletonCount = 3,
|
|
cardConfig,
|
|
ariaLabel = 'Loading content',
|
|
}: LoadingWrapperProps) {
|
|
// Size mappings for different variants
|
|
const sizeClasses = {
|
|
sm: {
|
|
spinner: 'w-4 h-4 border-2',
|
|
inline: 'xs' as const,
|
|
card: 'h-24',
|
|
},
|
|
md: {
|
|
spinner: 'w-10 h-10 border-2',
|
|
inline: 'sm' as const,
|
|
card: 'h-32',
|
|
},
|
|
lg: {
|
|
spinner: 'w-16 h-16 border-4',
|
|
inline: 'base' as const,
|
|
card: 'h-40',
|
|
},
|
|
};
|
|
|
|
const spinnerSize = sizeClasses[size].spinner;
|
|
const inlineSize = sizeClasses[size].inline;
|
|
const cardHeight = cardConfig?.height || sizeClasses[size].card;
|
|
|
|
// Render different variants
|
|
switch (variant) {
|
|
case 'spinner':
|
|
return (
|
|
<Box
|
|
display="flex"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
minHeight="200px"
|
|
className={className}
|
|
role="status"
|
|
aria-label={ariaLabel}
|
|
aria-live="polite"
|
|
>
|
|
<Stack align="center" gap={3}>
|
|
<Box
|
|
className={`${spinnerSize} border-primary-blue border-t-transparent rounded-full animate-spin`}
|
|
/>
|
|
<Text color="text-gray-400" size="sm">{message}</Text>
|
|
</Stack>
|
|
</Box>
|
|
);
|
|
|
|
case 'skeleton':
|
|
return (
|
|
<Stack
|
|
gap={3}
|
|
className={className}
|
|
role="status"
|
|
aria-label={ariaLabel}
|
|
aria-live="polite"
|
|
>
|
|
{Array.from({ length: skeletonCount }).map((_, index) => (
|
|
<Box
|
|
key={index}
|
|
fullWidth
|
|
bg="bg-iron-gray/40"
|
|
rounded="lg"
|
|
animate="pulse"
|
|
style={{ height: cardHeight }}
|
|
/>
|
|
))}
|
|
</Stack>
|
|
);
|
|
|
|
case 'full-screen':
|
|
return (
|
|
<Box
|
|
position="fixed"
|
|
inset="0"
|
|
zIndex={50}
|
|
bg="bg-deep-graphite/90"
|
|
blur="sm"
|
|
display="flex"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
p={4}
|
|
className={className}
|
|
role="status"
|
|
aria-label={ariaLabel}
|
|
aria-live="polite"
|
|
>
|
|
<Box textAlign="center" maxWidth="md">
|
|
<Stack align="center" gap={4}>
|
|
<Box className="w-16 h-16 border-4 border-primary-blue border-t-transparent rounded-full animate-spin" />
|
|
<Text color="text-white" size="lg" weight="medium">{message}</Text>
|
|
<Text color="text-gray-400" size="sm">This may take a moment...</Text>
|
|
</Stack>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
|
|
case 'inline':
|
|
return (
|
|
<Box
|
|
display="inline-flex"
|
|
alignItems="center"
|
|
gap={2}
|
|
className={className}
|
|
role="status"
|
|
aria-label={ariaLabel}
|
|
aria-live="polite"
|
|
>
|
|
<Box className="w-4 h-4 border-2 border-primary-blue border-t-transparent rounded-full animate-spin" />
|
|
<Text color="text-gray-400" size={inlineSize}>{message}</Text>
|
|
</Box>
|
|
);
|
|
|
|
case 'card':
|
|
const cardCount = cardConfig?.count || 3;
|
|
const cardClassName = cardConfig?.className || '';
|
|
|
|
return (
|
|
<Box
|
|
display="grid"
|
|
gap={4}
|
|
className={className}
|
|
role="status"
|
|
aria-label={ariaLabel}
|
|
aria-live="polite"
|
|
>
|
|
{Array.from({ length: cardCount }).map((_, index) => (
|
|
<Box
|
|
key={index}
|
|
bg="bg-iron-gray/40"
|
|
rounded="xl"
|
|
overflow="hidden"
|
|
border
|
|
borderColor="border-charcoal-outline/50"
|
|
className={cardClassName}
|
|
style={{ height: cardHeight }}
|
|
>
|
|
<Box h="full" w="full" display="flex" alignItems="center" justifyContent="center">
|
|
<Box className="w-8 h-8 border-2 border-primary-blue border-t-transparent rounded-full animate-spin" />
|
|
</Box>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenience component for full-screen loading
|
|
*/
|
|
export function FullScreenLoading({ message = 'Loading...', className = '' }: Pick<LoadingWrapperProps, 'message' | 'className'>) {
|
|
return (
|
|
<LoadingWrapper
|
|
variant="full-screen"
|
|
message={message}
|
|
className={className}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Convenience component for inline loading
|
|
*/
|
|
export function InlineLoading({ message = 'Loading...', size = 'sm', className = '' }: Pick<LoadingWrapperProps, 'message' | 'size' | 'className'>) {
|
|
return (
|
|
<LoadingWrapper
|
|
variant="inline"
|
|
message={message}
|
|
size={size}
|
|
className={className}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Convenience component for skeleton loading
|
|
*/
|
|
export function SkeletonLoading({ skeletonCount = 3, className = '' }: Pick<LoadingWrapperProps, 'skeletonCount' | 'className'>) {
|
|
return (
|
|
<LoadingWrapper
|
|
variant="skeleton"
|
|
skeletonCount={skeletonCount}
|
|
className={className}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Convenience component for card loading
|
|
*/
|
|
export function CardLoading({ cardConfig, className = '' }: Pick<LoadingWrapperProps, 'cardConfig' | 'className'>) {
|
|
return (
|
|
<LoadingWrapper
|
|
variant="card"
|
|
cardConfig={cardConfig}
|
|
className={className}
|
|
/>
|
|
);
|
|
}
|