fix(dashboard): never truncate the tile value, let the unit yield instead

The small KPI tiles in the dashboard strip clipped the numeric value at
narrow widths: the headline value span carried `min-w-0 truncate` while
the unit span was `shrink-0`, so on a dense strip (many tiles → narrow
grid column) the number — not the unit — shrank below its content width
and ellipsis-clipped, rendering "130 mmHg" as a truncated "13…".

Flip the priority. The value span is now `shrink-0 whitespace-nowrap` so
every digit always renders in full on one line, and the unit span takes
`min-w-0 truncate` so it yields when the column is too narrow. The
paired-value font step-down (text-3xl → text-2xl for BP systolic/
diastolic) stays as headroom.

Add guards asserting the value node never carries truncate/min-w-0 and
the full reading (single and paired) renders inside it.
This commit is contained in:
Marc-André Bombeck committed 2026-06-02 10:00:48 +02:00
1 parent 9652047e8e
commit be90c4b9ad
2 files changed
+107 -10

No files matched your search

@@ -123,6 +123,93 @@ describe("<TrendCard> directionSentiment", () => {
});
});
describe("<TrendCard> value is never truncated", () => {
// v1.8.7 — the maintainer is emphatic: the numeric value must always
// render in full at any tile width (dense strip → narrowest column).
// The value span keeps its full intrinsic width (`shrink-0
// whitespace-nowrap`) and never carries `truncate`/`min-w-0`; when
// space is tight the UNIT yields instead. These guards pin the
// contract so a future layout churn can't reintroduce the clip.
function valueSpan(html: string): string {
const marker = 'data-slot="trend-card-value"';
const idx = html.indexOf(marker);
expect(idx).toBeGreaterThan(-1);
// Walk back to the opening "<span" of the value node.
const open = html.lastIndexOf("<span", idx);
const close = html.indexOf("</span>", idx);
return html.slice(open, close);
}
it("renders the full BP value (130) — never clipped to '13'", () => {
const html = render(
<TrendCard
label="BP systolic"
latest={130}
unit="mmHg"
avg7={128}
avg30={129}
slope30={null}
icon={Activity}
/>,
);
// Value span renders the full reading (formatter adds one decimal).
expect(valueSpan(html)).toContain("130.0");
});
it("renders the full paired BP value (131/85)", () => {
const html = render(
<TrendCard
label="BP"
latest={131}
unit="mmHg"
avg7={130}
avg30={130}
slope30={null}
icon={Activity}
secondary={{ latest: 85, avg7: 84, avg30: 84 }}
/>,
);
expect(valueSpan(html)).toContain("131.0/85.0");
});
it("does not put truncate or min-w-0 on the value node", () => {
const html = render(
<TrendCard
label="Weight"
latest={80.4}
unit="kg"
avg7={80}
avg30={80}
slope30={null}
icon={Activity}
/>,
);
const value = valueSpan(html);
expect(value).not.toContain("truncate");
expect(value).not.toContain("min-w-0");
// The value holds its width and the number stays on one line.
expect(value).toContain("shrink-0");
expect(value).toContain("whitespace-nowrap");
});
it("lets the unit yield (min-w-0 truncate) instead of the value", () => {
const html = render(
<TrendCard
label="VO2"
latest={42}
unit="mL/(kg·min)"
avg7={42}
avg30={42}
slope30={null}
icon={Activity}
/>,
);
// The unit text renders in full in the markup; the truncate is a
// density safeguard that only engages when the column is too narrow.
expect(html).toContain("mL/(kg·min)");
});
});
describe("<TrendCard> responsive layout", () => {
it("keeps long BP target tile content wrappable inside the card", () => {
// v1.4.28 FB-C2 — the BD-Zielbereich tile dropped `avgAllTime`
+20 -10
View File
@@ -265,21 +265,30 @@ export function TrendCard({
ask: "rechts neben der Zahl … der Pfeil [ist], wie gerade der
Trend ist". */}
<div
className="mt-2 flex min-w-0 items-baseline gap-x-1.5"
className="mt-2 flex items-baseline gap-x-1.5"
data-slot="trend-card-value-row"
>
<span
className={cn(
"min-w-0 truncate font-semibold tracking-tight tabular-nums",
"shrink-0 whitespace-nowrap font-semibold tracking-tight tabular-nums",
// v1.8.7 — the value NEVER truncates, clips, or wraps
// mid-number. The maintainer is emphatic: every digit of
// the reading must render in full ("130 mmHg" must read as
// "130", never "13" then "3 mmHg"). The headline value span
// is `shrink-0 whitespace-nowrap` so it keeps its full
// intrinsic width and wins the horizontal-space contest
// against the unit. When the strip is dense (many tiles →
// narrow column) the UNIT yields — it shrinks (`min-w-0
// truncate`) and may ellipsis — rather than losing any
// digit of the number. The earlier `min-w-0 truncate` on
// this span was the bug: it let the value shrink below its
// content width and clip the number.
//
// v1.4.49.4 — paired values (BP systolic/diastolic) render
// as `131/85` and outgrow the `text-3xl` footprint on the
// narrowest grid columns, ellipsis-clipping to "13…" on the
// dashboard. The truncate stays as a defence-in-depth safety
// net, but paired tiles drop one font step (text-3xl →
// text-2xl) so the full pair stays visible at every column
// width. Single-value tiles (weight, pulse, glucose, …)
// keep text-3xl because their value never threatens the
// budget on its own.
// narrowest grid columns. Paired tiles drop one font step
// (text-3xl → text-2xl) so the full pair fits; single-value
// tiles (weight, pulse, glucose, …) keep text-3xl.
secondary ? "text-2xl" : "text-3xl",
// `leading-none` must come AFTER the font-size class in the
// cn() arg list. Tailwind v4 `text-3xl` / `text-2xl` ship
@@ -290,10 +299,11 @@ export function TrendCard({
// across every tile in the strip).
"leading-none",
)}
data-slot="trend-card-value"
>
{latest !== null ? renderPair(latest, secondary?.latest) : "—"}
</span>
<span className="text-muted-foreground shrink-0 text-sm tabular-nums">
<span className="text-muted-foreground min-w-0 truncate text-sm tabular-nums">
{unit}
</span>
<span