animate
This commit is contained in:
parent
27da51ecf2
commit
4d3f57870a
|
|
@ -827,6 +827,318 @@ function HeatmapGrid({
|
|||
);
|
||||
}
|
||||
|
||||
// ── Skeleton wave path (representative 14-day pattern) ─────────────────────
|
||||
const SKEL_LINE =
|
||||
"M34,155 C60,145 90,50 130,42 C170,34 200,100 240,108 C280,116 315,38 355,30 C395,22 430,82 468,96 C495,105 520,128 572,158";
|
||||
const SKEL_AREA = `${SKEL_LINE} L572,${LP_T + LCH} L34,${LP_T + LCH} Z`;
|
||||
const SKEL_HEAT = [
|
||||
0.2, 0.55, 0.85, 0.95, 0.7, 0.3, 0.45, 0.9, 1.0, 0.9, 0.65, 0.4, 0.75,
|
||||
0.1,
|
||||
];
|
||||
|
||||
function LineChartSkeleton() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-4 w-4 animate-pulse rounded"
|
||||
style={{ background: "rgba(139,92,246,0.30)" }}
|
||||
/>
|
||||
<div
|
||||
className="h-4 w-24 animate-pulse rounded"
|
||||
style={{ background: "rgba(255,255,255,0.10)" }}
|
||||
/>
|
||||
<div
|
||||
className="h-5 w-9 animate-pulse rounded"
|
||||
style={{ background: "rgba(139,92,246,0.20)" }}
|
||||
/>
|
||||
<div
|
||||
className="hidden h-3 w-16 animate-pulse rounded sm:block"
|
||||
style={{ background: "rgba(255,255,255,0.07)" }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="h-8 w-[86px] animate-pulse rounded-lg"
|
||||
style={{
|
||||
background: "rgba(139,92,246,0.12)",
|
||||
border: "1px solid rgba(139,92,246,0.20)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<svg
|
||||
viewBox={`0 0 ${LVW} ${LVH}`}
|
||||
width="100%"
|
||||
style={{ display: "block", height: LVH }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="skel-v-grad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="rgba(139,92,246,0.22)" />
|
||||
<stop offset="80%" stopColor="rgba(139,92,246,0.02)" />
|
||||
<stop offset="100%" stopColor="rgba(139,92,246,0)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="skel-shimmer"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="260"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0%" stopColor="rgba(255,255,255,0)" />
|
||||
<stop offset="50%" stopColor="rgba(255,255,255,0.07)" />
|
||||
<stop offset="100%" stopColor="rgba(255,255,255,0)" />
|
||||
</linearGradient>
|
||||
<clipPath id="skel-clip">
|
||||
<rect x={LP_L} y={LP_T - 2} width={LCW} height={LCH + 4} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
{[1, 2, 3, 4].map((i) => {
|
||||
const y = LP_T + LCH - (i / 4) * LCH;
|
||||
return (
|
||||
<g key={i}>
|
||||
<line
|
||||
x1={LP_L}
|
||||
y1={y}
|
||||
x2={LP_L + LCW}
|
||||
y2={y}
|
||||
strokeDasharray="4 5"
|
||||
style={{ stroke: W12, strokeWidth: 1 }}
|
||||
/>
|
||||
<rect
|
||||
x={2}
|
||||
y={y - 4}
|
||||
width={26}
|
||||
height={8}
|
||||
rx={2}
|
||||
style={{ fill: "rgba(255,255,255,0.06)" }}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
<line
|
||||
x1={LP_L}
|
||||
y1={LP_T + LCH}
|
||||
x2={LP_L + LCW}
|
||||
y2={LP_T + LCH}
|
||||
style={{ stroke: W12, strokeWidth: 1 }}
|
||||
/>
|
||||
<path
|
||||
d={SKEL_AREA}
|
||||
fill="url(#skel-v-grad)"
|
||||
clipPath="url(#skel-clip)"
|
||||
/>
|
||||
<path
|
||||
d={SKEL_LINE}
|
||||
fill="none"
|
||||
stroke="rgba(139,92,246,0.38)"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
clipPath="url(#skel-clip)"
|
||||
/>
|
||||
<rect
|
||||
x={LP_L - 130}
|
||||
y={LP_T - 2}
|
||||
width={260}
|
||||
height={LCH + 4}
|
||||
fill="url(#skel-shimmer)"
|
||||
clipPath="url(#skel-clip)"
|
||||
>
|
||||
<animate
|
||||
attributeName="x"
|
||||
from={LP_L - 260}
|
||||
to={LP_L + LCW + 130}
|
||||
dur="1.8s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
{[0, 2, 4, 6, 8, 10, 12, 13].map((i) => (
|
||||
<rect
|
||||
key={i}
|
||||
x={LP_L + (i / 13) * LCW - 14}
|
||||
y={LVH - 14}
|
||||
width={28}
|
||||
height={8}
|
||||
rx={2}
|
||||
style={{ fill: "rgba(255,255,255,0.06)" }}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
<div className="mt-3 flex items-center justify-center gap-2.5">
|
||||
<div
|
||||
className="h-5 w-14 animate-pulse rounded"
|
||||
style={{ background: "rgba(139,92,246,0.18)" }}
|
||||
/>
|
||||
<div
|
||||
className="h-4 w-44 animate-pulse rounded"
|
||||
style={{ background: "rgba(255,255,255,0.07)" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeatmapSkeleton() {
|
||||
const cell = 15,
|
||||
gap = 4;
|
||||
const availableWidth = LVW - HLEFT - HRIGHT;
|
||||
const totalWidth =
|
||||
SKEL_HEAT.length * cell + (SKEL_HEAT.length - 1) * gap;
|
||||
const hx = HLEFT + (availableWidth - totalWidth) / 2;
|
||||
const hy = 54;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-4 w-4 animate-pulse rounded"
|
||||
style={{ background: "rgba(16,185,129,0.30)" }}
|
||||
/>
|
||||
<div
|
||||
className="h-4 w-28 animate-pulse rounded"
|
||||
style={{ background: "rgba(255,255,255,0.10)" }}
|
||||
/>
|
||||
<div
|
||||
className="h-5 w-9 animate-pulse rounded"
|
||||
style={{ background: "rgba(16,185,129,0.18)" }}
|
||||
/>
|
||||
<div
|
||||
className="hidden h-3 w-16 animate-pulse rounded sm:block"
|
||||
style={{ background: "rgba(255,255,255,0.07)" }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="h-8 w-[86px] animate-pulse rounded-lg"
|
||||
style={{
|
||||
background: "rgba(16,185,129,0.10)",
|
||||
border: "1px solid rgba(16,185,129,0.22)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<svg
|
||||
viewBox={`0 0 ${LVW} ${HVH}`}
|
||||
width="100%"
|
||||
style={{ display: "block", height: LVH }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="skel-heat-shimmer"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="200"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0%" stopColor="rgba(255,255,255,0)" />
|
||||
<stop offset="50%" stopColor="rgba(255,255,255,0.08)" />
|
||||
<stop offset="100%" stopColor="rgba(255,255,255,0)" />
|
||||
</linearGradient>
|
||||
<clipPath id="skel-heat-clip">
|
||||
<rect x={hx} y={hy} width={totalWidth} height={cell} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
{SKEL_HEAT.map((opacity, i) => (
|
||||
<rect
|
||||
key={i}
|
||||
x={hx + i * (cell + gap)}
|
||||
y={hy}
|
||||
width={cell}
|
||||
height={cell}
|
||||
rx={4}
|
||||
style={{ fill: `rgba(16,185,129,${opacity})` }}
|
||||
/>
|
||||
))}
|
||||
<rect
|
||||
x={hx - 100}
|
||||
y={hy}
|
||||
width={200}
|
||||
height={cell}
|
||||
fill="url(#skel-heat-shimmer)"
|
||||
clipPath="url(#skel-heat-clip)"
|
||||
>
|
||||
<animate
|
||||
attributeName="x"
|
||||
from={hx - 200}
|
||||
to={hx + totalWidth + 100}
|
||||
dur="1.8s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
{[0, 4, 9, 13].map((i) => (
|
||||
<rect
|
||||
key={i}
|
||||
x={hx + i * (cell + gap) + cell / 2 - 12}
|
||||
y={hy + cell + 9}
|
||||
width={24}
|
||||
height={8}
|
||||
rx={2}
|
||||
style={{ fill: "rgba(255,255,255,0.06)" }}
|
||||
/>
|
||||
))}
|
||||
{HMAP_FILL.map((fill, i) => (
|
||||
<rect
|
||||
key={i}
|
||||
x={HLEFT + 32 + i * 14}
|
||||
y={HVH - 20}
|
||||
width={11}
|
||||
height={11}
|
||||
rx={2}
|
||||
style={{ fill, opacity: 0.35 }}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<div
|
||||
className="h-8 w-20 animate-pulse rounded"
|
||||
style={{ background: "rgba(16,185,129,0.15)" }}
|
||||
/>
|
||||
<div
|
||||
className="h-3 w-56 animate-pulse rounded"
|
||||
style={{ background: "rgba(255,255,255,0.07)" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LineStatsSkeleton() {
|
||||
return (
|
||||
<div
|
||||
className="mx-auto flex max-w-xs items-center justify-center gap-8 rounded-xl px-8 py-4"
|
||||
style={{
|
||||
background: "rgba(139,92,246,0.07)",
|
||||
border: "1px solid rgba(139,92,246,0.20)",
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<div
|
||||
className="h-8 w-16 animate-pulse rounded"
|
||||
style={{ background: "rgba(52,211,153,0.18)" }}
|
||||
/>
|
||||
<div
|
||||
className="h-3 w-28 animate-pulse rounded"
|
||||
style={{ background: "rgba(255,255,255,0.07)" }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{ width: 1, height: 40, background: "rgba(139,92,246,0.25)" }}
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<div
|
||||
className="h-8 w-16 animate-pulse rounded"
|
||||
style={{ background: "rgba(248,113,113,0.18)" }}
|
||||
/>
|
||||
<div
|
||||
className="h-3 w-24 animate-pulse rounded"
|
||||
style={{ background: "rgba(255,255,255,0.07)" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main export ────────────────────────────────────────────────────────────
|
||||
export function ForgejoHeatmap({
|
||||
days,
|
||||
|
|
@ -842,28 +1154,16 @@ export function ForgejoHeatmap({
|
|||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{[0, 1].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4"
|
||||
>
|
||||
<div className="mb-3 flex justify-between">
|
||||
<div
|
||||
className="h-4 w-28 animate-pulse rounded"
|
||||
style={{ background: "rgba(255,255,255,0.08)" }}
|
||||
/>
|
||||
<div
|
||||
className="h-4 w-20 animate-pulse rounded"
|
||||
style={{ background: "rgba(255,255,255,0.08)" }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="animate-pulse rounded"
|
||||
style={{ height: LVH, background: "rgba(255,255,255,0.05)" }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush">
|
||||
<LineChartSkeleton />
|
||||
</section>
|
||||
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush">
|
||||
<HeatmapSkeleton />
|
||||
</section>
|
||||
</div>
|
||||
<LineStatsSkeleton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue