From 86f416fb2a141a3478b87c2c884704c85812788f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 7 Jun 2026 14:15:10 +0200 Subject: [PATCH 01/12] Unify Insights overview section headings onto one icon system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every overview section now leads with a single SectionHeading above its card: a Lucide icon in the foreground colour and an h2 title at one size and weight. Previously the overview mixed four heading dialects — a purple Sparkles glyph on the briefing, no icon on Trends, a white-icon tile header on the score and vitals strips, and in-card titles buried inside the period-in-review, cycle-summary and signals cards. - Add SectionHeading (icon + title + optional trailing action). - Briefing: purple icon to white; heading moves through SectionHeading. - Trends: add a TrendingUp icon (was icon-less); subtitle becomes the trailing action. - Health scores + vitals: migrate the section headers off TileHeader. - Period in review, signals of the day, device notifications: lift the in-card title out to a heading above the card. - Signals card min-height drops to match its slimmer body now that the title row left the card. --- .../coincident-deviation-card.test.tsx | 32 ++- .../insights/coincident-deviation-card.tsx | 209 ++++++++++-------- src/components/insights/daily-briefing.tsx | 29 +-- .../insights/derived/vitals-dashboard.tsx | 61 +++-- .../insights/derived/wellness-scores.tsx | 21 +- .../insights/period-narrative-card.tsx | 159 +++++++------ .../insights/rhythm-events-card.tsx | 121 +++++----- src/components/insights/section-heading.tsx | 70 ++++++ src/components/insights/trends-row.tsx | 27 ++- 9 files changed, 445 insertions(+), 284 deletions(-) create mode 100644 src/components/insights/section-heading.tsx diff --git a/src/components/insights/__tests__/coincident-deviation-card.test.tsx b/src/components/insights/__tests__/coincident-deviation-card.test.tsx index f964d32ad..9b64d26e0 100644 --- a/src/components/insights/__tests__/coincident-deviation-card.test.tsx +++ b/src/components/insights/__tests__/coincident-deviation-card.test.tsx @@ -48,7 +48,12 @@ function ok( metric: "COINCIDENT_DEVIATION", status: "ok", value, - coverage: { requiredInputs: 5, presentInputs: 5, historyDays: 30, missing: [] }, + coverage: { + requiredInputs: 5, + presentInputs: 5, + historyDays: 30, + missing: [], + }, confidence: { score: 90, band }, provenance: { inputs: ["RESTING_HEART_RATE", "RESPIRATORY_RATE"], @@ -65,7 +70,12 @@ function insufficient(): Resp { metric: "COINCIDENT_DEVIATION", status: "insufficient", value: null, - coverage: { requiredInputs: 2, presentInputs: 1, historyDays: 0, missing: [] }, + coverage: { + requiredInputs: 2, + presentInputs: 1, + historyDays: 0, + missing: [], + }, confidence: null, provenance: { inputs: ["RESTING_HEART_RATE"], @@ -90,7 +100,9 @@ describe("", () => { expect(html).toContain('data-slot="coincident-deviation-card-skeleton"'); expect(html).not.toContain('data-slot="coincident-deviation-card"'); // The skeleton reserves the resolved card's footprint (no downward shift). - expect(html).toContain("min-h-48"); + // The footprint shrank to `min-h-32` in v1.15.10: the in-card title row + // moved out to the section heading above the card. + expect(html).toContain("min-h-32"); }); it("the skeleton and a resolved card share the same min-height footprint", () => { @@ -111,8 +123,8 @@ describe("", () => { }), ); const fired = render(); - expect(skeleton).toContain("min-h-48"); - expect(fired).toContain("min-h-48"); + expect(skeleton).toContain("min-h-32"); + expect(fired).toContain("min-h-32"); }); it("renders nothing in the insufficient (building baselines) state", () => { @@ -127,7 +139,10 @@ describe("", () => { ok({ fired: false, day: "2026-06-03", - vitals: [deviation("RESTING_HEART_RATE", false), deviation("WEIGHT", false)], + vitals: [ + deviation("RESTING_HEART_RATE", false), + deviation("WEIGHT", false), + ], contributing: [], }), ); @@ -141,7 +156,10 @@ describe("", () => { ok({ fired: false, day: "2026-06-03", - vitals: [deviation("RESTING_HEART_RATE", true), deviation("WEIGHT", false)], + vitals: [ + deviation("RESTING_HEART_RATE", true), + deviation("WEIGHT", false), + ], contributing: [deviation("RESTING_HEART_RATE", true)], }), ); diff --git a/src/components/insights/coincident-deviation-card.tsx b/src/components/insights/coincident-deviation-card.tsx index e7b4ff005..dea43da1d 100644 --- a/src/components/insights/coincident-deviation-card.tsx +++ b/src/components/insights/coincident-deviation-card.tsx @@ -4,6 +4,7 @@ import { AlertTriangle, Activity } from "lucide-react"; import { useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; +import { SectionHeading } from "@/components/insights/section-heading"; import { MEASUREMENT_TYPE_LABEL_KEYS } from "@/components/measurements/measurement-list-meta"; import { CoverageMeter } from "@/components/insights/derived/coverage-meter"; import { ProvenanceExplainer } from "@/components/insights/derived/provenance-explainer"; @@ -78,48 +79,61 @@ function CoincidentProvenance({ } /** - * The card shell — uppercase label + the provenance affordance, with the - * 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. + * The card shell — the "Signals of the day" heading lives ABOVE the card now + * (via `SectionHeading`, mirroring every other overview section), with the + * provenance ⓘ as the heading's trailing action. The card body carries the + * state-specific content. 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, provenance, + className, children, }: { state: SignalState; provenance?: DerivedProvenance; + className?: string; children: React.ReactNode; }) { const { t } = useTranslations(); return ( -
-
- - {t("insights.derived.coincident.cardTitle")} - - {provenance ? : null} + + ) : undefined + } + /> +
+ {children}
- {children} -
+ ); } @@ -129,20 +143,27 @@ function CardShell({ * mirroring the provenance trigger the resolved header carries — so the card * does not shift when the real content drops in. */ -function CardSkeleton() { +function CardSkeleton({ className }: { className?: string }) { + const { t } = useTranslations(); return ( -