mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Start tabs
This commit is contained in:
@@ -11,14 +11,15 @@
|
||||
- [ ] Code block syntax highlighting
|
||||
- Theme
|
||||
- [ ] Toggler
|
||||
- Page feedback
|
||||
- Edit on GitHub
|
||||
- Header links
|
||||
- Page feedback
|
||||
- Edit on GitHub
|
||||
- Header links
|
||||
|
||||
## Differences
|
||||
|
||||
- Changed: 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
|
||||
- New: Scroll to top button in page aside
|
||||
- New: Proper RTL support
|
||||
- New: Per page OpenGraph cover image
|
||||
|
||||
@@ -9,6 +9,7 @@ import { tcls } from '@/lib/tailwind';
|
||||
import { Hint } from './Hint';
|
||||
import { DocumentContextProps } from './DocumentView';
|
||||
import { Images } from './Images';
|
||||
import { Tabs } from './Tabs';
|
||||
|
||||
export interface BlockProps<T> extends DocumentContextProps {
|
||||
block: T;
|
||||
@@ -37,6 +38,8 @@ export function Block<T>(props: BlockProps<T>) {
|
||||
return <Hint {...props} {...contextProps} />;
|
||||
case 'images':
|
||||
return <Images {...props} {...contextProps} />;
|
||||
case 'tabs':
|
||||
return <Tabs {...props} {...contextProps} />;
|
||||
default:
|
||||
return <div className={tcls(style)}>Unsupported block {block.type}</div>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Client side component for the tabs, taking care of interactions.
|
||||
*/
|
||||
export function DynamicTabs(props: {
|
||||
tabs: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}>;
|
||||
style: ClassValue;
|
||||
}) {
|
||||
const { tabs, style } = props;
|
||||
|
||||
const [active, setActive] = React.useState<null | string>(null);
|
||||
|
||||
return (
|
||||
<div className={tcls('rounded', 'border', 'border-slate-200', 'flex', 'flex-col', style)}>
|
||||
<div role="tablist" className={tcls('flex', 'flex-row', 'p-4')}>
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
role="tab"
|
||||
aria-selected={active === tab.id}
|
||||
aria-controls={`tabpanel-${tab.id}`}
|
||||
id={`tab-${tab.id}`}
|
||||
onClick={() => {
|
||||
setActive(tab.id);
|
||||
}}
|
||||
className={tcls(
|
||||
'bg-slate-50',
|
||||
'text-sm',
|
||||
'textslate-700',
|
||||
'rounded',
|
||||
'px-4',
|
||||
'py-2',
|
||||
'me-2',
|
||||
active === tab.id ? ['bg-slate-100'] : null,
|
||||
)}
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
role="tabpanel"
|
||||
id={`tabpanel-${tab.id}`}
|
||||
aria-labelledby={`tab-${tab.id}`}
|
||||
className={tcls('p-4', tab.id !== active ? 'hidden' : null)}
|
||||
>
|
||||
{tab.children}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
import { DynamicTabs } from './DynamicTabs';
|
||||
|
||||
export function Tabs(props: BlockProps<any>) {
|
||||
const { block, style, context } = props;
|
||||
|
||||
return (
|
||||
<DynamicTabs
|
||||
tabs={block.nodes.map((tab, index) => ({
|
||||
id: tab.key ?? index, // TODO: fix in API
|
||||
title: tab.data.title,
|
||||
children: <Blocks nodes={tab.nodes} context={context} />,
|
||||
}))}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Tabs';
|
||||
@@ -1,21 +1,19 @@
|
||||
import { getDocumentSections } from "@/lib/document";
|
||||
import { tcls } from "@/lib/tailwind";
|
||||
import { RevisionPageDocument } from "@gitbook/api";
|
||||
import React from "react";
|
||||
import { ScrollSectionsList } from "./ScrollSectionsList";
|
||||
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;
|
||||
}) {
|
||||
export function PageAside(props: { page: RevisionPageDocument; document: any }) {
|
||||
const { document } = props;
|
||||
const sections = getDocumentSections(document);
|
||||
|
||||
return (
|
||||
<aside className={tcls(
|
||||
<aside
|
||||
className={tcls(
|
||||
'hidden',
|
||||
'xl:flex',
|
||||
'flex-col',
|
||||
@@ -25,13 +23,12 @@ export function PageAside(props: {
|
||||
'sticky',
|
||||
'top-16',
|
||||
'h-[calc(100vh-4rem)]',
|
||||
'py-6'
|
||||
)}>
|
||||
'py-6',
|
||||
)}
|
||||
>
|
||||
{sections.length > 0 ? (
|
||||
<>
|
||||
<div className={tcls(
|
||||
'text-sm', 'text-slate-500', 'font-semibold', 'pb-3'
|
||||
)}>
|
||||
<div className={tcls('text-sm', 'text-slate-500', 'font-semibold', 'pb-3')}>
|
||||
On this page
|
||||
</div>
|
||||
<div className={tcls('overflow-auto', 'flex-1')}>
|
||||
@@ -41,9 +38,7 @@ export function PageAside(props: {
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<div></div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
'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";
|
||||
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[];
|
||||
}) {
|
||||
export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
||||
const { sections } = props;
|
||||
const [activeId, setActiveId] = React.useState(null);
|
||||
|
||||
@@ -18,15 +16,21 @@ export function ScrollSectionsList(props: {
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{sections.map(section => (
|
||||
{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',
|
||||
)}>
|
||||
<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}
|
||||
@@ -35,5 +39,5 @@ export function ScrollSectionsList(props: {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
+8
-5
@@ -1,4 +1,3 @@
|
||||
|
||||
export interface DocumentSection {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -21,8 +20,12 @@ 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') {
|
||||
document.nodes.forEach((block) => {
|
||||
if (
|
||||
block.type === 'heading-1' ||
|
||||
block.type === 'heading-2' ||
|
||||
block.type === 'heading-3'
|
||||
) {
|
||||
if (block.type === 'heading-1') {
|
||||
depth = 1;
|
||||
}
|
||||
@@ -32,7 +35,7 @@ export function getDocumentSections(document: any): DocumentSection[] {
|
||||
sections.push({
|
||||
id: block.data.id ?? title, // TODO: use slugify
|
||||
title,
|
||||
depth: block.type === 'heading-1' ? 1 : (depth > 0 ? 2 : 1),
|
||||
depth: block.type === 'heading-1' ? 1 : depth > 0 ? 2 : 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -49,7 +52,7 @@ export function getNodeText(node: any): string {
|
||||
return node.leaves.map((leaf) => leaf.text).join('');
|
||||
case 'block':
|
||||
case 'inline':
|
||||
return node.nodes.map(child => getNodeText(child)).join('');
|
||||
return node.nodes.map((child) => getNodeText(child)).join('');
|
||||
default:
|
||||
throw new Error('Invalid node');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user