Let the quota line shrink into the circles instead of snapping at 900px #134

Merged
lz merged 3 commits from fix/quota-display into main 2026-09-05 22:04:19 +02:00
Owner

The problem

The quota badge swapped to its two circular gauges at a single @media (max-width: 900px). Measured in a browser, the full line is 336px wide and the whole header wants 637px — so a half-width desktop window with a quarter of the header sitting empty still showed phone-shaped gauges, and so did every phone.

Two more symptoms were hiding behind the same rule:

  • The unknown state (no session has reported yet) had only the circles form, so the badge changed shape the moment the first poll landed: circles on load, line a moment later, on every page load.
  • The reset stamp is the first token the line drops, and the circles never carried it inline, so below 560px it survived only in the title tooltips — and a phone has no hover. The one surface that still had the answer was the one the operator could not reach.

What it does now

Four stages, each measured rather than picked:

Viewport What gives
≥ 680px full line, bar 46px
680 → 560 bar shrinks fluidly 46px → 20px
≤ 560px reset stamp drops (~63px per window)
≤ 410px Header hides its wordmark — 107px of the logo's 139px
≤ 330px circles

The ramp starts ~90px above where the line would actually stop fitting, so the narrowing reads as a warning rather than a surprise, and the switch sits at the foot of it.

The circles are the floor, not the mobile form. 330px is below the narrowest phone still in circulation, so a phone now gets a real reading.

The unknown state renders the same two forms instead of its own. A null window draws a dashed track and an em dash inside the same markup, so an arriving report fills the shape that was already there — and the two {#if} branches collapse into one.

Tapping the badge opens a panel with both windows' percentage and reset time (or "no report yet"). Being absolutely positioned it costs the header no width, so it reads identically at 1400px and 320px and in either form — which is why it's the answer rather than a fifth breakpoint, which would have had to buy the room from somewhere. It reuses HeaderMenu's disclosure contract: listeners only while open, and the outside-click root is the wrapper that contains the trigger, or tapping the badge again would close on pointerdown and reopen on click.

Approaches that do not work

Recorded in the code comments so they aren't retried:

  • Container query on .q — measures the badge's own (tiny) inline size. Size containment makes it ignore its children, so it read as tiny at every width and the line never rendered at all. (A container on .right is the variant that could work; see the last section for why it isn't taken.)
  • flex-shrink — fails in both directions. A definite width: 46px makes the bar's min-content contribution 46px too, so .right can't shrink and the logo absorbs the whole squeeze (measured: 139.5px → 91px, wordmark clipping, bars untouched). Swapping to flex-basis: 46px inverts it — flex intrinsic sizing then sizes the bar from its max-content contribution, which is 0 because the fill is a percentage-width block, so the bar sat at its floor at every viewport, 1400px included.

Verification

Driven in a real browser from 1400px down to 320px, including a forced-on pass below each switch to find the true floor rather than guessing it, and at the worst-case reading (100% in both windows, ~14px wider than two digits):

  • no wordmark clipping and no horizontal overflow at any width
  • no shape change when a report arrives
  • the panel opens on tap, closes on a second tap, on Escape (refocusing the trigger) and on an outside pointerdown; it stays inside the viewport at 320px and in the circles form
  • 1507 vitest tests pass, pnpm lint clean, svelte-check 0 errors

AGENTS.md fact #19 updated with the stage table, the disclosure, and both dead ends. Fact #21's "never put overflow: hidden on that header" now protects two popups instead of one.

/simplify pass (commit 2)

