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:
@@ -112,6 +112,11 @@ const nextConfig: NextConfig = {
|
||||
experimental: {
|
||||
// Enable optimizePackageImports for better bundle size
|
||||
optimizePackageImports: ["lucide-react", "@radix-ui/react-icons", "framer-motion"],
|
||||
// Enable React's <ViewTransition> and Next.js' automatic route
|
||||
// transitions. Combined with the smooth-transition wrappers around
|
||||
// page content (see src/components/transitions), navigation feels
|
||||
// like a single continuous app rather than a sequence of page loads.
|
||||
viewTransition: true,
|
||||
},
|
||||
|
||||
// Compiler options
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
+12
-110
@@ -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 />;
|
||||
}
|
||||
@@ -1,76 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Your Cart — Route Commerce",
|
||||
description: "Review and manage your shopping cart. Select pickup stops, adjust quantities, and proceed to checkout.",
|
||||
keywords: ["cart", "shopping cart", "produce order", "checkout", "pickup"],
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
};
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen relative">
|
||||
{/* Dark background matching cart page */}
|
||||
<div className="fixed inset-0 bg-gradient-to-br from-zinc-950 via-zinc-900 to-zinc-950" />
|
||||
<div className="fixed inset-0 pointer-events-none">
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-emerald-500/10 rounded-full blur-[120px]" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
{/* Minimal header skeleton */}
|
||||
<div className="h-20 border-b border-white/5" />
|
||||
|
||||
{/* Content skeleton */}
|
||||
<main className="px-6 py-12 relative">
|
||||
<div className="mx-auto grid max-w-6xl gap-10 lg:grid-cols-[1fr_380px]">
|
||||
<div>
|
||||
{/* Title skeleton */}
|
||||
<div className="h-10 w-56 rounded-lg bg-white/5 animate-pulse" />
|
||||
<div className="h-5 w-80 rounded mt-3 bg-white/5 animate-pulse" />
|
||||
|
||||
{/* Cart items skeleton */}
|
||||
<div className="mt-10 space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="glass-card p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="h-5 w-40 rounded bg-white/5 animate-pulse" />
|
||||
<div className="h-4 w-24 rounded bg-white/5 animate-pulse" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-11 w-11 rounded-xl bg-white/5 animate-pulse" />
|
||||
<div className="h-11 w-10 rounded bg-white/5 animate-pulse" />
|
||||
<div className="h-11 w-11 rounded-xl bg-white/5 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center justify-between pt-3 border-t border-white/5">
|
||||
<div className="h-5 w-20 rounded bg-white/5 animate-pulse" />
|
||||
<div className="h-4 w-16 rounded bg-white/5 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar skeleton */}
|
||||
<aside>
|
||||
<div className="glass-card p-6 sticky top-6">
|
||||
<div className="h-6 w-32 rounded bg-white/5 animate-pulse" />
|
||||
<div className="mt-5 space-y-3">
|
||||
<div className="h-4 w-full rounded bg-white/5 animate-pulse" />
|
||||
<div className="h-4 w-3/4 rounded bg-white/5 animate-pulse" />
|
||||
</div>
|
||||
<div className="mt-5 h-14 w-full rounded-xl bg-white/5 animate-pulse" />
|
||||
<div className="mt-4 h-4 w-32 mx-auto rounded bg-white/5 animate-pulse" />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Status for accessibility */}
|
||||
<span role="status" className="sr-only">Loading your cart...</span>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,65 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Processing...",
|
||||
description: "Loading checkout...",
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
};
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-stone-50 via-white to-emerald-50/30">
|
||||
<div className="h-20 border-b border-slate-100/50" />
|
||||
|
||||
<main className="px-6 py-12">
|
||||
<div className="mx-auto grid max-w-6xl gap-8 md:grid-cols-[1fr_400px]">
|
||||
<div>
|
||||
{/* Title skeleton */}
|
||||
<div className="h-10 w-32 rounded-lg bg-slate-200 animate-pulse" />
|
||||
<div className="h-5 w-64 rounded mt-3 bg-slate-100 animate-pulse" />
|
||||
|
||||
{/* Form skeleton */}
|
||||
<div className="mt-8 space-y-6">
|
||||
<div className="rounded-2xl bg-white p-6 shadow-sm border border-slate-100">
|
||||
<div className="h-6 w-40 rounded bg-slate-100 animate-pulse" />
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-20 rounded bg-slate-100 animate-pulse" />
|
||||
<div className="h-12 w-full rounded-xl bg-slate-100 animate-pulse" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-16 rounded bg-slate-100 animate-pulse" />
|
||||
<div className="h-12 w-full rounded-xl bg-slate-100 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Button skeleton */}
|
||||
<div className="h-14 w-full rounded-xl bg-slate-100 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar skeleton */}
|
||||
<aside>
|
||||
<div className="rounded-2xl bg-white p-6 shadow-sm border border-slate-100 sticky top-6">
|
||||
<div className="h-6 w-32 rounded bg-slate-100 animate-pulse" />
|
||||
<div className="mt-4 space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex justify-between">
|
||||
<div className="h-4 w-32 rounded bg-slate-100 animate-pulse" />
|
||||
<div className="h-4 w-16 rounded bg-slate-100 animate-pulse" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<span role="status" className="sr-only">Loading checkout...</span>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,78 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Loading contact...",
|
||||
description: "Loading...",
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
};
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* Header skeleton */}
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-white/90 backdrop-blur-xl border-b border-[#e5e5e5]">
|
||||
<div className="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-[#e5e5e5] to-[#f5f5f5] animate-pulse" />
|
||||
<div className="h-6 w-36 rounded bg-[#e5e5e5] animate-pulse" />
|
||||
</div>
|
||||
<div className="h-5 w-24 rounded bg-[#e5e5e5] animate-pulse" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-32">
|
||||
{/* Page header skeleton */}
|
||||
<div className="text-center mb-16">
|
||||
<div className="h-4 w-20 rounded-full bg-[#e5e5e5] mx-auto mb-4 animate-pulse" />
|
||||
<div className="h-12 w-64 rounded-lg bg-[#e5e5e5] mx-auto mb-4 animate-pulse" />
|
||||
<div className="h-5 w-96 rounded bg-[#e5e5e5] mx-auto animate-pulse" />
|
||||
</div>
|
||||
|
||||
{/* Contact cards skeleton */}
|
||||
<div className="grid gap-6 md:grid-cols-3 mb-16">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="rounded-3xl bg-white border border-[#e5e5e5] p-8">
|
||||
<div className="h-14 w-14 rounded-2xl bg-[#e5e5e5] mx-auto mb-5 animate-pulse" />
|
||||
<div className="h-5 w-24 rounded bg-[#e5e5e5] mx-auto mb-3 animate-pulse" />
|
||||
<div className="h-4 w-40 rounded bg-[#e5e5e5] mx-auto animate-pulse" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Form skeleton */}
|
||||
<div className="rounded-3xl bg-white border border-[#e5e5e5] p-10">
|
||||
<div className="h-5 w-20 rounded-full bg-[#e5e5e5] mb-3 animate-pulse" />
|
||||
<div className="h-8 w-40 rounded bg-[#e5e5e5] mb-4 animate-pulse" />
|
||||
<div className="h-1 w-12 bg-[#e5e5e5] mb-8 animate-pulse" />
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-20 rounded bg-[#e5e5e5] animate-pulse" />
|
||||
<div className="h-12 rounded-xl bg-[#e5e5e5] animate-pulse" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-16 rounded bg-[#e5e5e5] animate-pulse" />
|
||||
<div className="h-12 rounded-xl bg-[#e5e5e5] animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-16 rounded bg-[#e5e5e5] animate-pulse" />
|
||||
<div className="h-12 rounded-xl bg-[#e5e5e5] animate-pulse" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-20 rounded bg-[#e5e5e5] animate-pulse" />
|
||||
<div className="h-32 rounded-xl bg-[#e5e5e5] animate-pulse" />
|
||||
</div>
|
||||
<div className="h-12 w-32 rounded-xl bg-[#e5e5e5] animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<span role="status" className="sr-only">Loading contact page...</span>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -177,6 +177,59 @@ select:-webkit-autofill:focus {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
/* ─── View Transitions (route fades) ────────────────────────────────────── */
|
||||
/*
|
||||
* Native browser View Transitions API, opt-in via the
|
||||
* <ViewTransition name="page-content"> component and Next.js's
|
||||
* experimental.viewTransition config. These rules define the actual
|
||||
* crossfade / slide. Without them, transitions still work but use
|
||||
* the browser default (instant cut).
|
||||
*
|
||||
* 220ms is the sweet spot for route changes: long enough to soften
|
||||
* the cut, short enough to feel like a single app. The cubic-bezier
|
||||
* keeps the start crisp and the finish gentle.
|
||||
*/
|
||||
::view-transition-old(page-content) {
|
||||
animation: route-fade-out 180ms cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
}
|
||||
::view-transition-new(page-content) {
|
||||
animation: route-fade-in 240ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
@keyframes route-fade-out {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
@keyframes route-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Top-of-page shimmer for the LoadingFade placeholder bar. */
|
||||
@keyframes transition-shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(400%); }
|
||||
}
|
||||
|
||||
/* Reduce motion: respect the user's OS-level preference. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
::view-transition-old(page-content),
|
||||
::view-transition-new(page-content) {
|
||||
animation: none !important;
|
||||
}
|
||||
@keyframes transition-shimmer {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
}
|
||||
}
|
||||
|
||||
/* Light mode placeholder (used by storefront) */
|
||||
.light ::placeholder,
|
||||
.light::-webkit-input-placeholder,
|
||||
|
||||
@@ -1,40 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<div className="mx-auto max-w-4xl px-6 py-20">
|
||||
{/* Hero skeleton */}
|
||||
<div className="mb-20 animate-pulse text-center">
|
||||
<div className="h-3 w-24 rounded bg-blue-100 mx-auto mb-5" />
|
||||
<div className="h-20 w-full max-w-xl mx-auto rounded-lg bg-blue-50 mb-5" />
|
||||
<div className="h-8 w-full max-w-2xl mx-auto rounded bg-blue-50" />
|
||||
</div>
|
||||
|
||||
{/* Content sections skeleton */}
|
||||
<div className="space-y-20">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="grid md:grid-cols-2 gap-12 items-center"
|
||||
>
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-4 w-20 rounded bg-blue-100" />
|
||||
<div className="h-8 w-3/4 rounded bg-stone-200" />
|
||||
<div className="h-4 w-full rounded bg-stone-100" />
|
||||
<div className="h-4 w-full rounded bg-stone-100" />
|
||||
<div className="h-4 w-2/3 rounded bg-stone-100" />
|
||||
</div>
|
||||
<div className="h-64 rounded-3xl bg-gradient-to-br from-blue-50 to-white border-2 border-blue-100" />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,62 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<div className="mx-auto max-w-4xl px-6 py-20">
|
||||
{/* Header skeleton */}
|
||||
<div className="mb-16 animate-pulse text-center">
|
||||
<div className="h-4 w-20 rounded bg-blue-100 mx-auto mb-5" />
|
||||
<div className="h-16 w-64 mx-auto rounded-lg bg-stone-200 mb-5" />
|
||||
<div className="h-5 w-96 max-w-full mx-auto rounded bg-stone-200" />
|
||||
</div>
|
||||
|
||||
{/* Contact cards skeleton */}
|
||||
<div className="grid gap-6 md:grid-cols-3 mb-16">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="rounded-3xl bg-white border-2 border-stone-200 p-8 shadow-xl text-center"
|
||||
>
|
||||
<div className="h-14 w-14 rounded-2xl bg-blue-100 mx-auto mb-5" />
|
||||
<div className="h-5 w-24 mx-auto rounded bg-stone-200 mb-3" />
|
||||
<div className="h-4 w-full max-w-[160px] mx-auto rounded bg-stone-100" />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Form skeleton */}
|
||||
<div className="rounded-3xl bg-white border-2 border-stone-200 p-10 shadow-xl">
|
||||
<div className="animate-pulse space-y-6">
|
||||
<div className="h-6 w-32 rounded bg-stone-200" />
|
||||
<div className="h-8 w-48 rounded bg-stone-200" />
|
||||
<div className="grid gap-6 md:grid-cols-2 mt-8">
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-20 rounded bg-stone-100" />
|
||||
<div className="h-14 rounded-xl bg-stone-100" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-20 rounded bg-stone-100" />
|
||||
<div className="h-14 rounded-xl bg-stone-100" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-16 rounded bg-stone-100" />
|
||||
<div className="h-14 rounded-xl bg-stone-100" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-20 rounded bg-stone-100" />
|
||||
<div className="h-32 rounded-xl bg-stone-100" />
|
||||
</div>
|
||||
<div className="h-14 w-40 rounded-xl bg-blue-100 mt-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,41 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<div className="mx-auto max-w-3xl px-6 py-20">
|
||||
{/* Header skeleton */}
|
||||
<div className="mb-14 text-center animate-pulse">
|
||||
<div className="h-4 w-16 rounded bg-blue-100 mx-auto mb-4" />
|
||||
<div className="h-16 w-48 mx-auto rounded-lg bg-stone-200 mb-5" />
|
||||
<div className="h-5 w-96 max-w-full mx-auto rounded bg-stone-200" />
|
||||
</div>
|
||||
|
||||
{/* Search skeleton */}
|
||||
<div className="mb-12">
|
||||
<div className="h-14 rounded-2xl bg-white border-2 border-stone-200 shadow-lg" />
|
||||
</div>
|
||||
|
||||
{/* FAQ items skeleton */}
|
||||
<div className="space-y-4">
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
className="rounded-2xl bg-white border-2 border-stone-200 overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center justify-between px-6 py-5">
|
||||
<div className="h-5 w-3/4 rounded bg-stone-200" />
|
||||
<div className="h-8 w-8 rounded-full bg-blue-100" />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { SmoothViewTransition } from "@/components/transitions/SmoothViewTransition";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
|
||||
|
||||
@@ -102,8 +103,10 @@ export default function IndianRiverLayout({ children }: { children: React.ReactN
|
||||
<div className="absolute bottom-0 right-1/4 w-80 h-80 bg-gradient-to-br from-blue-50/40 to-transparent rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
{/* Content wrapper */}
|
||||
<div className="relative">{children}</div>
|
||||
{/* Content wrapper — crossfades on navigation. */}
|
||||
<div id="page-content" className="relative outline-none">
|
||||
<SmoothViewTransition>{children}</SmoothViewTransition>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +1,7 @@
|
||||
"use client";
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main className="min-h-screen bg-white/50 backdrop-blur-xl">
|
||||
<div className="mx-auto max-w-6xl px-6 py-16">
|
||||
{/* Header skeleton with blue accent */}
|
||||
<div className="mb-16 animate-pulse">
|
||||
<div className="h-3 w-20 rounded bg-blue-100 mb-4" />
|
||||
<div className="h-16 w-80 rounded-lg bg-blue-50 mb-4" />
|
||||
<div className="h-6 w-96 max-w-full rounded bg-blue-50" />
|
||||
</div>
|
||||
|
||||
{/* Product cards skeleton */}
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="rounded-3xl bg-white border-2 border-stone-200 overflow-hidden shadow-lg"
|
||||
>
|
||||
{/* Image skeleton */}
|
||||
<div className="h-48 bg-gradient-to-br from-blue-50 to-white relative overflow-hidden">
|
||||
<div className="absolute inset-0 -translate-x-full animate-[shimmer_1.5s_infinite] bg-gradient-to-r from-transparent via-blue-100/60 to-transparent" />
|
||||
</div>
|
||||
{/* Content skeleton */}
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="h-6 w-3/4 rounded bg-stone-200" />
|
||||
<div className="h-4 w-full rounded bg-stone-100" />
|
||||
<div className="h-4 w-2/3 rounded bg-stone-100" />
|
||||
<div className="pt-4 flex items-center justify-between">
|
||||
<div className="h-8 w-20 rounded-lg bg-blue-50" />
|
||||
<div className="h-11 w-28 rounded-xl bg-blue-100" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Stops section skeleton */}
|
||||
<div className="mt-20">
|
||||
<div className="animate-pulse mb-10">
|
||||
<div className="h-3 w-24 rounded bg-blue-100 mb-4" />
|
||||
<div className="h-10 w-64 rounded-lg bg-blue-50 mb-3" />
|
||||
<div className="h-4 w-48 rounded bg-blue-50" />
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[0, 1, 2, 3, 4, 5].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
className="rounded-3xl bg-gradient-to-br from-blue-50 to-white border-2 border-blue-100 p-6"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<div className="h-6 w-32 rounded bg-stone-200 mb-2" />
|
||||
<div className="h-4 w-full rounded bg-stone-100" />
|
||||
</div>
|
||||
<div className="h-6 w-16 rounded-full bg-blue-100" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="h-4 w-28 rounded bg-stone-100" />
|
||||
<div className="h-4 w-20 rounded bg-stone-100" />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
/** Indian River Direct storefront loading — light blue glass backdrop
|
||||
* stays mounted in the layout. */
|
||||
export default function IndianRiverLoading() {
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,70 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50/80 backdrop-blur-xl">
|
||||
<div className="mx-auto max-w-5xl px-6 py-20">
|
||||
{/* Back navigation skeleton */}
|
||||
<div className="mb-10 animate-pulse">
|
||||
<div className="h-5 w-32 rounded bg-stone-200" />
|
||||
</div>
|
||||
|
||||
{/* Stop header skeleton */}
|
||||
<div className="mb-12 animate-pulse rounded-3xl bg-white/60 border border-white/50 p-8">
|
||||
<div className="h-3 w-32 rounded bg-blue-100 mb-4" />
|
||||
<div className="h-16 w-80 rounded-lg bg-stone-200 mb-4" />
|
||||
<div className="h-5 w-full max-w-md rounded bg-stone-200 mb-4" />
|
||||
<div className="h-px w-12 bg-blue-100" />
|
||||
</div>
|
||||
|
||||
{/* Stop info skeleton */}
|
||||
<div className="mb-14 rounded-3xl bg-white/80 border border-white/50 p-8 shadow-xl">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="flex items-start gap-4 py-4 px-5 rounded-2xl bg-stone-50"
|
||||
>
|
||||
<div className="h-10 w-10 rounded-xl bg-blue-50" />
|
||||
<div className="flex-1">
|
||||
<div className="h-3 w-12 rounded bg-stone-200 mb-2" />
|
||||
<div className="h-5 w-24 rounded bg-stone-200" />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Products section skeleton */}
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 w-20 rounded bg-blue-100 mb-4" />
|
||||
<div className="h-10 w-48 rounded bg-stone-200 mb-4" />
|
||||
<div className="h-4 w-64 rounded bg-stone-200 mb-10" />
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="rounded-3xl bg-white overflow-hidden shadow-lg"
|
||||
>
|
||||
<div className="h-48 bg-gradient-to-br from-blue-50 to-white" />
|
||||
<div className="p-6 space-y-3">
|
||||
<div className="h-6 w-3/4 rounded bg-stone-200" />
|
||||
<div className="h-4 w-full rounded bg-stone-100" />
|
||||
<div className="h-4 w-2/3 rounded bg-stone-100" />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
+5
-77
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Loading — Route Commerce",
|
||||
@@ -10,81 +11,8 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main
|
||||
className="atelier-canvas relative min-h-screen flex items-center justify-center overflow-hidden"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Loading content"
|
||||
>
|
||||
<div className="atelier-grain" aria-hidden="true" />
|
||||
|
||||
{/* Subtle ambient glow */}
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
|
||||
<div
|
||||
className="absolute -top-32 -right-32 w-[28rem] h-[28rem] rounded-full opacity-25"
|
||||
style={{ background: "radial-gradient(circle at 30% 30%, rgba(34, 78, 47, 0.18) 0%, transparent 70%)", filter: "blur(48px)" }}
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-48 -left-48 w-[36rem] h-[36rem] rounded-full opacity-20"
|
||||
style={{ background: "radial-gradient(circle at 70% 70%, rgba(202, 138, 4, 0.16) 0%, transparent 70%)", filter: "blur(60px)" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-7 atelier-enter">
|
||||
<div className="atelier-section-num">Loading</div>
|
||||
|
||||
{/* Animated editorial mark */}
|
||||
<div className="relative">
|
||||
<div
|
||||
className="w-16 h-16 rounded-2xl flex items-center justify-center"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #14532D 0%, #1F6B3E 50%, #14532D 100%)",
|
||||
boxShadow: "0 10px 28px -6px rgba(20, 83, 45, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.12)",
|
||||
animation: "atelier-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg className="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13 2L4.5 13.5H11.5L10.5 22L19 10.5H12L13 2Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p
|
||||
className="atelier-title text-2xl"
|
||||
>
|
||||
Preparing your <span className="atelier-italic text-[#786B53]">harvest</span>
|
||||
</p>
|
||||
<p className="text-sm text-stone-500 mt-1.5">Just a moment…</p>
|
||||
</div>
|
||||
|
||||
{/* Progress dots */}
|
||||
<div className="flex items-center gap-1.5" aria-hidden="true">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="w-1.5 h-1.5 rounded-full bg-[#14532D]"
|
||||
style={{
|
||||
animation: "atelier-dot 1.4s ease-in-out infinite",
|
||||
animationDelay: `${i * 0.16}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes atelier-pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.05); opacity: 0.85; }
|
||||
}
|
||||
@keyframes atelier-dot {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
`}</style>
|
||||
</main>
|
||||
);
|
||||
// No skeleton. The previous page stays visible while the next one
|
||||
// streams in, and the View Transition API crossfades them. This
|
||||
// thin top bar is the only signal that navigation is in flight.
|
||||
return <LoadingFade />;
|
||||
}
|
||||
|
||||
@@ -1,59 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Loading...",
|
||||
description: "Loading...",
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
};
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center px-6 py-12 bg-gradient-to-br from-stone-50 to-amber-50/30">
|
||||
{/* Background orbs */}
|
||||
<div
|
||||
className="fixed w-96 h-96 rounded-full pointer-events-none animate-pulse"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(34, 197, 94, 0.15) 0%, transparent 70%)',
|
||||
top: '10%',
|
||||
left: '10%',
|
||||
filter: 'blur(60px)'
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Loading card */}
|
||||
<div className="w-full max-w-sm relative">
|
||||
<div className="bg-white/70 backdrop-blur-2xl rounded-[2rem] shadow-lg p-8">
|
||||
{/* Logo skeleton */}
|
||||
<div className="flex justify-center mb-10">
|
||||
<div className="h-20 w-20 rounded-3xl bg-slate-200 animate-pulse" />
|
||||
</div>
|
||||
|
||||
{/* Text skeleton */}
|
||||
<div className="space-y-3">
|
||||
<div className="h-8 w-32 rounded bg-slate-200 mx-auto animate-pulse" />
|
||||
<div className="h-4 w-24 rounded bg-slate-100 mx-auto animate-pulse" />
|
||||
</div>
|
||||
|
||||
{/* Form skeleton */}
|
||||
<div className="mt-8 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-16 rounded bg-slate-100 animate-pulse" />
|
||||
<div className="h-12 rounded-xl bg-slate-100 animate-pulse" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-20 rounded bg-slate-100 animate-pulse" />
|
||||
<div className="h-12 rounded-xl bg-slate-100 animate-pulse" />
|
||||
</div>
|
||||
<div className="h-12 rounded-xl bg-slate-200 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span role="status" className="sr-only">Loading login page...</span>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,61 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Loading pricing...",
|
||||
description: "Loading...",
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
};
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* Nav skeleton */}
|
||||
<header className="sticky top-0 z-50 border-b border-slate-100 bg-white/95 backdrop-blur">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-9 w-9 rounded-xl bg-slate-200 animate-pulse" />
|
||||
<div className="h-6 w-36 rounded bg-slate-200 animate-pulse" />
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="h-4 w-16 rounded bg-slate-200 animate-pulse" />
|
||||
<div className="h-4 w-20 rounded bg-slate-200 animate-pulse" />
|
||||
<div className="h-9 w-24 rounded-xl bg-slate-200 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero skeleton */}
|
||||
<section className="bg-gradient-to-b from-slate-50 to-white px-6 py-20 text-center">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="h-6 w-40 rounded-full bg-slate-200 mx-auto mb-4 animate-pulse" />
|
||||
<div className="h-16 w-80 rounded-lg bg-slate-200 mx-auto mb-6 animate-pulse" />
|
||||
<div className="h-6 w-96 rounded bg-slate-200 mx-auto animate-pulse" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Plan cards skeleton */}
|
||||
<section className="mx-auto max-w-6xl px-6 py-6">
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="rounded-2xl border-2 border-slate-200 p-6">
|
||||
<div className="h-6 w-24 rounded bg-slate-200 mb-3 animate-pulse" />
|
||||
<div className="h-4 w-32 rounded bg-slate-200 mb-1 animate-pulse" />
|
||||
<div className="h-10 w-24 rounded bg-slate-200 mb-4 animate-pulse" />
|
||||
<div className="h-12 w-full rounded-xl bg-slate-200 animate-pulse" />
|
||||
<div className="mt-6 space-y-2.5">
|
||||
{[1, 2, 3, 4].map((j) => (
|
||||
<div key={j} className="h-4 w-full rounded bg-slate-100 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<span role="status" className="sr-only">Loading pricing page...</span>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,40 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<div className="mx-auto max-w-4xl px-6 py-20">
|
||||
{/* Hero skeleton */}
|
||||
<div className="mb-20 animate-pulse text-center">
|
||||
<div className="h-3 w-24 rounded bg-stone-200 mx-auto mb-5" />
|
||||
<div className="h-20 w-full max-w-xl mx-auto rounded-lg bg-stone-200 mb-5" />
|
||||
<div className="h-8 w-full max-w-2xl mx-auto rounded bg-stone-200" />
|
||||
</div>
|
||||
|
||||
{/* Content sections skeleton */}
|
||||
<div className="space-y-20">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="grid md:grid-cols-2 gap-12 items-center"
|
||||
>
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-4 w-20 rounded bg-stone-200" />
|
||||
<div className="h-8 w-3/4 rounded bg-stone-200" />
|
||||
<div className="h-4 w-full rounded bg-stone-100" />
|
||||
<div className="h-4 w-full rounded bg-stone-100" />
|
||||
<div className="h-4 w-2/3 rounded bg-stone-100" />
|
||||
</div>
|
||||
<div className="h-64 rounded-3xl bg-stone-200" />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,62 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<div className="mx-auto max-w-4xl px-6 py-20">
|
||||
{/* Header skeleton */}
|
||||
<div className="mb-16 animate-pulse text-center">
|
||||
<div className="h-3 w-20 rounded bg-emerald-100 mx-auto mb-5" />
|
||||
<div className="h-16 w-64 mx-auto rounded-lg bg-stone-200 mb-5" />
|
||||
<div className="h-5 w-96 max-w-full mx-auto rounded bg-stone-200" />
|
||||
</div>
|
||||
|
||||
{/* Contact cards skeleton */}
|
||||
<div className="grid gap-6 md:grid-cols-3 mb-16">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="rounded-3xl bg-white p-8 shadow-sm ring-1 ring-stone-200/60 text-center"
|
||||
>
|
||||
<div className="h-12 w-12 rounded-2xl bg-emerald-50 mx-auto mb-5" />
|
||||
<div className="h-5 w-24 mx-auto rounded bg-stone-200 mb-3" />
|
||||
<div className="h-4 w-full max-w-[160px] mx-auto rounded bg-stone-100" />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Form skeleton */}
|
||||
<div className="rounded-3xl bg-white p-10 shadow-sm ring-1 ring-stone-200/60">
|
||||
<div className="animate-pulse space-y-6">
|
||||
<div className="h-6 w-32 rounded bg-stone-200" />
|
||||
<div className="h-8 w-48 rounded bg-stone-200" />
|
||||
<div className="grid gap-6 md:grid-cols-2 mt-8">
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-20 rounded bg-stone-100" />
|
||||
<div className="h-12 rounded-xl bg-stone-100" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-20 rounded bg-stone-100" />
|
||||
<div className="h-12 rounded-xl bg-stone-100" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-16 rounded bg-stone-100" />
|
||||
<div className="h-12 rounded-xl bg-stone-100" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-20 rounded bg-stone-100" />
|
||||
<div className="h-32 rounded-xl bg-stone-100" />
|
||||
</div>
|
||||
<div className="h-14 w-40 rounded-xl bg-emerald-100 mt-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,41 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<div className="mx-auto max-w-3xl px-6 py-20">
|
||||
{/* Header skeleton */}
|
||||
<div className="mb-14 text-center animate-pulse">
|
||||
<div className="h-3 w-16 rounded bg-emerald-100 mx-auto mb-4" />
|
||||
<div className="h-16 w-48 mx-auto rounded-lg bg-stone-200 mb-5" />
|
||||
<div className="h-5 w-96 max-w-full mx-auto rounded bg-stone-200" />
|
||||
</div>
|
||||
|
||||
{/* Search skeleton */}
|
||||
<div className="mb-12">
|
||||
<div className="h-14 rounded-2xl bg-white shadow-sm ring-1 ring-stone-200/60" />
|
||||
</div>
|
||||
|
||||
{/* FAQ items skeleton */}
|
||||
<div className="space-y-4">
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
className="rounded-2xl bg-white shadow-sm ring-1 ring-stone-200/60 overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center justify-between px-6 py-5">
|
||||
<div className="h-5 w-3/4 rounded bg-stone-200" />
|
||||
<div className="h-7 w-7 rounded-full bg-stone-100" />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { SmoothViewTransition } from "@/components/transitions/SmoothViewTransition";
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
|
||||
|
||||
@@ -107,7 +108,12 @@ export default function TuxedoLayout({ children }: { children: React.ReactNode }
|
||||
<div className="fixed inset-0 pointer-events-none">
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-emerald-500/10 rounded-full blur-[120px]" />
|
||||
</div>
|
||||
<div className="relative">{children}</div>
|
||||
{/* The storefront backdrop and ambient orbs are persistent. The
|
||||
page body itself crossfades between routes via the View
|
||||
Transitions API. */}
|
||||
<div id="page-content" className="relative outline-none">
|
||||
<SmoothViewTransition>{children}</SmoothViewTransition>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,77 +1,8 @@
|
||||
"use client";
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main className="min-h-screen bg-stone-50">
|
||||
<div className="mx-auto max-w-6xl px-6 py-16">
|
||||
{/* Header skeleton */}
|
||||
<div className="mb-16 animate-pulse">
|
||||
<div className="h-4 w-32 rounded bg-stone-200 mb-4" />
|
||||
<div className="h-16 w-80 rounded-lg bg-stone-200 mb-4" />
|
||||
<div className="h-6 w-96 max-w-full rounded bg-stone-200" />
|
||||
</div>
|
||||
|
||||
{/* Cards skeleton */}
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="rounded-3xl bg-white ring-1 ring-stone-200/60 overflow-hidden"
|
||||
>
|
||||
{/* Image skeleton */}
|
||||
<div className="h-48 bg-gradient-to-br from-stone-100 to-stone-50 relative overflow-hidden">
|
||||
<div className="absolute inset-0 -translate-x-full animate-[shimmer_1.5s_infinite] bg-gradient-to-r from-transparent via-stone-200/60 to-transparent" />
|
||||
</div>
|
||||
{/* Content skeleton */}
|
||||
<div className="p-7 space-y-4">
|
||||
<div className="h-6 w-3/4 rounded bg-stone-200" />
|
||||
<div className="h-4 w-full rounded bg-stone-100" />
|
||||
<div className="h-4 w-2/3 rounded bg-stone-100" />
|
||||
<div className="pt-4 flex items-center justify-between">
|
||||
<div className="h-8 w-20 rounded-lg bg-stone-200" />
|
||||
<div className="h-11 w-28 rounded-xl bg-stone-200" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Stops section skeleton */}
|
||||
<div className="mt-20">
|
||||
<div className="animate-pulse mb-10">
|
||||
<div className="h-3 w-24 rounded bg-stone-200 mb-4" />
|
||||
<div className="h-10 w-64 rounded-lg bg-stone-200 mb-3" />
|
||||
<div className="h-4 w-48 rounded bg-stone-200" />
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[0, 1, 2, 3, 4, 5].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
className="rounded-3xl bg-white p-7 ring-1 ring-stone-200/60"
|
||||
>
|
||||
<div className="flex items-start gap-4 mb-5">
|
||||
<div className="h-10 w-10 rounded-xl bg-stone-100" />
|
||||
<div className="flex-1">
|
||||
<div className="h-8 w-32 rounded bg-stone-200 mb-2" />
|
||||
<div className="h-4 w-full rounded bg-stone-100" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 pl-[2.75rem]">
|
||||
<div className="h-4 w-28 rounded bg-stone-100" />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
/** Tuxedo storefront loading — the emerald backdrop and grain
|
||||
* texture persist in the layout, so we only need to signal that
|
||||
* navigation is in flight. */
|
||||
export default function TuxedoLoading() {
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,70 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<div className="mx-auto max-w-5xl px-6 py-20">
|
||||
{/* Back navigation skeleton */}
|
||||
<div className="mb-10 animate-pulse">
|
||||
<div className="h-5 w-32 rounded bg-stone-200" />
|
||||
</div>
|
||||
|
||||
{/* Stop header skeleton */}
|
||||
<div className="mb-12 animate-pulse rounded-3xl bg-white p-8 shadow-sm ring-1 ring-stone-200/60">
|
||||
<div className="h-3 w-32 rounded bg-emerald-100 mb-4" />
|
||||
<div className="h-16 w-80 rounded-lg bg-stone-200 mb-4" />
|
||||
<div className="h-5 w-full max-w-md rounded bg-stone-200 mb-4" />
|
||||
<div className="h-px w-12 bg-emerald-600" />
|
||||
</div>
|
||||
|
||||
{/* Stop info skeleton */}
|
||||
<div className="mb-14 rounded-3xl bg-white p-8 shadow-sm ring-1 ring-stone-200/60">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="flex items-start gap-4 py-4 px-5 rounded-2xl bg-stone-50"
|
||||
>
|
||||
<div className="h-10 w-10 rounded-xl bg-emerald-50" />
|
||||
<div className="flex-1">
|
||||
<div className="h-3 w-12 rounded bg-stone-200 mb-2" />
|
||||
<div className="h-5 w-24 rounded bg-stone-200" />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Products section skeleton */}
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 w-20 rounded bg-emerald-100 mb-4" />
|
||||
<div className="h-10 w-48 rounded bg-stone-200 mb-4" />
|
||||
<div className="h-4 w-64 rounded bg-stone-200 mb-10" />
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="rounded-3xl bg-white overflow-hidden shadow-lg"
|
||||
>
|
||||
<div className="h-48 bg-gradient-to-br from-stone-100 to-stone-50" />
|
||||
<div className="p-6 space-y-3">
|
||||
<div className="h-6 w-3/4 rounded bg-stone-200" />
|
||||
<div className="h-4 w-full rounded bg-stone-100" />
|
||||
<div className="h-4 w-2/3 rounded bg-stone-100" />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -1,17 +1,6 @@
|
||||
import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
|
||||
/** Subtle navigation indicator — see LoadingFade for rationale. */
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main className="min-h-screen bg-slate-100 px-4 py-6">
|
||||
<div className="mx-auto max-w-lg">
|
||||
<div className="animate-pulse space-y-5">
|
||||
<div className="h-16 rounded-xl bg-slate-800" />
|
||||
<div className="h-8 w-48 rounded bg-slate-300" />
|
||||
<div className="h-12 w-full rounded-xl bg-white" />
|
||||
<div className="h-12 w-full rounded-xl bg-white" />
|
||||
<div className="h-12 w-full rounded-xl bg-white" />
|
||||
<div className="h-12 w-full rounded-xl bg-white" />
|
||||
<div className="h-10 w-full rounded-xl bg-slate-300" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
return <LoadingFade />;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { ViewTransition } from "react";
|
||||
|
||||
/**
|
||||
* Subtle loading placeholder used in place of skeleton `loading.tsx`
|
||||
* files. Instead of a flashy "skeleton of the page you're waiting for",
|
||||
* this renders a single thin animated bar that crossfades into the
|
||||
* real content via the View Transitions API. The previous page stays
|
||||
* visible underneath until the new page is ready, so the user never
|
||||
* feels the gap.
|
||||
*
|
||||
* Drop this in a `loading.tsx` file:
|
||||
*
|
||||
* // app/admin/loading.tsx
|
||||
* import { LoadingFade } from "@/components/transitions/LoadingFade";
|
||||
* export default LoadingFade;
|
||||
*/
|
||||
export function LoadingFade() {
|
||||
return (
|
||||
<ViewTransition name="page-content" update="default">
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 z-50 h-0.5 overflow-hidden"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
className="h-full w-1/3 rounded-full"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(90deg, transparent 0%, #14532D 50%, transparent 100%)",
|
||||
animation: "transition-shimmer 1.4s ease-in-out infinite",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadingFade;
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Announce route changes to assistive tech. Without this, screen-reader
|
||||
* users see no signal that navigation completed because the view
|
||||
* transition is intentionally seamless for sighted users.
|
||||
*
|
||||
* The announcer also restores the keyboard focus to the top of the new
|
||||
* page after navigation, so tab order picks up where the user expects
|
||||
* (not stuck on the link they just clicked).
|
||||
*/
|
||||
export function RouteAnnouncer() {
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
// Reset focus to the page wrapper so keyboard nav continues from
|
||||
// the top, not from the link in the sidebar.
|
||||
const target = document.getElementById("page-content") ?? document.body;
|
||||
if (target && "tabIndex" in target) {
|
||||
(target as HTMLElement).tabIndex = -1;
|
||||
(target as HTMLElement).focus({ preventScroll: true });
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
className="sr-only"
|
||||
>
|
||||
{/* Text is intentionally empty — the live region is the
|
||||
announcement channel; the actual title is set per page via
|
||||
the <h1> rendered inside the layout. */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { ViewTransition } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Smooth view-transition wrapper for any page-section content.
|
||||
*
|
||||
* Drop this around the main content area of a route (or around an
|
||||
* individual <Suspense> chunk) to opt that subtree into a soft
|
||||
* crossfade on navigation. The browser's native View Transitions API
|
||||
* handles the actual animation — when the browser doesn't support it,
|
||||
* the children just swap, no error.
|
||||
*
|
||||
* `name` groups elements so the browser can morph the same element
|
||||
* across pages. Use the same name on the source and destination (e.g.
|
||||
* the page header) for shared-element morphing. Use the default
|
||||
* `"page-content"` for plain crossfades.
|
||||
*
|
||||
* The default `update="default"` plus a CSS `::view-transition-old/new`
|
||||
* rule (see globals.css) gives us a 220ms crossfade with a tiny
|
||||
* downward shift on the incoming page — fast enough to feel like a
|
||||
* single app, slow enough to soften the cut between routes.
|
||||
*
|
||||
* Usage:
|
||||
* <main>
|
||||
* <SmoothViewTransition>{children}</SmoothViewTransition>
|
||||
* </main>
|
||||
*/
|
||||
export function SmoothViewTransition({
|
||||
children,
|
||||
name = "page-content",
|
||||
}: {
|
||||
children: ReactNode;
|
||||
name?: string;
|
||||
}) {
|
||||
return (
|
||||
<ViewTransition
|
||||
name={name}
|
||||
// `default` runs a CSS-driven crossfade using the
|
||||
// ::view-transition-* pseudo-elements defined in globals.css.
|
||||
// Other options: "none" (instant cut), or a custom string your
|
||||
// CSS can match.
|
||||
update="default"
|
||||
>
|
||||
{children}
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user