Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🪆 Improve Table of Contents #254

Merged
merged 1 commit into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions packages/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@
"dependencies": {
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.18",
"@myst-theme/common": "^0.5.10",
"@myst-theme/diagrams": "^0.5.10",
"@myst-theme/frontmatter": "^0.5.10",
"@myst-theme/jupyter": "^0.5.10",
"@myst-theme/common": "^0.5.10",
"@myst-theme/providers": "^0.5.10",
"@radix-ui/react-collapsible": "^1.0.3",
"classnames": "^2.3.2",
"lodash.throttle": "^4.1.1",
"myst-common": "^1.1.8",
"myst-spec-ext": "^1.1.8",
"myst-config": "^1.1.8",
"myst-demo": "^0.5.10",
"myst-spec-ext": "^1.1.8",
"myst-to-react": "^0.5.10",
"nbtx": "^0.2.3",
"node-cache": "^5.1.2",
Expand All @@ -36,10 +37,10 @@
"unist-util-select": "^4.0.1"
},
"peerDependencies": {
"@remix-run/node": "^1.17 || ^2.0",
"@remix-run/react": "^1.17 || ^2.0",
"@types/react": "^16.8 || ^17.0 || ^18.0",
"@types/react-dom": "^16.8 || ^17.0 || ^18.0",
"@remix-run/react": "^1.17 || ^2.0",
"@remix-run/node": "^1.17 || ^2.0",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
Expand Down
111 changes: 5 additions & 106 deletions packages/site/src/components/Navigation/TableOfContents.tsx
Original file line number Diff line number Diff line change
@@ -1,115 +1,14 @@
import React, { useEffect, useRef } from 'react';
import classNames from 'classnames';
import { useLocation, useNavigation } from '@remix-run/react';
import type { SiteManifest } from 'myst-config';
import { useNavigation } from '@remix-run/react';
import {
useNavLinkProvider,
useNavOpen,
useSiteManifest,
useBaseurl,
withBaseurl,
useGridSystemProvider,
useThemeTop,
} from '@myst-theme/providers';
import { getProjectHeadings, type Heading } from '@myst-theme/common';

type Props = {
folder?: string;
headings: Heading[];
sections?: ManifestProject[];
};

type ManifestProject = Required<SiteManifest>['projects'][0];

const HeadingLink = ({
path,
isIndex,
title,
children,
}: {
path: string;
isIndex?: boolean;
title?: string;
children: React.ReactNode;
}) => {
const { pathname } = useLocation();
const NavLink = useNavLinkProvider();
const exact = pathname === path;
const baseurl = useBaseurl();
const [, setOpen] = useNavOpen();
return (
<NavLink
prefetch="intent"
title={title}
className={({ isActive }: { isActive: boolean }) =>
classNames('block break-words', {
'mb-8 lg:mb-3 font-semibold': isIndex,
'text-slate-900 dark:text-slate-200': isIndex && !exact,
'text-blue-500 dark:text-blue-400': isIndex && exact,
'border-l pl-4 text-blue-500 border-current dark:text-blue-400': !isIndex && isActive,
'font-semibold': isActive,
'border-l pl-4 border-transparent hover:border-slate-400 dark:hover:border-slate-500 text-slate-700 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-300':
!isIndex && !isActive,
})
}
to={withBaseurl(path, baseurl)}
suppressHydrationWarning // The pathname is not defined on the server always.
onClick={() => {
// Close the nav panel if it is open
setOpen(false);
}}
>
{children}
</NavLink>
);
};

const HEADING_CLASSES = 'text-slate-900 leading-5 dark:text-slate-100';

const Headings = ({ folder, headings, sections }: Props) => {
const secs = sections || [];
return (
<ul className="space-y-6 lg:space-y-2">
{secs.map((sec) => {
if (sec.slug === folder) {
return headings.map((heading, index) => (
<li
key={heading.slug || index}
className={classNames('', {
[HEADING_CLASSES]: heading.level === 'index',
'font-semibold': heading.level === 'index',
// 'pl-4': heading.level === 2,
// 'pl-6': heading.level === 3,
// 'pl-8': heading.level === 4,
// 'pl-10': heading.level === 5,
// 'pl-12': heading.level === 6,
})}
>
{heading.path ? (
<HeadingLink
title={heading.title}
path={heading.path}
isIndex={heading.level === 'index'}
>
{heading.short_title || heading.title}
</HeadingLink>
) : (
<h5 className="mb-3 font-semibold break-words lg:mt-8 dark:text-white">
{heading.short_title || heading.title}
</h5>
)}
</li>
));
}
return (
<li key={sec.slug} className={classNames('p-1 my-2 lg:hidden', HEADING_CLASSES)}>
<HeadingLink path={`/${sec.slug}`}>{sec.short_title || sec.title}</HeadingLink>
</li>
);
})}
</ul>
);
};
import { getProjectHeadings } from '@myst-theme/common';
import { Toc } from './TableOfContentsItems.js';

export function useTocHeight<T extends HTMLElement = HTMLElement>(top = 0, inset = 0) {
const container = useRef<T>(null);
Expand Down Expand Up @@ -189,7 +88,7 @@ export const TableOfContents = ({
aria-label="Table of Contents"
className="flex-grow overflow-y-auto transition-opacity mt-6 pb-3 ml-3 xl:ml-0 mr-3 max-w-[350px]"
>
<Headings folder={projectSlug} headings={headings} sections={config?.projects} />
<Toc headings={headings} />
</nav>
{footer && (
<div
Expand Down Expand Up @@ -221,7 +120,7 @@ export const InlineTableOfContents = ({
if (!headings) return null;
return (
<nav aria-label="Table of Contents" className={className} ref={tocRef}>
<Headings folder={projectSlug} headings={headings} sections={config?.projects} />
<Toc headings={headings} />
</nav>
);
};
162 changes: 162 additions & 0 deletions packages/site/src/components/Navigation/TableOfContentsItems.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React, { useEffect } from 'react';
import classNames from 'classnames';
import * as Collapsible from '@radix-ui/react-collapsible';
import type { Heading } from '@myst-theme/common';
import { useBaseurl, useNavLinkProvider, useNavOpen, withBaseurl } from '@myst-theme/providers';
import { useLocation, useNavigation } from '@remix-run/react';
import { ChevronRightIcon } from '@heroicons/react/24/solid';

type NestedHeading = Heading & { id: string; children: NestedHeading[] };

function nestToc(toc: Heading[]): NestedHeading[] {
const items: NestedHeading[] = [];
const stack: (Omit<NestedHeading, 'level'> & { level: number })[] = [];

toc.forEach((tocItem, id) => {
const item = tocItem as NestedHeading;
item.children = [];
item.id = String(id);
if (item.level === 'index') {
while (stack.length) stack.pop();
items.push(item);
return;
}
while (stack.length && stack[stack.length - 1].level >= item.level) {
stack.pop();
}
const top = stack[stack.length - 1];
if (top) {
top.children.push(item);
} else {
items.push(item);
}
stack.push(item as any);
});
return items;
}

function childrenOpen(headings: NestedHeading[], pathname: string): string[] {
return headings
.map((heading) => {
if (heading.path === pathname) return [heading.id];
const open = childrenOpen(heading.children, pathname);
if (open.length === 0) return [];
return [heading.id, ...open];
})
.flat();
}

export const Toc = ({ headings }: { headings: Heading[] }) => {
const nested = nestToc(headings);
return (
<div className="w-full px-1 dark:text-white">
{nested.map((item) => (
<NestedToc heading={item} key={item.id} />
))}
</div>
);
};

function LinkItem({
className,
heading,
onClick,
}: {
className?: string;
heading: NestedHeading;
onClick?: () => void;
}) {
const NavLink = useNavLinkProvider();
const baseurl = useBaseurl();
const [, setOpen] = useNavOpen();
if (!heading.path) {
return (
<div
title={heading.title}
className={classNames('block break-words rounded', className)}
onClick={() => {
onClick?.();
}}
>
{heading.short_title || heading.title}
</div>
);
}
return (
<NavLink
prefetch="intent"
title={heading.title}
className={classNames(
'block break-words focus:outline outline-blue-200 outline-2 rounded',
className,
)}
to={withBaseurl(heading.path, baseurl)}
onClick={() => {
onClick?.();
setOpen(false);
}}
>
{heading.short_title || heading.title}
</NavLink>
);
}

const NestedToc = ({ heading }: { heading: NestedHeading }) => {
const { pathname } = useLocation();
const startOpen = childrenOpen([heading], pathname).includes(heading.id);
const nav = useNavigation();
nav.state;
const [open, setOpen] = React.useState(startOpen);
useEffect(() => {
if (nav.state === 'idle') setOpen(startOpen);
}, [nav.state]);
const exact = pathname === heading.path;
if (!heading.children || heading.children.length === 0) {
return (
<LinkItem
className={classNames('p-2 my-1 rounded-lg', {
'bg-blue-300/30': exact,
'hover:bg-slate-300/30': !exact,
'font-bold': heading.level === 'index',
})}
heading={heading}
/>
);
}
return (
<Collapsible.Root className="w-full" open={open} onOpenChange={setOpen}>
<div
className={classNames(
'flex flex-row w-full gap-2 px-2 my-1 text-left rounded-lg outline-none',
{
'bg-blue-300/30': exact,
'hover:bg-slate-300/30': !exact,
},
)}
>
<LinkItem
className={classNames('py-2 grow', {
'font-semibold text-blue-800 dark:text-blue-200': startOpen,
'cursor-pointer': !heading.path,
})}
heading={heading}
onClick={() => setOpen(heading.path ? true : !open)}
/>
<Collapsible.Trigger asChild>
<button className="self-center flex-none rounded-md group hover:bg-slate-300/30 focus:outline outline-blue-200 outline-2">
<ChevronRightIcon
className="transition-transform duration-300 group-data-[state=open]:rotate-90 text-text-slate-700 dark:text-slate-100"
height="1.5rem"
width="1.5rem"
/>
</button>
</Collapsible.Trigger>
</div>
<Collapsible.Content className="pl-3 pr-[2px] collapsible-content">
{heading.children.map((item) => (
<NestedToc heading={item} key={item.id} />
))}
</Collapsible.Content>
</Collapsible.Root>
);
};
1 change: 1 addition & 0 deletions styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
@import './grid.css';
@import './hover.css';
@import './proof.css';
@import './toc.css';
Loading
Loading