DocDoeAI / src /components /states /LoadingState.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
3.91 kB
"use client";
import { cn } from "@/lib/utils";
type LoadingStateProps = {
message?: string;
size?: "sm" | "md" | "lg";
className?: string;
};
export function LoadingState({
message = "Loading...",
size = "md",
className,
}: LoadingStateProps) {
const sizeClasses = {
sm: "h-6 w-6 border-2",
md: "h-8 w-8 border-2",
lg: "h-12 w-12 border-3",
};
return (
<div
className={cn(
"flex flex-col items-center justify-center gap-4 p-8",
className,
)}
aria-live="polite"
aria-busy="true"
>
<div
className={cn(
"animate-spin rounded-full border-current border-t-transparent",
sizeClasses[size],
)}
role="status"
aria-label="Loading"
/>
{message && (
<p className="text-[14px] text-[var(--text-secondary)] loading-message">
{message}
</p>
)}
</div>
);
}
type SkeletonProps = {
variant?: "text" | "card" | "circle" | "rect";
width?: string | number;
height?: string | number;
lines?: number;
className?: string;
};
export function Skeleton({
variant = "rect",
width,
height,
lines = 1,
className,
}: SkeletonProps) {
const baseClasses = "skeleton";
const variantClasses = {
text: "rounded-md",
card: "rounded-lg",
circle: "rounded-full",
rect: "rounded-sm",
};
const style = {
width: width || (variant === "text" ? "100%" : "120px"),
height: height || (variant === "text" ? "1em" : variant === "circle" ? width || "40px" : "80px"),
};
if (variant === "text" && lines > 1) {
return (
<div className={cn("flex flex-col gap-2", className)}>
{Array.from({ length: lines }).map((_, i) => (
<div
key={i}
className={cn(baseClasses, variantClasses[variant])}
style={{
width: width || (i === lines - 1 ? "70%" : "100%"),
height: height || "1em",
}}
/>
))}
</div>
);
}
return (
<div
className={cn(baseClasses, variantClasses[variant], className)}
style={style}
aria-hidden="true"
/>
);
}
type PageLoadingProps = {
message?: string;
subtitle?: string;
};
export function PageLoading({ message, subtitle }: PageLoadingProps) {
return (
<div className="flex min-h-[400px] flex-1 items-center justify-center">
<LoadingState message={message || "Loading..."} size="lg" />
{subtitle && (
<p className="mt-2 text-center text-[13px] text-[var(--text-muted)]">
{subtitle}
</p>
)}
</div>
);
}
type InlineLoadingProps = {
text?: string;
size?: "sm" | "md" | "lg";
className?: string;
};
export function InlineLoading({
text = "Loading...",
size = "sm",
className,
}: InlineLoadingProps) {
return (
<div
className={cn(
"flex items-center gap-2 text-[var(--text-secondary)]",
className,
)}
>
<span
className={cn(
"animate-spin rounded-full border-current border-t-transparent",
size === "sm" && "h-3 w-3 border-2",
size === "md" && "h-4 w-4 border-2",
size === "lg" && "h-5 w-5 border-2",
)}
/>
<span className="text-[12px] sm:text-[13px]">{text}</span>
</div>
);
}
type CardSkeletonProps = {
lines?: number;
withImage?: boolean;
className?: string;
};
export function CardSkeleton({
lines = 3,
withImage = false,
className,
}: CardSkeletonProps) {
return (
<div
className={cn(
"workspace-card overflow-hidden p-4",
className,
)}
>
{withImage && (
<Skeleton variant="rect" width="100%" height={120} className="mb-4" />
)}
<Skeleton variant="text" width="60%" className="mb-2" />
<Skeleton variant="text" width="100%" lines={lines} />
</div>
);
}