5.4 KiB
Align the year-row timelines in the Overview tab
Context
In the Dashboard Overview tab each year is a <details class="disclosure year-row"> whose
<summary> is its own grid: grid-template-columns: 3.5rem minmax(0, 1fr) auto var(--icon-sm)
(frontend/src/styles/components.css:225). Because every row is an independent grid, the auto
figures column is sized per row by that row's own text — "· expiring 5", a wider saved number,
a longer parent name, a 3-digit vs 2-digit PL value. The minmax(0, 1fr) track column absorbs the
difference, so the timeline bars end at a different x per year (see timeline-offset.png: 2029's
track is clearly shortest, 2025 the longest). The figures columns are ragged for the same reason.
Goal: every year's timeline track starts and ends at the same x, and the figure columns line up down the list — without hardcoding a pixel width that breaks in Swedish (sv strings are longer than en) or when a number gains a digit.
Approach — one shared grid via subgrid
Promote the column definition from each summary to the .year-rows list and have the rows opt into
it with grid-template-columns: subgrid. Tracks are then sized once, by the widest content in the
whole list, and every row inherits identical column widths. Content still sizes the columns, so it
stays bilingual-safe.
Split the figures cell into its three real columns (name | PL left | vacation group) at the list level too, so the numbers align vertically across years as well.
CSS only — no TSX changes. The existing markup already has the right elements
(.year-rows → .year-row (details) → .year-row__summary / .year-row__body,
.year-row__figures → .year-row__parent { display: contents }).
Files
frontend/src/styles/components.css—.year-rows(162),.year-row__summary(225),.year-row__summary::after(238),.year-row__year(244),.year-row__track(252),.year-row__figures(289),.year-row__body(322)frontend/src/styles/responsive.css— mobile block at 93–120 (must reset subgrid)
Shape
.year-rows becomes the grid owner:
.year-rows {
display: grid;
/* year | track | name | PL left | vacation group | chevron */
grid-template-columns: 3.5rem minmax(0, 1fr) auto auto auto var(--icon-sm);
}
Each row spans it and re-exposes it:
.year-row { /* the <details> */
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
}
.year-row__summary {
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
/* keep: align-items, column-gap, row-gap, padding, font-weight */
}
.year-row__body { grid-column: 1 / -1; }
Column placement inside the summary shifts to match the six tracks: year 1, track 2,
.year-row__figures grid-column: 3 / 6 + grid-template-columns: subgrid (keep
display: grid, align-items: baseline, white-space: nowrap; drop its own
auto auto auto), chevron ::after grid-column: 6. .year-row__parent { display: contents }
stays — each parent's three spans land in the three shared columns.
Notes:
--sp-4column-gap must be declared on.year-rowsas well, since a subgrid inherits the parent's gaps unless it sets its own; keep the summary'scolumn-gapso the values agree..year-rows > .disclosure:last-child { border-bottom: none }and the.disclosurehairlines inprimitives.cssare unaffected (borders on thedetailsstill paint).- In the solo case
.year-row__nameisn't rendered; that column collapses to0for every row — still uniform. - Left-over
grid-row: 1declarations on year/track/figures stay valid.
Fallback
Wrap the promotion in a support check so older engines keep today's (merely ragged) layout rather than collapsing:
@supports (grid-template-columns: subgrid) { /* the rules above */ }
Keep the current .year-row__summary rule as the base; the @supports block overrides it.
Mobile
responsive.css already re-lays the summary as 3rem minmax(0, 1fr) var(--icon-sm) with the
figures wrapped to row 2 as a flex column. Inside that media query, reset the new rules:
.year-rows { display: flex; flex-direction: column } and
.year-row { display: block } — so the mobile block's existing declarations apply unchanged.
Place these resets after the @supports block in source order, or repeat them inside their own
@supports guard, so specificity/order lets mobile win.
Verification
cd frontend && npm run dev, open the Dashboard Overview tab with the demo workspace (demo@example.com) — delegate the browser pass to a Sonnet subagent (never drive Chrome from the coordinator).- Check: every year's track left and right edge align to the pixel, including 2029 (shortest
figures) vs 2025 (no expiring) vs a row with
expiring; the "PL left" and "Vacation left" labels form straight columns. - Repeat in both Swedish and English (longer sv strings are the real stress test), and in a solo workspace (no parent names) as well as a couple workspace.
- Narrow to the mobile breakpoint and confirm figures still wrap under the track, one parent per
line (use the same-origin iframe trick —
resize_windowis a no-op in this setup). - Gates (delegate to a Haiku subagent, raw output): from
frontend/npm run typecheck,npm run lint,npm run coverage. CSS-only change —YearRows.test.tsxandDashboard.test.tsxshould be untouched and green.