diff --git a/design-system.md b/design-system.md
new file mode 100644
index 0000000..94b8985
--- /dev/null
+++ b/design-system.md
@@ -0,0 +1,323 @@
+# Crabwalk Design System
+
+## Aesthetic Direction: **Terminal Control Panel**
+
+A command-line inspired interface that feels like operating a sophisticated monitoring system. Clean, precise, information-dense but not cluttered. The aesthetic borrows from terminal emulators, mission control panels, and developer tools—functional beauty.
+
+---
+
+## Core Principles
+
+### 1. Information Hierarchy Through Typography
+- **Labels**: Tiny, uppercase, muted (`text-[10px] text-shell-500 uppercase tracking-widest`)
+- **Values**: Slightly larger, brighter (`text-xs text-gray-300`)
+- **Key Metrics**: Accent colors with the display font (`text-sm text-neon-mint`)
+- **Actions**: Console font, tracking-wider (`font-console tracking-wider`)
+
+### 2. Contained Modules
+Every piece of information lives in a clearly bounded container:
+- `bg-shell-900/95` or `bg-shell-800/50` backgrounds
+- `border border-shell-700/80` borders
+- `rounded-lg` corners (8px)
+- `backdrop-blur-sm` for layered elements
+
+### 3. Status Through Color
+| State | Color | CSS Class |
+|-------|-------|-----------|
+| Active/Connected | Mint | `text-neon-mint`, `bg-neon-mint/10` |
+| Warning/Processing | Peach | `text-neon-peach`, `bg-neon-peach/10` |
+| Error/Primary Action | Crab Red | `text-crab-400`, `bg-crab-500/15` |
+| Inactive/Muted | Shell Gray | `text-shell-500`, `bg-shell-800` |
+
+### 4. Pulse Indicators
+Small animated dots communicate live status:
+```tsx
+
+```
+
+### 5. Traffic Light Decorations
+Decorative dots that reference classic window controls, add personality:
+```tsx
+
+```
+
+---
+
+## Color Palette
+
+### Primary: Crab Reds
+```css
+--color-crab-400: #f87171; /* Text accents */
+--color-crab-500: #ef4444; /* Primary actions */
+--color-crab-600: #dc2626; /* Buttons */
+--color-crab-900: #7f1d1d; /* Backgrounds */
+```
+
+### Backgrounds: Shell Darks
+```css
+--color-shell-950: #0a0a0f; /* Page background */
+--color-shell-900: #12121a; /* Card backgrounds */
+--color-shell-800: #1a1a26; /* Input backgrounds */
+--color-shell-700: #252535; /* Borders */
+--color-shell-500: #52526e; /* Muted text */
+```
+
+### Accents: Neon Status
+```css
+--color-neon-mint: #98ffc8; /* Success, active, connected */
+--color-neon-peach: #ffb088; /* Warning, processing */
+--color-neon-coral: #ff6b6b; /* Attention */
+--color-neon-cyan: #00ffff; /* Special highlights */
+```
+
+---
+
+## Typography
+
+### Font Stack
+```css
+--font-arcade: 'Press Start 2P'; /* Headlines only, sparingly */
+--font-console: 'JetBrains Mono'; /* Primary UI font */
+```
+
+### Type Scale
+| Use Case | Size | Weight | Tracking |
+|----------|------|--------|----------|
+| Micro labels | `text-[9px]` | Regular | `tracking-widest` |
+| Labels | `text-[10px]` | Regular | `tracking-widest` |
+| Body/UI | `text-xs` (12px) | Regular | `tracking-wider` |
+| Values | `text-sm` (14px) | Medium | Default |
+| Headers | `text-base` (16px) | Semibold | Default |
+
+---
+
+## Component Patterns
+
+### Status Pill
+```tsx
+
+
+
+ {active ? 'CONNECTED' : 'DISCONNECTED'}
+
+
+```
+
+### Stat Block
+```tsx
+
+
+ Sessions
+
+
+ {count}
+
+
+```
+
+### Keyboard Shortcut Badge
+```tsx
+
+ ALT
+
+```
+
+### Section Divider (Vertical)
+```tsx
+
+```
+
+### Icon Container
+```tsx
+
+
+
+```
+
+### Input Field
+```tsx
+
+
+
+
+```
+
+### Action Button (Primary)
+```tsx
+
+```
+
+### Action Button (Ghost)
+```tsx
+
+```
+
+---
+
+## Animation Guidelines
+
+### Timing Functions
+- **Snappy interactions**: `duration-150`
+- **Smooth transitions**: `duration-200`
+- **Entrance animations**: `duration-300`
+
+### Motion Presets
+```tsx
+// Panel entrance
+initial={{ opacity: 0, y: -10, scale: 0.95 }}
+animate={{ opacity: 1, y: 0, scale: 1 }}
+transition={{ duration: 0.2, ease: [0.23, 1, 0.32, 1] }}
+
+// Fade backdrop
+initial={{ opacity: 0 }}
+animate={{ opacity: 1 }}
+transition={{ duration: 0.15 }}
+
+// Staggered list items
+transition={{ duration: 0.2, delay: index * 0.05 }}
+
+// Rotating loader
+animate={{ rotate: 360 }}
+transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
+```
+
+---
+
+## Layout Patterns
+
+### Header Structure
+```
+┌─────────────────────────────────────────────────────────────┐
+│ [Nav Trigger] [Context Controls] [Stats] [Actions] │
+└─────────────────────────────────────────────────────────────┘
+```
+
+- Nav trigger: Fixed position overlay (z-50)
+- Header: Sticky, semi-transparent (`bg-shell-900/80 backdrop-blur-sm`)
+- Left side: Spacer for nav + page-specific context
+- Right side: Stats, status indicators, action buttons
+
+### Z-Index Layers
+| Layer | Z-Index | Use |
+|-------|---------|-----|
+| Base content | 0 | Main content area |
+| In-page controls | 10 | Graph controls, sticky headers |
+| Header | 30 | App header bar |
+| Toolbar | 40 | Mobile bottom toolbar |
+| Navigation backdrop | 40 | CommandNav backdrop |
+| Navigation/drawers | 50 | CommandNav panel, mobile drawers |
+| Settings backdrop | 60 | Settings panel backdrop |
+| Settings panel | 70 | Settings panel (top layer) |
+
+---
+
+## Iconography
+
+Use **Lucide React** icons exclusively. Preferred sizes:
+- Inline with text: `14px`
+- Standalone buttons: `16-18px`
+- Feature icons: `20px`
+- Loading states: `20px`
+
+Icon style: Stroke-based, 2px stroke width (Lucide default).
+
+---
+
+## Responsive Behavior
+
+### Breakpoints
+- `sm:` (640px) - Mobile/desktop split
+- Headers hide on mobile, replaced by bottom toolbar
+- Stats/secondary info hidden on mobile
+
+### Mobile Patterns
+- Bottom fixed toolbar for primary actions
+- Slide-up sheets for forms
+- Slide-in drawers for navigation lists
+- Larger touch targets (min 44x44px)
+
+---
+
+## Loading States
+
+### Page Loading
+Geometric spinner with context icon:
+```tsx
+
+```
+
+### Inline Loading
+```tsx
+
+```
+
+### Connection Retry
+```tsx
+
+
+
+ retrying ({count}/{max})...
+
+
+```
+
+---
+
+## Do's and Don'ts
+
+### Do
+- Use monospace font for all UI text
+- Keep labels uppercase and tiny
+- Use color to indicate state, not decoration
+- Add subtle borders to define boundaries
+- Include keyboard shortcuts for power users
+- Use backdrop blur for layered elements
+
+### Don't
+- Use large, bold headers
+- Add decorative gradients without purpose
+- Use more than 2-3 accent colors per view
+- Animate everything—be selective
+- Use rounded-full except for status dots
+- Mix different font families
+
+---
+
+## File Organization
+
+```
+src/
+├── components/
+│ ├── navigation/ # Global nav system
+│ ├── monitor/ # Monitor-specific components
+│ ├── workspace/ # Workspace-specific components
+│ └── ui/ # Shared UI primitives (future)
+├── hooks/
+│ └── useIsMobile.ts # Responsive detection
+└── styles.css # Global styles & CSS variables
+```
diff --git a/src/components/layout/AppHeader.tsx b/src/components/layout/AppHeader.tsx
new file mode 100644
index 0000000..49c57c2
--- /dev/null
+++ b/src/components/layout/AppHeader.tsx
@@ -0,0 +1,326 @@
+import { motion } from 'framer-motion'
+import { Loader2 } from 'lucide-react'
+import { ReactNode } from 'react'
+
+interface AppHeaderProps {
+ /** Left side content - appears after nav spacer */
+ left?: ReactNode
+ /** Center content - typically page-specific controls */
+ center?: ReactNode
+ /** Right side content - stats, actions, settings */
+ right?: ReactNode
+ /** Whether header is visible (hidden on mobile) */
+ hiddenOnMobile?: boolean
+}
+
+export function AppHeader({ left, center, right, hiddenOnMobile = true }: AppHeaderProps) {
+ return (
+
+ )
+}
+
+// ============================================================================
+// Header Building Blocks
+// ============================================================================
+
+/** Status pill showing connection or active state */
+interface StatusPillProps {
+ status: 'connected' | 'connecting' | 'disconnected' | 'active' | 'inactive'
+ label?: string
+}
+
+export function StatusPill({ status, label }: StatusPillProps) {
+ const config = {
+ connected: {
+ bg: 'bg-neon-mint/10 border-neon-mint/30',
+ dot: 'bg-neon-mint animate-pulse',
+ text: 'text-neon-mint',
+ defaultLabel: 'CONNECTED',
+ },
+ active: {
+ bg: 'bg-neon-mint/10 border-neon-mint/30',
+ dot: 'bg-neon-mint animate-pulse',
+ text: 'text-neon-mint',
+ defaultLabel: 'ACTIVE',
+ },
+ connecting: {
+ bg: 'bg-neon-peach/10 border-neon-peach/30',
+ dot: 'bg-neon-peach animate-pulse',
+ text: 'text-neon-peach',
+ defaultLabel: 'CONNECTING',
+ },
+ disconnected: {
+ bg: 'bg-shell-800/50 border-shell-700',
+ dot: 'bg-shell-600',
+ text: 'text-shell-500',
+ defaultLabel: 'DISCONNECTED',
+ },
+ inactive: {
+ bg: 'bg-shell-800/50 border-shell-700',
+ dot: 'bg-shell-600',
+ text: 'text-shell-500',
+ defaultLabel: 'INACTIVE',
+ },
+ }
+
+ const c = config[status]
+
+ return (
+
+
+
+ {label || c.defaultLabel}
+
+
+ )
+}
+
+/** Stat block showing a label and value */
+interface StatBlockProps {
+ label: string
+ value: string | number
+ color?: 'mint' | 'peach' | 'coral' | 'default'
+}
+
+export function StatBlock({ label, value, color = 'default' }: StatBlockProps) {
+ const colorClass = {
+ mint: 'text-neon-mint',
+ peach: 'text-neon-peach',
+ coral: 'text-neon-coral',
+ default: 'text-gray-300',
+ }[color]
+
+ return (
+
+
+ {label}
+
+ {value}
+
+ )
+}
+
+/** Stats container with dividers */
+export function StatsGroup({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
+
+/** Vertical divider for stats */
+export function StatsDivider() {
+ return
+}
+
+/** Icon button with hover states */
+interface IconButtonProps {
+ icon: ReactNode
+ onClick?: () => void
+ disabled?: boolean
+ loading?: boolean
+ title?: string
+ active?: boolean
+ variant?: 'ghost' | 'subtle'
+}
+
+export function IconButton({
+ icon,
+ onClick,
+ disabled,
+ loading,
+ title,
+ active,
+ variant = 'ghost',
+}: IconButtonProps) {
+ const baseStyles = 'p-2 rounded-lg transition-all group'
+ const variantStyles = {
+ ghost: `hover:bg-shell-800 border border-transparent hover:border-shell-600 ${
+ active ? 'bg-shell-800 border-shell-600' : ''
+ }`,
+ subtle: `bg-shell-800/50 hover:bg-shell-700/50 border border-shell-700/50 hover:border-shell-600 ${
+ active ? 'bg-crab-500/10 border-crab-500/30' : ''
+ }`,
+ }
+
+ return (
+
+ )
+}
+
+/** Badge counter for notifications/counts */
+interface BadgeCounterProps {
+ count: number
+ onClick?: () => void
+ icon: ReactNode
+ title?: string
+}
+
+export function BadgeCounter({ count, onClick, icon, title }: BadgeCounterProps) {
+ if (count === 0) return null
+
+ return (
+
+ )
+}
+
+/** Service indicator (like persistence) */
+interface ServiceIndicatorProps {
+ active: boolean
+ icon: ReactNode
+ onClick?: () => void
+ title?: string
+}
+
+export function ServiceIndicator({ active, icon, onClick, title }: ServiceIndicatorProps) {
+ return (
+
+ )
+}
+
+/** Retry indicator for connection attempts */
+interface RetryIndicatorProps {
+ retryCount: number
+ maxRetries: number
+}
+
+export function RetryIndicator({ retryCount, maxRetries }: RetryIndicatorProps) {
+ return (
+
+
+
+ RETRY {retryCount}/{maxRetries}
+
+
+ )
+}
+
+/** Path input field styled for the header */
+interface PathInputProps {
+ value: string
+ onChange: (value: string) => void
+ onSubmit: () => void
+ placeholder?: string
+ icon: ReactNode
+ submitLabel?: string
+ submitDisabled?: boolean
+ error?: string | null
+}
+
+export function PathInput({
+ value,
+ onChange,
+ onSubmit,
+ placeholder,
+ icon,
+ submitLabel = 'Open',
+ submitDisabled,
+ error,
+}: PathInputProps) {
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ onSubmit()
+ }
+ }
+
+ return (
+
+
+
+ {icon}
+
+
onChange(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder={placeholder}
+ className="w-full bg-shell-800 border border-shell-700 rounded-lg pl-9 pr-3 py-2 text-sm font-console text-gray-200 placeholder-shell-500 focus:outline-none focus:border-crab-500 focus:ring-1 focus:ring-crab-500/20 transition-colors"
+ />
+
+
+
+ {error && (
+
+
+ {error}
+
+ )}
+
+ )
+}
diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts
new file mode 100644
index 0000000..190180e
--- /dev/null
+++ b/src/components/layout/index.ts
@@ -0,0 +1,12 @@
+export {
+ AppHeader,
+ StatusPill,
+ StatBlock,
+ StatsGroup,
+ StatsDivider,
+ IconButton,
+ BadgeCounter,
+ ServiceIndicator,
+ RetryIndicator,
+ PathInput,
+} from './AppHeader'
diff --git a/src/components/monitor/SettingsPanel.tsx b/src/components/monitor/SettingsPanel.tsx
index fee7808..cb59d1c 100644
--- a/src/components/monitor/SettingsPanel.tsx
+++ b/src/components/monitor/SettingsPanel.tsx
@@ -25,6 +25,8 @@ interface SettingsPanelProps {
onPersistenceStart: () => void
onPersistenceStop: () => void
onPersistenceClear: () => void
+ /** Hide the built-in trigger button (use when providing external trigger) */
+ hideTrigger?: boolean
}
export function SettingsPanel({
@@ -50,16 +52,19 @@ export function SettingsPanel({
onPersistenceStart,
onPersistenceStop,
onPersistenceClear,
+ hideTrigger,
}: SettingsPanelProps) {
return (
<>
-
+ {!hideTrigger && (
+
+ )}
{open && (
@@ -70,7 +75,7 @@ export function SettingsPanel({
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => onOpenChange(false)}
- className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40"
+ className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[60]"
/>
{/* Panel */}
@@ -79,7 +84,7 @@ export function SettingsPanel({
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 300 }}
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
- className="fixed right-0 top-0 h-full w-80 bg-shell-900 z-50 p-5 overflow-y-auto"
+ className="fixed right-0 top-0 h-full w-80 bg-shell-900 z-[70] p-5 overflow-y-auto"
>
{/* Texture overlay */}
diff --git a/src/components/workspace/FileTree.tsx b/src/components/workspace/FileTree.tsx
index 7ff6fa6..fde2bb2 100644
--- a/src/components/workspace/FileTree.tsx
+++ b/src/components/workspace/FileTree.tsx
@@ -124,14 +124,14 @@ function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, level }:
{/* Icon */}
{isDirectory ? (
expanded ? (
-
+
) : (
-
+
)
) : (
@@ -148,7 +148,7 @@ function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, level }:
{/* Metadata for files */}
{!isDirectory && (
-
+
{entry.size !== undefined && formatFileSize(entry.size)}
)}
diff --git a/src/routes/monitor/index.tsx b/src/routes/monitor/index.tsx
index 6f58364..dda4edf 100644
--- a/src/routes/monitor/index.tsx
+++ b/src/routes/monitor/index.tsx
@@ -1,10 +1,20 @@
import { useState, useEffect, useCallback, useMemo } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { useLiveQuery } from '@tanstack/react-db'
-import { motion } from 'framer-motion'
-import { Activity, Loader2, HardDrive, Trash2 } from 'lucide-react'
+import { motion, AnimatePresence } from 'framer-motion'
+import { Activity, HardDrive, Trash2, Settings } from 'lucide-react'
import { trpc } from '~/integrations/trpc/client'
import { CommandNav } from '~/components/navigation'
+import {
+ AppHeader,
+ StatusPill,
+ StatBlock,
+ StatsGroup,
+ StatsDivider,
+ BadgeCounter,
+ ServiceIndicator,
+ RetryIndicator,
+} from '~/components/layout'
import {
sessionsCollection,
actionsCollection,
@@ -21,7 +31,6 @@ import {
ActionGraph,
SessionList,
SettingsPanel,
- StatusIndicator,
} from '~/components/monitor'
export const Route = createFileRoute('/monitor/')({
@@ -374,120 +383,55 @@ function MonitorPage() {
{/* Header */}
-
- {/* Gradient accent */}
-
-
- {/* Spacer for nav + connection status */}
-
-
-
- {/* Connection status pill */}
-
-
-
- {connected ? 'CONNECTED' : connecting ? 'CONNECTING' : 'DISCONNECTED'}
-
-
-
-
-
- {connecting && (
-
-
-
- {retryCount > 0 ? `retrying (${retryCount}/${MAX_RETRIES})...` : 'connecting...'}
-
-
- )}
-
- {/* Clear Completed button */}
- {completedCount > 0 && (
-
- )}
-
- {/* Persistence indicator */}
-
- {/* Stats display */}
-
-
- Sessions
- {sessions.length}
-
-
-
- Actions
- {actions.length}
-
-
+ {/* Persistence service */}
+
}
+ onClick={() => setSettingsOpen(true)}
+ title={persistenceEnabled ? 'Background service running' : 'Service stopped'}
+ />
-
-
-
+ {/* Stats */}
+
+
+
+
+
+
+ {/* Settings trigger */}
+
+ >
+ }
+ />
{/* Main content */}
@@ -511,6 +455,33 @@ function MonitorPage() {
/>
+
+ {/* Settings panel - rendered at root level to avoid z-index clipping */}
+
)
}
diff --git a/src/routes/workspace/index.tsx b/src/routes/workspace/index.tsx
index d39a4c3..b188345 100644
--- a/src/routes/workspace/index.tsx
+++ b/src/routes/workspace/index.tsx
@@ -5,7 +5,6 @@ import {
FolderOpen,
FolderTree,
RefreshCw,
- AlertCircle,
PanelLeft,
PanelLeftClose,
Star,
@@ -13,8 +12,9 @@ import {
} from 'lucide-react'
import { trpc } from '~/integrations/trpc/client'
import { CommandNav } from '~/components/navigation'
+import { AppHeader, StatusPill, IconButton, PathInput } from '~/components/layout'
import {
- FileTree,
+ FileTree as FileTreeComponent,
MarkdownViewer,
MobileBottomToolbar,
MobileFileDrawer,
@@ -340,12 +340,6 @@ function WorkspacePage() {
}
}, [workspacePath, pathValid, selectedPath, loadFile])
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === 'Enter') {
- validateAndSetPath()
- }
- }
-
// Handle starring/unstarring files
const handleStar = useCallback((filePath: string) => {
setStarredPaths((prev) => {
@@ -368,62 +362,34 @@ function WorkspacePage() {
{/* Global navigation */}
- {/* Header - path controls */}
-
- {/* Gradient accent */}
-
-
- {/* Spacer for nav button */}
-
-
- {/* Path input - centered */}
-
-
-
- setWorkspacePathInput(e.target.value)}
- onKeyDown={handleKeyDown}
- placeholder="Enter workspace path..."
- className="w-full bg-shell-800 border border-shell-700 rounded-lg pl-9 pr-3 py-1.5 text-sm font-console text-gray-200 placeholder-shell-500 focus:outline-none focus:border-crab-500 focus:ring-1 focus:ring-crab-500/20"
- />
-
-
-
- {pathError && (
-
- )}
-
-
- {/* Refresh button */}
-
-
+ }
+ center={
+
}
+ submitDisabled={pathValid && workspacePathInput === workspacePath}
+ error={pathError}
+ />
+ }
+ right={
+
}
onClick={handleRefresh}
disabled={!pathValid || loading}
- className="p-2 hover:bg-shell-800 rounded-lg transition-all border border-transparent hover:border-shell-600 disabled:opacity-50 disabled:cursor-not-allowed group"
- title="Refresh"
- >
-
-
-
-
+ title="Refresh workspace"
+ />
+ }
+ />
{/* Main content */}
@@ -518,7 +484,7 @@ function WorkspacePage() {
>
@@ -530,7 +496,7 @@ function WorkspacePage() {
e.stopPropagation()
handleStar(filePath)
}}
- className="text-yellow-400 hover:text-yellow-300 flex-shrink-0"
+ className="text-yellow-400 hover:text-yellow-300 shrink-0"
title="Unstar file"
>
@@ -547,7 +513,7 @@ function WorkspacePage() {
{!sidebarCollapsed && (
{pathValid ? (
-