chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* Thin brand-colored progress bar at the top of the page.
|
||||
* Animates to 100% on route change, then fades out.
|
||||
* Pure CSS animation — no external dependency.
|
||||
*/
|
||||
export function NavProgressBar() {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [width, setWidth] = useState(0);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const prevPathRef = useRef<string | null>(null);
|
||||
|
||||
// Detect link clicks to start the bar early
|
||||
useEffect(() => {
|
||||
function handleClick(e: MouseEvent) {
|
||||
const target = (e.target as Element).closest("a");
|
||||
if (!target) return;
|
||||
const href = target.getAttribute("href");
|
||||
if (!href || href.startsWith("http") || href.startsWith("#") || href.startsWith("mailto")) return;
|
||||
// Internal navigation — start bar
|
||||
setVisible(true);
|
||||
setWidth(60); // jump to 60% immediately, await route change for completion
|
||||
}
|
||||
document.addEventListener("click", handleClick);
|
||||
return () => document.removeEventListener("click", handleClick);
|
||||
}, []);
|
||||
|
||||
// Complete bar when route actually changes
|
||||
useEffect(() => {
|
||||
const current = pathname + searchParams.toString();
|
||||
if (prevPathRef.current !== null && prevPathRef.current !== current) {
|
||||
// Route changed — complete the bar
|
||||
setWidth(100);
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => {
|
||||
setVisible(false);
|
||||
setWidth(0);
|
||||
}, 350);
|
||||
}
|
||||
prevPathRef.current = current;
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
// Cleanup
|
||||
useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
|
||||
|
||||
if (!visible && width === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="fixed top-0 left-0 right-0 z-[9999] h-0.5 pointer-events-none"
|
||||
>
|
||||
<div
|
||||
className="h-full bg-brand-500 transition-all ease-out"
|
||||
style={{
|
||||
width: `${width}%`,
|
||||
transitionDuration: width === 100 ? "200ms" : "400ms",
|
||||
opacity: visible ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user