SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
4.7 KB · 110 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import Link from "next/link";4import { usePathname } from "next/navigation";5import { ChevronRight, ExternalLink, Menu, X } from "lucide-react";6import { Button } from "@/components/ui/button";7import { cn } from "@/lib/utils";8import { DOCS_NAV, type DocsNavLink } from "./nav-config";910function NavLinks({ onNavigate, className }: { onNavigate?: () => void; className?: string }) {11  const pathname = usePathname();12  return (13    <nav aria-label="Documentation" className={cn("text-[13.5px]", className)}>14      {DOCS_NAV.map((section) => (15        <div key={section.title} className="mb-6 last:mb-0">16          <div className="mb-1.5 px-2 text-[11.5px] font-semibold uppercase tracking-wide text-fg-subtle">{section.title}</div>17          <ul className="space-y-px">18            {section.links.map((l) => (19              <li key={l.href}>20                <NavItem link={l} active={pathname === l.href} onNavigate={onNavigate} />21              </li>22            ))}23          </ul>24        </div>25      ))}26    </nav>27  );28}2930function NavItem({ link, active, onNavigate }: { link: DocsNavLink; active: boolean; onNavigate?: () => void }) {31  return (32    <Link33      href={link.href}34      onClick={onNavigate}35      aria-current={active ? "page" : undefined}36      className={cn(37        "flex items-center justify-between gap-2 rounded-md px-2 py-1.5 transition-colors",38        active ? "bg-accent-soft font-medium text-accent" : "text-fg-muted hover:bg-bg-muted hover:text-fg",39      )}40    >41      <span className="truncate">{link.title}</span>42      {link.badge ? <span className="shrink-0 rounded-full bg-warning-soft px-1.5 py-px text-[10.5px] font-medium text-warning">{link.badge}</span> : null}43      {link.external ? <ExternalLink className="size-3.5 shrink-0 text-fg-subtle" aria-hidden /> : null}44    </Link>45  );46}4748/** Desktop sidebar: sticky under the marketing header. */49export function DocsSidebar({ className }: { className?: string }) {50  return (51    <aside className={cn("sticky top-14 hidden max-h-[calc(100dvh-3.5rem)] overflow-y-auto py-8 pr-4 scrollbar-thin lg:block", className)}>52      <NavLinks />53    </aside>54  );55}5657/** Mobile: compact bar with the current page and a drawer with the full navigation. */58export function DocsMobileNav() {59  const [open, setOpen] = React.useState(false);60  const pathname = usePathname();61  const current = React.useMemo(() => DOCS_NAV.flatMap((s) => s.links.map((l) => ({ ...l, section: s.title }))).find((l) => l.href === pathname), [pathname]);6263  React.useEffect(() => {64    if (!open) return;65    const onKey = (e: KeyboardEvent) => {66      if (e.key === "Escape") setOpen(false);67    };68    document.addEventListener("keydown", onKey);69    const prev = document.body.style.overflow;70    document.body.style.overflow = "hidden";71    return () => {72      document.removeEventListener("keydown", onKey);73      document.body.style.overflow = prev;74    };75  }, [open]);7677  return (78    <div className="lg:hidden">79      <div className="-mx-5 flex items-center gap-2 border-b border-border bg-bg-subtle/60 px-5 py-2.5 text-[13px] sm:-mx-8 sm:px-8">80        <Button variant="outline" size="sm" onClick={() => setOpen(true)} aria-expanded={open} aria-controls="docs-drawer" className="gap-1.5">81          <Menu className="size-4" aria-hidden />82          Menu83        </Button>84        <span className="flex min-w-0 items-center gap-1 text-fg-muted">85          <span className="truncate">{current?.section ?? "Docs"}</span>86          <ChevronRight className="size-3.5 shrink-0 text-fg-subtle" aria-hidden />87          <span className="truncate font-medium text-fg">{current?.title ?? "Documentation"}</span>88        </span>89      </div>9091      {open ? (92        <div className="fixed inset-0 z-50" role="dialog" aria-modal="true" aria-label="Documentation navigation" id="docs-drawer">93          <button type="button" className="absolute inset-0 bg-fg/30 backdrop-blur-[2px] animate-fade-in" aria-label="Close navigation" onClick={() => setOpen(false)} />94          <div className="absolute inset-y-0 left-0 flex w-[min(20rem,85vw)] flex-col border-r border-border bg-bg shadow-lg animate-fade-in">95            <div className="flex h-14 items-center justify-between border-b border-border px-4">96              <span className="text-[13.5px] font-semibold text-fg">Documentation</span>97              <Button variant="ghost" size="icon-sm" onClick={() => setOpen(false)} aria-label="Close navigation">98                <X />99              </Button>100            </div>101            <div className="flex-1 overflow-y-auto px-3 py-4 scrollbar-thin">102              <NavLinks onNavigate={() => setOpen(false)} />103            </div>104          </div>105        </div>106      ) : null}107    </div>108  );109}110