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.
This commit is contained in:
177
src/components/ui-showcase/TableOfContents.tsx
Normal file
177
src/components/ui-showcase/TableOfContents.tsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import { StatusBadge } from '@/components/ui/StatusBadge';
|
|||||||
import { KeyboardShortcutsModal } from '@/components/ui/KeyboardShortcutsModal';
|
import { KeyboardShortcutsModal } from '@/components/ui/KeyboardShortcutsModal';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
|
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
|
||||||
|
import { TableOfContents } from './TableOfContents';
|
||||||
|
|
||||||
export function UIShowcaseClient() {
|
export function UIShowcaseClient() {
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
@@ -133,11 +134,19 @@ export function UIShowcaseClient() {
|
|||||||
subtitle="Démonstration de tous les composants UI disponibles"
|
subtitle="Démonstration de tous les composants UI disponibles"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="max-w-6xl mx-auto p-8 space-y-16">
|
<div className="max-w-7xl mx-auto p-8">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||||
|
{/* Menu de navigation */}
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<TableOfContents />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contenu principal */}
|
||||||
|
<div className="lg:col-span-3 space-y-16">
|
||||||
|
|
||||||
|
|
||||||
{/* Buttons Section */}
|
{/* Buttons Section */}
|
||||||
<section className="space-y-8">
|
<section id="buttons" className="space-y-8">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
Buttons
|
Buttons
|
||||||
</h2>
|
</h2>
|
||||||
@@ -176,7 +185,7 @@ export function UIShowcaseClient() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Badges Section */}
|
{/* Badges Section */}
|
||||||
<section className="space-y-8">
|
<section id="badges" className="space-y-8">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
Badges
|
Badges
|
||||||
</h2>
|
</h2>
|
||||||
@@ -198,7 +207,7 @@ export function UIShowcaseClient() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Alerts Section */}
|
{/* Alerts Section */}
|
||||||
<section className="space-y-8">
|
<section id="alerts" className="space-y-8">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
Alerts
|
Alerts
|
||||||
</h2>
|
</h2>
|
||||||
@@ -242,7 +251,7 @@ export function UIShowcaseClient() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Inputs Section */}
|
{/* Inputs Section */}
|
||||||
<section className="space-y-8">
|
<section id="inputs" className="space-y-8">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
Inputs
|
Inputs
|
||||||
</h2>
|
</h2>
|
||||||
@@ -273,7 +282,7 @@ export function UIShowcaseClient() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Cards Section */}
|
{/* Cards Section */}
|
||||||
<section className="space-y-6">
|
<section id="cards" className="space-y-6">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-2">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-2">
|
||||||
Cards
|
Cards
|
||||||
</h2>
|
</h2>
|
||||||
@@ -386,7 +395,7 @@ export function UIShowcaseClient() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Interactive Demo */}
|
{/* Interactive Demo */}
|
||||||
<section className="space-y-6">
|
<section id="interactive-demo" className="space-y-6">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-2">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-2">
|
||||||
Démonstration Interactive
|
Démonstration Interactive
|
||||||
</h2>
|
</h2>
|
||||||
@@ -457,7 +466,7 @@ export function UIShowcaseClient() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Dashboard Components Section */}
|
{/* Dashboard Components Section */}
|
||||||
<section className="space-y-8">
|
<section id="dashboard-components" className="space-y-8">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
Dashboard Components
|
Dashboard Components
|
||||||
</h2>
|
</h2>
|
||||||
@@ -810,7 +819,7 @@ export function UIShowcaseClient() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Kanban Components Section */}
|
{/* Kanban Components Section */}
|
||||||
<section className="space-y-8">
|
<section id="kanban-components" className="space-y-8">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
Kanban Components
|
Kanban Components
|
||||||
</h2>
|
</h2>
|
||||||
@@ -1103,7 +1112,7 @@ export function UIShowcaseClient() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Jira Dashboard Components Section */}
|
{/* Jira Dashboard Components Section */}
|
||||||
<section className="space-y-8">
|
<section id="jira-dashboard-components" className="space-y-8">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
Jira Dashboard Components
|
Jira Dashboard Components
|
||||||
</h2>
|
</h2>
|
||||||
@@ -1190,7 +1199,7 @@ export function UIShowcaseClient() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Daily Components Section */}
|
{/* Daily Components Section */}
|
||||||
<section className="space-y-8">
|
<section id="daily-components" className="space-y-8">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
Daily Components
|
Daily Components
|
||||||
</h2>
|
</h2>
|
||||||
@@ -1335,7 +1344,7 @@ export function UIShowcaseClient() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Weekly Manager Components Section */}
|
{/* Weekly Manager Components Section */}
|
||||||
<section className="space-y-8">
|
<section id="weekly-manager-components" className="space-y-8">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
Weekly Manager Components
|
Weekly Manager Components
|
||||||
</h2>
|
</h2>
|
||||||
@@ -1431,7 +1440,7 @@ export function UIShowcaseClient() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Additional UI Components Section */}
|
{/* Additional UI Components Section */}
|
||||||
<section className="space-y-8">
|
<section id="additional-ui-components" className="space-y-8">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
Additional UI Components
|
Additional UI Components
|
||||||
</h2>
|
</h2>
|
||||||
@@ -1605,6 +1614,8 @@ export function UIShowcaseClient() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
Reference in New Issue
Block a user