Skip to content

refactor: routing rewrite on TanStack Solid Router (@miodec) - #8459

Open
Miodec wants to merge 172 commits into
masterfrom
router-rewrite
Open

Miodec wants to merge 172 commits into
masterfrom
router-rewrite

Conversation

@Miodec

@Miodec Miodec commented Oct 13, 2026

Copy link
Copy Markdown
Member

Replaces #8451. Pages move to SolidJS and are rendered by their TanStack Solid Router routes; the router's own pending, error and not-found handling replaces the hand-rolled page state. Decisions: docs/adr/0001-routes-render-pages.md, 0002-out-in-page-fade.md, 0003-pages-show-before-background-data.md; terms in GLOSSARY.md.

Summary

 <RouterProvider>
-  <Pages>                         # outside the router, staticData.page picks the page
-    hand-rolled fade, loading page, load error signals
+  <RootLayout>                    # out-in page fade around <Outlet/>
+    loading page                  # while the loading bar or pending spinner shows
+    <Outlet/>
+      <Page>                      # mounts content once attached, Suspense net
+        <AsyncContent>            # own Suspense per section
+  defaultPendingComponent         # loading page + spinner after 500ms, kept ≥500ms
+  defaultErrorComponent           # loading page error + Retry (router.invalidate)
+  notFoundComponent               # 404 page, replaces the `$` route

Navigation:

link hover            -> intent preload (preload-safe: no loading steps, no global state)
onBeforeNavigate      -> leaving the test page stops the test before the fade out
history blocker       -> test restarting, result calculating, no-quit funbox, loading bar shown
app beforeLoad        -> first auth wait, plans one loading bar for the navigation
  loading steps       -> user data, results (account), friends; each a share of 0-90, then "Done"
loaders               -> background data: prefetchQuery, not awaited
  • Routes read auth and queryClient from router context.
  • Typed navigation everywhere (navigate({ to, params, search })); Button / Balloon are real links (createLink, cmd+click).
  • The URL is the source of truth for leaderboards, settings and account settings selections (validateSearch + useSearch, replace writes); ?mode2=15 keeps its format.

Evidence

  • Before: no tests at the router boundary; old Pages.spec mocked page selection.
    After: app-shell tests drive the real route tree in a real RouterProvider with memory history, fake auth and a test query client: guards and redirects, loading bar plans (refresh on account 0-50 / 50-90 / Done, refresh on test 0-90 / Done, test -> account 0-90 / Done), fades (never two pages, rapid navigation ends on the last page), pending spinner (fake timers), errors + Retry, 404, preload safety, search params, leaving the test page. Frontend: 63 files, 1416 tests passing; type check clean.
  • A dev-transform spec runs the dev Solid/Vite transform over every .tsx file (catches component patterns tests and lint miss, e.g. function overloads).
  • Browser verification: in progress.

Merge Danger

Door: two-way

Frontend only, no API or data changes. Revertable.

Blast Radius: navigation

Every page change, first load, sign in/out redirects, and leaving the test page go through new code. Known behaviour changes:

  • Bare /leaderboards redirects to the last shown selection.
  • Navigation is blocked while the loading bar is shown.
  • zod-urlsearchparams dependency removed.
  • Leaderboard entries are preloaded on hover for the hovered selection only.

Miodec and others added 30 commits October 2, 2026 16:10
# Conflicts:
#	frontend/src/index.html
#	frontend/src/ts/components/common/Setting.tsx
#	frontend/src/ts/components/mount.tsx
#	frontend/src/ts/components/pages/test/TestPage.tsx
#	frontend/src/ts/components/pages/test/result/replay.ts
#	frontend/src/ts/controllers/chart-controller.ts
#	frontend/src/ts/elements/caret.ts
#	frontend/src/ts/elements/test-init-failed.ts
#	frontend/src/ts/event-handlers/test.ts
#	frontend/src/ts/index.ts
#	frontend/src/ts/test/caret.ts
#	frontend/src/ts/test/funbox/layoutfluid-funbox-timer.ts
#	frontend/src/ts/test/funbox/memory-funbox-timer.ts
#	frontend/src/ts/test/pace-caret.ts
#	frontend/src/ts/test/result.ts
#	frontend/src/ts/test/test-logic.ts
#	frontend/src/ts/test/test-screenshot.ts
#	frontend/src/ts/test/test-ui.ts
Adopt master's test page unmount (bindTestElements, optional carets) over
the branch's placeholder refs; test page show/hide logic stays in TestPage,
driven by the router signals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFv5oixtD54uhzpzvLu34N
- move route/loading/fade/transitioning state into router/page-transition.ts, driven by events from the router, withLoading and Pages
- route load failures go through tanstack (loaders throw) instead of a global error flag
- render the test page like every other page (drop the always-mounted shell)
- route replaceSearch through the router so its location stays in sync
- add isTestPageActive memo
- remove leftover lorem ipsum from settings page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFv5oixtD54uhzpzvLu34N
- move navigation guards from navigate() into a router history blocker
  (covers links, navigate() and back/forward; redirects and
  replaceSearch bypass it)
