From e761005b57d13c53a4d951e073a75081d7641a55 Mon Sep 17 00:00:00 2001 From: "techmannih (aider)" Date: Thu, 6 Mar 2025 09:16:37 +0530 Subject: [PATCH 01/10] feat: Add Table of Contents component to documentation layout --- components/Layout.tsx | 12 +++++++++--- components/TOC.tsx | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 components/TOC.tsx diff --git a/components/Layout.tsx b/components/Layout.tsx index 441b83188..9f7f3e6f3 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -11,6 +11,7 @@ import DarkModeToggle from './DarkModeToggle'; import extractPathWithoutFragment from '~/lib/extractPathWithoutFragment'; import ScrollButton from './ScrollButton'; import Image from 'next/image'; +import TOC from './TOC'; type Props = { children: React.ReactNode; @@ -104,9 +105,14 @@ export default function Layout({ -
- {showMobileNav && } - {children} +
+
+ {showMobileNav && } + {children} +
+
diff --git a/components/TOC.tsx b/components/TOC.tsx new file mode 100644 index 000000000..8766f3035 --- /dev/null +++ b/components/TOC.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +type Heading = { + id: string; + text: string; +}; + +type TOCProps = { + headings?: Heading[]; +}; + +const TOC: React.FC = ({ headings = [] }) => { + return ( + + ); +}; + +export default TOC; From 674a3700a2e095d1c4304aee1b5761638d08d5cc Mon Sep 17 00:00:00 2001 From: techmannih Date: Thu, 6 Mar 2025 10:25:45 +0530 Subject: [PATCH 02/10] add toc --- components/Layout.tsx | 8 +--- components/Sidebar.tsx | 14 ++++++- components/TOC.tsx | 95 +++++++++++++++++++++++++++++++++--------- 3 files changed, 91 insertions(+), 26 deletions(-) diff --git a/components/Layout.tsx b/components/Layout.tsx index 9f7f3e6f3..1badb63cc 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -11,7 +11,6 @@ import DarkModeToggle from './DarkModeToggle'; import extractPathWithoutFragment from '~/lib/extractPathWithoutFragment'; import ScrollButton from './ScrollButton'; import Image from 'next/image'; -import TOC from './TOC'; type Props = { children: React.ReactNode; @@ -105,14 +104,11 @@ export default function Layout({
-
-
+
+
{showMobileNav && } {children}
-
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index ad52e2ab8..6e4d48a2d 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -10,6 +10,7 @@ import CarbonAds from './CarbonsAds'; import { useTheme } from 'next-themes'; import ExternalLinkIcon from '../public/icons/external-link-black.svg'; import Image from 'next/image'; +import TOC from './TOC'; const DocLink = ({ uri, label, @@ -243,6 +244,7 @@ export const SidebarLayout = ({ children }: { children: React.ReactNode }) => {
+ {/* Sidebar */}
@@ -252,9 +254,19 @@ export const SidebarLayout = ({ children }: { children: React.ReactNode }) => { />
-
+ + {/* Main Content (Children) */} +
{children}
+ + {/* TOC */} +
+ +
diff --git a/components/TOC.tsx b/components/TOC.tsx index 8766f3035..7fa689695 100644 --- a/components/TOC.tsx +++ b/components/TOC.tsx @@ -1,27 +1,84 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; -type Heading = { - id: string; - text: string; -}; +const TOC: React.FC = () => { + const router = useRouter(); + const [headings, setHeadings] = useState< + { id: string; text: string; level: number }[] + >([]); + const [activeId, setActiveId] = useState(null); -type TOCProps = { - headings?: Heading[]; -}; + useEffect(() => { + const mainContent = document.getElementById('main-content'); + const elements = mainContent + ? mainContent.querySelectorAll('h1, h2, h3, h4') + : []; + const newHeadings: { id: string; text: string; level: number }[] = []; + elements.forEach((el) => { + const text = el.textContent || ''; + if (text.trim().toLowerCase() === 'on this page') return; + if (el.closest('#sidebar')) return; + const currentFolder = router.pathname.split('/').pop()?.toLowerCase(); + if (text.trim().toLowerCase() === currentFolder) return; + // Filter out headings that look like file or folder names + if ( + text.includes('/') || + text.includes('\\') || + /\.md$|\.tsx$|\.jsx$|\.js$/i.test(text.trim()) + ) + return; + const level = parseInt(el.tagName.replace('H', '')); + // If the heading doesn't have an ID, assign one from its text content + if (!el.id) { + const generatedId = text + .toLowerCase() + .trim() + .replace(/\s+/g, '-') + .replace(/[^\w\-]+/g, ''); + if (generatedId) { + el.id = generatedId; + } + } + newHeadings.push({ + id: el.id, + text: text, + level, + }); + }); + setHeadings(newHeadings); + + const handleScroll = () => { + let currentId: string | null = null; + elements.forEach((el) => { + const rect = (el as HTMLElement).getBoundingClientRect(); + if (rect.top <= 100) { + currentId = el.id; + } + }); + setActiveId(currentId); + }; + + window.addEventListener('scroll', handleScroll); + handleScroll(); + return () => window.removeEventListener('scroll', handleScroll); + }, [router.asPath]); -const TOC: React.FC = ({ headings = [] }) => { return ( -