diff --git a/new-features/alignment-plan.md b/new-features/alignment-plan.md new file mode 100644 index 0000000..daebbde --- /dev/null +++ b/new-features/alignment-plan.md @@ -0,0 +1,116 @@ +# Align the year-row timelines in the Overview tab + +## Context + +In the Dashboard Overview tab each year is a `
` whose +`` 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: + +```css +.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: + +```css +.year-row { /* the
*/ + 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-4` column-gap must be declared on `.year-rows` as well, since a subgrid inherits the + parent's gaps unless it sets its own; keep the summary's `column-gap` so the values agree. +- `.year-rows > .disclosure:last-child { border-bottom: none }` and the `.disclosure` hairlines + in `primitives.css` are unaffected (borders on the `details` still paint). +- In the solo case `.year-row__name` isn't rendered; that column collapses to `0` for every row — + still uniform. +- Left-over `grid-row: 1` declarations 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: + +```css +@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 + +1. `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). +2. 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. +3. 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. +4. Narrow to the mobile breakpoint and confirm figures still wrap under the track, one parent per + line (use the same-origin iframe trick — `resize_window` is a no-op in this setup). +5. Gates (delegate to a Haiku subagent, raw output): from `frontend/` `npm run typecheck`, + `npm run lint`, `npm run coverage`. CSS-only change — `YearRows.test.tsx` and `Dashboard.test.tsx` + should be untouched and green. diff --git a/timeline-offset.png b/timeline-offset.png new file mode 100644 index 0000000..c4f36a6 Binary files /dev/null and b/timeline-offset.png differ