/* ========================================================= App — Fran Ruiz portfolio composer ========================================================= */ const { useEffect, useState } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "light", "lang": "en", "accent": "#e8624a" }/*EDITMODE-END*/; // Override the default language with whatever the user picked on a // previous page — we persist it under "fran-lang" so language follows // the user across index ↔ contact ↔ project navigations. const STORED_LANG = typeof localStorage !== "undefined" && localStorage.getItem("fran-lang"); const RUNTIME_DEFAULTS = STORED_LANG ? { ...TWEAK_DEFAULTS, lang: STORED_LANG } : TWEAK_DEFAULTS; const ACCENT_OPTIONS = [ "#e8624a", // terracotta (default — from Fimed) "#5fa7d4", // sky blue (JP) "#a3b59a", // sage green "#f4ae5b", // warm sand (Garden Brasas) "#9fc9be", // mint (Bodegas) "#1a1a1a", // ink (monochrome) ]; const apply = (t) => { const r = document.documentElement; r.setAttribute("data-theme", t.theme); r.setAttribute("data-lang", t.lang); r.style.setProperty("--accent", t.accent); }; const App = () => { const [tweaks, setTweak] = useTweaks(RUNTIME_DEFAULTS); const lang = tweaks.lang; const t = T[lang] || T.en; useEffect(() => { apply(tweaks); }, [tweaks]); // Scroll reveal (manual fallback) — opts the page into hide-then-reveal animation useEffect(() => { document.documentElement.setAttribute("data-anim", ""); // If the animation timeline isn't advancing (e.g. sandboxed iframe), // disable hide-then-reveal so content stays visible. const t0 = document.timeline?.currentTime ?? 0; setTimeout(() => { const t1 = document.timeline?.currentTime ?? 0; if (t0 === t1) document.documentElement.removeAttribute("data-anim"); }, 200); const check = () => { const els = document.querySelectorAll(".reveal:not(.in), .reveal-stagger:not(.in)"); els.forEach((el) => { const r = el.getBoundingClientRect(); if (r.top < window.innerHeight + 80 && r.bottom > -80) { el.classList.add("in"); } }); }; // Run check more aggressively at start to catch initial in-view content check(); requestAnimationFrame(check); setTimeout(check, 50); setTimeout(check, 200); setTimeout(check, 500); window.addEventListener("scroll", check, { passive: true }); window.addEventListener("resize", check); const id = setInterval(check, 400); // Safety net: after 2.5s, reveal anything still hidden regardless of viewport const safety = setTimeout(() => { document.querySelectorAll(".reveal:not(.in), .reveal-stagger:not(.in)").forEach((el) => el.classList.add("in")); }, 2500); return () => { window.removeEventListener("scroll", check); window.removeEventListener("resize", check); clearInterval(id); clearTimeout(safety); }; }, []); const setLang = (v) => { setTweak("lang", v); try { localStorage.setItem("fran-lang", v); } catch (e) {} }; const toggleTheme = () => setTweak("theme", tweaks.theme === "dark" ? "light" : "dark"); return ( <>