refactor: adopt shared Input/Textarea components #101

Merged
chihlasm merged 9 commits from refactor/adopt-input-textarea into main 2026-03-09 20:12:22 +00:00
7 changed files with 98 additions and 17 deletions
Showing only changes of commit 84885bc065 - Show all commits

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>
)
}

View File

@@ -147,6 +147,11 @@
80% { transform: rotate(-2deg); }
100% { transform: rotate(0deg); }
}
@keyframes stagger-fade-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
}
/* ── Root CSS variables (non-theme: glass, shadows, layout) ── */
@@ -249,6 +254,12 @@
animation: breatheGlow 3s ease-in-out infinite alternate;
}
@utility stagger-item {
opacity: 0;
animation: stagger-fade-in 350ms var(--ease-out-smooth) forwards;
animation-delay: calc(var(--stagger-index, 0) * 50ms);
}
@utility rdp-custom {
@apply text-foreground;
& .rdp-month { @apply w-full; }

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree, Plus, ListOrdered, ChevronDown, Wrench } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { StaggerList } from '@/components/common/StaggerList'
import { Button } from '@/components/ui/Button'
import { treesApi } from '@/api/trees'
import { sessionsApi } from '@/api/sessions'
@@ -217,7 +218,7 @@ export function MyTreesPage() {
</div>
</div>
) : (
<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}
@@ -352,7 +353,7 @@ export function MyTreesPage() {
</div>
</div>
))}
</div>
</StaggerList>
)}
{/* Delete Confirmation */}

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { PageMeta } from '@/components/common/PageMeta'
import { StaggerList } from '@/components/common/StaggerList'
import { sessionsApi } from '@/api/sessions'
import { treesApi } from '@/api/trees'
import type { Session, TreeListItem } from '@/types'
@@ -213,7 +214,8 @@ export function SessionHistoryPage() {
}
/>
) : (
<div className="space-y-4">
<>
<StaggerList className="space-y-4">
{sessions.map((session) => (
<div
key={session.id}
@@ -301,16 +303,17 @@ export function SessionHistoryPage() {
</div>
</div>
))}
{hasMore ? (
<p className="text-center text-sm text-muted-foreground py-4">
Showing the 50 most recent sessions
</p>
) : sessions.length > 0 ? (
<p className="text-center text-sm text-muted-foreground py-4">
Showing all {sessions.length} sessions
</p>
) : null}
</div>
</StaggerList>
{hasMore ? (
<p className="text-center text-sm text-muted-foreground py-4">
Showing the 50 most recent sessions
</p>
) : sessions.length > 0 ? (
<p className="text-center text-sm text-muted-foreground py-4">
Showing all {sessions.length} sessions
</p>
) : null}
</>
)}
</div>
</>