Four cleanup agents reviewed the diff. Reuse: clean. Applied:

  • One resolve() per window. The markup asked the same three questions in four places — w ? band(…) : 'unknown' twice on adjacent lines, plus a tooltip helper re-deriving the rounded percentage and re-running the Intl-backed fmtReset at each of its two call sites. fmtReset drops from three calls per window to one.
  • .track.unknown class instead of a bound stroke-dasharray, so all four "unknown" tokens are expressed the same way.
  • Corrected the summary comment, which still said "two stages" and called 560px the switch to circles — contradicting the two stage comments 40 lines below it.
  • quota-badge-stages.test.ts — the altitude review flagged that Header's wordmark constant is meaningful only in terms of the badge's internal budget, with the two files correlated by prose alone. The test pins the ordering, not the numbers: retuning a breakpoint stays green, inverting two stages or moving one without the other does not. All six guards were mutation-checked (inverting the circles floor, moving the wordmark drop below it, detuning the ramp end, moving the reset drop below the wordmark drop, renaming the .rst rule) — each produced the expected failures.

Skipped, with reasons:

  • Merging .pct.unknown / .cap.unknown into one rule — saves a line but breaks their co-location with their own token families. The reviewer agreed.
  • Container query on .right — the one variant that could actually work. It needs flex: 1 1 0; min-width: 0, which zeroes the content-based minimum that AGENTS.md fact #21 depends on (the logo has to be what yields under the window-controls overlay, or the right cluster slides under the window controls and the menu stops hit-testing). That trades a measured guarantee for an approximation, so it stays viewport-based and the reasoning is now recorded in the file.
  • Splitting into separate PRs — the three concerns are separable, but each was only discovered because the previous one exposed it. They are three separate commits here.
