mirror of
https://github.com/crabwise-ai/crabwalk.git
synced 2026-08-14 09:02:07 +00:00
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:
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { CommandNav } from './CommandNav'
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user