/* global React, ReactDOM */ const { useState, useEffect, useRef, useMemo, useCallback } = React; /* ============== Tweaks defaults ============== */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accent": "#3b82f6", "bgMode": "shapes", "intensity": 1, "theme": "light", "fontPair": "modern" }/*EDITMODE-END*/; const FONT_PAIRS = { modern: { display: "'Instrument Serif', serif", sans: "'Inter', sans-serif", label: "Instrument × Inter" }, technical: { display: "'Space Grotesk', sans-serif", sans: "'Inter', sans-serif", label: "Space Grotesk × Inter" }, editorial: { display: "'Fraunces', serif", sans: "'DM Sans', sans-serif", label: "Fraunces × DM Sans" }, geometric: { display: "'Syne', sans-serif", sans: "'Manrope', sans-serif", label: "Syne × Manrope" }, }; const ACCENTS = ["#3b82f6", "#7c5cff", "#00b894", "#ff6b35", "#0f172a"]; /* ============== Router ============== */ function useRoute() { const [route, setRoute] = useState(() => (location.hash.replace("#/", "") || "home")); useEffect(() => { const onHash = () => setRoute(location.hash.replace("#/", "") || "home"); window.addEventListener("hashchange", onHash); return () => window.removeEventListener("hashchange", onHash); }, []); const navigate = useCallback((to) => { if (to === route) return; // refined curtain — single smooth panel, soft easing, accent-tinted edge const curtain = document.createElement("div"); curtain.className = "curtain in"; for (let i = 0; i < 6; i++) curtain.appendChild(document.createElement("span")); document.body.appendChild(curtain); setTimeout(() => { location.hash = "#/" + to; window.scrollTo({ top: 0, behavior: "instant" }); curtain.classList.remove("in"); curtain.classList.add("out"); setTimeout(() => curtain.remove(), 900); }, 760); }, [route]); return [route, navigate]; } /* ============== Reveal hook ============== */ function useReveal() { useEffect(() => { const els = document.querySelectorAll(".reveal, .reveal-stagger"); // start hidden, then reveal next frame for transition els.forEach(el => el.classList.add("pre")); requestAnimationFrame(() => { requestAnimationFrame(() => { els.forEach(el => { const r = el.getBoundingClientRect(); if (r.top < window.innerHeight * 1.0) el.classList.remove("pre"); }); }); }); let io; try { io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) e.target.classList.remove("pre"); }); }, { threshold: 0.12, rootMargin: "0px 0px -10% 0px" }); els.forEach(el => io.observe(el)); } catch (_) {} // safety fallback — make sure nothing stays hidden after 1.2s const safety = setTimeout(() => { document.querySelectorAll(".reveal.pre, .reveal-stagger.pre").forEach(el => el.classList.remove("pre")); }, 1200); return () => { if (io) io.disconnect(); clearTimeout(safety); }; }); } /* ============== Cursor glow ============== */ function CursorGlow() { const ref = useRef(null); useEffect(() => { const el = ref.current; let raf; let tx = 0, ty = 0, x = 0, y = 0; const onMove = (e) => { tx = e.clientX; ty = e.clientY; }; window.addEventListener("mousemove", onMove); const tick = () => { x += (tx - x) * 0.12; y += (ty - y) * 0.12; if (el) el.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%)`; raf = requestAnimationFrame(tick); }; tick(); return () => { window.removeEventListener("mousemove", onMove); cancelAnimationFrame(raf); }; }, []); return
; } /* ============== Nav ============== */ function Nav({ route, navigate }) { const [scrolled, setScrolled] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 8); window.addEventListener("scroll", onScroll); return () => window.removeEventListener("scroll", onScroll); }, []); const items = [ { id: "home", label: "Home" }, { id: "demo", label: "Live Demo" }, { id: "about", label: "About" }, { id: "stories", label: "Stories" }, { id: "contact", label: "Contact" }, ]; return ( ); } /* ============== Footer ============== */ function Footer({ navigate }) { return ( ); } window.NavFooter = { Nav, Footer, CursorGlow, useReveal, useRoute, FONT_PAIRS, ACCENTS, TWEAK_DEFAULTS };