// ═══════════════════════════════════════════════════════════════ // SHELL — shared chrome, helpers, persistence across all pages // ═══════════════════════════════════════════════════════════════ const { useEffect, useRef, useState, useMemo } = React; // ───────────── Palettes ───────────── const PALETTES = { aurora: ["#b5fd5b", "#6ee7ff", "#d894ff"], ember: ["#ffb14a", "#ff6a88", "#f24e64"], arctic: ["#a7f3ff", "#7aa6ff", "#c9d1ff"], mono: ["#f4f4f5", "#a1a1aa", "#71717a"], }; // ───────────── Cross-page Tweak persistence ───────────── // useTweaks() comes from tweaks-panel.jsx — wraps it with localStorage so palette + grain // follow the user across pages, while the per-page EDITMODE block still persists official defaults. function useSiteTweaks(defaults) { const stored = (() => { try { return JSON.parse(localStorage.getItem("digitacurve.tweaks") || "{}"); } catch (e) { return {}; } })(); const merged = { ...defaults, ...stored }; const t = useTweaks(merged); const wrapped = useMemo(() => { const orig = t.setTweak; return Object.assign({}, t, { setTweak: (k, v) => { orig(k, v); const next = (typeof k === "object") ? { ...t, ...k } : { ...t, [k]: v }; try { const clean = {}; Object.keys(defaults).forEach(key => { clean[key] = next[key]; }); localStorage.setItem("digitacurve.tweaks", JSON.stringify(clean)); } catch (e) {} } }); }, [t]); return wrapped; } // ───────────── Reveal on scroll ───────────── function useReveal() { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver((ents) => { ents.forEach(e => { if (e.isIntersecting) { el.classList.add("in"); io.unobserve(el); }}); }, { threshold: 0.12 }); io.observe(el); return () => io.disconnect(); }, []); return ref; } function Reveal({ children, delay = 0, as: As = "div", style = {}, className = "" }) { const r = useReveal(); return {children}; } // ───────────── 3D tilt card ───────────── function TiltCard({ children, max = 6, style = {}, className = "", onClick }) { const ref = useRef(null); const onMove = (e) => { const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); const x = (e.clientX - r.left) / r.width; const y = (e.clientY - r.top) / r.height; el.style.transform = `perspective(1100px) rotateX(${(0.5 - y) * max}deg) rotateY(${(x - 0.5) * max}deg) translateZ(0)`; el.style.setProperty("--mx", `${x * 100}%`); el.style.setProperty("--my", `${y * 100}%`); }; const onLeave = () => { const el = ref.current; if (el) el.style.transform = "perspective(1100px) rotateX(0) rotateY(0)"; }; return (
{children}
); } // ───────────── Logo ───────────── function LogoMark({ accent }) { return (
); } // ───────────── Cross-page Nav ───────────── const NAV_LINKS = [ { label: "Home", href: "index.html" }, { label: "Work", href: "work.html" }, { label: "Projects", href: "work.html#archive" }, { label: "Services", href: "services.html" }, { label: "Contact", href: "contact.html" }, ]; function isActive(href) { const path = (window.location.pathname.split("/").pop() || "index.html"); return path === href || (href === "index.html" && (path === "" || path === "index.html")); } function Nav({ accent }) { const [scrolled, setScrolled] = useState(false); const [open, setOpen] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 30); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); return ( <>
Digitacurve
{NAV_LINKS.map(l => { const on = isActive(l.href); return ( e.currentTarget.style.color="#fff"} onMouseLeave={e=>e.currentTarget.style.color=on?"#fff":"rgba(255,255,255,0.7)"}> {l.label} {on && } ); })}
Start a project
{/* Mobile drawer */} {open && (
{NAV_LINKS.map(l => ( {l.label} ))}
)} ); } // ───────────── Footer ───────────── function Footer({ accent }) { return ( ); } // ───────────── Custom cursor ───────────── function useCustomCursor(accent) { useEffect(() => { const dot = document.querySelector(".cursor-dot"); if (!dot) return; let raf, tx = 0, ty = 0, x = 0, y = 0; const onMove = (e) => { tx = e.clientX; ty = e.clientY; }; const tick = () => { x += (tx - x) * 0.22; y += (ty - y) * 0.22; dot.style.transform = `translate(${x}px, ${y}px) translate(-50%,-50%)`; raf = requestAnimationFrame(tick); }; window.addEventListener("mousemove", onMove); raf = requestAnimationFrame(tick); const isHover = (el) => el && (el.matches && el.matches("a, button, [data-cursor]") || el.closest && el.closest("a, button, [data-cursor]")); const onOver = (e) => { if (isHover(e.target)) dot.classList.add("hover"); }; const onOut = (e) => { if (isHover(e.target)) dot.classList.remove("hover"); }; document.addEventListener("mouseover", onOver); document.addEventListener("mouseout", onOut); return () => { cancelAnimationFrame(raf); window.removeEventListener("mousemove", onMove); document.removeEventListener("mouseover", onOver); document.removeEventListener("mouseout", onOut); }; }, []); } // ───────────── Site Chrome wrapper (cursor + nav + content + footer + tweaks) ───────────── function SiteChrome({ accent, t, children }) { useCustomCursor(accent); return ( <>