// ═══════════════════════════════════════════════════════════════
// 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 (
<>
Start a project
{/* Mobile drawer */}
{open && (
)}
>
);
}
// ───────────── 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 (
<>