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">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import MdiIcon from './MdiIcon.svelte';
|
import MdiIcon from './MdiIcon.svelte';
|
||||||
|
import { requestHighlight } from '$lib/highlight';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
href,
|
href,
|
||||||
@@ -16,20 +17,17 @@
|
|||||||
title?: string;
|
title?: string;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const targetHref = $derived(
|
|
||||||
entityId != null
|
|
||||||
? `${href.split('?')[0]}?highlight=${entityId}${href.includes('?') ? '&' + href.split('?')[1] : ''}`
|
|
||||||
: href
|
|
||||||
);
|
|
||||||
|
|
||||||
function navigate(e: MouseEvent) {
|
function navigate(e: MouseEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
goto(targetHref);
|
if (entityId != null) {
|
||||||
|
requestHighlight(entityId);
|
||||||
|
}
|
||||||
|
goto(href);
|
||||||
}
|
}
|
||||||
</script>
|
</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} />
|
<MdiIcon name={icon} size={12} />
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,50 +1,63 @@
|
|||||||
/**
|
/**
|
||||||
* Card highlight system for cross-entity navigation.
|
* Card highlight system for cross-entity navigation.
|
||||||
*
|
*
|
||||||
* When navigating via CrossLink, the target entity ID is passed as ?highlight=<id>.
|
* CrossLink calls requestHighlight(id) before goto(), storing the ID globally.
|
||||||
* The destination page calls highlightFromUrl() after data loads, which:
|
* The destination page calls highlightFromUrl() after data loads, which
|
||||||
* 1. Shows a dim overlay behind everything
|
* picks up the pending ID and highlights the matching card.
|
||||||
* 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
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const HIGHLIGHT_DURATION = 2500;
|
const HIGHLIGHT_DURATION = 2500;
|
||||||
const PULSE_INTERVAL = 400;
|
const PULSE_INTERVAL = 400;
|
||||||
const WAIT_TIMEOUT = 5000;
|
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 {
|
export function highlightFromUrl(): void {
|
||||||
if (typeof window === 'undefined') return;
|
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;
|
if (!id) return;
|
||||||
|
|
||||||
// Clean the URL immediately (remove highlight param, keep others)
|
// Wait a tick for DOM to render after loaded=true
|
||||||
params.delete('highlight');
|
requestAnimationFrame(() => {
|
||||||
const qs = params.toString();
|
requestAnimationFrame(() => {
|
||||||
const cleanUrl = window.location.pathname + (qs ? '?' + qs : '');
|
const card = document.querySelector(`[data-entity-id="${id}"]`);
|
||||||
window.history.replaceState(null, '', cleanUrl);
|
if (card) {
|
||||||
|
_highlightCard(card as HTMLElement);
|
||||||
// Try to find the card now, or wait for it
|
} else {
|
||||||
const card = document.querySelector(`[data-entity-id="${id}"]`);
|
_waitForCard(id!);
|
||||||
if (card) {
|
}
|
||||||
// Small delay for layout to settle after loaded=true
|
});
|
||||||
setTimeout(() => _highlightCard(card as HTMLElement), 100);
|
});
|
||||||
} else {
|
|
||||||
_waitForCard(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _highlightCard(card: HTMLElement): void {
|
function _highlightCard(card: HTMLElement): void {
|
||||||
// Show dim overlay
|
|
||||||
const overlay = _showDimOverlay();
|
const overlay = _showDimOverlay();
|
||||||
|
|
||||||
// Scroll to card
|
|
||||||
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
|
||||||
// Save original styles
|
// Save originals
|
||||||
const origBoxShadow = card.style.boxShadow;
|
const origBoxShadow = card.style.boxShadow;
|
||||||
const origZIndex = card.style.zIndex;
|
const origZIndex = card.style.zIndex;
|
||||||
const origPosition = card.style.position;
|
const origPosition = card.style.position;
|
||||||
@@ -54,11 +67,11 @@ function _highlightCard(card: HTMLElement): void {
|
|||||||
card.style.position = 'relative';
|
card.style.position = 'relative';
|
||||||
card.style.zIndex = '11';
|
card.style.zIndex = '11';
|
||||||
|
|
||||||
// Get primary color from CSS variable
|
// Get primary color
|
||||||
const primary = getComputedStyle(document.documentElement)
|
const primary = getComputedStyle(document.documentElement)
|
||||||
.getPropertyValue('--color-primary').trim();
|
.getPropertyValue('--color-primary').trim();
|
||||||
|
|
||||||
// Pulse effect via interval
|
// Pulsing glow
|
||||||
let on = true;
|
let on = true;
|
||||||
const glowOn = `0 0 0 3px ${primary}, 0 0 24px ${primary}60`;
|
const glowOn = `0 0 0 3px ${primary}, 0 0 24px ${primary}60`;
|
||||||
const glowOff = `0 0 0 2px ${primary}80`;
|
const glowOff = `0 0 0 2px ${primary}80`;
|
||||||
@@ -74,7 +87,6 @@ function _highlightCard(card: HTMLElement): void {
|
|||||||
// Cleanup
|
// Cleanup
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
clearInterval(pulseTimer);
|
clearInterval(pulseTimer);
|
||||||
// Fade out: transition the box-shadow away
|
|
||||||
card.style.transition = 'box-shadow 0.3s ease, border-color 0.3s ease';
|
card.style.transition = 'box-shadow 0.3s ease, border-color 0.3s ease';
|
||||||
card.style.boxShadow = origBoxShadow;
|
card.style.boxShadow = origBoxShadow;
|
||||||
card.style.borderColor = origBorderColor;
|
card.style.borderColor = origBorderColor;
|
||||||
@@ -96,7 +108,6 @@ function _showDimOverlay(): HTMLElement {
|
|||||||
overlay.className = 'nav-dim-overlay';
|
overlay.className = 'nav-dim-overlay';
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
}
|
}
|
||||||
// Force reflow then activate
|
|
||||||
void overlay.offsetHeight;
|
void overlay.offsetHeight;
|
||||||
overlay.classList.add('active');
|
overlay.classList.add('active');
|
||||||
return overlay;
|
return overlay;
|
||||||
@@ -109,7 +120,7 @@ function _waitForCard(id: string): void {
|
|||||||
const card = document.querySelector(`[data-entity-id="${id}"]`);
|
const card = document.querySelector(`[data-entity-id="${id}"]`);
|
||||||
if (card) {
|
if (card) {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
setTimeout(() => _highlightCard(card as HTMLElement), 100);
|
setTimeout(() => _highlightCard(card as HTMLElement), 50);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Date.now() - start > WAIT_TIMEOUT) {
|
if (Date.now() - start > WAIT_TIMEOUT) {
|
||||||
|
|||||||
Reference in New Issue
Block a user