'use strict'; /** * REVEAL (D1-finish, 2026-07-31) — IntersectionObserver row reveal + rail highlight, * per `Vyndr System.dc.html`. * * Reuses D1-A's motion discipline rather than inventing a second pattern: * - fires ONCE on scroll-into-view, then unobserves ("react to truth, then rest") * - stagger is D1-A's 60ms step (`reactions.bootDelayMs`) — one source of truth * - reduced-motion is honoured by the CSS (`.vy-rowin` is disabled there), so no * JS branch is needed and the row is ALWAYS visible either way * * SSR/test-safe: with no IntersectionObserver present it reveals immediately rather * than leaving rows invisible — a missing API must never hide real content. */ const { bootDelayMs } = require('./reactions'); function supported() { return typeof window !== 'undefined' && typeof window.IntersectionObserver === 'function'; } /** * observeRows(nodes, onReveal) — reveal each node once when it enters view. * Returns a disconnect function. `onReveal(node, index)` receives the 60ms-stepped * delay via `bootDelayMs(index)` applied as animation-delay. */ function observeRows(nodes, onReveal) { const list = Array.from(nodes || []); const fire = (node, i) => { if (!node || node.dataset && node.dataset.revealed === '1') return; if (node.dataset) node.dataset.revealed = '1'; if (node.style) node.style.animationDelay = `${bootDelayMs(i)}ms`; if (node.classList) node.classList.add('vy-rowin'); if (typeof onReveal === 'function') onReveal(node, i); }; if (!supported()) { list.forEach(fire); return () => {}; } const idx = new Map(list.map((n, i) => [n, i])); const io = new window.IntersectionObserver((entries) => { for (const e of entries) { if (!e.isIntersecting) continue; fire(e.target, idx.get(e.target) || 0); io.unobserve(e.target); // ONCE — never a loop } }, { rootMargin: '0px 0px -10% 0px', threshold: 0.15 }); list.forEach((n) => n && io.observe(n)); return () => io.disconnect(); } module.exports = { observeRows, supported };