blog.z0x.ca/src/components/TableOfContents.astro
z0x 36870785bc
All checks were successful
build dist / build-dist (push) Successful in 32s
refactor: biome lint
2025-04-24 22:12:22 -04:00

103 lines
No EOL
2.8 KiB
Text

---
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import type { MarkdownHeading } from "astro";
import { Icon } from "astro-icon/components";
type Props = {
headings: MarkdownHeading[];
};
const { headings } = Astro.props;
function getHeadingMargin(depth: number): string {
const margins: Record<number, string> = {
3: "ml-4",
4: "ml-8",
5: "ml-12",
6: "ml-16",
};
return margins[depth] || "";
}
---
<details
open
class="group col-start-2 rounded-xl border p-4 xl:sticky xl:top-20 xl:col-start-1 xl:mr-8 xl:ml-auto xl:h-[calc(100vh-5rem)] xl:max-w-fit xl:rounded-none xl:border-none xl:p-0"
>
<summary
class="flex cursor-pointer items-center justify-between text-xl font-medium group-open:pb-4 xl:hidden"
>
<span>Table of Contents</span>
<Icon
name="lucide:chevron-down"
class="size-5 shrink-0 transition-transform group-open:rotate-180"
/>
</summary>
<ScrollArea
client:load
className="flex max-h-64 flex-col overflow-y-auto xl:max-h-[calc(100vh-8rem)]"
type="always"
>
<ul
class="flex list-none flex-col gap-y-2 px-4 xl:mr-8"
id="table-of-contents"
>
<li class="hidden text-lg font-medium xl:block">Table of Contents</li>
{
headings.map((heading) => (
<li
class={cn(
'text-foreground/60 px-4 text-sm xl:p-0',
getHeadingMargin(heading.depth),
)}
>
<a
href={`#${heading.slug}`}
class="marker:text-foreground/30 list-item list-disc underline decoration-transparent underline-offset-[3px] transition-colors duration-200 hover:decoration-inherit xl:list-none"
>
{heading.text}
</a>
</li>
))
}
</ul>
</ScrollArea>
</details>
<script>
function setupToc() {
const header = document.querySelector('header')
const headerHeight = header ? header.offsetHeight : 0
const observer = new IntersectionObserver(
(sections) => {
sections.forEach((section) => {
const heading = section.target.querySelector('h2, h3, h4, h5, h6')
if (!heading) return
const id = heading.getAttribute('id')
const link = document.querySelector(
`#table-of-contents li a[href="#${id}"]`,
)
if (!link) return
const addRemove = section.isIntersecting ? 'add' : 'remove'
link.classList[addRemove]('text-foreground')
})
},
{
rootMargin: `-${headerHeight}px 0px 0px 0px`,
},
)
const sections = document.querySelectorAll('.prose section')
sections.forEach((section) => {
observer.observe(section)
})
}
document.addEventListener('astro:page-load', setupToc)
document.addEventListener('astro:after-swap', setupToc)
</script>