feat: add heading flash animation on hash change
This commit is contained in:
parent
d78d15d8ec
commit
a934901cce
|
|
@ -23,6 +23,7 @@ import {
|
||||||
} from "./components";
|
} from "./components";
|
||||||
import { generateToc, type FileNode, type TocNode } from "./data-loader";
|
import { generateToc, type FileNode, type TocNode } from "./data-loader";
|
||||||
import { JournalPanel } from "./components/journal";
|
import { JournalPanel } from "./components/journal";
|
||||||
|
import { useHeadingFlash } from "./components/useHeadingFlash";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Scroll container context – lets child components (FileTree, RevealManager)
|
// Scroll container context – lets child components (FileTree, RevealManager)
|
||||||
|
|
@ -70,6 +71,8 @@ const App: Component = () => {
|
||||||
void loadToc();
|
void loadToc();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useHeadingFlash();
|
||||||
|
|
||||||
const handleSourceChanged = () => {
|
const handleSourceChanged = () => {
|
||||||
setTocKey((k) => k + 1);
|
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-user-select: none;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-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