whitepaper-html: left-rail TOC built at runtime from section ids
inject-whitepaper-css.py adds CSS for a fixed 280px left sidebar plus a DOMContentLoaded script that walks `section[id] > h2, > h3` and builds a chapter/section nav. IntersectionObserver tracks active section as you scroll; the rail scrolls itself to keep the current entry in view. Below 1100px the rail hides behind a hamburger toggle with backdrop dismiss. Cancels docutils responsive.css `body > *` blanket padding on the rail via the same `all: revert` pattern already used for the uncloseai floating button. 59/59 source sections present in rendered DOM.
This commit is contained in:
parent
28a76631e2
commit
d9f4e38222
2 changed files with 446 additions and 0 deletions
|
|
@ -76,6 +76,122 @@ main img, section img {{
|
|||
object-fit: contain;
|
||||
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.6);
|
||||
}}
|
||||
/* Left-rail table of contents. Cancels the same docutils responsive.css
|
||||
`body > *` blanket rule that broke the uncloseai button (padding /
|
||||
margin / max-width / background / line-height) and then lays the
|
||||
sidebar out as a fixed left rail on wide viewports. Below 1100px the
|
||||
sidebar collapses behind a toggle button so it cannot crowd the
|
||||
single-column reading flow on phones and narrow laptops. */
|
||||
body > .wp-toc {{
|
||||
all: revert;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 280px;
|
||||
padding: 1.25rem 1rem 1.25rem 1.5rem;
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
border-right: 1px solid #e5e5e5;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
z-index: 50;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica,
|
||||
Arial, sans-serif;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.4;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
.wp-toc-title {{
|
||||
font-family: 'chunkfive', Georgia, serif;
|
||||
font-size: 1.15rem;
|
||||
margin: 0 0 0.6rem 0;
|
||||
color: #227842;
|
||||
letter-spacing: 0.02em;
|
||||
}}
|
||||
.wp-toc-title .period {{ color: #227842; }}
|
||||
.wp-toc ol {{
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}}
|
||||
.wp-toc li {{
|
||||
margin: 0;
|
||||
}}
|
||||
.wp-toc a {{
|
||||
display: block;
|
||||
padding: 0.22rem 0.4rem;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
border-left: 2px solid transparent;
|
||||
border-radius: 0 3px 3px 0;
|
||||
word-wrap: break-word;
|
||||
}}
|
||||
.wp-toc a:hover {{
|
||||
background: #efefef;
|
||||
color: #000;
|
||||
}}
|
||||
.wp-toc a.active {{
|
||||
background: #e8f1ec;
|
||||
color: #155c30;
|
||||
border-left-color: #227842;
|
||||
font-weight: 600;
|
||||
}}
|
||||
.wp-toc li.lvl-3 a {{
|
||||
padding-left: 1.2rem;
|
||||
font-size: 0.78rem;
|
||||
color: #555;
|
||||
}}
|
||||
.wp-toc li.lvl-3 a.active {{
|
||||
color: #155c30;
|
||||
}}
|
||||
/* Toggle button — hidden on wide viewports, shown when the sidebar
|
||||
collapses. Same `all: revert` trick so docutils does not stretch it. */
|
||||
body > .wp-toc-toggle {{
|
||||
all: revert;
|
||||
position: fixed;
|
||||
top: 0.75rem;
|
||||
left: 0.75rem;
|
||||
z-index: 51;
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
border: 1px solid #cfcfcf;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
color: #227842;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
}}
|
||||
body > .wp-toc-backdrop {{
|
||||
all: revert;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
z-index: 49;
|
||||
display: none;
|
||||
}}
|
||||
@media (min-width: 1100px) {{
|
||||
/* Push the centered docutils column right so it never slides under
|
||||
the rail. The blanket `body > *` rule already centers content with
|
||||
huge calc() padding, so we just add a left margin to body. */
|
||||
body {{ margin-left: 280px; }}
|
||||
/* The uncloseai floating button is `position: fixed; left: ...`, so
|
||||
it sits independent of body margin. No further adjustment. */
|
||||
}}
|
||||
@media (max-width: 1099px) {{
|
||||
body > .wp-toc {{
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.2s ease-out;
|
||||
width: min(85vw, 320px);
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.12);
|
||||
}}
|
||||
body > .wp-toc.open {{ transform: translateX(0); }}
|
||||
body > .wp-toc-toggle {{ display: block; }}
|
||||
body > .wp-toc-backdrop.open {{ display: block; }}
|
||||
}}
|
||||
</style>
|
||||
<script>
|
||||
(function () {{
|
||||
|
|
@ -103,6 +219,113 @@ main img, section img {{
|
|||
}});
|
||||
}});
|
||||
}})();
|
||||
(function () {{
|
||||
if (window.__wpToc) return;
|
||||
window.__wpToc = true;
|
||||
document.addEventListener("DOMContentLoaded", function () {{
|
||||
var main = document.querySelector("main");
|
||||
if (!main) return;
|
||||
// Pick up only the top two heading levels — section-id <section>s
|
||||
// that wrap <h2> (chapters) and <h3> (sections). Anything deeper
|
||||
// creates noise in a left rail. Skip the doc title <h1> since the
|
||||
// sidebar header already labels the doc.
|
||||
var sections = main.querySelectorAll("section[id] > h2, section[id] > h3");
|
||||
if (sections.length < 3) return;
|
||||
var nav = document.createElement("nav");
|
||||
nav.className = "wp-toc";
|
||||
nav.setAttribute("aria-label", "Table of contents");
|
||||
var title = document.createElement("p");
|
||||
title.className = "wp-toc-title";
|
||||
title.innerHTML = 'lumbda<span class="period" aria-hidden="true">.</span>';
|
||||
nav.appendChild(title);
|
||||
var list = document.createElement("ol");
|
||||
var byId = {{}};
|
||||
sections.forEach(function (h) {{
|
||||
var sec = h.parentElement;
|
||||
var id = sec.id;
|
||||
if (!id) return;
|
||||
var lvl = h.tagName === "H2" ? 2 : 3;
|
||||
var li = document.createElement("li");
|
||||
li.className = "lvl-" + lvl;
|
||||
var a = document.createElement("a");
|
||||
a.href = "#" + id;
|
||||
a.textContent = h.textContent.trim();
|
||||
li.appendChild(a);
|
||||
list.appendChild(li);
|
||||
byId[id] = a;
|
||||
}});
|
||||
nav.appendChild(list);
|
||||
var backdrop = document.createElement("div");
|
||||
backdrop.className = "wp-toc-backdrop";
|
||||
var toggle = document.createElement("button");
|
||||
toggle.className = "wp-toc-toggle";
|
||||
toggle.setAttribute("aria-label", "Open table of contents");
|
||||
toggle.textContent = "☰"; // hamburger
|
||||
function close() {{
|
||||
nav.classList.remove("open");
|
||||
backdrop.classList.remove("open");
|
||||
}}
|
||||
function open() {{
|
||||
nav.classList.add("open");
|
||||
backdrop.classList.add("open");
|
||||
}}
|
||||
toggle.addEventListener("click", function () {{
|
||||
if (nav.classList.contains("open")) close(); else open();
|
||||
}});
|
||||
backdrop.addEventListener("click", close);
|
||||
// Auto-close after navigating from a link on narrow viewports.
|
||||
nav.addEventListener("click", function (e) {{
|
||||
if (e.target.tagName === "A" && window.innerWidth < 1100) close();
|
||||
}});
|
||||
document.body.insertBefore(nav, document.body.firstChild);
|
||||
document.body.appendChild(backdrop);
|
||||
document.body.appendChild(toggle);
|
||||
// Active-section tracking. IntersectionObserver picks the
|
||||
// highest-up section currently in view; falls back to a scroll
|
||||
// handler when IO is unavailable (very old browsers).
|
||||
var active = null;
|
||||
function setActive(id) {{
|
||||
if (active === id) return;
|
||||
if (active && byId[active]) byId[active].classList.remove("active");
|
||||
active = id;
|
||||
if (byId[id]) {{
|
||||
byId[id].classList.add("active");
|
||||
// Keep the active link in view inside the rail.
|
||||
var rect = byId[id].getBoundingClientRect();
|
||||
var navRect = nav.getBoundingClientRect();
|
||||
if (rect.top < navRect.top || rect.bottom > navRect.bottom) {{
|
||||
byId[id].scrollIntoView({{ block: "nearest" }});
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
if ("IntersectionObserver" in window) {{
|
||||
var visible = new Set();
|
||||
var io = new IntersectionObserver(function (entries) {{
|
||||
entries.forEach(function (e) {{
|
||||
if (e.isIntersecting) visible.add(e.target.id);
|
||||
else visible.delete(e.target.id);
|
||||
}});
|
||||
// Pick the visible section that appears earliest in the doc.
|
||||
var first = null;
|
||||
sections.forEach(function (h) {{
|
||||
var sid = h.parentElement.id;
|
||||
if (visible.has(sid) && first === null) first = sid;
|
||||
}});
|
||||
if (first) setActive(first);
|
||||
}}, {{ rootMargin: "-80px 0px -60% 0px", threshold: 0 }});
|
||||
sections.forEach(function (h) {{ io.observe(h.parentElement); }});
|
||||
}} else {{
|
||||
window.addEventListener("scroll", function () {{
|
||||
var y = window.scrollY + 100;
|
||||
var current = null;
|
||||
sections.forEach(function (h) {{
|
||||
if (h.parentElement.offsetTop <= y) current = h.parentElement.id;
|
||||
}});
|
||||
if (current) setActive(current);
|
||||
}}, {{ passive: true }});
|
||||
}}
|
||||
}});
|
||||
}})();
|
||||
</script>
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -863,6 +863,122 @@ main img, section img {
|
|||
object-fit: contain;
|
||||
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
/* Left-rail table of contents. Cancels the same docutils responsive.css
|
||||
`body > *` blanket rule that broke the uncloseai button (padding /
|
||||
margin / max-width / background / line-height) and then lays the
|
||||
sidebar out as a fixed left rail on wide viewports. Below 1100px the
|
||||
sidebar collapses behind a toggle button so it cannot crowd the
|
||||
single-column reading flow on phones and narrow laptops. */
|
||||
body > .wp-toc {
|
||||
all: revert;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 280px;
|
||||
padding: 1.25rem 1rem 1.25rem 1.5rem;
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
border-right: 1px solid #e5e5e5;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
z-index: 50;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica,
|
||||
Arial, sans-serif;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.4;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.wp-toc-title {
|
||||
font-family: 'chunkfive', Georgia, serif;
|
||||
font-size: 1.15rem;
|
||||
margin: 0 0 0.6rem 0;
|
||||
color: #227842;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.wp-toc-title .period { color: #227842; }
|
||||
.wp-toc ol {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.wp-toc li {
|
||||
margin: 0;
|
||||
}
|
||||
.wp-toc a {
|
||||
display: block;
|
||||
padding: 0.22rem 0.4rem;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
border-left: 2px solid transparent;
|
||||
border-radius: 0 3px 3px 0;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.wp-toc a:hover {
|
||||
background: #efefef;
|
||||
color: #000;
|
||||
}
|
||||
.wp-toc a.active {
|
||||
background: #e8f1ec;
|
||||
color: #155c30;
|
||||
border-left-color: #227842;
|
||||
font-weight: 600;
|
||||
}
|
||||
.wp-toc li.lvl-3 a {
|
||||
padding-left: 1.2rem;
|
||||
font-size: 0.78rem;
|
||||
color: #555;
|
||||
}
|
||||
.wp-toc li.lvl-3 a.active {
|
||||
color: #155c30;
|
||||
}
|
||||
/* Toggle button — hidden on wide viewports, shown when the sidebar
|
||||
collapses. Same `all: revert` trick so docutils does not stretch it. */
|
||||
body > .wp-toc-toggle {
|
||||
all: revert;
|
||||
position: fixed;
|
||||
top: 0.75rem;
|
||||
left: 0.75rem;
|
||||
z-index: 51;
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
border: 1px solid #cfcfcf;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
color: #227842;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
body > .wp-toc-backdrop {
|
||||
all: revert;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
z-index: 49;
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
/* Push the centered docutils column right so it never slides under
|
||||
the rail. The blanket `body > *` rule already centers content with
|
||||
huge calc() padding, so we just add a left margin to body. */
|
||||
body { margin-left: 280px; }
|
||||
/* The uncloseai floating button is `position: fixed; left: ...`, so
|
||||
it sits independent of body margin. No further adjustment. */
|
||||
}
|
||||
@media (max-width: 1099px) {
|
||||
body > .wp-toc {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.2s ease-out;
|
||||
width: min(85vw, 320px);
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
body > .wp-toc.open { transform: translateX(0); }
|
||||
body > .wp-toc-toggle { display: block; }
|
||||
body > .wp-toc-backdrop.open { display: block; }
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
|
|
@ -890,6 +1006,113 @@ main img, section img {
|
|||
});
|
||||
});
|
||||
})();
|
||||
(function () {
|
||||
if (window.__wpToc) return;
|
||||
window.__wpToc = true;
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var main = document.querySelector("main");
|
||||
if (!main) return;
|
||||
// Pick up only the top two heading levels — section-id <section>s
|
||||
// that wrap <h2> (chapters) and <h3> (sections). Anything deeper
|
||||
// creates noise in a left rail. Skip the doc title <h1> since the
|
||||
// sidebar header already labels the doc.
|
||||
var sections = main.querySelectorAll("section[id] > h2, section[id] > h3");
|
||||
if (sections.length < 3) return;
|
||||
var nav = document.createElement("nav");
|
||||
nav.className = "wp-toc";
|
||||
nav.setAttribute("aria-label", "Table of contents");
|
||||
var title = document.createElement("p");
|
||||
title.className = "wp-toc-title";
|
||||
title.innerHTML = 'lumbda<span class="period" aria-hidden="true">.</span>';
|
||||
nav.appendChild(title);
|
||||
var list = document.createElement("ol");
|
||||
var byId = {};
|
||||
sections.forEach(function (h) {
|
||||
var sec = h.parentElement;
|
||||
var id = sec.id;
|
||||
if (!id) return;
|
||||
var lvl = h.tagName === "H2" ? 2 : 3;
|
||||
var li = document.createElement("li");
|
||||
li.className = "lvl-" + lvl;
|
||||
var a = document.createElement("a");
|
||||
a.href = "#" + id;
|
||||
a.textContent = h.textContent.trim();
|
||||
li.appendChild(a);
|
||||
list.appendChild(li);
|
||||
byId[id] = a;
|
||||
});
|
||||
nav.appendChild(list);
|
||||
var backdrop = document.createElement("div");
|
||||
backdrop.className = "wp-toc-backdrop";
|
||||
var toggle = document.createElement("button");
|
||||
toggle.className = "wp-toc-toggle";
|
||||
toggle.setAttribute("aria-label", "Open table of contents");
|
||||
toggle.textContent = "☰"; // hamburger
|
||||
function close() {
|
||||
nav.classList.remove("open");
|
||||
backdrop.classList.remove("open");
|
||||
}
|
||||
function open() {
|
||||
nav.classList.add("open");
|
||||
backdrop.classList.add("open");
|
||||
}
|
||||
toggle.addEventListener("click", function () {
|
||||
if (nav.classList.contains("open")) close(); else open();
|
||||
});
|
||||
backdrop.addEventListener("click", close);
|
||||
// Auto-close after navigating from a link on narrow viewports.
|
||||
nav.addEventListener("click", function (e) {
|
||||
if (e.target.tagName === "A" && window.innerWidth < 1100) close();
|
||||
});
|
||||
document.body.insertBefore(nav, document.body.firstChild);
|
||||
document.body.appendChild(backdrop);
|
||||
document.body.appendChild(toggle);
|
||||
// Active-section tracking. IntersectionObserver picks the
|
||||
// highest-up section currently in view; falls back to a scroll
|
||||
// handler when IO is unavailable (very old browsers).
|
||||
var active = null;
|
||||
function setActive(id) {
|
||||
if (active === id) return;
|
||||
if (active && byId[active]) byId[active].classList.remove("active");
|
||||
active = id;
|
||||
if (byId[id]) {
|
||||
byId[id].classList.add("active");
|
||||
// Keep the active link in view inside the rail.
|
||||
var rect = byId[id].getBoundingClientRect();
|
||||
var navRect = nav.getBoundingClientRect();
|
||||
if (rect.top < navRect.top || rect.bottom > navRect.bottom) {
|
||||
byId[id].scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("IntersectionObserver" in window) {
|
||||
var visible = new Set();
|
||||
var io = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (e) {
|
||||
if (e.isIntersecting) visible.add(e.target.id);
|
||||
else visible.delete(e.target.id);
|
||||
});
|
||||
// Pick the visible section that appears earliest in the doc.
|
||||
var first = null;
|
||||
sections.forEach(function (h) {
|
||||
var sid = h.parentElement.id;
|
||||
if (visible.has(sid) && first === null) first = sid;
|
||||
});
|
||||
if (first) setActive(first);
|
||||
}, { rootMargin: "-80px 0px -60% 0px", threshold: 0 });
|
||||
sections.forEach(function (h) { io.observe(h.parentElement); });
|
||||
} else {
|
||||
window.addEventListener("scroll", function () {
|
||||
var y = window.scrollY + 100;
|
||||
var current = null;
|
||||
sections.forEach(function (h) {
|
||||
if (h.parentElement.offsetTop <= y) current = h.parentElement.id;
|
||||
});
|
||||
if (current) setActive(current);
|
||||
}, { passive: true });
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue