diff --git a/src/components/docPage/index.tsx b/src/components/docPage/index.tsx index 51e1c7fb995da..3c94ac73a783a 100644 --- a/src/components/docPage/index.tsx +++ b/src/components/docPage/index.tsx @@ -16,11 +16,11 @@ import {CopyMarkdownButton} from '../copyMarkdownButton'; import {DocFeedback} from '../docFeedback'; import {GitHubCTA} from '../githubCTA'; import {Header} from '../header'; +import {InlineTableOfContents} from '../inlineTableOfContents'; import Mermaid from '../mermaid'; import {PaginationNav} from '../paginationNav'; import {PlatformSdkDetail} from '../platformSdkDetail'; import {Sidebar} from '../sidebar'; -import {SidebarTableOfContents} from '../sidebarTableOfContents'; import {ReaderDepthTracker} from '../track-reader-depth'; type Props = { @@ -96,6 +96,7 @@ export function DocPage({

{frontMatter.title}

{frontMatter.description}

+ {/* This exact id is important for Algolia indexing */}
{children} @@ -122,7 +123,6 @@ export function DocPage({ className="sticky h-[calc(100vh-var(--header-height))] top-[var(--header-height)] overflow-y-auto hidden toc:block flex-none w-[250px] min-w-[250px]" >
-
diff --git a/src/components/inlineTableOfContents/index.tsx b/src/components/inlineTableOfContents/index.tsx new file mode 100644 index 0000000000000..4c6be2d1765c9 --- /dev/null +++ b/src/components/inlineTableOfContents/index.tsx @@ -0,0 +1,105 @@ +'use client'; + +import {useEffect, useState} from 'react'; + +import {isNotNil} from 'sentry-docs/utils'; + +import styles from './style.module.scss'; + +interface TocItem { + title: string; + url: string; +} + +function getMainElement() { + if (typeof document === 'undefined') { + return null; + } + return document.getElementById('main'); +} + +function getTocItems(main: HTMLElement): TocItem[] { + return Array.from(main.querySelectorAll('h2')) + .map(el => { + const title = el.textContent?.trim(); + if (!el.id || !title) { + return null; + } + // This is a relatively new API, that checks if the element is visible in the document + // With this, we filter out e.g. sections hidden via CSS + if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) { + return null; + } + return { + url: `#${el.id}`, + title, + }; + }) + .filter(isNotNil); +} + +// Inline table of contents that appears at the top of the page content +// Shows only H2 headings, and only appears if there are 3 or more sections +export function InlineTableOfContents() { + const [tocItems, setTocItems] = useState([]); + + // gather the toc items on mount + useEffect(() => { + const main = getMainElement(); + if (!main) { + return; + } + + const items = getTocItems(main); + setTocItems(items); + }, []); + + // ensure toc items are kept up-to-date if the DOM changes + useEffect(() => { + const main = getMainElement(); + if (!main) { + return () => {}; + } + + const observer = new MutationObserver(() => { + const newTocItems = getTocItems(main); + + // Avoid flashing if nothing changes + if ( + newTocItems.length === tocItems.length && + newTocItems.every((item, index) => item.url === tocItems[index].url) + ) { + return; + } + setTocItems(newTocItems); + }); + + // Start observing the target node for any changes in its subtree + observer.observe(main, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class', 'id', 'style'], + }); + + return () => observer.disconnect(); + }, [tocItems]); + + // Only show if there are 3 or more sections + if (tocItems.length < 3) { + return null; + } + + return ( +
+
On this page
+ +
+ ); +} diff --git a/src/components/inlineTableOfContents/style.module.scss b/src/components/inlineTableOfContents/style.module.scss new file mode 100644 index 0000000000000..57a83d0d6eccb --- /dev/null +++ b/src/components/inlineTableOfContents/style.module.scss @@ -0,0 +1,62 @@ +.inline-toc { + --title-color: var(--gray-11); + --link-color: var(--gray-11); + --link-hover-color: var(--accent-purple); + + border-left: 2px solid var(--gray-6); + padding-left: 1rem; + padding-right: 1rem; + margin: 1.5rem 0; + max-width: 100%; + width: 100%; +} + +:global(.dark) { + .inline-toc { + --title-color: var(--gray-11); + --link-color: var(--gray-11); + } +} + +.inline-toc-title { + font-weight: 600; + font-size: 0.925rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--title-color); + margin-bottom: 0.5rem; + white-space: normal; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.inline-toc-list { + list-style-type: none; + padding-left: 0; + margin: 0; + max-width: 100%; + width: 100%; + + li { + margin: 0; + max-width: 100%; + } + + a { + color: var(--link-color); + text-decoration: none; + font-size: 0.875rem; + line-height: 1.8; + display: block; + transition: color 0.2s ease; + white-space: normal; + word-wrap: break-word; + overflow-wrap: break-word; + max-width: 100%; + + &:hover { + color: var(--link-hover-color); + } + } +} +