SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
3.8 KB · 76 lines tsx
Raw Blame History
1'use client';2import { Activity, BarChart3, Globe2, MoreHorizontal, Search } from 'lucide-react';3import Link from 'next/link';4import { usePathname } from 'next/navigation';5import { useEffect, useState } from 'react';6import { cn } from '@/lib/cn';7import { moreNav, primaryNav, routes } from '@/lib/site';8import { useOpenSearch } from './search-context';910/**11 * Native-feeling bottom tab bar (< md): Explore · Search · Events · Stats · More. Safe-area aware; the body reserves12 * `pb-[calc(58px+env(safe-area-inset-bottom))]` so it never covers the footer. Hidden on /explore (full-screen globe has its own controls).13 */14export function MobileTabBar() {15  const pathname = usePathname();16  const openSearch = useOpenSearch();17  const [more, setMore] = useState(false);18  useEffect(() => setMore(false), [pathname]);19  const tabs = [20    { href: routes.explore(), label: 'Explore', Icon: Globe2, match: (p: string) => p === '/' || p.startsWith('/explore') || p.startsWith('/satellite') },21    { href: routes.events(), label: 'Events', Icon: Activity, match: (p: string) => p.startsWith('/events') },22    { href: routes.stats(), label: 'Stats', Icon: BarChart3, match: (p: string) => p.startsWith('/stats') || p.startsWith('/rankings') },23  ];24  return (25    <>26      {more && (27        <div className="fixed inset-0 z-[45] bg-black/50 md:hidden" onClick={() => setMore(false)}>28          <div className="panel absolute inset-x-2 bottom-[calc(var(--tabbar-h)+env(safe-area-inset-bottom,0px)+8px)] max-h-[70vh] overflow-y-auto p-3" onClick={(e) => e.stopPropagation()}>29            <p className="eyebrow px-2 pb-2">Browse</p>30            <ul className="grid grid-cols-2 gap-1">31              {[...primaryNav, ...moreNav].map((n) => (32                <li key={n.href}>33                  <Link href={n.href} className="block rounded-md px-3 py-3 text-[15px] text-ink-2 hover:bg-plane-2 hover:text-ink">{n.label}</Link>34                </li>35              ))}36            </ul>37          </div>38        </div>39      )}40      <nav aria-label="Primary (mobile)" className="safe-bottom fixed inset-x-0 bottom-0 z-[46] border-t border-rule bg-plane/95 backdrop-blur-md md:hidden">41        <ul className="grid h-[58px] grid-cols-5">42          {tabs.slice(0, 1).map(({ href, label, Icon, match }) => (43            <TabLink key={href} href={href} label={label} Icon={Icon} active={match(pathname)} />44          ))}45          <li className="min-w-0">46            <button type="button" onClick={openSearch} className="flex h-full w-full flex-col items-center justify-center gap-0.5 text-2xs text-ink-2">47              <Search size={21} aria-hidden strokeWidth={1.75} />48              <span className="truncate">Search</span>49            </button>50          </li>51          {tabs.slice(1).map(({ href, label, Icon, match }) => (52            <TabLink key={href} href={href} label={label} Icon={Icon} active={match(pathname)} />53          ))}54          <li className="min-w-0">55            <button type="button" onClick={() => setMore((m) => !m)} aria-expanded={more} className={cn('flex h-full w-full flex-col items-center justify-center gap-0.5 text-2xs', more ? 'text-accent' : 'text-ink-2')}>56              <MoreHorizontal size={21} aria-hidden strokeWidth={1.75} />57              <span className="truncate">More</span>58            </button>59          </li>60        </ul>61      </nav>62    </>63  );64}6566function TabLink({ href, label, Icon, active }: { href: string; label: string; Icon: typeof Globe2; active: boolean }) {67  return (68    <li className="min-w-0">69      <Link href={href} aria-current={active ? 'page' : undefined} className={cn('flex h-full flex-col items-center justify-center gap-0.5 text-2xs', active ? 'text-accent' : 'text-ink-2')}>70        <Icon size={21} aria-hidden strokeWidth={active ? 2.25 : 1.75} />71        <span className="truncate">{label}</span>72      </Link>73    </li>74  );75}76