mirror of
https://github.com/LukeHagar/jsdoc-cheatsheet.git
synced 2025-12-06 12:37:48 +00:00
Add a dedicated quick reference section to the JSDoc cheatsheet, streamline the layout for better readability, and adjust sidebar and table of contents styles for consistency.
178 lines
6.0 KiB
TypeScript
178 lines
6.0 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)
|
|
|
|
// Read initial hash from URL on mount
|
|
useEffect(() => {
|
|
const hash = window.location.hash.slice(1) // Remove the # symbol
|
|
if (hash && items.some(item => item.id === hash)) {
|
|
setActiveId(hash)
|
|
}
|
|
}, [items])
|
|
|
|
// Listen for hash changes
|
|
useEffect(() => {
|
|
const handleHashChange = () => {
|
|
const hash = window.location.hash.slice(1) // Remove the # symbol
|
|
if (hash && items.some(item => item.id === hash)) {
|
|
setActiveId(hash)
|
|
}
|
|
}
|
|
|
|
window.addEventListener('hashchange', handleHashChange)
|
|
return () => window.removeEventListener('hashchange', handleHashChange)
|
|
}, [items])
|
|
|
|
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)
|
|
// Update URL hash when scrolling to a section
|
|
if (window.location.hash !== `#${entry.target.id}`) {
|
|
window.history.replaceState(null, '', `#${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) {
|
|
// Update URL hash immediately
|
|
window.history.pushState(null, '', `#${id}`)
|
|
setActiveId(id)
|
|
|
|
// 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 fixed right-0 top-16 h-[calc(100vh-4rem)] z-40 overflow-y-auto hidden xl:block ${className}`}>
|
|
<div className="p-2">
|
|
<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>
|
|
)
|
|
}
|