Table Of Contents
Table des matieres generee automatiquement a partir des titres de la page. Installez Table Of Contents depuis le registre VLLNT UI avec la CLI shadcn.
Table des matieres generee automatiquement a partir des titres de la page. Fait partie de la famille utility dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.
Installation
Ajoutez Table Of Contents a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/table-of-contents.jsonSource
"use client";
import { useEffect, useState } from "react";
import type { HeadingTag } from "../../lib/types";
import { cn } from "../../lib/utils";
type TableOfContentsProps = {
/** Heading tag for the "On This Page" label. Defaults to `h3`. */
as?: HeadingTag;
sections: { id: string; title: string }[];
};
function handleClick(id: string) {
const element = document.querySelector(`#${id}`);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
function useActiveSection(sections: { id: string; title: string }[]) {
const [activeSection, setActiveSection] = useState<null | string>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveSection(entry.target.id);
}
});
},
{
rootMargin: "-100px 0px -66% 0px",
threshold: 0,
},
);
sections.forEach((section) => {
const element = document.querySelector(`#${section.id}`);
if (element) {
observer.observe(element);
}
});
return () => {
sections.forEach((section) => {
const element = document.querySelector(`#${section.id}`);
if (element) {
observer.unobserve(element);
}
});
};
}, [sections]);
return activeSection;
}
export function TableOfContents({
as: Heading = "h3",
sections,
}: TableOfContentsProps) {
const activeSection = useActiveSection(sections);
if (sections.length === 0) {
return null;
}
return (
<aside className="hidden xl:block">
<div className="sticky top-8">
<div className="border-l-2 border-border pl-4">
<Heading className="text-sm font-semibold mb-4 text-muted-foreground uppercase tracking-wider">
On This Page
</Heading>
<nav className="space-y-2">
{sections.map((section) => (
<button
className={cn(
"block text-left text-sm transition-colors",
"hover:text-foreground",
activeSection === section.id
? "text-foreground font-medium"
: "text-muted-foreground",
)}
key={section.id}
onClick={() => {
handleClick(section.id);
}}
type="button"
>
{section.title}
</button>
))}
</nav>
</div>
</div>
</aside>
);
}
Stories
Explorez chaque variante et etat dans le Storybook interactif :
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.