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 ────────────────────────────────────────────────────────────
|
// ── Main export ────────────────────────────────────────────────────────────
|
||||||
export function ForgejoHeatmap({
|
export function ForgejoHeatmap({
|
||||||
days,
|
days,
|
||||||
|
|
@ -842,28 +1154,16 @@ export function ForgejoHeatmap({
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<div className="space-y-4">
|
||||||
{[0, 1].map((i) => (
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
<div
|
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush">
|
||||||
key={i}
|
<LineChartSkeleton />
|
||||||
className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4"
|
</section>
|
||||||
>
|
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush">
|
||||||
<div className="mb-3 flex justify-between">
|
<HeatmapSkeleton />
|
||||||
<div
|
</section>
|
||||||
className="h-4 w-28 animate-pulse rounded"
|
</div>
|
||||||
style={{ background: "rgba(255,255,255,0.08)" }}
|
<LineStatsSkeleton />
|
||||||
/>
|
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue