Files
Codewalkers/apps/web/src/layouts/AppLayout.tsx

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>
)
}