Skip to content

Fix the first paint, and route with TanStack Router - #15

Merged
DHUKK merged 2 commits into
mainfrom
fix-first-paint
Aug 17, 2026
Merged

DHUKK merged 2 commits into
mainfrom
fix-first-paint

Conversation

@DHUKK

@DHUKK DHUKK commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Two things a reader saw on first load, and a simplification that came out of fixing the second.

The theme flash

ThemeToggle applied the stored theme from an effect, so a reader who had picked light while the OS prefers dark saw the dark palette for a frame. An inline script in the head now stamps data-theme before anything renders.

CSS already covers the unpinned case through prefers-color-scheme, so system deliberately stamps nothing and falls through to it. color-scheme was already handled in three layers, so nothing changed there.

The deep-link flash

#/trust-auth showed the landing page until its capture arrived. App derived the screen from three separate pieces of state and fell back to the landing page while a fetch was in flight.

Each capture route now has a loader, so a screen does not render until its capture is in hand. This is structural rather than a guard flag. App.tsx is gone, replaced by the route tree.

Hash history, because GitHub Pages cannot rewrite an arbitrary path back to index.html, and it is the mode the router's own docs recommend for that case.

Routes gain a leading slash. A capture is now at #/trust-auth rather than #trust-auth. Nothing in the repo pointed at the old form and the sitemap lists only the root, so there was nothing internal to migrate.

Dropping the message deep link

Linking to one message is removed rather than ported. It meant carrying a session id and a packet id through the router, the loaded capture and the explorer, and it was the only reason the packet list centred a row it revealed. The message index is reference data now, so its Example column goes with it.

That also fixed a real bug: arrow-keying off the bottom of the packet list jumped the next row to the middle of the viewport. Centering was deliberate, but only ever intended for the deep link. It never did anything for Home/End either, since the centred position always exceeded maxScrollTop and clamped to flush anyway.

The list now aligns a revealed row to the nearest edge, matching what hexWindow.ts already did, so a keyboard step moves one row. Two tests were added for exactly that, since the old ones only covered large jumps and so never caught it.

Smoother stepping

scroll-behavior: smooth on the list, with a prefers-reduced-motion override. Only programmatic scrolls are affected, so the wheel and scrollbar stay native.

This required focus({ preventScroll: true }) on the row. At the point the focus effect runs the row is still off-screen, so focus() would reveal it instantly and cancel the animation.

Notes for review

  • Held arrow keys do not drift, because scrollTopToReveal derives an absolute target from the row's offset rather than a delta, so a keypress mid-animation just retargets.
  • Home/End now animate across the whole list. Capped by the browser at a few hundred ms, but if it reads badly the fix is to animate only small deltas in JS instead of via CSS.
  • A missing saved capture used to set a transient notice and clear the URL. It now renders the landing page with the reason in the error banner and leaves the URL alone, because setting React state from a loader is not clean.
  • MESSAGE_EXAMPLES is kept but is now imported only by its test, which guards that each entry names a packet that really is that message type. That leaves test-only data in a module whose header says "display data only", so it may belong in the test file.
  • The router adds roughly 27KB gzipped.
  • Verified: typecheck, 216 tests, build. Smooth scrolling and the two flashes were checked by hand in the browser.

DHUKK added 2 commits August 17, 2026 22:04
ThemeToggle applied the stored theme from an effect, so a reader who had
picked light while the OS prefers dark saw the dark palette for a frame.
An inline script in the head now sets data-theme before anything renders.

The storage key is duplicated there because the script cannot be a
module. CSS already covers the unpinned case through prefers-color-scheme,
so "system" deliberately stamps nothing and falls through to it.
A deep link such as #/trust-auth showed the landing page until its capture
arrived. App derived the screen from three separate pieces of state and
fell back to the landing page while a fetch was still in flight. Each
capture route now has a loader, so a screen does not render until its
capture is in hand. App.tsx is gone, replaced by the route tree.

Hash history, because GitHub Pages cannot rewrite an arbitrary path back
to index.html. Routes gain a leading slash as a result, so a capture is
now at #/trust-auth rather than #trust-auth.

Linking to a single message is removed rather than ported over. It meant
carrying a session id and a packet id through the router, the loaded
capture and the explorer, and it was the only reason the packet list
centred a row it revealed. The message index is reference data now, so its
Example column goes with it.

The packet list aligns a revealed row to the nearest edge instead, which
is what hexWindow.ts already did, so a keyboard step moves the list by one
row rather than half a viewport. That scroll is animated, and the row's
focus() call passes preventScroll so it cannot cancel the animation.

The message summaries also gain backticks around TLS, GSSAPI, SQL, COPY
and the rest, so renderInline marks them up like every other literal.
@DHUKK
DHUKK merged commit c930b17 into main Aug 17, 2026
2 checks passed
@DHUKK
DHUKK deleted the fix-first-paint branch August 29, 2026 23:39
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.

1 participant