/** * Card highlight system for cross-entity navigation. * * CrossLink/SearchPalette call requestHighlight(id) before goto(). * The destination page calls highlightFromUrl() after data loads, which * picks up the pending ID and highlights the matching card with a * smooth CSS keyframe glow + dim overlay. */ const HIGHLIGHT_DURATION = 2000; const WAIT_TIMEOUT = 5000; /** Pending highlight ID — set before navigation. */ let _pendingHighlight: string | null = null; /** Request a card highlight. Called before goto(). */ export function requestHighlight(id: string | number): void { _pendingHighlight = String(id); } /** Check for pending highlight, find card, scroll & highlight. Call after data loads. */ export function highlightFromUrl(): void { if (typeof window === 'undefined') return; // Check global pending first, then URL param as fallback let id = _pendingHighlight; _pendingHighlight = null; if (!id) { const params = new URLSearchParams(window.location.search); id = params.get('highlight'); if (id) { params.delete('highlight'); const qs = params.toString(); const cleanUrl = window.location.pathname + (qs ? '?' + qs : ''); window.history.replaceState(null, '', cleanUrl); } } if (!id) return; // Wait for DOM to render after loaded=true requestAnimationFrame(() => { requestAnimationFrame(() => { const card = document.querySelector(`[data-entity-id="${CSS.escape(id)}"]`); if (card) { _highlightCard(card as HTMLElement); } else { _waitForCard(id!); } }); }); } function _highlightCard(card: HTMLElement): void { const overlay = _showDimOverlay(); // Scroll to card card.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Apply highlight via inline style (overrides stagger CSS class animation) card.style.animation = 'cardHighlight 2s ease-in-out'; card.style.position = 'relative'; card.style.zIndex = '11'; // Cleanup: set animation to 'none' (inline beats class, prevents stagger replay) setTimeout(() => { card.style.animation = 'none'; card.style.removeProperty('position'); card.style.removeProperty('z-index'); overlay.classList.remove('active'); setTimeout(() => overlay.remove(), 300); }, HIGHLIGHT_DURATION); } function _showDimOverlay(): HTMLElement { let overlay = document.querySelector('.nav-dim-overlay') as HTMLElement | null; if (!overlay) { overlay = document.createElement('div'); overlay.className = 'nav-dim-overlay'; document.body.appendChild(overlay); } void overlay.offsetHeight; overlay.classList.add('active'); return overlay; } function _waitForCard(id: string): void { const start = Date.now(); const observer = new MutationObserver(() => { const card = document.querySelector(`[data-entity-id="${CSS.escape(id)}"]`); if (card) { observer.disconnect(); setTimeout(() => _highlightCard(card as HTMLElement), 50); return; } if (Date.now() - start > WAIT_TIMEOUT) { observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => observer.disconnect(), WAIT_TIMEOUT); }