SPB Git

spb/ai-atlas

Public
6.5 KB · 140 lines tsx
1'use client';2import { ChevronDown, Menu, Search, X } from 'lucide-react';3import Link from 'next/link';4import { usePathname } from 'next/navigation';5import { useEffect, useRef, useState } from 'react';6import { Wordmark } from '@/components/brand/logo';7import { cn } from '@/lib/cn';8import { exploreNav, moreNav, primaryNav, routes } from '@/lib/site';9import { useOpenSearch } from './search-context';10import { ThemeToggle } from './theme';1112const EXPLORE_PREFIXES = exploreNav.map((n) => n.href);1314export function SiteHeader() {15  const pathname = usePathname();16  const openSearch = useOpenSearch();17  const [menu, setMenu] = useState(false);18  const [explore, setExplore] = useState(false);19  const exploreRef = useRef<HTMLDivElement>(null);20  useEffect(() => {21    setMenu(false);22    setExplore(false);23  }, [pathname]);24  useEffect(() => {25    if (!explore) return;26    const onDoc = (e: MouseEvent) => {27      if (!exploreRef.current?.contains(e.target as Node)) setExplore(false);28    };29    const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setExplore(false);30    document.addEventListener('mousedown', onDoc);31    document.addEventListener('keydown', onKey);32    return () => {33      document.removeEventListener('mousedown', onDoc);34      document.removeEventListener('keydown', onKey);35    };36  }, [explore]);3738  const exploreActive = pathname === '/explore' || EXPLORE_PREFIXES.some((p) => pathname === p || pathname.startsWith(p + '/'));39  const navCls = (active: boolean) => cn('flex h-9 items-center gap-1 rounded-sm px-2.5 text-[13.5px] transition-colors', active ? 'text-ink font-medium' : 'text-ink-2 hover:text-ink');4041  return (42    <header className="sticky top-0 z-40 border-b border-rule bg-canvas/85 backdrop-blur-md">43      <div className="container-x mx-auto flex h-[var(--header-h)] max-w-[1600px] items-center gap-3">44        <Link href={routes.home()} className="flex h-11 items-center" aria-label="AI Atlas home">45          <Wordmark />46        </Link>47        <nav aria-label="Primary" className="ml-3 hidden items-center gap-0.5 lg:flex">48          <div className="relative" ref={exploreRef}>49            <button type="button" onClick={() => setExplore((v) => !v)} aria-expanded={explore} aria-haspopup="menu" className={navCls(exploreActive)}>50              Explore <ChevronDown className={cn('size-3.5 transition-transform', explore && 'rotate-180')} aria-hidden />51            </button>52            {explore && (53              <div role="menu" className="panel absolute left-0 top-full mt-1 w-[520px] p-2 shadow-lg">54                <ul className="grid grid-cols-2 gap-0.5">55                  {exploreNav.map((n) => (56                    <li key={n.href}>57                      <Link role="menuitem" href={n.href} className={cn('block rounded-sm px-3 py-2 text-sm hover:bg-surface-2', pathname.startsWith(n.href) ? 'text-ink font-medium' : 'text-ink-2 hover:text-ink')}>58                        {n.label}59                      </Link>60                    </li>61                  ))}62                  <li className="col-span-2 mt-1 border-t border-rule pt-1">63                    <Link role="menuitem" href={routes.explore()} className="block rounded-sm px-3 py-2 text-sm text-accent hover:bg-surface-2">64                      All entity types with live counts →65                    </Link>66                  </li>67                </ul>68              </div>69            )}70          </div>71          {primaryNav.map((n) => {72            const active = pathname === n.href || pathname.startsWith(n.href + '/');73            return (74              <Link key={n.href} href={n.href} aria-current={active ? 'page' : undefined} className={navCls(active)}>75                {n.label}76              </Link>77            );78          })}79        </nav>80        <div className="ml-auto flex items-center gap-1">81          <button type="button" onClick={openSearch} className="hidden h-9 min-w-[280px] items-center gap-2 rounded-sm border border-rule bg-surface px-3 text-sm text-ink-3 hover:border-rule-strong hover:text-ink-2 md:flex" aria-label="Open search">82            <Search className="size-4" aria-hidden />83            <span className="flex-1 text-left">Search the AI ecosystem…</span>84            <kbd className="mono rounded-[3px] border border-rule px-1.5 py-0.5 text-[10px]">⌘K</kbd>85          </button>86          <button type="button" onClick={openSearch} className="flex size-11 items-center justify-center rounded-sm text-ink-2 hover:bg-surface-2 hover:text-ink md:hidden" aria-label="Open search">87            <Search className="size-5" aria-hidden />88          </button>89          <ThemeToggle />90          <button type="button" onClick={() => setMenu((m) => !m)} className="flex size-11 items-center justify-center rounded-sm text-ink-2 hover:bg-surface-2 hover:text-ink lg:hidden" aria-label={menu ? 'Close menu' : 'Open menu'} aria-expanded={menu}>91            {menu ? <X className="size-5" aria-hidden /> : <Menu className="size-5" aria-hidden />}92          </button>93        </div>94      </div>95      {menu && (96        <div className="border-t border-rule bg-canvas lg:hidden">97          <div className="container-x mx-auto grid max-w-[1600px] gap-6 py-5 sm:grid-cols-3">98            <div>99              <p className="eyebrow mb-2">Explore</p>100              <ul className="grid grid-cols-2 gap-0.5 sm:grid-cols-1">101                {exploreNav.map((n) => (102                  <li key={n.href}>103                    <Link href={n.href} className="block py-2.5 text-[15px] text-ink-2 hover:text-ink">104                      {n.label}105                    </Link>106                  </li>107                ))}108              </ul>109            </div>110            <div>111              <p className="eyebrow mb-2">Track</p>112              <ul className="grid grid-cols-2 gap-0.5 sm:grid-cols-1">113                {primaryNav.map((n) => (114                  <li key={n.href}>115                    <Link href={n.href} className="block py-2.5 text-[15px] text-ink-2 hover:text-ink">116                      {n.label}117                    </Link>118                  </li>119                ))}120              </ul>121            </div>122            <div>123              <p className="eyebrow mb-2">About</p>124              <ul className="grid grid-cols-2 gap-0.5 sm:grid-cols-1">125                {moreNav.map((n) => (126                  <li key={n.href}>127                    <Link href={n.href} className="block py-2.5 text-[15px] text-ink-2 hover:text-ink">128                      {n.label}129                    </Link>130                  </li>131                ))}132              </ul>133            </div>134          </div>135        </div>136      )}137    </header>138  );139}140
Files · 9f1db171c786cf4a3b99436329f29b145c02aeec
Menu

Theme