feat: enhance Monitor and Workspace pages with loading indicators and navigation improvements

- Replaced CrabIdleAnimation with geometric loading indicators in both Monitor and Workspace pages for a modernized loading experience.
- Introduced CommandNav component for global navigation in both pages, improving user accessibility.
- Updated header styles and connection status display for better visual feedback and usability.
This commit is contained in:
luccast
2026-02-01 21:09:16 -05:00
parent c107407083
commit 306a3b9c10
4 changed files with 324 additions and 87 deletions
+260
View File
@@ -0,0 +1,260 @@
import { useState, useEffect } from 'react'
import { Link, useLocation } from '@tanstack/react-router'
import { motion, AnimatePresence } from 'framer-motion'
import { Activity, FolderTree, Home, Terminal, ChevronRight, Zap } from 'lucide-react'
interface NavItem {
path: string
label: string
shortcut: string
icon: React.ReactNode
description: string
}
const NAV_ITEMS: NavItem[] = [
{
path: '/',
label: 'HOME',
shortcut: 'H',
icon: <Home size={16} />,
description: 'system root',
},
{
path: '/monitor',
label: 'MONITOR',
shortcut: 'M',
icon: <Activity size={16} />,
description: 'agent tracker',
},
{
path: '/workspace',
label: 'WORKSPACE',
shortcut: 'W',
icon: <FolderTree size={16} />,
description: 'file explorer',
},
]
export function CommandNav() {
const location = useLocation()
const [expanded, setExpanded] = useState(false)
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// ESC to close
if (e.key === 'Escape' && expanded) {
e.preventDefault()
setExpanded(false)
return
}
// Only trigger navigation with Alt/Option key
if (!e.altKey) return
const key = e.key.toUpperCase()
const item = NAV_ITEMS.find((i) => i.shortcut === key)
if (item) {
e.preventDefault()
window.location.href = item.path
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [expanded])
// Close expanded state on route change
useEffect(() => {
setExpanded(false)
}, [location.pathname])
const currentPath = location.pathname.replace(/\/$/, '') || '/'
const currentItem = NAV_ITEMS.find(
(item) => item.path === currentPath || (item.path !== '/' && currentPath.startsWith(item.path))
)
if (!mounted) return null
return (
<>
{/* Main nav trigger - positioned in top-left */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
className="fixed top-4 left-4 z-50"
>
<button
onClick={() => setExpanded(!expanded)}
className="group relative flex items-center gap-3 px-4 py-2.5 bg-shell-900/95 backdrop-blur-sm border border-shell-700/80 rounded-lg transition-all duration-200 hover:border-shell-600 hover:bg-shell-850"
>
{/* Pulse indicator */}
<div className="relative">
<Zap
size={14}
className={`transition-colors duration-200 ${
expanded ? 'text-crab-400' : 'text-shell-500 group-hover:text-crab-400'
}`}
/>
<span className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-neon-mint animate-pulse" />
</div>
{/* Current location */}
<div className="flex items-center gap-2">
<span className="text-shell-500 font-console text-[10px]">/</span>
<span className="font-console text-xs text-gray-300 tracking-wider">
{currentItem?.label || 'UNKNOWN'}
</span>
</div>
{/* Expand indicator */}
<ChevronRight
size={12}
className={`text-shell-500 transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`}
/>
{/* Keyboard hint */}
<div className="hidden sm:flex items-center gap-1 ml-2 pl-3 border-l border-shell-700">
<kbd className="px-1.5 py-0.5 bg-shell-800 rounded text-[9px] font-console text-shell-500">
ALT
</kbd>
<span className="text-shell-600 text-[9px]">+</span>
<kbd className="px-1.5 py-0.5 bg-shell-800 rounded text-[9px] font-console text-shell-500">
{currentItem?.shortcut || '?'}
</kbd>
</div>
</button>
</motion.div>
{/* Expanded navigation panel */}
<AnimatePresence>
{expanded && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
onClick={() => setExpanded(false)}
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-40"
/>
{/* Nav panel */}
<motion.div
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.2, ease: [0.23, 1, 0.32, 1] }}
className="fixed top-4 left-4 z-50 w-72"
>
<div className="bg-shell-900/98 backdrop-blur-md border border-shell-700/80 rounded-xl overflow-hidden shadow-2xl shadow-black/40">
{/* Header */}
<div className="px-4 py-3 border-b border-shell-800 flex items-center justify-between">
<div className="flex items-center gap-2">
<Terminal size={14} className="text-crab-500" />
<span className="font-console text-[10px] text-shell-500 uppercase tracking-widest">
Navigation
</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full bg-crab-500/80" />
<div className="w-2 h-2 rounded-full bg-neon-peach/80" />
<div className="w-2 h-2 rounded-full bg-neon-mint/80" />
</div>
</div>
{/* Nav items */}
<div className="p-2">
{NAV_ITEMS.map((item, index) => {
const isActive =
item.path === currentPath ||
(item.path !== '/' && currentPath.startsWith(item.path))
return (
<motion.div
key={item.path}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2, delay: index * 0.05 }}
>
<Link
to={item.path}
onClick={() => setExpanded(false)}
className={`group flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-150 ${
isActive
? 'bg-crab-500/15 border border-crab-500/30'
: 'border border-transparent hover:bg-shell-800/80 hover:border-shell-700/50'
}`}
>
{/* Icon */}
<div
className={`p-1.5 rounded-md transition-colors duration-150 ${
isActive
? 'bg-crab-500/20 text-crab-400'
: 'bg-shell-800 text-shell-500 group-hover:text-gray-400'
}`}
>
{item.icon}
</div>
{/* Label + description */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span
className={`font-console text-sm tracking-wide ${
isActive ? 'text-crab-400' : 'text-gray-300'
}`}
>
{item.label}
</span>
{isActive && (
<span className="px-1.5 py-0.5 bg-crab-500/20 rounded text-[9px] font-console text-crab-400 uppercase">
active
</span>
)}
</div>
<span className="text-[10px] text-shell-500 font-console">
{item.description}
</span>
</div>
{/* Keyboard shortcut */}
<kbd
className={`px-2 py-1 rounded text-[10px] font-console transition-colors duration-150 ${
isActive
? 'bg-crab-500/20 text-crab-400 border border-crab-500/30'
: 'bg-shell-800 text-shell-500 border border-shell-700 group-hover:border-shell-600'
}`}
>
{item.shortcut}
</kbd>
</Link>
</motion.div>
)
})}
</div>
{/* Footer */}
<div className="px-4 py-2.5 border-t border-shell-800 bg-shell-950/50">
<div className="flex items-center justify-between">
<span className="font-console text-[9px] text-shell-600">
ALT + KEY to navigate
</span>
<span className="font-console text-[9px] text-shell-600">ESC to close</span>
</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
)
}
+1
View File
@@ -0,0 +1 @@
export { CommandNav } from './CommandNav'
+36 -40
View File
@@ -1,9 +1,10 @@
import { useState, useEffect, useCallback, useMemo } from 'react'
import { createFileRoute, Link } from '@tanstack/react-router'
import { createFileRoute } from '@tanstack/react-router'
import { useLiveQuery } from '@tanstack/react-db'
import { motion } from 'framer-motion'
import { ArrowLeft, Loader2, HardDrive, Trash2 } from 'lucide-react'
import { Activity, Loader2, HardDrive, Trash2 } from 'lucide-react'
import { trpc } from '~/integrations/trpc/client'
import { CommandNav } from '~/components/navigation'
import {
sessionsCollection,
actionsCollection,
@@ -22,7 +23,6 @@ import {
SettingsPanel,
StatusIndicator,
} from '~/components/monitor'
import { CrabIdleAnimation } from '~/components/ani'
export const Route = createFileRoute('/monitor/')({
component: MonitorPageWrapper,
@@ -41,15 +41,20 @@ function MonitorPageWrapper() {
animate={{ opacity: 1 }}
className="flex flex-col items-center gap-4"
>
<div className="crab-icon-glow">
<CrabIdleAnimation className="w-16 h-16" />
</div>
<div className="flex items-center gap-3">
<Loader2 size={18} className="animate-spin text-crab-400" />
<span className="font-display text-sm text-gray-400 tracking-wide uppercase">
Loading Monitor...
</span>
{/* Geometric loading indicator */}
<div className="relative w-16 h-16">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
className="absolute inset-0 border-2 border-shell-700 border-t-crab-500 rounded-lg"
/>
<div className="absolute inset-2 bg-shell-900 rounded flex items-center justify-center">
<Activity size={20} className="text-crab-400" />
</div>
</div>
<span className="font-console text-xs text-shell-500 tracking-widest uppercase">
Loading Monitor
</span>
</motion.div>
</div>
)
@@ -365,41 +370,32 @@ function MonitorPage() {
return (
<div className="h-screen flex flex-col bg-shell-950 text-white overflow-hidden">
{/* Global navigation */}
<CommandNav />
{/* Header */}
<header className="flex items-center justify-between px-4 py-3 bg-shell-900 relative">
<header className="flex items-center justify-between px-4 py-3 bg-shell-900/80 backdrop-blur-sm relative z-30">
{/* Gradient accent */}
<div className="absolute inset-0 bg-linear-to-r from-crab-950/20 via-transparent to-transparent pointer-events-none" />
{/* Spacer for nav + connection status */}
<div className="relative flex items-center gap-4">
<Link
to="/"
className="p-2 hover:bg-shell-800 rounded-lg transition-all border border-transparent hover:border-shell-600 group"
>
<ArrowLeft size={18} className="text-gray-400 group-hover:text-crab-400" />
</Link>
<div className="w-48" />
{/* Navigation tabs */}
<div className="flex items-center gap-1">
{/* Monitor tab - active */}
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-crab-900/30 border border-crab-700/30">
<div className="crab-icon-glow">
<CrabIdleAnimation className="w-5 h-5" />
</div>
<span className="font-arcade text-xs text-crab-400 glow-red tracking-wider">
MONITOR
</span>
<StatusIndicator status={connecting ? 'thinking' : connected ? 'active' : 'idle'} />
</div>
{/* Workspace tab - inactive */}
<Link
to="/workspace"
className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-shell-800 transition-all border border-transparent hover:border-shell-600"
>
<span className="font-arcade text-xs text-gray-500 tracking-wider">
WORKSPACE
</span>
</Link>
{/* Connection status pill */}
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border ${
connected
? 'bg-neon-mint/10 border-neon-mint/30'
: connecting
? 'bg-neon-peach/10 border-neon-peach/30'
: 'bg-shell-800/50 border-shell-700'
}`}>
<StatusIndicator status={connecting ? 'thinking' : connected ? 'active' : 'idle'} />
<span className={`font-console text-xs ${
connected ? 'text-neon-mint' : connecting ? 'text-neon-peach' : 'text-shell-500'
}`}>
{connected ? 'CONNECTED' : connecting ? 'CONNECTING' : 'DISCONNECTED'}
</span>
</div>
</div>
+27 -47
View File
@@ -1,9 +1,9 @@
import { useState, useEffect, useCallback } from 'react'
import { createFileRoute, Link } from '@tanstack/react-router'
import { createFileRoute } from '@tanstack/react-router'
import { motion } from 'framer-motion'
import {
ArrowLeft,
FolderOpen,
FolderTree,
RefreshCw,
AlertCircle,
PanelLeft,
@@ -12,6 +12,7 @@ import {
FileText,
} from 'lucide-react'
import { trpc } from '~/integrations/trpc/client'
import { CommandNav } from '~/components/navigation'
import {
FileTree,
MarkdownViewer,
@@ -19,7 +20,6 @@ import {
MobileFileDrawer,
MobilePathSheet,
} from '~/components/workspace'
import { CrabIdleAnimation } from '~/components/ani'
import { useIsMobile } from '~/hooks/useIsMobile'
import type { DirectoryEntry } from '~/lib/workspace-fs'
@@ -53,14 +53,20 @@ function WorkspacePageWrapper() {
animate={{ opacity: 1 }}
className="flex flex-col items-center gap-4"
>
<div className="crab-icon-glow">
<CrabIdleAnimation className="w-16 h-16" />
</div>
<div className="flex items-center gap-3">
<span className="font-display text-sm text-gray-400 tracking-wide uppercase">
Loading Workspace...
</span>
{/* Geometric loading indicator */}
<div className="relative w-16 h-16">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
className="absolute inset-0 border-2 border-shell-700 border-t-crab-500 rounded-lg"
/>
<div className="absolute inset-2 bg-shell-900 rounded flex items-center justify-center">
<FolderTree size={20} className="text-crab-400" />
</div>
</div>
<span className="font-console text-xs text-shell-500 tracking-widest uppercase">
Loading Workspace
</span>
</motion.div>
</div>
)
@@ -359,45 +365,19 @@ function WorkspacePage() {
return (
<div className="h-screen flex flex-col bg-shell-950 text-white overflow-hidden">
{/* Header */}
<header className="flex items-center justify-between px-4 py-3 bg-shell-900 relative">
{/* Global navigation */}
<CommandNav />
{/* Header - path controls */}
<header className="hidden sm:flex items-center justify-between px-4 py-3 bg-shell-900/80 backdrop-blur-sm relative z-30">
{/* Gradient accent */}
<div className="absolute inset-0 bg-linear-to-r from-crab-950/20 via-transparent to-transparent pointer-events-none" />
<div className="relative flex items-center gap-4">
<Link
to="/"
className="p-2 hover:bg-shell-800 rounded-lg transition-all border border-transparent hover:border-shell-600 group"
>
<ArrowLeft size={18} className="text-gray-400 group-hover:text-crab-400" />
</Link>
{/* Spacer for nav button */}
<div className="w-48" />
{/* Navigation tabs */}
<div className="flex items-center gap-1">
{/* Monitor tab - inactive */}
<Link
to="/monitor"
className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-shell-800 transition-all border border-transparent hover:border-shell-600"
>
<span className="font-arcade text-xs text-gray-500 tracking-wider">
MONITOR
</span>
</Link>
{/* Workspace tab - active */}
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-crab-900/30 border border-crab-700/30">
<div className="crab-icon-glow">
<CrabIdleAnimation className="w-5 h-5" />
</div>
<span className="font-arcade text-xs text-crab-400 glow-red tracking-wider">
WORKSPACE
</span>
</div>
</div>
</div>
{/* Path input - desktop only */}
<div className="hidden sm:flex relative items-center gap-2 flex-1 max-w-2xl mx-4">
{/* Path input - centered */}
<div className="flex relative items-center gap-2 flex-1 max-w-2xl">
<div className="flex-1 relative">
<FolderOpen size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-shell-500 pointer-events-none" />
<input
@@ -429,8 +409,8 @@ function WorkspacePage() {
)}
</div>
{/* Refresh button - desktop only */}
<div className="hidden sm:flex relative items-center gap-3">
{/* Refresh button */}
<div className="flex relative items-center gap-3 ml-4">
<button
onClick={handleRefresh}
disabled={!pathValid || loading}