Header polish: - Glassmorphism: bg-background/80 backdrop-blur-lg + subtle border - Search bar focus-within ring on container - Avatar hover: ring-primary/50 + scale-105 - Notification badge animate-pulse Card hover effects: - Interactive Card variant: hover border-primary/20 tint - ProductCard: lift (-translate-y-1) + shadow-lg + cover scale-105 - PlaylistCard: lift + shadow-lg + cover scale-105 - CourseCard: lift + shadow-xl + cover scale-105 ContentTransition component (new): - Reusable skeleton-to-content crossfade with AnimatePresence - Applied to DashboardPage as proof-of-concept Notification badge pulse: - Sidebar collapsed badges: radar-ping effect (animate-ping behind solid dot) - Header notification bell: matching ping animation on unread count Co-authored-by: Cursor <cursoragent@cursor.com>
42 lines
1.1 KiB
TypeScript
42 lines
1.1 KiB
TypeScript
import { AnimatePresence, motion } from 'framer-motion';
|
|
import { ReactNode } from 'react';
|
|
|
|
interface ContentTransitionProps {
|
|
/** Whether content is still loading */
|
|
isLoading: boolean;
|
|
/** The skeleton/loading placeholder */
|
|
skeleton: ReactNode;
|
|
/** The actual content */
|
|
children: ReactNode;
|
|
/** Optional className for the wrapper */
|
|
className?: string;
|
|
}
|
|
|
|
export function ContentTransition({ isLoading, skeleton, children, className }: ContentTransitionProps) {
|
|
return (
|
|
<div className={className}>
|
|
<AnimatePresence mode="wait">
|
|
{isLoading ? (
|
|
<motion.div
|
|
key="skeleton"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.15 }}
|
|
>
|
|
{skeleton}
|
|
</motion.div>
|
|
) : (
|
|
<motion.div
|
|
key="content"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 0.2, delay: 0.05 }}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
}
|