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:
@@ -7,15 +7,17 @@ import type { Route } from "next";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { clsx } from "clsx";
|
||||
import { Suspense, useMemo, useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { PreferencesModal } from "./PreferencesModal.js";
|
||||
import { ThemeProvider } from "./ThemeProvider.js";
|
||||
import { PageTransition } from "./PageTransition.js";
|
||||
import { NotificationBell } from "../notifications/NotificationBell.js";
|
||||
import { ChatPanel } from "../assistant/ChatPanel.js";
|
||||
import { NavProgressBar } from "~/components/ui/NavProgressBar.js";
|
||||
|
||||
function IconFrame({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-xl border border-white/60 bg-white/80 text-slate-600 shadow-sm dark:border-slate-800 dark:bg-slate-900/70 dark:text-slate-300">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-xl border border-white/60 bg-white/80 text-slate-600 shadow-sm transition-shadow duration-200 hover:shadow-[0_0_12px_rgba(var(--accent-500),0.15)] dark:border-slate-800 dark:bg-slate-900/70 dark:text-slate-300">
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
@@ -223,7 +225,7 @@ function Sidebar({ userRole, onChatOpen }: { userRole: string; onChatOpen: () =>
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="w-72 shrink-0 border-r border-white/60 bg-white/80 backdrop-blur-xl dark:border-slate-800 dark:bg-slate-950/75 flex flex-col">
|
||||
<nav className="w-72 shrink-0 border-r border-gray-200/60 bg-white/60 shadow-[1px_0_12px_rgba(0,0,0,0.03)] backdrop-blur-2xl backdrop-saturate-150 dark:border-slate-800 dark:bg-slate-950/75 dark:shadow-none flex flex-col">
|
||||
{/* Logo */}
|
||||
<div className="border-b border-gray-200/80 px-6 py-6 dark:border-slate-800">
|
||||
<div className="inline-flex items-center gap-3 rounded-2xl border border-brand-200/70 bg-gradient-to-br from-white to-brand-50 px-4 py-3 shadow-sm dark:border-brand-900/50 dark:from-slate-950 dark:to-slate-900">
|
||||
@@ -272,25 +274,45 @@ function Sidebar({ userRole, onChatOpen }: { userRole: string; onChatOpen: () =>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
{!isCollapsed && (
|
||||
<div className="space-y-0.5">
|
||||
{section.items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href as Route}
|
||||
className={clsx(
|
||||
"group flex items-center gap-3 rounded-2xl px-3 py-2 text-sm font-medium transition-all",
|
||||
activeHrefSet.has(item.href)
|
||||
? "bg-gradient-to-r from-brand-50 to-brand-100/70 text-brand-800 shadow-sm ring-1 ring-brand-200/70 dark:from-brand-900/30 dark:to-brand-800/20 dark:text-brand-200 dark:ring-brand-900/40"
|
||||
: "text-gray-700 hover:bg-gray-100/90 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-slate-900 dark:hover:text-white",
|
||||
)}
|
||||
>
|
||||
<IconFrame>{item.icon}</IconFrame>
|
||||
<span className="flex-1">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<AnimatePresence initial={false}>
|
||||
{!isCollapsed && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
{section.items.map((item) => {
|
||||
const isActive = activeHrefSet.has(item.href);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href as Route}
|
||||
className={clsx(
|
||||
"group relative flex items-center gap-3 rounded-2xl px-3 py-2 text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "text-brand-800 dark:text-brand-200"
|
||||
: "text-gray-700 hover:bg-gray-100/90 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-slate-900 dark:hover:text-white",
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="nav-indicator"
|
||||
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-brand-100 to-brand-50/80 shadow-sm ring-1 ring-brand-200/80 dark:from-brand-900/30 dark:to-brand-800/20 dark:ring-brand-900/40"
|
||||
transition={{ type: "spring", stiffness: 350, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
<IconFrame>{item.icon}</IconFrame>
|
||||
<span className="relative flex-1">{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -327,40 +349,68 @@ function Sidebar({ userRole, onChatOpen }: { userRole: string; onChatOpen: () =>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{!subCollapsed && (
|
||||
<div className="ml-4 space-y-0.5 border-l border-gray-200 pl-2 dark:border-slate-800">
|
||||
{entry.items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href as Route}
|
||||
className={clsx(
|
||||
"group flex items-center gap-3 rounded-2xl px-3 py-1.5 text-sm font-medium transition-all",
|
||||
activeHrefSet.has(item.href)
|
||||
? "bg-gradient-to-r from-brand-50 to-brand-100/70 text-brand-800 shadow-sm ring-1 ring-brand-200/70 dark:from-brand-900/30 dark:to-brand-800/20 dark:text-brand-200 dark:ring-brand-900/40"
|
||||
: "text-gray-700 hover:bg-gray-100/90 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-slate-900 dark:hover:text-white",
|
||||
)}
|
||||
>
|
||||
<span className="flex-1">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<AnimatePresence initial={false}>
|
||||
{!subCollapsed && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="ml-4 space-y-0.5 border-l border-gray-200 pl-2 dark:border-slate-800">
|
||||
{entry.items.map((item) => {
|
||||
const isActive = activeHrefSet.has(item.href);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href as Route}
|
||||
className={clsx(
|
||||
"group relative flex items-center gap-3 rounded-2xl px-3 py-1.5 text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "text-brand-800 dark:text-brand-200"
|
||||
: "text-gray-700 hover:bg-gray-100/90 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-slate-900 dark:hover:text-white",
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="nav-indicator"
|
||||
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-brand-100 to-brand-50/80 shadow-sm ring-1 ring-brand-200/80 dark:from-brand-900/30 dark:to-brand-800/20 dark:ring-brand-900/40"
|
||||
transition={{ type: "spring", stiffness: 350, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
<span className="relative flex-1">{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const isActive = activeHrefSet.has(entry.href);
|
||||
return (
|
||||
<Link
|
||||
key={entry.href}
|
||||
href={entry.href as Route}
|
||||
className={clsx(
|
||||
"group flex items-center gap-3 rounded-2xl px-3 py-2 text-sm font-medium transition-all",
|
||||
activeHrefSet.has(entry.href)
|
||||
? "bg-gradient-to-r from-brand-50 to-brand-100/70 text-brand-800 shadow-sm ring-1 ring-brand-200/70 dark:from-brand-900/30 dark:to-brand-800/20 dark:text-brand-200 dark:ring-brand-900/40"
|
||||
"group relative flex items-center gap-3 rounded-2xl px-3 py-2 text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "text-brand-800 dark:text-brand-200"
|
||||
: "text-gray-700 hover:bg-gray-100/90 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-slate-900 dark:hover:text-white",
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="nav-indicator"
|
||||
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-brand-100 to-brand-50/80 shadow-sm ring-1 ring-brand-200/80 dark:from-brand-900/30 dark:to-brand-800/20 dark:ring-brand-900/40"
|
||||
transition={{ type: "spring", stiffness: 350, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
<IconFrame>{entry.icon}</IconFrame>
|
||||
<span className="flex-1">{entry.label}</span>
|
||||
<span className="relative flex-1">{entry.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
@@ -430,7 +480,9 @@ export function AppShell({ children, userRole = "USER" }: { children: React.Reac
|
||||
</Suspense>
|
||||
<div className="flex h-screen bg-transparent">
|
||||
<Sidebar userRole={userRole} onChatOpen={() => setChatOpen(true)} />
|
||||
<main className="flex-1 overflow-auto bg-transparent">{children}</main>
|
||||
<main className="flex-1 overflow-auto bg-transparent">
|
||||
<PageTransition>{children}</PageTransition>
|
||||
</main>
|
||||
{chatOpen && <ChatPanel onClose={() => setChatOpen(false)} />}
|
||||
</div>
|
||||
{/* Floating chat FAB — always visible when panel is closed */}
|
||||
|
||||
Reference in New Issue
Block a user