feat(nx-dev): add TOC markdoc component for blog posts

This commit is contained in:
Juri 2024-12-23 14:55:40 +01:00 committed by Juri Strumpflohner
parent 90e12a77af
commit 3e564864fd
3 changed files with 85 additions and 0 deletions

View File

@ -56,6 +56,8 @@ import { pill } from './lib/tags/pill.schema';
import { fence } from './lib/nodes/fence.schema'; import { fence } from './lib/nodes/fence.schema';
import { FenceWrapper } from './lib/nodes/fence-wrapper.component'; import { FenceWrapper } from './lib/nodes/fence-wrapper.component';
import { VideoPlayer, videoPlayer } from './lib/tags/video-player.component'; import { VideoPlayer, videoPlayer } from './lib/tags/video-player.component';
import { TableOfContents } from './lib/tags/table-of-contents.component';
import { tableOfContents } from './lib/tags/table-of-contents.schema';
// TODO fix this export // TODO fix this export
export { GithubRepository } from './lib/tags/github-repository.component'; export { GithubRepository } from './lib/tags/github-repository.component';
@ -92,6 +94,7 @@ export const getMarkdocCustomConfig = (
tab, tab,
tabs, tabs,
'terminal-video': terminalVideo, 'terminal-video': terminalVideo,
toc: tableOfContents,
tweet, tweet,
youtube, youtube,
'video-link': videoLink, 'video-link': videoLink,
@ -121,6 +124,7 @@ export const getMarkdocCustomConfig = (
SideBySide, SideBySide,
Tab, Tab,
Tabs, Tabs,
TableOfContents,
TerminalVideo, TerminalVideo,
Tweet, Tweet,
YouTube, YouTube,

View File

@ -0,0 +1,70 @@
'use client';
import { useEffect, useState } from 'react';
interface TocItem {
id: string;
text: string;
level: number;
}
interface TableOfContentsProps {
maxDepth?: number;
}
export function TableOfContents({
maxDepth = 3,
}: TableOfContentsProps): JSX.Element {
const [headings, setHeadings] = useState<TocItem[]>([]);
useEffect(() => {
// Find the main content wrapper where markdown content is rendered
const content = document.querySelector('[data-document="main"]');
if (!content) return;
// Get all headings h1-h6 within the content
const headingElements = content.querySelectorAll('h1, h2, h3, h4, h5, h6');
const items: TocItem[] = Array.from(headingElements)
.map((heading) => {
const level = parseInt(heading.tagName[1]);
if (level > maxDepth) return null;
return {
id: heading.id,
text: heading.textContent || '',
level,
};
})
.filter((item): item is TocItem => item !== null);
setHeadings(items);
}, [maxDepth]);
if (headings.length === 0) {
return null;
}
return (
<nav className="toc not-prose mb-8 rounded-lg border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900/50">
<p className="mb-3 text-sm font-semibold text-slate-900 dark:text-slate-100">
Table of Contents
</p>
<ul className="space-y-2 text-sm">
{headings.map((heading) => (
<li
key={heading.id}
style={{ paddingLeft: `${(heading.level - 1) * 1}rem` }}
>
<a
href={`#${heading.id}`}
className="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200"
>
{heading.text}
</a>
</li>
))}
</ul>
</nav>
);
}

View File

@ -0,0 +1,11 @@
import { Schema } from '@markdoc/markdoc';
export const tableOfContents: Schema = {
render: 'TableOfContents',
attributes: {
maxDepth: {
type: 'Number',
default: 3,
},
},
};