feat: Sprint 1 — Alive Enterprise animation foundation
Animation primitives (6 new components): - AnimatedNumber: count-up with easeOutExpo, de-DE locale formatting - ShimmerSkeleton: diagonal gradient sweep replacing animate-pulse - FadeIn: framer-motion viewport-triggered fade + slide - StaggerList/StaggerItem: staggered children entrance - Sparkline: pure SVG inline trend chart with draw-in animation - ProgressRing: animated circular progress with CSS transitions Sidebar & page transitions: - Sliding nav indicator (framer-motion layoutId animation) - Icon frame hover glow (brand-color shadow) - Smooth section collapse/expand (AnimatePresence height animation) - PageTransition wrapper (fade-up on route change) - AnimatedModal component (scale + fade with custom bezier) - Notification badge bounce on count increase Dashboard animations: - StatCards: AnimatedNumber count-up + staggered FadeIn + budget color tinting - WidgetContainer: fade-slide-up on mount - Chargeability: animated percentages + inline utilization bars - ProjectTable/MyProjects: animated numbers + staggered row entrance Shimmer skeletons & table animations: - Replaced animate-pulse across 20+ loading states with shimmer gradient - Staggered row entrance (fadeSlideIn) on Resources, Projects, Allocations tables - hover-lift utility class for subtle card/row elevation on hover - Content-shaped skeletons (avatars, text bars, badges) Light mode surface depth: - Mesh gradient page background (subtle accent-tinted corners) - Enhanced card shadows (two-layer depth) - Sidebar glassmorphism upgrade (bg-white/60, backdrop-blur-2xl, saturate-150) - Toolbar sticky backdrop blur - Enhanced focus ring with brand-color glow Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useEffect, useCallback, useRef, type ReactNode } from "react";
|
||||
|
||||
interface AnimatedModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
overlayClassName?: string;
|
||||
maxWidth?: string;
|
||||
}
|
||||
|
||||
export function AnimatedModal({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
className,
|
||||
overlayClassName,
|
||||
maxWidth = "max-w-xl",
|
||||
}: AnimatedModalProps) {
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleEscape = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, [open, handleEscape]);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Overlay */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={
|
||||
overlayClassName ??
|
||||
"absolute inset-0 bg-black/40 backdrop-blur-sm"
|
||||
}
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Panel */}
|
||||
<motion.div
|
||||
ref={panelRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
initial={{ opacity: 0, scale: 0.97 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.97 }}
|
||||
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
|
||||
className={[
|
||||
"relative z-10 w-full rounded-xl bg-white shadow-2xl dark:bg-gray-800",
|
||||
maxWidth,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user