rf-v4.11-ecosistema listo
This commit is contained in:
+109
-52
@@ -1,5 +1,6 @@
|
||||
/* ==========================================================================
|
||||
ECOSISTEMA IDE — Cambio in-place entre tarjetas y paneles de unidad
|
||||
ECOSISTEMA IDE — Cambio in-place + deep link por hash
|
||||
URLs válidas: #eco-excellence | #eco-projects | #eco-digital | #ecosistema
|
||||
========================================================================== */
|
||||
(function () {
|
||||
'use strict';
|
||||
@@ -14,117 +15,173 @@
|
||||
const backBtn = section.querySelector('[data-eco-close]');
|
||||
const pills = Array.from(section.querySelectorAll('.eco-pill'));
|
||||
|
||||
let lastFocusedTrigger = null;
|
||||
const VALID_UNITS = ['excellence', 'projects', 'digital'];
|
||||
const HASH_PREFIX = '#eco-';
|
||||
const HASH_ECOSYSTEM = '#ecosistema';
|
||||
|
||||
/* --- Mostrar panel de una unidad --- */
|
||||
function openUnit(id) {
|
||||
let lastFocusedTrigger = null;
|
||||
let currentUnit = null; // null = ecosistema
|
||||
|
||||
/* ---------- Utilidades de URL ---------- */
|
||||
function unitFromHash() {
|
||||
const h = window.location.hash;
|
||||
if (h === HASH_ECOSYSTEM) return null;
|
||||
if (h.startsWith(HASH_PREFIX)) {
|
||||
const id = h.slice(HASH_PREFIX.length);
|
||||
return VALID_UNITS.includes(id) ? id : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hashForUnit(id) {
|
||||
return id ? HASH_PREFIX + id : HASH_ECOSYSTEM;
|
||||
}
|
||||
|
||||
/* ---------- Render (no toca la URL) ---------- */
|
||||
function renderUnit(id) {
|
||||
const panel = panels.find(p => p.dataset.ecoPanel === id);
|
||||
if (!panel) return;
|
||||
|
||||
// Guardamos el trigger que originó el cambio (para devolver foco)
|
||||
const origin = triggers.find(t => t.dataset.ecoOpen === id);
|
||||
if (origin) lastFocusedTrigger = origin;
|
||||
|
||||
// Ocultar ecosistema, mostrar estado unidad
|
||||
stateEcosystem.hidden = true;
|
||||
stateUnit.hidden = false;
|
||||
|
||||
// Alternar paneles
|
||||
panels.forEach(p => {
|
||||
const isActive = p.dataset.ecoPanel === id;
|
||||
p.hidden = !isActive;
|
||||
if (isActive) {
|
||||
// Re-trigger de la animación de entrada
|
||||
p.style.animation = 'none';
|
||||
// Forzar reflow
|
||||
void p.offsetWidth;
|
||||
void p.offsetWidth; // reflow para reiniciar animación
|
||||
p.style.animation = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Aria expandido en tarjetas
|
||||
triggers.forEach(t => {
|
||||
const isThis = t.dataset.ecoOpen === id;
|
||||
t.setAttribute('aria-expanded', isThis ? 'true' : 'false');
|
||||
t.setAttribute('aria-expanded', t.dataset.ecoOpen === id ? 'true' : 'false');
|
||||
});
|
||||
|
||||
// Pills activas
|
||||
pills.forEach(pill => {
|
||||
pill.classList.toggle('is-active', pill.dataset.ecoOpen === id);
|
||||
});
|
||||
|
||||
// Foco accesible al título del panel
|
||||
const title = panel.querySelector('.eco-panel__title');
|
||||
if (title) {
|
||||
title.setAttribute('tabindex', '-1');
|
||||
// Pequeño timeout para asegurar visibilidad tras el cambio de estado
|
||||
requestAnimationFrame(() => title.focus({ preventScroll: false }));
|
||||
}
|
||||
|
||||
// Scroll suave al inicio de la sección (solo si quedó fuera de vista)
|
||||
const rect = section.getBoundingClientRect();
|
||||
if (rect.top < -80) {
|
||||
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
currentUnit = id;
|
||||
}
|
||||
|
||||
/* --- Volver al estado ecosistema --- */
|
||||
function closeUnit() {
|
||||
function renderEcosystem({ restoreFocus = true } = {}) {
|
||||
stateUnit.hidden = true;
|
||||
stateEcosystem.hidden = false;
|
||||
|
||||
triggers.forEach(t => t.setAttribute('aria-expanded', 'false'));
|
||||
pills.forEach(p => p.classList.remove('is-active'));
|
||||
|
||||
// Devolver foco a la tarjeta origen
|
||||
if (lastFocusedTrigger && typeof lastFocusedTrigger.focus === 'function') {
|
||||
lastFocusedTrigger.focus({ preventScroll: false });
|
||||
} else {
|
||||
// Fallback: primer botón del grid
|
||||
const first = triggers[0];
|
||||
if (first) first.focus({ preventScroll: false });
|
||||
if (restoreFocus) {
|
||||
if (lastFocusedTrigger && typeof lastFocusedTrigger.focus === 'function') {
|
||||
lastFocusedTrigger.focus({ preventScroll: false });
|
||||
} else if (triggers[0]) {
|
||||
triggers[0].focus({ preventScroll: false });
|
||||
}
|
||||
}
|
||||
|
||||
currentUnit = null;
|
||||
}
|
||||
|
||||
/* ---------- API con URL ---------- */
|
||||
function openUnit(id, { push = true, scroll = true } = {}) {
|
||||
if (!VALID_UNITS.includes(id)) return;
|
||||
renderUnit(id);
|
||||
|
||||
if (push) {
|
||||
const url = window.location.pathname + window.location.search + hashForUnit(id);
|
||||
history.pushState({ eco: id }, '', url);
|
||||
}
|
||||
|
||||
if (scroll) {
|
||||
const rect = section.getBoundingClientRect();
|
||||
if (rect.top < -80 || rect.top > 200) {
|
||||
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Listeners --- */
|
||||
function closeUnit({ push = true, scroll = false } = {}) {
|
||||
renderEcosystem();
|
||||
if (push) {
|
||||
const url = window.location.pathname + window.location.search + HASH_ECOSYSTEM;
|
||||
history.pushState({ eco: null }, '', url);
|
||||
}
|
||||
if (scroll) {
|
||||
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Listeners de UI ---------- */
|
||||
triggers.forEach(trigger => {
|
||||
trigger.addEventListener('click', (e) => {
|
||||
const id = trigger.dataset.ecoOpen;
|
||||
if (!id) return;
|
||||
// Si es un <a> real (academy), dejamos que navegue
|
||||
// Academy es <a href="academy.html">: dejamos navegar
|
||||
if (trigger.tagName === 'A' && trigger.getAttribute('href')) return;
|
||||
e.preventDefault();
|
||||
openUnit(id);
|
||||
});
|
||||
|
||||
// Soporte teclado para tarjetas no-button (por si acaso)
|
||||
trigger.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
const id = trigger.dataset.ecoOpen;
|
||||
if (!id) return;
|
||||
if (trigger.tagName === 'A') return;
|
||||
e.preventDefault();
|
||||
openUnit(id);
|
||||
}
|
||||
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||
const id = trigger.dataset.ecoOpen;
|
||||
if (!id) return;
|
||||
if (trigger.tagName === 'A') return;
|
||||
e.preventDefault();
|
||||
openUnit(id);
|
||||
});
|
||||
});
|
||||
|
||||
if (backBtn) backBtn.addEventListener('click', closeUnit);
|
||||
if (backBtn) backBtn.addEventListener('click', () => closeUnit({ scroll: true }));
|
||||
|
||||
// Escape cierra el panel expandido
|
||||
section.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !stateUnit.hidden) {
|
||||
closeUnit();
|
||||
}
|
||||
if (e.key === 'Escape' && !stateUnit.hidden) closeUnit();
|
||||
});
|
||||
|
||||
/* --- Deep link opcional por hash (#eco-projects) sin recargar --- */
|
||||
function readHash() {
|
||||
const hash = window.location.hash.replace('#eco-', '');
|
||||
if (['excellence', 'projects', 'digital'].includes(hash)) {
|
||||
openUnit(hash);
|
||||
/* ---------- Sincronización con la URL ---------- */
|
||||
function syncFromLocation() {
|
||||
const id = unitFromHash();
|
||||
if (id && id !== currentUnit) {
|
||||
renderUnit(id);
|
||||
} else if (!id && currentUnit !== null) {
|
||||
renderEcosystem({ restoreFocus: false });
|
||||
}
|
||||
}
|
||||
window.addEventListener('hashchange', readHash);
|
||||
readHash();
|
||||
|
||||
// Carga inicial: si el hash trae unidad, abrir + scroll
|
||||
(function initFromUrl() {
|
||||
const id = unitFromHash();
|
||||
if (id) {
|
||||
renderUnit(id);
|
||||
requestAnimationFrame(() => {
|
||||
section.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
});
|
||||
} else {
|
||||
renderEcosystem({ restoreFocus: false });
|
||||
}
|
||||
})();
|
||||
|
||||
// Cambios manuales del hash (pegar link, editar barra, links compartidos)
|
||||
window.addEventListener('hashchange', syncFromLocation);
|
||||
|
||||
// Atrás/adelante del navegador
|
||||
window.addEventListener('popstate', syncFromLocation);
|
||||
|
||||
/* ---------- API pública opcional ---------- */
|
||||
window.EcoIDE = {
|
||||
open: openUnit,
|
||||
close: closeUnit,
|
||||
current: () => currentUnit,
|
||||
};
|
||||
})();
|
||||
+50
-20
@@ -116,7 +116,7 @@ const nodosData = [
|
||||
|
||||
// Crear pulsos
|
||||
conn.pulsos = [];
|
||||
const cantPulsos = 10;
|
||||
const cantPulsos = 6;
|
||||
|
||||
for (let i = 0; i < cantPulsos; i++) {
|
||||
const pathPulso = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
@@ -149,24 +149,54 @@ const nodosData = [
|
||||
});
|
||||
}, 1200);
|
||||
|
||||
// 4. Motor de Animación Ultraligero
|
||||
function animarFrame() {
|
||||
conexiones.forEach(conn => {
|
||||
conn.pulsos.forEach(p => {
|
||||
p.dash -= p.speed;
|
||||
|
||||
// OPTIMIZACIÓN EXTREMA: Solo interactuamos con el DOM si el pulso saltó de carril
|
||||
if (p.needsPathUpdate) {
|
||||
p.el.setAttribute('d', conn.rutasCache[p.currentOffset]);
|
||||
p.needsPathUpdate = false;
|
||||
}
|
||||
|
||||
// Lo único que se actualiza contínuamente es el desplazamiento
|
||||
p.el.style.strokeDashoffset = p.dash;
|
||||
});
|
||||
});
|
||||
// 4. Motor de Animación Ultraligero
|
||||
let animando = false;
|
||||
let rafId = null;
|
||||
|
||||
requestAnimationFrame(animarFrame);
|
||||
}
|
||||
function animarFrame() {
|
||||
if (!animando) return; // corta el loop cuando no es visible
|
||||
conexiones.forEach(conn => {
|
||||
conn.pulsos.forEach(p => {
|
||||
p.dash -= p.speed;
|
||||
if (p.needsPathUpdate) {
|
||||
p.el.setAttribute('d', conn.rutasCache[p.currentOffset]);
|
||||
p.needsPathUpdate = false;
|
||||
}
|
||||
p.el.style.strokeDashoffset = p.dash;
|
||||
});
|
||||
});
|
||||
rafId = requestAnimationFrame(animarFrame);
|
||||
}
|
||||
|
||||
animarFrame();
|
||||
function iniciarAnimacion() {
|
||||
if (animando) return;
|
||||
animando = true;
|
||||
rafId = requestAnimationFrame(animarFrame);
|
||||
}
|
||||
|
||||
function detenerAnimacion() {
|
||||
animando = false;
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
}
|
||||
|
||||
// Detectar visibilidad de la sección del header
|
||||
const heroVisual = document.querySelector('.hero-visual');
|
||||
if (heroVisual && 'IntersectionObserver' in window) {
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) iniciarAnimacion();
|
||||
else detenerAnimacion();
|
||||
});
|
||||
}, { threshold: 0.05 });
|
||||
io.observe(heroVisual);
|
||||
} else {
|
||||
iniciarAnimacion(); // fallback
|
||||
}
|
||||
|
||||
// Pausar también cuando la pestaña queda oculta
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) detenerAnimacion();
|
||||
else if (heroVisual && heroVisual.getBoundingClientRect().top < window.innerHeight) {
|
||||
iniciarAnimacion();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user