Add sticky ToC sidebar to document read view
Builds a table of contents client-side from rendered heading IDs. Highlights the current section on scroll. Collapses to inline on narrow screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -353,8 +353,75 @@ button:hover, .btn:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================
|
/* ===========================
|
||||||
Read view
|
Read view — two-column layout
|
||||||
=========================== */
|
=========================== */
|
||||||
|
.read-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 2rem 2rem 0;
|
||||||
|
}
|
||||||
|
.read-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 220px 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.read-main {
|
||||||
|
max-width: 900px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.read-toc {
|
||||||
|
position: sticky;
|
||||||
|
top: 1rem;
|
||||||
|
max-height: calc(100vh - 2rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-right: 1px solid var(--border-lt);
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
.read-toc ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.read-toc li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.read-toc a {
|
||||||
|
color: var(--text-lt);
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
padding: 0.125rem 0;
|
||||||
|
transition: color 0.1s;
|
||||||
|
}
|
||||||
|
.read-toc a:hover {
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.read-toc a.toc-active {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.read-container {
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
}
|
||||||
|
.read-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.read-toc {
|
||||||
|
position: static;
|
||||||
|
max-height: none;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border-lt);
|
||||||
|
padding-right: 0;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.read-header {
|
.read-header {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
{{define "title"}} — {{.Doc.Title}}{{end}}
|
{{define "title"}} — {{.Doc.Title}}{{end}}
|
||||||
|
{{define "container-class"}}read-container{{end}}
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
|
<div class="read-layout">
|
||||||
|
<nav class="read-toc" id="toc" aria-label="Table of contents"></nav>
|
||||||
|
<div class="read-main">
|
||||||
<div class="read-header">
|
<div class="read-header">
|
||||||
<h2>{{.Doc.Title}}</h2>
|
<h2>{{.Doc.Title}}</h2>
|
||||||
<div class="read-meta">
|
<div class="read-meta">
|
||||||
@@ -26,7 +30,46 @@
|
|||||||
<a href="/" class="btn-ghost btn btn-sm">Back to queue</a>
|
<a href="/" class="btn-ghost btn btn-sm">Back to queue</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card markdown-body">
|
<div class="card markdown-body" id="article">
|
||||||
{{.HTML}}
|
{{.HTML}}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var toc=document.getElementById("toc");
|
||||||
|
var headings=document.querySelectorAll("#article h1, #article h2, #article h3");
|
||||||
|
if(headings.length<2){toc.style.display="none";return;}
|
||||||
|
var ul=document.createElement("ul");
|
||||||
|
var minLevel=6;
|
||||||
|
headings.forEach(function(h){var l=parseInt(h.tagName[1]);if(l<minLevel)minLevel=l;});
|
||||||
|
headings.forEach(function(h){
|
||||||
|
if(!h.id)return;
|
||||||
|
var li=document.createElement("li");
|
||||||
|
var a=document.createElement("a");
|
||||||
|
a.href="#"+h.id;
|
||||||
|
a.textContent=h.textContent;
|
||||||
|
var depth=parseInt(h.tagName[1])-minLevel;
|
||||||
|
li.style.paddingLeft=(depth*0.75)+"rem";
|
||||||
|
li.appendChild(a);
|
||||||
|
ul.appendChild(li);
|
||||||
|
});
|
||||||
|
toc.appendChild(ul);
|
||||||
|
/* highlight current section on scroll */
|
||||||
|
var links=toc.querySelectorAll("a");
|
||||||
|
var ids=[];links.forEach(function(a){ids.push(a.getAttribute("href").slice(1));});
|
||||||
|
function onScroll(){
|
||||||
|
var current="";
|
||||||
|
for(var i=0;i<ids.length;i++){
|
||||||
|
var el=document.getElementById(ids[i]);
|
||||||
|
if(el&&el.getBoundingClientRect().top<=80)current=ids[i];
|
||||||
|
}
|
||||||
|
links.forEach(function(a){
|
||||||
|
a.classList.toggle("toc-active",a.getAttribute("href")==="#"+current);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.addEventListener("scroll",onScroll,{passive:true});
|
||||||
|
onScroll();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user