Merge branch 'fix/mobile-density' into release/v1.27.14

This commit is contained in:
Marc-André Bombeck committed 2026-07-07 10:38:17 +02:00
commit d7b98ad9f8
8 files changed
+342 -33

No files matched your search

@@ -180,4 +180,34 @@ describe("<BriefingSpotlight>", () => {
const rows = html.match(/data-slot="dashboard-briefing-spotlight-row"/g);
expect(rows).toHaveLength(3);
});
it("stacks headline over delta below sm and keeps the row at sm+", () => {
const html = render({
briefing: briefing({
signalsOfDay: [
{
sourceMetric: "bp",
tone: "watch",
headline:
"Systolischer Blutdruck deutlich unter deinem Monatsmittel gemessen",
nudge: "n",
delta: "−12 mmHg vs. 30-Tage-Mittel",
},
],
}),
briefingState: "ready",
briefingStale: false,
});
// Phone-first column, row restored at sm+ — a wide delta must never
// squeeze the headline into a narrow multi-line column (or push
// itself past the tile edge).
expect(html).toMatch(
/class="[^"]*flex min-w-0 flex-1 flex-col gap-1 sm:flex-row[^"]*"/,
);
// The delta is left-aligned content-width in the stacked state and
// only refuses to shrink in the sm+ row.
expect(html).toMatch(
/data-slot="dashboard-briefing-spotlight-delta"[^>]*class="[^"]*self-start[^"]*sm:shrink-0[^"]*"/,
);
});
});
@@ -196,7 +196,14 @@ export function BriefingSpotlight({
)}
aria-hidden="true"
/>
<div className="flex min-w-0 flex-1 items-start justify-between gap-3">
{/* Phone widths stack headline over delta — a long
signal headline beside a long delta squeezed the
headline into a one-word-per-line column and pushed
the delta past the tile edge. At `sm+` the
side-by-side row returns. The headline always wraps
(content is never truncated); only the delta is meta
and may stay on one line. */}
<div className="flex min-w-0 flex-1 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between sm:gap-3">
<p className="text-foreground text-sm leading-snug font-medium">
{stripChartTokens(row.headline)}
</p>
@@ -204,7 +211,7 @@ export function BriefingSpotlight({
<span
data-slot="dashboard-briefing-spotlight-delta"
className={cn(
"shrink-0 text-xs font-semibold tabular-nums",
"self-start text-xs font-semibold tabular-nums sm:shrink-0",
TONE_TEXT_CLASSNAME[row.tone],
)}
>
@@ -102,6 +102,20 @@ describe("<DailyBriefing>", () => {
expect(html).toContain("+6 mmHg");
});
it("stacks headline over delta below sm and keeps the row at sm+", () => {
const html = render(<DailyBriefing briefing={baseBriefing} />);
// Phone-first column (headline above, delta beneath), side-by-side
// restored at sm+ — a wide delta must never squeeze the headline
// into a narrow multi-line column.
expect(html).toMatch(
/class="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between sm:gap-3"/,
);
// Delta badge stays content-width + left-aligned in the stacked state.
expect(html).toMatch(
/data-slot="daily-briefing-delta"[^>]*class="[^"]*self-start[^"]*sm:shrink-0[^"]*"/,
);
});
it("does NOT render a delta badge when delta is null", () => {
const html = render(
<DailyBriefing
@@ -157,6 +157,31 @@ describe("<HealthScoreCard>", () => {
);
});
it("stacks the explainer caption BELOW the delta line, not beside it", () => {
// The caption is a multi-sentence read; rendering it inside the
// delta's inline-flex row squeezed the short delta into a narrow
// multi-line column on every viewport. The wrapper stacks instead.
const html = ssr(
<HealthScoreCard
score={64}
band="yellow"
components={baseComponents}
delta={-3}
/>,
);
const wrapper = html.match(
/data-slot="health-score-card-delta"[^>]*class="([^"]*)"/,
);
expect(wrapper).not.toBeNull();
expect(wrapper?.[1]).toContain("space-y-1");
expect(wrapper?.[1]).not.toContain("inline-flex");
// The delta line's <p> closes before the explainer body opens —
// the caption is a sibling below, never a flex neighbour.
expect(html).toMatch(
/vs last week[^<]*<\/span><\/p><span[^>]*data-slot="health-score-delta-explainer-body"/,
);
});
it("renders four component rows with their values", () => {
const html = ssr(
<HealthScoreCard
+8 -2
View File
@@ -197,7 +197,9 @@ function DeltaBadge({
<span
data-slot="daily-briefing-delta"
className={cn(
"text-xs font-semibold tabular-nums",
// `self-start` keeps the badge left-aligned + content-width when
// the parent row stacks into a column below `sm`.
"self-start text-xs font-semibold tabular-nums sm:shrink-0",
TONE_TEXT_CLASSNAME[tone],
)}
>
@@ -243,7 +245,11 @@ function BriefingRow({
aria-hidden="true"
/>
<div className="min-w-0 flex-1 space-y-1">
<div className="flex items-start justify-between gap-3">
{/* Phone widths stack headline over delta (same treatment as the
dashboard spotlight rows): side-by-side squeezed a long
headline into a narrow multi-line column beside a long delta.
At `sm+` the row layout returns. */}
<div className="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between sm:gap-3">
<p className="text-sm leading-snug font-medium">
{stripChartTokens(headline)}
</p>
+35 -24
View File
@@ -407,41 +407,52 @@ export function HealthScoreCard({
on phone-class viewports. The explainer only mounts when
a numeric delta is available; the "no history yet"
branch keeps the existing single-line caption. */}
<p
{/* The delta digit and the explainer caption stack — the caption
is a multi-sentence read, and rendering it INSIDE the delta's
inline-flex row squeezed the short delta line into a narrow
multi-line column beside it on every viewport. The delta line
keeps its icon row; the caption sits below at full width. */}
<div
data-slot="health-score-card-delta"
className="text-muted-foreground inline-flex items-center gap-1 text-xs"
className="text-muted-foreground space-y-1 text-xs"
>
{delta === null ? (
<span>{t("insights.healthScore.deltaUnavailable")}</span>
<p>{t("insights.healthScore.deltaUnavailable")}</p>
) : (
<>
{delta > 0 && (
<ArrowUp className="text-success h-3 w-3" aria-hidden="true" />
)}
{delta < 0 && (
<ArrowDown
className="text-destructive h-3 w-3"
aria-hidden="true"
/>
)}
{delta === 0 && (
<Minus
className="text-muted-foreground h-3 w-3"
aria-hidden="true"
/>
)}
<span aria-describedby={deltaExplainerId}>
{t("insights.healthScore.deltaVsLastWeek", {
delta: delta > 0 ? `+${delta}` : `${delta}`,
})}
</span>
<p className="flex items-center gap-1">
{delta > 0 && (
<ArrowUp
className="text-success h-3 w-3"
aria-hidden="true"
/>
)}
{delta < 0 && (
<ArrowDown
className="text-destructive h-3 w-3"
aria-hidden="true"
/>
)}
{delta === 0 && (
<Minus
className="text-muted-foreground h-3 w-3"
aria-hidden="true"
/>
)}
<span aria-describedby={deltaExplainerId}>
{t("insights.healthScore.deltaVsLastWeek", {
delta: delta > 0 ? `+${delta}` : `${delta}`,
})}
</span>
</p>
<HealthScoreDeltaExplainer
className="block"
delta={delta}
bodyId={deltaExplainerId}
/>
</>
)}
</p>
</div>
{/* v1.18.6 — Rest Mode legibility. While an illness episode is
active the server suppresses (never penalises) the score, so