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:
+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 />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user