This commit is contained in:
null 2026-05-27 19:04:51 -05:00
parent 27da51ecf2
commit 4d3f57870a
1 changed files with 322 additions and 22 deletions

View File

@ -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>
);
}