cockpit-mobile-nav.tsx83 lines · main
| 1 | 'use client'; |
| 2 | |
| 3 | import Link from 'next/link'; |
| 4 | import { usePathname } from 'next/navigation'; |
| 5 | |
| 6 | /** |
| 7 | * Mobile-only horizontal nav strip for the cockpit. Mirrors the desktop |
| 8 | * CockpitNav links so sections stay reachable on phones (the sidebar is |
| 9 | * hidden below md). Renders under the cockpit header and disappears at md+ |
| 10 | * where the sidebar takes over — same pattern as the customer dashboard's |
| 11 | * DashboardMobileNav. Labels/hrefs mirror CockpitNav; icons are omitted |
| 12 | * here since the strip is text-only by design. |
| 13 | */ |
| 14 | type NavEntry = |
| 15 | | { kind: 'label'; text: string } |
| 16 | | { kind: 'link'; href: string; label: string; exact?: boolean }; |
| 17 | |
| 18 | // Mirrors CockpitNav's grouping. Group titles render as small inline |
| 19 | // dividers between the links so the strip stays scannable on phones. |
| 20 | const NAV: readonly NavEntry[] = [ |
| 21 | { kind: 'link', href: '/admin', label: 'overview', exact: true }, |
| 22 | { kind: 'label', text: 'money' }, |
| 23 | { kind: 'link', href: '/admin/billing', label: 'subscribers & billing' }, |
| 24 | { kind: 'label', text: 'customers' }, |
| 25 | { kind: 'link', href: '/admin/users', label: 'users' }, |
| 26 | { kind: 'link', href: '/admin/projects', label: 'projects' }, |
| 27 | { kind: 'label', text: 'agents' }, |
| 28 | { kind: 'link', href: '/admin/mcp', label: 'mcp / agent access' }, |
| 29 | { kind: 'link', href: '/admin/agents', label: 'ai agents' }, |
| 30 | { kind: 'label', text: 'platform' }, |
| 31 | { kind: 'link', href: '/admin/health', label: 'platform health' }, |
| 32 | { kind: 'link', href: '/admin/realtime', label: 'realtime' }, |
| 33 | { kind: 'link', href: '/admin/storage', label: 'storage' }, |
| 34 | { kind: 'link', href: '/admin/deploys', label: 'deploys' }, |
| 35 | { kind: 'label', text: 'operations' }, |
| 36 | { kind: 'link', href: '/admin/incidents', label: 'incidents' }, |
| 37 | { kind: 'link', href: '/admin/abuse-reports', label: 'abuse & allowlist' }, |
| 38 | { kind: 'link', href: '/admin/migrations', label: 'migrations' }, |
| 39 | { kind: 'link', href: '/admin/messages', label: 'messages' }, |
| 40 | { kind: 'link', href: '/admin/tickets', label: 'tickets' }, |
| 41 | { kind: 'link', href: '/admin/email-events', label: 'email log' }, |
| 42 | { kind: 'label', text: 'system' }, |
| 43 | { kind: 'link', href: '/admin/launch', label: 'launch controls' }, |
| 44 | { kind: 'link', href: '/admin/settings', label: 'settings' }, |
| 45 | ] as const; |
| 46 | |
| 47 | export function CockpitMobileNav() { |
| 48 | const pathname = usePathname(); |
| 49 | return ( |
| 50 | <nav |
| 51 | aria-label="admin sections" |
| 52 | className="flex shrink-0 items-center gap-1 overflow-x-auto border-b border-[var(--color-border-subtle)] px-4 py-2 md:hidden [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" |
| 53 | > |
| 54 | {NAV.map((item) => { |
| 55 | if (item.kind === 'label') { |
| 56 | return ( |
| 57 | <span |
| 58 | key={`label-${item.text}`} |
| 59 | className="shrink-0 whitespace-nowrap pl-2 pr-1 font-mono text-[10px] uppercase tracking-wider text-[var(--color-text-subtle)]" |
| 60 | > |
| 61 | {item.text} |
| 62 | </span> |
| 63 | ); |
| 64 | } |
| 65 | const active = item.exact ? pathname === item.href : pathname.startsWith(item.href); |
| 66 | return ( |
| 67 | <Link |
| 68 | key={item.href} |
| 69 | href={item.href} |
| 70 | aria-current={active ? 'page' : undefined} |
| 71 | className={`shrink-0 whitespace-nowrap rounded-md px-3 py-1.5 font-mono text-xs transition ${ |
| 72 | active |
| 73 | ? 'bg-[var(--color-surface-raised)] text-[var(--color-text)]' |
| 74 | : 'text-[var(--color-text-muted)] hover:text-[var(--color-text)]' |
| 75 | }`} |
| 76 | > |
| 77 | {item.label} |
| 78 | </Link> |
| 79 | ); |
| 80 | })} |
| 81 | </nav> |
| 82 | ); |
| 83 | } |