feat: add page transitions and staggered list animations

- ViewTransitionOutlet: wraps Outlet with fade-in-up animation keyed
  to route path. Sidebar/topbar stay still, only content area animates.
- StaggerList: reusable component that cascades children with
  incremental delay (50ms default). Pure CSS via @utility stagger-item.
- Applied stagger to TreeGridView, MyTreesPage cards, SessionHistoryPage.
- New stagger-fade-in keyframe in @theme block.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-08 04:30:25 -04:00
parent 337d933fe2
commit 84885bc065
7 changed files with 98 additions and 17 deletions

View File

@@ -0,0 +1,43 @@
import { Children, type ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface StaggerListProps {
children: ReactNode
className?: string
/** Base delay before the first item animates (ms). Default: 0 */
baseDelay?: number
/** Delay increment between each item (ms). Default: 50 */
staggerMs?: number
}
/**
* Wraps each child in a stagger-item animation container.
* Children fade in sequentially with a configurable delay.
*
* Usage:
* <StaggerList className="grid grid-cols-3 gap-4">
* {items.map(item => <Card key={item.id} ... />)}
* </StaggerList>
*/
export function StaggerList({
children,
className,
baseDelay = 0,
staggerMs = 50,
}: StaggerListProps) {
const items = Children.toArray(children)
return (
<div className={cn(className)}>
{items.map((child, i) => (
<div
key={i}
className="stagger-item"
style={{ '--stagger-index': i, animationDelay: `${baseDelay + i * staggerMs}ms` } as React.CSSProperties}
>
{child}
</div>
))}
</div>
)
}

View File

@@ -1,5 +1,5 @@
import { useEffect, useState, useCallback } from 'react'
import { Outlet, useLocation, useNavigate, Link } from 'react-router-dom'
import { useLocation, useNavigate, Link } from 'react-router-dom'
import { Menu, X, LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, Settings, LogOut, Shield } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
import { usePermissions } from '@/hooks/usePermissions'
@@ -8,6 +8,7 @@ import { BrandLogo } from '@/components/common/BrandLogo'
import { TopBar } from './TopBar'
import { Sidebar } from './Sidebar'
import { EmailVerificationBanner } from './EmailVerificationBanner'
import { ViewTransitionOutlet } from './ViewTransitionOutlet'
import { cn } from '@/lib/utils'
export function AppLayout() {
@@ -185,7 +186,7 @@ export function AppLayout() {
{/* Main Content */}
<main className="main-content overflow-y-auto">
<EmailVerificationBanner />
<Outlet />
<ViewTransitionOutlet />
</main>
</div>
</>

View File

@@ -0,0 +1,21 @@
import { Outlet, useLocation } from 'react-router-dom'
/**
* Wraps <Outlet> with a fade-in animation keyed to the route path.
* When the route changes, React unmounts/remounts the wrapper div,
* triggering the CSS animation. Sidebar and topbar stay still.
*/
export function ViewTransitionOutlet() {
const location = useLocation()
// Use the first two path segments as the key to avoid re-animating
// on param changes within the same page (e.g., /trees/123 → /trees/456)
const segments = location.pathname.split('/').filter(Boolean)
const routeKey = segments.slice(0, 2).join('/') || '/'
return (
<div key={routeKey} className="animate-fade-in-up">
<Outlet />
</div>
)
}

View File

@@ -2,6 +2,7 @@ import { Link } from 'react-router-dom'
import { Pencil, Globe, Lock, Trash2, GitBranch, FileText, Wrench, Star, Download } from 'lucide-react'
import type { TreeListItem } from '@/types'
import { TagBadges } from '@/components/common/TagBadges'
import { StaggerList } from '@/components/common/StaggerList'
import { cn } from '@/lib/utils'
import { usePermissions } from '@/hooks/usePermissions'
import { getTreeEditorPath } from '@/lib/routing'
@@ -33,7 +34,7 @@ export function TreeGridView({
const { canEditTree, canDeleteTree } = usePermissions()
return (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<StaggerList className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{trees.map((tree) => (
<div
key={tree.id}
@@ -182,6 +183,6 @@ export function TreeGridView({
</div>
</div>
))}
</div>
</StaggerList>
)
}