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