Files
towercontrol/src/components/ui-showcase/TableOfContents.tsx
Julien Froidefond 785dc91159 feat: add Table of Contents component to UI showcase
- Introduced `TableOfContents` component for improved navigation within the UI showcase.
- Implemented section extraction and intersection observer for active section tracking.
- Updated `UIShowcaseClient` to include the new component, enhancing user experience with a sticky navigation menu.
- Added IDs to sections for better linking and scrolling functionality.
2025-09-30 22:31:57 +02:00

178 lines
5.6 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
interface TableOfContentsProps {
className?: string;
}
interface Section {
id: string;
title: string;
level: number;
}
export function TableOfContents({ className = '' }: TableOfContentsProps) {
const [activeSection, setActiveSection] = useState<string>('');
const [sections, setSections] = useState<Section[]>([]);
useEffect(() => {
const extractSections = () => {
const sectionsWithId = document.querySelectorAll('section[id]');
const extractedSections: Section[] = Array.from(sectionsWithId).map((section) => {
const h2 = section.querySelector('h2');
return {
id: section.id,
title: h2?.textContent || section.id,
level: 2
};
});
setSections(extractedSections);
return sectionsWithId;
};
// Fonction pour configurer l'intersection observer
const setupIntersectionObserver = (sections: NodeListOf<Element>) => {
const intersectionObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveSection(entry.target.id);
}
});
},
{
rootMargin: '-20% 0px -70% 0px',
threshold: 0
}
);
sections.forEach((section) => intersectionObserver.observe(section));
return intersectionObserver;
};
// Essayer immédiatement
let sectionsElements = extractSections();
let intersectionObserver: IntersectionObserver | null = null;
if (sectionsElements.length > 0) {
intersectionObserver = setupIntersectionObserver(sectionsElements);
} else {
// Utiliser MutationObserver pour surveiller les changements
const mutationObserver = new MutationObserver((mutations) => {
let shouldCheck = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
// Vérifier si des sections ont été ajoutées
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element;
if (element.tagName === 'SECTION' || element.querySelector('section[id]')) {
shouldCheck = true;
}
}
});
}
});
if (shouldCheck) {
sectionsElements = extractSections();
if (sectionsElements.length > 0 && !intersectionObserver) {
intersectionObserver = setupIntersectionObserver(sectionsElements);
mutationObserver.disconnect();
}
}
});
// Surveiller le contenu principal
const mainContent = document.querySelector('.lg\\:col-span-3') || document.body;
mutationObserver.observe(mainContent, {
childList: true,
subtree: true
});
// Fallback avec des tentatives périodiques
const intervalId = setInterval(() => {
sectionsElements = extractSections();
if (sectionsElements.length > 0) {
if (!intersectionObserver) {
intersectionObserver = setupIntersectionObserver(sectionsElements);
}
mutationObserver.disconnect();
clearInterval(intervalId);
}
}, 1000);
return () => {
mutationObserver.disconnect();
clearInterval(intervalId);
if (intersectionObserver) {
sectionsElements.forEach((section) => intersectionObserver!.unobserve(section));
}
};
}
return () => {
if (intersectionObserver) {
sectionsElements.forEach((section) => intersectionObserver!.unobserve(section));
}
};
}, []);
const scrollToSection = (sectionId: string) => {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
};
return (
<nav className={`sticky top-8 ${className}`}>
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-semibold text-[var(--foreground)] mb-3 flex items-center gap-2">
<span className="text-[var(--primary)]">📋</span>
Navigation
</h3>
{sections.length === 0 ? (
<div className="text-sm text-[var(--muted-foreground)] py-2">
Chargement des sections...
</div>
) : (
<>
<ul className="space-y-1">
{sections.map((section) => (
<li key={section.id}>
<button
onClick={() => scrollToSection(section.id)}
className={`w-full text-left px-2 py-1 rounded text-sm transition-colors ${
activeSection === section.id
? 'bg-[var(--primary)] text-[var(--primary-foreground)] font-medium'
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--card-hover)]'
}`}
>
{section.title}
</button>
</li>
))}
</ul>
{/* Indicateur de progression */}
<div className="mt-4 pt-3 border-t border-[var(--border)]">
<div className="flex items-center justify-between text-xs text-[var(--muted-foreground)]">
<span>Sections</span>
<span>{sections.length}</span>
</div>
</div>
</>
)}
</div>
</nav>
);
}