Skip to content

Commit 28813c0

Browse files
committed
Add reusable Skeleton loading components
Introduces a generic Skeleton component with support for text, circular, and rectangular variants, as well as pre-built skeletons for cards, stats, projects, tables, and lists. These components provide consistent loading placeholders throughout the UI.
1 parent 8c76d60 commit 28813c0

1 file changed

Lines changed: 132 additions & 0 deletions

File tree

components/skeleton.tsx

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"use client"
2+
3+
import { cn } from "@/lib/utils"
4+
5+
export interface SkeletonProps {
6+
className?: string
7+
variant?: "text" | "circular" | "rectangular"
8+
width?: string | number
9+
height?: string | number
10+
count?: number
11+
}
12+
13+
export function Skeleton({
14+
className,
15+
variant = "rectangular",
16+
width,
17+
height,
18+
count = 1,
19+
...props
20+
}: SkeletonProps & React.HTMLAttributes<HTMLDivElement>) {
21+
const baseClasses = "animate-pulse bg-muted"
22+
23+
const variantClasses = {
24+
text: "h-4 rounded",
25+
circular: "rounded-full",
26+
rectangular: "rounded",
27+
}
28+
29+
const style: React.CSSProperties = {
30+
width: width || undefined,
31+
height: height || undefined,
32+
}
33+
34+
if (count === 1) {
35+
return (
36+
<div
37+
className={cn(baseClasses, variantClasses[variant], className)}
38+
style={style}
39+
{...props}
40+
/>
41+
)
42+
}
43+
44+
return (
45+
<div className="space-y-2">
46+
{Array.from({ length: count }).map((_, i) => (
47+
<div
48+
key={i}
49+
className={cn(baseClasses, variantClasses[variant], className)}
50+
style={style}
51+
{...props}
52+
/>
53+
))}
54+
</div>
55+
)
56+
}
57+
58+
// Pre-built skeleton components for common use cases
59+
export function CardSkeleton() {
60+
return (
61+
<div className="border border-border p-6 bg-card space-y-4">
62+
<Skeleton className="h-6 w-3/4" />
63+
<Skeleton count={3} className="h-4" />
64+
<div className="flex gap-2 pt-2">
65+
<Skeleton className="h-8 w-20" />
66+
<Skeleton className="h-8 w-20" />
67+
</div>
68+
</div>
69+
)
70+
}
71+
72+
export function StatCardSkeleton() {
73+
return (
74+
<div className="border border-border p-6 bg-card">
75+
<div className="flex items-start justify-between">
76+
<div className="space-y-2 flex-1">
77+
<Skeleton className="h-4 w-24" />
78+
<Skeleton className="h-8 w-16" />
79+
<Skeleton className="h-3 w-20" />
80+
</div>
81+
<Skeleton variant="circular" width={32} height={32} />
82+
</div>
83+
</div>
84+
)
85+
}
86+
87+
export function ProjectCardSkeleton() {
88+
return (
89+
<div className="border border-border p-4 bg-card">
90+
<div className="flex items-center justify-between">
91+
<div className="space-y-2 flex-1">
92+
<Skeleton className="h-5 w-48" />
93+
<Skeleton className="h-4 w-32" />
94+
</div>
95+
<Skeleton className="h-6 w-20" />
96+
</div>
97+
</div>
98+
)
99+
}
100+
101+
export function TableSkeleton({ rows = 5 }: { rows?: number }) {
102+
return (
103+
<div className="space-y-2">
104+
{Array.from({ length: rows }).map((_, i) => (
105+
<div key={i} className="border border-border p-4 flex items-center gap-4">
106+
<Skeleton variant="circular" width={40} height={40} />
107+
<div className="flex-1 space-y-2">
108+
<Skeleton className="h-4 w-3/4" />
109+
<Skeleton className="h-3 w-1/2" />
110+
</div>
111+
<Skeleton className="h-8 w-24" />
112+
</div>
113+
))}
114+
</div>
115+
)
116+
}
117+
118+
export function ListSkeleton({ items = 5 }: { items?: number }) {
119+
return (
120+
<div className="space-y-3">
121+
{Array.from({ length: items }).map((_, i) => (
122+
<div key={i} className="flex items-start gap-3">
123+
<Skeleton variant="circular" width={8} height={8} className="mt-2" />
124+
<div className="flex-1 space-y-2">
125+
<Skeleton className="h-4 w-full" />
126+
<Skeleton className="h-3 w-2/3" />
127+
</div>
128+
</div>
129+
))}
130+
</div>
131+
)
132+
}

0 commit comments

Comments
 (0)