mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(insights): strip the duplicate briefing paragraph and rebalance the Health Score column
The /insights hero strip already renders briefing.paragraph as its
subtitle directly above the Daily Briefing card; the card then mounted
the same paragraph a second time inside its CardContent before the
key-findings list. The user read the same string twice within 200 px.
Drop the leading paragraph render from <DailyBriefing>; the card now
opens directly on the structured key-findings list (the part the hero
subtitle cannot surface). The empty-state branch keeps its 'Generate
briefing' CTA so the populated → empty flow remains intact.
Rebalance the Health Score card so it reads as a co-equal column of
the hero rather than a small inset:
- lg:w-[260px] → lg:w-[360px] xl:w-[400px]
- score number text-4xl → text-5xl sm:text-6xl
- progress bar h-1.5 → h-2
- inline Ask-the-Coach button retires; the hero strip's existing
action covers it. onAskCoach stays on the prop signature so callers
don't break.
Fold in BL-P4-9 L2: disclaimer text-[10px] → text-[11px] (the 10 px
size was borderline against the 12 px mobile floor flagged in the
W10 design review).
Tests updated: daily-briefing test asserts the paragraph slot is
absent in the populated branch; health-score-card tests pin the new
no-button render across English and German locales.
This commit is contained in:
1 parent
25952d7d05
commit
5b8a47b4ff
4 files changed
+60
-54
No files matched your search
@@ -63,10 +63,15 @@ describe("<DailyBriefing>", () => {
|
||||
expect(html).toContain("Tagesbriefing");
|
||||
});
|
||||
|
||||
it("renders the narrative paragraph", () => {
|
||||
it("v1.4.27 B1 — does NOT render the leading narrative paragraph", () => {
|
||||
// The hero strip subtitle on /insights renders the same
|
||||
// `briefing.paragraph` directly above this card, so the card now
|
||||
// opens straight on the key-findings list. The paragraph slot is
|
||||
// gone from the populated branch; the empty-state branch still
|
||||
// owns its CTA copy.
|
||||
const html = render(<DailyBriefing briefing={baseBriefing} />);
|
||||
expect(html).toMatch(/data-slot="daily-briefing-paragraph"/);
|
||||
expect(html).toContain(
|
||||
expect(html).not.toContain('data-slot="daily-briefing-paragraph"');
|
||||
expect(html).not.toContain(
|
||||
"Blood pressure is in target band on 9 of 10 readings",
|
||||
);
|
||||
});
|
||||
@@ -133,13 +138,16 @@ describe("<DailyBriefing>", () => {
|
||||
});
|
||||
|
||||
it("hides the keyFindings section when the array is empty", () => {
|
||||
// v1.4.27 B1 — the leading paragraph dropped; the card now renders
|
||||
// only the header + (when present) the findings list. With both
|
||||
// gone the populated branch produces no body content, which the
|
||||
// hero strip already covers.
|
||||
const html = render(
|
||||
<DailyBriefing briefing={{ ...baseBriefing, keyFindings: [] }} />,
|
||||
);
|
||||
expect(html).not.toContain('data-slot="daily-briefing-findings-title"');
|
||||
expect(html).not.toContain('data-slot="daily-briefing-findings"');
|
||||
// Paragraph still renders.
|
||||
expect(html).toContain("trending well this week");
|
||||
expect(html).not.toContain('data-slot="daily-briefing-paragraph"');
|
||||
});
|
||||
|
||||
it("renders the empty state when briefing is null and not loading", () => {
|
||||
|
||||
@@ -179,8 +179,12 @@ describe("<HealthScoreCard>", () => {
|
||||
expect(html).toContain("Indicative");
|
||||
});
|
||||
|
||||
it("does NOT render the Ask-the-Coach button when onAskCoach is omitted", () => {
|
||||
const html = ssr(
|
||||
it("v1.4.27 B1 — no longer mounts an inline Ask-the-Coach button (hero strip carries the CTA)", () => {
|
||||
// The hero strip's existing "Ask the coach" action covers this
|
||||
// surface; the card-internal duplicate button retired. The
|
||||
// onAskCoach prop stays on the type signature so callers don't
|
||||
// break, but the card no longer renders it.
|
||||
const omitted = ssr(
|
||||
<HealthScoreCard
|
||||
score={86}
|
||||
band="green"
|
||||
@@ -188,11 +192,7 @@ describe("<HealthScoreCard>", () => {
|
||||
delta={null}
|
||||
/>,
|
||||
);
|
||||
expect(html).not.toContain('data-slot="health-score-card-ask-coach"');
|
||||
});
|
||||
|
||||
it("renders the Ask-the-Coach button when onAskCoach is supplied", () => {
|
||||
const html = ssr(
|
||||
const supplied = ssr(
|
||||
<HealthScoreCard
|
||||
score={86}
|
||||
band="green"
|
||||
@@ -201,8 +201,8 @@ describe("<HealthScoreCard>", () => {
|
||||
onAskCoach={() => {}}
|
||||
/>,
|
||||
);
|
||||
expect(html).toMatch(/data-slot="health-score-card-ask-coach"/);
|
||||
expect(html).toContain("Ask the Coach");
|
||||
expect(omitted).not.toContain('data-slot="health-score-card-ask-coach"');
|
||||
expect(supplied).not.toContain('data-slot="health-score-card-ask-coach"');
|
||||
});
|
||||
|
||||
it("captures the score-aware prefill argument when the button is invoked", () => {
|
||||
@@ -244,9 +244,11 @@ describe("<HealthScoreCard>", () => {
|
||||
"de",
|
||||
);
|
||||
expect(html).toContain("Gesundheitsscore");
|
||||
expect(html).toContain("Coach fragen");
|
||||
expect(html).toContain("im Vergleich zur Vorwoche");
|
||||
expect(html).toContain("Orientierungshilfe");
|
||||
// v1.4.27 B1 — the inline Coach button retired; hero strip carries
|
||||
// the action now.
|
||||
expect(html).not.toContain("Coach fragen");
|
||||
});
|
||||
|
||||
it("uses tabular-nums on the headline number for stable layout", () => {
|
||||
|
||||
@@ -232,12 +232,13 @@ export function DailyBriefing({
|
||||
</>
|
||||
) : briefing ? (
|
||||
<div className="space-y-4">
|
||||
<p
|
||||
data-slot="daily-briefing-paragraph"
|
||||
className="text-foreground text-sm leading-relaxed"
|
||||
>
|
||||
{stripChartTokens(briefing.paragraph)}
|
||||
</p>
|
||||
{/* v1.4.27 B1 — the leading narrative paragraph dropped.
|
||||
The hero strip subtitle on `/insights` already renders
|
||||
the same `briefing.paragraph` text directly above this
|
||||
card, so the user used to read the same string twice
|
||||
within 200 px. The card now opens straight on the
|
||||
structured key-findings list, which is the part the
|
||||
hero subtitle cannot surface. */}
|
||||
{briefing.keyFindings.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useId, useMemo, useState } from "react";
|
||||
import { ArrowDown, ArrowUp, ChevronDown, Minus, Sparkles } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowDown, ArrowUp, ChevronDown, Minus } from "lucide-react";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -149,7 +148,12 @@ export function HealthScoreCard({
|
||||
band,
|
||||
components,
|
||||
delta,
|
||||
onAskCoach,
|
||||
// v1.4.27 B1 — onAskCoach stays on the prop signature so the hero
|
||||
// strip's existing wire-up doesn't break, but the card no longer
|
||||
// mounts an inline button (the hero strip already carries the same
|
||||
// action). Destructure-and-ignore is intentional.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onAskCoach: _onAskCoach,
|
||||
initiallyExpanded = false,
|
||||
}: HealthScoreCardProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
@@ -228,17 +232,13 @@ export function HealthScoreCard({
|
||||
className={cn(
|
||||
"bg-card/65 rounded-xl border px-4 py-4 shadow-sm backdrop-blur-sm",
|
||||
BAND_BORDER_CLASS[band],
|
||||
// v1.4.25 W3 — Marc reported the German "Einnahmetreue" label
|
||||
// overlapping the band-coloured component-value pill at the
|
||||
// right of each row. The original `w-[220px]` left only ~64px
|
||||
// for the label column after the bar + value chip ate the
|
||||
// rest, which fits "Mood" / "BP" but truncates the longer
|
||||
// German strings. Bumped to `w-[260px]` so the card sits
|
||||
// ~18 % wider on `lg+` without disturbing the title block's
|
||||
// visual centre of gravity; the label column is widened in
|
||||
// step with this so the breathing room actually lands on the
|
||||
// text.
|
||||
"w-full lg:w-[260px] lg:shrink-0",
|
||||
// v1.4.27 B1 — bumped from `lg:w-[260px]` to `lg:w-[360px]
|
||||
// xl:w-[400px]` so the score occupies ~28-31 % of the hero on
|
||||
// 1280-px viewports and ~36 % on 1536+, reading as a true
|
||||
// co-equal column rather than a small inset. The narrower
|
||||
// earlier value was a reaction to a label-overflow bug that
|
||||
// is now fixed by the wider label column below.
|
||||
"w-full lg:w-[360px] lg:shrink-0 xl:w-[400px]",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -277,7 +277,11 @@ export function HealthScoreCard({
|
||||
<span
|
||||
data-slot="health-score-card-number"
|
||||
className={cn(
|
||||
"text-4xl leading-none font-semibold tabular-nums",
|
||||
// v1.4.27 B1 — bumped from text-4xl to text-5xl
|
||||
// (sm:text-6xl on wider viewports) so the headline
|
||||
// number becomes the visual centre of gravity of the
|
||||
// expanded card.
|
||||
"text-5xl leading-none font-semibold tabular-nums sm:text-6xl",
|
||||
BAND_NUMBER_CLASS[band],
|
||||
)}
|
||||
>
|
||||
@@ -293,7 +297,7 @@ export function HealthScoreCard({
|
||||
|
||||
<div
|
||||
data-slot="health-score-card-progress"
|
||||
className="bg-muted/50 h-1.5 w-full overflow-hidden rounded-full"
|
||||
className="bg-muted/50 h-2 w-full overflow-hidden rounded-full"
|
||||
role="progressbar"
|
||||
aria-valuenow={score}
|
||||
aria-valuemin={0}
|
||||
@@ -533,28 +537,19 @@ export function HealthScoreCard({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* v1.4.27 B1 — disclaimer bumped from text-[10px] to
|
||||
text-[11px] (BL-P4-9 L2: the 10 px size was borderline
|
||||
against the 12 px mobile floor). The hero strip already
|
||||
carries an "Ask the coach" action so the inline button
|
||||
here retires; the parent still receives onAskCoach via
|
||||
other surfaces but the card no longer mounts a duplicate
|
||||
CTA. */}
|
||||
<p
|
||||
data-slot="health-score-card-disclaimer"
|
||||
className="text-muted-foreground text-[10px] leading-snug"
|
||||
className="text-muted-foreground text-[11px] leading-snug"
|
||||
>
|
||||
{t("insights.healthScore.disclaimer")}
|
||||
</p>
|
||||
|
||||
{onAskCoach && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
data-slot="health-score-card-ask-coach"
|
||||
className="w-full gap-1.5"
|
||||
onClick={() =>
|
||||
onAskCoach(t("insights.healthScore.coachPrompt", { score }))
|
||||
}
|
||||
>
|
||||
<Sparkles className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
<span>{t("insights.healthScore.askCoach")}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user