103 lines
No EOL
2.8 KiB
Text
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> |