mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-22 18:43:29 +00:00
Progressive loading (#79)
* Suspense boundary and progressive loading on blocks * Start refactoring to better leverage app router * Use skeleton for page layout * Improve dynamic toc * Switch page full width to be client side only * Format * Remove old PageLoading * Fix first page not marked as active * Close search when clicking search link
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { RevisionPage, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { pageHref } from '@/lib/links';
|
||||
import { getPagePath } from '@/lib/pages';
|
||||
import { ContentRefContext } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
@@ -11,64 +11,18 @@ import { ToggleableLinkItem } from './ToggleableLinkItem';
|
||||
export function PageDocumentItem(props: {
|
||||
rootPages: RevisionPage[];
|
||||
page: RevisionPageDocument;
|
||||
activePage: RevisionPageDocument;
|
||||
ancestors: Array<RevisionPageDocument | RevisionPageGroup>;
|
||||
context: ContentRefContext;
|
||||
}) {
|
||||
const { rootPages, page, activePage, ancestors, context } = props;
|
||||
|
||||
const hasActiveDescendant = ancestors.some((ancestor) => ancestor.id === page.id);
|
||||
|
||||
const linkProps = {
|
||||
href: pageHref(rootPages, page),
|
||||
className: tcls(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'justify-between',
|
||||
'pl-5',
|
||||
'pr-1.5',
|
||||
'py-1.5',
|
||||
'text-sm',
|
||||
'transition-colors',
|
||||
'relative',
|
||||
'textwrap-balance',
|
||||
'before:border-l',
|
||||
'before:absolute',
|
||||
'before:left-[-1px]',
|
||||
'before:top-0',
|
||||
'before:h-full',
|
||||
'rounded-md',
|
||||
'[&+div_a]:rounded-l-none',
|
||||
activePage.id === page.id
|
||||
? [
|
||||
'before:border-primary/6',
|
||||
'font-semibold',
|
||||
'text-primary',
|
||||
'hover:bg-primary/3',
|
||||
'dark:text-primary-400',
|
||||
'hover:before:border-primary',
|
||||
'dark:hover:bg-primary-500/3',
|
||||
'dark:hover:before:border-primary',
|
||||
]
|
||||
: [
|
||||
'before:border-transparent',
|
||||
'font-normal',
|
||||
'text-dark/8',
|
||||
'hover:bg-dark/1',
|
||||
'hover:before:border-dark/3',
|
||||
'dark:text-light/7',
|
||||
'dark:hover:bg-light/2',
|
||||
'dark:hover:before:border-light/3',
|
||||
],
|
||||
),
|
||||
};
|
||||
const { rootPages, page, ancestors, context } = props;
|
||||
|
||||
return (
|
||||
<li className={tcls('flex', 'flex-col')}>
|
||||
{page.pages && page.pages.length ? (
|
||||
<ToggleableLinkItem
|
||||
{...linkProps}
|
||||
descendants={
|
||||
<ToggleableLinkItem
|
||||
href={pageHref(rootPages, page)}
|
||||
pathname={getPagePath(rootPages, page)}
|
||||
descendants={
|
||||
page.pages && page.pages.length ? (
|
||||
<PagesList
|
||||
rootPages={rootPages}
|
||||
pages={page.pages}
|
||||
@@ -79,19 +33,14 @@ export function PageDocumentItem(props: {
|
||||
'border-dark/3',
|
||||
'dark:border-light/2',
|
||||
)}
|
||||
activePage={activePage}
|
||||
ancestors={ancestors}
|
||||
context={context}
|
||||
/>
|
||||
}
|
||||
defaultOpen={hasActiveDescendant || activePage.id === page.id}
|
||||
isActive={activePage.id === page.id}
|
||||
>
|
||||
{page.title}
|
||||
</ToggleableLinkItem>
|
||||
) : (
|
||||
<Link {...linkProps}>{page.title}</Link>
|
||||
)}
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{page.title}
|
||||
</ToggleableLinkItem>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,10 @@ import { PagesList } from './PagesList';
|
||||
export function PageGroupItem(props: {
|
||||
rootPages: RevisionPage[];
|
||||
page: RevisionPageGroup;
|
||||
activePage: RevisionPageDocument;
|
||||
ancestors: Array<RevisionPageDocument | RevisionPageGroup>;
|
||||
context: ContentRefContext;
|
||||
}) {
|
||||
const { rootPages, page, activePage, ancestors, context } = props;
|
||||
const { rootPages, page, ancestors, context } = props;
|
||||
|
||||
return (
|
||||
<li className={tcls('flex', 'flex-col')}>
|
||||
@@ -43,7 +42,6 @@ export function PageGroupItem(props: {
|
||||
<PagesList
|
||||
rootPages={rootPages}
|
||||
pages={page.pages}
|
||||
activePage={activePage}
|
||||
ancestors={ancestors}
|
||||
context={context}
|
||||
/>
|
||||
|
||||
@@ -10,12 +10,11 @@ import { PageLinkItem } from './PageLinkItem';
|
||||
export function PagesList(props: {
|
||||
rootPages: RevisionPage[];
|
||||
pages: RevisionPage[];
|
||||
activePage: RevisionPageDocument;
|
||||
ancestors: Array<RevisionPageDocument | RevisionPageGroup>;
|
||||
context: ContentRefContext;
|
||||
style?: ClassValue;
|
||||
}) {
|
||||
const { rootPages, pages, activePage, ancestors, context, style } = props;
|
||||
const { rootPages, pages, ancestors, context, style } = props;
|
||||
|
||||
return (
|
||||
<ul className={tcls('flex', 'flex-1', 'flex-col', 'gap-y-0.5', style)}>
|
||||
@@ -26,7 +25,6 @@ export function PagesList(props: {
|
||||
key={page.id}
|
||||
rootPages={rootPages}
|
||||
page={page}
|
||||
activePage={activePage}
|
||||
ancestors={ancestors}
|
||||
context={context}
|
||||
/>
|
||||
@@ -40,7 +38,6 @@ export function PagesList(props: {
|
||||
key={page.id}
|
||||
rootPages={rootPages}
|
||||
page={page}
|
||||
activePage={activePage}
|
||||
ancestors={ancestors}
|
||||
context={context}
|
||||
/>
|
||||
|
||||
@@ -20,7 +20,6 @@ export function TableOfContents(props: {
|
||||
content: ContentPointer;
|
||||
context: ContentRefContext;
|
||||
pages: Revision['pages'];
|
||||
activePage: RevisionPageDocument;
|
||||
ancestors: Array<RevisionPageDocument | RevisionPageGroup>;
|
||||
header?: React.ReactNode;
|
||||
withHeaderOffset: boolean;
|
||||
@@ -30,7 +29,6 @@ export function TableOfContents(props: {
|
||||
space,
|
||||
customization,
|
||||
pages,
|
||||
activePage,
|
||||
ancestors,
|
||||
header,
|
||||
context,
|
||||
@@ -95,7 +93,6 @@ export function TableOfContents(props: {
|
||||
<PagesList
|
||||
rootPages={pages}
|
||||
pages={pages}
|
||||
activePage={activePage}
|
||||
ancestors={ancestors}
|
||||
context={context}
|
||||
/>
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
import IconChevronRight from '@geist-ui/icons/chevronRight';
|
||||
import { motion, stagger, useAnimate } from 'framer-motion';
|
||||
import Link, { LinkProps } from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useSelectedLayoutSegment } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
/**
|
||||
* Client component to allow toggling of a page's children.
|
||||
*/
|
||||
|
||||
const show = {
|
||||
opacity: 1,
|
||||
height: 'auto',
|
||||
@@ -25,30 +22,53 @@ const hide = {
|
||||
},
|
||||
};
|
||||
|
||||
export function ToggleableLinkItem(
|
||||
props: LinkProps & {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
descendants?: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
isActive?: boolean;
|
||||
},
|
||||
) {
|
||||
const { children, descendants, defaultOpen = false, isActive = false, ...linkProps } = props;
|
||||
const staggerMenuItems = stagger(0.02, { ease: (p) => Math.pow(p, 2) });
|
||||
const staggerMenuItems = stagger(0.02, { ease: (p) => Math.pow(p, 2) });
|
||||
|
||||
/**
|
||||
* Client component for a page document to toggle its children and be marked as active.
|
||||
*/
|
||||
export function ToggleableLinkItem(props: {
|
||||
href: string;
|
||||
pathname: string;
|
||||
children: React.ReactNode;
|
||||
descendants: React.ReactNode;
|
||||
}) {
|
||||
const { href, children, descendants, pathname } = props;
|
||||
|
||||
const activeSegment = useSelectedLayoutSegment() ?? '';
|
||||
|
||||
const isActive = activeSegment === pathname;
|
||||
const hasDescendants = !!descendants;
|
||||
const hasActiveDescendant =
|
||||
hasDescendants && (isActive || activeSegment.startsWith(pathname + '/'));
|
||||
|
||||
const [scope, animate] = useAnimate();
|
||||
const [isVisible, setIsVisible] = useState(defaultOpen);
|
||||
const [isVisible, setIsVisible] = React.useState(hasActiveDescendant);
|
||||
|
||||
const toggleVisibility = () => {
|
||||
const willBecomeVisible = !isVisible;
|
||||
setIsVisible(willBecomeVisible);
|
||||
// Update the visibility of the children, if we are navigating to a descendant.
|
||||
React.useEffect(() => {
|
||||
if (!hasDescendants) {
|
||||
return;
|
||||
}
|
||||
setIsVisible((prev) => prev || hasActiveDescendant);
|
||||
|
||||
animate(scope.current, willBecomeVisible ? show : hide, {
|
||||
if (hasActiveDescendant) {
|
||||
console.log('activeSegment', { isActive, activeSegment, pathname });
|
||||
}
|
||||
}, [hasActiveDescendant, hasDescendants]);
|
||||
|
||||
// Animate the visibility of the children
|
||||
// only after the initial state.
|
||||
React.useEffect(() => {
|
||||
if (!mountedRef.current || !hasDescendants) {
|
||||
return;
|
||||
}
|
||||
|
||||
animate(scope.current, isVisible ? show : hide, {
|
||||
duration: 0.1,
|
||||
});
|
||||
|
||||
if (willBecomeVisible)
|
||||
if (isVisible)
|
||||
animate(
|
||||
'& > ul > li',
|
||||
{ opacity: 1 },
|
||||
@@ -56,65 +76,119 @@ export function ToggleableLinkItem(
|
||||
delay: staggerMenuItems,
|
||||
},
|
||||
);
|
||||
else animate('& > ul > li', { opacity: 0 });
|
||||
};
|
||||
else {
|
||||
animate('& > ul > li', { opacity: 0 });
|
||||
}
|
||||
}, [isVisible, hasDescendants]);
|
||||
|
||||
// Track if the component is mounted.
|
||||
const mountedRef = React.useRef(false);
|
||||
React.useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Link {...linkProps}>
|
||||
<Link
|
||||
href={href}
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'justify-between',
|
||||
'pl-5',
|
||||
'pr-1.5',
|
||||
'py-1.5',
|
||||
'text-sm',
|
||||
'transition-colors',
|
||||
'relative',
|
||||
'textwrap-balance',
|
||||
'before:border-l',
|
||||
'before:absolute',
|
||||
'before:left-[-1px]',
|
||||
'before:top-0',
|
||||
'before:h-full',
|
||||
'rounded-md',
|
||||
'[&+div_a]:rounded-l-none',
|
||||
isActive
|
||||
? [
|
||||
'before:border-primary/6',
|
||||
'font-semibold',
|
||||
'text-primary',
|
||||
'hover:bg-primary/3',
|
||||
'dark:text-primary-400',
|
||||
'hover:before:border-primary',
|
||||
'dark:hover:bg-primary-500/3',
|
||||
'dark:hover:before:border-primary',
|
||||
]
|
||||
: [
|
||||
'before:border-transparent',
|
||||
'font-normal',
|
||||
'text-dark/8',
|
||||
'hover:bg-dark/1',
|
||||
'hover:before:border-dark/3',
|
||||
'dark:text-light/7',
|
||||
'dark:hover:bg-light/2',
|
||||
'dark:hover:before:border-light/3',
|
||||
],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<span
|
||||
className={tcls(
|
||||
'group',
|
||||
'relative',
|
||||
'rounded-full',
|
||||
'w-5',
|
||||
'h-5',
|
||||
'after:grid-area-1-1',
|
||||
'after:absolute',
|
||||
'after:-top-1',
|
||||
'after:grid',
|
||||
'after:-left-1',
|
||||
'after:w-7',
|
||||
'after:h-7',
|
||||
'hover:bg-dark/2',
|
||||
'hover:text-current',
|
||||
'dark:hover:bg-light/2',
|
||||
'dark:hover:text-current',
|
||||
isActive ? ['hover:bg-primary/4', 'dark:hover:bg-primary/4'] : [],
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
toggleVisibility();
|
||||
}}
|
||||
>
|
||||
<IconChevronRight
|
||||
{hasDescendants ? (
|
||||
<span
|
||||
className={tcls(
|
||||
'grid',
|
||||
'flex-shrink-0',
|
||||
'group',
|
||||
'relative',
|
||||
'rounded-full',
|
||||
'w-5',
|
||||
'h-5',
|
||||
'p-0.5',
|
||||
'[&>path]:transition-[stroke-opacity]',
|
||||
'text-current',
|
||||
'transition-transform',
|
||||
'[&>path]:[stroke-opacity:0.40]',
|
||||
'group-hover:[&>path]:[stroke-opacity:1]',
|
||||
|
||||
isVisible ? ['rotate-90'] : ['rotate-0'],
|
||||
'after:grid-area-1-1',
|
||||
'after:absolute',
|
||||
'after:-top-1',
|
||||
'after:grid',
|
||||
'after:-left-1',
|
||||
'after:w-7',
|
||||
'after:h-7',
|
||||
'hover:bg-dark/2',
|
||||
'hover:text-current',
|
||||
'dark:hover:bg-light/2',
|
||||
'dark:hover:text-current',
|
||||
isActive ? ['hover:bg-primary/4', 'dark:hover:bg-primary/4'] : [],
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsVisible((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
<IconChevronRight
|
||||
className={tcls(
|
||||
'grid',
|
||||
'flex-shrink-0',
|
||||
'w-5',
|
||||
'h-5',
|
||||
'p-0.5',
|
||||
'[&>path]:transition-[stroke-opacity]',
|
||||
'text-current',
|
||||
'transition-transform',
|
||||
'[&>path]:[stroke-opacity:0.40]',
|
||||
'group-hover:[&>path]:[stroke-opacity:1]',
|
||||
|
||||
isVisible ? ['rotate-90'] : ['rotate-0'],
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
{/* TODO: fix recursive issue */}
|
||||
<motion.div
|
||||
ref={scope}
|
||||
className={tcls(isVisible ? null : '[&_ul>li]:opacity-1')}
|
||||
initial={isVisible ? show : hide}
|
||||
>
|
||||
{descendants}
|
||||
</motion.div>
|
||||
{hasDescendants ? (
|
||||
<motion.div
|
||||
ref={scope}
|
||||
className={tcls(isVisible ? null : '[&_ul>li]:opacity-1')}
|
||||
initial={isVisible ? show : hide}
|
||||
>
|
||||
{descendants}
|
||||
</motion.div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user