From 7772e3872bc98f5e9aab57247d53c730e6a7b710 Mon Sep 17 00:00:00 2001 From: Vasanthdev2004 Date: Wed, 19 Aug 2026 15:06:51 +0530 Subject: [PATCH 1/6] fix(mobile): show the node status text, and stop content running off the side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/components/home/AgentSurface.tsx | 8 +++---- src/components/ui/Dropdown.tsx | 2 +- src/components/ui/SegmentedControl.tsx | 5 +++- src/components/ui/icon-cloud.tsx | 6 ++++- src/pages/HomePage.tsx | 33 +++++++++++++++----------- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/components/home/AgentSurface.tsx b/src/components/home/AgentSurface.tsx index 02b807d..649cbd3 100644 --- a/src/components/home/AgentSurface.tsx +++ b/src/components/home/AgentSurface.tsx @@ -35,9 +35,9 @@ export function AgentSurface() { const origin = typeof window === 'undefined' ? '' : window.location.origin; return ( -
-
-

+

+
+

Agents are not a second audience here. Every page answers in plain markdown to any client that does not ask for HTML, identity is an Ed25519 keypair rather than an account, and each push produces a @@ -53,7 +53,7 @@ export function AgentSurface() {

-
    +
      {SURFACES.map(surface => (
    • ({ id={controlId} aria-labelledby={`${labelId} ${controlId}`} className={cn( - 'group inline-flex items-center gap-1.5 h-8 pl-3.5 pr-3 text-[13px] whitespace-nowrap', + 'group inline-flex items-center gap-1.5 h-9 sm:h-8 pl-3.5 pr-3 text-[13px] whitespace-nowrap', 'rounded-[var(--radius-control)] border border-border bg-transparent text-foreground', 'cursor-pointer transition-colors hover:border-foreground/45 hover:bg-surface-secondary', 'data-[state=open]:border-foreground/60 data-[state=open]:bg-surface-secondary', diff --git a/src/components/ui/SegmentedControl.tsx b/src/components/ui/SegmentedControl.tsx index 74c9638..565674e 100644 --- a/src/components/ui/SegmentedControl.tsx +++ b/src/components/ui/SegmentedControl.tsx @@ -110,7 +110,10 @@ export function SegmentedControl({ onClick={() => onChange(option.value)} aria-pressed={active} className={cn( - 'relative inline-flex items-center gap-1.5 px-3 h-7 text-[12.5px] whitespace-nowrap transition-colors', + // 36px on a phone, 28 from sm up. These are the primary filter controls on + // the listing pages and a 28px target is uncomfortable to hit with a + // thumb; desktop keeps the tighter density. + 'relative inline-flex items-center gap-1.5 px-3.5 sm:px-3 h-9 sm:h-7 text-[13px] sm:text-[12.5px] whitespace-nowrap transition-colors', 'rounded-[var(--radius-control)]', 'focus:outline-none focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2', active ? 'text-background' : 'text-muted hover:text-foreground', diff --git a/src/components/ui/icon-cloud.tsx b/src/components/ui/icon-cloud.tsx index 46c8a6b..e39348d 100644 --- a/src/components/ui/icon-cloud.tsx +++ b/src/components/ui/icon-cloud.tsx @@ -355,7 +355,11 @@ export function IconCloud({ onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} - className="rounded-lg" + // The width/height attributes size the drawing buffer, not the box. + // Left unconstrained the canvas lays out at a fixed 400px and runs off + // the side of a phone; these let CSS scale it down while the buffer, + // and so the rendering, stays 400x400. + className="h-auto max-w-full rounded-lg" aria-label="Interactive 3D Icon Cloud" role="img" /> diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index d92e7fb..7222dc0 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -25,7 +25,6 @@ import { DotPattern } from '../components/ui/dot-pattern'; import { AnimatedBeam } from '../components/ui/animated-beam'; import { Terminal, TypingAnimation, AnimatedSpan } from '../components/ui/terminal'; import { AuroraText } from '../components/ui/aurora-text'; -import { AnimatedGradientText } from '../components/ui/animated-gradient-text'; import { RainbowButton } from '../components/ui/rainbow-button'; import { RippleButton } from '../components/ui/ripple-button'; import { ConfettiButton } from '../components/ui/confetti'; @@ -164,22 +163,28 @@ export default function HomePage() { />
      + {/* Plain text, not AnimatedGradientText. + + That component paints its text with `bg-clip-text` over a + transparent colour, so the words exist only as a mask on a + gradient. A tester on Safari/iOS saw the green dot and nothing + beside it — the dot is a real `fill`, so it survived, while the + status and version vanished with the gradient. Whether the node + is reachable and which version it runs is the page’s first + factual claim; it should not depend on a paint effect. */} {!loading && ( - - - +

      +

      )}

      From 9a2d9bda7c758072f2cdbd17726cca44e9c37f27 Mon Sep 17 00:00:00 2001 From: Vasanthdev2004 Date: Wed, 19 Aug 2026 15:16:10 +0530 Subject: [PATCH 2/6] fix(mobile): keep the current tab and document visible, and size header targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/components/layout/TopBar.tsx | 6 ++--- src/components/register/Tabs.tsx | 42 +++++++++++++++++++++++++++++++ src/components/ui/ThemeToggle.tsx | 2 +- src/pages/DocsPage.tsx | 24 ++++++++++++++++-- 4 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx index b390fae..60a5da4 100644 --- a/src/components/layout/TopBar.tsx +++ b/src/components/layout/TopBar.tsx @@ -57,7 +57,7 @@ export default function TopBar() {
      - + gitlawb @@ -123,7 +123,7 @@ export default function TopBar() { title={`Search — ${paletteHint}`} aria-label={`Open command palette (${paletteHint})`} aria-keyshortcuts="Meta+K Control+K" - className="inline-flex h-8 w-8 items-center justify-center + className="inline-flex h-10 w-10 sm:h-8 sm:w-8 items-center justify-center text-muted transition-colors hover:text-foreground" > @@ -136,7 +136,7 @@ export default function TopBar() { aria-label="Navigation menu" aria-expanded={menuOpen} onClick={() => setMenuOpen(o => !o)} - className="md:hidden inline-flex h-8 w-8 items-center justify-center text-muted hover:text-foreground" + className="md:hidden inline-flex h-10 w-10 items-center justify-center text-muted hover:text-foreground" > {menuOpen ? : } diff --git a/src/components/register/Tabs.tsx b/src/components/register/Tabs.tsx index 0a9ab8c..33ce650 100644 --- a/src/components/register/Tabs.tsx +++ b/src/components/register/Tabs.tsx @@ -2,6 +2,7 @@ import { createContext, useContext, useId, + useLayoutEffect, useRef, type ReactNode, type KeyboardEvent, @@ -50,6 +51,47 @@ function ListContainer({ children, className }: { children: ReactNode; className function List({ children, ...rest }: { children: ReactNode; 'aria-label'?: string }) { const listRef = useRef(null); + const { selected } = useTabs(); + + // Keep the selected tab inside the strip. + // + // The strip scrolls horizontally once the tabs outrun their box, which on a + // 375px screen happens at six tabs — 440px of tabs in 343px. Arriving on + // ?tab=events then left the selected tab off the right edge with the strip + // still at scrollLeft 0, so the reader saw the first tabs and no indication + // of which one they were on. + // + // Only the container is scrolled, never the page: scrollIntoView would also + // move the document vertically and jump the reader past the repository + // header they had just landed on. + useLayoutEffect(() => { + const list = listRef.current; + if (!list) return; + + const reveal = () => { + const tab = list.querySelector('[role="tab"][aria-selected="true"]'); + if (!tab) return; + const listBox = list.getBoundingClientRect(); + const tabBox = tab.getBoundingClientRect(); + const margin = 12; + if (tabBox.left < listBox.left) { + list.scrollLeft -= listBox.left - tabBox.left + margin; + } else if (tabBox.right > listBox.right) { + list.scrollLeft += tabBox.right - listBox.right + margin; + } + }; + + reveal(); + + // Re-run when the strip resizes. Each tab carries a count that arrives + // after its own fetch, so a tab grows from "certs" to "certs 1" a moment + // after selection — enough to push a tab revealed on the first pass back + // over the edge. + const observer = new ResizeObserver(reveal); + observer.observe(list); + for (const child of Array.from(list.children)) observer.observe(child); + return () => observer.disconnect(); + }, [selected]); // Roving arrow keys across the tab strip, wrapping at both ends. const onKeyDown = (e: KeyboardEvent) => { diff --git a/src/components/ui/ThemeToggle.tsx b/src/components/ui/ThemeToggle.tsx index 8fdcf4f..d6f06df 100644 --- a/src/components/ui/ThemeToggle.tsx +++ b/src/components/ui/ThemeToggle.tsx @@ -28,7 +28,7 @@ export function ThemeToggle() { // the global `*` reset sets border-color, so a transparent border still // paints as a 1px frame. The icon is sized down from the component's // 24px default, which was cramped inside a 32px hit area. - className="inline-flex h-8 w-8 items-center justify-center + className="inline-flex h-10 w-10 sm:h-8 sm:w-8 items-center justify-center text-muted transition-colors hover:text-foreground [&_svg]:size-[17px]" /> diff --git a/src/pages/DocsPage.tsx b/src/pages/DocsPage.tsx index 454832c..ae695cd 100644 --- a/src/pages/DocsPage.tsx +++ b/src/pages/DocsPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { NavLink, Navigate, useParams, useLocation } from 'react-router-dom'; import { DocArticle } from '../components/docs/DocArticle'; @@ -32,6 +32,7 @@ interface DocState { export default function DocsPage() { const { slug = 'quickstart' } = useParams(); const { hash } = useLocation(); + const docNavRef = useRef(null); const [loaded, setLoaded] = useState({ slug: null, html: null, error: null }); useEffect(() => { @@ -93,6 +94,23 @@ export default function DocsPage() { document.getElementById(id)?.scrollIntoView({ block: 'start' }); }, [loaded.slug, loaded.html, slug, hash]); + // Keep the current document visible in the section nav. + // + // Below lg the nav is a horizontal strip, and four labels come to 419px in + // 351px of room. Opening /docs/node therefore left "Run a node" past the + // right edge with the strip unscrolled, so the reader could not see which + // document they were in. Only the strip scrolls, never the page. + useLayoutEffect(() => { + const nav = docNavRef.current; + const active = nav?.querySelector('[aria-current="page"]') ?? + nav?.querySelector('a[class*="bg-foreground"]'); + if (!nav || !active) return; + const navBox = nav.getBoundingClientRect(); + const box = active.getBoundingClientRect(); + if (box.left < navBox.left) nav.scrollLeft -= navBox.left - box.left + 12; + else if (box.right > navBox.right) nav.scrollLeft += box.right - navBox.right + 12; + }, [slug]); + const headings = useMemo( () => (state.html ? extractTocHeadings(state.html) : []), [state.html], @@ -114,6 +132,7 @@ export default function DocsPage() {

      Documentation

      + {/* Two lines on a phone, one on desktop. Stacked, the row has the width + for a second line, and a description cut mid-word tells the reader + less than the sentence does. Desktop keeps one line so the list + stays scannable at density. */} {repo.description && ( -

      +

      {repo.description}

      )} @@ -92,7 +96,7 @@ export function RepoRow({ repo }: RepoRowProps) {
      -
      +
      {repo.stars > 0 && ( @@ -100,7 +104,7 @@ export function RepoRow({ repo }: RepoRowProps) { )} {repo.updatedAt} - +
      diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 7222dc0..5cb6b5d 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -145,7 +145,7 @@ export default function HomePage() {
      {/* ── Hero ─────────────────────────────────────────────────────────── */} -
      +
      {/* Ripple centres on the node itself: concentric rings reading as a signal going out to the network. */} )} -

      +

      Every push, provably signed

      -

      +

      {node?.name ?? 'This node'} runs a decentralized git network where agents and humans push as equals. Every push is signed by a key that never leaves the pusher’s machine and issues a certificate you can verify without trusting the node that served it.

      -
      +
      {/* Primary: the heaviest button in the set, on the one action that converts — installing gl and making a first signed push. */} - - + + Start pushing @@ -210,8 +210,8 @@ export default function HomePage() { {/* Secondary: quieter, and it carries a live figure. */} - - + + Browse {stats ? stats.repos.toLocaleString() : ''} repositories @@ -229,7 +229,7 @@ export default function HomePage() {
      {/* Live counts as hoverable cards rather than a flat row. */} -
      +
      {[ { label: 'Repositories', count: stats?.repos, to: '/repos' }, { label: 'Agents', count: stats?.agents, to: '/agents' }, From 6fb65377919149992f3bdbc49085118f81c5938d Mon Sep 17 00:00:00 2001 From: Vasanthdev2004 Date: Wed, 19 Aug 2026 15:43:07 +0530 Subject: [PATCH 4/6] feat(mobile): make the navigation a drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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. --- package-lock.json | 53 +++++++++- package.json | 3 +- src/components/layout/MobileNavDrawer.tsx | 121 ++++++++++++++++++++++ src/components/layout/TopBar.tsx | 49 ++------- 4 files changed, 185 insertions(+), 41 deletions(-) create mode 100644 src/components/layout/MobileNavDrawer.tsx diff --git a/package-lock.json b/package-lock.json index 79cd903..a2420f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,8 @@ "shiki": "^4.3.0", "tailwind-merge": "^3.6.0", "tailwind-variants": "^3.3.0", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -1546,6 +1547,43 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", @@ -6381,6 +6419,19 @@ } } }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/package.json b/package.json index 6b9b9ac..05610e5 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "shiki": "^4.3.0", "tailwind-merge": "^3.6.0", "tailwind-variants": "^3.3.0", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/src/components/layout/MobileNavDrawer.tsx b/src/components/layout/MobileNavDrawer.tsx new file mode 100644 index 0000000..36dcdb1 --- /dev/null +++ b/src/components/layout/MobileNavDrawer.tsx @@ -0,0 +1,121 @@ +import { Drawer } from 'vaul'; +import { Link } from 'react-router-dom'; +import { ArrowUpRight, Check } from 'lucide-react'; +import type { NodeInfo, NodeStats } from '../../lib/api'; +import { cn } from '../../lib/utils'; + +/** + * The navigation drawer on phones. + * + * This replaced a panel that animated its height open beneath the header. That + * worked, but it read as part of the page rather than as a layer over it: it + * pushed the content down, it could not be dismissed by dragging or by tapping + * away, and it left the page behind it fully lit. + * + * Vaul is a drawer built on Radix Dialog, so the behaviour a menu needs comes + * with it — focus is trapped while open and restored on close, Escape closes, + * the page behind is inert and does not scroll, and the sheet can be thrown + * shut with a flick. It opens from the bottom, which is where a thumb already + * is on a tall phone; the top of a 812px screen is the hardest place to reach. + */ +interface MobileNavDrawerProps { + open: boolean; + onOpenChange: (open: boolean) => void; + links: readonly { to: string; label: string; end?: boolean }[]; + isActive: (to: string, end?: boolean) => boolean; + node: NodeInfo | null; + stats: NodeStats | null; +} + +/** Machine-readable copies, the thing that makes this explorer unusual. */ +const TEXT_SURFACES = [ + { href: '/llms.txt', label: 'llms.txt' }, + { href: '/skill.md', label: 'skill.md' }, +]; + +export function MobileNavDrawer({ + open, + onOpenChange, + links, + isActive, + node, + stats, +}: MobileNavDrawerProps) { + // autoFocus moves focus into the sheet on open. Vaul leaves it off by + // default, which left focus on : 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. + return ( + + + + + {/* The grabber is the affordance for the drag Vaul provides; without + it the sheet gives no sign it can be thrown shut. */} +
      - - {menuOpen && ( - -
        - {NAV_LINKS.map(({ to, label, end }) => ( -
      • - - {label} - {isActive(to, end) && ( -
      • - ))} -
      - {stats && ( -

      - {stats.repos.toLocaleString()} repos · {stats.agents.toLocaleString()} agents -

      - )} -
      - )} -
      +
      ); } From cbcf2880e07e4d82a75eba9ad7980f703fef6eda Mon Sep 17 00:00:00 2001 From: Vasanthdev2004 Date: Wed, 19 Aug 2026 15:52:23 +0530 Subject: [PATCH 5/6] fix(network): the federation map ran off the side of a phone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/components/network/FederationMap.tsx | 7 ++++--- src/components/network/NetworkGlobe.tsx | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/network/FederationMap.tsx b/src/components/network/FederationMap.tsx index d29a099..47ebd9f 100644 --- a/src/components/network/FederationMap.tsx +++ b/src/components/network/FederationMap.tsx @@ -47,7 +47,8 @@ function NodeChip({ {lead ? : } - {snap.node.label} + {snap.node.label.split('.')[0]} + {snap.node.label} {snap.stats && ( @@ -74,11 +75,11 @@ export function FederationMap({ snapshots }: { snapshots: NodeSnapshot[] }) { return (
      -
      +
      {shown.map((s, i) => ( ))} diff --git a/src/components/network/NetworkGlobe.tsx b/src/components/network/NetworkGlobe.tsx index 10c1903..630484b 100644 --- a/src/components/network/NetworkGlobe.tsx +++ b/src/components/network/NetworkGlobe.tsx @@ -58,8 +58,11 @@ export function NetworkGlobe() { [isDark], ); + // The globe is smaller on a phone. The sphere is decorative — the markers + // are a stock world spread, not peer positions — and at 343px square it took + // most of a screen, nearly all of it dark sphere against a dark ground. return ( -
      +
      ); From 774547f379983f7fcccf14574471e7384f3bce3b Mon Sep 17 00:00:00 2001 From: Vasanthdev2004 Date: Wed, 19 Aug 2026 16:05:45 +0530 Subject: [PATCH 6/6] fix(a11y): layer the default focus ring so components can style their own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/index.css | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/index.css b/src/index.css index 108658b..8119823 100644 --- a/src/index.css +++ b/src/index.css @@ -379,9 +379,22 @@ body { } ::-webkit-scrollbar-thumb:hover { background: var(--color-muted); } -:focus-visible { - outline: 2px solid var(--color-accent); - outline-offset: -1px; +/* The default focus ring MUST live in @layer base. + + Unlayered, it beat every Tailwind focus utility, because unlayered CSS wins + over layered CSS regardless of specificity — the same rule that broke the + border-color reset above. A control that had been given its own focus + treatment got this one instead: the command palette drew a hard 2px box, + inset by a pixel, around a search field that sits flush inside a rounded + panel and already shows focus with its caret. + + Layered, this stays the default for anything that does not style its own + focus, and a component that does style it now wins. */ +@layer base { + :focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: -1px; + } } a { text-underline-offset: 0.2em; }