Skip to content

Mobile: fix content running off the side, and rework the navigation as a drawer - #12

Merged
kevincodex1 merged 6 commits into
Gitlawb:mainfrom
Vasanthdev2004:fix/mobile-responsive
Aug 19, 2026
Merged

Mobile: fix content running off the side, and rework the navigation as a drawer#12
kevincodex1 merged 6 commits into
Gitlawb:mainfrom
Vasanthdev2004:fix/mobile-responsive

Conversation

@Vasanthdev2004

@Vasanthdev2004 Vasanthdev2004 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

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 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; "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 AuroraText had 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

  • 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 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. Capped with max-w-full; the buffer is untouched, so the render is unchanged.
  • The federation map put this node and its peers in two columns. Two columns of ~130px chips plus a 32px gap do not fit in 343px, so node2, node3 and manila were cut 67–75px past the edge. Peers show the first label of the host below sm; the full host was already in each chip's title.

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=events left it at scrollLeft 0 with 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/node opened with "Run a node" out of sight.

Both scroll only their own container. scrollIntoView would 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-current and a check.

autoFocus is 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-visible rule 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 base and focus:outline-none at 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

tsc clean, eslint clean, 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:

  • The pane reports an inflated 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.
  • My first overflow sweep 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, and it is why the federation map came back clean on the first pass.

…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.

@kevincodex1 kevincodex1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@kevincodex1
kevincodex1 merged commit f4af95d into Gitlawb:main Aug 19, 2026
1 check passed
@Vasanthdev2004
Vasanthdev2004 deleted the fix/mobile-responsive branch August 19, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants