From 4e2248dfff3f3613c5d1ceb7ec1350c952bfd5de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 4 Jun 2026 18:20:02 +0200 Subject: [PATCH] fix(insights): drop the empty tail on the signal and narrative cards Both overview cards carried a min-h floor sized for their tallest state and applied it to every state, so the calm everyday read left a large empty band at the bottom. - Today's signal: the min-h-48 floor now applies only to the tall `fired` state; all-clear / watch / insufficient size to their short content. - Period overview: the resolved card drops the min-h-40 floor and the footer's mt-auto that pinned the timestamp to the bottom of the floored height. The in-flight skeletons keep their min-h footprint, so the first paint still reserves the row and does not shift as the read resolves. --- .../insights/coincident-deviation-card.tsx | 21 +++++++++++-------- .../insights/period-narrative-card.tsx | 19 ++++++++++------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/components/insights/coincident-deviation-card.tsx b/src/components/insights/coincident-deviation-card.tsx index 68eb1d2f7..cd427fffa 100644 --- a/src/components/insights/coincident-deviation-card.tsx +++ b/src/components/insights/coincident-deviation-card.tsx @@ -81,8 +81,9 @@ function CoincidentProvenance({ /** * The card shell — uppercase label + the provenance affordance, with the - * state-specific body as children. Keeps every state on one card geometry so - * the page reserves a stable footprint. + * state-specific body as children. The tall `fired` state keeps a `min-h` floor; + * the calmer everyday states size to their (short) content so the card carries + * no empty tail. */ function CardShell({ state, @@ -99,13 +100,15 @@ function CardShell({ data-slot="coincident-deviation-card" data-state={state} className={cn( - // `min-h` sized to the tallest realistic state (fired: 44px provenance - // header + the named-vitals body + the mandatory factors line). Every - // calmer state shares this footprint so the card — and everything below - // it on the overview — never shifts as the state resolves (the CLS the - // design pass closes). `min-h` not fixed `h`: a rare taller body still - // grows, it just never shrinks below the reserved height. - "bg-card flex min-h-48 w-full min-w-0 flex-col gap-2 rounded-xl border p-4 md:p-6", + "bg-card flex w-full min-w-0 flex-col gap-2 rounded-xl border p-4 md:p-6", + // `min-h` floor only on the tallest state (fired: 44px provenance header + // + the named-vitals body + the mandatory factors line). The calmer + // everyday states (all-clear / watch / insufficient) carry a single + // short body, so flooring them to the fired height left a large empty + // tail; they now size to their content. The in-flight `CardSkeleton` + // keeps the `min-h-48` footprint so the initial paint still reserves the + // row and does not shift as the read resolves. + state === "fired" && "min-h-48", // At most amber — never destructive/red. The fired state borders // warning; every calmer state keeps the neutral card border. state === "fired" ? "border-warning/40" : "border-border", diff --git a/src/components/insights/period-narrative-card.tsx b/src/components/insights/period-narrative-card.tsx index ea4ac0a11..03298d1d5 100644 --- a/src/components/insights/period-narrative-card.tsx +++ b/src/components/insights/period-narrative-card.tsx @@ -22,10 +22,10 @@ import { * fresh one out of band, so this card never blocks on the provider. * * Restraint guarantees: plain React text children (NO markdown — the whole - * tree forbids a markdown library for XSS reasons), no score, no chart, a - * single ⓘ provenance disclosure, and a fixed `min-h` footprint shared across - * the loading / preparing / resolved states so the card — and everything below - * it on the overview — never shifts (no CLS). + * tree forbids a markdown library for XSS reasons), no score, no chart, and a + * single ⓘ provenance disclosure. The in-flight skeleton reserves a `min-h` + * footprint so the first paint holds the row open (CLS-safe); the resolved card + * then sizes to its content so a short narrative leaves no empty tail. */ type NarrativePeriod = "week" | "month"; @@ -98,7 +98,12 @@ function ProvenanceDisclosure({ } const SHELL = - "bg-card border-border flex min-h-40 w-full min-w-0 flex-col gap-3 rounded-xl border p-4 md:p-6"; + "bg-card border-border flex w-full min-w-0 flex-col gap-3 rounded-xl border p-4 md:p-6"; +// The in-flight skeleton keeps a `min-h` floor so the initial paint reserves the +// row (CLS-safe). The resolved card drops the floor and sizes to its content — +// a short narrative used to leave a large empty tail under the prose, widened by +// the footer's `mt-auto` pinning it to the bottom of the floored height. +const SKELETON_SHELL = `${SHELL} min-h-40`; export function PeriodNarrativeCard({ enabled = true, @@ -123,7 +128,7 @@ export function PeriodNarrativeCard({