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:
Samy Pessé
2023-12-28 14:38:15 +01:00
committed by GitHub
parent 1c2d68c518
commit 2fdf3e3838
41 changed files with 722 additions and 574 deletions
@@ -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}
/>
+1 -4
View File
@@ -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>
);
}