cockpit-mobile-nav.tsx83 lines · main
1'use client';
2
3import Link from 'next/link';
4import { 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 */
14type 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.
20const 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
47export 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}