sites: scroll-spy tracks scroll position, fires the last section on bottom

The IntersectionObserver-based spy only fired when a section entered a
narrow band 40–45% down the viewport. Short sections at the tail of a
page — Operator key and Roadmap on theseus.x/extensions, whatever came
last on the other pages — never made it into that band because the
page runs out of scroll before their top crosses 40%, so their sidebar
links stayed dim no matter how far the reader scrolled.

Rewrote the spy across every page with a sidebar (theseus.x home /
plug-ins / extensions and sirius.x home / tld / theseus / docs / admin
/ brand): it listens to window scroll, picks the last section whose
top has crossed 35% of the viewport, and — when the window is scrolled
to within 4px of the bottom — snaps to the final section regardless of
where its top is. Every link in the rail can now activate.

theseus.x/extensions also had the spy IIFE bundled with the manifest
loader in one <script> block near the middle of the page, so at run
time the parser hadn't yet reached the last three sections and only
Bundled/Community/Build were ever registered. Moved the spy to its own
<script> just before </body> so it sees every section that ships.
This commit is contained in:
Local Dev 2026-09-20 19:20:30 +02:00
parent 0e8e1949df
commit 172d16e84b
3 changed files with 686 additions and 655 deletions

View file

@ -260,23 +260,6 @@
</div>`).join(""); </div>`).join("");
} catch (e) { box.innerHTML = '<div class="ext" style="color:var(--dim)">Catalog unavailable right now.</div>'; } } catch (e) { box.innerHTML = '<div class="ext" style="color:var(--dim)">Catalog unavailable right now.</div>'; }
})(); })();
// Sidenav scroll-spy: highlight the section currently in view.
(() => {
const links = document.querySelectorAll(".sidenav a[href^='#']");
if (!links.length) return;
const map = new Map();
links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) map.set(el, a); });
const io = new IntersectionObserver((entries) => {
entries.forEach((en) => {
if (!en.isIntersecting) return;
const a = map.get(en.target); if (!a) return;
links.forEach((l) => l.classList.remove("active"));
a.classList.add("active");
});
}, { rootMargin: "-40% 0px -55% 0px", threshold: 0 });
map.forEach((_, el) => io.observe(el));
})();
</script> </script>
<section id="updates"> <section id="updates">
@ -318,5 +301,33 @@
</div> </div>
<script>
// Sidenav scroll-spy: highlight the section currently in view.
// Uses scroll-position tracking so short sections at the end of the page
// still activate their link when the reader scrolls to them.
(() => {
const links = document.querySelectorAll(".sidenav a[href^='#']");
if (!links.length) return;
const entries = [];
links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) entries.push({ el, link: a }); });
if (!entries.length) return;
const setActive = (link) => { links.forEach((l) => l.classList.remove("active")); link.classList.add("active"); };
const update = () => {
const doc = document.documentElement;
const scrollBottom = window.scrollY + window.innerHeight;
if (scrollBottom >= (doc.scrollHeight - 4)) { setActive(entries[entries.length - 1].link); return; }
const line = window.scrollY + window.innerHeight * 0.35;
let active = entries[0].link;
for (const e of entries) {
if (e.el.getBoundingClientRect().top + window.scrollY <= line) active = e.link;
else break;
}
setActive(active);
};
window.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
update();
})();
</script>
</body> </body>
</html> </html>

View file

@ -375,20 +375,30 @@ Expected:
})(); })();
// Sidenav scroll-spy: highlight the section currently in view. // Sidenav scroll-spy: highlight the section currently in view.
// Uses scroll-position tracking so short sections at the end of the page
// still activate their link when the reader scrolls to them.
(() => { (() => {
const links = document.querySelectorAll(".sidenav a[href^='#']"); const links = document.querySelectorAll(".sidenav a[href^='#']");
if (!links.length) return; if (!links.length) return;
const map = new Map(); const entries = [];
links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) map.set(el, a); }); links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) entries.push({ el, link: a }); });
const io = new IntersectionObserver((entries) => { if (!entries.length) return;
entries.forEach((en) => { const setActive = (link) => { links.forEach((l) => l.classList.remove("active")); link.classList.add("active"); };
if (!en.isIntersecting) return; const update = () => {
const a = map.get(en.target); if (!a) return; const doc = document.documentElement;
links.forEach((l) => l.classList.remove("active")); const scrollBottom = window.scrollY + window.innerHeight;
a.classList.add("active"); if (scrollBottom >= (doc.scrollHeight - 4)) { setActive(entries[entries.length - 1].link); return; }
}); const line = window.scrollY + window.innerHeight * 0.35;
}, { rootMargin: "-40% 0px -55% 0px", threshold: 0 }); let active = entries[0].link;
map.forEach((_, el) => io.observe(el)); for (const e of entries) {
if (e.el.getBoundingClientRect().top + window.scrollY <= line) active = e.link;
else break;
}
setActive(active);
};
window.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
update();
})(); })();
</script> </script>
</body> </body>

View file

@ -223,20 +223,30 @@
})(); })();
// Sidenav scroll-spy: highlight the section currently in view. // Sidenav scroll-spy: highlight the section currently in view.
// Uses scroll-position tracking so short sections at the end of the page
// still activate their link when the reader scrolls to them.
(() => { (() => {
const links = document.querySelectorAll(".sidenav a[href^='#']"); const links = document.querySelectorAll(".sidenav a[href^='#']");
if (!links.length) return; if (!links.length) return;
const map = new Map(); const entries = [];
links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) map.set(el, a); }); links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) entries.push({ el, link: a }); });
const io = new IntersectionObserver((entries) => { if (!entries.length) return;
entries.forEach((en) => { const setActive = (link) => { links.forEach((l) => l.classList.remove("active")); link.classList.add("active"); };
if (!en.isIntersecting) return; const update = () => {
const a = map.get(en.target); if (!a) return; const doc = document.documentElement;
links.forEach((l) => l.classList.remove("active")); const scrollBottom = window.scrollY + window.innerHeight;
a.classList.add("active"); if (scrollBottom >= (doc.scrollHeight - 4)) { setActive(entries[entries.length - 1].link); return; }
}); const line = window.scrollY + window.innerHeight * 0.35;
}, { rootMargin: "-40% 0px -55% 0px", threshold: 0 }); let active = entries[0].link;
map.forEach((_, el) => io.observe(el)); for (const e of entries) {
if (e.el.getBoundingClientRect().top + window.scrollY <= line) active = e.link;
else break;
}
setActive(active);
};
window.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
update();
})(); })();
</script> </script>
</body> </body>