feat(dashboard): make the spotlight heading and the verdict sentence the links

The briefing spotlight carried a separate underlined 'open Insights'
text link that thickened the card; every signal row below already
deep-links. Drop it and make the Tagesbriefing heading itself the link
— quiet, no underline, hover lift only — targeting the briefing card
directly via a new #daily-briefing anchor on the insights section
(scroll-mt-28, the app-wide offset token).

After the broad Insights button left the hero, the scoreDrop and
briefing verdict sentences were dead ends. Link-CTA verdicts without a
button label now make the sentence itself clickable (same quiet
treatment): scoreDrop routes to /insights, the briefing sentence
deep-links to the picked finding's metric sub-page via the METRIC_HREF
map (fallback /insights). Verdicts with their own action keep their
button.
This commit is contained in:
Marc-André Bombeck committed 2026-07-05 18:09:10 +02:00
1 parent 8c22d66dd9
commit e32e27d27f
12 files changed
+74 -36

No files matched your search

+1 -2
View File
@@ -340,8 +340,7 @@
"logMeasurement": "Messung erfassen"
},
"briefing": {
"title": "Tagesbriefing",
"viewAll": "Insights öffnen"
"title": "Tagesbriefing"
}
},
"greetingSalutation": "Hallo, {name}",
+1 -2
View File
@@ -340,8 +340,7 @@
"logMeasurement": "Log measurement"
},
"briefing": {
"title": "Today’s briefing",
"viewAll": "Open Insights"
"title": "Today’s briefing"
}
},
"greetingSalutation": "Hi, {name}",
+1 -2
View File
@@ -340,8 +340,7 @@
"logMeasurement": "Registrar medición"
},
"briefing": {
"title": "Resumen de hoy",
"viewAll": "Abrir Insights"
"title": "Resumen de hoy"
}
},
"greetingSalutation": "Hola, {name}",
+1 -2
View File
@@ -340,8 +340,7 @@
"logMeasurement": "Saisir une mesure"
},
"briefing": {
"title": "Briefing du jour",
"viewAll": "Ouvrir Insights"
"title": "Briefing du jour"
}
},
"greetingSalutation": "Bonjour, {name}",
+1 -2
View File
@@ -340,8 +340,7 @@
"logMeasurement": "Registra misurazione"
},
"briefing": {
"title": "Briefing di oggi",
"viewAll": "Apri Insights"
"title": "Briefing di oggi"
}
},
"greetingSalutation": "Ciao, {name}",
+1 -2
View File
@@ -340,8 +340,7 @@
"logMeasurement": "Zapisz pomiar"
},
"briefing": {
"title": "Briefing dnia",
"viewAll": "Otwórz Insights"
"title": "Briefing dnia"
}
},
"greetingSalutation": "Cześć, {name}",
@@ -117,7 +117,10 @@ describe("<BriefingSpotlight>", () => {
});
expect(html).toContain('data-slot="dashboard-briefing-spotlight"');
expect(html).toContain("Short on sleep this week");
expect(html).toContain('href="/insights"');
// The heading itself links to the briefing card anchor; the former
// separate "view all" text link is gone.
expect(html).toContain('href="/insights#daily-briefing"');
expect(html).not.toContain('data-slot="dashboard-briefing-spotlight-link"');
expect(html).toContain("-1.2 h");
// Signals win over key findings when present.
expect(html).not.toContain("should not appear when signals exist");
@@ -152,18 +152,17 @@ export function BriefingSpotlight({
return (
<div data-slot="dashboard-briefing-spotlight" className="space-y-2">
<div className="flex items-center justify-between gap-3">
<p className="text-muted-foreground text-[11px] font-semibold tracking-wide uppercase">
{t("dashboard.hero.briefing.title")}
</p>
<Link
href="/insights"
data-slot="dashboard-briefing-spotlight-link"
className="text-muted-foreground hover:text-foreground text-[11px] underline underline-offset-2"
>
{t("dashboard.hero.briefing.viewAll")}
</Link>
</div>
{/* The heading itself routes to the briefing card on /insights —
quiet (no underline, hover lift only). The former separate
"open Insights" text link is gone: it thickened the card while
every signal row below already deep-links. */}
<Link
href="/insights#daily-briefing"
data-slot="dashboard-briefing-spotlight-title"
className="text-muted-foreground hover:text-foreground inline-block text-[11px] font-semibold tracking-wide uppercase transition-colors"
>
{t("dashboard.hero.briefing.title")}
</Link>
<div className="space-y-2">
{rows.map((row, index) => {
const Icon = METRIC_ICON[row.sourceMetric];
@@ -53,6 +53,7 @@ import { ScoreRing } from "@/components/insights/derived/score-ring";
import type { RingHue } from "@/components/insights/derived/ring-hues";
import { RestModeBanner } from "@/components/insights/rest-mode-banner";
import { BriefingSpotlight } from "@/components/dashboard/hero/briefing-spotlight";
import { METRIC_HREF } from "@/components/insights/daily-briefing";
import {
resolveDashboardVerdict,
type DashboardVerdictVariant,
@@ -199,6 +200,23 @@ export function DashboardHero({
const cta = verdict.cta;
const ctaLabelKey = CTA_LABEL_KEY[verdict.variant];
// Link-CTA verdicts WITHOUT a button label (scoreDrop, briefing) make
// the sentence itself the link — after the broad "open Insights"
// button was dropped the sentence was a dead end. The briefing
// sentence deep-links to the picked finding's metric sub-page (the
// METRIC_HREF map the briefing rows use); metrics without a routed
// sub-page fall back to the resolver's `/insights` href. Verdicts
// with their own action (take dose, view BP, …) keep the button.
const sentenceHref =
cta !== null && cta.kind === "link" && !ctaLabelKey
? verdict.variant === "briefing" &&
typeof verdict.values.sourceMetric === "string"
? (METRIC_HREF[
verdict.values.sourceMetric as keyof typeof METRIC_HREF
] ?? cta.href)
: cta.href
: null;
// ── Provisional score copy ──────────────────────────────────────────
// The score's four pillars are weight, BP, mood, and medication
// compliance. When ANY of them already carries data, a null score is
@@ -250,13 +268,24 @@ export function DashboardHero({
{welcomeText}
</p>
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<p
data-slot="dashboard-hero-verdict"
data-verdict-variant={verdict.variant}
className="text-foreground/90 text-base font-medium"
>
{sentence}
</p>
{sentenceHref ? (
<Link
href={sentenceHref}
data-slot="dashboard-hero-verdict"
data-verdict-variant={verdict.variant}
className="text-foreground/90 hover:text-foreground text-base font-medium transition-colors"
>
{sentence}
</Link>
) : (
<p
data-slot="dashboard-hero-verdict"
data-verdict-variant={verdict.variant}
className="text-foreground/90 text-base font-medium"
>
{sentence}
</p>
)}
{cta !== null && ctaLabelKey ? (
cta.kind === "link" ? (
<Button
+5 -1
View File
@@ -379,9 +379,13 @@ export function DailyBriefing({
// straight on its content and the surface reclaims the vertical
// space the in-card header used to occupy.
<section
// Anchor target for the dashboard spotlight's heading link
// (`/insights#daily-briefing`); `scroll-mt-28` is the app-wide
// sticky-header offset token.
id="daily-briefing"
data-slot="daily-briefing-section"
aria-label={t("insights.dailyBriefing.title")}
className="space-y-3"
className="scroll-mt-28 space-y-3"
>
<SectionHeading
icon={Sparkles}
+8 -2
View File
@@ -606,7 +606,10 @@ describe("rung 8 — briefing", () => {
NOW,
);
expect(verdict.variant).toBe("briefing");
expect(verdict.values).toEqual({ headline: WATCH_FINDING.headline });
expect(verdict.values).toEqual({
headline: WATCH_FINDING.headline,
sourceMetric: WATCH_FINDING.sourceMetric,
});
expect(verdict.cta).toEqual({ kind: "link", href: "/insights" });
});
@@ -623,7 +626,10 @@ describe("rung 8 — briefing", () => {
NOW,
);
expect(verdict.variant).toBe("briefing");
expect(verdict.values).toEqual({ headline: FRESH_FINDING.headline });
expect(verdict.values).toEqual({
headline: FRESH_FINDING.headline,
sourceMetric: FRESH_FINDING.sourceMetric,
});
});
it("skips a STALE briefing even in ready-shaped delivery", () => {
+4 -1
View File
@@ -273,7 +273,10 @@ export function resolveDashboardVerdict(
const picked = findings.find((f) => f.tone === "watch") ?? findings[0];
return {
variant: "briefing",
values: { headline: picked.headline },
// `sourceMetric` rides along so the hero can deep-link the
// sentence to the picked finding's metric sub-page (METRIC_HREF);
// renderers that don't know the field ignore it.
values: { headline: picked.headline, sourceMetric: picked.sourceMetric },
cta: { kind: "link", href: "/insights" },
};
}