From 2decc37f07d42cd49773f22b3ac01119aabd7693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 25 Jul 2026 14:09:22 +0200 Subject: [PATCH] refactor(insights): one shell and one rhythm for the overview sections Seven overview sections had hand-rolled the same card literal, and the copies had already drifted on the one thing the literal did not pin: the space between blocks. Three used 12 px, two 8 px, one 16 px, one under the other in the same column. `InsightSectionCard` composes `Card`, so the frame comes from the card contract, and fixes the inner rhythm at 12 px. The daily briefing moves onto it as well, and the lab-changes list keeps its edge-to-edge rows through the documented flush variant. Visible change on the overview column: two bodies open by 4 px, one tightens by 4 px, and all of them pick up the shadow their card neighbours already had. Chart bodies are untouched. --- .../__tests__/insight-section-card.test.tsx | 122 ++++++ .../insights/breathing-screening-card.tsx | 8 +- .../insights/coincident-deviation-card.tsx | 16 +- src/components/insights/daily-briefing.tsx | 375 +++++++++--------- src/components/insights/ecg-section.tsx | 8 +- .../insights/health-status-card.tsx | 8 +- .../insights/insight-section-card.tsx | 66 +++ src/components/insights/labs-changes-card.tsx | 8 +- .../insights/period-narrative-card.tsx | 21 +- .../insights/rhythm-events-card.tsx | 8 +- 10 files changed, 405 insertions(+), 235 deletions(-) create mode 100644 src/components/insights/__tests__/insight-section-card.test.tsx create mode 100644 src/components/insights/insight-section-card.tsx diff --git a/src/components/insights/__tests__/insight-section-card.test.tsx b/src/components/insights/__tests__/insight-section-card.test.tsx new file mode 100644 index 000000000..487e93b63 --- /dev/null +++ b/src/components/insights/__tests__/insight-section-card.test.tsx @@ -0,0 +1,122 @@ +import { readFileSync, readdirSync } from "node:fs"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { InsightSectionCard } from "../insight-section-card"; + +const INSIGHTS_DIR = path.join(process.cwd(), "src/components/insights"); + +/** + * The overview sections that compose the shared shell, by the `data-slot` + * each one keeps. A section that drifts back onto a hand-rolled literal — or + * a rename that silently drops one off the primitive — fails here instead of + * surviving until the next audit. + */ +const MIGRATED_SLOTS = [ + "health-status-card", + "breathing-screening-card", + "coincident-deviation-card", + "rhythm-events-card", + "period-narrative-card", + "labs-changes-card", + "ecg-card", + "daily-briefing", +]; + +/** The shell literal the primitive replaces. */ +function paintsHandRolledShell(line: string): boolean { + return ( + /className=/.test(line) && + /\bbg-card\b/.test(line) && + /\brounded-xl\b/.test(line) && + /\bborder\b/.test(line) && + /\bp-4 md:p-6\b/.test(line) + ); +} + +function insightsSources(): { file: string; src: string }[] { + return readdirSync(INSIGHTS_DIR) + .filter((f) => f.endsWith(".tsx")) + .map((f) => ({ + file: f, + src: readFileSync(path.join(INSIGHTS_DIR, f), "utf8"), + })); +} + +describe("", () => { + it("composes Card with the pinned overview rhythm", () => { + const html = renderToStaticMarkup( + +

body

+
, + ); + expect(html).toContain('data-slot="insight-section-card"'); + expect(html).toContain('data-slot="card-content"'); + // Frame from the Card contract, body inset from CardContent. + expect(html).toContain("bg-card"); + expect(html).toContain("rounded-xl"); + expect(html).toContain("px-4"); + expect(html).toContain("md:px-6"); + // One inner rhythm — not the 8/12/16 px the copies had drifted to. + expect(html).toContain("gap-3"); + expect(html).not.toContain("gap-2"); + expect(html).not.toContain("space-y-4"); + }); + + it("takes a slot override and merges caller classes onto the frame", () => { + const html = renderToStaticMarkup( + +

body

+
, + ); + expect(html).toContain('data-slot="rhythm-events-card"'); + expect(html).toContain("min-h-32"); + expect(html).not.toContain('data-slot="insight-section-card"'); + }); + + it("flush drops the body inset for an edge-to-edge divided list", () => { + const html = renderToStaticMarkup( + +
row
+
, + ); + expect(html).toContain('data-slot="labs-changes-card"'); + expect(html).toContain("divide-y"); + // No CardContent wrapper — the rows own their own inset. + expect(html).not.toContain('data-slot="card-content"'); + expect(html).toContain("py-0"); + }); + + it("every migrated overview section renders through the primitive", () => { + // Collapse whitespace so a prettier reflow onto multiple lines still + // matches the ` src.replace(/\s+/g, " ")) + .join("\n"); + + for (const slot of MIGRATED_SLOTS) { + expect( + sources, + `${slot} should be rendered through `, + ).toContain(` { + const owners = insightsSources().filter(({ src }) => + MIGRATED_SLOTS.some((slot) => src.includes(`slot="${slot}"`)), + ); + // Sanity: the scan actually found the files it means to guard. + expect(owners.length).toBeGreaterThanOrEqual(7); + + const offenders = owners.flatMap(({ file, src }) => + src + .split("\n") + .filter(paintsHandRolledShell) + .map((line) => `${file}: ${line.trim()}`), + ); + expect(offenders).toEqual([]); + }); +}); diff --git a/src/components/insights/breathing-screening-card.tsx b/src/components/insights/breathing-screening-card.tsx index 0da2d22fc..4ef705c67 100644 --- a/src/components/insights/breathing-screening-card.tsx +++ b/src/components/insights/breathing-screening-card.tsx @@ -13,6 +13,7 @@ import type { BreathingClassification, BreathingTrend, } from "@/lib/insights/breathing-screening"; +import { InsightSectionCard } from "./insight-section-card"; /** * v1.25 — sleep-breathing screening card. @@ -83,10 +84,7 @@ export function BreathingScreeningCard({ title={t("insights.breathingScreening.sectionTitle")} subtitle={t("insights.breathingScreening.subtitle")} /> -
+ {classificationLabel ? (

{t("insights.breathingScreening.disclaimer")}

-
+
); } diff --git a/src/components/insights/coincident-deviation-card.tsx b/src/components/insights/coincident-deviation-card.tsx index d7ddcbd24..74fc12075 100644 --- a/src/components/insights/coincident-deviation-card.tsx +++ b/src/components/insights/coincident-deviation-card.tsx @@ -14,6 +14,7 @@ import { useDerivedMetric } from "@/components/insights/derived/use-derived-metr // (the v1.9.0 lesson, mirrored at vitals-dashboard.tsx). import type { CoincidentDeviationValue } from "@/lib/insights/derived/coincident-deviation"; import type { DerivedProvenance } from "@/lib/insights/derived/types"; +import { InsightSectionCard } from "./insight-section-card"; /** * v1.10.3 — "Today's signal" headline card. @@ -113,11 +114,10 @@ function CardShell({ ) : undefined } /> -
{children} -
+
); } @@ -155,14 +155,14 @@ function CardSkeleton({ className }: { className?: string }) { icon={Activity} title={t("insights.derived.coincident.cardTitle")} /> -