1/** 2 * ============================================================ 3 * CRUZ ESTUDIO ® â Trabajos archive 4 * 5 * - View switcher Grid / List (legacy). 6 * - Menú hamburguesa mobile (match index). 7 * - HOVER CAROUSEL en cada card: al pasar el cursor sobre un 8 * .project-item, sus imágenes adicionales (data-images JSON 9 * array) se ciclan con crossfade suave + barra de progreso. 10 * ============================================================ 11 */ 12document.addEventListener('DOMContentLoaded', () => { 13 14 /* ---------- 1. VIEW SWITCHER (Explorar / Grid / List) ---------- 15 La vista EXPLORAR (canvas arrastrable) es la default en TODOS los dispositivos 16 â también en mobile (arrastre táctil). El canvas lo controla 17 window.CruzCanvas (java/trabajos-canvas.js). */ 18 const viewBtns = document.querySelectorAll('.cg-viewswitch .view-btn'); 19 const container = document.getElementById('archive-main'); 20 21 function setView(view) { 22 viewBtns.forEach(b => b.classList.toggle('active', b.getAttribute('data-view') === view)); 23 if (view === 'canvas') { 24 if (window.CruzCanvas) window.CruzCanvas.activate(); 25 } else { 26 if (window.CruzCanvas) window.CruzCanvas.deactivate(); 27 container.classList.toggle('list-mode', view === 'list'); 28 container.classList.toggle('grid-mode', view !== 'list'); 29 } 30 } 31 32 viewBtns.forEach(btn => { 33 btn.addEventListener('click', () => setView(btn.getAttribute('data-view'))); 34 }); 35 36 // Vista por defecto: EXPLORAR (inmersiva) en TODOS los dispositivos. 37 if (window.CruzCanvas) setView('canvas'); 38 else setView('grid'); 39 40 /* ---------- 2. MENà HAMBURGUESA (match index) ---------- */ 41 const menuToggle = document.getElementById('mobile-menu'); 42 const navLinks = document.getElementById('nav-links'); 43 if (menuToggle) { 44 menuToggle.addEventListener('click', () => { 45 navLinks.classList.toggle('active'); 46 menuToggle.classList.toggle('open'); 47 document.body.classList.toggle('menu-open'); 48 }); 49 } 50 51 /* ---------- 3. HOVER CAROUSEL EN LAS CARDS ---------- */ 52 initHoverCarousel(); 53}); 54 55/** 56 * Activa el carousel de imágenes adicionales al hover en cada .project-item. 57 * Espera que cada item tenga: 58 * data-images='["url1", "url2", "url3", ...]' 59 * (JSON array de URLs). Si el atributo no existe o tiene <2 imágenes, 60 * el item se ignora (queda con su comportamiento normal). 61 */ 62function initHoverCarousel() { 63 const items = document.querySelectorAll('.project-item[data-images]'); 64 if (!items.length) return; 65 66 const CYCLE_MS = 800; // tiempo entre cambios de imagen 67 // Touch / sin hover real: skip carousel (no aporta nada) 68 const isTouch = window.matchMedia('(hover: none)').matches; 69 if (isTouch) return; 70 71 items.forEach((item) => { 72 const visualBox = item.querySelector('.visual-box'); 73 if (!visualBox) return; 74 75 // Parse de las imágenes del carousel 76 let extras = []; 77 try { 78 extras = JSON.parse(item.getAttribute('data-images') || '[]'); 79 } catch (_) { 80 extras = []; 81 } 82 if (!Array.isArray(extras) || extras.length < 2) return; 83 84 // Inyectamos cada URL como un <img class="pi-cycle-img"> stackeado. 85 // La primera arranca con .is-active. El <img> estático original queda 86 // como fallback debajo del stack. 87 const cycleImgs = extras.map((src, i) => { 88 const img = document.createElement('img'); 89 img.className = 'pi-cycle-img' + (i === 0 ? ' is-active' : ''); 90 img.alt = ''; 91 img.setAttribute('aria-hidden', 'true'); 92 img.loading = 'eager'; 93 img.decoding = 'async'; 94 img.src = src; 95 return img; 96 }); 97 98 // Indicador de progreso (lÃnea inferior amarilla) 99 const indicator = document.createElement('div'); 100 indicator.className = 'pi-cycle-indicator'; 101 const indicatorFill = document.createElement('div'); 102 indicatorFill.className = 'pi-cycle-indicator-fill'; 103 indicator.appendChild(indicatorFill); 104
105 cycleImgs.forEach(img => visualBox.appendChild(img)); 106 visualBox.appendChild(indicator); 107 108 let currentIdx = 0; 109 let cycleTimer = null; 110 111 function showIndex(idx) { 112 cycleImgs.forEach((img, i) => { 113 img.classList.toggle('is-active', i === idx); 114 }); 115 // Reset + animar progress bar 116 indicatorFill.style.transition = 'none'; 117 indicatorFill.style.width = '0%'; 118 void indicatorFill.offsetWidth; // force reflow 119 indicatorFill.style.transition = 'width ' + CYCLE_MS + 'ms linear'; 120 indicatorFill.style.width = '100%'; 121 } 122 123 function startCycle() { 124 currentIdx = 0; 125 showIndex(currentIdx); 126 cycleTimer = setInterval(() => { 127 currentIdx = (currentIdx + 1) % cycleImgs.length; 128 showIndex(currentIdx); 129 }, CYCLE_MS); 130 } 131 132 function stopCycle() { 133 if (cycleTimer) { 134 clearInterval(cycleTimer); 135 cycleTimer = null; 136 } 137 cycleImgs.forEach((img, i) => { 138 img.classList.toggle('is-active', i === 0); 139 }); 140 indicatorFill.style.transition = 'width 0.3s ease'; 141 indicatorFill.style.width = '0%'; 142 } 143 144 item.addEventListener('mouseenter', startCycle); 145 item.addEventListener('mouseleave', stopCycle); 146 }); 147}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.