Skip to content

feat(ux): eliminate SSR page-transition lag + loading states (#202) - #209

Merged
important-new merged 7 commits into
InspectorHub:mainfrom
important-new:feat/ssr-transition-202-upstream
Jun 28, 2026
Merged

feat(ux): eliminate SSR page-transition lag + loading states (#202)#209
important-new merged 7 commits into
InspectorHub:mainfrom
important-new:feat/ssr-transition-202-upstream

Conversation

@important-new

Copy link
Copy Markdown
Contributor

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

  • Tier 1 — global navigation progress bar (NavProgress): a top-of-viewport bar keyed on React Router's useNavigation().state; self-clears on both success and 404, mounted in the root layout. Gives instant feedback the moment a link is clicked.
  • Tier 2 — route-matched loading skeletons (RouteSkeleton + InspectionsListSkeleton / InspectionCardSkeleton): auth-layout picks 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).
  • Tier 3 is already satisfied on the engine: third-party lookups are client-side/on-demand, not loader-blocking.

Logo FOUC fix

  • logo.svg / favicon.svg carried only a viewBox (no intrinsic width/height), so <img> rendered at the ~300px default before CSS applied — a large logo flash. Added small aspect-preserving intrinsic dimensions.
  • New lint:svg gate (scripts/check-svg-dimensions.mjs) fails any public/**/*.svg missing width+height; wired into npm run lint + pre-commit.
  • Reserved the report cover-photo box (ReportView, h-44 sm:h-56) to match its placeholder and avoid layout shift.

Type cleanup (#202 follow-up)

  • Removed as unknown as env casts in server/api/* and the worker entry by completing AppEnv (STRIPE_PUBLISHABLE_KEY) and the worker's local Env (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).
  • Full unit suite + type-check + lint green.

🤖 Generated with Claude Code

important-new and others added 7 commits June 28, 2026 17:57
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
@important-new
important-new merged commit e25621f into InspectorHub:main Jun 28, 2026
4 checks passed
@important-new
important-new deleted the feat/ssr-transition-202-upstream branch June 28, 2026 15:55
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.

[Optimization] Eliminate SSR Page-Transition Lag & Implement Loading States

1 participant