94 lines
3.4 KiB
TypeScript
94 lines
3.4 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
|
|
interface Heading {
|
|
id: string
|
|
text: string
|
|
level: number
|
|
}
|
|
|
|
interface TOCProps {
|
|
headings: Heading[]
|
|
}
|
|
|
|
export function TableOfContents({ headings }: TOCProps) {
|
|
const [activeId, setActiveId] = useState('')
|
|
|
|
useEffect(() => {
|
|
const observer = new IntersectionObserver(
|
|
entries => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
setActiveId(entry.target.id)
|
|
}
|
|
})
|
|
},
|
|
{ rootMargin: '-100px 0px -66%' }
|
|
)
|
|
|
|
headings.forEach(({ id }) => {
|
|
const element = document.getElementById(id)
|
|
if (element) observer.observe(element)
|
|
})
|
|
|
|
return () => observer.disconnect()
|
|
}, [headings])
|
|
|
|
return (
|
|
<aside className="hidden lg:block sticky top-24 w-64 h-fit">
|
|
<div className="bg-black border border-[var(--neon-cyan)] p-6 relative overflow-hidden shadow-[0_0_15px_rgba(90,139,149,0.3),inset_0_0_15px_rgba(90,139,149,0.05)]">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-cyan-500/5 via-magenta-500/3 to-transparent pointer-events-none" />
|
|
<div className="absolute top-0 left-0 w-full h-0.5 bg-gradient-to-r from-transparent via-[var(--neon-cyan)] to-transparent opacity-50" />
|
|
|
|
<div className="border-b border-[var(--neon-magenta)] pb-3 mb-4 relative">
|
|
<div className="flex gap-1.5 mb-2 justify-end">
|
|
<div
|
|
className="w-3 h-3 border border-[var(--neon-cyan)]/40 hover:bg-[var(--neon-cyan)]/10 transition-colors cursor-pointer"
|
|
title="Minimize"
|
|
/>
|
|
<div
|
|
className="w-3 h-3 border border-[var(--neon-cyan)]/40 hover:bg-[var(--neon-cyan)]/10 transition-colors cursor-pointer"
|
|
title="Maximize"
|
|
/>
|
|
<div
|
|
className="w-3 h-3 border border-[var(--neon-pink)]/40 hover:bg-[var(--neon-pink)]/10 transition-colors cursor-pointer"
|
|
title="Close"
|
|
/>
|
|
</div>
|
|
<h3
|
|
className="text-xs font-mono font-bold text-[var(--neon-cyan)] uppercase tracking-wider"
|
|
style={{ textShadow: '0 0 6px rgba(90,139,149,0.5)' }}
|
|
>
|
|
>> NAVIGATION
|
|
</h3>
|
|
</div>
|
|
|
|
<nav className="space-y-1 relative">
|
|
{headings.map(heading => (
|
|
<a
|
|
key={heading.id}
|
|
href={`#${heading.id}`}
|
|
className={`
|
|
block text-sm font-mono py-2 border-l-2 transition-all duration-150
|
|
${heading.level === 2 ? 'pl-3' : 'pl-6'}
|
|
${
|
|
activeId === heading.id
|
|
? 'text-[var(--neon-cyan)] border-[var(--neon-cyan)] bg-cyan-500/5 shadow-[0_0_8px_rgba(90,139,149,0.3)]'
|
|
: 'text-zinc-500 border-zinc-900 hover:border-[var(--neon-magenta)] hover:text-[var(--neon-magenta)] hover:bg-magenta-500/3 hover:shadow-[0_0_4px_rgba(155,90,142,0.2)]'
|
|
}
|
|
`}
|
|
style={activeId === heading.id ? { textShadow: '0 0 4px rgba(90,139,149,0.5)' } : {}}
|
|
>
|
|
<span className="inline-block">{activeId === heading.id ? '▶ ' : '◆ '}</span>
|
|
{heading.text}
|
|
</a>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="absolute bottom-0 left-0 w-full h-0.5 bg-gradient-to-r from-transparent via-[var(--neon-purple)] to-transparent opacity-40" />
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|