From eb730bf7f3c31458ae0cc1e625f715284b9384be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 5 Jul 2026 22:00:53 +0200 Subject: [PATCH] fix(insights): one card language on the sub-page spine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A measured pass over every insights sub-page, the dashboard, and the settings sections (computed text colour, left text edge relative to the card edge, header→body distance, per card) surfaced the drift the eye kept catching: - The blood-pressure pulse-pressure/MAP explainer was a bare muted paragraph at the container edge, 16–24 px left of every neighbouring card's text. It now rides a compact Card in the regular foreground. - The 'Usual range' read strip was a hand-rolled bg-card/60 shell with its own padding; rebuilt on Card + TileHeader + CardContent at the compact density so background, radius, icon spacing and text edge match the neighbouring tiles. - The spine tiles carried three different header→body distances (6, 12 and 24 px) via off-scale overrides (gap-1.5, md:gap-3, md:py-5, gap-3). Assessment, target summary, medication tiles, the mood stand-out card and the sleep hypnogram now share the sanctioned compact tier (gap-2 py-3 md:py-4). - The hero score card and its skeleton kept their translucent identity but move from a static px-4 py-4 to the p-4 md:p-6 card norm, putting their text on the same edge as every other card. - Two dashboard shells (recent workouts, recent unlocks) froze at p-4 on every breakpoint; now p-4 md:p-6. - Settings: the modules hint moves into the header's description slot and the injection-site grid joins the section-wide mt-4 rhythm — the two gaps that deviated from the settings 16 px header→content norm. --- src/app/insights/blood-pressure/page.tsx | 31 ++++++----- src/app/insights/medications/page.tsx | 2 +- .../dashboard/recent-workouts-tile.tsx | 4 +- .../gamification/recent-achievements-card.tsx | 2 +- .../insights/derived/coach-read-strip.tsx | 52 +++++++++++-------- src/components/insights/health-score-card.tsx | 2 +- src/components/insights/hero-strip.tsx | 2 +- .../insights/insight-status-card.tsx | 43 ++++++--------- .../insights/metric-target-summary.tsx | 8 +-- .../insights/mood/mood-insights-sections.tsx | 2 +- src/components/insights/sleep-hypnogram.tsx | 2 +- .../settings/injection-sites-card.tsx | 2 +- src/components/settings/modules-section.tsx | 11 ++-- 13 files changed, 84 insertions(+), 79 deletions(-) diff --git a/src/app/insights/blood-pressure/page.tsx b/src/app/insights/blood-pressure/page.tsx index 252395a67..36518729c 100644 --- a/src/app/insights/blood-pressure/page.tsx +++ b/src/app/insights/blood-pressure/page.tsx @@ -9,6 +9,7 @@ import { useTranslations } from "@/lib/i18n/context"; import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs"; import { useChartDomainStats } from "@/hooks/use-chart-domain-stats"; import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic"; import { SlugInsightStatusCard } from "@/components/insights/slug-insight-status-card"; import { MeasurementDiversityNudge } from "@/components/insights/measurement-diversity-nudge"; @@ -189,18 +190,24 @@ export default function InsightsBlutdruckPage() { const pp = Math.round(sbp - dbp); const map = Math.round((sbp + 2 * dbp) / 3); return ( -

- {t("insights.subPage.bpDerived.pulsePressure", { - value: pp, - })}{" "} - {t("insights.subPage.bpDerived.meanArterialPressure", { - value: map, - })}{" "} - {t("insights.subPage.bpDerived.caveat")} -

+ // Card shell at the compact density so the explainer shares the + // spine's left text edge (the bare paragraph sat at the container + // edge, ~16–24 px left of every neighbouring card's text) — and + // foreground, not muted: multi-sentence explainer prose is + // content, muted stays reserved for meta. + + +

+ {t("insights.subPage.bpDerived.pulsePressure", { + value: pp, + })}{" "} + {t("insights.subPage.bpDerived.meanArterialPressure", { + value: map, + })}{" "} + {t("insights.subPage.bpDerived.caveat")} +

+
+
); })()} diff --git a/src/app/insights/medications/page.tsx b/src/app/insights/medications/page.tsx index e5b7d4af7..02eef163f 100644 --- a/src/app/insights/medications/page.tsx +++ b/src/app/insights/medications/page.tsx @@ -210,7 +210,7 @@ export default function InsightsMedikamentePage() { ? t("medications.categoryVitamin") : t("medications.categoryOther"); return ( - +
diff --git a/src/components/dashboard/recent-workouts-tile.tsx b/src/components/dashboard/recent-workouts-tile.tsx index ad111d972..a4557b5c0 100644 --- a/src/components/dashboard/recent-workouts-tile.tsx +++ b/src/components/dashboard/recent-workouts-tile.tsx @@ -103,7 +103,9 @@ export function RecentWorkoutsTile() { return (
diff --git a/src/components/gamification/recent-achievements-card.tsx b/src/components/gamification/recent-achievements-card.tsx index df45024f8..061af2df7 100644 --- a/src/components/gamification/recent-achievements-card.tsx +++ b/src/components/gamification/recent-achievements-card.tsx @@ -111,7 +111,7 @@ export function RecentAchievementsCard() { return (
diff --git a/src/components/insights/derived/coach-read-strip.tsx b/src/components/insights/derived/coach-read-strip.tsx index 6e5eb68aa..8971fd4ea 100644 --- a/src/components/insights/derived/coach-read-strip.tsx +++ b/src/components/insights/derived/coach-read-strip.tsx @@ -8,6 +8,7 @@ import { useMounted } from "@/hooks/use-mounted"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import { apiGet } from "@/lib/api/api-fetch"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { TileHeader } from "@/components/insights/tile-header"; import type { CoachReadStripData } from "@/lib/insights/derived/coach-read-shape"; @@ -109,28 +110,35 @@ export function CoachReadStrip({ // fires defensively.) if (!baselineLine && !driverLine) return null; - // Standard card anatomy — the same TileHeader-led shape as every other - // insights card (foreground icon + title), instead of the former inline - // mini-icon strip that read as chrome outside the card system. Body prose - // in the regular foreground; muted stays reserved for meta lines. + // Standard card anatomy — the real Card + TileHeader + CardContent + // primitives at the compact density (the metric-stat-strip reference), + // so background, radius, icon spacing and the left text edge match + // every other insights tile. The former hand-rolled shell + // (`bg-card/60 … px-4 py-3.5`) painted a translucent background and a + // text edge ~8 px left of the card norm on md+. Body prose in the + // regular foreground; muted stays reserved for meta lines. return ( -
- -
- {baselineLine ? ( -

- {baselineLine} -

- ) : null} - {driverLine ? ( -

- {driverLine} -

- ) : null} -
-
+ + + + + +
+ {baselineLine ? ( +

+ {baselineLine} +

+ ) : null} + {driverLine ? ( +

+ {driverLine} +

+ ) : null} +
+
+
); } diff --git a/src/components/insights/health-score-card.tsx b/src/components/insights/health-score-card.tsx index 9eab66de4..1dc4a637b 100644 --- a/src/components/insights/health-score-card.tsx +++ b/src/components/insights/health-score-card.tsx @@ -302,7 +302,7 @@ export function HealthScoreCard({ data-slot="health-score-card" data-band={band} className={cn( - "bg-card/65 rounded-xl border px-4 py-4 shadow-sm backdrop-blur-sm", + "bg-card/65 rounded-xl border p-4 shadow-sm backdrop-blur-sm md:p-6", BAND_BORDER_CLASS[band], // v1.4.27 MB7 / CF-34 — basis-based width so the score column // flexes inside the hero strip's `md:flex-row` split. Earlier diff --git a/src/components/insights/hero-strip.tsx b/src/components/insights/hero-strip.tsx index 7cde2c5d2..d97dbf8b9 100644 --- a/src/components/insights/hero-strip.tsx +++ b/src/components/insights/hero-strip.tsx @@ -368,7 +368,7 @@ export function HeroStrip({ data-slot="health-score-card-skeleton" aria-hidden="true" className={cn( - "bg-card/65 border-border/60 rounded-xl border px-4 py-4 shadow-sm backdrop-blur-sm", + "bg-card/65 border-border/60 rounded-xl border p-4 shadow-sm backdrop-blur-sm md:p-6", "w-full md:shrink-0 md:grow-0 md:basis-[22rem] xl:basis-[26rem]", "flex h-full min-h-64 flex-col gap-3.5", )} diff --git a/src/components/insights/insight-status-card.tsx b/src/components/insights/insight-status-card.tsx index 08c86e806..9067a7b5d 100644 --- a/src/components/insights/insight-status-card.tsx +++ b/src/components/insights/insight-status-card.tsx @@ -107,7 +107,7 @@ export function InsightStatusCard({ aria-busy="true" aria-live="polite" data-testid="insight-status-card-preparing" - className="gap-1.5 py-4 md:py-5" + className="gap-2 py-3 md:py-4" > @@ -140,11 +140,10 @@ export function InsightStatusCard({ aria-busy="true" aria-live="polite" data-testid="insight-status-card-loading" - // v1.13.1 — match the canonical `gap-1.5` + `pb-1` heading-to-body - // rhythm the populated / preparing / empty states use, so the - // heading and the first skeleton line sit on the same tight - // baseline across every assessment-card state. - className="gap-1.5 py-4 md:py-5" + // Compact card density — the same tier the populated / preparing / + // empty states use, so the heading and the first skeleton line sit + // on the same baseline across every assessment-card state. + className="gap-2 py-3 md:py-4" >
@@ -165,7 +164,7 @@ export function InsightStatusCard({ if (!hasProvider) { return ( - + @@ -187,7 +186,7 @@ export function InsightStatusCard({ if (!text) { return ( - + @@ -211,25 +210,15 @@ export function InsightStatusCard({ // v1.12.2 — stable hook for the spine guard test, which asserts the // assessment is the LAST content block on every bespoke metric page. data-slot="insight-assessment" - // v1.8.5 W4a — the `Card` primitive ships `gap-4 md:gap-6` as the flex - // gap between header and content, which floated the "Einschätzung" - // heading ~16-24 px above its prose. Override to `gap-2 md:gap-3` (the - // `CardHeader`'s own `pb-2` does not touch the flex gap) and trim the - // shell padding to `py-4 md:py-5` so the assessment block reads tight - // in the denser sub-page rhythm. The same override lands on every - // status variant below so the gap is consistent across states. - // - // v1.8.6 — drop the purple left accent (`border-l-dracula-purple - // border-l-2`). The coloured rule made the assessment card read as - // restless against the calmer surrounding cards; the plain card - // border carries enough separation on its own. - // - // v1.12.8 — tighten the header-to-prose gap. The card flex gap is - // `gap-1.5` (was `gap-2 md:gap-3`) and the `CardHeader` trims its - // bottom padding to `pb-1` (was `pb-2`), so the `` sits - // close to the prose and matches the header-to-body rhythm of the - // sibling tiles. The loading skeleton keeps its own spacing. - className="animate-insight-in gap-1.5 py-4 md:py-5" + // Compact card density (`gap-2 py-3 md:py-4`) — the sanctioned tight + // tier, so the assessment shares one header→body rhythm with the + // target summary, the read strip, and the stat strip on the same + // spine. Earlier bespoke values (`gap-1.5` + `md:py-5`) predate the + // two-density contract and were normalised onto it. The same class + // lands on every status variant so the gap is consistent across + // states. (The one-time purple left accent stays dropped — the plain + // card border carries enough separation.) + className="animate-insight-in gap-2 py-3 md:py-4" > {/* v1.11.5 — the top-right "cached" label was removed: it surfaced diff --git a/src/components/insights/metric-target-summary.tsx b/src/components/insights/metric-target-summary.tsx index 2f5622e2b..41d4eec69 100644 --- a/src/components/insights/metric-target-summary.tsx +++ b/src/components/insights/metric-target-summary.tsx @@ -296,14 +296,14 @@ function TargetReferencePanel({ target.consistency7d.length > 0; return ( - // Card-wrapped so the header-to-body offset matches the `` tiles on the same subpage spine rather than the old bare-div - // `space-y-3`. The body keeps its inter-row `space-y-3` inside + // Card-wrapped at the compact density so the header-to-body offset + // matches the assessment / read-strip / stat-strip tiles on the same + // subpage spine. The body keeps its inter-row `space-y-3` inside // `CardContent`; every data-slot is preserved. {/* Row 1: the canonical tile header — a single-word "Ziel" / "Target" heading (the range numbers live on the range bar below, so the diff --git a/src/components/insights/mood/mood-insights-sections.tsx b/src/components/insights/mood/mood-insights-sections.tsx index 025993f3f..5fe0d9385 100644 --- a/src/components/insights/mood/mood-insights-sections.tsx +++ b/src/components/insights/mood/mood-insights-sections.tsx @@ -287,7 +287,7 @@ export function MoodInsightsSections({ // v1.13.1 — match the canonical `gap-1.5` + `pb-1` heading-to-body // rhythm the other assessment cards use, so the heading sits tight // above its body instead of floating ~16-24 px above it. - className="animate-insight-in gap-1.5 py-4 md:py-5" + className="animate-insight-in gap-2 py-3 md:py-4" > +
diff --git a/src/components/settings/injection-sites-card.tsx b/src/components/settings/injection-sites-card.tsx index 78cfb3a24..04473f624 100644 --- a/src/components/settings/injection-sites-card.tsx +++ b/src/components/settings/injection-sites-card.tsx @@ -73,7 +73,7 @@ export function InjectionSitesCard({ title={t("settings.globalExcludedInjectionSitesLabel")} description={t("settings.globalExcludedInjectionSitesHint")} /> -
+
{INJECTION_SITE_KEYS.map((site) => (