Files
Nexus/apps/web/src/components/ui/ShimmerSkeleton.tsx
T
Hartmut ae92923c28 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>
2026-03-19 00:48:55 +01:00

157 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 }} />;
}