Skip to content

refactor: render the app with Solid, mount pages on demand (@miodec) - #8467

Merged
Miodec merged 20 commits into
masterfrom
pages-mount-on-demand
Oct 15, 2026
Merged

Miodec merged 20 commits into
masterfrom
pages-mount-on-demand

Conversation

@Miodec

@Miodec Miodec commented Oct 15, 2026

Copy link
Copy Markdown
Member

Summary

One Pages component 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 from router-rewrite is not included.

 <main data-component="pages">
-  #pageLoading, #pageTest, #pageAbout, … (static, hidden, each with its own mountpoint)
+  <Pages registry>                      shown = isLoadingShown() ? "loading" : getActivePage()
+    createSwitchTransition(out-in)      125ms fade each way, appear: false
+      <Page name>                       div.page#page<Name>; content mounts once attached
 page-controller.change()
-  PageTransition.set(true)
-  fade old element out, Skeleton.remove
-  showSyncLoading: fade #pageLoading in … out
+  guard (internal `navigating`, replaces PageTransition)
+  leaveTestPage (if leaving test)
+  sync loading → setLoadingShown(true), bar/keyframes as before
+  batch(setActivePage(next), setLoadingShown(false))
   beforeShow
-  fade new element in, Skeleton.append
+  await waitForShownPage()              resolves when the fade in has finished
   focus reset, ad reinstate

Page lifecycle now lives in the page itself:

  • Test page: mountTestPage owns 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.
  • Other pages: isOpen gates and enabled: isOpen() queries are gone; open work runs in onMount and close work in onCleanup. Leaderboard and account settings URL syncing moves into their pages.
  • Removed: the PageTransition flag, the pages/test.ts and pages/loading.ts modules, the common <Page> gate, and Skeleton usage for pages.

Docs: root GLOSSARY.md (Navigation section) and two ADRs, 0001-pages-unmount-on-leave.md and 0002-out-in-page-fade.md.

Evidence

Tests: npx vitest run in frontend: 1131 tests pass in 63 files. New tests:

  • Pages.spec.tsx:
    • never more than one .page in the DOM
    • old → new with no loading; old → loading → new when loading
    • loading → loading causes no transition
    • content mounts only once its wrapper is attached
    • a page leaving before its fade-in started is cleaned up
    • the fade-in promise resolves after the page is fully in
    • a needsAuthentication page renders an empty wrapper when signed out
  • test-page-state.spec.ts: canStartTest.
  • page-controller.spec.ts: the navigation guard.
  • states/test.spec.ts: isLongTest is recomputed when the test page mounts.
  • key-state.spec.ts: the shared key-state util.

Browser (dev, signed in):

Check Result
Every page renders, never more than one .page in the DOM ✅
Account: test → loading → account, out-in ✅
Boot-style load: the loading page stays the same element while the spinner turns into the bar (no fade out and back in), then fades to the page ✅
Leaving mid-test ends the test; returning gives a fresh test with focus, caret and live stats ✅
Typing on another page, or during the test page fade-out, doesn't start a test ✅
Rapid navigation clicks are ignored ✅
Refocus restarts an unstarted test; resize keeps the words ✅
Mirror funbox is on for the test page, off on others, back on return ✅
Leaderboard and account settings URLs sync only while on those pages; profile search input is focused ✅

Not 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:

  • Mount cost: pages mount on demand, so heavy pages pay their mount cost on entry. Loading → account shows about 300ms of blank between the fade-out and the fade-in.
  • Fade-in keypress: a keypress while the test page is fading in can now start a test. Master blocked this until the navigation finished.
  • beforeShow timing: beforeShow now overlaps the old page's fade-out. A slow beforeShow (friends' invalidateConnections) can let the page mount before it finishes.
  • Selectors and CSS: anything that queried a page element while that page wasn't shown now finds nothing. Wrapper ids and classes are unchanged, so CSS keeps working.

🤖 Generated with Claude Code

Miodec and others added 10 commits October 15, 2026 14:17
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>
Copilot AI balanced review requested due to automatic review settings October 15, 2026 13:29
@monkeytypegeorge monkeytypegeorge added frontend User interface or web stuff docs Related to Markdown files and documentation labels Oct 15, 2026
@Miodec Miodec changed the title refactor(pages): mount pages on demand with out-in page fade refactor(pages): mount pages on demand with out-in page fade (@miodec) Oct 15, 2026
@github-actions github-actions Bot added the waiting for review Pull requests that require a review before continuing label Oct 15, 2026
@Miodec
Miodec added this pull request to stack #8468 October 15, 2026 13:32
@Miodec
Miodec force-pushed the pages-mount-on-demand branch from e456b8e to d05e3e3 Compare October 15, 2026 13:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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-registry drive on-demand mounting + out-in fade; page-controller replaces PageTransition with an internal navigating guard and waitForShownPage().
  • 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 isOpen gates (work runs in onMount/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.

Miodec and others added 7 commits October 15, 2026 15:47
# 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>
@monkeytypegeorge monkeytypegeorge added the local dev Related to the build process or dependencies label Oct 15, 2026
@Miodec Miodec changed the title refactor(pages): mount pages on demand with out-in page fade (@miodec) refactor: render the app with Solid, mount pages on demand (@miodec) Oct 15, 2026
Miodec and others added 2 commits October 15, 2026 19:51
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>
@monkeytypegeorge monkeytypegeorge added the packages Changes in local packages label Oct 15, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Miodec
Miodec merged commit c92720d into master Oct 15, 2026
14 checks passed
@Miodec
Miodec deleted the pages-mount-on-demand branch October 15, 2026 20:04
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 packages Changes in local packages waiting for review Pull requests that require a review before continuing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants