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
@@ -4,27 +4,51 @@ import { trpc } from "~/lib/trpc/client.js";
import type { WidgetProps } from "~/components/dashboard/widget-registry.js";
import { formatMoney } from "~/lib/format.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { AnimatedNumber } from "~/components/ui/AnimatedNumber.js";
import { FadeIn } from "~/components/ui/FadeIn.js";
function StatCard({
label,
value,
suffix,
sub,
info,
accentColor,
delay = 0,
}: {
label: string;
value: string | number;
value: number;
suffix?: string;
sub?: string;
info?: React.ReactNode;
accentColor?: "green" | "amber" | "red";
delay?: number;
}) {
const accentBorder = accentColor === "red"
? "border-l-red-500"
: accentColor === "amber"
? "border-l-amber-500"
: accentColor === "green"
? "border-l-green-500"
: "";
return (
<div className="rounded-2xl border border-gray-200 bg-white/80 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-900/70">
<span className="flex items-center text-xs font-semibold uppercase tracking-[0.18em] text-gray-500">
{label}
{info && <InfoTooltip content={info} />}
</span>
<span className="mt-2 text-2xl font-semibold text-gray-900 dark:text-gray-50">{value}</span>
{sub && <span className="mt-1 text-xs text-gray-500 dark:text-gray-400">{sub}</span>}
</div>
<FadeIn delay={delay} direction="up">
<div
className={`rounded-2xl border border-gray-200 bg-white/80 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-900/70 ${
accentColor ? `border-l-[3px] ${accentBorder}` : ""
}`}
>
<span className="flex items-center text-xs font-semibold uppercase tracking-[0.18em] text-gray-500">
{label}
{info && <InfoTooltip content={info} />}
</span>
<span className="mt-2 text-2xl font-semibold text-gray-900 dark:text-gray-50">
<AnimatedNumber value={value} suffix={suffix} />
</span>
{sub && <span className="mt-1 text-xs text-gray-500 dark:text-gray-400">{sub}</span>}
</div>
</FadeIn>
);
}
@@ -37,21 +61,25 @@ export function StatCardsWidget(_props: Partial<WidgetProps> = {}) {
if (isLoading || !data) {
return (
<div className="grid grid-cols-2 gap-3 h-full animate-pulse">
<div className="grid grid-cols-2 gap-3 h-full">
{[...Array(4)].map((_, i) => (
<div
key={i}
className="rounded-2xl border border-gray-200 bg-gray-100 p-4 dark:border-gray-700 dark:bg-gray-800"
>
<div className="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded" />
<div className="h-3 w-20 shimmer-skeleton rounded" />
<div className="h-7 w-16 bg-gray-300 dark:bg-gray-600 rounded" />
<div className="h-2 w-24 bg-gray-200 dark:bg-gray-700 rounded" />
<div className="h-2 w-24 shimmer-skeleton rounded" />
</div>
))}
</div>
);
}
const budgetPct = data.budgetSummary.avgUtilizationPercent;
const budgetAccent: "red" | "amber" | "green" =
budgetPct > 90 ? "red" : budgetPct >= 70 ? "amber" : "green";
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 h-full content-start">
<StatCard
@@ -59,24 +87,30 @@ export function StatCardsWidget(_props: Partial<WidgetProps> = {}) {
value={data.totalResources}
sub={`${data.activeResources} active`}
info="All resources in the system. Sub-line shows active resources only."
delay={0}
/>
<StatCard
label="Active Projects"
value={data.activeProjects}
sub={`${data.totalProjects} total`}
info="Projects with status ACTIVE. Total includes all statuses (DRAFT, ON_HOLD, COMPLETED, CANCELLED)."
delay={0.05}
/>
<StatCard
label="Total Allocations"
value={data.totalAllocations}
sub={`${data.activeAllocations} not cancelled`}
info="All allocation records ever created. 'Not cancelled' excludes allocations with status CANCELLED."
delay={0.1}
/>
<StatCard
label="Budget Utilization"
value={`${data.budgetSummary.avgUtilizationPercent}%`}
value={budgetPct}
suffix="%"
sub={`${formatMoney(data.budgetSummary.totalCostCents)} of ${formatMoney(data.budgetSummary.totalBudgetCents)}`}
info="Sum of costs across non-cancelled allocations divided by total project budgets. Cost = resource LCR × booked hours."
accentColor={budgetAccent}
delay={0.15}
/>
</div>
);