Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions devlog/_plan/260829_gui_dashboard_slop/000_baseline_and_roadmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# 000 — Dashboard sidecar pair alignment and GUI polish

Reported: the dashboard at `#dashboard` "슬롭이 났다" — components misaligned
horizontally and vertically, and wrong under dynamic viewports.

This unit fixes what is *measured*, not what is asserted. Every defect below has
a numeric baseline captured with a CDP geometry harness that overrides the
viewport (`Emulation.setDeviceMetricsOverride`, dpr 2) and reads live
`getBoundingClientRect` values, so a claim can be re-checked rather than
re-argued.

## Harness and why it is trustworthy

Two harness bugs were found and fixed *before* any defect was accepted, because
each one silently manufactured agreement:

1. **Viewport lag.** The first sweep measured a cell before the emulated
viewport had actually resized, so rows reported the *previous* width
(`ru/w1100` measured `vw=1440`). Fixed with a settle loop that requires
`innerWidth === target` before measuring, and the verdict tool now fails on
any unsettled cell.
2. **Locale never applied.** `Page.navigate` to a URL differing only in its
hash does not reload the document, so all four locales measured byte-identical
geometry — an invalid multi-locale claim that *looked* like passing evidence.
Fixed with an explicit `Page.reload` plus a settle condition on
`document.documentElement.lang`. The probe now records the rendered hint
length per card, and the verdict tool **fails** when every locale reports the
same signature. Post-fix signatures: `en=66,81 ko=30,41 ru=82,114 fr=88,111`.

The second bug is the important one: without it, this unit would have "verified"
the locale dimension while never rendering a non-English string.

## Defect 1 — the sidecar pair loses its shared control row (TOP PRIORITY)

`.dash-sidecar-row-card` (web search) and `.dash-vision-sidecar-card` are
documented in the stylesheet as a *matched pair* whose first `Select` must land
on one line. Measured `ctrlYDelta` (vertical offset between the two control
rows):

| locale | 1440 | 1100 | 1024 |
|--------|------|------|------|
| en | 0.1 | 0.1 | **27.4** |
| ko | 0.4 | 0.4 | **27.8** |
| ru | 0.1 | **7.1** | **22.6** |
| fr | 0.1 | **2.3** | **22.6** |

### Root cause

Both cards are `flex-wrap: wrap` with `align-items: center`, and the grid
stretches them to equal height. Below ~`36rem` of *card* width the container
query gives copy and controls `flex-basis: 100%`, so each card becomes two
wrapped flex lines. The two cards then have **equal outer height but different
content height** — vision's control column is taller (select row + advanced
disclosure). Flexbox distributes the leftover space of each card independently
and `align-items: center` centres each line within its own leftover, so the
shorter card's control row sinks by half the difference. Nothing ties one card's
second line to the other's.

The shipped mitigation is a hard-coded reserved band:

```css
.dash-sidecar-row-card .dash-sidecar-copy { min-height: 3.9375rem; }
```

`3.9375rem` = 63px = "21px title + 3px hint margin + two 19.5px hint lines".
That number only holds while *both* hints wrap to at most two lines. It is the
third attempt at this alignment recorded in the file — after wrapping only one
card, then `align-items: flex-start` on one card — each adding a magic number
instead of removing the cause. The ru/fr breakage at 1100px is the band failing
exactly as predicted: longer hints take a third line, overflow the band, and the
pair desynchronises at a width where English still looks fine.

### Fix direction

Align the rows *structurally* so no number has to be maintained: the pair shares
one row grid, and each card's copy row and control row are placed into shared
tracks. Then alignment holds for any hint length in any locale, and the band can
be deleted rather than re-tuned. The existing comment correctly warns that
`container-type` layout containment blocks `subgrid` from reading parent
tracks, so the container query must not sit on a subgrid participant.

## Defect 2 — phantom zero-width grid track

At vw ≥ 1440, `.dash-sidecar-grid` and `.dash-overview-tools` compute
`grid-template-columns: 555px 555px 0px`. `repeat(auto-fit, minmax(min(100%, 21rem), 1fr))`
emits a third, zero-width track. Trailing gap measures 0 today, so nothing
visibly shifts — but the track is real, and it becomes a phantom gap the moment a
third card is added to either grid.

## Defect 3 — static viewport units in scroll surfaces

`gui/src/styles.css:2003` caps `.logs-table-wrap` with
`max-height: calc(100vh - 260px)`. Static `vh` resolves against the *large*
viewport, ignoring mobile browser chrome, while the rest of the shell already
uses `100dvh` (styles.css:244, 247, 411, 412, 2198). The log table is therefore
sized for a viewport the user cannot see. `styles.css:755` and `1222` cap toast
width with `calc(100vw - Npx)`, which ignores classic scrollbar width.

The probe measures this behaviourally — comparing each scroll container's
computed cap against `visualViewport.height` — rather than grepping for the
unit, so the assertion survives a refactor.

