mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(dashboard): reserve two label lines on trend tiles instead of truncating
The strip tile pinned its label to one line so the value rows share a
baseline, and cut anything longer: uppercase compounds and glucose
contexts ("HERZFREQUENZVARIABILITAET", "GLUKOSE, VOR DEM SCHLAFEN")
ended in an ellipsis. The heading row now reserves two lines at a fixed
height, so the baseline promise holds and the label wraps at a syllable;
a label that would need a third line clamps with its full text in title.
The skeleton reserves the same height.
This commit is contained in:
1 parent
7af2e3d29a
commit
e312d99162
3 files changed
+53
-29
No files matched your search
@@ -11,10 +11,13 @@ import { I18nProvider } from "@/lib/i18n/context";
|
||||
* Maintainer directive 2026-05-14 named three issues with the 5-tile top row:
|
||||
*
|
||||
* 1. Headings wrapped to two lines at narrow viewports (e.g. "Blood
|
||||
* Pressure" on Pixel 5 / 393 px / 2-col grid). Locked the heading
|
||||
* row to a deterministic `h-5` with `whitespace-nowrap` so even an
|
||||
* unexpectedly long string never breaks the baseline of the value
|
||||
* row below.
|
||||
* Pressure" on Pixel 5 / 393 px / 2-col grid), moving the value row.
|
||||
* The heading row is a deterministic height so the value row keeps
|
||||
* its baseline. It reserves TWO lines (`h-10`, v1.43.8): pinned to one
|
||||
* line it could only keep that promise by cutting the label, and long
|
||||
* German compounds and glucose contexts ended in an ellipsis on every
|
||||
* dashboard. A label wraps (hyphenated) inside the reserve; the rare
|
||||
* third line clamps with the full text in `title`.
|
||||
* 2. Trend arrow rendered as a wrapping flex child after the unit —
|
||||
* sometimes wrapped to its own line, sometimes missing entirely
|
||||
* when slope30 was null. Now sits in a fixed `h-4 w-4` slot with
|
||||
@@ -27,8 +30,8 @@ import { I18nProvider } from "@/lib/i18n/context";
|
||||
* tile in the strip to share the same baseline.
|
||||
*
|
||||
* These tests pin the load-bearing class names so a future refactor
|
||||
* that drops `h-5` / `items-baseline` / `whitespace-nowrap` triggers a
|
||||
* red CI before it reaches the dashboard.
|
||||
* that drops `h-10` / `items-baseline` / `line-clamp-2` triggers a red CI
|
||||
* before it reaches the dashboard.
|
||||
*/
|
||||
|
||||
const RISING = { slope: 0.5, direction: "up" as const, confidence: 0.8 };
|
||||
@@ -53,13 +56,14 @@ describe("<TrendCard> W20a baseline alignment", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// h-5 is the load-bearing class — keeps the value row below at the
|
||||
// same y-coordinate regardless of the label string's actual width.
|
||||
expect(html).toContain("h-5");
|
||||
expect(html).toContain('data-slot="trend-card-label"');
|
||||
// h-10 is the load-bearing class — keeps the value row below at the
|
||||
// same y-coordinate whether the label takes one line or two.
|
||||
expect(html).toMatch(
|
||||
/class="flex h-10 [^"]*"[^>]*><span[^>]*data-slot="trend-card-label"/,
|
||||
);
|
||||
});
|
||||
|
||||
it("forbids wrapping on the heading row even with a long label", () => {
|
||||
it("wraps a long label inside the reserve instead of cutting it", () => {
|
||||
const html = render(
|
||||
<TrendCard
|
||||
// Stress test — the live tile uses dashboard.*Short keys but
|
||||
@@ -75,8 +79,19 @@ describe("<TrendCard> W20a baseline alignment", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).toContain("whitespace-nowrap");
|
||||
expect(html).toContain("truncate");
|
||||
const label = html.match(
|
||||
/<span class="([^"]*)" data-slot="trend-card-label" title="([^"]*)"/,
|
||||
);
|
||||
expect(label, "label span not found").not.toBeNull();
|
||||
const [, classes, title] = label!;
|
||||
// Wraps at a syllable, clamps only past the two reserved lines, and the
|
||||
// clamp keeps the whole name reachable.
|
||||
expect(classes).toContain("hyphens-auto");
|
||||
expect(classes).toContain("break-words");
|
||||
expect(classes).toContain("line-clamp-2");
|
||||
expect(classes).not.toContain("truncate");
|
||||
expect(classes).not.toContain("whitespace-nowrap");
|
||||
expect(title).toBe("A really long blood pressure metric name");
|
||||
});
|
||||
|
||||
it("renders the value row with baseline alignment", () => {
|
||||
@@ -166,9 +181,10 @@ describe("<TrendCard> W20a baseline alignment", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Same heading-row height contract — `h-5` + `whitespace-nowrap`.
|
||||
expect(html).toContain("h-5");
|
||||
expect(html).toContain("whitespace-nowrap");
|
||||
// Same heading-row height contract — two reserved label lines.
|
||||
expect(html).toMatch(
|
||||
/class="flex h-10 [^"]*"[^>]*><span[^>]*data-slot="trend-card-label"/,
|
||||
);
|
||||
// Same value-row baseline contract — `items-baseline` + `text-3xl`
|
||||
// + `leading-none`. Identical font geometry across all five tiles
|
||||
// means the digits land at the same y-coordinate inside the grid.
|
||||
|
||||
@@ -31,10 +31,11 @@ export function TrendCardSkeleton({ className }: { className?: string }) {
|
||||
// legacy empty silhouette reserved.
|
||||
className={cn("min-h-[8rem]", className)}
|
||||
>
|
||||
{/* Heading row — label + icon (TrendCard pins this row at h-5). */}
|
||||
<div className="flex h-5 items-center justify-between gap-2">
|
||||
<Skeleton className="h-3 w-16" />
|
||||
<Skeleton className="h-4 w-4 shrink-0" />
|
||||
{/* Heading row — label + icon (TrendCard reserves two label lines,
|
||||
h-10, so the loaded tile does not move the value row). */}
|
||||
<div className="flex h-10 items-start justify-between gap-2">
|
||||
<Skeleton className="mt-1 h-3 w-16" />
|
||||
<Skeleton className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
</div>
|
||||
{/* Headline value row — `text-3xl leading-none` ≈ 30 px, plus the
|
||||
unit slot, baseline-aligned exactly like the loaded tile. */}
|
||||
|
||||
@@ -206,20 +206,27 @@ export function TrendCard({
|
||||
|
||||
return (
|
||||
<TrendTile data-slot="trend-card">
|
||||
{/* v1.4.25 — single-line discipline + deterministic height for
|
||||
the heading row so the value row below sits at the same baseline
|
||||
across every tile. The label is locale-abbreviated upstream (see
|
||||
`dashboard.*Short` keys) so `truncate` is a layout safeguard
|
||||
rather than the primary defence against wraps. `h-5` matches
|
||||
`text-xs leading-5` ≈ 20 px; identical across every tile. */}
|
||||
<div className="flex h-5 min-w-0 items-center justify-between gap-2">
|
||||
{/* The heading row has a deterministic height so the value row below
|
||||
sits at the same baseline across every tile in the strip. It
|
||||
reserves two lines, not one: a one-line row could only keep that
|
||||
promise by cutting the label, and the label is the only thing that
|
||||
names the figure. An uppercase German compound
|
||||
("HERZFREQUENZVARIABILITÄT") or a glucose context ("GLUKOSE, VOR
|
||||
DEM SCHLAFEN") does not fit a strip tile on one line in any width
|
||||
the grid hands out, and ended in an ellipsis. `hyphens-auto` splits
|
||||
a compound at a syllable (driven by the document's `lang`),
|
||||
`break-words` is the floor, and the rare label that needs a third
|
||||
line clamps with its full text in `title`. `h-10` is two
|
||||
`text-xs leading-5` lines; identical across every tile. */}
|
||||
<div className="flex h-10 min-w-0 items-start justify-between gap-2">
|
||||
<span
|
||||
className="text-muted-foreground min-w-0 flex-1 truncate text-xs leading-5 font-medium tracking-wide whitespace-nowrap uppercase"
|
||||
className="text-muted-foreground line-clamp-2 min-w-0 flex-1 text-xs leading-5 font-medium tracking-wide break-words hyphens-auto uppercase"
|
||||
data-slot="trend-card-label"
|
||||
title={label}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<Icon className="text-muted-foreground h-4 w-4 shrink-0" />
|
||||
<Icon className="text-muted-foreground mt-0.5 h-4 w-4 shrink-0" />
|
||||
</div>
|
||||
{/* v1.4.25 — baseline-aligned value row. The headline value,
|
||||
unit, and trend arrow share a single flex line with
|
||||
|
||||
Reference in new issue
Block a user