/* Blackstock — runtime de animaciones premium (sin libs). * * Apple/Linear-feel: * - Smooth scroll global con ease-out. * - Scroll reveal: cualquier elemento con `data-bs-reveal` aparece al * entrar en viewport (fade + translate sutil). * - Counters: cualquier `data-bs-count="120"` se anima de 0 a N al * entrar en viewport. * - Navbar reactivo: cualquier `[data-bs-navbar]` recibe la clase * `is-scrolled` cuando scrollY > 24px. * - Smooth anchors: clicks en links `[href^="#"]` hacen scroll suave. * - Click feedback: cualquier `.bs-btn` lanza una micro-onda al pulsar. * * Sin dependencias, IntersectionObserver nativo, prefers-reduced-motion * respetado. Todo idempotente: re-ejecutar no rompe nada. */ (function () { if (window.__bsAnimationsRuntimeInstalled) return; window.__bsAnimationsRuntimeInstalled = true; const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; // ── Scroll reveal ─────────────────────────────────────────────── // Auto-añadir data-bs-reveal a tarjetas comunes (cards, secciones) // sin tocar el código existente. El observer aplica .is-visible // y CSS hace el resto. const revealObserver = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { entry.target.classList.add('bs-reveal-visible'); revealObserver.unobserve(entry.target); } } }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' } ); function watchReveal() { if (reduceMotion) return; document.querySelectorAll('[data-bs-reveal]').forEach((el) => { if (!el.classList.contains('bs-reveal-visible')) { el.classList.add('bs-reveal-init'); revealObserver.observe(el); } }); } // ── Counters animados ─────────────────────────────────────────── function animateCounter(el) { const target = Number(el.getAttribute('data-bs-count')) || 0; const duration = Number(el.getAttribute('data-bs-count-duration')) || 1400; const prefix = el.getAttribute('data-bs-count-prefix') || ''; const suffix = el.getAttribute('data-bs-count-suffix') || ''; const decimals = Number(el.getAttribute('data-bs-count-decimals')) || 0; if (reduceMotion) { el.textContent = prefix + target.toFixed(decimals) + suffix; return; } const startTs = performance.now(); const tick = (now) => { const t = Math.min(1, (now - startTs) / duration); // ease-out cubic const eased = 1 - Math.pow(1 - t, 3); const value = target * eased; el.textContent = prefix + value.toFixed(decimals) + suffix; if (t < 1) requestAnimationFrame(tick); else el.textContent = prefix + target.toFixed(decimals) + suffix; }; requestAnimationFrame(tick); } const counterObserver = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { animateCounter(entry.target); counterObserver.unobserve(entry.target); } } }, { threshold: 0.45 } ); function watchCounters() { document.querySelectorAll('[data-bs-count]').forEach((el) => { if (!el.dataset.bsCountInit) { el.dataset.bsCountInit = '1'; el.textContent = '0'; counterObserver.observe(el); } }); } // ── Navbar reactivo ───────────────────────────────────────────── let lastScrolled = false; function onScroll() { const isScrolled = window.scrollY > 24; if (isScrolled !== lastScrolled) { lastScrolled = isScrolled; document.querySelectorAll('[data-bs-navbar]').forEach((el) => { el.classList.toggle('is-scrolled', isScrolled); }); document.body.classList.toggle('bs-scrolled', isScrolled); } } window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); // ── Smooth anchors ────────────────────────────────────────────── document.addEventListener('click', (e) => { const link = e.target?.closest?.('a[href^="#"]'); if (!link) return; const href = link.getAttribute('href'); if (!href || href === '#' || href.length < 2) return; const target = document.querySelector(href); if (!target) return; e.preventDefault(); if (reduceMotion) { target.scrollIntoView(); } else { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); // ── Click feedback en botones ─────────────────────────────────── // Añade un destello sutil al pulsar cualquier .bs-btn (sin romper // el comportamiento de los botones existentes). document.addEventListener( 'pointerdown', (e) => { if (reduceMotion) return; const btn = e.target?.closest?.('.bs-btn'); if (!btn || btn.disabled) return; const rect = btn.getBoundingClientRect(); const ripple = document.createElement('span'); ripple.className = 'bs-ripple'; const size = Math.max(rect.width, rect.height) * 1.4; ripple.style.width = ripple.style.height = `${size}px`; ripple.style.left = `${e.clientX - rect.left - size / 2}px`; ripple.style.top = `${e.clientY - rect.top - size / 2}px`; btn.appendChild(ripple); setTimeout(() => ripple.remove(), 700); }, { passive: true } ); // ── Anti-doble-click sobre botones de submit/CTA ──────────────── // Solo si se marca explícitamente con data-bs-once. Evita doble // submit en formularios sin que rompamos botones normales. document.addEventListener('click', (e) => { const btn = e.target?.closest?.('button[data-bs-once]'); if (!btn || btn.disabled) return; btn.disabled = true; btn.classList.add('bs-loading'); setTimeout(() => { btn.disabled = false; btn.classList.remove('bs-loading'); }, 1800); }); // ── Auto-aplicar data-bs-reveal a secciones/cards comunes ────── // Heurística suave: cualquier
directa y .bs-card no // marcadas. El user puede des-marcar con data-bs-no-reveal. function autoMarkReveal() { if (reduceMotion) return; const candidates = document.querySelectorAll( 'section, .bs-card, .bs-pricing-card, [data-pricing-card]' ); candidates.forEach((el) => { if ( el.hasAttribute('data-bs-reveal') || el.hasAttribute('data-bs-no-reveal') || el.closest('[data-bs-no-reveal]') ) return; el.setAttribute('data-bs-reveal', ''); }); } // Ejecuta al cargar y observa el DOM por si los componentes React // se montan después (Babel runtime es lento). function refreshAll() { autoMarkReveal(); watchReveal(); watchCounters(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', refreshAll); } else { refreshAll(); } // Re-aplica cada vez que el DOM cambia (React monta tras Babel) const mo = new MutationObserver((muts) => { let added = false; for (const m of muts) { if (m.addedNodes && m.addedNodes.length) { added = true; break; } } if (added) refreshAll(); }); mo.observe(document.body, { childList: true, subtree: true }); // Smooth scroll global vía CSS — fallback si el browser no aplica. document.documentElement.style.scrollBehavior = reduceMotion ? 'auto' : 'smooth'; window.bsRefreshAnimations = refreshAll; })();