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:
2026-03-19 00:48:55 +01:00
parent 407266bc28
commit ae92923c28
48 changed files with 1301 additions and 287 deletions
@@ -0,0 +1,156 @@
"use client";
import { Children, cloneElement, isValidElement, ReactNode } from "react";
/* ------------------------------------------------------------------ */
/* ShimmerSkeleton */
/* ------------------------------------------------------------------ */
interface ShimmerSkeletonProps {
className?: string;
width?: string | number;
height?: string | number;
rounded?: "sm" | "md" | "lg" | "xl" | "2xl" | "full";
variant?: "text" | "circle" | "card" | "rect";
}
const roundedMap: Record<string, string> = {
sm: "rounded-sm",
md: "rounded-md",
lg: "rounded-lg",
xl: "rounded-xl",
"2xl": "rounded-2xl",
full: "rounded-full",
};
const variantDefaults: Record<
string,
{ width?: string | number; height?: string | number; rounded: string }
> = {
text: { width: "100%", height: "1em", rounded: "rounded" },
circle: { width: 40, height: 40, rounded: "rounded-full" },
card: { width: "100%", height: 120, rounded: "rounded-xl" },
rect: { width: "100%", height: 40, rounded: "rounded-md" },
};
export function ShimmerSkeleton({
className = "",
width,
height,
rounded,
variant = "rect",
}: ShimmerSkeletonProps) {
const defaults = variantDefaults[variant] ?? variantDefaults.rect;
const resolvedWidth = width ?? defaults?.width ?? "100%";
const resolvedHeight = height ?? defaults?.height ?? 40;
const resolvedRounded = rounded
? roundedMap[rounded] ?? "rounded-md"
: defaults?.rounded ?? "rounded-md";
return (
<div
className={`shimmer-skeleton ${resolvedRounded} ${className}`}
style={{
width:
typeof resolvedWidth === "number"
? `${resolvedWidth}px`
: resolvedWidth,
height:
typeof resolvedHeight === "number"
? `${resolvedHeight}px`
: resolvedHeight,
}}
/>
);
}
/* ------------------------------------------------------------------ */
/* ShimmerGroup staggers children entrance by 50ms each */
/* ------------------------------------------------------------------ */
interface ShimmerGroupProps {
children: ReactNode;
staggerMs?: number;
className?: string;
}
export function ShimmerGroup({
children,
staggerMs = 50,
className,
}: ShimmerGroupProps) {
return (
<div className={className}>
{Children.map(children, (child, index) => {
if (!isValidElement(child)) return child;
return cloneElement(child as React.ReactElement<{ style?: React.CSSProperties }>, {
style: {
...(child.props as { style?: React.CSSProperties }).style,
animationDelay: `${index * staggerMs}ms`,
},
});
})}
</div>
);
}
/* ------------------------------------------------------------------ */
/* Inline <style> injected once via a hidden element */
/* ------------------------------------------------------------------ */
const shimmerCSS = `
.shimmer-skeleton {
position: relative;
overflow: hidden;
background: var(--shimmer-bg, #e5e7eb);
}
@media (prefers-color-scheme: dark) {
.shimmer-skeleton {
--shimmer-bg: #374151;
--shimmer-highlight: rgba(255, 255, 255, 0.06);
}
}
:root {
--shimmer-bg: #e5e7eb;
--shimmer-highlight: rgba(255, 255, 255, 0.5);
}
.dark .shimmer-skeleton {
--shimmer-bg: #374151;
--shimmer-highlight: rgba(255, 255, 255, 0.06);
}
.shimmer-skeleton::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
110deg,
transparent 25%,
var(--shimmer-highlight) 50%,
transparent 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
`;
/**
* Mount this once (e.g. in a layout) to inject shimmer styles,
* or simply import ShimmerSkeleton — the styles tag is rendered
* alongside the first skeleton automatically.
*/
export function ShimmerStyles() {
return <style dangerouslySetInnerHTML={{ __html: shimmerCSS }} />;
}