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
+162
View File
@@ -0,0 +1,162 @@
"use client";
import { useId } from "react";
interface SparklineProps {
data: number[];
width?: number;
height?: number;
color?: string;
fillOpacity?: number;
strokeWidth?: number;
className?: string;
animated?: boolean;
}
function buildPoints(
data: number[],
width: number,
height: number,
strokeWidth: number,
): string {
if (data.length === 0) return "";
const padding = strokeWidth;
const drawWidth = width - padding * 2;
const drawHeight = height - padding * 2;
const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1; // avoid division by zero when all values equal
return data
.map((v, i) => {
const x =
data.length === 1
? width / 2
: padding + (i / (data.length - 1)) * drawWidth;
const y = padding + drawHeight - ((v - min) / range) * drawHeight;
return `${x},${y}`;
})
.join(" ");
}
function buildAreaPoints(
linePoints: string,
width: number,
height: number,
strokeWidth: number,
dataLength: number,
): string {
if (!linePoints) return "";
const padding = strokeWidth;
const drawWidth = width - padding * 2;
const bottom = height - padding;
const lastX =
dataLength === 1 ? width / 2 : padding + drawWidth;
const firstX = dataLength === 1 ? width / 2 : padding;
return `${linePoints} ${lastX},${bottom} ${firstX},${bottom}`;
}
export function Sparkline({
data,
width = 60,
height = 20,
color = "currentColor",
fillOpacity = 0.1,
strokeWidth = 1.5,
className,
animated = true,
}: SparklineProps) {
const id = useId();
if (data.length === 0) {
return (
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
className={className}
role="img"
aria-label="No data"
/>
);
}
const linePoints = buildPoints(data, width, height, strokeWidth);
const areaPoints = buildAreaPoints(
linePoints,
width,
height,
strokeWidth,
data.length,
);
// Approximate path length for dash animation
const approxLength = width * 2;
return (
<>
{animated && (
<style
dangerouslySetInnerHTML={{
__html: `
@keyframes sparkline-draw-${CSS.escape(id)} {
from { stroke-dashoffset: ${approxLength}; }
to { stroke-dashoffset: 0; }
}
`,
}}
/>
)}
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
className={className}
role="img"
aria-label={`Sparkline: ${data.join(", ")}`}
>
{/* Area fill */}
{data.length > 1 && (
<polygon
points={areaPoints}
fill={color}
opacity={fillOpacity}
/>
)}
{/* Line */}
{data.length === 1 ? (
<circle
cx={width / 2}
cy={height / 2}
r={strokeWidth}
fill={color}
/>
) : (
<polyline
points={linePoints}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
{...(animated
? {
strokeDasharray: approxLength,
strokeDashoffset: approxLength,
style: {
animation: `sparkline-draw-${CSS.escape(id)} 600ms ease-out forwards`,
},
}
: {})}
/>
)}
</svg>
</>
);
}