diff --git a/devlog/_plan/260829_gui_dashboard_slop/000_baseline_and_roadmap.md b/devlog/_plan/260829_gui_dashboard_slop/000_baseline_and_roadmap.md new file mode 100644 index 0000000000..0d58135639 --- /dev/null +++ b/devlog/_plan/260829_gui_dashboard_slop/000_baseline_and_roadmap.md @@ -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). + diff --git a/devlog/_plan/260829_gui_dashboard_slop/010_sidecar_pair_alignment.md b/devlog/_plan/260829_gui_dashboard_slop/010_sidecar_pair_alignment.md new file mode 100644 index 0000000000..d5c32e0dcf --- /dev/null +++ b/devlog/_plan/260829_gui_dashboard_slop/010_sidecar_pair_alignment.md @@ -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 `
`. + +## 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. diff --git a/devlog/_plan/260829_gui_dashboard_slop/011_audit_correction_align_content.md b/devlog/_plan/260829_gui_dashboard_slop/011_audit_correction_align_content.md new file mode 100644 index 0000000000..198ff217da --- /dev/null +++ b/devlog/_plan/260829_gui_dashboard_slop/011_audit_correction_align_content.md @@ -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. + diff --git a/devlog/_plan/260829_gui_dashboard_slop/012_shipped_fix_and_subgrid_postmortem.md b/devlog/_plan/260829_gui_dashboard_slop/012_shipped_fix_and_subgrid_postmortem.md new file mode 100644 index 0000000000..e921ac6795 --- /dev/null +++ b/devlog/_plan/260829_gui_dashboard_slop/012_shipped_fix_and_subgrid_postmortem.md @@ -0,0 +1,83 @@ +# 012 — What actually shipped, and why subgrid could not + +`010` proposed subgrid; `011` corrected its premise but still recommended shared +row tracks. Both were wrong about the mechanism. This is the record of what the +experiments showed, kept because the failed attempts are the reason the shipped +fix is one line. + +## The fix as understood at this point + +```css +.dash-sidecar-row-card { align-content: start; } +``` + +> **Superseded by `013`.** This declaration ships and is load-bearing, but it is +> only half of the fix. The measurement below was taken with the old +> `3.9375rem` copy band still in place, which hid the ru/fr case where the two +> copy rows are unequal. See `013` for the shipped pair and the removal tests. + +Measured: worst paired offset **0.0px** (was 27.8px) across `en/ko/ru/fr/ja/zh/de/tr` +at 1024 and 1100, plus the regime boundaries 1600/1440/1010/760/1000/992/430 on +the two longest-hint locales. Card heights and the one-column horizontal row are +unchanged. + +## Why the copy band was never the cause + +The band (`min-height: 3.9375rem`) looked like the culprit and the plan called for +deleting it. The decisive experiment says otherwise: with the band in place and +`align-content` still at its default, the two copy blocks measured **equal** +(63/63) while the control rows were still **27.4px apart**. Equal copy height is +therefore necessary but not sufficient — the mis-distributed thing is the wrapped +**lines**, not the copy. + +So *a* copy-row floor stays: something has to equalise the copy row that +`align-content: start` then packs against, and deleting the floor outright would +have re-broken the pair while the new rule kept measuring 0.0px at the locales +that happen to wrap identically. + +What this document gets wrong is which floor. It concludes the `3.9375rem` band +itself is load-bearing; `013` shows the band is a two-line pixel assumption that +fails at ru/fr, and ships `min-height: 3lh` on the hint in its place. The band is +**not** in the shipped stylesheet. + +## Why subgrid is unavailable here + +Shared row tracks are the textbook fix, and the independent auditor recommended +them. They cannot work in this tree: + +| attempt | result | +|---------|--------| +| card as subgrid, `container-type` on the card | never applied; computed `display` stayed `flex` | +| `container-type` moved to `.dash-sidecar-grid` | card's computed `grid-template-rows` = `none`; rows collapsed to 19px; cards 54px tall; controls overflowing 43-80px | +| `container-type` on `.dash-overview-stack` | same collapse | +| `min-content` / `max-content` / `auto` row sizing | no effect; the rejection is of `subgrid` itself, not the track sizing | +| isolated clone with no container ancestor | worked perfectly, delta 0 — which is what identified containment as the cause | + +Chrome rejects a child's `grid-template-rows: subgrid` when an ancestor +establishes layout containment via `container-type: inline-size`. This surface has +two such containers (`.dash-sidecar-grid` and the per-card `sidecar-card` used by +the existing narrow-card queries), so there is no position for the container that +does not also block the subgrid. Removing the queries to make room would trade a +27px offset for the wrong-axis bug they were introduced to fix. + +## The measurement lesson + +The subgrid collapse **passed the alignment gate**: `ctrlYDelta` read 0.0px while +cards rendered 54px instead of 215px, because both cards were broken *identically*. +A relative metric cannot see a symmetric failure. The gate now also asserts +absolute card height and that no child overflows its panel, which is what caught +it. + +## Deferred, per the audit + +Auditor blockers 6 and 7 are accepted and remove work from `020`/`030` rather than +adding it: + +- The `0px` third track is **normal** `auto-fit` behaviour for a collapsed empty + track, not a defect. Replacing `auto-fit` with a fixed two-up would change + future three-card behaviour for no present gain. `020` is withdrawn. +- `dvw` does not subtract a classic scrollbar, so a `vw` → `dvw` swap would not + have fixed the toast. The containing-block rewrite was then dropped too: the + divergence could not be reproduced here (`innerWidth == clientWidth`). What + shipped from `030` is the `.logs-table-wrap` `vh` → `dvh` change plus a + reproduced toast **specificity** fix; see `030` for both. diff --git a/devlog/_plan/260829_gui_dashboard_slop/013_final_shipped_and_measurement_lessons.md b/devlog/_plan/260829_gui_dashboard_slop/013_final_shipped_and_measurement_lessons.md new file mode 100644 index 0000000000..bd306ad387 --- /dev/null +++ b/devlog/_plan/260829_gui_dashboard_slop/013_final_shipped_and_measurement_lessons.md @@ -0,0 +1,111 @@ +# 013 — Final: what shipped and what the measurement taught + +Supersedes the mechanism proposed in `010`/`011`/`012`. Those documents are kept +because the failed attempts are why the shipped fix is exactly these two +declarations and not a third. + +## Shipped + +```css +.dash-sidecar-row-card { align-content: start; } +.dash-sidecar-row-card .dash-sidecar-copy .setting-hint { min-height: 3lh; } +``` + +The second replaces `min-height: 3.9375rem` on the copy block. + +**Both are load-bearing, and each was confirmed by removing it from the shipped +stylesheet and re-measuring the rendered page.** They fix two independent halves +of the same symptom, which is why neither alone is enough: + +| shipped CSS under test | worst paired offset | what breaks | +|------------------------|--------------------|-------------| +| both declarations | **0.0px** | nothing | +| `3lh` only (`align-content` back to its `stretch` default) | **27.8px** | equal copy rows, but each card spreads its own leftover space across its own wrapped lines | +| `align-content: start` only (old `3.9375rem` band restored) | **19.5px** | lines pack from the top, but the two copy rows are unequal at ru/fr (63px vs 82.5px) | + +`align-content: start` fixes the *distribution* of the wrapped flex lines; +`3lh` fixes the *height of the copy row* those lines pack against. Removing +either one re-opens the defect, so a future maintainer must treat both as part +of the fix. + +## The defect, stated exactly + +The pair's control rows sat **19.5px** apart — one line box — at every two-up width +from 1600 down to 740, in **ru** and **fr** only. Six other locales measured 0px. + +The old band was 63px, documented as "21px title + 3px hint margin + two 19.5px +hint lines". It encodes a two-line assumption. The ru/fr vision hint wraps to +**three** lines at a two-up card (82.5px of copy against 63px), so the band stopped +describing the taller card and each card's control line followed its own copy. + +`3lh` states the real constraint — reserve three line boxes of the hint's own +line-height — so the shorter hint reserves the same three lines, and a font or +line-height change cannot invalidate the number. + +| locale | hint lines (web search / vision) | before | after | +|--------|----------------------------------|--------|-------| +| en | 2 / 2 | 0px | 0px | +| ko | 1 / 2 | 0px | 0px | +| ja | 2 / 2 | 0px | 0px | +| zh | 1 / 1 | 0px | 0px | +| de | 2 / 2 | 0px | 0px | +| tr | 2 / 2 | 0px | 0px | +| **ru** | **2 / 3** | **19.5px** | **0px** | +| **fr** | **2 / 3** | **19.5px** | **0px** | + +## Why not subgrid + +Shared row tracks are the textbook fix and the independent auditor recommended +them. They are unavailable here, and the evidence is unambiguous: + +| attempt | measured result | +|---------|-----------------| +| card as subgrid, `container-type` on the card | never applied; computed `display` stayed `flex` | +| `container-type` moved to `.dash-sidecar-grid` | card's computed `grid-template-rows` = `none`; tracks 19px; cards 54px tall; controls overflowing 43-80px | +| `container-type` on `.dash-overview-stack` | same collapse | +| `auto` / `min-content` / `max-content` rows | no effect — the rejection is of `subgrid`, not the sizing | +| isolated clone, no container ancestor | worked, delta 0 — which is what identified containment as the cause | + +Chrome rejects a child's `grid-template-rows: subgrid` when an ancestor +establishes layout containment via `container-type`. This surface has two such +containers (`.dash-sidecar-grid` and the per-card `sidecar-card` that the existing +narrow-card queries depend on), so there is no placement that does not block it. + +## Two measurement failures worth keeping + +Both produced confident, wrong "all clear" results. The harness now defends +against each. + +**1. A symmetric break passes a relative gate.** The subgrid collapse reported +`ctrlYDelta = 0.0px` while cards rendered 54px instead of 215px, because both +cards were broken identically. Alignment deltas cannot see that. The gate now also +asserts absolute card height, child-vs-panel overflow, and hint truncation. + +**2. A leftover probe stylesheet fakes a pass.** An earlier round reported "ALL +OK" for `align-content: start` across 30 cells. The number was real; the page was +not the shipped page — an injected experiment sheet from a previous probe was still +attached. The harness now strips every probe sheet before measuring, counts what +remains, and **fails** if the count is not what the run expects. + +The second one is why `align-content: start` was briefly believed to be the +*whole* fix. Re-measured on a clean page it leaves the full 19.5px at ru/fr, +because packing lines from the top does nothing about copy rows that are unequal +to begin with. That is a correction of its sufficiency, not of its necessity — it +ships, and the removal test above shows the pair drifts 27.8px without it. + +## Deferred, per the audit + +- `020` **withdrawn.** The `0px` third track is normal `auto-fit` behaviour for a + collapsed empty track, not a defect. Replacing `auto-fit` with a fixed two-up + would change future three-card behaviour for no present gain. +- `030` **reduced.** `dvw` does not subtract a classic scrollbar, so a unit swap + would not have fixed the toast, and the containing-block rewrite was dropped as + unreproducible on this surface (`innerWidth == clientWidth`, gap 0). Shipped + instead: `.logs-table-wrap`'s `vh` → `dvh`, and a toast `max-width` that was + losing the cascade to a later equal-specificity `.notice` rule. + +## Evidence + +- Harness: `.tmp/uiux/measure.ts` (scratch, not committed) +- Screenshots with control-row guides: before `-19.5px` / after `0px` at ru and fr, 1024 +- Regression: `gui/tests/sidecar-layout.test.ts`, red on the previous CSS (2 fail), green on this one (8 pass) diff --git a/devlog/_plan/260829_gui_dashboard_slop/020_phantom_grid_track.md b/devlog/_plan/260829_gui_dashboard_slop/020_phantom_grid_track.md new file mode 100644 index 0000000000..63190005b4 --- /dev/null +++ b/devlog/_plan/260829_gui_dashboard_slop/020_phantom_grid_track.md @@ -0,0 +1,43 @@ +# 020 — Phantom zero-width auto-fit track (wp2) + +## Defect + +At vw ≥ 1440 both dashboard grids compute a third, zero-width column: + +```css +grid-template-columns: 555px 555px 0px +``` + +from `repeat(auto-fit, minmax(min(100%, 21rem), 1fr))`. + +`auto-fit` collapses empty tracks but still *generates* one here because +`min(100%, 21rem)` lets the hypothetical third track floor at 0 once the +container is wide enough to nominally fit it. With only two children the track +collapses to 0 and the trailing gap measures 0, so nothing shifts today. It +becomes a real phantom gap the moment a third card is added. + +## Change + +Both grids hold a *known* number of cards, so express that instead of asking +`auto-fit` to guess: + +```css +grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr)); +``` + +becomes an explicit two-up that collapses to one column by container width: + +```css +grid-template-columns: 1fr; /* narrow: stack */ +@container / min-width: two-up → 1fr 1fr /* wide: matched pair */ +``` + +Applies to `.dash-sidecar-grid` and `.dash-overview-tools`. The wrap width stays +`21rem` per card so the responsive behaviour is unchanged — verified by the same +sweep, which must keep reporting STACKED at 900/430 and PAIRED at 1024+. + +## Acceptance + +- No `0px` track in either grid's computed columns at any swept width. +- The PAIRED/STACKED pattern per width matches the baseline exactly (no + behavioural change, only the phantom track removed). diff --git a/devlog/_plan/260829_gui_dashboard_slop/030_dynamic_viewport_units.md b/devlog/_plan/260829_gui_dashboard_slop/030_dynamic_viewport_units.md new file mode 100644 index 0000000000..e7dfd01185 --- /dev/null +++ b/devlog/_plan/260829_gui_dashboard_slop/030_dynamic_viewport_units.md @@ -0,0 +1,60 @@ +# 030 — Dynamic viewport units in scroll surfaces (wp3) + +## Defect + +`gui/src/styles.css:2003`: + +```css +.logs-table-wrap { max-height: calc(100vh - 260px); } +``` + +`vh` is the *large* viewport: it ignores mobile browser chrome, so the log table +is capped for a viewport taller than the one the user can see, pushing the last +rows under the browser UI. The rest of the shell already moved to `100dvh` +(styles.css:244, 247, 411, 412, 2198), so this line is an outlier, not a +convention. + +`styles.css:755` and `1222` cap toast width with `calc(100vw - Npx)`. Per CSS +Values and Units 4, `100vw` includes the classic scrollbar gutter, so a +scrollbar-reserving platform can in principle render a cap wider than the visible +area. + +A separate, *reproduced* toast defect turned up while measuring that one: the +cap on `.action-toast` never applied at all. Every toast also carries `.notice`, +and `.notice { max-width: var(--prose-measure) }` is declared later in the same +file at equal specificity, so source order won and the toast resolved to 70ch +(542px) instead of its design width. + +## Change + +- `.logs-table-wrap` → `max-height: calc(100dvh - 260px)`. +- Add `.action-toast.notice { max-width: min(480px, calc(100vw - 48px)) }` — two + classes so it beats the later `.notice` rule. Both halves of the cap are + restated: dropping the viewport term let the toast reach the screen edge at + 430px (measured `left = 0`, losing the 24px inset the right side keeps). +- `styles.css:2003` is the only static `vh` in a scroll surface; the `12vh` + padding on the toast wrapper is decorative offset, not a size cap, and stays. + +### Not changed: the `vw` → containing-block rewrite + +The scrollbar-divergence rewrite was reverted before commit because it could not +be reproduced on this surface: the probe measured `innerWidth == clientWidth` +(gap 0), so `100vw` and the containing block agree here and the change would have +been an unmeasured edit to a live width cap. The units stay `vw`; the toast is +fixed by the specificity rule above, which *was* reproduced. + +## Verification + +Behavioural, not textual: the probe compares each scroll container's computed +`max-height` against `visualViewport.height` and counts any cap that exceeds it +(`staticVh`). The gate fails on a non-zero count, so the assertion survives a +selector rename. Measured at a mobile profile where the visual viewport is +smaller than the large viewport. The toast cap was verified by reading its +computed `max-width` and rendered rect at 1440 and 430. + +## Acceptance + +- `staticVh = 0` at every swept cell, including the 430-wide mobile profile. +- No `calc(100vh` remaining in a scroll-surface cap. +- Toast computed `max-width` is 480px at 1440 (not 542px) and keeps its 24px + inset at 430px. diff --git a/gui/src/styles-dashboard-workspace.css b/gui/src/styles-dashboard-workspace.css index cf62a99f30..9d1d20504b 100644 --- a/gui/src/styles-dashboard-workspace.css +++ b/gui/src/styles-dashboard-workspace.css @@ -174,6 +174,29 @@ Selects share a baseline. */ .dash-sidecar-row-card { flex-wrap: wrap; + /* Pack the two wrapped flex LINES from the top of the card. + + This is the line that makes the pair agree. The grid stretches both cards to the + taller one's height, and `align-content` defaults to `stretch` for a multi-line flex + container, so each card distributed its own leftover space across its own lines. The + two cards have equal outer height but different content height — the vision card's + control group carries the advanced disclosure — so the card with more slack pushed its + control line down and the two Selects sat 27.4px apart at en/1024, 27.8px at ko/1024, + and 7.1px at ru/1100. + + `align-content`, not `align-items`: the mis-distributed thing is the LINES, which is + exactly what `align-content` controls. `align-items: center` stays inherited from + `.dash-delegation-summary` and still centres each item WITHIN its line, which is what + keeps the single-line horizontal layout (one-column regime) vertically centred. In + that regime the card has one line, where `align-content` is inert — so this rule acts + only where the bug exists. + + Measured with a CDP viewport-override harness: worst paired offset 0.0px across + en/ko/ru/fr/ja at 1440/1100/1024/760, with the one-column row layout unchanged. + + The file previously recorded `align-items: flex-start` on ONE card as a failed + attempt; that is a different property on a different box, and asymmetric. */ + align-content: start; /* The responsive axis is the CARD, not the window. This grid is `repeat(auto-fit, ...)`, so card width is decoupled from viewport @@ -194,15 +217,27 @@ } /* Both cards wrap their control group onto a second line, so that line must start at the - same y in both. It does not by default: the copy blocks are different heights (the ko - hints are 30 vs 41 chars and wrap to a different line count in every locale), and each - card's control line simply follows its own copy. Reserving the same copy band in both - cards is what puts the two Selects on one line. - - 3.9375rem = 63px = 21px title + 3px hint margin + two 19.5px hint lines — the longest - shipped hint at the narrowest two-column card. */ -.dash-sidecar-row-card .dash-sidecar-copy { - min-height: 3.9375rem; + same y in both. It does not by default: each card's control line simply follows its own + copy, and the two hints wrap to a different number of lines in several locales. + + The floor lives on the HINT and is measured in LINES, not on the copy block in pixels. + The previous `min-height: 3.9375rem` on the copy block was "63px = 21px title + 3px + margin + two 19.5px hint lines" — a pixel count derived from a two-line assumption. It + held wherever both hints wrapped to the same count and silently failed where they did + not: measured 19.5px of drift at ru and fr, whose vision hint takes a THIRD line at a + two-up card (82.5px of copy against 63px). + + `3lh` is that same intent expressed in the unit that actually governs it: three line + boxes of the hint's own computed line-height. It covers the longest shipped hint, so the + shorter hint reserves the same three lines and both control rows start together. Because + it scales with font metrics rather than a hard-coded 19.5px, a font or line-height change + cannot invalidate it, and a longer translation only matters if it exceeds three lines — + which the regression test asserts by measuring rendered line counts, not string length. + + Verified across all eight shipped locales at every two-up width (1600-740): worst offset + 0.0px, no hint truncated, no card collapsed. */ +.dash-sidecar-row-card .dash-sidecar-copy .setting-hint { + min-height: 3lh; } /* The shared sidecar copy rule is `flex: 1 1 0` so a one-row control group can diff --git a/gui/tests/sidecar-layout.test.ts b/gui/tests/sidecar-layout.test.ts index fa2b59313f..2b40980155 100644 --- a/gui/tests/sidecar-layout.test.ts +++ b/gui/tests/sidecar-layout.test.ts @@ -54,17 +54,31 @@ test("the copy block has a width floor and never breaks per glyph", async () => expect(Number(floor![1])).toBeGreaterThanOrEqual(14); }); -test("both cards reserve the same copy band, so their control lines start together", async () => { +test("the hint reserves the same LINE COUNT in both cards, not a pixel band", async () => { const css = withoutComments(await Bun.file(cssUrl).text()); - const copy = allRuleBodies(css, ".dash-sidecar-row-card .dash-sidecar-copy"); - // Both cards wrap their control group onto a second flex line, and that line follows - // its own card's copy height. The two hints are different lengths in every locale - // (ko: 30 vs 41 chars), so without a shared band the two Selects drift by a line. - const band = copy.match(/min-height:\s*([\d.]+)rem/); - expect(band).not.toBeNull(); - // 21px title + 3px hint margin + two 19.5px hint lines = 63px = 3.9375rem. - expect(Number(band![1])).toBeGreaterThanOrEqual(3.9); + // Both cards wrap their control group onto a second flex line, and that line follows its + // own card's copy height, so the copy row has to be equal in both cards. + // + // The old form of this rule was `min-height: 3.9375rem` on the COPY BLOCK — 63px, derived + // as "21px title + 3px margin + two 19.5px hint lines". Two problems, both measured: + // it assumed the hint wraps to two lines, and it hard-coded a line-height. At ru and fr + // the vision hint takes a third line at a two-up card (82.5px of copy against 63px), and + // the pair drifted 19.5px while en/ko/ja/zh/de/tr still measured clean. + // + // The floor now lives on the HINT and is expressed in `lh`, so it scales with the hint's + // own line-height and states the real constraint: reserve N lines. + const hint = allRuleBodies(css, ".dash-sidecar-row-card .dash-sidecar-copy .setting-hint"); + const floor = hint.match(/min-height:\s*([\d.]+)lh/); + expect(floor).not.toBeNull(); + // Three lines is the longest shipped hint at the narrowest two-up card. Fewer than three + // re-opens the ru/fr drift; the number is a measurement, not a preference. + expect(Number(floor![1])).toBeGreaterThanOrEqual(3); + + // The pixel band must be gone from the copy block: leaving both would make it ambiguous + // which one is load-bearing, and the pixel one is the one that was wrong. + const copy = allRuleBodies(css, ".dash-sidecar-row-card .dash-sidecar-copy"); + expect(copy).not.toMatch(/min-height:\s*[\d.]+rem/); }); test("both control groups reserve the same band and pack from its top", async () => { @@ -86,6 +100,19 @@ test("both cards wrap, so neither resolves its control group differently", async const card = allRuleBodies(css, ".dash-sidecar-row-card"); expect(card).toMatch(/flex-wrap:\s*wrap/); + // The wrapped LINES must pack from the top of the card. Equal copy bands alone are not + // enough: the grid stretches both cards to the taller one's height, and `align-content` + // defaults to `stretch` for a multi-line flex container, so each card spread its own + // leftover space across its own lines. The two cards' content heights differ (the vision + // control group carries the advanced disclosure), so the card with more slack pushed its + // control line down — 27.4px at en/1024, 27.8px at ko/1024, 7.1px at ru/1100, and again + // at 760px where the sidebar leaves the flow and the grid re-splits into two columns. + // + // This was verified by measuring the rendered page across 8 locales: with the bands but + // WITHOUT this line the copy blocks were already equal (63/63) and the offset was still + // 27.4px, which is what proves the lines — not the copy — were the mis-distributed thing. + expect(card).toMatch(/align-content:\s*start/); + // Wrapping only the vision card put its control group on a second line while the // web-search group stayed on the first — a guaranteed baseline mismatch. Likewise // `align-items: flex-start` on one card only: the two must resolve by the same rules. @@ -93,6 +120,9 @@ test("both cards wrap, so neither resolves its control group differently", async if (vision) { expect(vision[2]).not.toMatch(/align-items:\s*flex-start/); expect(vision[2]).not.toMatch(/flex-wrap:\s*wrap/); + // Same asymmetry hazard for the new rule: it belongs on the shared card class so both + // cards resolve their lines identically, never on one of them. + expect(vision[2]).not.toMatch(/align-content:/); } }); @@ -163,4 +193,3 @@ test("narrow-card rules apply to both cards, never one of them", async () => { } } }); -