From 785dc91159428349bcac8ee54cc2ee93025fa885 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Tue, 30 Sep 2025 22:31:57 +0200 Subject: [PATCH] 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. --- .../ui-showcase/TableOfContents.tsx | 177 ++++++++++++++++++ .../ui-showcase/UIShowcaseClient.tsx | 47 +++-- 2 files changed, 206 insertions(+), 18 deletions(-) create mode 100644 src/components/ui-showcase/TableOfContents.tsx diff --git a/src/components/ui-showcase/TableOfContents.tsx b/src/components/ui-showcase/TableOfContents.tsx new file mode 100644 index 0000000..fb6454b --- /dev/null +++ b/src/components/ui-showcase/TableOfContents.tsx @@ -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(''); + const [sections, setSections] = useState([]); + + 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) => { + 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 ( + + ); +} diff --git a/src/components/ui-showcase/UIShowcaseClient.tsx b/src/components/ui-showcase/UIShowcaseClient.tsx index 71cd30d..ada5dbc 100644 --- a/src/components/ui-showcase/UIShowcaseClient.tsx +++ b/src/components/ui-showcase/UIShowcaseClient.tsx @@ -28,6 +28,7 @@ import { StatusBadge } from '@/components/ui/StatusBadge'; import { KeyboardShortcutsModal } from '@/components/ui/KeyboardShortcutsModal'; import { Modal } from '@/components/ui/Modal'; import { FontSizeToggle } from '@/components/ui/FontSizeToggle'; +import { TableOfContents } from './TableOfContents'; export function UIShowcaseClient() { const [inputValue, setInputValue] = useState(''); @@ -133,11 +134,19 @@ export function UIShowcaseClient() { subtitle="Démonstration de tous les composants UI disponibles" /> -
+
+
+ {/* Menu de navigation */} +
+ +
+ + {/* Contenu principal */} +
{/* Buttons Section */} -
+

Buttons

@@ -176,7 +185,7 @@ export function UIShowcaseClient() {
{/* Badges Section */} -
+

Badges

@@ -198,7 +207,7 @@ export function UIShowcaseClient() {
{/* Alerts Section */} -
+

Alerts

@@ -242,7 +251,7 @@ export function UIShowcaseClient() {
{/* Inputs Section */} -
+

Inputs

@@ -273,7 +282,7 @@ export function UIShowcaseClient() {
{/* Cards Section */} -
+

Cards

@@ -386,7 +395,7 @@ export function UIShowcaseClient() {
{/* Interactive Demo */} -
+

Démonstration Interactive

@@ -457,7 +466,7 @@ export function UIShowcaseClient() {
{/* Dashboard Components Section */} -
+

Dashboard Components

@@ -810,7 +819,7 @@ export function UIShowcaseClient() {
{/* Kanban Components Section */} -
+

Kanban Components

@@ -1103,7 +1112,7 @@ export function UIShowcaseClient() {
{/* Jira Dashboard Components Section */} -
+

Jira Dashboard Components

@@ -1190,7 +1199,7 @@ export function UIShowcaseClient() {
{/* Daily Components Section */} -
+

Daily Components

@@ -1335,7 +1344,7 @@ export function UIShowcaseClient() {
{/* Weekly Manager Components Section */} -
+

Weekly Manager Components

@@ -1431,7 +1440,7 @@ export function UIShowcaseClient() {
{/* Additional UI Components Section */} -
+

Additional UI Components

@@ -1598,11 +1607,13 @@ export function UIShowcaseClient() {
- {/* Footer */} -
-

- Cette page est accessible via /ui-showcase -

+ {/* Footer */} +
+

+ Cette page est accessible via /ui-showcase +

+
+