121 lines
4.0 KiB
TypeScript
121 lines
4.0 KiB
TypeScript
import { Box } from '@/ui/Box';
|
|
import { LoadingWrapperProps } from '@/ui/state-types';
|
|
import { Text } from '@/ui/Text';
|
|
|
|
export function LoadingWrapper({
|
|
variant = 'spinner',
|
|
message = 'Loading...',
|
|
size = 'md',
|
|
skeletonCount = 3,
|
|
cardConfig,
|
|
ariaLabel = 'Loading content',
|
|
}: LoadingWrapperProps) {
|
|
const sizeMap = {
|
|
sm: { spinner: '1rem', inline: 'xs' as const, card: '6rem' },
|
|
md: { spinner: '2.5rem', inline: 'sm' as const, card: '8rem' },
|
|
lg: { spinner: '4rem', inline: 'base' as const, card: '10rem' },
|
|
};
|
|
|
|
const spinnerSize = sizeMap[size].spinner;
|
|
const inlineSize = sizeMap[size].inline;
|
|
const cardHeight = cardConfig?.height || sizeMap[size].card;
|
|
|
|
const Spinner = ({ size: s }: { size: string }) => (
|
|
<Box
|
|
style={{
|
|
width: s,
|
|
height: s,
|
|
border: '2px solid var(--ui-color-intent-primary)',
|
|
borderTopColor: 'transparent',
|
|
borderRadius: '9999px'
|
|
}}
|
|
className="animate-spin"
|
|
/>
|
|
);
|
|
|
|
switch (variant) {
|
|
case 'spinner':
|
|
return (
|
|
<Box display="flex" flexDirection="col" alignItems="center" justifyContent="center" minHeight="12rem" role="status" aria-label={ariaLabel}>
|
|
<Spinner size={spinnerSize} />
|
|
<Box marginTop={3}>
|
|
<Text variant="low" size="sm">{message}</Text>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
|
|
case 'skeleton':
|
|
return (
|
|
<Box display="flex" flexDirection="col" gap={3} role="status" aria-label={ariaLabel}>
|
|
{Array.from({ length: skeletonCount }).map((_, index) => (
|
|
<Box
|
|
key={index}
|
|
fullWidth
|
|
bg="var(--ui-color-bg-surface-muted)"
|
|
rounded="lg"
|
|
style={{ height: cardHeight, opacity: 0.5 }}
|
|
className="animate-pulse"
|
|
/>
|
|
))}
|
|
</Box>
|
|
);
|
|
|
|
case 'full-screen':
|
|
return (
|
|
<Box position="fixed" inset={0} zIndex={100} bg="var(--ui-color-bg-base)" display="flex" alignItems="center" justifyContent="center" padding={6} role="status" aria-label={ariaLabel}>
|
|
<Box textAlign="center">
|
|
<Box display="flex" justifyContent="center" marginBottom={4}>
|
|
<Spinner size="4rem" />
|
|
</Box>
|
|
<Text variant="high" size="lg" weight="medium" block>{message}</Text>
|
|
<Text variant="low" size="sm" block marginTop={1}>This may take a moment...</Text>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
|
|
case 'inline':
|
|
return (
|
|
<Box display="inline-flex" alignItems="center" gap={2} role="status" aria-label={ariaLabel}>
|
|
<Spinner size="1rem" />
|
|
<Text variant="low" size={inlineSize}>{message}</Text>
|
|
</Box>
|
|
);
|
|
|
|
case 'card':
|
|
const cardCount = cardConfig?.count || 3;
|
|
return (
|
|
<Box display="grid" gap={4} role="status" aria-label={ariaLabel}>
|
|
{Array.from({ length: cardCount }).map((_, index) => (
|
|
<Box
|
|
key={index}
|
|
bg="var(--ui-color-bg-surface-muted)"
|
|
rounded="xl"
|
|
style={{ height: cardHeight, border: '1px solid var(--ui-color-border-default)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
|
>
|
|
<Spinner size="2rem" />
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function FullScreenLoading({ message = 'Loading...' }: Pick<LoadingWrapperProps, 'message'>) {
|
|
return <LoadingWrapper variant="full-screen" message={message} />;
|
|
}
|
|
|
|
export function InlineLoading({ message = 'Loading...', size = 'sm' }: Pick<LoadingWrapperProps, 'message' | 'size'>) {
|
|
return <LoadingWrapper variant="inline" message={message} size={size} />;
|
|
}
|
|
|
|
export function SkeletonLoading({ skeletonCount = 3 }: Pick<LoadingWrapperProps, 'skeletonCount'>) {
|
|
return <LoadingWrapper variant="skeleton" skeletonCount={skeletonCount} />;
|
|
}
|
|
|
|
export function CardLoading({ cardConfig }: Pick<LoadingWrapperProps, 'cardConfig'>) {
|
|
return <LoadingWrapper variant="card" cardConfig={cardConfig} />;
|
|
}
|