From 1e469d8185a6c9207debea0a7204dc2409ccb28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 3 Jun 2026 20:06:51 +0200 Subject: [PATCH] fix(insights): eliminate layout shift on the Today's-signal card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dynamic loader skeleton reserved 120px while the resolved card's insufficient (~168px) and fired (~192px) states ran taller, shifting the hero and everything below it down 48–72px on resolve. Pin a min-h-48 on the card shell sized to the tallest realistic state, and match both the chunk-loader skeleton and the in-component CardSkeleton to that footprint (including the 44px provenance header row) so there is zero shift across loading → any state. Announce the fired state to screen readers via a polite role=status (not the assertive role=alert), and mark the decorative loader skeleton aria-hidden. --- src/app/insights/page.tsx | 12 +++++--- .../coincident-deviation-card.test.tsx | 27 +++++++++++++++++ .../insights/coincident-deviation-card.tsx | 29 +++++++++++++++---- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index df65d17cd..5e13eae83 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -100,9 +100,10 @@ const RhythmEventsCard = dynamic( // v1.10.3 — "Today's signal" headline card. Promotes COINCIDENT_DEVIATION from // a buried below-the-fold tile to the top-of-overview daily read (the // always-present Apple/WHOOP/Oura pattern). Deferred behind `next/dynamic`; it -// owns its own derived-metric query and renders four calm states. A -// fixed-height skeleton matches the resolved card so the top of the page does -// not shift while the chunk + data resolve. +// owns its own derived-metric query and renders four calm states. The chunk +// loader's skeleton shares the card's `min-h-48` footprint (and the in-card +// `CardSkeleton` matches it too) so the top of the page does not shift across +// loading → any resolved state. Decorative → `aria-hidden`. const CoincidentDeviationCard = dynamic( () => import("@/components/insights/coincident-deviation-card").then((mod) => ({ @@ -111,7 +112,10 @@ const CoincidentDeviationCard = dynamic( { ssr: false, loading: () => ( -
+