proposal-list.ts
typescript
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef
debug(push/stream): instrument O-frame decode path with INF…
Sonnet 4.6
patch
121 days ago
| 1 | /** |
| 2 | * proposal-list.ts — Proposal list page behaviour. |
| 3 | * |
| 4 | * Responsibilities: |
| 5 | * - Sync filter-bar tab active state after HTMX swaps (URL may change without |
| 6 | * a full navigation, so we re-derive active state from the current URL). |
| 7 | * - Restart the row entrance animation after HTMX injects new rows so the |
| 8 | * stagger plays on every tab/sort switch, not just on first load. |
| 9 | */ |
| 10 | |
| 11 | type PageData = Record<string, unknown>; |
| 12 | |
| 13 | export function initProposalList(_data: PageData): void { |
| 14 | syncFilterBar(); |
| 15 | hookHtmxSwap(); |
| 16 | } |
| 17 | |
| 18 | /** Re-read the current URL and mark the matching tab + sort option as active. */ |
| 19 | function syncFilterBar(): void { |
| 20 | const params = new URLSearchParams(window.location.search); |
| 21 | const state = params.get('state') ?? 'open'; |
| 22 | const sort = params.get('sort') ?? 'newest'; |
| 23 | |
| 24 | document.querySelectorAll<HTMLAnchorElement>('.prl-tab').forEach((tab) => { |
| 25 | const url = new URL(tab.href, location.origin); |
| 26 | const tabState = url.searchParams.get('state') ?? 'open'; |
| 27 | tab.classList.toggle('prl-tab--active', tabState === state); |
| 28 | }); |
| 29 | |
| 30 | document.querySelectorAll<HTMLAnchorElement>('.prl-sort-opt').forEach((opt) => { |
| 31 | const url = new URL(opt.href, location.origin); |
| 32 | const optSort = url.searchParams.get('sort') ?? 'newest'; |
| 33 | opt.classList.toggle('prl-sort-opt--active', optSort === sort); |
| 34 | }); |
| 35 | } |
| 36 | |
| 37 | /** |
| 38 | * After HTMX swaps #proposal-rows, restart the row entrance animation by |
| 39 | * briefly removing and re-adding the animation class via a forced reflow. |
| 40 | */ |
| 41 | function hookHtmxSwap(): void { |
| 42 | document.body.addEventListener('htmx:afterSwap', (e: Event) => { |
| 43 | const target = (e as CustomEvent).detail?.target as Element | undefined; |
| 44 | if (!target) return; |
| 45 | const container = target.id === 'proposal-rows' ? target : target.closest('#proposal-rows'); |
| 46 | if (!container) return; |
| 47 | |
| 48 | // Sync active states based on the now-updated URL |
| 49 | syncFilterBar(); |
| 50 | |
| 51 | // Re-trigger animations: clone trick forces reflow without DOM removal |
| 52 | const rows = container.querySelectorAll<HTMLElement>('.prl-row'); |
| 53 | rows.forEach((row, i) => { |
| 54 | row.style.animationDelay = `${i < 8 ? i * 28 : 0}ms`; |
| 55 | row.style.animation = 'none'; |
| 56 | void row.offsetHeight; // force reflow |
| 57 | row.style.animation = ''; |
| 58 | }); |
| 59 | }); |
| 60 | } |
File History
1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef
debug(push/stream): instrument O-frame decode path with INF…
Sonnet 4.6
patch
121 days ago