From 10bc136b9cb827e6a0e588d65b1ece108f69d503 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 29 Aug 2026 22:50:04 +0900 Subject: [PATCH 1/3] docs(devlog): make the non-shipped notes say so, and fix EOF whitespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the remaining two points of @Ingwannu's review of dc78ceb7d. Point 1 (the 012/013 records contradicting the shipped CSS) was fixed in 1a7f22fe5. **Trailing blank lines.** `git diff --check` reported "new blank line at EOF" on `000_baseline_and_roadmap.md:124` and `011_audit_correction_align_content.md:115`. Both had two newlines at EOF; now one. The review said all seven files, but the other five already ended with a single newline — checked individually rather than rewriting files that were already correct. **020 and 030 now state their status in the first line the reader sees.** Neither ships in this commit, and that was only inferable from a "Deferred" section at the bottom of 013: - `020` is WITHDRAWN outright — the zero-width `auto-fit` track it reports is normal behaviour, no code change was made, and the document exists so the next person to measure it does not re-open it. - `030` is the plan for the stacked child PR's `gui/src/styles.css` change, not for the sidecar commit it sits beside. I kept both files here rather than moving them to the child, per the review's "either move to the child or make explicitly non-shipped": `020` has no child to move to (nothing implements it), and splitting `030` from the audit pass that found it would leave the unit's roadmap referring to a document in another branch. The banners make the boundary explicit instead. The two-file GUI fix is unchanged. Focused gate rerun on this head: `gui/tests/sidecar-layout.test.ts` 8 pass / 0 fail. --- .../260829_gui_dashboard_slop/000_baseline_and_roadmap.md | 1 - .../011_audit_correction_align_content.md | 1 - .../260829_gui_dashboard_slop/020_phantom_grid_track.md | 5 +++++ .../260829_gui_dashboard_slop/030_dynamic_viewport_units.md | 5 +++++ 4 files changed, 10 insertions(+), 2 deletions(-) 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 index 0d58135639..aaffa4e61e 100644 --- a/devlog/_plan/260829_gui_dashboard_slop/000_baseline_and_roadmap.md +++ b/devlog/_plan/260829_gui_dashboard_slop/000_baseline_and_roadmap.md @@ -121,4 +121,3 @@ distinct. `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/011_audit_correction_align_content.md b/devlog/_plan/260829_gui_dashboard_slop/011_audit_correction_align_content.md index 198ff217da..3c316777a6 100644 --- 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 @@ -112,4 +112,3 @@ 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/020_phantom_grid_track.md b/devlog/_plan/260829_gui_dashboard_slop/020_phantom_grid_track.md index 63190005b4..6ead7444cb 100644 --- a/devlog/_plan/260829_gui_dashboard_slop/020_phantom_grid_track.md +++ b/devlog/_plan/260829_gui_dashboard_slop/020_phantom_grid_track.md @@ -1,5 +1,10 @@ # 020 — Phantom zero-width auto-fit track (wp2) +> **WITHDRAWN — nothing in this document ships.** The investigation concluded the +> reported defect is not a defect: a collapsed zero-width `auto-fit` track is +> normal behaviour, and no code change was made for it. Kept as the record of why +> the track is expected, so the next person who measures it does not re-open it. + ## Defect At vw ≥ 1440 both dashboard grids compute a third, zero-width column: 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 index e7dfd01185..700e4d7b58 100644 --- a/devlog/_plan/260829_gui_dashboard_slop/030_dynamic_viewport_units.md +++ b/devlog/_plan/260829_gui_dashboard_slop/030_dynamic_viewport_units.md @@ -1,5 +1,10 @@ # 030 — Dynamic viewport units in scroll surfaces (wp3) +> **Implemented by this pull request**, separately from the sidecar alignment fix +> that the rest of this unit records. It is a different change to a different +> file (`gui/src/styles.css`), kept in the same unit because one audit pass found +> both. + ## Defect `gui/src/styles.css:2003`: From 36e90bb2d9f549123a75b924895feaed827bf98d Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 29 Aug 2026 22:16:18 +0900 Subject: [PATCH 2/3] fix(gui): cap the log table against the dynamic viewport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.logs-table-wrap` capped its scroll height with `calc(100vh - 260px)`. Static `vh` is the LARGE viewport, so on mobile the cap is computed for a viewport taller than the visible one and the last rows sit under the browser chrome. Now `100dvh`, which is what the rest of the shell already uses — `.app`, `.sidebar`, `.main-inner--combos` and the mobile drawer are all `100dvh`, so this line was the outlier rather than the convention. Also makes the corner toast's width cap actually apply. `.action-toast` sets `max-width: min(480px, calc(100vw - 48px))`, but every toast also carries `.notice`, which sets `max-width: var(--prose-measure)` (70ch = 542px) and is declared later in the file. At equal specificity source order won, so the toast's own cap never took effect and a long message rendered 542px wide instead of 480px. The cap is now `.action-toast.notice` — two classes — and restates both halves of the original expression, because dropping the viewport term let the toast run to the screen edge at narrow widths (measured left=0 at 430px against the 24px inset the right side keeps). Verified in a real browser by synthesizing both toasts and the log container: - corner toast with a long message: 480px at 1440, 382px at 430, 312px at 360 — identical to the previous rules at every width, with the 24px right inset intact and no overflow - log cap resolves to 540px at an 800px viewport, inside the visual viewport One thing I could not demonstrate and therefore did not change: the `calc(100vw - Npx)` terms are theoretically wrong because `vw` includes a classic scrollbar's width, but in this environment `innerWidth` and `clientWidth` never diverged (overlay scrollbars), so rewriting them against the containing block changed no measured geometry. Left as-is rather than shipping an unverifiable change. --- gui/src/styles.css | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/gui/src/styles.css b/gui/src/styles.css index 222c0f1166..0ef2053fac 100644 --- a/gui/src/styles.css +++ b/gui/src/styles.css @@ -763,6 +763,16 @@ a.btn, a.btn:hover { text-decoration: none; } } /* Refresh icon spins while a sync is in flight (the `.spin` keyframes already exist). */ .spin-icon { animation: spin 0.9s linear infinite; } +/* The width cap needs two classes to beat `.notice { max-width: var(--prose-measure) }`, + which every toast also carries and which is declared LATER in this file. At equal + specificity source order won, so the toast resolved to 70ch (542px) rather than its + design width — visible as a toast wider than intended at desktop sizes. */ +.action-toast.notice { + /* Both halves of the original cap have to be restated here, not just the design width: + dropping the viewport term let the toast run to the screen edge at narrow widths + (measured left=0 at 430px, losing the 24px inset the right side keeps). */ + max-width: min(480px, calc(100vw - 48px)); +} /* Toast dismiss affordance: quiet ghost button inside the notice row. */ .action-toast-dismiss { flex: none; @@ -2000,7 +2010,12 @@ table.logs-table { .logs-table-wrap { overflow-y: auto; - max-height: calc(100vh - 260px); + /* `dvh`, not `vh`: static `vh` resolves against the LARGE viewport, so on mobile the + cap is computed for a viewport taller than the one the user can see and the last rows + sit under the browser chrome. The rest of the shell already moved to `100dvh` + (see .app, .sidebar, .main-inner--combos, and the mobile drawer), so this was the + outlier rather than the convention. */ + max-height: calc(100dvh - 260px); } .logs-table thead { From 21e4a5196f104f6fca0465ef7e120ad355166e8f Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 29 Aug 2026 22:37:25 +0900 Subject: [PATCH 3/3] test(gui): pin the dynamic-viewport and toast width caps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hygiene gate flagged `missing_regression_test`: this branch changes `gui/src/styles.css` with no test change, and the reviewer asked for the same treatment #2905 gave the sidecar rules rather than a test exception. Two source-text assertions, matching the existing `gui/tests` convention for CSS contracts (happy-dom performs no layout, so a computed value here would prove nothing; the rendered proof was taken in a real browser via CDP). - `.logs-table-wrap` must cap against `100dvh` and must not go back to static `100vh`. Static `vh` resolves against the large viewport, which is what put the last log rows under mobile browser chrome. - The toast cap must stay on the two-class `.action-toast.notice` selector, and both halves of `min(480px, calc(100vw - 48px))` must survive. A single-class `.action-toast` cap silently loses to `.notice { max-width: var(--prose-measure) }` — equal specificity, declared later — and dropping the viewport term let the toast reach the screen edge at 430px. The second test also guards its own premise: it asserts `.notice` still appears after the compound rule, because the whole reason the compound selector is needed is that source order. If `.notice` ever moves above it, the test fails rather than quietly permitting a "simplification" back to one class. Driven red first: against the parent commit's stylesheet both tests fail (the `dvh` assertion, and `.action-toast.notice` not existing at all), and both pass on this branch's. --- gui/tests/viewport-scroll-caps.test.ts | 65 ++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 gui/tests/viewport-scroll-caps.test.ts diff --git a/gui/tests/viewport-scroll-caps.test.ts b/gui/tests/viewport-scroll-caps.test.ts new file mode 100644 index 0000000000..a8996cadf5 --- /dev/null +++ b/gui/tests/viewport-scroll-caps.test.ts @@ -0,0 +1,65 @@ +import { expect, test } from "bun:test"; + +/** + * Viewport-dependent sizing contracts in the shared stylesheet. + * + * Source-text assertions, not rendered measurements: happy-dom performs no layout, so a + * computed max-width here would prove nothing. Rendered proof was captured in a real + * browser via CDP while fixing these two rules; this file's job is to stop the specific + * shapes that caused the defects from coming back silently. + */ + +const cssUrl = new URL("../src/styles.css", import.meta.url); + +/** Strip comments so no assertion can pass on prose that quotes an old value. */ +function withoutComments(css: string): string { + return css.replace(/\/\*[\s\S]*?\*\//g, ""); +} + +/** All bodies for a selector, which may be declared more than once. */ +function allRuleBodies(css: string, selector: string): string { + const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const matches = [...css.matchAll(new RegExp("(^|\\n)\\s*" + escaped + "\\s*\\{([^}]*)\\}", "g"))]; + if (matches.length === 0) throw new Error("rule not found: " + selector); + return matches.map((m) => m[2]).join("\n"); +} + +test("the log table caps its scroll height against the dynamic viewport", async () => { + const css = withoutComments(await Bun.file(cssUrl).text()); + const wrap = allRuleBodies(css, ".logs-table-wrap"); + + // Static `vh` resolves against the LARGE viewport, which ignores mobile browser chrome: + // the cap is then computed for a viewport taller than the one the user can see and the + // last rows sit underneath the address bar. The rest of the shell (.app, .sidebar, + // .main-inner--combos, the mobile drawer) already uses 100dvh, so this rule was the + // outlier rather than the convention. + expect(wrap).toMatch(/max-height:\s*calc\(\s*100dvh\s*-/); + expect(wrap).not.toMatch(/max-height:\s*calc\(\s*100vh\s*-/); +}); + +test("the toast width cap outranks the later .notice rule", async () => { + const css = withoutComments(await Bun.file(cssUrl).text()); + + // Every toast carries BOTH classes, and `.notice { max-width: var(--prose-measure) }` + // (70ch = 542px) is declared later in this same file at equal specificity 0,1,0. Source + // order therefore won and a single-class `.action-toast` cap never applied - the toast + // rendered 542px instead of its design width. Two classes is what wins the cascade, so + // the cap must stay on the compound selector. + const compound = allRuleBodies(css, ".action-toast.notice"); + const cap = compound.match(/max-width:\s*min\(\s*([\d.]+)px\s*,\s*calc\(\s*100vw\s*-\s*([\d.]+)px\s*\)\s*\)/); + + // Both halves are asserted on purpose. An earlier revision kept only the design width, + // which dropped the viewport term and let the toast reach the screen edge at narrow + // widths (measured left = 0 at 430px, losing the 24px inset the right side keeps). + expect(cap).not.toBeNull(); + expect(Number(cap![1])).toBeGreaterThan(0); + expect(Number(cap![2])).toBeGreaterThan(0); + + // Guard the ordering premise itself: if `.notice` ever moved ABOVE this rule, a + // single-class cap would start working and someone could "simplify" the compound + // selector away. The assertion is only meaningful while `.notice` still comes later. + const noticeIndex = css.search(/(^|\n)\s*\.notice\s*\{/); + const compoundIndex = css.search(/(^|\n)\s*\.action-toast\.notice\s*\{/); + expect(compoundIndex).toBeGreaterThanOrEqual(0); + expect(noticeIndex).toBeGreaterThan(compoundIndex); +});