|
1 | 1 | 'use client'
|
2 | 2 |
|
3 | 3 | import type { Doc } from '@/content'
|
4 |
| -import React, { useRef } from 'react' |
5 |
| -import { Badge } from '../ui/badge' |
| 4 | +import { useRef, useLayoutEffect, useState, useEffect } from 'react' |
6 | 5 | import Link from 'next/link'
|
7 |
| -import { FaFacebook, FaFilePdf, FaLinkedin } from 'react-icons/fa' |
8 | 6 | import { DocTracingBeam } from './DocTracingBeam'
|
9 |
| -import { BASE_URL } from '@/lib/constants' |
10 |
| -import { title } from 'radash' |
11 | 7 | import { Button } from '../ui/button'
|
12 | 8 | import { GitHubIcon } from '../icons/GitHubIcon'
|
13 |
| - |
14 |
| -interface Props { |
15 |
| - doc: Doc |
16 |
| - articleRef?: React.RefObject<HTMLDivElement> |
17 |
| -} |
| 9 | +import { useMotionValue } from 'framer-motion' |
| 10 | +import { cn } from '@/lib/utils' |
18 | 11 |
|
19 | 12 | interface Toc {
|
20 | 13 | url: string
|
21 | 14 | value: string
|
22 | 15 | depth: number
|
23 | 16 | }
|
24 | 17 |
|
25 |
| -const DocToC: React.FC<Props> = ({ doc }) => { |
26 |
| - const articleRef = useRef<HTMLDivElement>(null) |
| 18 | +const DocToC = ({ doc }: { doc: Doc }) => { |
| 19 | + const initial = 14 |
| 20 | + const sectionSize = 28 |
| 21 | + const offset = 10 |
| 22 | + |
| 23 | + const y1 = useMotionValue(0) |
| 24 | + const y2 = useMotionValue(0) |
| 25 | + |
| 26 | + const [activeIndex, setActiveIndex] = useState(-1) |
| 27 | + |
| 28 | + useEffect(() => { |
| 29 | + const headings = document |
| 30 | + .querySelectorAll<HTMLHeadingElement>('h2.md-content-header') |
| 31 | + .values() |
| 32 | + .toArray() |
| 33 | + |
| 34 | + const options = { |
| 35 | + root: null, |
| 36 | + rootMargin: '0px', |
| 37 | + threshold: 1, // Adjust based on when you want to highlight |
| 38 | + } |
| 39 | + |
| 40 | + const observer = new IntersectionObserver((entries) => { |
| 41 | + entries.forEach((entry) => { |
| 42 | + if (entry.isIntersecting) { |
| 43 | + const index = headings.findIndex( |
| 44 | + (heading) => heading.textContent === entry.target.textContent, |
| 45 | + ) |
| 46 | + |
| 47 | + y2.set(initial + (index * sectionSize + offset)) |
| 48 | + |
| 49 | + setActiveIndex(index) |
| 50 | + } |
| 51 | + }) |
| 52 | + }, options) |
| 53 | + |
| 54 | + headings.forEach((h2) => observer.observe(h2)) |
| 55 | + |
| 56 | + return () => { |
| 57 | + headings.forEach((h2) => observer.unobserve(h2)) |
| 58 | + } |
| 59 | + }, []) |
27 | 60 |
|
28 | 61 | return (
|
29 |
| - <div className="hidden h-full min-w-52 md:block" ref={articleRef}> |
| 62 | + <div className="hidden h-full min-w-52 md:block"> |
30 | 63 | <aside className="sticky top-[calc(var(--header-height)+1px+2rem)] max-h-[calc(100vh-var(--header-height)-3rem)] min-w-40 space-y-6">
|
31 | 64 | {doc.toc.length ? (
|
32 | 65 | <div className="relative flex flex-col">
|
33 | 66 | <p className="mb-2 font-mono text-sm uppercase dark:text-zinc-300">
|
34 | 67 | On this page
|
35 | 68 | </p>
|
36 |
| - <DocTracingBeam targetRef={articleRef}> |
| 69 | + <DocTracingBeam y1={y1} y2={y2}> |
37 | 70 | <ol className="flex flex-col gap-y-1 pl-4 text-sm font-medium">
|
38 | 71 | {doc.toc.map((item: Toc, i: number) => {
|
39 | 72 | return (
|
40 | 73 | <li key={item.url + i}>
|
41 | 74 | <Link
|
42 | 75 | href={item.url}
|
43 |
| - className="text-2xs text-muted-foreground transition-colors hover:text-zinc-900 dark:hover:text-zinc-200" |
| 76 | + className={cn( |
| 77 | + 'text-2xs', |
| 78 | + activeIndex === i |
| 79 | + ? 'text-zinc-900 dark:text-zinc-200' |
| 80 | + : 'text-muted-foreground transition-colors hover:text-zinc-900 dark:hover:text-zinc-200', |
| 81 | + )} |
44 | 82 | >
|
45 | 83 | {item.value}
|
46 | 84 | </Link>
|
|
0 commit comments