feat: smooth view transitions, no skeleton flash
Deploy to route.crispygoat.com / deploy (push) Successful in 4m21s

User pain point: skeleton loading.tsx files made the app feel like
a sequence of page reloads, exposing backend latency. Replaced with
a single 1px shimmer bar + crossfade via React's <ViewTransition>.

Changes:
- Enable experimental.viewTransition in next.config.ts
- Add SmoothViewTransition wrapper (ViewTransition name=page-content)
- Add LoadingFade component: thin animated bar instead of skeleton
- Add RouteAnnouncer for a11y (screen readers + focus reset)
- Add ::view-transition-old/new CSS for the crossfade (220ms, no
  jarring slide, respects prefers-reduced-motion)
- Wrap admin/tuxedo/IRD layout children in SmoothViewTransition
  (sidebar/header/footer stay mounted; only the body fades)
- Replace 19 skeleton loading.tsx files with the fade component

Result: navigation now feels like a single app, not a series of
preload-and-render events. The user never sees a 'skeleton of the
page they're about to load.'
This commit is contained in:
Tyler
2026-06-16 23:37:00 -06:00
parent 9458fd0506
commit 4ebbc6dacf
27 changed files with 304 additions and 1105 deletions
+4 -3
View File
@@ -1,5 +1,6 @@
import CommunicationsLoading from "@/components/admin/CommunicationsLoading";
import { LoadingFade } from "@/components/transitions/LoadingFade";
/** Subtle navigation indicator — see LoadingFade for rationale. */
export default function Loading() {
return <CommunicationsLoading activeTab="campaigns" />;
}
return <LoadingFade />;
}
+13 -2
View File
@@ -8,6 +8,8 @@ import { getSession } from "@/lib/auth";
import "@/styles/admin-design-system.css";
import { ToastProvider } from "@/components/admin/Toast";
import { ToastContainer } from "@/components/admin/ToastContainer";
import { SmoothViewTransition } from "@/components/transitions/SmoothViewTransition";
import { RouteAnnouncer } from "@/components/transitions/RouteAnnouncer";
// Admin layout calls getAdminUser() which reads cookies(). Without this,
// Next.js tries to prerender the entire /admin/* tree statically and the
@@ -105,8 +107,17 @@ export default async function AdminLayout({ children }: { children: React.ReactN
activeBrandId={activeBrandId}
brands={brands}
/>
<div className="min-h-screen lg:pl-60 admin-section" style={{ backgroundColor: "var(--admin-bg)" }}>
{children}
{/* The main content area swaps on every navigation. Wrapping
it in <SmoothViewTransition> opts the swap into a soft
crossfade via the View Transitions API — sidebar and
background stay mounted, only the body fades. */}
<div
id="page-content"
className="min-h-screen lg:pl-60 admin-section outline-none"
style={{ backgroundColor: "var(--admin-bg)" }}
>
<SmoothViewTransition>{children}</SmoothViewTransition>
<RouteAnnouncer />
</div>
</ToastProviderWrapper>
);
+13 -111
View File
@@ -1,112 +1,14 @@
import { LoadingFade } from "@/components/transitions/LoadingFade";
/**
* Admin shell loading state.
*
* Replaces the previous skeleton (header + 4 stat cards + 2 panels +
* 5-row table) with a 1px-thick animated bar at the top of the page.
* The AdminSidebar and parchment background stay mounted, so the
* transition is a subtle fade — the user never sees an "empty" version
* of the admin while the next page streams in.
*/
export default function AdminLoading() {
return (
<main className="min-h-screen px-4 sm:px-6 md:px-8 py-6" style={{ backgroundColor: "var(--admin-bg)" }}>
{/* Header skeleton */}
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-3">
<div className="animate-pulse w-9 h-9 rounded-lg" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="space-y-2">
<div className="animate-pulse h-5 w-36 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-3 w-24 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
</div>
<div className="animate-pulse h-8 w-28 rounded-lg" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
{/* Stats cards skeleton */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-5">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="rounded-xl bg-white border overflow-hidden">
<div className="flex items-center gap-3 p-4">
<div className="animate-pulse w-9 h-9 rounded-xl flex-shrink-0" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="space-y-1.5 flex-1">
<div className="animate-pulse h-2.5 w-16 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-6 w-12 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
</div>
</div>
))}
</div>
{/* Quick Actions + Usage skeleton */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3 mb-5">
<div className="rounded-xl bg-white border overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: "var(--admin-border-light)" }}>
<div className="animate-pulse h-3 w-20 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
<div className="grid grid-cols-2 gap-2 p-3">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="animate-pulse rounded-lg h-10" style={{ backgroundColor: "var(--admin-border)" }} />
))}
</div>
</div>
<div className="lg:col-span-2 rounded-xl bg-white border overflow-hidden">
<div className="flex items-center gap-2 px-4 py-3 border-b" style={{ borderColor: "var(--admin-border-light)" }}>
<div className="animate-pulse h-5 w-16 rounded-full" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-3 w-24 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 p-4">
{[1, 2, 3].map((i) => (
<div key={i} className="space-y-2">
<div className="flex items-center justify-between">
<div className="animate-pulse h-2.5 w-10 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-3 w-12 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
<div className="animate-pulse h-1.5 rounded-full" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
))}
</div>
</div>
</div>
{/* Recent orders skeleton */}
<div className="rounded-xl bg-white border overflow-hidden mb-5">
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: "var(--admin-border-light)" }}>
<div className="animate-pulse h-3 w-20 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-3 w-14 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
<div className="divide-y" style={{ borderColor: "var(--admin-border-light)" }}>
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-3">
<div className="animate-pulse w-8 h-8 rounded-lg flex-shrink-0" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="space-y-1">
<div className="animate-pulse h-3 w-28 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-2.5 w-16 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
</div>
<div className="flex items-center gap-3">
<div className="animate-pulse h-3 w-14 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-5 w-16 rounded-full" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
</div>
))}
</div>
</div>
{/* Filter tabs skeleton */}
<div className="flex gap-2 mb-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="animate-pulse h-9 w-20 rounded-lg" style={{ backgroundColor: "var(--admin-border)" }} />
))}
</div>
{/* Section cards skeleton */}
<div className="grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-4 gap-3">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="rounded-xl bg-white border overflow-hidden">
<div className="flex items-start justify-between p-4">
<div className="animate-pulse w-9 h-9 rounded-xl" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse w-12 h-4 rounded-full" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
<div className="px-4 pb-4 space-y-1.5">
<div className="animate-pulse h-3 w-24 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-2.5 w-full rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-2.5 w-3/4 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
</div>
))}
</div>
</main>
);
}
return <LoadingFade />;
}