Repository navigation
refactor: render the app with Solid, mount pages on demand (@miodec) - #8467
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Page components no longer watch the active page: the common <Page> gate wraps them at the mount site, so their setup runs on open and cleanup on close. Drop enabled: isOpen() query gating, profile search focuses on mount, leaderboard invalidates its cache on cleanup, and the account settings url sync moves from module scope into its page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
leaveTestPage runs on test page cleanup and in beforeHide. bindTestElements handles ads, incomplete tests, funbox clear and modifier/monkey key listeners (shared key-state util) with the mount's abort signal. Autofocus keydown and resize handling move into test-ui init. restartIfNotStarted replaces module level focus/visibility listeners. isMounted() replaces getActivePage() checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One Pages component, mounted into <main>, renders the shown page (loading while loadingShown, else the active page) in an out-in switch transition with 125ms opacity fades. Pages and their wrappers mount on enter and unmount on leave; content mounts once the wrapper is attached. Static page wrappers, per-page mountpoints, Skeleton page usage and the common Page gate are gone. page-controller no longer animates elements: it flips loadingShown for sync loading and awaits the shown page's fade in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
startTest checks canStartTest() (test page mounted and not leaving). route-controller reads page-controller's navigation guard. Focus and autofocus keydown guards dropped. Test and loading page modules and display/afterHide/afterShow removed from Page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- isLongTest recomputes when the test page mounts - test-page-state is the single source of "mounted" (reactive), set by TestUI.init; TestUI.isMounted reads it. Remount before old unmount stays mounted - explain why forced test->test navigation only stops the test - rename bindTestElements to mountTestPage - Pages: clearer names (shownPageEl, isBootPage), drop duplicated element var Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e456b8e to
d05e3e3
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
An app-wide rewrite of page and test-page lifecycle whose correctness hinges on Solid transition/ownership internals that warrant human verification despite no concrete defect found.
0 open findings
What changed in this PR
This PR refactors frontend page navigation from "all pages rendered at boot and toggled hidden" to on-demand mounting with an out-in fade. A single Pages component renders only the shown page (isLoadingShown() ? "loading" : getActivePage()), mounting a page on entry and unmounting it on leave — including the test page, so leaving ends the test. A createSwitchTransition guarantees two pages are never in the DOM at once. The legacy PageTransition flag, Page gate component, per-page isOpen/enabled guards, static page wrappers in index.html, and Skeleton page usage are removed; test-page lifecycle (ads, listeners, funbox, resize/autofocus) now binds to the mount's AbortSignal.
Changes:
- New
Pages/page-registrydrive on-demand mounting + out-in fade;page-controllerreplacesPageTransitionwith an internalnavigatingguard andwaitForShownPage(). - Test lifecycle centralized in
mountTestPage/test-page-state(canStartTest,leaveTestPage); key-state listeners (monkey/modifiers) and autofocus/resize moved onto the mount signal. - Page components drop
isOpengates (work runs inonMount/onCleanup); docs added (GLOSSARY, ADR 0001/0002) and new tests for the core pieces.
| File | Description |
|---|---|
| components/pages/Pages.tsx | New core: switch-transition renderer, fade, waitForShownPage, attach-gated content mount |
| components/pages/page-registry.ts | Declares each page's component + wrapper options |
| controllers/page-controller.ts | navigating guard replaces PageTransition; loading via setLoadingShown; batches active-page/loading |
| controllers/route-controller.ts | Busy check uses PageController.isNavigating() |
| test/test-page-state.ts | New mount/leaving state, canStartTest, reactive isTestPageMounted |
| test/mount-test-page.ts | Renamed from bind; owns ads, incomplete reset, funbox, key listeners on signal |
| test/test-ui.ts | isMounted via isTestPageMounted; autofocus + resize handlers bound to signal |
| test/test-logic.ts | leaveTestPage/restartIfNotStarted; startTest gated by canStartTest |
| test/focus.ts, event-handlers/global.ts, ui.ts | Remove PageTransition/moved autofocus & resize logic |
| states/monkey.ts, modifiers.ts | Replace page-effect listeners with listenFor*Keys(signal) via new utils/key-state |
| states/test.ts, loading-page.ts, account-settings.ts | Recompute on mount; isLoadingShown/bar-start signals; URL sync moved into pages |
| components/pages/*Page.tsx, common/Page.tsx, mount.tsx, index.html | Drop <Page> gate/static wrappers; single data-component="pages" mountpoint |
| tests/**, docs/adr/**, GLOSSARY.md | New tests and documentation |
No blocking objective issues were found: removed/renamed symbols have no dangling references, imports resolve, ids/classes are preserved (capitalizeFirstLetter matches old wrapper ids), and the behavioral changes (fade-in keypress, beforeShow overlap, mount cost) are intentional and documented. The core Pages transition/ownership behavior depends on Solid + @solid-primitives/transition-group internals that cannot be fully verified statically here; it is covered by new tests and the author's browser testing.
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
# Conflicts: # frontend/src/ts/test/test-ui.ts
# Conflicts: # frontend/src/ts/states/modifiers.ts # frontend/src/ts/states/monkey.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove unused beforeHide, path and async loading mode, and scope the loading bar keyframe abort to a single load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page wrapper is attached before page content mounts, so the frame wait is no longer needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Index renders App into the body instead of mounting components into static mount points. Static fallback markup stays in index.html. Import sorting is disabled for the entry since import order has side effects. Also removes the unused video ad popup and moves the alerts popup into modals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds monkeytype-rules/no-module-scope-dom-query: DOM queries that run on module load break now that Solid renders the app. Browser docs now check frame timing instead of visibilityState and note that the console buffer survives reloads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
One
Pagescomponent renders the shown page, and only that page. A page mounts when it is navigated to and unmounts when it is left; this includes the test page, so leaving it ends the test. The page fade is an out-in switch transition, so two pages are never in the DOM at once. The loading page is one of the switched pages. The router fromrouter-rewriteis not included.Page lifecycle now lives in the page itself:
mountTestPageowns the ads, incomplete-test reset, funbox clear, modifier/monkey key listeners, resize handling, autofocus, and focus/visibility restart, all bound to the mount's abort signal.canStartTest()is true when the test page is mounted and not leaving.isOpengates andenabled: isOpen()queries are gone; open work runs inonMountand close work inonCleanup. Leaderboard and account settings URL syncing moves into their pages.PageTransitionflag, thepages/test.tsandpages/loading.tsmodules, the common<Page>gate, andSkeletonusage for pages.Docs: root
GLOSSARY.md(Navigation section) and two ADRs,0001-pages-unmount-on-leave.mdand0002-out-in-page-fade.md.Evidence
Tests:
npx vitest runin frontend: 1131 tests pass in 63 files. New tests:Pages.spec.tsx:.pagein the DOMneedsAuthenticationpage renders an empty wrapper when signed outtest-page-state.spec.ts:canStartTest.page-controller.spec.ts: the navigation guard.states/test.spec.ts:isLongTestis recomputed when the test page mounts.key-state.spec.ts: the shared key-state util.Browser (dev, signed in):
.pagein the DOMNot checked: signed-out boot, and modifier/monkey keys with real hardware key state.
Merge Danger
Door: two-way
Pure frontend refactor, no data or API changes. A revert brings back the old static wrappers.
Blast Radius: app-wide
Every navigation goes through the new path. Things to watch:
beforeShowtiming:beforeShownow overlaps the old page's fade-out. A slowbeforeShow(friends'invalidateConnections) can let the page mount before it finishes.🤖 Generated with Claude Code