From 88e21e41e2cbc66c6fd1cc640a7ca031a0006e57 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sun, 22 Mar 2026 00:11:32 +0300 Subject: [PATCH] fix: switch highlight to global store instead of URL params URL param timing was unreliable with SvelteKit client-side routing. Now CrossLink calls requestHighlight(id) setting a global variable before goto(), and highlightFromUrl() reads it after data loads. Double requestAnimationFrame ensures DOM has rendered after loaded=true. Falls back to ?highlight= URL param for direct links. --- frontend/src/lib/components/CrossLink.svelte | 14 ++-- frontend/src/lib/highlight.ts | 75 +++++++++++--------- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/frontend/src/lib/components/CrossLink.svelte b/frontend/src/lib/components/CrossLink.svelte index 6e2b02b..3e4aa85 100644 --- a/frontend/src/lib/components/CrossLink.svelte +++ b/frontend/src/lib/components/CrossLink.svelte @@ -1,6 +1,7 @@ - + {label} diff --git a/frontend/src/lib/highlight.ts b/frontend/src/lib/highlight.ts index 03f975d..2cf9ab5 100644 --- a/frontend/src/lib/highlight.ts +++ b/frontend/src/lib/highlight.ts @@ -1,50 +1,63 @@ /** * Card highlight system for cross-entity navigation. * - * When navigating via CrossLink, the target entity ID is passed as ?highlight=. - * The destination page calls highlightFromUrl() after data loads, which: - * 1. Shows a dim overlay behind everything - * 2. Finds the card with [data-entity-id=""] - * 3. Scrolls to it smoothly - * 4. Applies a pulsing glow via direct style manipulation - * 5. Cleans up after 2.5 seconds + * CrossLink calls requestHighlight(id) before goto(), storing the ID globally. + * The destination page calls highlightFromUrl() after data loads, which + * picks up the pending ID and highlights the matching card. */ const HIGHLIGHT_DURATION = 2500; const PULSE_INTERVAL = 400; const WAIT_TIMEOUT = 5000; -/** Show dim overlay, find card, scroll & highlight. Call after data loads. */ +/** Pending highlight ID — set by CrossLink before navigation. */ +let _pendingHighlight: string | null = null; + +/** Request a card highlight. Called by CrossLink 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; - const params = new URLSearchParams(window.location.search); - const id = params.get('highlight'); + + // 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; - // Clean the URL immediately (remove highlight param, keep others) - params.delete('highlight'); - const qs = params.toString(); - const cleanUrl = window.location.pathname + (qs ? '?' + qs : ''); - window.history.replaceState(null, '', cleanUrl); - - // Try to find the card now, or wait for it - const card = document.querySelector(`[data-entity-id="${id}"]`); - if (card) { - // Small delay for layout to settle after loaded=true - setTimeout(() => _highlightCard(card as HTMLElement), 100); - } else { - _waitForCard(id); - } + // Wait a tick for DOM to render after loaded=true + requestAnimationFrame(() => { + requestAnimationFrame(() => { + const card = document.querySelector(`[data-entity-id="${id}"]`); + if (card) { + _highlightCard(card as HTMLElement); + } else { + _waitForCard(id!); + } + }); + }); } function _highlightCard(card: HTMLElement): void { - // Show dim overlay const overlay = _showDimOverlay(); - // Scroll to card card.scrollIntoView({ behavior: 'smooth', block: 'center' }); - // Save original styles + // Save originals const origBoxShadow = card.style.boxShadow; const origZIndex = card.style.zIndex; const origPosition = card.style.position; @@ -54,11 +67,11 @@ function _highlightCard(card: HTMLElement): void { card.style.position = 'relative'; card.style.zIndex = '11'; - // Get primary color from CSS variable + // Get primary color const primary = getComputedStyle(document.documentElement) .getPropertyValue('--color-primary').trim(); - // Pulse effect via interval + // Pulsing glow let on = true; const glowOn = `0 0 0 3px ${primary}, 0 0 24px ${primary}60`; const glowOff = `0 0 0 2px ${primary}80`; @@ -74,7 +87,6 @@ function _highlightCard(card: HTMLElement): void { // Cleanup setTimeout(() => { clearInterval(pulseTimer); - // Fade out: transition the box-shadow away card.style.transition = 'box-shadow 0.3s ease, border-color 0.3s ease'; card.style.boxShadow = origBoxShadow; card.style.borderColor = origBorderColor; @@ -96,7 +108,6 @@ function _showDimOverlay(): HTMLElement { overlay.className = 'nav-dim-overlay'; document.body.appendChild(overlay); } - // Force reflow then activate void overlay.offsetHeight; overlay.classList.add('active'); return overlay; @@ -109,7 +120,7 @@ function _waitForCard(id: string): void { const card = document.querySelector(`[data-entity-id="${id}"]`); if (card) { observer.disconnect(); - setTimeout(() => _highlightCard(card as HTMLElement), 100); + setTimeout(() => _highlightCard(card as HTMLElement), 50); return; } if (Date.now() - start > WAIT_TIMEOUT) {