feat(ux): eliminate SSR page-transition lag + loading states (#202) - #209
Merged
important-new merged 7 commits intoJun 28, 2026
Merged
Conversation
React Router runs a route's loader before swapping the page in, so a click can sit silent while data is fetched — users read that as a frozen app. Add a thin top progress bar that ramps toward 90% while navigation is in flight, then snaps to 100% and fades once the route commits. Keys off navigation.state only, so it self-resolves on success and on error/404 (no stuck-forever bar). Uses the DS-0523 --ih-primary accent, mounted globally in the root so it covers every route. Closes Tier 1 of InspectorHub#202. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The auth-layout already swaps a skeleton in for the stale <Outlet/> during a navigation, but it was a single generic placeholder for every route. Make it route-aware: the inspections list now shows a skeleton that mirrors its real shape (header + four-up stat grid + tab strip + InspectionCardSkeleton rows) so the page keeps its structure during the loader wait instead of flashing a generic block and then shifting. Unknown routes keep the generic fallback. Streaming via RR defer/<Await> was evaluated for the list/editor but they are heavily stateful (derive their whole view from synchronous loader data); a route-matched skeleton delivers the 'structural skeleton, not blank' goal with no risky loader refactor. (CMS detail pages, which are presentational, get real Suspense streaming in the portal half of InspectorHub#202.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
logo.svg carried only a viewBox, no width/height, so any <img src=/logo.svg> without explicit dimensions (e.g. the public booking/report header logos, which use h-10 w-auto on the tenant logoUrl that falls back to /logo.svg) rendered at the ~300px replaced-element default until the stylesheet loaded — a large logo flash on cold load. Give the SVG a small aspect-correct intrinsic size (552x420 -> 46x35) so it lays out at <=46px before CSS. App-chrome logos already pass width/height attributes; this covers the rest. No visual change once styled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Guardrail against the logo FOUC recurring: scripts/check-svg-dimensions.mjs fails if any public/**/*.svg lacks an intrinsic width+height on its root <svg> (viewBox alone lets an <img> default to ~300px before CSS). Wired into npm run lint (CI gate) and the pre-commit hook. favicon.svg gets its intrinsic size to pass. Also reserve the report cover-photo box: the cover <img> used w-full max-h-72 with no reserved height, so it reflowed the report downward on load. Pin it to a fixed height that matches CoverPhotoPlaceholder (h-44 sm:h-56) so loaded and error states share one layout and there is no CLS. The aspect-ratio media tiles were already CLS-safe and are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SSR page-transition UX: progress bar + route skeletons + logo FOUC (InspectorHub#202)
…ctorHub#202 follow-up) Same pattern as the portal cleanup: the env casts existed because helpers/locals viewed the typed binding interface (AppEnv) as a loose record or reached a binding the type was missing. - AppEnv gains STRIPE_PUBLISHABLE_KEY (a real var that was read but untyped); the worker's local Env gains API_WORKER (the in-process self-binding). - integrations / stripe-webhook / public-report drop `c.env as unknown as Record<string,string|undefined>` for `const env = c.env` (every key read is on AppEnv); session-context reads c.env.PRIVACY_URL directly. - The two RR loaders read context.cloudflare.env.API_WORKER directly. Net 135 -> 126 non-test casts. The remainder are genuine data-shape/interop (Ed25519 ArrayBuffer args, drizzle JSON-column `as object`, D1 row -> domain mappings, SDK/Stripe interop, the IMAGES binding's dual API surface, and the two dynamic-JWT-key lookups in m2m-auth) — not augmentation-fixable. type-check + 2596 unit tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(types): remove 9 env casts via AppEnv
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
Eliminates SSR page-transition lag and adds loading states (Closes #202), plus a logo-FOUC fix and a small type-safety cleanup that fell out of the same pass.
#202 — SSR page-transition lag & loading states
NavProgress): a top-of-viewport bar keyed on React Router'suseNavigation().state; self-clears on both success and 404, mounted in the root layout. Gives instant feedback the moment a link is clicked.RouteSkeleton+InspectionsListSkeleton/InspectionCardSkeleton):auth-layoutpicks a skeleton by the target path during navigation, so heavy list routes show their shape immediately instead of a blank frame. Stateful editors intentionally keep a generic skeleton (defer-streaming there is high-risk, low-reward).Logo FOUC fix
logo.svg/favicon.svgcarried only aviewBox(no intrinsicwidth/height), so<img>rendered at the ~300px default before CSS applied — a large logo flash. Added small aspect-preserving intrinsic dimensions.lint:svggate (scripts/check-svg-dimensions.mjs) fails anypublic/**/*.svgmissing width+height; wired intonpm run lint+ pre-commit.ReportView,h-44 sm:h-56) to match its placeholder and avoid layout shift.Type cleanup (#202 follow-up)
as unknown asenv casts inserver/api/*and the worker entry by completingAppEnv(STRIPE_PUBLISHABLE_KEY) and the worker's localEnv(API_WORKER) — direct, typed binding access instead of double-casts.Tests
tests/web/unit/nav-progress.spec.ts,tests/web/unit/route-skeleton.spec.ts(new).🤖 Generated with Claude Code