mirror of
https://github.com/LukeHagar/jsdoc-cheatsheet.git
synced 2025-12-06 04:20:07 +00:00
Integrate react-syntax-highlighter for improved code example rendering and enhance the JSDoc cheatsheet layout with a new navigation structure and quick reference section.
149 lines
5.1 KiB
TypeScript
149 lines
5.1 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { List, ChevronRight, Hash } from "lucide-react"
|
|
|
|
interface TOCItem {
|
|
id: string
|
|
title: string
|
|
level: number
|
|
}
|
|
|
|
interface TableOfContentsProps {
|
|
items: TOCItem[]
|
|
className?: string
|
|
}
|
|
|
|
export default function TableOfContents({ items, className = "" }: TableOfContentsProps) {
|
|
const [activeId, setActiveId] = useState<string>("")
|
|
const [isVisible, setIsVisible] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (items.length === 0) return
|
|
|
|
// Calculate header height dynamically
|
|
const header = document.querySelector('header')
|
|
const headerHeight = header ? header.offsetHeight + 20 : 100 // 20px buffer
|
|
const rootMargin = `-${headerHeight}px 0% -35% 0%`
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
setActiveId(entry.target.id)
|
|
}
|
|
})
|
|
},
|
|
{
|
|
rootMargin,
|
|
threshold: 0.1,
|
|
}
|
|
)
|
|
|
|
// Observe all TOC items
|
|
items.forEach((item) => {
|
|
const element = document.getElementById(item.id)
|
|
if (element) {
|
|
observer.observe(element)
|
|
}
|
|
})
|
|
|
|
return () => {
|
|
items.forEach((item) => {
|
|
const element = document.getElementById(item.id)
|
|
if (element) {
|
|
observer.unobserve(element)
|
|
}
|
|
})
|
|
}
|
|
}, [items])
|
|
|
|
useEffect(() => {
|
|
// Show TOC if there are items
|
|
setIsVisible(items.length > 0)
|
|
}, [items])
|
|
|
|
const scrollToSection = (id: string) => {
|
|
const element = document.getElementById(id)
|
|
if (element) {
|
|
// Get the actual header height dynamically
|
|
const header = document.querySelector('header')
|
|
const headerHeight = header ? header.offsetHeight + 20 : 100 // 20px buffer
|
|
const elementPosition = element.offsetTop - headerHeight
|
|
|
|
// Add a small delay to ensure smooth scrolling
|
|
setTimeout(() => {
|
|
window.scrollTo({
|
|
top: Math.max(0, elementPosition), // Ensure we don't scroll to negative position
|
|
behavior: "smooth"
|
|
})
|
|
}, 10)
|
|
}
|
|
}
|
|
|
|
if (!isVisible) return null
|
|
|
|
return (
|
|
<aside className={`w-72 bg-gradient-to-b from-card/95 to-card/90 backdrop-blur-sm border-l border-border/50 fixed right-0 top-16 h-[calc(100vh-4rem)] z-40 overflow-y-auto hidden xl:block shadow-2xl ${className}`}>
|
|
<div className="p-6">
|
|
<Card className="bg-card/80 border-border/30 shadow-lg backdrop-blur-sm">
|
|
<CardHeader className="pb-4 border-b border-border/20">
|
|
<CardTitle className="text-base text-foreground flex items-center gap-3 font-semibold">
|
|
<div className="p-2 rounded-lg bg-primary/10">
|
|
<List className="h-4 w-4 text-primary" />
|
|
</div>
|
|
<div>
|
|
<div>Table of Contents</div>
|
|
<div className="text-xs text-muted-foreground font-normal mt-1">
|
|
{items.length} {items.length === 1 ? 'section' : 'sections'}
|
|
</div>
|
|
</div>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<nav className="space-y-1 p-4">
|
|
{items.map((item, index) => {
|
|
const isActive = activeId === item.id
|
|
const indentClass = item.level > 1 ? `ml-${(item.level - 1) * 6}` : ""
|
|
|
|
return (
|
|
<div key={item.id} className="group">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className={`w-full justify-start gap-3 text-sm transition-all duration-300 group-hover:translate-x-1 ${
|
|
isActive
|
|
? "bg-primary text-primary-foreground hover:bg-primary/90 shadow-md"
|
|
: "hover:bg-primary/5 text-muted-foreground hover:text-foreground"
|
|
} ${indentClass} h-10 px-3`}
|
|
onClick={() => scrollToSection(item.id)}
|
|
>
|
|
<div className={`flex items-center gap-2 flex-1 min-w-0`}>
|
|
{isActive ? (
|
|
<div className="w-2 h-2 rounded-full bg-primary-foreground flex-shrink-0" />
|
|
) : (
|
|
<Hash className="h-3 w-3 flex-shrink-0 text-muted-foreground group-hover:text-foreground transition-colors" />
|
|
)}
|
|
<span className="truncate font-medium">{item.title}</span>
|
|
</div>
|
|
{isActive && (
|
|
<ChevronRight className="h-3 w-3 flex-shrink-0 animate-pulse" />
|
|
)}
|
|
</Button>
|
|
{index < items.length - 1 && (
|
|
<div className="h-px bg-border/30 mx-3 my-1" />
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</nav>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|