126 lines
5.1 KiB
TypeScript
126 lines
5.1 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// ScrollHeader — pre-built responsive header for single-page scroll sites
|
|
// ---------------------------------------------------------------------------
|
|
// PRE-INSTALLED in every single-page React project. App.tsx imports:
|
|
// import ScrollHeader from "@/components/ScrollHeader"
|
|
// <ScrollHeader brand="Site Name" links={[{href:"#about",label:"About"},{href:"#contact",label:"Contact"}]} />
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
interface NavLink {
|
|
href: string;
|
|
label: string;
|
|
}
|
|
|
|
interface ScrollHeaderProps {
|
|
brand: string;
|
|
links: NavLink[];
|
|
ctaLabel?: string;
|
|
ctaHref?: string;
|
|
}
|
|
|
|
export default function ScrollHeader({ brand, links, ctaLabel, ctaHref }: ScrollHeaderProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const [scrolled, setScrolled] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const onScroll = () => setScrolled(window.scrollY > 40);
|
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
return () => window.removeEventListener("scroll", onScroll);
|
|
}, []);
|
|
|
|
const handleClick = (href: string) => {
|
|
setOpen(false);
|
|
const id = href.replace("#", "");
|
|
const el = document.getElementById(id);
|
|
if (el) {
|
|
el.scrollIntoView({ behavior: "smooth" });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<header
|
|
className={`fixed top-0 inset-x-0 z-50 transition-all duration-300 ${
|
|
scrolled
|
|
? "bg-background/95 backdrop-blur-md border-b border-border shadow-sm"
|
|
: "bg-transparent"
|
|
}`}
|
|
>
|
|
<div className="mx-auto max-w-7xl flex items-center justify-between px-6 h-16 md:h-20">
|
|
{/* Brand */}
|
|
<a
|
|
href="#"
|
|
onClick={(e) => { e.preventDefault(); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
|
className="text-lg font-bold tracking-tight text-foreground hover:text-primary transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded"
|
|
>
|
|
{brand}
|
|
</a>
|
|
|
|
{/* Desktop nav */}
|
|
<nav className="hidden md:flex items-center gap-6">
|
|
{links.map((link) => (
|
|
<a
|
|
key={link.href}
|
|
href={link.href}
|
|
onClick={(e) => { e.preventDefault(); handleClick(link.href); }}
|
|
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors duration-150 py-2 focus-visible:ring-2 focus-visible:ring-primary rounded"
|
|
>
|
|
{link.label}
|
|
</a>
|
|
))}
|
|
{ctaLabel && (
|
|
<a
|
|
href={ctaHref || "#"}
|
|
onClick={(e) => { if (ctaHref?.startsWith("#")) { e.preventDefault(); handleClick(ctaHref); } }}
|
|
className="inline-flex items-center justify-center h-10 px-6 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.98] transition-all duration-150 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
|
|
>
|
|
{ctaLabel}
|
|
</a>
|
|
)}
|
|
</nav>
|
|
|
|
{/* Mobile hamburger */}
|
|
<button
|
|
onClick={() => setOpen(!open)}
|
|
className="md:hidden inline-flex items-center justify-center w-10 h-10 rounded-md text-foreground hover:bg-muted transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-primary"
|
|
aria-label={open ? "Close menu" : "Open menu"}
|
|
>
|
|
{open ? (
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M18 6L6 18M6 6l12 12" /></svg>
|
|
) : (
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Mobile nav overlay */}
|
|
{open && (
|
|
<div className="md:hidden absolute inset-x-0 top-full bg-background/98 backdrop-blur-lg border-b border-border shadow-lg">
|
|
<nav className="flex flex-col px-6 py-4 gap-1">
|
|
{links.map((link) => (
|
|
<a
|
|
key={link.href}
|
|
href={link.href}
|
|
onClick={(e) => { e.preventDefault(); handleClick(link.href); }}
|
|
className="py-3 text-base font-medium text-foreground hover:text-primary transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-primary rounded px-2"
|
|
>
|
|
{link.label}
|
|
</a>
|
|
))}
|
|
{ctaLabel && (
|
|
<a
|
|
href={ctaHref || "#"}
|
|
onClick={(e) => { if (ctaHref?.startsWith("#")) { e.preventDefault(); handleClick(ctaHref); } }}
|
|
className="mt-2 inline-flex items-center justify-center h-12 px-6 text-base font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.98] transition-all duration-150"
|
|
>
|
|
{ctaLabel}
|
|
</a>
|
|
)}
|
|
</nav>
|
|
</div>
|
|
)}
|
|
</header>
|
|
);
|
|
}
|