From 36b9db34c56f27dee48a5c2b1017358ebb49f9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 10 May 2026 20:49:55 +0200 Subject: [PATCH] fix(insights): row-fill rule + equal-height trends + hide-on-insufficient-data Three layout problems landed together because they share the same root: the `/insights` grids assumed every row would be full, so when a section dropped a card the layout left a half-row of empty space. - Row-fill rule: every grid on `/insights` now picks `xl:grid-cols-2` only when 2+ cards actually render. With 1 card the grid collapses to a single full-width column. The BP-medication grid, the weight- correlation grid, and the medications-per-day grid all carried the half-empty-row bug; all three are normalised. - Trends row equal heights: each `` card is now a flex column with `min-h-[300px]` so a multi-line AI annotation under one chart doesn't push that card taller than its neighbours. The visual rhythm holds even when the prose varies in length. - Hide insufficient-data correlation tiles: `` drops any hypothesis with `status !== "ok"` instead of painting a placeholder. When zero hypotheses qualify, the entire row hides itself (no header, no disclaimer). 1 ok card spans 100 % width, 2 or 3 ok cards split 50/50 on `>=md`. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/insights/page.tsx | 28 +++++++- .../__tests__/correlation-row.test.tsx | 67 ++++++++++++------- src/components/insights/correlation-row.tsx | 42 +++++++++--- src/components/insights/trends-row.tsx | 17 ++++- 4 files changed, 113 insertions(+), 41 deletions(-) diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index b4fbbbbce..3c2eb6b2a 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -1060,7 +1060,15 @@ export default function InsightsPage() { annotations={bpStoryboardAnnotations} /> -
+ {/* v1.4.22 A4 — row-fill rule: 2 cards → 50/50 (xl), 1 card → + 100 % width. When the mood section is hidden the BP-medication + card collapses to full-width instead of leaving the right + half empty. */} +
@@ -1262,7 +1270,13 @@ export default function InsightsPage() { compareBaseline={compareBaseline} /> -
+ {/* v1.4.22 A4 — row-fill rule: 2 cards → 50/50 (xl), 1 card → + 100 % width. Mirrors the BP-section grid normalisation. */} +
@@ -1483,7 +1497,15 @@ export default function InsightsPage() {
{data.medications.length > 0 ? ( -
+ /* v1.4.22 A4 — row-fill rule: a single medication card fills + 100 % width instead of half the row. */ +
= 2 + ? "grid gap-4 sm:grid-cols-2" + : "grid gap-4" + } + > {data.medications.map((med) => { const medicationSummary = medicationSummaryById.get(med.id); return ( diff --git a/src/components/insights/__tests__/correlation-row.test.tsx b/src/components/insights/__tests__/correlation-row.test.tsx index 4882dab18..96a171ca9 100644 --- a/src/components/insights/__tests__/correlation-row.test.tsx +++ b/src/components/insights/__tests__/correlation-row.test.tsx @@ -5,13 +5,17 @@ import type { CorrelationResult } from "@/lib/insights/correlations"; /** * v1.4.20 phase B3 — `` layout + disclaimer. + * v1.4.22 A4 update — empty-state cards are dropped instead of rendered. * * Acceptance: - * 1. Renders the three hypothesis cards. - * 2. Mounts the correlation-disclaimer footer once at the row level. - * 3. 2-up grid on >= md, single column on < md. - * 4. Below-threshold cards delegate their empty-state to - * `` (smoke-checked via the per-card slot). + * 1. Renders only the hypothesis cards whose `status === "ok"`. + * 2. Mounts the correlation-disclaimer footer once at the row level + * when at least one card renders; the row hides itself entirely + * when zero cards render (no header, no disclaimer). + * 3. 2-up grid on >= md when 2+ cards, full-width single column when + * exactly 1 card renders. + * 4. Below-threshold cards are filtered at the row level so the + * layout collapses cleanly. */ vi.mock("next/dynamic", () => ({ @@ -67,7 +71,7 @@ const okWeightWeekday: CorrelationResult = { }; describe("", () => { - it("renders all three hypothesis cards", () => { + it("renders only the ok-status hypothesis cards (insufficient ones are dropped)", () => { const html = render( ", () => { />, ); const matches = html.match(/data-slot="correlation-card"/g) ?? []; - expect(matches.length).toBe(3); + expect(matches.length).toBe(2); expect(html).toMatch(/data-kind="bp-compliance"/); - expect(html).toMatch(/data-kind="mood-pulse"/); + expect(html).not.toMatch(/data-kind="mood-pulse"/); expect(html).toMatch(/data-kind="weight-weekday"/); }); @@ -99,7 +103,7 @@ describe("", () => { expect(html).toContain("Patterns are observational, not causal"); }); - it("applies the 2-up grid on >= md and single column on < md", () => { + it("applies the 2-up grid on >= md when 2+ ok cards render", () => { const html = render( ", () => { expect(html).toMatch(/md:grid-cols-2/); }); + it("collapses to a full-width single column when exactly 1 ok card renders", () => { + const html = render( + , + ); + expect(html).toMatch(/grid-cols-1/); + // The 2-up rule must NOT activate when only one card has data — + // a single card should span 100 % width on its own row instead of + // half-width with empty space next to it. + expect(html).not.toMatch(/md:grid-cols-2/); + }); + + it("hides the row entirely (no header, no disclaimer) when zero cards have data", () => { + const html = render( + , + ); + expect(html).toBe(""); + }); + it("renders German copy when locale=de", () => { const html = render( ", () => { />, "de", ); - expect(html).toContain("Muster"); expect(html).toContain("nicht kausal"); }); - - it("delegates empty-state to per-card empty-state when below threshold", () => { - const html = render( - , - ); - const matches = html.match(/Need more data to see this pattern/g) ?? []; - expect(matches.length).toBe(3); - }); }); diff --git a/src/components/insights/correlation-row.tsx b/src/components/insights/correlation-row.tsx index d651fe255..f7714f823 100644 --- a/src/components/insights/correlation-row.tsx +++ b/src/components/insights/correlation-row.tsx @@ -7,14 +7,18 @@ import { CorrelationCard } from "./correlation-card"; /** * v1.4.20 phase B3 — Correlation row. * - * Renders the three pre-defined hypothesis cards in a 2-up grid on - * `>=md` and a single column on `=md` and + * a single column on `` itself owns the empty-state for below- - * threshold results, so this row never has to filter — it always - * mounts all three cards regardless of `status`. + * v1.4.22 A4 — empty-state cards are dropped instead of rendered. Any + * hypothesis whose `status !== "ok"` is filtered out so the layout + * collapses cleanly: 2 ok cards → 50/50, 1 ok card → 100 %, 0 ok cards + * → the row hides itself entirely (no header, no disclaimer). Up to + * v1.4.21 the `` painted an EmptyState for below- + * threshold results, which left half-rows of greyed-out placeholders + * on a sparse account. */ interface CorrelationRowProps { @@ -28,6 +32,16 @@ interface CorrelationRowProps { export function CorrelationRow({ results }: CorrelationRowProps) { const { t } = useTranslations(); + // Drop insufficient-data tiles up-front. The grid below picks 1-col vs + // 2-col automatically so 1 ok card spans 100 % width on its own row. + const okResults = [ + results.bpCompliance, + results.moodPulse, + results.weightWeekday, + ].filter((r) => r.status === "ok"); + + if (okResults.length === 0) return null; + return (
-
- - - +
+ {okResults.map((result) => ( + + ))}

+ {/* v1.4.22 A4 — equal-height cards. The AI annotation prose + below each chart varies in length, which used to leave the + three cards on visibly different baselines. Each card is now + a flex column with `min-h-[300px]` so the chart anchors to + the top and the annotation grows downward into a shared + minimum height. The grid + min-h together preserve the visual + rhythm even on a row where only one annotation is multi-line. */}
-
+