## The problem The quota badge swapped to its two circular gauges at a single `@media (max-width: 900px)`. Measured in a browser, the full line is **336px** wide and the whole header wants **637px** — so a half-width desktop window with a quarter of the header sitting empty still showed phone-shaped gauges, and so did every phone. Two more symptoms were hiding behind the same rule: - The unknown state (no session has reported yet) had *only* the circles form, so the badge changed **shape** the moment the first poll landed: circles on load, line a moment later, on every page load. - The reset stamp is the first token the line drops, and the circles never carried it inline, so below 560px it survived **only in the `title` tooltips** — and a phone has no hover. The one surface that still had the answer was the one the operator could not reach. ## What it does now Four stages, each measured rather than picked: | Viewport | What gives | |---|---| | ≥ 680px | full line, bar 46px | | 680 → 560 | bar shrinks fluidly 46px → 20px | | ≤ 560px | reset stamp drops (~63px per window) | | ≤ 410px | `Header` hides its wordmark — 107px of the logo's 139px | | ≤ 330px | circles | The ramp starts ~90px above where the line would actually stop fitting, so the narrowing reads as a warning rather than a surprise, and the switch sits at the foot of it. **The circles are the floor, not the mobile form.** 330px is below the narrowest phone still in circulation, so a phone now gets a real reading. **The unknown state renders the same two forms** instead of its own. A null window draws a dashed track and an em dash inside the same markup, so an arriving report fills the shape that was already there — and the two `{#if}` branches collapse into one. **Tapping the badge opens a panel** with both windows' percentage and reset time (or "no report yet"). Being absolutely positioned it costs the header no width, so it reads identically at 1400px and 320px and in either form — which is why it's the answer rather than a fifth breakpoint, which would have had to buy the room from somewhere. It reuses `HeaderMenu`'s disclosure contract: listeners only while open, and the outside-click root is the wrapper that *contains* the trigger, or tapping the badge again would close on `pointerdown` and reopen on `click`. ## Approaches that do not work Recorded in the code comments so they aren't retried: - **Container query on `.q`** — measures the badge's *own* (tiny) inline size. Size containment makes it ignore its children, so it read as tiny at every width and the line never rendered at all. (A container on `.right` is the variant that *could* work; see the last section for why it isn't taken.) - **flex-shrink** — fails in *both* directions. A definite `width: 46px` makes the bar's min-content contribution 46px too, so `.right` can't shrink and the logo absorbs the whole squeeze (measured: 139.5px → 91px, wordmark clipping, bars untouched). Swapping to `flex-basis: 46px` inverts it — flex intrinsic sizing then sizes the bar from its *max-content* contribution, which is **0** because the fill is a percentage-width block, so the bar sat at its floor at every viewport, 1400px included. ## Verification Driven in a real browser from 1400px down to 320px, including a forced-on pass below each switch to find the true floor rather than guessing it, and at the worst-case reading (100% in both windows, ~14px wider than two digits): - no wordmark clipping and no horizontal overflow at any width - no shape change when a report arrives - the panel opens on tap, closes on a second tap, on Escape (refocusing the trigger) and on an outside pointerdown; it stays inside the viewport at 320px and in the circles form - **1507 vitest tests pass, `pnpm lint` clean, `svelte-check` 0 errors** AGENTS.md fact #19 updated with the stage table, the disclosure, and both dead ends. Fact #21's "never put `overflow: hidden` on that header" now protects two popups instead of one. ## /simplify pass (commit 2) Four cleanup agents reviewed the diff. Reuse: clean. Applied: - **One `resolve()` per window.** The markup asked the same three questions in four places — `w ? band(…) : 'unknown'` twice on adjacent lines, plus a tooltip helper re-deriving the rounded percentage and re-running the Intl-backed `fmtReset` at each of its two call sites. `fmtReset` drops from three calls per window to one. - **`.track.unknown` class** instead of a bound `stroke-dasharray`, so all four "unknown" tokens are expressed the same way. - **Corrected the summary comment**, which still said "two stages" and called 560px the switch to circles — contradicting the two stage comments 40 lines below it. - **`quota-badge-stages.test.ts`** — the altitude review flagged that Header's wordmark constant is meaningful only in terms of the badge's internal budget, with the two files correlated by prose alone. The test pins the *ordering*, not the numbers: retuning a breakpoint stays green, inverting two stages or moving one without the other does not. All six guards were mutation-checked (inverting the circles floor, moving the wordmark drop below it, detuning the ramp end, moving the reset drop below the wordmark drop, renaming the `.rst` rule) — each produced the expected failures. Skipped, with reasons: - **Merging `.pct.unknown` / `.cap.unknown`** into one rule — saves a line but breaks their co-location with their own token families. The reviewer agreed. - **Container query on `.right`** — the one variant that could actually work. It needs `flex: 1 1 0; min-width: 0`, which zeroes the content-based minimum that AGENTS.md fact #21 depends on (the logo has to be what yields under the window-controls overlay, or the right cluster slides under the window controls and the menu stops hit-testing). That trades a measured guarantee for an approximation, so it stays viewport-based and the reasoning is now recorded in the file. - **Splitting into separate PRs** — the three concerns are separable, but each was only discovered because the previous one exposed it. They are three separate commits here.
lz added 2 commits 2026-09-05 21:10:36 +02:00
The badge swapped to the mobile circles at a 900px viewport, but the line
is 336px wide and the whole header only wants 637px — so a half-width
desktop window with a quarter of the header empty was still showing
phone-shaped gauges, and so was every phone.

Four stages now, each measured rather than picked. The bar is a clamp()
running 46px -> 20px across 680px -> 560px, starting ~90px above where the
line would actually stop fitting so the narrowing reads as a warning. At
560 the bar has nothing left to give, so the reset stamp drops — the
widest optional token, and the one the circles' tooltip still carries. At
410 the header hides its wordmark: measured, the text is 107px of the
logo's 139px, and freeing it is the only thing standing between a phone
and a real reading. The circles then take over at 330px, below the
narrowest phone still in circulation — they are the floor, not the
mobile form.

The unknown state renders the same two forms rather than its own. It used
to have only circles, so the badge changed SHAPE as soon as the first poll
landed: circles on load, line a moment later, on every page. A null window
now draws a dashed track and an em dash inside the same markup, so an
arriving report fills the shape that was already there, and the two
branches collapse into one.

