buildmymcpserver/apps/web/components/mobile-action-bar.tsx
Marco Sadjadi f80bd8afbe
All checks were successful
Deploy to Production / deploy (push) Successful in 52s
feat(web): app-like mobile dashboard — bottom tab bar, minimal top
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>
2026-05-25 23:15:44 +02:00

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