mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(insights): improve narrative contrast and spacing
This commit is contained in:
1 parent
ab450bbfd1
commit
90b1a4eaee
11 files changed
+92
-44
No files matched your search
@@ -185,7 +185,7 @@ describe("<TodayHero>", () => {
|
||||
sourceMetric: "pulse",
|
||||
tone: "info",
|
||||
headline: "Pulse is settling lately",
|
||||
nudge: null,
|
||||
nudge: "",
|
||||
delta: null,
|
||||
},
|
||||
})}
|
||||
|
||||
@@ -301,7 +301,7 @@ describe("<HeroStrip>", () => {
|
||||
healthScore={SCORE_REPORT}
|
||||
/>,
|
||||
);
|
||||
expect(html).toMatch(/data-slot="health-score-card"/);
|
||||
expect(html).toMatch(/data-slot="health-score-card-compact"/);
|
||||
expect(html).toMatch(/data-band="green"/);
|
||||
expect(html).toContain(">86<");
|
||||
});
|
||||
@@ -406,7 +406,7 @@ describe("<HeroStrip>", () => {
|
||||
healthScore={SCORE_REPORT}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain('data-slot="health-score-card"');
|
||||
expect(html).toContain('data-slot="health-score-card-compact"');
|
||||
expect(html).not.toContain('data-slot="health-score-card-skeleton"');
|
||||
});
|
||||
});
|
||||
@@ -183,11 +183,15 @@ describe("<InsightStatusCard>", () => {
|
||||
expect(empty).toContain("No assessment yet.");
|
||||
expect(populated).toContain('data-slot="insight-assessment"');
|
||||
|
||||
for (const html of [loading, preparing, noProvider, empty, populated]) {
|
||||
for (const html of [loading, preparing, noProvider, empty]) {
|
||||
expect(html).toContain("gap-2");
|
||||
expect(html).toContain("py-3");
|
||||
expect(html).toContain("md:py-4");
|
||||
}
|
||||
expect(populated).toContain("gap-1.5");
|
||||
expect(populated).toContain("space-y-1.5");
|
||||
expect(populated).toContain("py-3");
|
||||
expect(populated).toContain("md:py-4");
|
||||
expect(populated).toContain("text-foreground");
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
} from "@/lib/analytics/score/types";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ScoreRing } from "@/components/insights/derived/score-ring";
|
||||
|
||||
const PILLAR_LABEL_KEY: Record<ScorePillarId, string> = {
|
||||
BLOOD_PRESSURE: "insights.healthScore.pillar.bloodPressure",
|
||||
@@ -83,6 +84,8 @@ export interface HealthScoreCardProps {
|
||||
negative: string[];
|
||||
} | null;
|
||||
returnToBand?: { metricLabel: string; daysInside: number } | null;
|
||||
/** Keep only the score face when the card is used above the fold. */
|
||||
compact?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -90,6 +93,7 @@ export function HealthScoreCard({
|
||||
report,
|
||||
tension,
|
||||
returnToBand,
|
||||
compact = false,
|
||||
className,
|
||||
}: HealthScoreCardProps) {
|
||||
const { t } = useTranslations();
|
||||
@@ -223,6 +227,36 @@ export function HealthScoreCard({
|
||||
? 1 / eligibleCount
|
||||
: 0,
|
||||
}));
|
||||
if (compact) {
|
||||
return (
|
||||
<div
|
||||
data-slot="health-score-card-compact"
|
||||
className={cn(
|
||||
"border-border/70 bg-card/80 flex min-w-0 flex-col items-center justify-center gap-2 rounded-xl border p-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ScoreRing
|
||||
score={composite.status === "ok" ? composite.value.score : null}
|
||||
band={composite.status === "ok" ? composite.value.band : undefined}
|
||||
size="md"
|
||||
flat
|
||||
label={t("insights.healthScore.label")}
|
||||
/>
|
||||
{composite.status === "ok" ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t(`insights.healthScore.band.${composite.value.band}`)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("insights.healthScore.insufficient", {
|
||||
count: composite.coverage.presentInputs,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
data-slot="health-score-card"
|
||||
|
||||
@@ -383,6 +383,7 @@ export function HeroStrip({
|
||||
report={healthScore}
|
||||
tension={tensionProp}
|
||||
returnToBand={returnToBandProp}
|
||||
compact
|
||||
className="w-full md:shrink-0 md:grow-0 md:basis-[22rem] xl:basis-[26rem]"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -264,7 +264,7 @@ 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"
|
||||
// Compact card density (`gap-2 py-3 md:py-4`) — the sanctioned tight
|
||||
// Compact card density (`gap-1.5 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
|
||||
@@ -272,7 +272,7 @@ export function InsightStatusCard({
|
||||
// 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"
|
||||
className="animate-insight-in gap-1.5 py-3 md:py-4"
|
||||
>
|
||||
<CardHeader>
|
||||
{/* v1.11.5 — the top-right "cached" label was removed: it surfaced
|
||||
@@ -298,7 +298,7 @@ export function InsightStatusCard({
|
||||
}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<CardContent className="space-y-1.5">
|
||||
{/* v1.4.27 — defence-in-depth strip. Cached status text from
|
||||
pre-v1.4.27 rows can still carry literal `metric:<TYPE>`
|
||||
tokens the model embedded on the assumption a chart would
|
||||
|
||||
@@ -197,7 +197,7 @@ function SeriesBlock({
|
||||
data-slot="metric-stat-series"
|
||||
data-series={dataKey}
|
||||
data-windowed={windowed ? "true" : undefined}
|
||||
className="space-y-2"
|
||||
className="space-y-1.5"
|
||||
>
|
||||
{seriesLabel ? (
|
||||
<TileHeader
|
||||
@@ -207,7 +207,7 @@ function SeriesBlock({
|
||||
/>
|
||||
) : null}
|
||||
{description ? (
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
<p className="text-foreground text-sm leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -260,7 +260,7 @@ export function MetricStatStrip({
|
||||
data-multi-series="true"
|
||||
role="group"
|
||||
aria-label={groupLabel ?? t("insights.subPage.stats.label")}
|
||||
className="gap-2 py-3 md:py-4"
|
||||
className="gap-1.5 py-3 md:py-4"
|
||||
>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-4 md:grid-cols-2">
|
||||
@@ -293,7 +293,7 @@ export function MetricStatStrip({
|
||||
? `${t("insights.subPage.stats.label")} — ${seriesLabel}`
|
||||
: t("insights.subPage.stats.label")
|
||||
}
|
||||
className="gap-2 py-3 md:py-4"
|
||||
className="gap-1.5 py-3 md:py-4"
|
||||
>
|
||||
<CardContent>
|
||||
<SeriesBlock
|
||||
|
||||
@@ -326,7 +326,7 @@ function TargetReferencePanel({
|
||||
<Card
|
||||
data-slot="metric-target-summary"
|
||||
data-target-type={target.type}
|
||||
className="gap-2 py-3 md:py-4"
|
||||
className="gap-1.5 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
|
||||
|
||||
@@ -38,7 +38,10 @@ import { InlineLearnLink } from "@/components/ui/learn-more-link";
|
||||
/** Default paragraph styling shared by every prose surface + the Coach
|
||||
* streaming tail, so the streamed and settled renders line up exactly. */
|
||||
export const PROSE_PARAGRAPH_CLASS =
|
||||
"leading-relaxed break-words [&:not(:first-child)]:mt-3";
|
||||
// Keep consecutive narrative blocks visually connected. The previous
|
||||
// 12px paragraph step made short assessments look like separate cards on
|
||||
// mobile, especially directly below a status pill or tile heading.
|
||||
"leading-relaxed break-words [&:not(:first-child)]:mt-2";
|
||||
|
||||
/**
|
||||
* Split a trusted-as-text string into real paragraphs on blank lines.
|
||||
|
||||
@@ -389,7 +389,7 @@ export function SubPageShell({
|
||||
</p>
|
||||
) : null}
|
||||
{description && !explainerMetric ? (
|
||||
<p className="text-muted-foreground text-sm">{description}</p>
|
||||
<p className="text-foreground text-sm">{description}</p>
|
||||
) : null}
|
||||
</header>
|
||||
{/* v1.12.6 — the Min / Max / Median / Mittelwert stat strip LEADS
|
||||
|
||||
Reference in new issue
Block a user