/** * proposal-list.ts — Proposal list page behaviour. * * Responsibilities: * - Sync filter-bar tab active state after HTMX swaps (URL may change without * a full navigation, so we re-derive active state from the current URL). * - Restart the row entrance animation after HTMX injects new rows so the * stagger plays on every tab/sort switch, not just on first load. */ type PageData = Record; export function initProposalList(_data: PageData): void { syncFilterBar(); hookHtmxSwap(); } /** Re-read the current URL and mark the matching tab + sort option as active. */ function syncFilterBar(): void { const params = new URLSearchParams(window.location.search); const state = params.get('state') ?? 'open'; const sort = params.get('sort') ?? 'newest'; document.querySelectorAll('.prl-tab').forEach((tab) => { const url = new URL(tab.href, location.origin); const tabState = url.searchParams.get('state') ?? 'open'; tab.classList.toggle('prl-tab--active', tabState === state); }); document.querySelectorAll('.prl-sort-opt').forEach((opt) => { const url = new URL(opt.href, location.origin); const optSort = url.searchParams.get('sort') ?? 'newest'; opt.classList.toggle('prl-sort-opt--active', optSort === sort); }); } /** * After HTMX swaps #proposal-rows, restart the row entrance animation by * briefly removing and re-adding the animation class via a forced reflow. */ function hookHtmxSwap(): void { document.body.addEventListener('htmx:afterSwap', (e: Event) => { const target = (e as CustomEvent).detail?.target as Element | undefined; if (!target) return; const container = target.id === 'proposal-rows' ? target : target.closest('#proposal-rows'); if (!container) return; // Sync active states based on the now-updated URL syncFilterBar(); // Re-trigger animations: clone trick forces reflow without DOM removal const rows = container.querySelectorAll('.prl-row'); rows.forEach((row, i) => { row.style.animationDelay = `${i < 8 ? i * 28 : 0}ms`; row.style.animation = 'none'; void row.offsetHeight; // force reflow row.style.animation = ''; }); }); }