## Work phases

| phase | doc | deliverable |
|-------|-----|-------------|
| wp0 | this unit | measured baseline + roadmap |
| wp1 | `010` | sidecar pair structural alignment (top priority) |
| wp2 | `020` | phantom auto-fit track |
| wp3 | `030` | dynamic viewport units |

Acceptance for every implementation phase: `ctrlYDelta ≤ 1px` and
`heightDelta ≤ 1px` while paired, no horizontal overflow, no zero-width track,
no scroll cap exceeding the visual viewport, across
`1440/1100/1024/900/430` × `en/ko/ru/fr`, with locale signatures proven
distinct.

## Constraints

- The local suite is not run here (user instruction). Gates run remotely via
`ssh lidge` + `ocx-run`; pushes use `--no-verify` only after those gates.
- Delivery is a stacked PR chain onto `dev`, each PR carrying screenshots
(`enforce-target` requires a screenshot for GUI PRs).

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 010 — Sidecar pair: structural row alignment (wp1, TOP PRIORITY)

Removes the magic reserved band and makes the two sidecar cards share real row
tracks, so their control rows align for any hint length in any locale.

## Current shape

```text
.dash-sidecar-grid grid, auto-fit 2 tracks, align-items: stretch
└─ .panel.dash-delegation-summary.dash-sidecar-row-card
├─ .dash-sidecar-copy (title + hint)
└─ .dash-delegation-controls (selects / switch / disclosure)
```

Each card is its own flex container (`flex-wrap: wrap`, `align-items: center`)
and, below `36rem` of card width, both children take `flex-basis: 100%` — two
wrapped lines whose position depends only on that card's own leftover space.

## Change

Make each card a two-row grid and let both cards inherit the *same* two rows from
the pair grid:

1. `.dash-sidecar-grid` gains `grid-template-rows: auto auto` so there are named
parent rows to inherit.
2. `.dash-sidecar-row-card` becomes `display: grid` with
`grid-template-rows: subgrid` spanning both rows, so copy lands in row 1 and
controls in row 2 **in both cards**. Row 1 is sized by the taller of the two
copy blocks, automatically — which is exactly what the 63px band was
hand-computing.
3. Delete `.dash-sidecar-row-card .dash-sidecar-copy { min-height: 3.9375rem }`
and the `min-height: 3.6875rem` band on `.dash-delegation-controls`. They are
the numbers being replaced.
4. Move `container-type: inline-size` **off** the subgrid participant. Layout
containment blocks a subgrid from reading parent tracks (the stylesheet already
warns about this). The container is re-established on a wrapper so the existing
`@container sidecar-card` rules keep working unchanged.

## Wrapper

Subgrid requires the card to be a grid *item* of the pair grid, but the card also
has to be the container query root's child. Structure becomes:

```text
.dash-sidecar-grid (grid, 2 rows)
└─ .dash-sidecar-cell (container-type: inline-size, display: grid, rows: subgrid, span 2)
└─ .dash-sidecar-row-card (display: grid, rows: subgrid, span 2)
```

The cell carries the container query; the card carries the visible panel styling.
Both pass the rows through, so row 1 and row 2 are shared across the pair.

Requires one JSX change in `dashboard-overview-sections.tsx`: wrap each of the
two existing card `div`s in `<div className="dash-sidecar-cell">`.

## Stacked state

When the container query stacks a card (card narrower than `22rem`), the two
cards are in *different* grid columns of a single-column grid — i.e. different
rows of the pair — so cross-card alignment is meaningless and must not be
asserted. The verdict tool already treats `sameRow: false` as `STACKED` and
skips the delta check.

## Fallback

`grid-template-rows: subgrid` is supported in Chrome 117+, Safari 16+, Firefox
71+. Guard with `@supports (grid-template-rows: subgrid)`; without support the
cards keep the current flex row behaviour, which is the shipped status quo rather
than a regression. The deleted bands are restored inside the negative branch so
unsupported browsers keep today's approximation.

## Acceptance

- `ctrlYDelta ≤ 1px` and `heightDelta ≤ 1px` at every PAIRED cell across
`1440/1100/1024` × `en/ko/ru/fr` (baseline: up to 27.8px).
- No `min-height` band remains on `.dash-sidecar-copy`.
- Locale hint signatures distinct, no unsettled cells.
- A focused GUI test asserts the subgrid contract so a future edit that reverts
to the band fails.
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# 011 — Audit correction: subgrid was the wrong fix

The `010` plan was written before the regime sweep existed. The sweep contradicts
its central assumption, so `010` is superseded by this document. Recorded rather
than silently edited, because the wrong assumption is the interesting part.

## What `010` assumed

That the cards render as a horizontal row at desktop width (copy LEFT, controls
RIGHT) and only stack when narrow — so a two-row subgrid was needed to align the
"second line" of each card.

