diff --git a/doc/style.css b/doc/style.css
index 9410d39e54be..c844d85e9fb7 100644
--- a/doc/style.css
+++ b/doc/style.css
@@ -156,8 +156,7 @@ body {
}
a {
- text-decoration: none;
- border-bottom: 1px solid;
+ text-decoration: underline;
color: var(--link-color);
}
@@ -521,6 +520,44 @@ nav.toc-sidebar li {
nav.toc-sidebar summary {
cursor: pointer;
+ display: flex;
+ align-items: center;
+ list-style: none;
+}
+
+nav.toc summary::before {
+ content: "▸";
+ flex: none;
+ font-size: 0.75em;
+}
+nav.toc details[open] > summary::before {
+ content: "▾";
+}
+
+nav.toc a {
+ display: block;
+ width: 100%;
+ padding: 3px 8px;
+ border-left: 3px solid transparent;
+ border-radius: 3px;
+ color: inherit;
+ text-decoration: none;
+ transition:
+ background-color 120ms ease,
+ border-color 120ms ease;
+}
+nav.toc a:hover {
+ background-color: #f2f2f2;
+}
+
+nav.toc a.active {
+ background-color: #d8d8d8;
+ border-left-color: #444;
+ font-weight: 600;
+}
+nav.toc a.active-trail {
+ border-left-color: #bbb;
+ background-color: #e8e8e8;
}
@media screen and (min-width: 768px) {
@@ -553,7 +590,6 @@ nav.toc-sidebar summary {
max-height: none;
overflow-y: auto;
border: none;
- border-right: 0.0625rem solid #d8d8d8;
}
/* Hide the toggle button on desktop */
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manual.py b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manual.py
index 9177b0ad5202..4ef5d68f2d88 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manual.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manual.py
@@ -384,6 +384,64 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer):
});
});
"""
+ intersection_observer_js = """
+function createObserver() {
+ const content = document.querySelector(".content");
+ const headings = content.querySelectorAll("h1, h2, h3, h4, h5, h6");
+
+ const links = new Map();
+ document.querySelectorAll("ol li a").forEach((a) => {
+ links.set(a.hash.slice(1), a);
+ });
+ const visible = new Set();
+
+ function setActive(link) {
+ document
+ .querySelectorAll("ol.toc a.active, ol.toc a.active-trail")
+ .forEach((a) => a.classList.remove("active", "active-trail"));
+
+ link.classList.add("active");
+
+ let li = link.closest("li")?.parentElement.closest("li");
+ for (; li; li = li.parentElement.closest("li")) {
+ li.querySelector(":scope > details > summary > a")?.classList.add(
+ "active-trail",
+ );
+ }
+ }
+
+ const handleIntersect = (entries) => {
+ for (const entry of entries) {
+ if (entry.isIntersecting) visible.add(entry.target);
+ else visible.delete(entry.target);
+ }
+
+ const current = [...visible]
+ .filter((h) => links.has(h.id))
+ .sort(
+ (a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top,
+ )[0];
+
+ if (!current) return;
+
+ const link = links.get(current.id);
+ if (link) setActive(link);
+ };
+
+ const observer = new IntersectionObserver(handleIntersect, {
+ root: content,
+ rootMargin: "0px 0px -70% 0px",
+ threshold: 0,
+ });
+
+ [...headings]
+ .filter((h) => h.id && links.has(h.id))
+ .forEach((h) => observer.observe(h));
+}
+
+document.addEventListener("DOMContentLoaded", createObserver);
+ """
+
header_content = ""
if self._html_params.header:
with open(self._html_params.header) as header_file:
@@ -403,6 +461,7 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer):
"".join((f''
for script in scripts)),
f"",
+ f"",
f' ',
f' ' if home.target.href() else "",
f' {up_link}{prev_link}{next_link}',