diff --git a/src/app/insights/page-client.tsx b/src/app/insights/page-client.tsx index 439522754..772d30b99 100644 --- a/src/app/insights/page-client.tsx +++ b/src/app/insights/page-client.tsx @@ -26,6 +26,7 @@ import { QueryErrorCard } from "@/components/ui/query-error-card"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { HeroStrip } from "@/components/insights/hero-strip"; +import { HealthScoreCard } from "@/components/insights/health-score-card"; import { useInsightsAdvisorQuery } from "@/components/insights/use-insights-advisor"; import { useAnalyticsQuery } from "@/lib/queries/use-analytics-query"; import { useDashboardSnapshot } from "@/lib/queries/use-dashboard-snapshot"; @@ -474,36 +475,41 @@ export default function InsightsPageClient() { /> ), "daily-briefing": flags.briefing ? ( - + <> + + {analytics?.healthScore ? ( + + ) : null} + ) : null, vitals: , trends: ( diff --git a/src/components/daily/__tests__/today-hero.test.tsx b/src/components/daily/__tests__/today-hero.test.tsx index 4ca384a0b..00ca9ccc4 100644 --- a/src/components/daily/__tests__/today-hero.test.tsx +++ b/src/components/daily/__tests__/today-hero.test.tsx @@ -185,7 +185,7 @@ describe("", () => { sourceMetric: "pulse", tone: "info", headline: "Pulse is settling lately", - nudge: null, + nudge: "", delta: null, }, })} diff --git a/src/components/insights/__tests__/hero-strip.test.tsx b/src/components/insights/__tests__/hero-strip.test.tsx index 5f2a00bd1..c06d080d0 100644 --- a/src/components/insights/__tests__/hero-strip.test.tsx +++ b/src/components/insights/__tests__/hero-strip.test.tsx @@ -301,7 +301,7 @@ describe("", () => { 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("", () => { 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"'); }); }); diff --git a/src/components/insights/__tests__/insight-status-card.test.tsx b/src/components/insights/__tests__/insight-status-card.test.tsx index d40928eb2..85ac75a17 100644 --- a/src/components/insights/__tests__/insight-status-card.test.tsx +++ b/src/components/insights/__tests__/insight-status-card.test.tsx @@ -183,11 +183,15 @@ describe("", () => { 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"); }); }); diff --git a/src/components/insights/health-score-card.tsx b/src/components/insights/health-score-card.tsx index a4e7b7ee1..ec642dc1e 100644 --- a/src/components/insights/health-score-card.tsx +++ b/src/components/insights/health-score-card.tsx @@ -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 = { 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 ( +
+ + {composite.status === "ok" ? ( +

+ {t(`insights.healthScore.band.${composite.value.band}`)} +

+ ) : ( +

+ {t("insights.healthScore.insufficient", { + count: composite.coverage.presentInputs, + })} +

+ )} +
+ ); + } return (
)} diff --git a/src/components/insights/insight-status-card.tsx b/src/components/insights/insight-status-card.tsx index 6718a4178..82c9b2f11 100644 --- a/src/components/insights/insight-status-card.tsx +++ b/src/components/insights/insight-status-card.tsx @@ -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" > {/* v1.11.5 — the top-right "cached" label was removed: it surfaced @@ -298,7 +298,7 @@ export function InsightStatusCard({ } /> - + {/* v1.4.27 — defence-in-depth strip. Cached status text from pre-v1.4.27 rows can still carry literal `metric:` tokens the model embedded on the assumption a chart would diff --git a/src/components/insights/metric-stat-strip.tsx b/src/components/insights/metric-stat-strip.tsx index ab25b0c48..7b7d981a9 100644 --- a/src/components/insights/metric-stat-strip.tsx +++ b/src/components/insights/metric-stat-strip.tsx @@ -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 ? ( ) : null} {description ? ( -

+

{description}

) : 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" >
@@ -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" > {/* 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/prose-blocks.tsx b/src/components/insights/prose-blocks.tsx index e362462e1..3010bfb15 100644 --- a/src/components/insights/prose-blocks.tsx +++ b/src/components/insights/prose-blocks.tsx @@ -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. diff --git a/src/components/insights/sub-page-shell.tsx b/src/components/insights/sub-page-shell.tsx index e28476f46..475a5876a 100644 --- a/src/components/insights/sub-page-shell.tsx +++ b/src/components/insights/sub-page-shell.tsx @@ -389,7 +389,7 @@ export function SubPageShell({

) : null} {description && !explainerMetric ? ( -

{description}

+

{description}

) : null} {/* v1.12.6 — the Min / Max / Median / Mittelwert stat strip LEADS