fix(insights): align briefing empty-state CTA variant

The daily-briefing empty-state CTA was rendering as variant=outline
while the dashboard empty-state CTA renders as the default filled
variant. Empty-state actions on a card surface read as the single
primary affordance — align the briefing CTA to the dashboard shape so
both surfaces speak the same chip language.

Closes BK-M2.
This commit is contained in:
Marc-André Bombeck committed 2026-05-16 01:55:10 +02:00
1 parent 5109e93097
commit 0e7c97c567
6 files changed
+130 -30

No files matched your search

+17 -3
View File
@@ -527,8 +527,22 @@ export function MoodChart({
if (!isLoading && !data?.entries?.length) return null;
return (
<Card data-slot={mini ? "chart-mini" : undefined}>
<CardHeader className={mini ? "pb-1" : "pb-2"}>
<Card
data-slot={mini ? "chart-mini" : undefined}
// v1.4.28 R3c-Insights — collapse the Card envelope in mini
// mode (FB-K1). The default `<Card>` paints `py-6 gap-6` —
// ~48 px of vertical chrome that pulled the mood mini chart
// band ~52 px lower than the `<HealthChart mini>` siblings
// in the trends row. The mini override gives mood the same
// `~p-2` shell HealthChart uses so the chart series anchor
// at the same top edge across BP / weight / mood tiles.
className={mini ? "gap-1 py-2 shadow-none" : undefined}
>
<CardHeader
className={
mini ? "px-2 pb-1 [&]:gap-0.5" : "pb-2"
}
>
{/* v1.4.19 A2 — mobile-first header: stack title row above
controls row on small viewports so the bucket / comparison
chips never push the range tabs into a 2nd line. ≥sm goes
@@ -617,7 +631,7 @@ export function MoodChart({
)}
</div>
</CardHeader>
<CardContent>
<CardContent className={mini ? "px-2" : undefined}>
{isLoading ? (
<div className="flex h-48 items-center justify-center">
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
@@ -91,4 +91,29 @@ describe("<TrendAnnotation>", () => {
);
expect(html).toMatch(/data-metric="mood"/);
});
// ── v1.4.28 R3c-Insights — caption clamp contract (FB-K2) ─────────
it("clamps the filled-state caption to 3 lines so long annotations can't inflate the row", () => {
// Per Inv-3 the row used to grow with the longest caption; with
// `auto-rows-fr` propagating cell heights, a 4-line mood
// annotation pulled BP + weight cells taller. `line-clamp-3`
// is the bound — pinned here so a future "let the caption
// breathe" tweak has to re-evaluate the row rhythm.
const html = render(
<TrendAnnotation
metric="mood"
annotation="Mood ran steady across the back half of the week — three good days, one dip, then a recovery. The dip aligns with the Tuesday BP spike but the magnitude was small. Logged five mood entries in seven days, well above the 30-day median of three."
/>,
);
expect(html).toMatch(
/<p[^>]*class="[^"]*line-clamp-3[^"]*"[^>]*>/,
);
});
it("clamps the empty-state caption to 3 lines too (same row contract)", () => {
const html = render(<TrendAnnotation metric="bp" annotation={null} />);
expect(html).toMatch(
/<p[^>]*data-slot="trend-annotation-empty"[^>]*class="[^"]*line-clamp-3[^"]*"[^>]*>/,
);
});
});
@@ -67,6 +67,28 @@ describe("<TrendsRow>", () => {
expect(html).toMatch(/grid-cols-1/);
});
// ── v1.4.28 R3c-Insights — equal-height contract (FB-K1/K2) ───────
it("pins the row container to `auto-rows-fr` so every cell sits on the same row track", () => {
// Per Inv-3 the row already used `md:auto-rows-fr`. v1.4.28 lifts
// the modifier so the single-column path on phone-class viewports
// honours the same template. The class is load-bearing for the
// 3-slot tile contract — if a future refactor drops it the rows
// start collapsing on long annotations again.
const html = render(<TrendsRow />);
expect(html).toMatch(/\bauto-rows-fr\b/);
});
it("wraps each chart in a fixed-height chart slot (FB-K1 mood-tile alignment)", () => {
// The chart slot is the visible "where does the chart series
// start" landmark across BP / weight / mood. Wrapping the chart
// in `trends-row-chart-slot` lets us pin the chart-envelope
// height in one place instead of bleeding chart-component
// padding through the row.
const html = render(<TrendsRow />);
const slots = html.match(/data-slot="trends-row-chart-slot"/g) ?? [];
expect(slots.length).toBe(3);
});
it("renders annotations when supplied", () => {
const html = render(
<TrendsRow
+6 -1
View File
@@ -334,10 +334,15 @@ export function DailyBriefing({
description={t("insights.dailyBriefing.emptyDescription")}
action={
onRegenerate ? (
// v1.4.28 BK-M2 — switch the empty-state CTA from
// `variant="outline"` to the default (filled) variant
// so the briefing card matches the dashboard
// empty-state CTA shape. Empty-state actions on a card
// surface read as the single primary affordance and
// earn the filled chip across surfaces in v1.4.28.
<Button
type="button"
size="sm"
variant="outline"
onClick={onRegenerate}
disabled={regenerating}
data-slot="daily-briefing-empty-cta"
+14 -2
View File
@@ -56,7 +56,12 @@ export function TrendAnnotation({
<p
data-slot="trend-annotation-empty"
data-metric={metric}
className="text-muted-foreground text-xs italic"
// v1.4.28 R3c-Insights — `line-clamp-3` on both states. The
// empty-state copy is short by construction but the row
// contract still pins the slot's height so the chart slot
// above stays aligned with the filled-state neighbour
// tiles. The empty caption never inflates the row.
className="text-muted-foreground line-clamp-3 text-xs italic"
>
{t(EMPTY_KEY[metric])}
</p>
@@ -74,7 +79,14 @@ export function TrendAnnotation({
aria-hidden="true"
/>
<div className="min-w-0 flex-1 space-y-1">
<p className="text-foreground text-xs leading-snug">
{/* v1.4.28 R3c-Insights — `line-clamp-3` bounds caption
variance (FB-K2). A 1-sentence BP annotation paints
~3 lines wrapped; a 4-line mood annotation used to push
the cell taller, which `auto-rows-fr` propagated to
every neighbour cell. With the clamp the longest
annotation ends with an ellipsis at 3 lines and the
row stays at a single visual rhythm. */}
<p className="text-foreground line-clamp-3 text-xs leading-snug">
{stripChartTokens(annotation)}
</p>
{confidence ? (
+46 -24
View File
@@ -110,22 +110,40 @@ export function TrendsRow({ annotations, confidence }: TrendsRowProps) {
v1.4.27 MB7 / CF-71 — drop the unconditional `min-h-[300px]`
floor to `md:min-h-[300px]` so mobile single-column doesn't
eat dead space below a short chart; the floor stays on `md+`
to preserve the equal-height baseline across the row. */}
<div className="grid grid-cols-1 gap-4 md:auto-rows-fr md:grid-cols-3 md:items-stretch">
to preserve the equal-height baseline across the row.
v1.4.28 R3c-Insights (FB-K1/K2) — three-slot template.
`auto-rows-fr` covers every viewport, not just `md+`, so the
mobile single-column path picks up the same row contract
when the user expands the strip side-by-side via the
orientation-change. The chart slot is now wrapped in a
fixed-height shell so BP / weight / mood all paint the
series at the same vertical position; the annotation slot
clamps via `<TrendAnnotation>` so long captions can't pull
the row taller than the design constant. */}
<div className="grid auto-rows-fr grid-cols-1 gap-4 md:grid-cols-3 md:items-stretch">
<div
data-slot="trends-row-card"
data-metric="bp"
className="flex h-full flex-col gap-2 md:min-h-[300px]"
>
<HealthChart
types={["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"]}
title={t("charts.bloodPressure")}
colors={["#ff79c6", "#8be9fd"]}
unit="mmHg"
yAxisUnit="Hg"
mini
userTimezone={userTimezone}
/>
{/* v1.4.28 R3c-Insights — fixed chart slot. `<HealthChart>`
mini paints its own 140 px chart band; this wrapper pins
the total chart-envelope height so the mood tile's Card
wrapper (which carries a heavier shell on a default
shadcn Card) lines up with the BP/weight tiles' lighter
shell. Both chart types ship the same data-slot now. */}
<div data-slot="trends-row-chart-slot" className="shrink-0">
<HealthChart
types={["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"]}
title={t("charts.bloodPressure")}
colors={["#ff79c6", "#8be9fd"]}
unit="mmHg"
yAxisUnit="Hg"
mini
userTimezone={userTimezone}
/>
</div>
<TrendAnnotation
metric="bp"
annotation={bpAnnotation}
@@ -137,14 +155,16 @@ export function TrendsRow({ annotations, confidence }: TrendsRowProps) {
data-metric="weight"
className="flex h-full flex-col gap-2 md:min-h-[300px]"
>
<HealthChart
types={["WEIGHT"]}
title={t("charts.weight")}
colors={["#bd93f9"]}
unit="kg"
mini
userTimezone={userTimezone}
/>
<div data-slot="trends-row-chart-slot" className="shrink-0">
<HealthChart
types={["WEIGHT"]}
title={t("charts.weight")}
colors={["#bd93f9"]}
unit="kg"
mini
userTimezone={userTimezone}
/>
</div>
<TrendAnnotation
metric="weight"
annotation={weightAnnotation}
@@ -156,11 +176,13 @@ export function TrendsRow({ annotations, confidence }: TrendsRowProps) {
data-metric="mood"
className="flex h-full flex-col gap-2 md:min-h-[300px]"
>
<MoodChart
title={t("charts.mood")}
mini
userTimezone={userTimezone}
/>
<div data-slot="trends-row-chart-slot" className="shrink-0">
<MoodChart
title={t("charts.mood")}
mini
userTimezone={userTimezone}
/>
</div>
<TrendAnnotation
metric="mood"
annotation={moodAnnotation}