feat: implement monochrome design system across entire frontend
Migrate all 84 frontend files from the old themed/colored design to a monochrome glass-morphism design system. Pure black backgrounds, white text with opacity levels, glass-card components with backdrop-blur, and functional color reserved for status indicators only. Foundation: remap CSS variables to monochrome, simplify Tailwind config, remove theme toggle, convert brand logo/wordmark to white. Pages: all 14 pages updated. Components: all common, library, session, step-library, tree-editor, tree-preview, admin, and subscription components converted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
227
frontend/src/components/layout/AppLayout-original.tsx
Normal file
227
frontend/src/components/layout/AppLayout-original.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Link, useLocation, useNavigate, Outlet } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { ThemeToggle } from '@/components/common/ThemeToggle'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { BrandWordmark } from '@/components/common/BrandWordmark'
|
||||
import { Menu, X } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function AppLayout() {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { user, logout } = useAuthStore()
|
||||
const { effectiveRole, isSuperAdmin } = usePermissions()
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
const handleLogout = async () => {
|
||||
setMobileMenuOpen(false)
|
||||
await logout()
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
// Close mobile menu on route change - key-based reset
|
||||
const [prevPath, setPrevPath] = useState(location.pathname)
|
||||
if (prevPath !== location.pathname) {
|
||||
setPrevPath(location.pathname)
|
||||
if (mobileMenuOpen) setMobileMenuOpen(false)
|
||||
}
|
||||
|
||||
// Close on Escape
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setMobileMenuOpen(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (mobileMenuOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
}, [mobileMenuOpen, handleKeyDown])
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', label: 'Home' },
|
||||
{ path: '/trees', label: 'Trees' },
|
||||
{ path: '/my-trees', label: 'My Trees' },
|
||||
{ path: '/sessions', label: 'Sessions' },
|
||||
{ path: '/account', label: 'Account' },
|
||||
{ path: '/settings', label: 'Settings' },
|
||||
...(isSuperAdmin ? [{ path: '/admin', label: 'Admin Panel' }] : []),
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-card backdrop-blur-sm">
|
||||
<div className="container mx-auto flex h-16 items-center justify-between px-4">
|
||||
<div className="flex items-center gap-8">
|
||||
{/* Mobile hamburger */}
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground sm:hidden"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<BrandLogo size="sm" />
|
||||
<BrandWordmark size="sm" />
|
||||
</Link>
|
||||
<nav className="hidden items-center gap-1 sm:flex">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'relative rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||
(item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path))
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="hidden text-sm text-muted-foreground sm:block">
|
||||
{user?.name || user?.email}
|
||||
</span>
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<span
|
||||
className={cn(
|
||||
'hidden rounded-full px-2 py-0.5 text-xs font-medium sm:inline-block',
|
||||
effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400',
|
||||
effectiveRole === 'owner' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
||||
effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
|
||||
)}
|
||||
>
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={cn(
|
||||
'hidden rounded-md px-3 py-1.5 text-sm font-medium sm:block',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile Nav Drawer */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="fixed inset-0 z-50 sm:hidden">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-background/80 backdrop-blur-sm animate-fade-in"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<nav className="absolute inset-y-0 left-0 w-72 border-r border-border bg-card shadow-xl animate-slide-in-left">
|
||||
<div className="flex h-16 items-center justify-between border-b border-border px-4">
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<BrandLogo size="sm" />
|
||||
<BrandWordmark size="sm" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col p-4">
|
||||
{/* User info */}
|
||||
<div className="mb-4 border-b border-border pb-4">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{user?.name || user?.email}
|
||||
</p>
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<span
|
||||
className={cn(
|
||||
'mt-1 inline-block rounded-full px-2 py-0.5 text-xs font-medium',
|
||||
effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400',
|
||||
effectiveRole === 'owner' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
||||
effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
|
||||
)}
|
||||
>
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nav items */}
|
||||
<div className="space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'block rounded-md px-3 py-2.5 text-sm font-medium transition-colors',
|
||||
(item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path))
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Theme toggle */}
|
||||
<div className="mt-4 border-t border-border pt-4">
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<span className="text-sm text-muted-foreground">Theme</span>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="mt-2">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={cn(
|
||||
'w-full rounded-md px-3 py-2.5 text-left text-sm font-medium',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="animate-fade-in">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppLayout
|
||||
@@ -2,10 +2,8 @@ import { useState, useEffect, useCallback } from 'react'
|
||||
import { Link, useLocation, useNavigate, Outlet } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { ThemeToggle } from '@/components/common/ThemeToggle'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { BrandWordmark } from '@/components/common/BrandWordmark'
|
||||
import { Menu, X } from 'lucide-react'
|
||||
import { Menu, X, LogOut, User, Shield } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function AppLayout() {
|
||||
@@ -21,7 +19,7 @@ export function AppLayout() {
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
// Close mobile menu on route change - key-based reset
|
||||
// Close mobile menu on route change
|
||||
const [prevPath, setPrevPath] = useState(location.pathname)
|
||||
if (prevPath !== location.pathname) {
|
||||
setPrevPath(location.pathname)
|
||||
@@ -57,68 +55,92 @@ export function AppLayout() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="min-h-screen bg-black">
|
||||
{/* Subtle radial overlay for depth */}
|
||||
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%),radial-gradient(circle_at_80%_80%,rgba(80,80,100,0.02),transparent_50%)]" />
|
||||
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-card backdrop-blur-sm">
|
||||
<header className="sticky top-0 z-50 border-b border-white/[0.06] bg-black/80 backdrop-blur-xl">
|
||||
<div className="container mx-auto flex h-16 items-center justify-between px-4">
|
||||
<div className="flex items-center gap-8">
|
||||
{/* Mobile hamburger */}
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground sm:hidden"
|
||||
className="rounded-xl p-2 text-white/50 hover:bg-white/10 hover:text-white transition-all sm:hidden"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<BrandLogo size="sm" />
|
||||
<BrandWordmark size="sm" />
|
||||
{/* Logo */}
|
||||
<Link to="/" className="flex items-center gap-3 group">
|
||||
<div className="w-9 h-9 rounded-xl bg-white flex items-center justify-center transition-transform group-hover:scale-105">
|
||||
<BrandLogo size="sm" className="h-5 w-5 invert" />
|
||||
</div>
|
||||
<span className="text-xl font-semibold text-white tracking-tight">
|
||||
ResolutionFlow
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden items-center gap-1 sm:flex">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'relative rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||
(item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path))
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
{navItems.map((item) => {
|
||||
const isActive = item.path === '/'
|
||||
? location.pathname === '/'
|
||||
: location.pathname.startsWith(item.path)
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'rounded-xl px-4 py-2 text-sm font-medium transition-all',
|
||||
isActive
|
||||
? 'bg-white/10 text-white border border-white/20'
|
||||
: 'text-white/50 hover:text-white hover:bg-white/[0.06]'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="hidden text-sm text-muted-foreground sm:block">
|
||||
{user?.name || user?.email}
|
||||
</span>
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<span
|
||||
className={cn(
|
||||
'hidden rounded-full px-2 py-0.5 text-xs font-medium sm:inline-block',
|
||||
effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400',
|
||||
effectiveRole === 'owner' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
||||
effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
|
||||
)}
|
||||
>
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
{/* Right side controls */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* User info */}
|
||||
<div className="hidden items-center gap-3 sm:flex">
|
||||
<div className="flex items-center gap-2 rounded-xl bg-white/[0.06] px-3 py-1.5 border border-white/10">
|
||||
<User className="h-4 w-4 text-white/40" />
|
||||
<span className="text-sm text-white/70">
|
||||
{user?.name || user?.email}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Role badge */}
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<div className="px-3 py-1.5 rounded-xl bg-white/10 border border-white/20">
|
||||
<span className="flex items-center gap-1.5 text-xs text-white font-semibold">
|
||||
<Shield className="h-3 w-3" />
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Logout button */}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={cn(
|
||||
'hidden rounded-md px-3 py-1.5 text-sm font-medium sm:block',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
'hidden items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium sm:flex',
|
||||
'text-white/50 hover:text-white hover:bg-white/10 transition-all',
|
||||
'border border-white/10 hover:border-white/20'
|
||||
)}
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
@@ -130,21 +152,25 @@ export function AppLayout() {
|
||||
<div className="fixed inset-0 z-50 sm:hidden">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-background/80 backdrop-blur-sm animate-fade-in"
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<nav className="absolute inset-y-0 left-0 w-72 border-r border-border bg-card shadow-xl animate-slide-in-left">
|
||||
<div className="flex h-16 items-center justify-between border-b border-border px-4">
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<BrandLogo size="sm" />
|
||||
<BrandWordmark size="sm" />
|
||||
<nav className="absolute inset-y-0 left-0 w-72 border-r border-white/[0.06] bg-black shadow-2xl animate-in slide-in-from-left duration-300">
|
||||
<div className="flex h-16 items-center justify-between border-b border-white/[0.06] px-4">
|
||||
<Link to="/" className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-white flex items-center justify-center">
|
||||
<BrandLogo size="sm" className="h-5 w-5 invert" />
|
||||
</div>
|
||||
<span className="text-xl font-semibold text-white tracking-tight">
|
||||
ResolutionFlow
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
className="rounded-xl p-2 text-white/50 hover:bg-white/10 hover:text-white transition-all"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
@@ -153,61 +179,60 @@ export function AppLayout() {
|
||||
|
||||
<div className="flex flex-col p-4">
|
||||
{/* User info */}
|
||||
<div className="mb-4 border-b border-border pb-4">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{user?.name || user?.email}
|
||||
</p>
|
||||
<div className="mb-4 border-b border-white/[0.06] pb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<User className="h-4 w-4 text-white/40" />
|
||||
<p className="text-sm font-medium text-white">
|
||||
{user?.name || user?.email}
|
||||
</p>
|
||||
</div>
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<span
|
||||
className={cn(
|
||||
'mt-1 inline-block rounded-full px-2 py-0.5 text-xs font-medium',
|
||||
effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400',
|
||||
effectiveRole === 'owner' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
||||
effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
|
||||
)}
|
||||
>
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
<div className="inline-flex px-3 py-1.5 rounded-xl bg-white/10 border border-white/20">
|
||||
<span className="flex items-center gap-1.5 text-xs text-white font-semibold">
|
||||
<Shield className="h-3 w-3" />
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nav items */}
|
||||
<div className="space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'block rounded-md px-3 py-2.5 text-sm font-medium transition-colors',
|
||||
(item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path))
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{navItems.map((item) => {
|
||||
const isActive = item.path === '/'
|
||||
? location.pathname === '/'
|
||||
: location.pathname.startsWith(item.path)
|
||||
|
||||
{/* Theme toggle */}
|
||||
<div className="mt-4 border-t border-border pt-4">
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<span className="text-sm text-muted-foreground">Theme</span>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'block rounded-xl px-4 py-3 text-sm font-medium transition-all',
|
||||
isActive
|
||||
? 'bg-white/10 text-white border border-white/20'
|
||||
: 'text-white/50 hover:text-white hover:bg-white/[0.06]'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="mt-2">
|
||||
<div className="mt-4 border-t border-white/[0.06] pt-4">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={cn(
|
||||
'w-full rounded-md px-3 py-2.5 text-left text-sm font-medium',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
'w-full flex items-center gap-2 rounded-xl px-4 py-3 text-sm font-medium',
|
||||
'text-white/50 hover:text-white hover:bg-white/10 transition-all',
|
||||
'border border-white/10 hover:border-white/20'
|
||||
)}
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
@@ -217,7 +242,7 @@ export function AppLayout() {
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="animate-fade-in">
|
||||
<main className="relative animate-in fade-in duration-500">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps)
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user