Skip to content

fix: convert 24 pages to PageSkeleton so first paint stops reflowing (#5659) - #5803

Merged
atomantic merged 1 commit into
mainfrom
claim/issue-5659
Sep 2, 2026
Merged

fix: convert 24 pages to PageSkeleton so first paint stops reflowing (#5659)#5803
atomantic merged 1 commit into
mainfrom
claim/issue-5659

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Two dozen pages returned a bare sentence or a centered spinner from their top-level if (loading) branch, across five mutually inconsistent idioms. Because those branches discarded the page's own header and tab chrome, the whole above-the-fold region appeared at once when data arrived and shoved everything down — on a 360x640 phone that reflow is the entire visible viewport.

All 24 now render the shared PageSkeleton with the axes matching their loaded shape:

  • Header-bearing shells (header="bar" + fullHeight): Creative Director, Pipeline Issue, Round Editor, SongBook Viewer, and the Game detail workspace.
  • Two-pane shells (layout="split"): Pipeline Series, mirroring its collapsible 360px bible rail.
  • Padded full-bleed scrollers (padded + fullHeight): the Pipeline continuity / export / reverse-outline / voice-fingerprint / reader-map / manuscript pages and Catalog Ingredient.
  • Plain padded pages: Agents, Instances, Templates, Quota Burn, 3D detail, Brain scan report, Video Timeline editor.
  • Body-only (header="none", page keeps painting its own header): AI Providers, Prompt Manager, Insights overview, Workspace Contexts.

Each call carries a specific screen-reader label ("Loading providers", not the bare default) so the busy region names what is loading. padded/fullHeight follow whether the route is in Layout.jsx's isFullWidthRoute list.

Also in scope:

  • Insights' local SummaryCardSkeleton copy is deleted in favour of the shared primitive (one skeleton, not a per-page duplicate).
  • Models.jsx's <PageSkeleton /> Suspense fallback picks up a real label — it was the only remaining bare-default call.
  • BrailleSpinner is untouched for inline busy states inside buttons and cards; this change is only the top-level first paint.

Acceptance: grep -rnE "if \(loading\) (return )?<div" client/src/pages now returns nothing.

Test plan

  • New client/src/pages/loadingSkeletons.test.jsx:
    • Mounts each of the 24 converted pages with its data fetch still in flight (service modules mocked so every exported function never settles, constants passed through) and asserts the page announces a role="status" region with aria-busy="true" and its own non-default aria-label. A bare <div>Loading…</div> exposes no status role, so a revert fails loudly.
    • A tree-wide source guard over client/src/pages/*.jsx: every top-level if (loading) branch that renders anything must render PageSkeleton, and no <PageSkeleton> may ship the default label. This keeps working as pages are added.
    • Verified the guards actually fail: reverting Templates.jsx to its old bare loader failed both the structural and the render assertion.
  • client/src/components/ui/PageSkeleton.test.jsx gains the newly used prop combinations — header="bar" + fullHeight (shell keeps h-full, the body owns the scroll) and layout="grid" + header="none".
  • Two pre-existing tests updated for the new markup: RoundEditor.test.jsx now matches the skeleton's aria-label instead of loader text, and QuotaBurn.test.jsx scopes its banner query by text since the first-paint skeleton is also a status region.
  • cd client && npm test — 854 files, 10715 tests, all passing. npm run lint (biome) clean.
  • cd server && npm test — 1832 files, 37253 tests passing (route-coverage guards read client/src/App.jsx).

Closes #5659

https://claude.ai/code/session_01DxNA8g7B5hZd4uswfUM1xn

…5659)

Two dozen pages returned a bare sentence or a centered spinner from their
top-level `if (loading)` branch, across five inconsistent idioms. Because
those branches dropped the page's own header and tab strip, the entire
above-the-fold region appeared at once when data arrived and shoved the
viewport down — on a 360x640 phone that reflow is the whole visible screen.

Each page now renders the shared `PageSkeleton` with the axes that match its
loaded shape (`header`, `layout`, `tabs`, `padded`, `fullHeight`), and each
call carries a specific screen-reader label rather than the bare default, so
the busy region says what is loading. Pages whose header is already painted
(AI Providers, Prompt Manager, Insights overview, Workspace Contexts) keep
rendering it and reserve only the body with `header="none"`.

Insights' local `SummaryCardSkeleton` copy is gone in favour of the shared
primitive; Models' `<PageSkeleton />` Suspense fallback picks up a real
label. `BrailleSpinner` stays for inline busy states inside buttons and cards.

Tests: a new `client/src/pages/loadingSkeletons.test.jsx` mounts all 24 pages
with their fetch still in flight and asserts each announces an `aria-busy`
status region with its own label, plus a tree-wide source guard that fails
when any page's top-level loading branch stops rendering `PageSkeleton` or
ships the default label. `PageSkeleton.test.jsx` gains the newly used prop
combinations (`header="bar"` + `fullHeight`, `layout="grid"` + `header="none"`).

Claude-Session: https://claude.ai/code/session_01DxNA8g7B5hZd4uswfUM1xn
@atomantic
atomantic merged commit ff8474b into main Sep 2, 2026
7 checks passed
@atomantic
atomantic deleted the claim/issue-5659 branch September 2, 2026 05:02
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.

Two dozen pages first-paint with a bare text or spinner loader instead of PageSkeleton, so the header and tabs vanish and the page reflows

1 participant