Files
template-developer-tools/src/components/ScrollHeader.tsx
T

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>
);
}