/* ===========================================================
1 A 1 — App: idioma, Tweaks (acento re-tematiza), reveal
=========================================================== */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"accent": "#4a8fac",
"motion": true,
"cursor": true,
"startLang": "es"
}/*EDITMODE-END*/;
/* Cursor moderno — punto central instantáneo + anillo externo con retraso físico (lerp) */
function CustomCursor() {
const dotRef = React.useRef(null);
const ringRef = React.useRef(null);
React.useEffect(() => {
const mq = window.matchMedia("(hover: hover) and (pointer: fine)");
if (!mq.matches) return;
const root = document.documentElement;
root.classList.add("cursor-custom-active");
const dot = dotRef.current;
const ring = ringRef.current;
const mouse = { x: -100, y: -100 };
const dotPos = { x: -100, y: -100 };
const ringPos = { x: -100, y: -100 };
let raf;
const loop = () => {
// Easing rápido para el punto central (mantiene precisión)
dotPos.x += (mouse.x - dotPos.x) * 0.4;
dotPos.y += (mouse.y - dotPos.y) * 0.4;
// Easing más lento para el anillo (efecto fluidez física)
ringPos.x += (mouse.x - ringPos.x) * 0.16;
ringPos.y += (mouse.y - ringPos.y) * 0.16;
if (dot) {
dot.style.transform = `translate(${dotPos.x}px, ${dotPos.y}px)`;
}
if (ring) {
ring.style.transform = `translate(${ringPos.x}px, ${ringPos.y}px)`;
}
raf = requestAnimationFrame(loop);
};
const onMove = (e) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
dot.classList.add("show");
ring.classList.add("show");
const hot = e.target.closest && e.target.closest("a,button,input,textarea,select,label,.gw-piece,.gfilter,[role=tab],image-slot,.contact-channel,.lang button");
if (hot) {
dot.classList.add("hot");
ring.classList.add("hot");
} else {
dot.classList.remove("hot");
ring.classList.remove("hot");
}
};
const onDown = () => {
dot.classList.add("down");
ring.classList.add("down");
};
const onUp = () => {
dot.classList.remove("down");
ring.classList.remove("down");
};
const onOut = (e) => {
if (!e.relatedTarget) {
dot.classList.remove("show");
ring.classList.remove("show");
}
};
window.addEventListener("mousemove", onMove, { passive: true });
window.addEventListener("mousedown", onDown);
window.addEventListener("mouseup", onUp);
window.addEventListener("mouseout", onOut);
raf = requestAnimationFrame(loop);
return () => {
cancelAnimationFrame(raf);
root.classList.remove("cursor-custom-active");
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mousedown", onDown);
window.removeEventListener("mouseup", onUp);
window.removeEventListener("mouseout", onOut);
};
}, []);
return (
);
}
function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
const [lang, setLang] = React.useState(() => localStorage.getItem("1a1-lang") || TWEAK_DEFAULTS.startLang);
const touched = React.useRef(false);
React.useEffect(() => {
localStorage.setItem("1a1-lang", lang);
document.documentElement.lang = lang;
}, [lang]);
const onSetLang = (l) => { touched.current = true; setLang(l); };
React.useEffect(() => { if (!touched.current) setLang(t.startLang); }, [t.startLang]);
// ---- Router: rutas reales por pestaña (/trabajo, /industrias, /herramientas-gratuitas) ----
const normalizePath = (p) => (p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p);
const [route, setRoute] = React.useState(() => normalizePath(window.location.pathname));
React.useEffect(() => {
const scrollToHashOrTop = (hash) => {
requestAnimationFrame(() => requestAnimationFrame(() => {
if (hash) {
const el = document.querySelector(hash);
if (el) { el.scrollIntoView(); return; }
}
window.scrollTo(0, 0);
}));
};
const onPop = () => {
setRoute(normalizePath(window.location.pathname));
scrollToHashOrTop(window.location.hash);
};
const onClick = (e) => {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
const a = e.target.closest && e.target.closest("a");
if (!a) return;
const raw = a.getAttribute("href");
if (!raw || a.target === "_blank" || a.hasAttribute("download")) return;
let url;
try { url = new URL(a.href, window.location.origin); } catch (_) { return; }
if (url.origin !== window.location.origin) return; // enlace externo
const cur = normalizePath(window.location.pathname);
const dest = normalizePath(url.pathname);
if (dest === cur) {
if (url.hash) return; // misma página con ancla → scroll nativo
e.preventDefault(); // misma página sin ancla (logo) → subir
window.scrollTo({ top: 0 });
return;
}
e.preventDefault(); // cambio de pestaña sin recargar
window.history.pushState({}, "", url.pathname + url.hash);
setRoute(dest);
scrollToHashOrTop(url.hash);
};
window.addEventListener("popstate", onPop);
document.addEventListener("click", onClick);
return () => {
window.removeEventListener("popstate", onPop);
document.removeEventListener("click", onClick);
};
}, []);
// Acento = color principal de toda la página
React.useEffect(() => {
document.documentElement.style.setProperty("--accent", t.accent);
}, [t.accent]);
// Movimiento (líneas + ticker)
React.useEffect(() => {
document.documentElement.classList.toggle("motion-off", !t.motion);
}, [t.motion]);
const copy = window.SITE[lang];
// Reveal on scroll — estado base visible; se oculta para animar sólo con JS.
React.useEffect(() => {
let scheduled = false;
const check = () => {
scheduled = false;
const vh = window.innerHeight || document.documentElement.clientHeight;
document.querySelectorAll("[data-reveal]:not(.in), .reveal-title:not(.in)").forEach((el) => {
if (el.getBoundingClientRect().top < vh * 0.92) el.classList.add("in");
});
};
const onScroll = () => { if (scheduled) return; scheduled = true; (window.requestAnimationFrame || setTimeout)(check); };
check();
document.documentElement.classList.add("reveal-on");
check();
const timers = [60, 200, 500, 1000].map((ms) => setTimeout(check, ms));
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll, { passive: true });
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
timers.forEach(clearTimeout);
};
}, [lang, route]);
return (
{route === "/trabajo" ? (
) : route === "/industrias" ? (
) : route === "/herramientas-gratuitas" ? (
) : (
)}
{t.cursor ? : null}
setTweak("accent", v)} />
setTweak("motion", v)} />
setTweak("cursor", v)} />
setTweak("startLang", v)} />
);
}
ReactDOM.createRoot(document.getElementById("root")).render();
// La app ya montó: avisa al preloader para que la ventana (portal) se abra
// sobre la página real y no sobre el esqueleto de carga.
requestAnimationFrame(function () {
if (typeof window.dismissPreloader === "function") window.dismissPreloader();
});