fix: search palette triggers highlight, restore CSS keyframe blink

- SearchPalette now calls requestHighlight(id) before goto()
- Restore smooth CSS @keyframes cardHighlight (0%→none, 25%→glow,
  75%→glow, 100%→none) instead of JS interval pulse
- Inline style.animation overrides class-based stagger animation;
  cleanup sets animation:'none' (inline beats class, no stagger replay)
This commit is contained in:
2026-03-22 00:13:53 +03:00
parent 88e21e41e2
commit 86115f5c75
3 changed files with 25 additions and 45 deletions
+7 -1
View File
@@ -184,7 +184,13 @@ a:focus-visible {
font-family: var(--font-mono); font-family: var(--font-mono);
} }
/* Card highlight dim overlay for cross-entity navigation */ /* Card highlight for cross-entity navigation */
@keyframes cardHighlight {
0%, 100% { box-shadow: none; }
25%, 75% { box-shadow: 0 0 0 3px var(--color-primary), 0 0 20px color-mix(in srgb, var(--color-primary) 30%, transparent); }
}
/* Dim overlay behind highlighted card */
.nav-dim-overlay { .nav-dim-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -2,6 +2,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { t } from '$lib/i18n'; import { t } from '$lib/i18n';
import MdiIcon from './MdiIcon.svelte'; import MdiIcon from './MdiIcon.svelte';
import { requestHighlight } from '$lib/highlight';
import { import {
fetchAllCaches, fetchAllCaches,
providersCache, providersCache,
@@ -140,6 +141,7 @@
function navigateTo(result: SearchResult) { function navigateTo(result: SearchResult) {
closePalette(); closePalette();
requestHighlight(result.id);
goto(result.href); goto(result.href);
} }
+16 -44
View File
@@ -1,19 +1,19 @@
/** /**
* Card highlight system for cross-entity navigation. * Card highlight system for cross-entity navigation.
* *
* CrossLink calls requestHighlight(id) before goto(), storing the ID globally. * CrossLink/SearchPalette call requestHighlight(id) before goto().
* The destination page calls highlightFromUrl() after data loads, which * The destination page calls highlightFromUrl() after data loads, which
* picks up the pending ID and highlights the matching card. * picks up the pending ID and highlights the matching card with a
* smooth CSS keyframe glow + dim overlay.
*/ */
const HIGHLIGHT_DURATION = 2500; const HIGHLIGHT_DURATION = 2000;
const PULSE_INTERVAL = 400;
const WAIT_TIMEOUT = 5000; const WAIT_TIMEOUT = 5000;
/** Pending highlight ID — set by CrossLink before navigation. */ /** Pending highlight ID — set before navigation. */
let _pendingHighlight: string | null = null; let _pendingHighlight: string | null = null;
/** Request a card highlight. Called by CrossLink before goto(). */ /** Request a card highlight. Called before goto(). */
export function requestHighlight(id: string | number): void { export function requestHighlight(id: string | number): void {
_pendingHighlight = String(id); _pendingHighlight = String(id);
} }
@@ -39,7 +39,7 @@ export function highlightFromUrl(): void {
if (!id) return; if (!id) return;
// Wait a tick for DOM to render after loaded=true // Wait for DOM to render after loaded=true
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const card = document.querySelector(`[data-entity-id="${id}"]`); const card = document.querySelector(`[data-entity-id="${id}"]`);
@@ -55,49 +55,21 @@ export function highlightFromUrl(): void {
function _highlightCard(card: HTMLElement): void { function _highlightCard(card: HTMLElement): void {
const overlay = _showDimOverlay(); const overlay = _showDimOverlay();
// Scroll to card
card.scrollIntoView({ behavior: 'smooth', block: 'center' }); card.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Save originals // Apply highlight via inline style (overrides stagger CSS class animation)
const origBoxShadow = card.style.boxShadow; card.style.animation = 'cardHighlight 2s ease-in-out';
const origZIndex = card.style.zIndex;
const origPosition = card.style.position;
const origBorderColor = card.style.borderColor;
// Elevate above overlay
card.style.position = 'relative'; card.style.position = 'relative';
card.style.zIndex = '11'; card.style.zIndex = '11';
// Get primary color // Cleanup: set animation to 'none' (inline beats class, prevents stagger replay)
const primary = getComputedStyle(document.documentElement)
.getPropertyValue('--color-primary').trim();
// 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`;
card.style.boxShadow = glowOn;
card.style.borderColor = primary;
const pulseTimer = setInterval(() => {
on = !on;
card.style.boxShadow = on ? glowOn : glowOff;
}, PULSE_INTERVAL);
// Cleanup
setTimeout(() => { setTimeout(() => {
clearInterval(pulseTimer); card.style.animation = 'none';
card.style.transition = 'box-shadow 0.3s ease, border-color 0.3s ease'; card.style.removeProperty('position');
card.style.boxShadow = origBoxShadow; card.style.removeProperty('z-index');
card.style.borderColor = origBorderColor; overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 300);
setTimeout(() => {
card.style.position = origPosition;
card.style.zIndex = origZIndex;
card.style.removeProperty('transition');
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 300);
}, 300);
}, HIGHLIGHT_DURATION); }, HIGHLIGHT_DURATION);
} }