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. */}
-
+