The comment records why the two better-looking measures do not work: a
container query measures the badge's own tiny width, and flex-shrink fails
in both directions — a definite width makes the bar's min-content
contribution 46px so the logo eats the squeeze and clips, while a
flex-basis makes its max-content contribution 0 so the bar sits at its
floor at every width. Both measured.

Verified in a browser from 1400px to 320px, with a forced-on pass below
each switch to find the real floor: no wordmark clipping and no horizontal
overflow at any width, and no shape change when a report arrives.
refactor(ui): resolve each quota window once, and pin the stage ordering
Some checks failed
ci / nexus (pull_request) Successful in 11m41s
ci / images (pull_request) Has been cancelled
b20441908c
Cleanup pass over the previous commit.

The markup was asking the same three questions of every window in four
places — `w ? band(...) : 'unknown'` twice on adjacent lines, plus a
tooltip helper that re-derived the rounded percentage and re-ran the
Intl-backed fmtReset at each of its two call sites. One `resolve()` per
window answers them once and leaves a single definition of what "unknown"
looks like, which also drops fmtReset from three calls per window to one.

The circles' dashed track moves from a bound `stroke-dasharray` attribute
to a `.track.unknown` class, so all four "unknown" tokens are now
expressed the same way.

Adds quota-badge-stages.test.ts. Four of the stages are hand-measured
viewport constants and one of them lives in Header.svelte, so the two
files were correlated by prose alone — the exact shape that drifts
silently. The test pins the ORDERING rather than the numbers: retuning a
breakpoint stays green, inverting two stages or moving one without the
other does not. Each of the six guards was mutation-checked: inverting
the circles floor, moving the wordmark drop below it, detuning the ramp
end, moving the reset drop below the wordmark drop, and renaming the
.rst rule each produce the expected failures, and the file restores green.

Also corrects the summary comment, which still said "two stages" and
described the 560px breakpoint as the switch to circles, contradicting
the two stage comments 40 lines below it. It now lists all four in order
and names the Header one, and records why `container-type` on .right —
the container-query variant that could actually work — is not taken:
it needs `flex: 1 1 0; min-width: 0`, which zeroes the content-based
minimum that fact #21's window-controls-overlay behaviour depends on.
feat(ui): tap the quota badge for the reset times it cannot show inline
All checks were successful
ci / nexus (pull_request) Successful in 10m31s
pr-image-cleanup / delete-pr-images (pull_request) Successful in 8s
ci / images (pull_request) Successful in 12m30s
cc06ed2b0a
The reset stamp is the first token the line drops (560px) and the circles
never carried it inline at all, so below that width it survived only in
the `title` tooltips. A phone has no hover — and a phone is exactly where
the stamp is hidden — so the one surface that still had the answer was the
one the operator could not reach.

The badge is now a disclosure. Tapping it opens a panel listing both
windows with their percentage and reset time, or "no report yet" for a
window nothing has reported. Being absolutely positioned it costs the
header no width, so it reads identically at 1400px and at 320px and in
either form; that is why it is the answer here rather than a fifth
breakpoint, which would have had to buy the room from somewhere.

Same disclosure contract as HeaderMenu, deliberately: listeners only while
open, and the outside-click root is the wrapper that CONTAINS the trigger,
so tapping the badge again closes it instead of closing on pointerdown and
reopening on click. Surface, border, shadow and z-index match that popup
too — same header, same kind of thing. Like it, the panel hangs below the
header strip, so fact #21's "never put overflow:hidden on that header"
now covers this as well.

.line/.dots/.dot become spans: they moved inside a <button>, whose content
model is phrasing content.

Verified in a browser at 1400/390/320: opens on tap, closes on a second
tap, on Escape (refocusing the trigger) and on an outside pointerdown;
panel stays inside the viewport at 320px and in the circles form; unknown
windows read "no report yet". 1507 tests, lint clean, svelte-check 0.
lz merged commit ce12c18ce8 into main 2026-09-05 22:04:19 +02:00
lz deleted branch fix/quota-display 2026-09-05 22:04:19 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus!134
No description provided.