diff --git a/src/app/admin/[section]/renderer.tsx b/src/app/admin/[section]/renderer.tsx index 89cb04f39..d927a353e 100644 --- a/src/app/admin/[section]/renderer.tsx +++ b/src/app/admin/[section]/renderer.tsx @@ -36,6 +36,8 @@ import { ModuleAvailabilitySection } from "@/components/admin/module-availabilit import { RemindersSection } from "@/components/admin/reminders-section"; import { ServicesSection } from "@/components/admin/services-section"; import { SystemStatusSection } from "@/components/admin/system-status-section"; +import { RevealWhenSettled } from "@/components/ui/reveal-when-settled"; +import { Skeleton } from "@/components/ui/skeleton"; import { MeasurementMaintenanceCard } from "@/components/admin/measurement-maintenance-card"; import { InviteTokensSection } from "@/components/admin/invite-tokens-section"; import { UserManagementSection } from "@/components/admin/user-management-section"; @@ -59,11 +61,18 @@ export function AdminSectionRenderer({ switch (slug) { case "system-status": return ( - - - {/* v1.42 — the table maintenance after the tombstone purge. */} - - + // The status cards grow as their reads land (host metrics, workers, + // queues); revealed together so the maintenance card below is not + // pushed down the page three times while they load. + } + > + + + {/* v1.42 — the table maintenance after the tombstone purge. */} + + + ); case "general": return ( diff --git a/src/app/measurements/page.tsx b/src/app/measurements/page.tsx index 64e0d653a..ac0efe6cc 100644 --- a/src/app/measurements/page.tsx +++ b/src/app/measurements/page.tsx @@ -17,6 +17,8 @@ import { PageHeader } from "@/components/ui/page-header"; import { PageAuthGate } from "@/components/ui/page-auth-gate"; import { PullToRefreshIndicator } from "@/components/ui/pull-to-refresh-indicator"; import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; +import { RevealWhenSettled } from "@/components/ui/reveal-when-settled"; +import { Skeleton } from "@/components/ui/skeleton"; import { Plus } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; import { useTranslations } from "@/lib/i18n/context"; @@ -203,14 +205,21 @@ export default function MeasurementsPage() { /> - { - setReturnTo(null); - setDialogOpen(true); - }} - /> + {/* The list's height is the data's; the custom metrics sit below it. + Revealed together once both reads settle, so neither pushes the + other down the page while it loads. */} + } + > + { + setReturnTo(null); + setDialogOpen(true); + }} + /> - + + ); } diff --git a/src/components/charts/chart-skeleton.tsx b/src/components/charts/chart-skeleton.tsx index 6e1c0e7c3..2d10370cd 100644 --- a/src/components/charts/chart-skeleton.tsx +++ b/src/components/charts/chart-skeleton.tsx @@ -48,11 +48,51 @@ import { useTranslations } from "@/lib/i18n/context"; const SLOW_HINT_DELAY_MS = 3_000; +/** + * The footer a loaded `` paints under its plot: the row of day + * dots and its caption on a day-linked chart, and the collapsed "show as + * table" disclosure. A loading shell without it was ~100 px shorter than the + * card it turned into (CLS on every metric page, the BMI page most visibly), + * so the shells reserve the same boxes: `h-3` rug, `mt-1` caption (two lines + * on a phone, one from `sm`), and the disclosure's `mt-3 border-t pt-3` frame + * around its `min-h-11` button. + */ +export function ChartFooterReserve({ + dayLinks = false, + dataTable = false, +}: { + dayLinks?: boolean; + dataTable?: boolean; +}) { + if (!dayLinks && !dataTable) return null; + return ( +