0a001daff3
- raw-markdown.ts: Check path bounds before any filesystem probes to prevent leaking file existence info outside the pages directory - TableOfContents.tsx: Fix SSR hydration mismatch by moving window.location.hash into useEffect + state instead of reading it during render
84 lines
2.2 KiB
TypeScript
84 lines
2.2 KiB
TypeScript
import React, { useState, useEffect } from "react"
|
|
import Link from "next/link"
|
|
|
|
export function TableOfContents({ toc }) {
|
|
const items = toc.filter((item) => item.id && (item.level === 2 || item.level === 3))
|
|
const [activeHash, setActiveHash] = useState("")
|
|
|
|
useEffect(() => {
|
|
const updateHash = () => setActiveHash(window.location.hash)
|
|
updateHash()
|
|
window.addEventListener("hashchange", updateHash)
|
|
return () => window.removeEventListener("hashchange", updateHash)
|
|
}, [])
|
|
|
|
if (items.length <= 1) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<nav className="toc">
|
|
<ul className="flex column">
|
|
{items.map((item) => {
|
|
const href = `#${item.id}`
|
|
const active = activeHash === href
|
|
return (
|
|
<li
|
|
key={item.title}
|
|
className={[active ? "active" : undefined, item.level === 3 ? "padded" : undefined]
|
|
.filter(Boolean)
|
|
.join(" ")}
|
|
>
|
|
<Link href={href}>{item.title}</Link>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
<style jsx>
|
|
{`
|
|
nav {
|
|
position: sticky;
|
|
top: 0;
|
|
max-height: calc(100vh - var(--top-nav-height) - 6rem);
|
|
width: 100%;
|
|
align-self: flex-start;
|
|
margin-bottom: 1rem;
|
|
padding: 0.5rem 0 0;
|
|
border-left: 1px solid var(--border-color);
|
|
transition: border-color 0.2s ease;
|
|
overflow-y: auto;
|
|
}
|
|
ul {
|
|
margin: 0;
|
|
padding-left: 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
li {
|
|
list-style-type: none;
|
|
margin: 0 0 1rem;
|
|
}
|
|
li :global(a) {
|
|
text-decoration: none;
|
|
color: var(--text-secondary);
|
|
}
|
|
li :global(a:hover),
|
|
li.active :global(a) {
|
|
text-decoration: underline;
|
|
}
|
|
li.padded {
|
|
padding-left: 1rem;
|
|
}
|
|
|
|
/* Hide on tablet and mobile */
|
|
@media (max-width: 1024px) {
|
|
nav {
|
|
display: none;
|
|
}
|
|
}
|
|
`}
|
|
</style>
|
|
</nav>
|
|
)
|
|
}
|