fix: convert 24 pages to PageSkeleton so first paint stops reflowing (#5659) - #5803
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
PageSkeletonwith the axes matching their loaded shape:header="bar"+fullHeight): Creative Director, Pipeline Issue, Round Editor, SongBook Viewer, and the Game detail workspace.layout="split"): Pipeline Series, mirroring its collapsible 360px bible rail.padded+fullHeight): the Pipeline continuity / export / reverse-outline / voice-fingerprint / reader-map / manuscript pages and Catalog Ingredient.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/fullHeightfollow whether the route is inLayout.jsx'sisFullWidthRoutelist.Also in scope:
SummaryCardSkeletoncopy 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.BrailleSpinneris 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/pagesnow returns nothing.Test plan
client/src/pages/loadingSkeletons.test.jsx:role="status"region witharia-busy="true"and its own non-defaultaria-label. A bare<div>Loading…</div>exposes nostatusrole, so a revert fails loudly.client/src/pages/*.jsx: every top-levelif (loading)branch that renders anything must renderPageSkeleton, and no<PageSkeleton>may ship the default label. This keeps working as pages are added.Templates.jsxto its old bare loader failed both the structural and the render assertion.client/src/components/ui/PageSkeleton.test.jsxgains the newly used prop combinations —header="bar"+fullHeight(shell keepsh-full, the body owns the scroll) andlayout="grid"+header="none".RoundEditor.test.jsxnow matches the skeleton'saria-labelinstead of loader text, andQuotaBurn.test.jsxscopes its banner query by text since the first-paint skeleton is also astatusregion.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 readclient/src/App.jsx).Closes #5659
https://claude.ai/code/session_01DxNA8g7B5hZd4uswfUM1xn