From 023115b05c4104bcc6a5e17557e15f1591085385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 10 Oct 2026 20:04:13 +0200 Subject: [PATCH 1/2] fix(timeline): keep each mean's dot and value one unbreakable unit The dot before a mean sat on the x-height middle, a little below the text, and a wrapping line could leave it at the end of one line while its value moved to the next. Each dot and value is now one inline-flex unit that centres the dot on the text line and never wraps inside; the row breaks only between units, a long name is cut short inside its own unit, and the leading mean sign rides in the first unit. The legend and chip dots no longer shrink beside long text. --- .../__tests__/timeline-components.test.tsx | 37 +++++++++++++++++++ src/components/timeline/selection-bar.tsx | 8 ++-- src/components/timeline/timeline-chart.tsx | 20 ++++++++-- .../timeline/timeline-chronicle.tsx | 11 ++++-- src/components/timeline/timeline-view.tsx | 9 +++-- 5 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/components/timeline/__tests__/timeline-components.test.tsx b/src/components/timeline/__tests__/timeline-components.test.tsx index d4a484f7f..e2542bc63 100644 --- a/src/components/timeline/__tests__/timeline-components.test.tsx +++ b/src/components/timeline/__tests__/timeline-components.test.tsx @@ -551,6 +551,43 @@ describe("value line colours", () => { expect(text(html)).toBe("129/82 mmHg, 82,6 kg"); expect(html).toContain('class="sr-only">, '); }); + + it("keeps each dot and its value in one unit that never breaks", () => { + const html = render( + , + ); + // Each unit opens with its own tag; everything up to the next unit's tag + // (the lead, the dot, the value) is inside it. + const units = html.split(/(?=]*data-slot="timeline-mean-part")/); + expect(units).toHaveLength(3); + for (const unit of units) { + const open = unit.slice(0, unit.indexOf(">")); + expect(open).toContain("inline-flex"); + expect(open).toContain("items-center"); + expect(open).toContain("whitespace-nowrap"); + const dot = unit.match( + /]*data-slot="timeline-series-dot"[^>]*>/, + )?.[0]; + expect(dot).toContain("shrink-0"); + expect(dot).not.toContain("align-middle"); + // The value follows the dot inside the same unit. + expect(text(unit.slice(unit.indexOf(dot!)))).toMatch(/\S/); + } + // The lead rides in the first unit, so "Ø" never ends a line alone. + expect(text(units[0]).startsWith("Ø")).toBe(true); + // A long name is cut short inside its unit. + expect(units[2]).toContain( + 'Daily strain with a very long name 12', + ); + }); }); describe("life events in the capture picker", () => { diff --git a/src/components/timeline/selection-bar.tsx b/src/components/timeline/selection-bar.tsx index 7c844e611..9c821c9af 100644 --- a/src/components/timeline/selection-bar.tsx +++ b/src/components/timeline/selection-bar.tsx @@ -39,7 +39,7 @@ import { } from "./timeline-dates"; import { useItemWords } from "./item-words"; import { itemsInPeriod } from "./timeline-geometry"; -import { MeanPartsLine, laneDotStyle } from "./timeline-chart"; +import { MEAN_PARTS_ROW, MeanPartsLine, laneDotStyle } from "./timeline-chart"; import { bucketValues, labelledMeanParts, @@ -201,12 +201,12 @@ export function SelectionBar({ })} {means.values.length > 0 && ( - {meanBefore} - {meanAfter} + {meanAfter ? {meanAfter} : null} )} diff --git a/src/components/timeline/timeline-chart.tsx b/src/components/timeline/timeline-chart.tsx index 86eee576c..106d1839a 100644 --- a/src/components/timeline/timeline-chart.tsx +++ b/src/components/timeline/timeline-chart.tsx @@ -1117,13 +1117,22 @@ function SeriesTable({ * ● 82,6 kg". The values stand apart by space, not by a separator glyph; * a screen reader hears a comma between them. The dot is decoration; the * text reads the same without it. + * + * Each dot and its value are one unit that never breaks: a line wraps only + * between units (the caller's container is a wrapping flex row), so a dot + * cannot stay behind at the end of a line while its value moves to the + * next. The dot sits on the middle of the text line, and a name too long for + * the row is cut short inside its unit. `lead` (the "Ø" in front of the + * selection bar's means) rides in the first unit for the same reason. */ export function MeanPartsLine({ parts, seriesColor, + lead, }: { parts: readonly MeanPart[]; seriesColor: (key: string) => string; + lead?: React.ReactNode; }) { return parts.map((part, i) => ( 0 ? "ml-3" : undefined} + className="inline-flex max-w-full min-w-0 items-center gap-1 whitespace-nowrap" > + {i === 0 && lead ? {lead} : null} {i > 0 ? , : null}