feat: smooth view transitions, no skeleton flash
Deploy to route.crispygoat.com / deploy (push) Successful in 4m21s
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:
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user