Start aside with sections

This commit is contained in:
Samy Pessé
2023-10-24 21:42:45 +02:00
parent e1372b0ede
commit 7295d25e4e
6 changed files with 145 additions and 1 deletions
+6 -1
View File
@@ -11,9 +11,14 @@
- [ ] Code block syntax highlighting
- Theme
- [ ] Toggler
- Page feedback
- Edit on GitHub
- Header links
## Differences
- Full-width mode:
- Changed: Full-width mode:
- When off, layout is centered
- It now detects based on the page
- New: Scroll to top button in page aside
- New: Proper RTL support
+49
View File
@@ -0,0 +1,49 @@
import { getDocumentSections } from "@/lib/document";
import { tcls } from "@/lib/tailwind";
import { RevisionPageDocument } from "@gitbook/api";
import React from "react";
import { ScrollSectionsList } from "./ScrollSectionsList";
/**
* Aside listing the headings in the document.
*/
export function PageAside(props: {
page: RevisionPageDocument;
document: any;
}) {
const { document } = props;
const sections = getDocumentSections(document);
return (
<aside className={tcls(
'hidden',
'xl:flex',
'flex-col',
'basis-56',
'grow-0',
'shrink-0',
'sticky',
'top-16',
'h-[calc(100vh-4rem)]',
'py-6'
)}>
{sections.length > 0 ? (
<>
<div className={tcls(
'text-sm', 'text-slate-500', 'font-semibold', 'pb-3'
)}>
On this page
</div>
<div className={tcls('overflow-auto', 'flex-1')}>
<React.Suspense fallback={null}>
<ScrollSectionsList sections={sections} />
</React.Suspense>
</div>
</>
) : null}
<div>
</div>
</aside>
)
}
@@ -0,0 +1,39 @@
'use client';
import { DocumentSection } from "@/lib/document";
import { tcls } from "@/lib/tailwind";
import Link from "next/link";
import React from "react";
import { IconChevronRight } from "@/components/icons/IconChevronRight";
export function ScrollSectionsList(props: {
sections: DocumentSection[];
}) {
const { sections } = props;
const [activeId, setActiveId] = React.useState(null);
React.useEffect(() => {
//
}, []);
return (
<ul>
{sections.map(section => (
<li key={section.id} className={tcls('flex', 'flex-row')}>
<Link href={`#${section.id}`} className={tcls(
'flex', 'flex-row',
'text-sm',
section.depth > 1 ? ['font-light', 'ps-3', 'py-1', 'text-slate-500', ] : ['py-2', 'text-slate-700', ],
activeId === section.id ? 'text-primary-500' : 'hover:text-slate-900',
)}>
{section.depth > 1 ? (
<IconChevronRight className={tcls('w-4', 'h-4', 'mr-1', 'mt-0.5')} />
) : null}
{section.title}
</Link>
</li>
))}
</ul>
)
}
+1
View File
@@ -0,0 +1 @@
export * from './PageAside';
@@ -9,6 +9,7 @@ import { SearchModal } from '@/components/Search';
import { Footer } from '@/components/Footer';
import { hasFullWidthBlock } from '@/lib/document';
import { CONTAINER_MAX_WIDTH_NORMAL, CONTAINER_PADDING } from '@/components/layout';
import { PageAside } from '../PageAside';
/**
* Render the entire content of the space (header, table of contents, footer, and page content).
@@ -36,6 +37,7 @@ export function SpaceContent(props: {
>
<TableOfContents revision={revision} activePage={page} ancestors={ancestors} />
<PageBody space={space} revision={revision} page={page} document={document} />
<PageAside page={page} document={document} />
</div>
<Footer space={space} />
<React.Suspense fallback={null}>
+48
View File
@@ -1,3 +1,10 @@
export interface DocumentSection {
id: string;
title: string;
depth: number;
}
/**
* Check if the document contains one block that should be rendered in full-width mode.
*/
@@ -6,3 +13,44 @@ export function hasFullWidthBlock(document: any): boolean {
return node.data.fullWidth || node.type === 'images';
});
}
/**
* Extract a list of sections from a document.
*/
export function getDocumentSections(document: any): DocumentSection[] {
const sections: DocumentSection[] = [];
let depth = 0;
document.nodes.forEach(block => {
if (block.type === 'heading-1' || block.type === 'heading-2' || block.type === 'heading-3') {
if (block.type === 'heading-1') {
depth = 1;
}
const title = getNodeText(block);
sections.push({
id: block.data.id ?? title, // TODO: use slugify
title,
depth: block.type === 'heading-1' ? 1 : (depth > 0 ? 2 : 1),
});
}
});
return sections;
}
/**
* Get the text of a block/inline.
*/
export function getNodeText(node: any): string {
switch (node.object) {
case 'text':
return node.leaves.map((leaf) => leaf.text).join('');
case 'block':
case 'inline':
return node.nodes.map(child => getNodeText(child)).join('');
default:
throw new Error('Invalid node');
}
}