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.
This commit is contained in:
Marc-André Bombeck committed 2026-06-04 18:21:14 +02:00
1 parent 93bdbd8e0c
commit 4e2248dfff
2 files changed
+24 -16

No files matched your search

@@ -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",
@@ -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({
<div
data-slot="period-narrative-card-skeleton"
aria-hidden="true"
className={cn(SHELL, className)}
className={cn(SKELETON_SHELL, className)}
>
<div className="flex h-7 items-center justify-between gap-2">
<div className="bg-muted/40 h-3 w-32 rounded" />
@@ -199,7 +204,7 @@ export function PeriodNarrativeCard({
)}
{narrative ? (
<p className="text-muted-foreground mt-auto text-[11px]">
<p className="text-muted-foreground text-[11px]">
{data?.revalidating
? t("insights.narrativeUpdating")
: t("insights.narrativeUpdated", {