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.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import MdiIcon from './MdiIcon.svelte';
|
||||
import { requestHighlight } from '$lib/highlight';
|
||||
|
||||
let {
|
||||
href,
|
||||
@@ -16,20 +17,17 @@
|
||||
title?: string;
|
||||
} = $props();
|
||||
|
||||
const targetHref = $derived(
|
||||
entityId != null
|
||||
? `${href.split('?')[0]}?highlight=${entityId}${href.includes('?') ? '&' + href.split('?')[1] : ''}`
|
||||
: href
|
||||
);
|
||||
|
||||
function navigate(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
goto(targetHref);
|
||||
if (entityId != null) {
|
||||
requestHighlight(entityId);
|
||||
}
|
||||
goto(href);
|
||||
}
|
||||
</script>
|
||||
|
||||
<a href={targetHref} class="crosslink" title={title || label} onclick={navigate}>
|
||||
<a {href} class="crosslink" title={title || label} onclick={navigate}>
|
||||
<MdiIcon name={icon} size={12} />
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
|
||||
@@ -1,50 +1,63 @@
|
||||
/**
|
||||
* Card highlight system for cross-entity navigation.
|
||||
*
|
||||
* When navigating via CrossLink, the target entity ID is passed as ?highlight=<id>.
|
||||
* The destination page calls highlightFromUrl() after data loads, which:
|
||||
* 1. Shows a dim overlay behind everything
|
||||
* 2. Finds the card with [data-entity-id="<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) {
|
||||
|
||||
Reference in New Issue
Block a user