veza/apps/web/src/components/ui/content-transition/ContentTransition.tsx
senke 2131da1a9a feat(ui): header glassmorphism, card hover effects, content transitions, badge animations
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>
2026-02-09 23:25:52 +01:00

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>
);
}