// final-app.jsx — landing final con tweaks que tienen efecto real
const FinalDefaults = /*EDITMODE-BEGIN*/{
"lang": "es",
"accent": "#2199ea",
"showTerminal": true,
"showParticles": true,
"magneticHover": true,
"customCursor": true,
"rotateInterval": 3500
}/*EDITMODE-END*/;
// Context exposes live tweak values to all sections
const TweaksCtx = React.createContext(FinalDefaults);
window.TweaksCtx = TweaksCtx;
function FinalApp() {
const [tweaks, setTweaks] = React.useState(FinalDefaults);
const setTweak = (k, v) => {
const next = typeof k === 'object' ? { ...tweaks, ...k } : { ...tweaks, [k]: v };
setTweaks(next);
try { window.parent.postMessage({ type: '__edit_mode_set_keys', edits: next }, '*'); } catch (e) {}
};
const [editMode, setEditMode] = React.useState(false);
React.useEffect(() => {
const onMsg = (e) => {
if (!e.data || !e.data.type) return;
if (e.data.type === '__activate_edit_mode') setEditMode(true);
if (e.data.type === '__deactivate_edit_mode') setEditMode(false);
};
window.addEventListener('message', onMsg);
try { window.parent.postMessage({ type: '__edit_mode_available' }, '*'); } catch (e) {}
return () => window.removeEventListener('message', onMsg);
}, []);
// Sync accent into F + CSS var so every section reads new value
React.useEffect(() => {
let s = document.getElementById('final-accent-css');
if (!s) { s = document.createElement('style'); s.id = 'final-accent-css'; document.head.appendChild(s); }
s.textContent = `:root { --f-accent: ${tweaks.accent}; --f-accent-glow: ${tweaks.accent}73; }`;
if (window.F) {
window.F.accent = tweaks.accent;
window.F.accentGlow = tweaks.accent + '73';
}
}, [tweaks.accent]);
const lang = tweaks.lang || 'es';
const content = { ...EUREKANT_CONTENT[lang], __lang: lang };
const scrollY = useGlobalScroll();
// doc scroll progress
const [docProg, setDocProg] = React.useState(0);
React.useEffect(() => {
const onScroll = () => {
const h = document.documentElement.scrollHeight - window.innerHeight;
setDocProg(h > 0 ? window.scrollY / h : 0);
};
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
// Custom cursor + trail
const cursorRef = React.useRef(null);
const cursorRingRef = React.useRef(null);
const trailRef = React.useRef([]);
const trailContainerRef = React.useRef(null);
React.useEffect(() => {
if (!tweaks.customCursor) {
document.body.style.cursor = '';
return;
}
document.body.style.cursor = 'none';
// Trail particles pool
const TRAIL = 18;
const pool = [];
const container = document.createElement('div');
container.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:248;overflow:hidden';
document.body.appendChild(container);
trailContainerRef.current = container;
for (let i = 0; i < TRAIL; i++) {
const dot = document.createElement('div');
const size = 4 + (TRAIL - i) * 0.6;
dot.style.cssText = `position:absolute;top:0;left:0;width:${size}px;height:${size}px;border-radius:50%;background:${tweaks.accent};pointer-events:none;opacity:0;will-change:transform,opacity;mix-blend-mode:screen;`;
container.appendChild(dot);
pool.push({ el: dot, x: 0, y: 0, opacity: 0 });
}
let mouse = { x: 0, y: 0 };
let positions = Array(TRAIL).fill({ x: 0, y: 0 });
let rafId;
const onMove = (e) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
if (cursorRef.current) cursorRef.current.style.transform = `translate3d(${e.clientX - 4}px, ${e.clientY - 4}px, 0)`;
if (cursorRingRef.current) cursorRingRef.current.style.transform = `translate3d(${e.clientX - 16}px, ${e.clientY - 16}px, 0)`;
};
const animate = () => {
// Each trail point follows the one before it with lag
positions = positions.map((p, i) => {
const target = i === 0 ? mouse : positions[i - 1];
const lag = 0.28 + i * 0.03;
return { x: p.x + (target.x - p.x) * lag, y: p.y + (target.y - p.y) * lag };
});
pool.forEach((p, i) => {
const pos = positions[i];
const opacity = (1 - i / TRAIL) * 0.55;
const size = 4 + (TRAIL - i) * 0.6;
p.el.style.transform = `translate3d(${pos.x - size / 2}px, ${pos.y - size / 2}px, 0)`;
p.el.style.opacity = opacity;
});
rafId = requestAnimationFrame(animate);
};
animate();
window.addEventListener('mousemove', onMove);
return () => {
window.removeEventListener('mousemove', onMove);
cancelAnimationFrame(rafId);
document.body.style.cursor = '';
container.remove();
};
}, [tweaks.customCursor, tweaks.accent]);
return (