All checks were successful
Deploy to Production / deploy (push) Successful in 52s
Top header on mobile was cramped: Logo + 5 icon-only nav buttons + avatar crammed into a 48px-tall row. Felt like a desktop nav shrunk down. Pivot to native-mobile-app pattern: - Top mobile: just Logo (left) + UserMenu avatar (right). Desktop top nav is `hidden sm:flex` so it disappears on phones. - Bottom: full tab bar replacing the single-button MobileActionBar. Five destinations: Overview · Servers · Create (FAB-style center) · Market · Settings. - "Create" is a raised FAB-style button (round accent fill, -mt-3 to overlap the bar border) — same prominent-action pattern as Instagram / Notion mobile. - Active tab gets accent color + aria-current=page. - Audit demoted from primary nav on mobile (low frequency); still reachable via direct /audit URL. Desktop unchanged — top nav stays. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
'use client';
|
|
|
|
import { cn } from '@/lib/cn';
|
|
import { LayoutGrid, Package, PlusCircle, Server, Settings } from 'lucide-react';
|
|
import Link from 'next/link';
|
|
import { usePathname } from 'next/navigation';
|
|
|
|
interface Tab {
|
|
href: string;
|
|
label: string;
|
|
icon: React.ComponentType<{ size?: number }>;
|
|
prominent?: boolean;
|
|
matchExact?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Mobile bottom navigation — replaces the top nav on small screens with a
|
|
* native-app-style tab bar. Five destinations, "Create" as a prominent
|
|
* FAB-style centre button. The top header drops to just Logo + UserMenu on
|
|
* mobile (see (dashboard)/layout.tsx) so there's no duplicated nav.
|
|
*
|
|
* Desktop is unchanged: top nav for destinations, "+ New server" pill in
|
|
* the header. The whole component is `sm:hidden`.
|
|
*
|
|
* Audit is intentionally demoted on mobile (low-frequency); still reachable
|
|
* via the direct /audit URL or via the Account section.
|
|
*/
|
|
const TABS: Tab[] = [
|
|
{ href: '/dashboard', label: 'Overview', icon: LayoutGrid, matchExact: true },
|
|
{ href: '/servers', label: 'Servers', icon: Server },
|
|
{ href: '/servers/new', label: 'Create', icon: PlusCircle, prominent: true, matchExact: true },
|
|
{ href: '/templates', label: 'Market', icon: Package },
|
|
{ href: '/settings', label: 'Settings', icon: Settings },
|
|
];
|
|
|
|
function useIsActive(pathname: string) {
|
|
return (tab: Tab): boolean => {
|
|
if (tab.matchExact) return pathname === tab.href;
|
|
// /servers matches /servers and /servers/<id> but NOT /servers/new
|
|
// (that's the dedicated Create tab).
|
|
if (tab.href === '/servers') {
|
|
return (
|
|
pathname === '/servers' ||
|
|
(pathname.startsWith('/servers/') && pathname !== '/servers/new')
|
|
);
|
|
}
|
|
return pathname.startsWith(tab.href);
|
|
};
|
|
}
|
|
|
|
export function MobileActionBar() {
|
|
const pathname = usePathname();
|
|
const isActive = useIsActive(pathname);
|
|
|
|
return (
|
|
<nav
|
|
aria-label="Primary"
|
|
className="fixed inset-x-0 bottom-0 z-40 flex border-t backdrop-blur-md sm:hidden"
|
|
style={{
|
|
backgroundColor: 'color-mix(in oklab, var(--color-bg-elevated) 90%, transparent)',
|
|
borderColor: 'var(--color-border)',
|
|
paddingBottom: 'env(safe-area-inset-bottom)',
|
|
}}
|
|
>
|
|
{TABS.map((tab) => {
|
|
const Icon = tab.icon;
|
|
const active = isActive(tab);
|
|
|
|
if (tab.prominent) {
|
|
// FAB-style centre button — raised above the bar, accent fill.
|
|
// The negative top margin (-mt-3) lifts it so it overlaps the
|
|
// border-top of the bar, mimicking iOS/Material tab-bar FABs.
|
|
return (
|
|
<Link
|
|
key={tab.href}
|
|
href={tab.href}
|
|
aria-label={tab.label}
|
|
className="relative flex flex-1 items-center justify-center"
|
|
>
|
|
<span
|
|
className="-mt-3 flex size-12 items-center justify-center rounded-full text-white shadow-lg shadow-black/40 transition-transform active:scale-95"
|
|
style={{ backgroundColor: 'var(--color-accent)' }}
|
|
>
|
|
<Icon size={22} />
|
|
</span>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Link
|
|
key={tab.href}
|
|
href={tab.href}
|
|
aria-current={active ? 'page' : undefined}
|
|
className={cn(
|
|
'flex flex-1 flex-col items-center justify-center gap-0.5 py-2 text-[10.5px] transition-colors',
|
|
active ? 'text-[--color-accent]' : 'text-[--color-fg-muted]',
|
|
)}
|
|
style={active ? { color: 'var(--color-accent)' } : undefined}
|
|
>
|
|
<Icon size={18} />
|
|
<span className="font-medium">{tab.label}</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
);
|
|
}
|