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
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Original file line number Diff line number Diff line change
Expand Up @@ -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.

Original file line number Diff line number Diff line change
@@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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`:
Expand Down
17 changes: 16 additions & 1 deletion gui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
65 changes: 65 additions & 0 deletions gui/tests/viewport-scroll-caps.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading