// 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 (
{tweaks.customCursor && (
)} setTweak('lang', l)} scrollY={scrollY} /> {editMode && ( { setEditMode(false); try { window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*'); } catch (e) {} }} /> )}
); } function FinalTweaksPanel({ tweaks, setTweak, onClose }) { return (
// TWEAKS
{['es', 'en'].map(l => ( ))}
{['#2199ea', '#63b7f0', '#59a4d6', '#5cf0b8', '#ffb86b', '#ff5f8f'].map(c => (
setTweak('rotateInterval', parseInt(e.target.value))} style={{ width: '100%', accentColor: tweaks.accent }} /> setTweak('showTerminal', !tweaks.showTerminal)} accent={tweaks.accent} /> setTweak('showParticles', !tweaks.showParticles)} accent={tweaks.accent} /> setTweak('magneticHover', !tweaks.magneticHover)} accent={tweaks.accent} /> setTweak('customCursor', !tweaks.customCursor)} accent={tweaks.accent} />
); } function TwRow({ label, children }) { return (
{label}
{children}
); } function TwToggle({ label, v, on, accent }) { return (
{label}
); } ReactDOM.createRoot(document.getElementById('root')).render();