daemon-sec-lotl

DÆMONBins: GTFOBins × LOLBAS × WADComs × LOOBins in one filterable catalog
git clone https://git.daemon-sec.xyz/daemon-sec-lotl.git
Log | Files | Refs | Submodules | README | LICENSE

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 }