feat: improved ToC highlighting

This commit is contained in:
enscribe 2024-09-13 17:45:18 -07:00
parent b93eddea6b
commit c2fa587935
No known key found for this signature in database
GPG key ID: 9BBD5C4114E25322
16 changed files with 636 additions and 115 deletions

View file

@ -1,7 +1,7 @@
---
import { Image } from 'astro:assets'
import { Badge } from '@/components/ui/badge'
import Link from '@components/Link.astro'
import { Image } from 'astro:assets'
import type { CollectionEntry } from 'astro:content'
type Props = {
@ -11,7 +11,9 @@ type Props = {
const { project } = Astro.props
---
<div class="overflow-hidden rounded-xl border transition-colors duration-300 ease-in-out hover:bg-secondary/50">
<div
class="overflow-hidden rounded-xl border transition-colors duration-300 ease-in-out hover:bg-secondary/50"
>
<Link href={project.data.link} class="block">
<Image
src={project.data.image}
@ -22,12 +24,18 @@ const { project } = Astro.props
/>
<div class="p-4">
<h3 class="mb-2 text-lg font-semibold">{project.data.name}</h3>
<p class="mb-4 text-sm text-muted-foreground">{project.data.description}</p>
<p class="mb-4 text-sm text-muted-foreground">
{project.data.description}
</p>
<div class="flex flex-wrap gap-2">
{project.data.tags.map((tag) => (
<Badge variant="secondary" showHash={false}>{tag}</Badge>
))}
{
project.data.tags.map((tag) => (
<Badge variant="secondary" showHash={false}>
{tag}
</Badge>
))
}
</div>
</div>
</Link>
</div>
</div>

View file

@ -1,28 +1,37 @@
---
import { ChevronDown } from 'lucide-react'
import TableOfContentsHeading from './TableOfContentsHeading.astro'
// https://kld.dev/building-table-of-contents/
const { headings } = Astro.props
const toc = buildToc(headings)
export interface Heading {
depth: number
slug: string
text: string
subheadings: Heading[]
}
function buildToc(headings: Heading[]) {
const { headings } = Astro.props
const toc = buildToc(headings)
function buildToc(headings: Heading[]): Heading[] {
const toc: Heading[] = []
const parentHeadings = new Map()
const stack: Heading[] = []
headings.forEach((h) => {
const heading = { ...h, subheadings: [] }
parentHeadings.set(heading.depth, heading)
if (heading.depth === 2) {
while (stack.length > 0 && stack[stack.length - 1].depth >= heading.depth) {
stack.pop()
}
if (stack.length === 0) {
toc.push(heading)
} else {
parentHeadings.get(heading.depth - 1).subheadings.push(heading)
stack[stack.length - 1].subheadings.push(heading)
}
stack.push(heading)
})
return toc
}
---
@ -32,17 +41,9 @@ function buildToc(headings: Heading[]) {
class="flex cursor-pointer items-center justify-between text-xl font-semibold"
>
Table of Contents
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-5 transition-transform group-open:rotate-180"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
<ChevronDown
className="size-5 transition-transform group-open:rotate-180"
/>
</summary>
<nav>
<ul class="pt-3">
@ -50,11 +51,15 @@ function buildToc(headings: 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="max-h-[calc(100vh-8rem)] space-y-2 overflow-y-auto">
<ul
class="max-h-[calc(100vh-8rem)] space-y-2 overflow-y-auto"
id="toc-container"
>
<li>
<h2 class="mb-2 text-lg font-semibold">Table of Contents</h2>
</li>
@ -62,3 +67,32 @@ function buildToc(headings: Heading[]) {
</ul>
</div>
</nav>
<script>
function setupToc() {
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.intersectionRatio > 0 ? 'add' : 'remove'
link.classList[addRemove]('font-semibold')
link.classList[addRemove]('text-foreground')
})
})
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>

View file

@ -8,7 +8,10 @@ const { heading } = Astro.props
<li
class="mr-2 list-inside list-disc px-6 py-1.5 text-sm text-foreground/60 xl:list-none xl:p-0"
>
<Link href={'#' + heading.slug} class="toc-link" data-heading={heading.slug}>
<Link
href={'#' + heading.slug}
class="toc-link transition-colors duration-200"
>
{heading.text}
</Link>
{
@ -21,33 +24,3 @@ const { heading } = Astro.props
)
}
</li>
<script>
function updateActiveHeading() {
const headings = document.querySelectorAll('h2, h3, h4, h5, h6')
const tocLinks = document.querySelectorAll('.toc-link')
let currentHeading = ''
headings.forEach((heading) => {
const top = heading.getBoundingClientRect().top
if (top < 200) {
currentHeading = heading.id
}
})
tocLinks.forEach((link) => {
const headingSlug = link.getAttribute('data-heading')
if (headingSlug === currentHeading) {
link.classList.add('underline')
link.classList.add('text-foreground')
} else {
link.classList.remove('underline')
link.classList.remove('text-foreground')
}
})
}
window.addEventListener('scroll', updateActiveHeading)
window.addEventListener('load', updateActiveHeading)
</script>

View file

@ -7,7 +7,7 @@ const Card = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('bg-background rounded-xl border', className)}
className={cn('rounded-xl border bg-background', className)}
{...props}
/>
))

View file

@ -23,13 +23,15 @@ export function ModeToggle() {
theme === 'dark' ||
(theme === 'system' &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
document.documentElement.classList.add('disable-transitions')
document.documentElement.classList[isDark ? 'add' : 'remove']('dark')
window.getComputedStyle(document.documentElement).getPropertyValue('opacity')
window
.getComputedStyle(document.documentElement)
.getPropertyValue('opacity')
requestAnimationFrame(() => {
document.documentElement.classList.remove('disable-transitions')
})
@ -65,4 +67,4 @@ export function ModeToggle() {
</DropdownMenuContent>
</DropdownMenu>
)
}
}