mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(insights): one card language on the sub-page spine
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.
This commit is contained in:
1 parent
a4b204ffe4
commit
eb730bf7f3
13 files changed
+84
-79
No files matched your search
@@ -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 (
|
||||
<p
|
||||
data-slot="bp-derived-caption"
|
||||
className="text-muted-foreground text-sm leading-relaxed"
|
||||
>
|
||||
{t("insights.subPage.bpDerived.pulsePressure", {
|
||||
value: pp,
|
||||
})}{" "}
|
||||
{t("insights.subPage.bpDerived.meanArterialPressure", {
|
||||
value: map,
|
||||
})}{" "}
|
||||
{t("insights.subPage.bpDerived.caveat")}
|
||||
</p>
|
||||
// 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.
|
||||
<Card data-slot="bp-derived-caption" className="gap-2 py-3 md:py-4">
|
||||
<CardContent>
|
||||
<p className="text-sm leading-relaxed">
|
||||
{t("insights.subPage.bpDerived.pulsePressure", {
|
||||
value: pp,
|
||||
})}{" "}
|
||||
{t("insights.subPage.bpDerived.meanArterialPressure", {
|
||||
value: map,
|
||||
})}{" "}
|
||||
{t("insights.subPage.bpDerived.caveat")}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})()}
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ export default function InsightsMedikamentePage() {
|
||||
? t("medications.categoryVitamin")
|
||||
: t("medications.categoryOther");
|
||||
return (
|
||||
<Card key={med.id} className="gap-2 py-4 md:gap-3 md:py-5">
|
||||
<Card key={med.id} className="gap-2 py-3 md:py-4">
|
||||
<CardHeader>
|
||||
<TileHeader icon={Pill} title={med.name} />
|
||||
<div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
|
||||
|
||||
@@ -103,7 +103,9 @@ export function RecentWorkoutsTile() {
|
||||
return (
|
||||
<div
|
||||
data-slot="recent-workouts-tile"
|
||||
className={cn("bg-card border-border space-y-3 rounded-xl border p-4")}
|
||||
className={cn(
|
||||
"bg-card border-border space-y-3 rounded-xl border p-4 md:p-6",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -111,7 +111,7 @@ export function RecentAchievementsCard() {
|
||||
return (
|
||||
<div
|
||||
data-slot="recent-achievements-card"
|
||||
className="bg-card border-border space-y-3 rounded-xl border p-4"
|
||||
className="bg-card border-border space-y-3 rounded-xl border p-4 md:p-6"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
data-slot="coach-read-strip"
|
||||
className="bg-card/60 border-border/60 space-y-2 rounded-xl border px-4 py-3.5"
|
||||
>
|
||||
<TileHeader icon={Sparkles} title={t("insights.coach.readStrip.label")} />
|
||||
<div className="text-foreground min-w-0 space-y-1 text-sm leading-relaxed">
|
||||
{baselineLine ? (
|
||||
<p data-slot="coach-read-baseline" className="text-pretty">
|
||||
{baselineLine}
|
||||
</p>
|
||||
) : null}
|
||||
{driverLine ? (
|
||||
<p data-slot="coach-read-driver" className="text-pretty">
|
||||
{driverLine}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Card data-slot="coach-read-strip" className="gap-2 py-3 md:py-4">
|
||||
<CardHeader>
|
||||
<TileHeader
|
||||
icon={Sparkles}
|
||||
title={t("insights.coach.readStrip.label")}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="min-w-0 space-y-1 text-sm leading-relaxed">
|
||||
{baselineLine ? (
|
||||
<p data-slot="coach-read-baseline" className="text-pretty">
|
||||
{baselineLine}
|
||||
</p>
|
||||
) : null}
|
||||
{driverLine ? (
|
||||
<p data-slot="coach-read-driver" className="text-pretty">
|
||||
{driverLine}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<CardHeader>
|
||||
<TileHeader icon={nodeIcon(icon)} title={title} />
|
||||
@@ -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"
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -165,7 +164,7 @@ export function InsightStatusCard({
|
||||
|
||||
if (!hasProvider) {
|
||||
return (
|
||||
<Card className="gap-1.5 py-4 opacity-80 md:py-5">
|
||||
<Card className="gap-2 py-3 opacity-80 md:py-4">
|
||||
<CardHeader>
|
||||
<TileHeader icon={nodeIcon(icon)} title={title} />
|
||||
</CardHeader>
|
||||
@@ -187,7 +186,7 @@ export function InsightStatusCard({
|
||||
|
||||
if (!text) {
|
||||
return (
|
||||
<Card className="gap-1.5 py-4 md:py-5">
|
||||
<Card className="gap-2 py-3 md:py-4">
|
||||
<CardHeader>
|
||||
<TileHeader icon={nodeIcon(icon)} title={title} />
|
||||
</CardHeader>
|
||||
@@ -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 `<TileHeader>` 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"
|
||||
>
|
||||
<CardHeader>
|
||||
{/* v1.11.5 — the top-right "cached" label was removed: it surfaced
|
||||
|
||||
@@ -296,14 +296,14 @@ function TargetReferencePanel({
|
||||
target.consistency7d.length > 0;
|
||||
|
||||
return (
|
||||
// Card-wrapped so the header-to-body offset matches the `<CardHeader
|
||||
// pb-2>` 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.
|
||||
<Card
|
||||
data-slot="metric-target-summary"
|
||||
data-target-type={target.type}
|
||||
className="gap-2 py-4 md:gap-3 md:py-5"
|
||||
className="gap-2 py-3 md:py-4"
|
||||
>
|
||||
{/* Row 1: the canonical tile header — a single-word "Ziel" / "Target"
|
||||
heading (the range numbers live on the range bar below, so the
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<CardHeader>
|
||||
<TileHeader
|
||||
|
||||
@@ -247,7 +247,7 @@ export function SleepHypnogram({ session }: SleepHypnogramProps) {
|
||||
// v1.12.0 — card tightened: drop the default Card `gap-4 md:gap-6` to
|
||||
// `gap-3` so the header sits closer to the timeline, reclaiming the empty
|
||||
// band the maintainer flagged. The chart keeps its 200 px footprint.
|
||||
<Card data-slot="sleep-hypnogram" className="gap-3 md:gap-3">
|
||||
<Card data-slot="sleep-hypnogram" className="gap-2">
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<TileHeader title={t("insights.sleep.hypnogram.title")} />
|
||||
|
||||
@@ -73,7 +73,7 @@ export function InjectionSitesCard({
|
||||
title={t("settings.globalExcludedInjectionSitesLabel")}
|
||||
description={t("settings.globalExcludedInjectionSitesHint")}
|
||||
/>
|
||||
<div className="mt-3 grid grid-cols-1 gap-2 pl-7 sm:grid-cols-2">
|
||||
<div className="mt-4 grid grid-cols-1 gap-2 pl-7 sm:grid-cols-2">
|
||||
{INJECTION_SITE_KEYS.map((site) => (
|
||||
<label key={site} className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
|
||||
@@ -121,16 +121,15 @@ export function ModulesSection() {
|
||||
// was removed: a domain that can't be turned off doesn't need to be listed.
|
||||
return (
|
||||
<SettingsCard>
|
||||
{/* The "what this section does" hint rides the header's standard
|
||||
description slot; the body follows at the settings-wide mt-4
|
||||
header→content rhythm instead of the former mb-2/mb-3 one-offs. */}
|
||||
<SettingsCardHeader
|
||||
icon={Blocks}
|
||||
title={t("settings.sections.modules.toggleable.title")}
|
||||
className="mb-2"
|
||||
description={t("settings.sections.modules.toggleable.description")}
|
||||
/>
|
||||
{/* v1.18.6 — restore the one-line "what this section does" hint. */}
|
||||
<p className="text-muted-foreground mb-3 pl-7 text-sm leading-relaxed">
|
||||
{t("settings.sections.modules.toggleable.description")}
|
||||
</p>
|
||||
<div className="divide-border divide-y pl-7">
|
||||
<div className="divide-border mt-4 divide-y pl-7">
|
||||
{(Object.keys(MODULE_REGISTRY) as ModuleKey[])
|
||||
// Modules switched off in code (pending a rebuild) carry no live
|
||||
// toggle — drop the row entirely so a user can't turn one on.
|
||||
|
||||
Reference in new issue
Block a user