Skip to content

Commit ffbc3b3

Browse files
committed
merge: Skeleton loader — shimmer gradient, varianti card/table/profile/chart/text/custom
2 parents 72bae8e + 2da0342 commit ffbc3b3

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

web/components/Skeleton.tsx

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
'use client'
2+
3+
export type SkeletonVariant = 'card' | 'table' | 'profile' | 'chart' | 'text' | 'custom'
4+
5+
export interface SkeletonProps {
6+
variant?: SkeletonVariant
7+
/** Per variant='table': numero di righe */
8+
rows?: number
9+
/** Per variant='custom' o override */
10+
width?: number | string
11+
height?: number | string
12+
/** Bordo tondo */
13+
rounded?: boolean
14+
className?: string
15+
}
16+
17+
/* ── Base shimmer block ── */
18+
function Bone({ w, h, r = 6, style }: { w?: number|string; h?: number|string; r?: number; style?: React.CSSProperties }) {
19+
return (
20+
<div style={{
21+
width: w ?? '100%', height: h ?? 14, borderRadius: r,
22+
background: 'var(--color-row)',
23+
backgroundImage: 'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.06) 50%, transparent 100%)',
24+
backgroundSize: '200% 100%',
25+
animation: 'skeleton-shimmer 1.6s ease-in-out infinite',
26+
flexShrink: 0,
27+
...style,
28+
}} />
29+
)
30+
}
31+
32+
/* ── Varianti ── */
33+
function CardSkeleton() {
34+
return (
35+
<div style={{ padding: 16, borderRadius: 10, border: '1px solid var(--color-border)', background: 'var(--color-panel)', display: 'flex', flexDirection: 'column', gap: 10 }}>
36+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
37+
<Bone w={40} h={40} r={8} />
38+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 6 }}>
39+
<Bone h={13} w="65%" />
40+
<Bone h={10} w="40%" />
41+
</div>
42+
<Bone w={52} h={22} r={10} />
43+
</div>
44+
<Bone h={10} w="90%" />
45+
<Bone h={10} w="75%" />
46+
<div style={{ display: 'flex', gap: 6, marginTop: 2 }}>
47+
{[52,44,60].map((w,i) => <Bone key={i} w={w} h={20} r={10} />)}
48+
</div>
49+
</div>
50+
)
51+
}
52+
53+
function TableSkeleton({ rows = 5 }: { rows: number }) {
54+
return (
55+
<div style={{ borderRadius: 8, border: '1px solid var(--color-border)', overflow: 'hidden' }}>
56+
{/* Header */}
57+
<div style={{ display: 'flex', gap: 12, padding: '10px 14px', background: 'var(--color-row)', borderBottom: '1px solid var(--color-border)' }}>
58+
<Bone w={16} h={16} r={4} style={{ flexShrink: 0 }} />
59+
{[120,90,80,60,50].map((w,i) => <Bone key={i} w={w} h={11} />)}
60+
</div>
61+
{/* Rows */}
62+
{Array.from({ length: rows }).map((_, i) => (
63+
<div key={i} style={{ display: 'flex', gap: 12, padding: '9px 14px', borderBottom: i < rows-1 ? '1px solid var(--color-border)' : 'none', alignItems: 'center' }}>
64+
<Bone w={16} h={16} r={4} style={{ flexShrink: 0 }} />
65+
{[120,90,80,60,50].map((w,j) => <Bone key={j} w={w * (0.7 + Math.random() * 0.6)} h={10} />)}
66+
</div>
67+
))}
68+
</div>
69+
)
70+
}
71+
72+
function ProfileSkeleton() {
73+
return (
74+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
75+
{/* Header */}
76+
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
77+
<Bone w={72} h={72} r={36} />
78+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 8 }}>
79+
<Bone h={16} w="50%" />
80+
<Bone h={11} w="35%" />
81+
<div style={{ display: 'flex', gap: 6, marginTop: 2 }}>
82+
{[70,56,80].map((w,i) => <Bone key={i} w={w} h={22} r={10} />)}
83+
</div>
84+
</div>
85+
<Bone w={88} h={32} r={8} />
86+
</div>
87+
{/* Stats row */}
88+
<div style={{ display: 'flex', gap: 10 }}>
89+
{[1,2,3,4].map(i => (
90+
<div key={i} style={{ flex: 1, padding: '10px 12px', borderRadius: 8, border: '1px solid var(--color-border)', display: 'flex', flexDirection: 'column', gap: 6 }}>
91+
<Bone h={18} w="50%" />
92+
<Bone h={10} w="70%" />
93+
</div>
94+
))}
95+
</div>
96+
{/* Bio lines */}
97+
{[100,85,90,60].map((w,i) => <Bone key={i} h={11} w={`${w}%`} />)}
98+
</div>
99+
)
100+
}
101+
102+
function ChartSkeleton() {
103+
return (
104+
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
105+
{/* Titolo + legenda */}
106+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
107+
<Bone h={14} w={120} />
108+
<div style={{ display: 'flex', gap: 8 }}>
109+
{[48,56,44].map((w,i) => <Bone key={i} w={w} h={11} />)}
110+
</div>
111+
</div>
112+
{/* Bars */}
113+
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 8, height: 100, paddingTop: 8 }}>
114+
{[65,45,80,55,70,40,90,60,75,50,85,45].map((h,i) => (
115+
<Bone key={i} style={{ flex: 1 }} h={h} r={4} />
116+
))}
117+
</div>
118+
{/* Asse x */}
119+
<div style={{ display: 'flex', gap: 8 }}>
120+
{Array.from({length:12}).map((_,i) => <Bone key={i} style={{ flex: 1 }} h={9} />)}
121+
</div>
122+
</div>
123+
)
124+
}
125+
126+
/* ── Export principale ── */
127+
export default function Skeleton({ variant = 'text', rows = 5, width, height, rounded, className }: SkeletonProps) {
128+
if (variant === 'card') return <CardSkeleton />
129+
if (variant === 'table') return <TableSkeleton rows={rows} />
130+
if (variant === 'profile') return <ProfileSkeleton />
131+
if (variant === 'chart') return <ChartSkeleton />
132+
133+
// 'text' | 'custom'
134+
return <Bone w={width ?? '100%'} h={height ?? 14} r={rounded ? 999 : 6} />
135+
}
136+
137+
/* ── CSS keyframe (iniettato una sola volta) ── */
138+
if (typeof document !== 'undefined' && !document.getElementById('skeleton-kf')) {
139+
const s = document.createElement('style')
140+
s.id = 'skeleton-kf'
141+
s.textContent = '@keyframes skeleton-shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }'
142+
document.head.appendChild(s)
143+
}

0 commit comments

Comments
 (0)