103 lines
2.8 KiB
TypeScript
103 lines
2.8 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
import menuSvg from "@/assets/menu.min.svg";
|
|
|
|
const NAV_ITEMS = [
|
|
{ label: "Browse", path: "/browse" },
|
|
{ label: "Compose", path: "/" },
|
|
{ label: "Settings", path: "/settings" },
|
|
] as const;
|
|
|
|
/**
|
|
* Smooth scroll-following: tracks the scroll container and lerps
|
|
* the menu's vertical offset so it glides into place with eased acceleration.
|
|
*/
|
|
function useSmoothScroll(scrollSelector: string, ease = 0.08) {
|
|
const [offset, setOffset] = useState(0);
|
|
const targetRef = useRef(0);
|
|
const currentRef = useRef(0);
|
|
const rafRef = useRef(0);
|
|
|
|
useEffect(() => {
|
|
const container = document.querySelector(scrollSelector);
|
|
if (!container) return;
|
|
|
|
const onScroll = () => {
|
|
targetRef.current = container.scrollTop;
|
|
};
|
|
container.addEventListener("scroll", onScroll, { passive: true });
|
|
|
|
const tick = () => {
|
|
const diff = targetRef.current - currentRef.current;
|
|
if (Math.abs(diff) < 0.5) {
|
|
currentRef.current = targetRef.current;
|
|
} else {
|
|
currentRef.current += diff * ease;
|
|
}
|
|
setOffset(currentRef.current);
|
|
rafRef.current = requestAnimationFrame(tick);
|
|
};
|
|
rafRef.current = requestAnimationFrame(tick);
|
|
|
|
return () => {
|
|
container.removeEventListener("scroll", onScroll);
|
|
cancelAnimationFrame(rafRef.current);
|
|
};
|
|
}, [scrollSelector, ease]);
|
|
|
|
return offset;
|
|
}
|
|
|
|
interface NavMenuProps {
|
|
theme: "terra" | "azure";
|
|
onToggleTheme: () => void;
|
|
}
|
|
|
|
export default function NavMenu({ theme, onToggleTheme }: NavMenuProps) {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const scrollY = useSmoothScroll("[data-scroll-root]", 0.07);
|
|
|
|
return (
|
|
<div
|
|
className="nav-menu-root"
|
|
style={{ transform: `translateY(${scrollY}px)` }}
|
|
>
|
|
{/* Frame — menu.min.svg as mask, colored by theme */}
|
|
<div
|
|
className="nav-menu-frame"
|
|
style={{
|
|
WebkitMaskImage: `url(${menuSvg})`,
|
|
maskImage: `url(${menuSvg})`,
|
|
}}
|
|
/>
|
|
|
|
{/* Navigation links */}
|
|
<nav className="nav-menu-links">
|
|
{NAV_ITEMS.map(({ label, path }) => {
|
|
const active = location.pathname === path;
|
|
return (
|
|
<button
|
|
key={path}
|
|
onClick={() => navigate(path)}
|
|
className="nav-menu-item"
|
|
data-active={active || undefined}
|
|
>
|
|
{label}
|
|
</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{/* Theme toggle — bottom container */}
|
|
<button
|
|
onClick={onToggleTheme}
|
|
className="nav-menu-theme-toggle"
|
|
title={`Switch to ${theme === "terra" ? "Azure" : "Terracotta"} theme`}
|
|
>
|
|
{theme === "terra" ? "◐ AZURE" : "◑ TERRA"}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|