From 68cb075928e4dc27cfe104728c28004f167be174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CDove1010=E2=80=9D?= <“bookwiffidrips@gmail.com”> Date: Mon, 19 Oct 2026 03:22:42 +0100 Subject: [PATCH] fix: match skeleton loader sizes to loaded content Skeletons used fixed heights that didn't match the content they stood in for, causing layout shift when data arrived. - TvlChart: use a responsive CSS height shared by the skeleton, empty state and chart instead of useBreakpointValue, whose "sm" fallback rendered the skeleton at 180px before jumping to 140px on phones. - PoolDetailClient: wrap the real stat value and pool title in Skeleton so they size to the text's line box; render the earnings placeholder as the same row of StatCards; render depositor placeholder rows inside the real table instead of taller standalone bars. - Contributors: set skeleton heights to the loaded text line heights. Co-Authored-By: Claude Opus 5.5 --- src/app/contributors/page.tsx | 14 +- src/app/farm/[poolId]/PoolDetailClient.tsx | 47 +++---- src/components/TvlChart/TvlChart.tsx | 141 +++++++++++---------- 3 files changed, 107 insertions(+), 95 deletions(-) diff --git a/src/app/contributors/page.tsx b/src/app/contributors/page.tsx index 38e3d83..c359e8f 100644 --- a/src/app/contributors/page.tsx +++ b/src/app/contributors/page.tsx @@ -6,16 +6,18 @@ import NextLink from "next/link"; import contributorData from "@/data/contributors.json"; import { safeHttpUrl } from "@/lib/sanitize"; +// Heights mirror the loaded page's line boxes (font size × 1.5 body line +// height) so swapping in real content doesn't shift the layout. function ContributorsSkeleton() { return ( - + - - + + - - - + + + ))} diff --git a/src/app/farm/[poolId]/PoolDetailClient.tsx b/src/app/farm/[poolId]/PoolDetailClient.tsx index fee281c..c7a5673 100644 --- a/src/app/farm/[poolId]/PoolDetailClient.tsx +++ b/src/app/farm/[poolId]/PoolDetailClient.tsx @@ -17,7 +17,6 @@ import { ModalHeader, ModalOverlay, Skeleton, - SkeletonText, Spinner, Table, Tbody, @@ -83,13 +82,13 @@ function StatCard({ {label} - {loading ? ( - - ) : ( + {/* Wrapping the real Text (rather than a fixed-height bar) sizes the + skeleton to the value's actual line box, so nothing shifts on load. */} + {value} - )} + ); } @@ -264,9 +263,7 @@ export default function PoolDetailClient({ poolId }: { poolId: string }) { {/* Header */} - {poolLoading ? ( - - ) : ( + - {pool?.asset.code ?? "Pool"} Pool + {poolLoading ? "Pool" : `${pool?.asset.code ?? "Pool"} Pool`} - )} + {!poolLoading && ( {creditsLoading || positionLoading ? ( - - {Array.from({ length: 3 }).map((_, i) => ( - - ))} + // Same row of StatCards as the loaded state, so the section + // keeps its height and layout when earnings arrive. + + + + ) : userCredits ? ( @@ -388,13 +387,7 @@ export default function PoolDetailClient({ poolId }: { poolId: string }) { Top 20 Depositors - {depositorsLoading ? ( - - {Array.from({ length: 5 }).map((_, i) => ( - - ))} - - ) : depositors.length === 0 ? ( + {!depositorsLoading && depositors.length === 0 ? ( No depositors yet. @@ -410,7 +403,17 @@ export default function PoolDetailClient({ poolId }: { poolId: string }) { - {depositors.map((d, i) => ( + {/* Skeleton rows reuse the real table's cells (and its + sm row height) so the list doesn't jump on load. */} + {depositorsLoading && Array.from({ length: 5 }).map((_, i) => ( + + + + + + + ))} + {!depositorsLoading && depositors.map((d, i) => ( {i + 1} diff --git a/src/components/TvlChart/TvlChart.tsx b/src/components/TvlChart/TvlChart.tsx index 2153e84..2284b25 100644 --- a/src/components/TvlChart/TvlChart.tsx +++ b/src/components/TvlChart/TvlChart.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import { Box, Skeleton, Text, useBreakpointValue } from "@chakra-ui/react"; +import { Box, Skeleton, Text } from "@chakra-ui/react"; import { AreaChart, Area, @@ -13,6 +13,14 @@ import { const ACCENT = "#4ae292"; +// Issue #214: a fixed 180px is tall relative to viewport width on small +// phones; shrink it below the sm breakpoint instead. Expressed as a +// responsive CSS value (not useBreakpointValue) so the skeleton, empty +// state and chart all resolve to the same height on first paint — the JS +// breakpoint hook falls back to "sm" until hydration, which made the +// skeleton render at 180px and then jump to 140px on phones. +const CHART_HEIGHT = { base: "140px", sm: "180px" }; + interface TvlDataPoint { date: string; tvl: string; @@ -26,9 +34,6 @@ interface TvlChartProps { export default function TvlChart({ poolId }: TvlChartProps) { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); - // Issue #214: a fixed 180px is tall relative to viewport width on small - // phones; shrink it below the sm breakpoint instead. - const chartHeight = useBreakpointValue({ base: 140, sm: 180 }, { fallback: "sm" }) ?? 180; useEffect(() => { let cancelled = false; @@ -50,13 +55,13 @@ export default function TvlChart({ poolId }: TvlChartProps) { }, [poolId]); if (loading) { - return ; + return ; } if (data.length === 0) { return ( - - - - - - - - - new Date(v).toLocaleDateString(undefined, { - month: "short", - day: "numeric", - }) - } - axisLine={false} - tickLine={false} - /> - - v >= 1_000_000 - ? `$${(v / 1_000_000).toFixed(1)}M` - : v >= 1_000 - ? `$${(v / 1_000).toFixed(0)}K` - : `$${v}` - } - axisLine={false} - tickLine={false} - width={56} - /> - - new Date(String(label)).toLocaleDateString(undefined, { - weekday: "short", - month: "short", - day: "numeric", - }) - } - formatter={(value) => [`$${Number(value).toLocaleString()}`, "TVL"]} - /> - - - + + + + + + + + + + + new Date(v).toLocaleDateString(undefined, { + month: "short", + day: "numeric", + }) + } + axisLine={false} + tickLine={false} + /> + + v >= 1_000_000 + ? `$${(v / 1_000_000).toFixed(1)}M` + : v >= 1_000 + ? `$${(v / 1_000).toFixed(0)}K` + : `$${v}` + } + axisLine={false} + tickLine={false} + width={56} + /> + + new Date(String(label)).toLocaleDateString(undefined, { + weekday: "short", + month: "short", + day: "numeric", + }) + } + formatter={(value) => [`$${Number(value).toLocaleString()}`, "TVL"]} + /> + + + + ); }