Repository navigation
Keep glossary popovers inside the viewport, and gate reflow at 380px - #20
Merged
Merged
Conversation
At 380px the page scrolled sideways to 561px on first paint. Every .gloss-pop is position:absolute and hidden with visibility, which still occupies layout; under 768px the CSS anchors each at its term's left edge with max-width 80vw, so a term near the right of a line pushed its hidden popover past the viewport. The hover/focus anchoring could not help: it ran only on interaction, and chose gloss-left/gloss-right from the term's centre, which disagreed with the narrow-viewport CSS, so a visible popover could run off the edge too. fitAllGlosses() now measures every popover and shifts it into [8, vw - 8] through a --gloss-dx custom property (applied with the individual `translate` property, so it composes with the existing transform), re-run when glossary terms render and on resize. positionGloss() measures the popover itself the same way. The a11y suite gains a 380px reflow block: first paint, the proof and attack journey, the result tables, and every glossary term focused in turn. With the fix reverted all four tests fail on span.gloss-pop right=561; with it in, all pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9wNFmWChS4HdDbqrEAq86
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
I built
main(f8cc01a) and served it at/crypto-lab-bulletproofs/. At 380x800 in Chromium,document.documentElement.scrollWidthwas 561 (clientWidth 380). It stayed 561 in every state I drove: first paint, proof generated, verifier accepted, the attack panels, export/import accepted, import failed, and the aggregate and benchmark tables.Culprit:
span.gloss-pop, 304px wide, right edge at 561. Several others sat at 501, 440, 417 and so on.Why it happened:
position:absoluteand is hidden withvisibility:hidden. A hidden box like that still takes part in layout.left:0,max-width:80vw). A term near the right of a line therefore pushed its hidden popover past the viewport.positionGloss, could not catch this. It only ran on hover or focus. It also chosegloss-left/gloss-rightfrom the term's centre, which assumes the desktop centred layout and not the narrow-viewport left anchor. So a popover could still run off the edge even while it was visible.Fix
Plain CSS cannot fix this: where a popover lands depends on where its term wraps.
src/app.ts:fitAllGlosses()measures every popover and shifts it horizontally into[8, vw - 8]by setting a--gloss-dxcustom property. It resets all shifts, reads all rects, then writes them, so it costs one layout pass.MutationObserverre-runs it, batched to one call per animation frame, whenever glossary terms are rendered. It also re-runs onresize.MutationObserverorrequestAnimationFrameis missing. The linkedom smoke scripts innpm testhave neither.positionGloss()now measures the popover itself in the same way, instead of guessing from the term's centre.src/style.css: addedtranslate: var(--gloss-dx, 0) 0on.gloss-pop. This is the separatetranslateproperty, so it combines with the existingtransform: translateX(-50%). No other styling or theme changes.After the fix: scrollWidth is 380 / 380 in every state above. I also focused each of the 15 glossary terms in turn at 380, 800 and 1280px wide. Every visible popover stayed fully on-screen.
New guard
e2e/reflow.ts: addedexpectNoHorizontalOverflow(page, label), adapted fromcrypto-lab-world-ciphers/e2e/gate.ts. When it names the culprit, it accounts for the containing block of absolutely positioned boxes.e2e/a11y.spec.ts: added areflow at 380pxblock with four tests:The check fails without the fix
src/app.tsandsrc/style.csswithgit checkout. The reverted diff was non-empty: 90 lines.scrollWidth: 561, widest: span.gloss-pop @304px right=561.Local gate
npm test: all 10 scripts passed. That covers 127 PASS lines in the linkedom smoke and a11y scripts and every KAT suite, with 0 FAIL.tsc && vite build): passed.CI=1: 24 passed, which is the existing 20 plus 4 new. I used an uncommitted config to point Playwright at the local Chromium.One caveat about the timeout. I ran the suite locally with
--timeout 150000. On this machine the existingno WCAG A/AA violations in the result tablestest takes longer than Playwright's default 30s timeout, and it fails that way on unmodifiedmaintoo. On CI it takes about 14s; the last main run passed 20/20. The new tables reflow test runs the same benchmark, so it costs about the same on CI.There are no visual snapshots in this lab.
🤖 Generated with Claude Code
https://claude.ai/code/session_01P9wNFmWChS4HdDbqrEAq86
Generated by Claude Code