DocDoeAI / src /components /shared /SharedSkeleton.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
4.93 kB
"use client";
import { HTMLAttributes } from "react";
import { clsx } from "clsx";
// =============================================================================
// Skeleton Types
// =============================================================================
export type SkeletonVariant =
| "text"
| "heading"
| "paragraph"
| "card"
| "button"
| "avatar"
| "image"
| "list"
| "circular";
export interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
variant?: SkeletonVariant;
lines?: number;
width?: string | number;
height?: string | number;
animated?: boolean;
}
// =============================================================================
// SharedSkeleton Component
// =============================================================================
export function SharedSkeleton({
variant = "text",
lines = 1,
width,
height,
animated = true,
className,
...props
}: SkeletonProps) {
// Default dimensions for each variant
const defaults: Record<SkeletonVariant, { width: string; height: string }> = {
text: { width: "100%", height: "1em" },
heading: { width: "60%", height: "1.5em" },
paragraph: { width: "100%", height: "1em" },
card: { width: "100%", height: "120px" },
button: { width: "120px", height: "40px" },
avatar: { width: "40px", height: "40px" },
image: { width: "100%", height: "200px" },
list: { width: "100%", height: "40px" },
circular: { width: "40px", height: "40px" },
};
const d = defaults[variant];
const w = width || d.width;
const h = height || d.height;
// Build style object
const style: React.CSSProperties = {
width: typeof w === "number" ? `${w}px` : w,
height: typeof h === "number" ? `${h}px` : h,
};
// Animation classes
const animationClasses = animated ? "animate-skeleton-pulse" : "";
// Variant-specific classes (reserved)
const _variantClasses = clsx({
circular: "rounded-full",
card: "rounded-lg",
button: "rounded-lg",
avatar: "rounded-full",
image: "rounded-lg",
// Default to rounded for text/paragraph/heading/list
});
if (variant === "paragraph" && lines > 1) {
return (
<div className={clsx("space-y-2", className)} {...props}>
{Array.from({ length: lines }).map((_, i) => (
<SharedSkeleton
key={i}
variant="text"
animated={animated}
width={i === 0 ? "80%" : i === lines - 1 ? "60%" : "100%"}
/>
))}
</div>
);
}
if (variant === "list" && lines > 1) {
return (
<div className={clsx("space-y-2", className)} {...props}>
{Array.from({ length: lines }).map((_, i) => (
<SharedSkeleton key={i} variant="list" animated={animated} />
))}
</div>
);
}
return (
<div
className={clsx(
"bg-[var(--bg-elevated)]",
"border",
"border-[var(--border-subtle)]",
variant !== "circular" && variant !== "avatar" && "rounded-md",
variant === "circular" && "rounded-full",
variant === "avatar" && "rounded-full",
animationClasses,
className,
)}
style={style}
{...props}
/>
);
}
// =============================================================================
// Skeleton Containers
// =============================================================================
// Card skeleton (matching SharedCard)
export function SkeletonCard({
lines = 3,
className,
...props
}: { lines?: number } & Omit<SkeletonProps, "variant">) {
return (
<SharedSkeleton
variant="card"
className={clsx("rounded-xl", className)}
{...props}
>
<div className="space-y-3 p-7">
<SharedSkeleton variant="heading" animated={props.animated} />
{Array.from({ length: lines }).map((_, i) => (
<SharedSkeleton
key={i}
variant="text"
animated={props.animated}
width={i === lines - 1 ? "75%" : "100%"}
/>
))}
</div>
</SharedSkeleton>
);
}
// Button skeleton
export function SkeletonButton({ className, ...props }: Omit<SkeletonProps, "variant">) {
return (
<SharedSkeleton
variant="button"
className={clsx("rounded-lg", className)}
{...props}
/>
);
}
// Input skeleton
export function SkeletonInput({ className, ...props }: Omit<SkeletonProps, "variant">) {
return (
<SharedSkeleton
variant="text"
height="44px"
className={clsx("rounded-lg w-full", className)}
{...props}
/>
);
}
// Avatar skeleton
export function SkeletonAvatar({
size = 40,
className,
...props
}: { size?: number } & Omit<SkeletonProps, "variant" | "width" | "height">) {
return (
<SharedSkeleton
variant="circular"
width={size}
height={size}
className={className}
{...props}
/>
);
}