Explorer redesign: identity, control system, docs, and search - #11
Merged
Conversation
hook.cache.json records per-session edit counts keyed by absolute paths on the machine that produced it, so it is local state rather than project config. config.json, which carries the shared detector settings, stays tracked.
…tion Adds @radix-ui/react-select (the sort menus), tw-animate-css (the enter/exit utilities Radix drives from data-[state]), github-slugger (heading anchors for docs search), motion, gsap, cobe, and the Magic UI / Aceternity runtime deps. tsconfig gains baseUrl and the "@/*" path mapping. Without it the shadcn CLI resolved "@/lib/utils" as a literal directory and wrote vendored components into a folder named "@" at the repo root. eslint.config.js exempts the vendored registry components from react-hooks/purity and react-hooks/set-state-in-effect. Those files are generated and `shadcn add` overwrites them, so the violations cannot be fixed in place.
Installed through the shadcn CLI from the Magic UI and Aceternity registries. They are checked in rather than depended on, which is how those registries are meant to be used: the source is the deliverable. Two fixes applied on top of what the registry shipped, both silent failures: - The pulse keyframes declared `boxShadow:` — a JS property name in a CSS block. Browsers dropped the declaration, so PulsatingButton's glow did nothing. Four declarations corrected to `box-shadow`. - ScrollProgress hardcoded a purple/pink/orange gradient. This palette is monochrome and inverts with the theme, so the bar now paints the foreground.
Colours are extracted from the live gitlawb.com rather than approximated: pure #000 ground, pure #fff text, Geist Mono throughout. The over-scoped theme customizer is removed in favour of a single dark/light toggle. Notable fixes folded in here, each of which broke something silently: - The `*` border-color reset sat outside any cascade layer, so it beat every layered border-color utility regardless of specificity. `border-success`, `border-danger`, and `border-transparent` all painted the same faint white. Moved into @layer base. - Syntax-highlight .tok-* colours existed only for dark, leaving near-white glyphs on the light ground. Both sets now defined. - The light palette had been mirrored from dark, but alpha compositing is not symmetric: black at 45% over white is 1.75:1 where white at 45% over black is 10:1. The light ramp is retuned, and --success moved to #00712f, which clears 4.5:1 on both the page and its own tint. - AuroraText and AnimatedGradientText painted through background-clip with a hardcoded ramp, so they rendered invisible in light mode with nothing for a contrast check to see. The ramp is now themed. Radius: rules and list hairlines stay square, controls take a full round to match the search field, and floating panels take 12px. useIsDark subscribes to the class on <html> via useSyncExternalStore, because canvas components (the globe, the icon cloud) bake colours into textures and image URLs and must re-render on a theme flip. useTheme cannot serve that — it holds its own state per call site, so a second caller never hears the toggle. useTheme also forces a full style recalculation after the flip. Chromium does not reliably invalidate `color` on existing elements when an inherited custom property changes on the root: measured after one toggle, 76 of 236 .text-muted elements kept the previous theme's colour while a freshly created element with the same class in the same parent resolved correctly.
At zero radius a bordered control reads as a hard box, and a listing page puts hundreds of them on screen at once. Buttons are frameless by default, fields are ruled with a single underline, and a frame or a fill is reserved for the control that has to stand apart. SearchField defines the one search shape every listing page uses, so there are no longer four slightly different ones. Dropdown replaces the native <select>. A native select paints its option list as an operating-system popup that lives outside the document: it cannot take this page's colours, its radius, or its type, and on Windows it draws as a grey box. Styling the closed control did nothing to the opened one. Built on Radix Select, which renders the list in the page while keeping the keyboard and screen-reader behaviour the native control had. SegmentedControl replaces loose pill rows for single-choice filters. One track, one indicator that slides between options, positioned from the active button's measured box and animated with a CSS transition. Motion's layoutId is the idiomatic way to write this, but the measured version's correctness is checkable without watching an animation, and it does not depend on Motion's projection surviving the router transition a filter click schedules. Also fixes every `rounded-[--radius]` in the kit. That is Tailwind v3 syntax for a CSS variable; v4 requires `rounded-[var(--radius)]`. The class compiled to nothing, so these controls had been drawing square no matter what the token said.
The nav rail is a single Motion element shared across links via layoutId, so it slides between items rather than cross-fading. The palette trigger is icon-only with the shortcut in its tooltip. Adds lib/platform.ts. The palette handler accepts metaKey || ctrlKey, but the UI hardcoded the Mac glyph, so Windows and Linux visitors were told to press a key their keyboard does not have. modifierKeyLabel() reports the right one.
One toolbar row: the shared search field, a sort dropdown, and the fork filter as a segmented control. Rows are denser and the constant-value pills that carried no signal are gone. The fork filter runs over the repos already fetched rather than the whole node, so while the list is paginated the scope is stated. Without it, filtering to "forks" answered "no repositories match" having examined 50 of 3,207 — which reads as "this node has no forks". timeAgo clamps at zero. The node's clock can sit ahead of the browser's, which produced timestamps like "-28s ago" on freshly pushed repos; under five seconds now reads "just now". Two tests cover it.
Removes StatsPanel, whose figures duplicated the header. The file tree, commit list, certificates, issues, and pulls move onto the shared control kit. Fixes a deep-link hang: opening ?tab=certs directly left the tab on "loading…" forever. The ref marking a tab as started survives StrictMode's simulated remount, while the cleanup aborts the in-flight fetch — so the retry was skipped because the marker said the work was already underway. The marker is now cleared when its request aborts.
Adds four sorts and a trust-band segmented filter. Band counts are computed after the search but before the band filter, so each band reports what it would yield rather than collapsing to the active one. AgentRow drops the gauge and the tier colours for a text readout, and prints a status chip only when the status departs from active — at 4,718 agents the decoration was the majority of the ink. AgentEcosystem is an icon cloud of the tooling that can drive this network. Its heading is "Works with" deliberately: the agents API returns did:key identities with no vendor field, so nothing here claims which of these have agents registered. Mark colours are part of the CDN request URL, so they are re-fetched per theme — white logos are invisible on the light ground.
The globe is deliberately illustrative. The node API exposes no geography — a
peer record is { did, http_url, last_seen, reachable } and nothing more — so
the markers are a stock world spread, not peer locations. A map pin gets read
as a claim about where a machine is, so the real federation view stays in the
topology diagram and the node cards below, which are driven by reachability.
Its colours are painted into a canvas and cannot come from CSS tokens, so they
switch on the theme here: a dark sphere on the light ground reads as a hole in
the page. The component replaces cobe's default config wholesale rather than
merging, so every field has to be supplied — the stock default ships ten
hardcoded demo cities.
Events moves to the shared SearchField; it was the last page hand-assembling its own search input, a near-verbatim copy that would have drifted the moment SearchField changed. The task status filter becomes a segmented control, with each status keeping its colour dot — SegmentedOption gained an icon slot for it, and the indicator now tracks both axes so a long filter row can wrap without the indicator landing on the wrong line.
Three columns: a sticky section nav, the article, and the scroll-spy TOC. The old layout capped the prose at 70ch and left the right half of the screen empty. Every document opened with its title twice — once from the page header and once from the markdown h1 — so the leading h1 is dropped. Each fence gains a language label and a copy button. Section headings gain anchors. A prev/next pager closes each page, since the four documents are meant to be read in order. "Read this as text" is now on the page at every width. It was hidden below the lg breakpoint, so the single thing that distinguishes these docs — that an agent can fetch the same words as raw markdown — was invisible on anything narrower than a laptop. Heading ids are now a pure function of the markdown. gfmHeadingId keeps one slugger on the shared engine and its de-duplication counter carries across parse calls, and engine.parse awaits Shiki for every fence, so two renders started close together interleave and feed the same slugger. StrictMode starts the fetch twice, so this was the normal case: ids came out `prerequisites-1`, then `-2`. Every deep link and every TOC anchor depended on how many times a document had been rendered that session. Parses are now serialized, and reset between.
The four documents are fetched once on first open and split at their headings, so a result lands on the paragraph that matched rather than the top of a page. Heading matches outrank body matches; fenced commands are indexed, so searching "gitlawb/gl" finds the install block. Anchors come from github-slugger, the same slugger the renderer uses — deriving the rule by hand would drift and produce links to anchors that do not exist. The parser splits on either line ending. These files are checked out CRLF, and splitting on "\n" alone leaves a trailing "\r" that the heading pattern can never match, because `.` in JavaScript excludes carriage return as a line terminator and `(.*)$` therefore stops short of the end of the string. Every heading silently failed and each document indexed as one undivided section. The palette itself takes the radius tokens and a per-kind mark, so a row's target is legible before the label is read. Its input is now a proper combobox with aria-controls and aria-activedescendant, and section headings are siblings of the rows rather than nested inside them — they had been announced as part of the first result.
vercel.json has no path that 404s: /docs/:slug rewrites into this function and :slug matches a segment containing a dot, while /(.*) catches everything else and serves index.html. So when public/docs/<slug>.md missed the filesystem the subrequest did not fail — it was rewritten back into the same function, which answered 200 with the hub index because the subrequest's Accept is not html. That was then served as the requested document, with Content-Type text/markdown and an hour of CDN caching. response.ok was always true, so the 502 fallback below it was unreachable. Two guards. The subrequest carries x-gitlawb-doc-markdown; reaching the handler with that header means the static file was absent, because a file that exists is served before rewrites are consulted, so it answers 404 at the first hop. And the fetched body is rejected if it is the SPA shell or contains the hub's marker line — fetchIndexHtml already guards its own fetch this way, by testing the body rather than trusting the status. A listed slug whose file is missing is a broken deploy, not a passing outage, so it now returns an uncached 404 rather than a cached 502 claiming to be temporary. The test stub modelled the deployment wrongly, which is why this survived: it returned 404 for absent files, under a comment claiming to reproduce deployment behaviour. It now performs the actual rewrite. Verified by disabling both guards — exactly the four new tests fail, the eleven existing ones pass.
quickstart.md and protocol.md both told the reader to verify a push on the global /events feed. That feed carries only ref updates gossiped in from peer nodes; a push this node received directly is recorded as a per-repo signed certificate and is absent from the feed's data entirely. This is the failure commit 8e941ee removed from the UI, still present in the prose — and in quickstart it lands at the worst moment, immediately after a first-time reader's first push, where an empty feed reads as a failed write. Both now point at the repository's certificates, which is what agents.md already said. agents.md listed "clone fails on https:// URL | wrong scheme | clone URLs must be gitlawb://". The node serves standard git smart-HTTP at the https clone_url, and protocol.md documents that transport. An agent hitting the most common documented failure — git-remote-gitlawb missing from PATH — was being steered away from the one fallback that works.
PRODUCT.md carries what the code cannot: who the surface is for (developers and agents evaluating the network), the visual register, the directions that were tried and rejected, and that Aceternity, Magic UI and GSAP are to be used as real components rather than reimplemented by hand. The detector exemption records why "geist mono" is not an overused-font finding here: the owner requires the explorer to match gitlawb.com, whose live site computes Geist Mono on body and h1.
The page ended wherever the activity list happened to stop. Measured: the sidebar's content came to 936px against the main column's 532px, and because both grid columns stretch to the taller one, that left 387px of dead space under the wide column — the page read as truncated rather than finished. Three additions, and one of them is placed to fix that directly. "Repositories on this node" goes in the main column, not below the grid, where it would have left the hole untouched. Six cards two-up come to roughly the height that was missing, so the columns now balance at 1061 against 936 and the remainder falls under the narrow column where it barely reads. The page had been closing on ref-update lines, which say the network is moving but not what is on it. Every field on a card is read from the repository record as the node returns it: star_count and forked_from are real columns, so the star count and the fork mark are facts, and both are omitted when they carry nothing. Order is whatever the node returns — fetchRepos omits the sort param when the sort is 'updated', so nothing claims these are the most recently touched, and the hook now records that so no future heading asserts it. "Built for agents" states the machine-readable surface on the front page: llms.txt, skill.md, and docs/agents.md, each a real file served from this origin with a copy button handing over the absolute URL an agent needs. This is the claim the product is built on, and it was only reachable through /docs — the overview sold the explorer as a dashboard and said nothing about the part that is unusual. "Start here" is the closing beat: publish, operate, understand. Every destination is a route declared in App.tsx. CopyButton gains srLabel. Its `label` was both the visible text and the accessible name, so a descriptive name rendered in full as button text. Three copy buttons in one list now announce which file each belongs to instead of "copy Copy" three times over, and no existing call site changes visually.
Both sections read /events/ref-updates, which carries only ref updates gossiped in from peer nodes. A push this node received directly is recorded as a per-repo signed certificate and never enters that feed. Measured against a live node: all 50 events returned had from_peer set, the newest was 2,919 minutes old, and all six repositories rendered further down the same page had been updated within the previous 51 minutes. Headed "Latest ref-update" and "Recent activity" with no qualifier, an actively used node reads as one that stopped two days ago. The data was right; the labels were not. They now say what the feed is, in the same terms /events already uses, and the list states where local pushes are recorded instead. The ordering and de-duplication were checked at the same time and are correct: the API returns newest-first, and the first distinct event is the one printed above the list, so slicing from index 1 skips exactly the row already shown.
The P2P section drew animated beams from this node to three named peers taken from `peers.filter(p => p.reachable)` — HTTP reachability from GET /peers, which says nothing about libp2p connectivity. Two lines below, the same section printed the real figures. Measured on a live node: 16 of 75 peers answer over HTTP, connected_peers 0, gossipsub_mesh_peers 0. The picture claimed a live mesh while the numbers in the same box denied it. Beams are now drawn from connected_peers, so none appear when nothing is connected. They are unlabelled, because /p2p/info reports how many peers are connected and not which — naming them means borrowing identities from the HTTP list and asserting a link the API never made. An empty mesh is stated as the ordinary condition it is, and the HTTP-reachable count is kept as its own separate figure rather than being quietly used to stand in for gossip. Clone a repository now names what it clones. The command is real, but it wraps a ~60-character did:key remote over three lines, so the repository was unreadable, and the button underneath offered to copy a command for something the reader could not identify. Agent tasks gains a proportion bar. This register is overwhelmingly one status — 198 of 200 completed here — and four figures in a row read as four comparable quantities. Widths come from the same counts as the numbers, so the two cannot disagree. The bar's fills come from a new taskStatusFill rather than from taskStatusColor with the prefix swapped at runtime. Tailwind generates CSS only for class names it can read in the source, so "text-success".replace(...) would have produced no rule and painted an invisible bar.
Rows carried 24px of vertical padding and came out 47px tall, so a twelve-entry tree ran past a screen and reading a repository's contents meant scrolling. A file list is for scanning; the row is now sized to its content at 33px, close to what a forge uses. On a 168-entry tree that takes the list from 7,932px to 5,579px. Two smaller corrections in the same pass: The size column was w-20, too narrow for "32.2 KB", so the unit wrapped under the number and that row alone stood 4px taller than its neighbours. It is now w-24/w-28 with the value held on one line. Both cells carry a fixed 20px line box. The size column is set in mono, whose taller line box would otherwise make every row that carries a size deeper than the directory rows that carry none. Rows now measure uniformly.
Vasanthdev2004
marked this pull request as ready for review
August 17, 2026 17:56
The component rendered `startValue` as its DOM text and only ever wrote the real figure from the spring's change handler, which is gated behind useInView. Any statistic below the fold therefore displayed "0" for a value that was already loaded — on the network page, "0 repos" for a node holding 2,120 — until the reader happened to scroll it into view, and permanently anywhere the animation could not run. Zero is not a placeholder. NodeCard prints "—" when a figure is genuinely missing, so a rendered 0 reads as a measured fact, and it was false. The span now renders the true value. When the animation runs its first frame overwrites this within a tick and the count-up is unchanged; when it does not, the number is simply correct. Verified with animations suppressed: the four node cards render 3,208 / 4,718 / 51,451 and 2,120 / 190 / 65, matching /api/v1/stats and /nodes/node2/api/v1/stats exactly, where previously every figure rendered 0. Vendored from the Magic UI registry — `shadcn add number-ticker` overwrites it.
A browser scrolls to the top on a real navigation; a single-page app does not, because the document never changes. Nothing in this app compensated, so following the "Next" pager at the foot of a document loaded the next one and left the reader parked at its bottom, looking at the pager again — the content had changed but the viewport had not moved. Every cross-page link behaved the same way; the pager only made it obvious, because that control sits at the very end of the page. ScrollToTop keys on the pathname and leaves three cases alone: - Back and forward. POP means the browser is restoring a position the visitor already had, and overriding that loses their place. - Anything carrying a hash, which names a heading to land on. The page owning that anchor scrolls to it once its content has rendered. - Query-string changes. Search, filters, sorting and paging rewrite the query while staying on the same page, and resetting there would yank the view to the top on every keystroke typed into a search field. Verified: pager 2707 to 0, sidebar link 1200 to 0, back restores 2707, and typing into the repository search at scroll 600 leaves it at 623 on a page still 3367 tall. Two fixes to the docs anchor scroll, which this exposed. The hash now comes from the router and is a dependency. Reading window.location.hash and keying only on the slug meant a jump into a document already open changed nothing the effect watched, so following a second search result within the same page did nothing at all. And the scroll no longer waits for requestAnimationFrame. DocArticle rewrites that markup in a layout effect, and a child layout effect runs before this parent effect, so the heading already sits at its final offset. The frame only bought a dependency on the tab being painted: opened in a background tab, the callback was deferred and the reader arrived at the top.
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.
Full visual redesign of the explorer, plus the correctness fixes found while building it. 22 commits, each self-contained — the history is worth reading commit by commit rather than as one diff.
What changed
Identity. Tokens are extracted from the live gitlawb.com rather than approximated: pure
#000ground, pure#ffftext, Geist Mono throughout. The over-scoped theme customizer is gone, replaced by a single toggle.Controls. A frameless kit — buttons without frames by default, fields ruled with one underline, a frame or fill reserved for the control that must stand apart.
SearchFielddefines the one search shape every listing page uses; all five now measure identical. Native<select>is gone in favour of a Radix-backedDropdown. Single-choice filters share oneSegmentedControl.Pages. Repos, repo detail, agents, network, peers, events, tasks, and docs all rebuilt on that kit.
Docs. Three-column layout with a scroll-spy TOC, per-fence language labels and copy buttons, heading anchors, a prev/next pager, and the "read this as text" panel now visible at every width. ⌘K searches the four documents at section level and jumps to the matched heading.
Bugs found and fixed
Several of these were silent — the code looked right and did nothing.
rounded-[--radius]never compiled. That is Tailwind v3 syntax for a CSS variable; v4 requiresrounded-[var(--radius)]. 20 occurrences across 12 files, all inert. This is why controls stayed square regardless of the token.*border-color reset sat outside any cascade layer, so it beat every layered border-color utility site-wide.border-success,border-danger, andborder-transparentall painted the same faint white.gfmHeadingIdkeeps one slugger on the shared engine, andengine.parseawaits Shiki, so concurrent renders interleave and feed the same counter. StrictMode starts the fetch twice, making this the normal case: ids came outprerequisites-1, then-2. Every deep link and TOC anchor depended on render count. Parses are now serialized.vercel.jsonhas no path that 404s, so a missingpublic/docs/<slug>.mdwas rewritten back into the same function and answered200with the hub index — wrong content, correct content-type, cached an hour, with the 502 fallback unreachable.colorwhen an inherited custom property changes on the root. After one theme toggle, 76 of 236.text-mutedelements kept the previous theme's colour..excludes carriage return in JavaScript, so(.*)$never reached end-of-string and every document indexed as one undivided section.useThemecan't serve that, henceuseIsDark.⌘Kwas shown on Windows and Linux; the handler always acceptedctrlKey.timeAgoproduced-28s agowhen the node's clock ran ahead of the browser's.Documentation corrections
Three claims contradicted by this repo's own code:
quickstart.mdandprotocol.mdtold readers to verify a push on/events. That feed carries only gossip from peer nodes — a push this node received directly never appears there. This is the failure8e941eeremoved from the UI, still present in the prose, and in quickstart it lands right after a first-time reader's first push where an empty feed reads as a failed write.agents.mdclaimed https clones cannot work. The node serves standard git smart-HTTP at the httpsclone_url, andprotocol.mddocuments that transport.Verification
tscclean,eslintclean acrosssrcandapi, production build clean. 152 tests, up from 131 — new coverage for the docs search parser (including CRLF and anchor parity againstgithub-slugger), the docs endpoint's missing-file path, andtimeAgo.The four new endpoint tests were confirmed to fail with the guards disabled and pass with them, so they demonstrate the bug rather than merely accompanying the fix.
Notes for review
boxShadow:in a CSS block; a hardcoded gradient)./reposstill only sees the current page; the page now says so rather than implying the node has no forks.Not addressed
Hardcoded colours remain in some vendored components (
dot-pattern,tracing-beam,rainbow-button) and in the file-type icon map, and the command palette still lacks a focus trap. Flagged by an audit pass, deliberately left out of scope.Added after this PR was opened
column’s ~532px, and because both grid columns stretch to the taller one, the
page ended in ~387px of dead space. A repositories grid now fills the main
column, followed by the machine-readable surface and a closing set of paths.
loaded the next document and left the reader at its bottom, looking at the
pager again. Back/forward, hash links and query-string changes are
deliberately excluded.
window.location.hashand keyed only on the slug, so a second search resultinside the same page moved nothing.
NumberTickerno longer renders 0 until it animates. Any figure below thefold showed “0 repos” for a node holding 2,120 until it was scrolled into
view. Zero is not a placeholder — these callers print “—” for missing data.
feed; measured against a live node, all 50 events carried
from_peer, thenewest was two days old, and repositories on the same page had been updated a
minute earlier.
animated beams to peers taken from HTTP reachability while the same section
printed “0 connected · 0 in mesh” from libp2p.
from 7,932px to 5,579px.