spb/autoscholar
Public
1"use client";2import Link from "next/link";3import { usePathname } from "next/navigation";4import { type ReactNode, useEffect, useRef, useState } from "react";5import { IconArrow, IconClose, IconMenu } from "./Icons";67/** Client shell for the header: scroll shadow, current-page marking and the accessible mobile drawer. */8export function HeaderChrome({9 links,10 children,11}: {12 links: Array<{ href: string; label: string }>;13 children: ReactNode;14}) {15 const [scrolled, setScrolled] = useState(false);16 const [open, setOpen] = useState(false);17 const pathname = usePathname();18 const panel = useRef<HTMLDivElement>(null);19 const opener = useRef<HTMLButtonElement>(null);2021 useEffect(() => {22 const onScroll = () => setScrolled(window.scrollY > 4);23 onScroll();24 window.addEventListener("scroll", onScroll, { passive: true });25 return () => window.removeEventListener("scroll", onScroll);26 }, []);27 // biome-ignore lint/correctness/useExhaustiveDependencies: close the drawer on navigation28 useEffect(() => {29 setOpen(false);30 }, [pathname]);31 useEffect(() => {32 if (!open) return;33 const prev = document.body.style.overflow;34 document.body.style.overflow = "hidden";35 const first = panel.current?.querySelector<HTMLElement>("a,button");36 first?.focus();37 const onKey = (e: KeyboardEvent) => {38 if (e.key === "Escape") setOpen(false);39 if (e.key === "Tab" && panel.current) {40 const f = [41 ...panel.current.querySelectorAll<HTMLElement>("a,button,input,[tabindex]:not([tabindex='-1'])"),42 ];43 if (f.length === 0) return;44 const a = f[0]!;45 const b = f[f.length - 1]!;46 if (e.shiftKey && document.activeElement === a) {47 e.preventDefault();48 b.focus();49 } else if (!e.shiftKey && document.activeElement === b) {50 e.preventDefault();51 a.focus();52 }53 }54 };55 document.addEventListener("keydown", onKey);56 return () => {57 document.body.style.overflow = prev;58 document.removeEventListener("keydown", onKey);59 opener.current?.focus();60 };61 }, [open]);6263 return (64 <header className="site-header" data-scrolled={scrolled}>65 <div className="relative">66 {children}67 <button68 ref={opener}69 type="button"70 className="btn-icon absolute right-[max(1rem,env(safe-area-inset-right))] top-1/2 -translate-y-1/2 lg:hidden"71 style={{ right: "calc(max(1rem, env(safe-area-inset-right)) + 5.5rem)" }}72 aria-label={open ? "Close menu" : "Open menu"}73 aria-expanded={open}74 aria-controls="mobile-nav"75 onClick={() => setOpen((v) => !v)}76 >77 {open ? <IconClose /> : <IconMenu />}78 </button>79 </div>80 {open && (81 <>82 <button83 type="button"84 className="drawer-backdrop lg:hidden"85 aria-label="Close menu"86 onClick={() => setOpen(false)}87 />88 <div89 ref={panel}90 id="mobile-nav"91 role="dialog"92 aria-modal="true"93 aria-label="Site navigation"94 className="drawer-panel lg:hidden"95 >96 <div className="flex items-center justify-between">97 <span className="eyebrow">Navigate</span>98 <button99 type="button"100 className="btn-icon"101 aria-label="Close menu"102 onClick={() => setOpen(false)}103 >104 <IconClose />105 </button>106 </div>107 <form action="/research" method="get" role="search" className="mt-4">108 <label htmlFor="drawer-q" className="sr-only">109 Search the library110 </label>111 <input112 id="drawer-q"113 name="q"114 type="search"115 placeholder="Search papers, keywords, IDs"116 className="input"117 />118 </form>119 <nav aria-label="Primary mobile" className="mt-4 flex flex-col">120 {links.map((l) => (121 <Link122 key={l.href}123 href={l.href}124 aria-current={pathname === l.href ? "page" : undefined}125 className="flex items-center justify-between border-b py-3.5 text-base no-underline hairline"126 >127 <span>{l.label}</span>128 <IconArrow size={16} className="subtle" />129 </Link>130 ))}131 </nav>132 <p className="subtle mt-6 text-xs leading-relaxed">133 AI-generated working papers; not formally peer reviewed. Research computed on MacLustr.134 </p>135 </div>136 </>137 )}138 </header>139 );140}141