- register router type so `to` is checked against the route tree
- Button takes `to`/`params` and renders Link; `href` is external only
- provide router context to all component roots so Link works outside
  RouterProvider
- remove initRouter click delegate, router-link attr and css hack
- remove unused `force` option from navigate()

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into solid-pages

# Conflicts:
#	frontend/src/ts/components/layout/footer/ScrollToTop.tsx
…ith plain signals and functions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous page fades out before the next one mounts, tracked by a local
signal in Pages. Navigation is no longer blocked during fades - the fade
always swaps to the latest target. startTest checks the route instead.
isTestPageActive is set by TestPage while mounted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
router.state is backed by solid signals, so the route page and load error
are memos over its matches instead of copies set in router events. The
router instance is a signal in navigate.ts so state modules can read it
without importing the route tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resize re-centering, autofocus, refocus restarts and the monkey/modifier
key listeners are registered by bindTestElements and removed when the
test page unmounts, instead of global listeners checking the active page.
Code that touches test elements checks TestUI.isMounted().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Components use useMatchRoute, vanilla code uses isTestRoute (router.matchRoute
over the router signal). TestPage no longer sets global state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The route page only changes once the next route has loaded, so check the
target page instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useIsTestRoute wraps useMatchRoute for components, listenForKeyState is
shared by the monkey and modifier listeners, and the loading page reset
reuses the initial indicator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…loaded signals

getSignal/signals read node values directly instead of the rAF-synced
mirror, signals registered after startup are tracked, and a hot reloaded
signal replaces the stale one with the same name and source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- drop isTestRoute/useIsTestRoute, use getRoutePage() everywhere
- loading-page.ts owns the loading page flag (hideLoadingPage)
- createSearchState + readSearchOnEnter for url search params
- test page lifecycle (ads, funbox, incomplete tests) moves into bindTestElements
- replaceSearch logs when the router isn't initialised

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Query reads inside the router suspend the nearest boundary (every route
is one), blanking the whole page. AsyncContent now wraps itself in
Suspense: its loader is the fallback, or its content without data when
alwaysShowContent is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The router holder is typed as the app router via a type-only import, and
navigate takes typed { to, params, search } options instead of url
strings, so a broken internal link fails the type check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Continuous integration check(s) failed. Please review the failing check's logs and make the necessary changes.

@github-actions github-actions Bot added the waiting for update Pull requests or issues that require changes/comments before continuing label Oct 16, 2026
@github-actions github-actions Bot removed the waiting for update Pull requests or issues that require changes/comments before continuing label Oct 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Continuous integration check(s) failed. Please review the failing check's logs and make the necessary changes.

@github-actions github-actions Bot added the waiting for update Pull requests or issues that require changes/comments before continuing label Oct 16, 2026
@github-actions github-actions Bot removed the waiting for update Pull requests or issues that require changes/comments before continuing label Oct 16, 2026
Miodec and others added 6 commits October 16, 2026 17:21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Anime: apply the initial state at once, start the enter animation on the
  next frame (content mounting right after insertion no longer eats it)
- AnimePresence: an element that exits and comes back fades in again
- AnimeShow: animate the fallback like the content
- AnimeSuspense: Suspense that fades between fallback and content
- tests for the above, AnimePresence exitBeforeEnter ordering tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PageSwitcher: keyed Show in AnimePresence instead of createSwitchTransition
  (page wrapper keeps the full-width grid layout)
- the router's pending component renders nothing: PageSwitcher shows the
  loading page; the loading page wrapper lives in PageSwitcher
- Page: a suspending page shows the loading page's spinner ("X page is
  still loading...")
- LoadingPage: no stale bar text under the spinner, spinner prop
- app-shell harness: animations finish at once, tests check the shown page.
  Fade ordering is tested with AnimePresence, plus a page's fade in starting
  after its content mounted

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	frontend/__tests__/components/common/anime/AnimePresence.spec.tsx
@github-actions

Copy link
Copy Markdown
Contributor

Continuous integration check(s) failed. Please review the failing check's logs and make the necessary changes.

@github-actions github-actions Bot added the waiting for update Pull requests or issues that require changes/comments before continuing label Oct 18, 2026
- comments still describing the createSwitchTransition page fade, the
  loading page as the router's pending component and the removed ADR
- PageSwitcher story: LoadingRoutePage is no longer exported, use an empty
  pending component like the app router

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the waiting for update Pull requests or issues that require changes/comments before continuing label Oct 18, 2026
Miodec and others added 6 commits October 18, 2026 13:06
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… place

Content was read in Show/Suspense, so every change to it built a new
wrapper and animated it in again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Errors outside a loading step showed only their message. They now read
"Failed to load the {page} page: ...". Loading step failures throw a
LoadingStepError and keep their message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Related to Markdown files and documentation frontend User interface or web stuff local dev Related to the build process or dependencies

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants