all repos — website @ bf2ab39d221ebabd17a9fab96f439ac7168f1b5e

My official website.

src/components/TableOfContents.astro (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
---
import { Icon } from 'astro-icon/components'
import TableOfContentsHeading from './TableOfContentsHeading.astro'

export interface Heading {
  depth: number
  slug: string
  text: string
  subheadings: Heading[]
}

const { headings } = Astro.props
const toc = buildToc(headings)

function buildToc(headings: Heading[]): Heading[] {
  const toc: Heading[] = []
  const stack: Heading[] = []

  headings.forEach((h) => {
    const heading = { ...h, subheadings: [] }

    while (stack.length > 0 && stack[stack.length - 1].depth >= heading.depth) {
      stack.pop()
    }

    if (stack.length === 0) {
      toc.push(heading)
    } else {
      stack[stack.length - 1].subheadings.push(heading)
    }

    stack.push(heading)
  })

  return toc
}
---

<details open class="group block rounded-xl border p-4 xl:hidden">
  <summary
    class="flex cursor-pointer items-center justify-between text-xl font-semibold"
  >
    Table of Contents
    <Icon
      name="lucide:chevron-down"
      class="size-5 transition-transform group-open:rotate-180"
    />
  </summary>
  <nav>
    <ul class="pt-3">
      {toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
    </ul>
  </nav>
</details>

<nav
  class="overflow-wrap-break-word sticky top-16 hidden h-0 w-[calc(50vw-50%-4rem)] translate-x-[calc(-100%-2em)] text-xs leading-4 xl:block"
>
  <div class="mr-6 flex justify-end">
    <ul
      class="mr-6 flex max-h-[calc(100vh-8rem)] flex-col justify-end gap-y-2 overflow-y-auto"
      id="toc-container"
    >
      <li>
        <h2 class="mb-2 text-lg font-semibold">Table of Contents</h2>
      </li>
      {toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
    </ul>
  </div>
</nav>

<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(
            `#toc-container 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>