15 lines
4.6 KiB
JSON
15 lines
4.6 KiB
JSON
{
|
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
"name": "toc",
|
|
"type": "registry:component",
|
|
"title": "Table of Contents",
|
|
"description": "Show your table of contents in a sidebar.",
|
|
"files": [
|
|
{
|
|
"path": "components/retroui/TableOfContents.tsx",
|
|
"content": "\n\"use client\";\n\nimport { cn } from '@/lib/utils';\nimport React, { useEffect, useState } from 'react';\n\ninterface TOCItem {\n title: string;\n url: string;\n level: number;\n items?: TOCItem[];\n}\n\ninterface TableOfContentsProps {\n depth?: number;\n className?: string;\n children?: React.ReactNode;\n}\n\nfunction generateTOCFromDOM(depth: number = 6): TOCItem[] {\n const headings: NodeListOf<HTMLHeadingElement> = document.querySelectorAll(\n Array.from({ length: depth }, (_, i) => `h${i + 1}`).join(', ')\n );\n\n const items: TOCItem[] = [];\n const stack: TOCItem[] = [];\n\n headings.forEach((heading) => {\n const level = parseInt(heading.tagName.charAt(1));\n const id = heading.id || heading.textContent?.toLowerCase().replace(/\\s+/g, '-').replace(/[^\\w-]/g, '') || '';\n\n if (!heading.id && id) {\n heading.id = id;\n }\n\n const item: TOCItem = {\n title: heading.textContent || '',\n url: `#${id}`,\n level,\n };\n\n while (stack.length > 0 && stack[stack.length - 1].level >= level) {\n stack.pop();\n }\n\n if (stack.length === 0) {\n items.push(item);\n } else {\n const parent = stack[stack.length - 1];\n if (!parent.items) parent.items = [];\n parent.items.push(item);\n }\n\n stack.push(item);\n });\n\n return items;\n}\n\nfunction renderTOCItems(items: TOCItem[], level = 0, activeId: string | null) {\n if (!items || items.length === 0) return null;\n\n return (\n <ul className={`space-y-1 ${level > 0 ? 'ml-4 mt-1' : ''}`}>\n {items.map((item, index) => {\n const isActive = activeId === item.url.substring(1);\n return (\n <li key={index}>\n <a\n href={item.url}\n className={`text-sm max-w-full truncate transition-colors block py-1 border-l-2 pl-2 ${\n isActive \n ? 'text-black border-black bg-accent' \n : 'border-transparent hover:border-black hover:text-black'\n }`}\n >\n {item.title}\n </a>\n {item.items && renderTOCItems(item.items, level + 1, activeId)}\n </li>\n );\n })}\n </ul>\n );\n}\n\nexport function TableOfContents({\n depth = 2,\n className = \"\",\n children,\n}: TableOfContentsProps) {\n const [tocItems, setTocItems] = useState<TOCItem[]>([]);\n const [activeId, setActiveId] = useState<string | null>(null);\n\n useEffect(() => {\n const items = generateTOCFromDOM(depth);\n setTocItems(items);\n\n const observer = new MutationObserver(() => {\n const updatedItems = generateTOCFromDOM(depth);\n setTocItems(updatedItems);\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['id']\n });\n\n return () => observer.disconnect();\n }, [depth]);\n\n useEffect(() => {\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n setActiveId(entry.target.id);\n }\n });\n },\n { rootMargin: '-20% 0% -35% 0%' }\n );\n\n const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');\n headings.forEach((heading) => observer.observe(heading));\n\n return () => observer.disconnect();\n }, []);\n\n if (tocItems.length === 0) {\n return null;\n }\n\n return (\n <div className={cn(\"border-2 shadow-md p-4 rounded w-52 h-60 overflow-y-auto sidebar-scroll\", className)}>\n {children}\n {renderTOCItems(tocItems, 0, activeId)}\n </div>\n );\n}",
|
|
"type": "registry:component",
|
|
"target": "components/retroui/TableOfContents.tsx"
|
|
}
|
|
]
|
|
} |