feat: add heading flash animation on hash change
This commit is contained in:
parent
d78d15d8ec
commit
a934901cce
|
|
@ -23,6 +23,7 @@ import {
|
|||
} from "./components";
|
||||
import { generateToc, type FileNode, type TocNode } from "./data-loader";
|
||||
import { JournalPanel } from "./components/journal";
|
||||
import { useHeadingFlash } from "./components/useHeadingFlash";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scroll container context – lets child components (FileTree, RevealManager)
|
||||
|
|
@ -70,6 +71,8 @@ const App: Component = () => {
|
|||
void loadToc();
|
||||
});
|
||||
|
||||
useHeadingFlash();
|
||||
|
||||
const handleSourceChanged = () => {
|
||||
setTocKey((k) => k + 1);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* useHeadingFlash — watches location.hash and applies a fading highlight
|
||||
* animation to the target heading element on every hash change.
|
||||
*
|
||||
* Handles page refresh / initial load by retrying up to ~500ms until the
|
||||
* target element exists in the DOM.
|
||||
*/
|
||||
|
||||
import { createEffect, onCleanup } from "solid-js";
|
||||
import { useLocation } from "@solidjs/router";
|
||||
|
||||
const FLASH_CLASS = "heading-flash";
|
||||
const MAX_RETRIES = 10;
|
||||
const RETRY_INTERVAL = 50;
|
||||
|
||||
function flashElement(el: HTMLElement) {
|
||||
// Re-trigger the animation: remove and re-add the class
|
||||
el.classList.remove(FLASH_CLASS);
|
||||
void el.offsetWidth; // force reflow
|
||||
el.classList.add(FLASH_CLASS);
|
||||
}
|
||||
|
||||
export function useHeadingFlash() {
|
||||
const location = useLocation();
|
||||
|
||||
createEffect(() => {
|
||||
const hash = location.hash;
|
||||
if (!hash) return;
|
||||
|
||||
const id = decodeURIComponent(hash.startsWith("#") ? hash.slice(1) : hash);
|
||||
if (!id) return;
|
||||
|
||||
let retries = 0;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
|
||||
const tryFlash = () => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
flashElement(el);
|
||||
el.addEventListener("animationend", function onEnd() {
|
||||
el.classList.remove(FLASH_CLASS);
|
||||
el.removeEventListener("animationend", onEnd);
|
||||
}, { once: true });
|
||||
} else if (retries < MAX_RETRIES) {
|
||||
retries++;
|
||||
timer = setTimeout(tryFlash, RETRY_INTERVAL);
|
||||
}
|
||||
};
|
||||
|
||||
// Delay slightly so Solid has a chance to flush DOM updates from
|
||||
// a concurrent SPA navigation before we look for the element.
|
||||
timer = setTimeout(tryFlash, 0);
|
||||
|
||||
onCleanup(() => clearTimeout(timer));
|
||||
});
|
||||
}
|
||||
|
|
@ -160,3 +160,14 @@ icon.big .icon-label-stroke {
|
|||
-webkit-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* heading flash highlight */
|
||||
|
||||
@keyframes heading-flash {
|
||||
0% { background-color: #fef08a; }
|
||||
100% { background-color: transparent; }
|
||||
}
|
||||
|
||||
.heading-flash {
|
||||
animation: heading-flash 1.5s ease-out;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue