102 lines
4.4 KiB
TypeScript
102 lines
4.4 KiB
TypeScript
import { Link } from '@tanstack/react-router'
|
|
import { Search } from 'lucide-react'
|
|
import { ThemeToggle } from '@/components/ThemeToggle'
|
|
import { HealthDot } from '@/components/HealthDot'
|
|
import { NavBadge } from '@/components/NavBadge'
|
|
import { trpc } from '@/lib/trpc'
|
|
import type { ConnectionState } from '@/hooks/useConnectionStatus'
|
|
|
|
const navItems = [
|
|
{ label: 'HQ', to: '/hq', badgeKey: null },
|
|
{ label: 'Initiatives', to: '/initiatives', badgeKey: null },
|
|
{ label: 'Agents', to: '/agents', badgeKey: 'running' as const },
|
|
{ label: 'Radar', to: '/radar', badgeKey: null },
|
|
{ label: 'Inbox', to: '/inbox', badgeKey: 'questions' as const },
|
|
{ label: 'Settings', to: '/settings', badgeKey: null },
|
|
] as const
|
|
|
|
interface AppLayoutProps {
|
|
children: React.ReactNode
|
|
onOpenCommandPalette?: () => void
|
|
connectionState: ConnectionState
|
|
}
|
|
|
|
export function AppLayout({ children, onOpenCommandPalette, connectionState }: AppLayoutProps) {
|
|
const agents = trpc.listAgents.useQuery(undefined, {
|
|
refetchInterval: 10000,
|
|
})
|
|
|
|
const badgeCounts = {
|
|
running: agents.data?.filter((a) => a.status === 'running').length ?? 0,
|
|
questions: agents.data?.filter((a) => a.status === 'waiting_for_input').length ?? 0,
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-screen flex-col bg-background">
|
|
{/* Single-row 48px header */}
|
|
<header className="relative z-sticky shrink-0 border-b border-border/50 bg-background/95 shadow-[0_1px_0_0_hsl(var(--border)/0.5),0_1px_3px_-1px_hsl(var(--primary)/0.1)] backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<div className="flex h-12 items-center justify-between px-5">
|
|
{/* Left: Logo + Nav */}
|
|
<div className="flex items-center gap-6">
|
|
<Link to="/hq" className="flex items-center gap-2">
|
|
<img src="/icon-dark-48.png" alt="" className="h-7 w-7 dark:hidden" />
|
|
<img src="/icon-light-48.png" alt="" className="hidden h-7 w-7 dark:block" />
|
|
<span className="hidden font-display font-bold tracking-tight sm:inline">
|
|
Codewalkers
|
|
</span>
|
|
</Link>
|
|
<nav className="flex items-center gap-1">
|
|
{navItems.map((item) => (
|
|
<Link
|
|
key={item.label}
|
|
to={item.to}
|
|
className="relative flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors duration-fast hover:text-foreground"
|
|
activeProps={{
|
|
className: 'relative flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-semibold text-foreground',
|
|
}}
|
|
>
|
|
{({ isActive }) => (
|
|
<>
|
|
{item.label}
|
|
{item.badgeKey && (
|
|
<NavBadge count={badgeCounts[item.badgeKey]} />
|
|
)}
|
|
{isActive && (
|
|
<span className="absolute -bottom-[13px] left-1/2 h-0.5 w-4 -translate-x-1/2 rounded-full bg-primary" />
|
|
)}
|
|
</>
|
|
)}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Right: Cmd+K, Theme toggle, Health, Workspace */}
|
|
<div className="flex items-center gap-2">
|
|
{onOpenCommandPalette && (
|
|
<button
|
|
onClick={onOpenCommandPalette}
|
|
className="flex items-center gap-1.5 rounded-md border border-border/60 bg-background/80 px-2.5 py-1 text-xs text-muted-foreground backdrop-blur-sm transition-all duration-fast hover:border-border hover:bg-accent/80 hover:text-foreground"
|
|
>
|
|
<Search className="h-3 w-3" />
|
|
<span className="hidden sm:inline">Search</span>
|
|
<kbd className="ml-1 hidden rounded border bg-background px-1 py-0.5 text-[10px] font-mono sm:inline">
|
|
⌘K
|
|
</kbd>
|
|
</button>
|
|
)}
|
|
<div className="mx-1 h-4 w-px bg-border/50" />
|
|
<ThemeToggle />
|
|
<HealthDot connectionState={connectionState} />
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Page content — no max-width here, pages control their own */}
|
|
<main className="flex-1 min-h-0 w-full overflow-auto px-6 py-6">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|