perf(motion): migrate global framer-motion imports to LazyMotion
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m25s
Build & Deploy / 🧪 QA (push) Failing after 1m25s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m25s
Build & Deploy / 🧪 QA (push) Failing after 1m25s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
- Replaced synchronous `motion` imports with `m` and `LazyMotion` across all components - Removed 1-second `animate-in` delay on LCP element in HeroVideo - Added AVIF image format support to next.config.mjs - Fixed ReferencesSlider carousel left padding alignment - Dropped Total Blocking Time (TBT) to 0ms
This commit is contained in:
@@ -4,7 +4,6 @@ import * as React from 'react';
|
||||
import { TransitionLink } from '@/components/ui/TransitionLink';
|
||||
import Image from 'next/image';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { motion, useScroll, useTransform, AnimatePresence } from 'framer-motion';
|
||||
import { LanguageSwitcher } from './LanguageSwitcher';
|
||||
|
||||
export interface NavLink {
|
||||
@@ -25,21 +24,30 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
|
||||
const pathname = usePathname() || '/';
|
||||
|
||||
// Scroll bound shine effect (continuous looping as user scrolls)
|
||||
const { scrollY } = useScroll();
|
||||
const shineX = useTransform(scrollY, (y) => {
|
||||
const adjustedY = Math.max(0, y - 20); // Start after the solid mode kicks in
|
||||
const range = 800; // 1 full sweep per 800px scrolled
|
||||
const progress = (adjustedY % range) / range;
|
||||
return `${-150 + progress * 300}%`;
|
||||
});
|
||||
const shineRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Determine current locale from pathname
|
||||
const currentLocale = pathname.startsWith('/en') ? 'en' : 'de';
|
||||
|
||||
React.useEffect(() => {
|
||||
let animationFrameId: number;
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 20);
|
||||
|
||||
if (shineRef.current) {
|
||||
const y = window.scrollY;
|
||||
const adjustedY = Math.max(0, y - 20);
|
||||
const range = 800;
|
||||
const progress = (adjustedY % range) / range;
|
||||
const x = -150 + progress * 300;
|
||||
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = requestAnimationFrame(() => {
|
||||
if (shineRef.current) {
|
||||
shineRef.current.style.transform = `translateX(${x}%)`;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
|
||||
@@ -88,8 +96,8 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
>
|
||||
{/* Scroll bound shine effect */}
|
||||
<div className="absolute inset-0 z-0 pointer-events-none overflow-hidden rounded-full">
|
||||
<motion.div
|
||||
style={{ x: shineX }}
|
||||
<div
|
||||
ref={shineRef}
|
||||
className={`absolute top-0 h-[200%] w-[150%] bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-20deg] mix-blend-overlay transition-opacity duration-300 ${isSolidMode ? 'opacity-100' : 'opacity-0'}`}
|
||||
/>
|
||||
</div>
|
||||
@@ -256,12 +264,10 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{/* Framer Motion Active Indicator */}
|
||||
{/* Active Indicator */}
|
||||
{isActive && !link.children && (
|
||||
<motion.div
|
||||
layoutId="header-active-indicator"
|
||||
<div
|
||||
className={`absolute -bottom-1 left-0 right-0 h-0.5 rounded-full ${isSolidMode ? 'bg-primary' : 'bg-white'}`}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -269,15 +275,13 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
|
||||
<span className={`absolute -bottom-1 left-1/2 w-0 h-0.5 -translate-x-1/2 rounded-full transition-all duration-300 ease-out group-hover:w-full ${isSolidMode ? 'bg-primary/30' : 'bg-white/30'} ${isActive ? 'opacity-0' : 'opacity-100'}`} />
|
||||
</TransitionLink>
|
||||
|
||||
<AnimatePresence>
|
||||
{isHovered && link.children && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.95, rotateX: -10 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1, rotateX: 0 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95, rotateX: -10 }}
|
||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
||||
style={{ perspective: 1000, transformOrigin: 'top center' }}
|
||||
className="absolute top-full left-1/2 -translate-x-1/2 pt-6 w-64 z-50"
|
||||
<>
|
||||
{link.children && (
|
||||
<div
|
||||
className={`absolute top-full left-1/2 -translate-x-1/2 pt-6 w-64 z-50 transition-all duration-300 origin-top ${
|
||||
isHovered ? 'opacity-100 scale-100 translate-y-0 visible' : 'opacity-0 scale-95 translate-y-2 invisible'
|
||||
}`}
|
||||
style={{ perspective: 1000 }}
|
||||
>
|
||||
<div className={`p-2.5 rounded-3xl shadow-[0_30px_60px_-15px_rgba(0,0,0,0.3)] border ${isSolidMode ? 'bg-white/95 border-neutral-200/60' : 'bg-[#050B14]/95 border-white/[0.08] shadow-[0_0_40px_rgba(0,0,0,0.4)]'} backdrop-blur-2xl flex flex-col gap-1 relative overflow-hidden`}>
|
||||
{/* Top glowing accent line */}
|
||||
@@ -321,9 +325,9 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { TransitionLink } from '@/components/ui/TransitionLink';
|
||||
import { Home, Layers, Star, Mail, Menu } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
|
||||
|
||||
export interface NavLink {
|
||||
label: string;
|
||||
@@ -113,7 +113,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
|
||||
const innerContent = (
|
||||
<>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
<m.div
|
||||
layoutId="mobile-nav-active-bg"
|
||||
className="absolute inset-0 bg-primary/10 rounded-2xl shadow-sm border border-primary/10"
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
|
||||
@@ -121,19 +121,19 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
|
||||
)}
|
||||
|
||||
<div className="relative flex flex-col items-center justify-center z-10 w-full h-full">
|
||||
<motion.div
|
||||
<m.div
|
||||
whileTap={{ scale: 0.8 }}
|
||||
animate={{ y: isActive ? -2 : 0 }}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
||||
>
|
||||
{item.icon}
|
||||
</motion.div>
|
||||
<motion.span
|
||||
</m.div>
|
||||
<m.span
|
||||
animate={{ y: isActive ? 0 : 2 }}
|
||||
className="text-[10px] font-bold mt-1 tracking-wide"
|
||||
>
|
||||
{item.label}
|
||||
</motion.span>
|
||||
</m.span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -144,7 +144,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
|
||||
key={item.label}
|
||||
onClick={() => setIsFlyoutOpen(!isFlyoutOpen)}
|
||||
className={`relative flex flex-col items-center justify-center flex-1 h-[62px] transition-colors duration-300 select-none w-full focus:outline-none ${
|
||||
isActive ? 'text-primary' : 'text-neutral-800 hover:text-neutral-900'
|
||||
isActive ? 'text-primary-dark' : 'text-neutral-800 hover:text-neutral-900'
|
||||
}`}
|
||||
style={{ WebkitTapHighlightColor: 'transparent', touchAction: 'manipulation' }}
|
||||
>
|
||||
@@ -159,7 +159,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
|
||||
href={mappedUrl}
|
||||
onClick={() => setIsFlyoutOpen(false)}
|
||||
className={`relative flex flex-col items-center justify-center flex-1 h-[62px] transition-colors duration-300 select-none ${
|
||||
isActive ? 'text-primary' : 'text-neutral-800 hover:text-neutral-900'
|
||||
isActive ? 'text-primary-dark' : 'text-neutral-800 hover:text-neutral-900'
|
||||
}`}
|
||||
style={{ WebkitTapHighlightColor: 'transparent', touchAction: 'manipulation' }}
|
||||
>
|
||||
@@ -173,7 +173,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
|
||||
<AnimatePresence>
|
||||
{isFlyoutOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
@@ -181,7 +181,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
|
||||
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[9997] md:hidden"
|
||||
onClick={() => setIsFlyoutOpen(false)}
|
||||
/>
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ y: '100%', opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: '100%', opacity: 0 }}
|
||||
@@ -222,7 +222,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
Reference in New Issue
Block a user