## What the measurement shows

Per-card internal layout, at full reload per width, sidebar state recorded:

| vw | mainInner | cardW | columns | inside the card | ctrlYDelta |
|------|-----------|-------|---------|-----------------|------------|
| 1600 | 1128 | 556 | 2 | STACKED | 0 |
| 1440 | 1126 | 555 | 2 | STACKED | 0 |
| 1280 | 966 | 475 | 2 | STACKED | 0 |
| 1100 | 786 | 385 | 2 | STACKED | -2.3 |
| 1024 | 782 | 347 | 2 | STACKED | **22.5** |
| 1010 | 768 | 340 | 2 | STACKED | **22.5** |
| 1000 | 758 | 686 | 1 | ROW | n/a (single column) |
| 992 | 750 | 678 | 1 | ROW | n/a |
| 980 | 738 | 666 | 1 | STACKED | n/a |
| 768 | 526 | 454 | 1 | STACKED | n/a |
| **760** | **750** | **349** | **2** | STACKED | **22.5** |
| 740 | 730 | 339 | 2 | STACKED | **22.5** |
| 720 | 710 | 674 | 1 | ROW | n/a |

Two facts kill `010`:

1. **The cards are ALREADY stacked internally at every two-column width.** The
`36rem` container query fires whenever the pair is side by side, because a
two-up card is at most ~556px = 34.75rem < 36rem. Copy and controls are already
on separate lines; there is no row to preserve and nothing for a two-row
subgrid to add. The horizontal row only appears when the grid collapses to ONE
column (cardW ≈ 674-686px > 36rem), and in that state the cards are stacked
vertically as a pair, so cross-card alignment is meaningless.
2. **A JSX wrapper would have been added for nothing**, and moving
`container-type` off the card would have silently killed the existing
`@container sidecar-card` rules — the exact "reads correct in review but does
nothing" failure the stylesheet already warns about.

## The real cause of the 22.5px offset

Both cards are stretched to equal height by the grid, and each is
`flex-wrap: wrap` + `align-items: center`. Two wrapped lines, equal outer
height, **different content height** (vision's control column is taller: select
row + 12px gap + the advanced disclosure). Flexbox gives each card its own
leftover space, and `align-items: center` centres each line inside its own
leftover. The card with less content has more leftover, so its control row sinks
by roughly half the difference. Nothing couples the two cards.

The 63px copy band mitigates this only while both hints wrap to the same number of
lines. At `ru`/`fr`, the vision hint takes a third line at 1100px, which is why
ru/fr break at a width where en/ko still measure clean.

## The fix

Pack the wrapped lines from the top of each card instead of centring them in
leftover space:

```css
.dash-sidecar-row-card { align-content: start; }
```

`align-content` is the correct property for a **multi-line** flex container — it
distributes the *lines*, which is exactly what is misdistributed here.
`align-items` (already `center` from `.dash-delegation-summary`) aligns items
*within* a line and must stay, so the single-line desktop row keeps its vertical
centring.

The stylesheet notes that `align-content` "has no effect on one line" — true, and
it is why `align-content` alone was rejected for the *control group*. But the
target here is the CARD, which genuinely has two lines in exactly the regime that
misaligns. In the one-column regime the card is a single line, where
`align-content: start` is inert and the row is unaffected. That is the property
doing precisely one job in precisely one regime.

With lines packed from the top, both control rows sit at
`padding-top + copyRowHeight`. Equal copy row height across the pair is then the
only remaining requirement, and it is what the `min-height` band already
provides — but now the band only needs to cover the *tallest actual* copy, and
alignment no longer depends on the two hints matching. The band is therefore
replaced by a locale-proof mechanism: the copy row's height is equalised by the
same `align-content` packing plus a shared floor expressed in line units
(`3lh`), not a pixel count derived from one locale's wrap count.

## Consequences for the existing test

`gui/tests/sidecar-layout.test.ts` currently asserts the magic band *as the
contract*:

- "both cards reserve the same copy band" requires `min-height >= 3.9rem` on the
copy block;
- "both control groups reserve the same band and pack from its top" requires
`min-height` and `align-items: flex-start` on the control group.

Those assertions encode the mitigation, not the requirement, so they must be
rewritten to assert the *cause* being removed (lines pack from the start; no
pixel-derived band is load-bearing). This is the file's stated purpose — "make the
specific CSS shape that caused the bug impossible to reintroduce" — applied to the
actual cause.

## Additional defect found by the sweep (new)

**The 760px two-column regression.** At `max-width: 760px` the sidebar leaves the
flow (`position: fixed`, off-canvas at `x=-280`), so `.main-inner` JUMPS from
526px to 750px. The sidecar grid re-splits into two columns at 349px each and the
22.5px misalignment returns — on tablet widths, below the width where it was last
believed fixed. Any fix must be verified at 760/740, not only at desktop widths.

Loading
Loading