snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
126
apps/v4/components/docs-toc.tsx
Normal file
126
apps/v4/components/docs-toc.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconMenu3 } from "@tabler/icons-react"
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
|
||||
function useActiveItem(itemIds: string[]) {
|
||||
const [activeId, setActiveId] = React.useState<string | null>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveId(entry.target.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ rootMargin: "0% 0% -80% 0%" }
|
||||
)
|
||||
|
||||
for (const id of itemIds ?? []) {
|
||||
const element = document.getElementById(id)
|
||||
if (element) {
|
||||
observer.observe(element)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const id of itemIds ?? []) {
|
||||
const element = document.getElementById(id)
|
||||
if (element) {
|
||||
observer.unobserve(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [itemIds])
|
||||
|
||||
return activeId
|
||||
}
|
||||
|
||||
export function DocsTableOfContents({
|
||||
toc,
|
||||
variant = "list",
|
||||
className,
|
||||
}: {
|
||||
toc: {
|
||||
title?: React.ReactNode
|
||||
url: string
|
||||
depth: number
|
||||
}[]
|
||||
variant?: "dropdown" | "list"
|
||||
className?: string
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const itemIds = React.useMemo(
|
||||
() => toc.map((item) => item.url.replace("#", "")),
|
||||
[toc]
|
||||
)
|
||||
const activeHeading = useActiveItem(itemIds)
|
||||
|
||||
if (!toc?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (variant === "dropdown") {
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn("h-8 md:h-7", className)}
|
||||
>
|
||||
<IconMenu3 /> On This Page
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="no-scrollbar max-h-[70svh]"
|
||||
>
|
||||
{toc.map((item) => (
|
||||
<DropdownMenuItem
|
||||
key={item.url}
|
||||
asChild
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
data-depth={item.depth}
|
||||
className="data-[depth=3]:pl-6 data-[depth=4]:pl-8"
|
||||
>
|
||||
<a href={item.url}>{item.title}</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2 p-4 pt-0 text-sm", className)}>
|
||||
<p className="h-6 bg-background text-xs font-medium text-muted-foreground">
|
||||
On This Page
|
||||
</p>
|
||||
{toc.map((item) => (
|
||||
<a
|
||||
key={item.url}
|
||||
href={item.url}
|
||||
className="text-[0.8rem] text-muted-foreground no-underline transition-colors hover:text-foreground data-[active=true]:font-medium data-[active=true]:text-foreground data-[depth=3]:pl-4 data-[depth=4]:pl-6"
|
||||
data-active={item.url === `#${activeHeading}`}
|
||||
data-depth={item.depth}
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user