hero.ts (2601B)
1 // The home hero controller — the collection ledger lights each collection in 2 // turn on a loop. Point at a deck (or tab to it) and it takes the loop over; 3 // leave and the loop resumes. The stat counters are rolled up separately by 4 // initCounters() in app.ts (shared with the platform stats below the fold). 5 // 6 // Everything the hero shows is in the server-rendered markup, so with JS off it 7 // is a static, fully-legible ledger. This only adds the cycling, which collapses 8 // to a single lit deck under prefers-reduced-motion. Bound once per element and 9 // torn down on view-transition navigations. 10 11 export function initHero(): void { 12 const hero = document.querySelector<HTMLElement>('[data-hero]'); 13 if (!hero || hero.dataset.heroBound) return; 14 hero.dataset.heroBound = '1'; 15 16 const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches || document.documentElement.dataset.motion === 'reduce'; 17 const rows = Array.from(hero.querySelectorAll<HTMLElement>('[data-hero-row]')); 18 const blurbEl = hero.querySelector<HTMLElement>('[data-hero-blurb]'); 19 if (!rows.length) return; 20 21 let active = 0; 22 let paused = false; 23 let timer = 0; 24 25 const light = (i: number): void => { 26 active = ((i % rows.length) + rows.length) % rows.length; 27 rows.forEach((row, j) => { 28 const on = j === active; 29 row.classList.toggle('is-lit', on); 30 if (on) row.setAttribute('aria-current', 'true'); 31 else row.removeAttribute('aria-current'); 32 }); 33 const blurb = rows[active].dataset.blurb; 34 if (blurbEl && blurb) blurbEl.textContent = blurb; 35 }; 36 37 const stop = (): void => { if (timer) { clearInterval(timer); timer = 0; } }; 38 const start = (): void => { 39 if (reduced) return; 40 stop(); 41 timer = window.setInterval(() => { if (!paused) light(active + 1); }, 3000); 42 }; 43 44 rows.forEach((row, i) => { 45 const take = (): void => { paused = true; light(i); }; 46 const release = (): void => { paused = false; }; 47 row.addEventListener('mouseenter', take); 48 row.addEventListener('focusin', take); 49 row.addEventListener('mouseleave', release); 50 row.addEventListener('focusout', release); 51 }); 52 53 // Pause the loop while the tab is hidden so it does not silently advance 54 // through several decks in the background and jump on return. 55 const onVis = (): void => { if (document.hidden) stop(); else start(); }; 56 document.addEventListener('visibilitychange', onVis); 57 58 light(0); 59 start(); 60 61 document.addEventListener('astro:before-swap', () => { 62 stop(); 63 document.removeEventListener('visibilitychange', onVis); 64 }, { once: true }); 65 }