From 88964ec842234a0cbcced196fb5fe130886bf93f Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 19 Feb 2026 21:36:59 +0200 Subject: [PATCH] feat: highlight active heading in sidebar table of contents --- src/App.tsx | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index d6b525b..90e0c33 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -343,6 +343,7 @@ function App() { const [selectionRects, setSelectionRects] = useState<{ left: number; top: number; width: number; height: number }[]>([]); const [tabFocusIndex, setTabFocusIndex] = useState(0); const [sidebarFocusIndex, setSidebarFocusIndex] = useState(0); + const [activeHeadingText, setActiveHeadingText] = useState(null); const contentRef = useRef(null); const sidebarRef = useRef(null); @@ -1033,6 +1034,35 @@ function App() { } }, [tabs.length, tabFocusIndex]); + // Active heading tracking via IntersectionObserver + useEffect(() => { + if (!activeTab || headings.length === 0) { + setActiveHeadingText(null); + return; + } + + const allHeadings = document.querySelectorAll( + '.markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content h5, .markdown-content h6' + ); + + if (allHeadings.length === 0) return; + + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + setActiveHeadingText(entry.target.textContent?.trim() || null); + break; + } + } + }, + { rootMargin: '0px 0px -70% 0px', threshold: 0.1 } + ); + + allHeadings.forEach(h => observer.observe(h)); + return () => observer.disconnect(); + }, [activeTab, headings, highlightedHtml]); + // Reset sidebar focus index when headings change useEffect(() => { setSidebarFocusIndex(0); @@ -1156,7 +1186,7 @@ function App() {