Files
gridpilot.gg/apps/website/ui/LoadingWrapper.tsx
2026-01-18 23:24:30 +01:00

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} />;
}