113 lines
2.4 KiB
Text
113 lines
2.4 KiB
Text
---
|
|
interface Heading {
|
|
depth: number;
|
|
slug: string;
|
|
text: string;
|
|
}
|
|
|
|
interface Props {
|
|
headings: Heading[];
|
|
}
|
|
|
|
const { headings } = Astro.props;
|
|
---
|
|
|
|
<nav class="toc">
|
|
<p class="toc__label">Table of Content</p>
|
|
<ol class="toc__list">
|
|
{headings.map((h) => (
|
|
<li class={`toc__item toc__item--h${h.depth}`}>
|
|
<a href={`#${h.slug}`} class="toc__link">{h.text}</a>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</nav>
|
|
|
|
<style>
|
|
.toc {
|
|
/* sticky is handled by the parent .toc-sidebar */
|
|
}
|
|
|
|
.toc__label {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: #888;
|
|
margin-bottom: 0.75rem;
|
|
padding-bottom: 0.75rem;
|
|
border-top: 2px solid #111;
|
|
padding-top: 0.75rem;
|
|
}
|
|
|
|
.toc__list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
.toc__item--h3 {
|
|
padding-left: 0.85rem;
|
|
}
|
|
|
|
.toc__link {
|
|
font-size: 0.8rem;
|
|
color: #666;
|
|
text-decoration: none;
|
|
line-height: 1.4;
|
|
display: block;
|
|
}
|
|
|
|
.toc__link:hover {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.toc__link--active {
|
|
color: #111;
|
|
font-weight: 500;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
const links = document.querySelectorAll<HTMLAnchorElement>(".toc__link");
|
|
const headingIds = [...links].map((a) => a.getAttribute("href")?.slice(1) ?? "");
|
|
|
|
const headingEls = headingIds
|
|
.map((id) => document.getElementById(id))
|
|
.filter(Boolean) as HTMLElement[];
|
|
|
|
function update() {
|
|
const nearBottom =
|
|
window.scrollY + window.innerHeight >= document.body.scrollHeight - 50;
|
|
|
|
let activeId = headingIds[0] ?? "";
|
|
|
|
if (nearBottom) {
|
|
activeId = headingIds[headingIds.length - 1] ?? activeId;
|
|
} else {
|
|
const threshold = window.scrollY + window.innerHeight * 0.25;
|
|
for (const el of headingEls) {
|
|
if (el.offsetTop <= threshold) activeId = el.id;
|
|
}
|
|
}
|
|
|
|
links.forEach((a) => {
|
|
const href = a.getAttribute("href")?.slice(1);
|
|
a.classList.toggle("toc__link--active", href === activeId);
|
|
});
|
|
}
|
|
|
|
window.addEventListener("scroll", update, { passive: true });
|
|
update();
|
|
|
|
links.forEach((a) => {
|
|
a.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
const id = a.getAttribute("href")?.slice(1);
|
|
document.getElementById(id ?? "")?.scrollIntoView({ behavior: "smooth" });
|
|
});
|
|
});
|
|
</script>
|