Mobile: fix content running off the side, and rework the navigation as a drawer - #12
Merged
Merged
Conversation
…the side A tester on Safari/iOS reported a green dot on the overview with nothing beside it, and asked whether the dot was intentional. It was not the dot that was wrong — the words next to it were missing. That line was rendered with AnimatedGradientText, which paints text through `bg-clip-text` over a transparent colour, so the words exist only as a mask on a gradient. The dot is a real fill and survived; the status and the version went with the gradient. Whether the node is reachable and which version it runs is the page's first factual claim, and it should not depend on a paint effect, so it is plain text now. This is the same failure AuroraText had in light mode: text that is invisible when its background does not render, with nothing in the markup to indicate anything is wrong. Two layout faults at 375px, both pushing content past the right edge where it could not be reached: - AgentSurface sat in a grid whose items default to `min-width: auto`, so a 46ch paragraph — 386px against 343px of available width — widened the track and carried the rows with it. The copy buttons ended up at x=441 on a 375px screen. The tracks may shrink now and the prose is capped to the viewport below sm. - The icon cloud canvas carries width/height attributes of 400, which size the drawing buffer rather than the box, so it laid out at a fixed 400px. It is capped with max-w-full; the buffer stays 400x400, so nothing about the rendering changes. Measured after: 343px on a 375px screen. Unclipped overflow across the eight routes went from 22 elements to 0. Filter controls were 28px tall, which is an uncomfortable thumb target; they are 36px below sm and unchanged from sm up. The segmented indicator measures both axes already, so it follows the taller row without further work. Worth recording that the page never actually scrolled sideways: the preview pane reports an inflated window.innerWidth, and comparing element rects against it invents overflow that is not there. The figures above come from comparing rects to the body's own box, where the scaling cancels out.
…er targets Two scrolling strips hid the very thing they were meant to show. The repository tab strip scrolls once its tabs outrun their box, which on a 375px screen is 440px of tabs in 343px. Arriving on ?tab=events left the strip at scrollLeft 0 with the selected tab past the right edge, so the reader saw the first tabs and nothing indicating which one they were on. The strip now reveals its selected tab. It re-runs on resize as well, because each tab's count arrives from its own fetch — a tab grows from "certs" to "certs 1" a moment after selection, which was enough to push a tab revealed on the first pass back over the edge. Verified across all six tabs. The docs section nav has the same shape below lg: four labels come to 419px in 351px, so /docs/node opened with "Run a node" out of sight. Same treatment. Both scroll only their own container. scrollIntoView would have moved the document vertically too and thrown the reader past the header they had just landed on; the page scroll position is unchanged through every case. Header targets on phones: - The wordmark was `hidden sm:inline`, leaving the home link as a 15x18px glyph. The bar has roughly 200px of empty spacer at 375px, so the wordmark now shows at every width — it identifies the site and gives the link a real target. - The palette, theme and menu buttons were 32px; they are 40px below sm. - Documentation nav links were 34px; they are 40px below sm. Desktop is untouched: segmented controls stay 28px, the sort trigger 32px, and the header buttons 32px from sm up. Unclipped overflow is 0 across all eight routes at 375px.
Until now the mobile work had been fixing faults — overflow, tap targets, strips that hid the item they were meant to show. This is the design pass. The hero was 918px tall on a 375x812 screen, so the whole first screen was hero and nothing of the product showed until the reader scrolled. Inside it the display line wrapped to three ragged lines at 40px, and the two calls to action stacked at 199px and 273px, which reads as an accident rather than a pair. - 32px display type below sm sets the line in two, not three. - The lede drops to 15px with tighter leading. Geist Mono is a wide face and seven lines of it is a wall on a phone. - Both buttons run full width and share an edge. - Hero padding and the gap above the counters come in. The hero is 791px now and the live counters reach the fold, so the first screen carries a real figure instead of ending on marketing. The repository row was a two-column grid at every width. On a phone the meta column took 146px of a 343px row, leaving the name and description to share 149px. Stacked below sm, the description gets 311px and runs to two lines rather than being cut mid-word; desktop keeps one line and the side-by-side layout, where density is the point. The clone-copy control was revealed on hover. A touch screen has no hover, so on a phone it could not be reached at all. It is always visible below sm. Not done, and worth a decision: the lede is still seven lines on a phone. The drop to 15px bought height, not lines — the sentence is simply long for a 343px measure in a monospace face. Shortening it is a copy change rather than a layout one, so it is left alone.
The menu was a panel that animated its height open beneath the header. It worked, but it behaved like page content rather than a layer over it: it pushed the page down, it could not be dismissed by dragging or by tapping away, and it left the page behind fully lit with nothing marking it as inactive. It is a bottom sheet now, built on Vaul — a drawer on top of Radix Dialog, so the behaviour a menu needs comes with the library rather than being reimplemented: focus trapped and restored, Escape to close, the page behind inert and locked against scrolling, and drag-to-dismiss. It opens from the bottom because that is where a thumb already rests on a tall phone; the top of an 812px screen is the hardest part of it to reach. Rows are 48px, the active section is filled and marked with aria-current and a check, and a grabber shows the sheet can be thrown shut. The node's own figures and the llms.txt / skill.md links ride along at the foot. autoFocus is set explicitly. Vaul leaves it off by default, which left focus on <body> after opening: a keyboard or screen-reader user opened the menu and was still outside it, with the next Tab starting from the top of the document behind the overlay. Measured at rest with transitions suppressed, since this environment does not advance them: the sheet sits at top 252 of an 812px screen, flush to the bottom edge, all eight links reachable, focus inside, Escape closing it and restoring body scroll. Desktop is untouched — the trigger stays md:hidden and the eight-link bar is unchanged.
The peer chips were cut 67-75px past the right edge, so node2, node3 and manila were unreadable on a 375px screen. The map is a two-column arrangement — this node on the left, its peers on the right — and two columns of ~130px chips plus a 32px gap do not fit in 343px of content width. Peers now show the first label of the host below sm, which is the part that distinguishes them; the full host was already in each chip's title and still shows from sm up. Column and row gaps come in on a phone. Every chip now ends at 359px against a 375px screen. The globe is capped at 250px below sm. It is decorative — the markers are a stock world spread, not peer positions, as the component has always said — and at 343px square it claimed most of a screen, nearly all of it dark sphere against a dark ground, which is the emptiness that made the section look wrong. The drawing buffer is untouched, so nothing about the render changes. The section is 57% of a phone screen now rather than 69%. Also worth recording: the earlier overflow sweep reported this page clean because it discounted anything an ancestor clipped, on the grounds that clipped content cannot scroll the page sideways. That was the wrong test — clipped content is still content the reader cannot see. Counting what sticks out regardless of clipping is what found this.
… own The global :focus-visible rule sat outside any cascade layer, and unlayered CSS beats layered CSS regardless of specificity. Every Tailwind focus utility in the codebase therefore lost to it, and each control got the default 2px accent outline inset by a pixel instead of the treatment it declared. The command palette showed it worst: opening with the shortcut is keyboard-initiated, so :focus-visible matched the autofocused search field and drew a hard rectangle around an input that sits flush inside a rounded panel and already indicates focus with its caret. It was visible on both desktop and mobile because it had nothing to do with viewport size. This is the second time this file has hit that rule — the border-color reset seventy lines above carries a comment warning about exactly it. Moving the ring into @layer base keeps it as the default for anything that does not style its own focus, while a component that does now wins. Verified in the built stylesheet: the base ring compiles at offset 8148 inside @layer base and focus:outline-none at 55995 inside @layer utilities, so the override applies. Inputs are not left without an indicator — they turn their underline to the foreground on focus, and the caret is visible. Buttons and tabs keep the focus-visible outlines they declare themselves.
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.
Follow-up to #11, prompted by a tester on Safari/iOS asking whether a lone green dot on the overview was intentional. It was not the dot that was wrong — the words beside it were missing. Chasing that turned up several more mobile faults, one of which affects desktop too.
The reported bug
The overview's status line was rendered with
AnimatedGradientText, which paints text throughbg-clip-textover a transparent colour, so the words exist only as a mask on a gradient. The dot is a realfilland survived; "Node online · v0.7.0" went with the gradient.Whether the node is reachable and which version it runs is the page's first factual claim, so it should not depend on a paint effect. It is plain text now. This is the same failure
AuroraTexthad in light mode: text that vanishes when its background does not render, with nothing in the markup to indicate anything is wrong.Content that could not be reached
AgentSurfacesat in a grid whose items default tomin-width: auto, so a46chparagraph — 386px against 343px of available width — widened the track and carried the rows with it. The copy buttons ended up at x=441 on a 375px screen.width/heightattributes of 400, which size the drawing buffer rather than the box, so it laid out at a fixed 400px. Capped withmax-w-full; the buffer is untouched, so the render is unchanged.node2,node3andmanilawere cut 67–75px past the edge. Peers show the first label of the host belowsm; the full host was already in each chip'stitle.Unclipped overflow across the eight routes went from 22 elements to 0.
Strips that hid the thing they were meant to show
The repository tab strip scrolls once its tabs outrun their box — 440px of tabs in 343px. Arriving on
?tab=eventsleft it atscrollLeft 0with the selected tab past the right edge. It now reveals its selected tab, and re-runs on resize, because each tab's count arrives from its own fetch: a tab grows from "certs" to "certs 1" a moment after selection, which was enough to push a tab revealed on the first pass back over the edge.The docs section nav has the same shape below
lg— four labels at 419px in 351px, so/docs/nodeopened with "Run a node" out of sight.Both scroll only their own container.
scrollIntoViewwould have moved the document vertically as well and thrown the reader past the header they had just landed on.Navigation is a drawer
The menu was a panel that animated its height open beneath the header. It behaved like page content rather than a layer over it: it pushed the page down, could not be dismissed by dragging or tapping away, and left the page behind fully lit.
It is a bottom sheet now, on Vaul — a drawer built on Radix Dialog, so focus trapping, Escape, an inert scroll-locked background and drag-to-dismiss come from the library. It opens from the bottom because that is where a thumb rests on a tall phone. Rows are 48px, the active section is marked with
aria-currentand a check.autoFocusis set explicitly. Vaul leaves it off by default, which left focus on<body>: a keyboard or screen-reader user opened the menu and was still outside it, with the next Tab starting from the top of the document behind the overlay.Composition
The hero was 918px on a 375×812 screen, so the whole first screen was hero. Display type to 32px sets the line in two rather than three; the lede drops to 15px; both calls to action run full width and share an edge, where they had stacked at 199px and 273px. It is 791px now and the live counters reach the fold.
Repository rows were a two-column grid at every width; on a phone the meta column took 146px of a 343px row. Stacked below
sm, the description gets 311px and runs to two lines instead of being cut mid-word.The clone-copy control was revealed on hover. A touch screen has no hover, so on a phone it could not be reached at all.
One desktop fix
The global
:focus-visiblerule sat outside any cascade layer, and unlayered CSS beats layered CSS regardless of specificity — so every Tailwind focus utility lost to it and each control got the default outline instead of the treatment it declared. Most visibly, the command palette drew a hard rectangle around its search field, on desktop as much as mobile.The border-color reset seventy lines above it in the same file carries a comment warning about this exact rule.
Moved into
@layer base. Verified in the built stylesheet: the base ring compiles at offset 8148 inside@layer baseandfocus:outline-noneat 55995 inside@layer utilities, so component overrides now apply. Nothing loses its indicator — inputs turn their underline to the foreground and show a caret, buttons and tabs keep the outlines they declare.Verification
tscclean,eslintclean, 158 tests, production build clean. Desktop checked unchanged at each step: segmented controls stay 28px, the sort trigger 32px, header buttons 32px, repository rows side by side with one-line descriptions, full hostnames on the federation map, globe at 460px.Caveat worth stating plainly: the browser pane in my environment never composited a frame, so none of this was verified by eye — every figure above is measured layout geometry, and the drag-to-dismiss gesture is unverified. Worth a look on a real phone before merging.
Two measurement traps I fell into and corrected, in case they matter to a reviewer:
window.innerWidth(506 against a real 375), and comparing element rects to it invents overflow that does not exist. The figures here come from comparing rects to the body's own box.