From f0b52512f1c2b723d836ec644be042f4d66a7d5b Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Fri, 23 Aug 2024 15:05:07 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AD=20Move=20collapsed=20top-bar=20to?= =?UTF-8?q?=20primary=20sidebar=20on=20small=20screens=20(#444)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/forty-carrots-cry.md | 7 + .changeset/mighty-ears-breathe.md | 6 + .changeset/olive-baboons-accept.md | 6 + .changeset/strong-cars-beam.md | 5 + packages/common/src/utils.ts | 5 +- packages/frontmatter/src/FrontmatterBlock.tsx | 2 +- packages/site/CHANGELOG.md | 4 +- packages/site/package.json | 6 +- packages/site/src/components/Headers.tsx | 2 +- .../Navigation/InlineTableOfContents.tsx | 25 +++ .../site/src/components/Navigation/Link.tsx | 47 ++++ .../src/components/Navigation/Navigation.tsx | 87 +++++++- .../components/Navigation/PrimarySidebar.tsx | 201 ++++++++++++++++++ .../components/Navigation/TableOfContents.tsx | 126 ----------- .../site/src/components/Navigation/TopNav.tsx | 38 +--- .../site/src/components/Navigation/index.tsx | 5 +- packages/site/src/pages/Article.tsx | 2 +- themes/book/app/components/ArticlePage.tsx | 8 +- themes/book/app/routes/$.tsx | 16 +- 19 files changed, 405 insertions(+), 193 deletions(-) create mode 100644 .changeset/forty-carrots-cry.md create mode 100644 .changeset/mighty-ears-breathe.md create mode 100644 .changeset/olive-baboons-accept.md create mode 100644 .changeset/strong-cars-beam.md create mode 100644 packages/site/src/components/Navigation/InlineTableOfContents.tsx create mode 100644 packages/site/src/components/Navigation/Link.tsx create mode 100644 packages/site/src/components/Navigation/PrimarySidebar.tsx delete mode 100644 packages/site/src/components/Navigation/TableOfContents.tsx diff --git a/.changeset/forty-carrots-cry.md b/.changeset/forty-carrots-cry.md new file mode 100644 index 000000000..8b41efaf4 --- /dev/null +++ b/.changeset/forty-carrots-cry.md @@ -0,0 +1,7 @@ +--- +'@myst-theme/frontmatter': patch +'@myst-theme/site': patch +'@myst-theme/book': patch +--- + +Some alignment fixes, leaving more control over content top alignment to the theme diff --git a/.changeset/mighty-ears-breathe.md b/.changeset/mighty-ears-breathe.md new file mode 100644 index 000000000..4943e7c27 --- /dev/null +++ b/.changeset/mighty-ears-breathe.md @@ -0,0 +1,6 @@ +--- +'@myst-theme/site': patch +'@myst-theme/book': patch +--- + +Renamed `Navigation` component and split for re-use in different (composed/multi-site) themes diff --git a/.changeset/olive-baboons-accept.md b/.changeset/olive-baboons-accept.md new file mode 100644 index 000000000..af94e3e82 --- /dev/null +++ b/.changeset/olive-baboons-accept.md @@ -0,0 +1,6 @@ +--- +'@myst-theme/site': minor +'@myst-theme/book': patch +--- + +Rework TOC to PrimarySidebar diff --git a/.changeset/strong-cars-beam.md b/.changeset/strong-cars-beam.md new file mode 100644 index 000000000..d9758f0f8 --- /dev/null +++ b/.changeset/strong-cars-beam.md @@ -0,0 +1,5 @@ +--- +'@myst-theme/common': patch +--- + +Modified `getProjectHeadings` to work with plain `projectSlugs` to support custom theme routes that use `baseurl` but have no separate project. diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index e12428f1f..f9994d377 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -40,7 +40,10 @@ export function getProjectHeadings( }, ...project.pages.map((p) => { if (!('slug' in p)) return p; - return { ...p, path: projectSlug ? `/${project.slug}/${p.slug}` : `/${p.slug}` }; + return { + ...p, + path: projectSlug && project.slug ? `/${project.slug}/${p.slug}` : `/${p.slug}`, + }; }), ]; if (opts.addGroups) { diff --git a/packages/frontmatter/src/FrontmatterBlock.tsx b/packages/frontmatter/src/FrontmatterBlock.tsx index 56dab11cd..d311cf643 100644 --- a/packages/frontmatter/src/FrontmatterBlock.tsx +++ b/packages/frontmatter/src/FrontmatterBlock.tsx @@ -235,7 +235,7 @@ export function FrontmatterBlock({ className={classNames(className)} > {showHeaderBlock && ( -
+
{subject && (
+
``` diff --git a/packages/site/package.json b/packages/site/package.json index 40ac60f72..042730319 100644 --- a/packages/site/package.json +++ b/packages/site/package.json @@ -10,9 +10,13 @@ "license": "MIT", "sideEffects": false, "scripts": { + "clean": "rimraf dist", "compile": "tsc --noEmit", "lint": "eslint src/**/*.ts*", - "lint:format": "prettier --check \"src/**/*.{ts,tsx,md}\"" + "lint:format": "prettier --check \"src/**/*.{ts,tsx,md}\"", + "dev": "npm-run-all --parallel \"build:* -- --watch\"", + "build:esm": "tsc", + "build": "npm-run-all -l clean -p build:esm" }, "dependencies": { "@headlessui/react": "^1.7.15", diff --git a/packages/site/src/components/Headers.tsx b/packages/site/src/components/Headers.tsx index 3e388edd2..0584a7ce9 100644 --- a/packages/site/src/components/Headers.tsx +++ b/packages/site/src/components/Headers.tsx @@ -85,7 +85,7 @@ export function ArticleHeader({ frontmatter={rest} authorStyle="list" className={classNames('flex-grow', { - 'pt-4 px-6': frontmatter?.banner, + 'pt-6 px-6': frontmatter?.banner, ...positionFrontmatter, })} hideBadges diff --git a/packages/site/src/components/Navigation/InlineTableOfContents.tsx b/packages/site/src/components/Navigation/InlineTableOfContents.tsx new file mode 100644 index 000000000..eebcb9233 --- /dev/null +++ b/packages/site/src/components/Navigation/InlineTableOfContents.tsx @@ -0,0 +1,25 @@ +import { useSiteManifest } from '@myst-theme/providers'; +import { getProjectHeadings } from '@myst-theme/common'; +import { Toc } from './TableOfContentsItems.js'; + +export const InlineTableOfContents = ({ + projectSlug, + sidebarRef, + className = 'flex-grow overflow-y-auto max-w-[350px]', +}: { + projectSlug?: string; + className?: string; + sidebarRef?: React.RefObject; +}) => { + const config = useSiteManifest(); + if (!config) return null; + const headings = getProjectHeadings(config, projectSlug, { + addGroups: false, + }); + if (!headings) return null; + return ( + + ); +}; diff --git a/packages/site/src/components/Navigation/Link.tsx b/packages/site/src/components/Navigation/Link.tsx new file mode 100644 index 000000000..281110c9a --- /dev/null +++ b/packages/site/src/components/Navigation/Link.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { useLinkProvider, useNavLinkProvider } from '@myst-theme/providers'; + +export function ExternalOrInternalLink({ + to, + className, + children, + nav, + onClick, + prefetch = 'intent', +}: { + to: string; + className?: string | ((props: { isActive: boolean }) => string); + children: React.ReactNode; + nav?: boolean; + onClick?: () => void; + prefetch?: 'intent' | 'render' | 'none'; +}) { + const Link = useLinkProvider(); + const NavLink = useNavLinkProvider(); + const staticClass = typeof className === 'function' ? className({ isActive: false }) : className; + if (to.startsWith('http') || to.startsWith('mailto:')) { + return ( + + {children} + + ); + } + if (nav) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); +} diff --git a/packages/site/src/components/Navigation/Navigation.tsx b/packages/site/src/components/Navigation/Navigation.tsx index 11156f30c..ded97ff03 100644 --- a/packages/site/src/components/Navigation/Navigation.tsx +++ b/packages/site/src/components/Navigation/Navigation.tsx @@ -1,37 +1,106 @@ -import { useNavOpen, useThemeTop } from '@myst-theme/providers'; -import { TableOfContents } from './TableOfContents.js'; +import { useNavOpen, useSiteManifest, useThemeTop } from '@myst-theme/providers'; +import { PrimarySidebar } from './PrimarySidebar.js'; +import type { Heading } from '@myst-theme/common'; +import { getProjectHeadings } from '@myst-theme/common'; +import type { SiteManifest } from 'myst-config'; -export function Navigation({ +/** + * PrimaryNavigation will load nav links and headers from the site manifest and display + * them in a mobile-friendly format. + */ +export const PrimaryNavigation = ({ children, projectSlug, - tocRef, + sidebarRef, hide_toc, + mobileOnly, footer, }: { children?: React.ReactNode; projectSlug?: string; - tocRef?: React.RefObject; + sidebarRef?: React.RefObject; hide_toc?: boolean; + mobileOnly?: boolean; footer?: React.ReactNode; -}) { +}) => { + const config = useSiteManifest(); + if (!config) return null; + + const headings = getProjectHeadings(config, projectSlug, { + addGroups: false, + }); + + const { nav } = config; + + return ( + + ); +}; + +/** +@deprecated use PrimaryNavigation instead + */ +export const Navigation = PrimaryNavigation; + +/** + * ConfigurablePrimaryNavigation will display a mobile-friendly navigation sidebar based on the + * nav, headings, and footer provided by the caller. Use this in situations where the PrimaryNavigation + * component may pick up the wrong SiteManifest. + */ +export const ConfigurablePrimaryNavigation = ({ + children, + sidebarRef, + hide_toc, + mobileOnly, + nav, + headings, + footer, +}: { + children?: React.ReactNode; + sidebarRef?: React.RefObject; + hide_toc?: boolean; + mobileOnly?: boolean; + nav?: SiteManifest['nav']; + headings?: Heading[]; + footer?: React.ReactNode; +}) => { const [open, setOpen] = useNavOpen(); const top = useThemeTop(); + if (children) console.warn( `Including children in Navigation can break keyboard accessbility and is deprecated. Please move children to the page component.`, ); + + // the logic on the following line looks wrong, this will return `null` or `<>` + // we should just return `null` if `hide_toc` is true? if (hide_toc) return children ? null : <>{children}; + return ( <> - {open && ( + {open && !mobileOnly && headings && (
setOpen(false)} >
)} - + {children} ); -} +}; diff --git a/packages/site/src/components/Navigation/PrimarySidebar.tsx b/packages/site/src/components/Navigation/PrimarySidebar.tsx new file mode 100644 index 000000000..af1287a85 --- /dev/null +++ b/packages/site/src/components/Navigation/PrimarySidebar.tsx @@ -0,0 +1,201 @@ +import React, { useEffect, useRef } from 'react'; +import classNames from 'classnames'; +import { useNavigation } from '@remix-run/react'; +import { + useNavOpen, + useSiteManifest, + useGridSystemProvider, + useThemeTop, +} from '@myst-theme/providers'; +import type { Heading } from '@myst-theme/common'; +import { Toc } from './TableOfContentsItems.js'; +import { ExternalOrInternalLink } from './Link.js'; +import type { SiteManifest, SiteNavItem } from 'myst-config'; +import * as Collapsible from '@radix-ui/react-collapsible'; +import { ChevronRightIcon } from '@heroicons/react/24/solid'; + +export function SidebarNavItem({ item }: { item: SiteNavItem }) { + if (!item.children?.length) { + return ( + + {item.title} + + ); + } + const [open, setOpen] = React.useState(false); + return ( + +
+ setOpen(!open)} + > + {item.title} + + + + +
+ + {item.children.map((action) => ( + + {action.title} + + ))} + +
+ ); +} + +export function SidebarNav({ nav }: { nav?: SiteManifest['nav'] }) { + if (!nav) return null; + return ( +
+ {nav.map((item) => { + return ; + })} +
+ ); +} + +export function useSidebarHeight(top = 0, inset = 0) { + const container = useRef(null); + const toc = useRef(null); + const transitionState = useNavigation().state; + const setHeight = () => { + if (!container.current || !toc.current) return; + const height = container.current.offsetHeight - window.scrollY; + const div = toc.current.firstChild as HTMLDivElement; + if (div) div.style.height = `min(calc(100vh - ${top}px), ${height + inset}px)`; + const nav = toc.current.querySelector('nav'); + if (nav) nav.style.opacity = height > 150 ? '1' : '0'; + }; + useEffect(() => { + setHeight(); + setTimeout(setHeight, 100); // Some lag sometimes + const handleScroll = () => setHeight(); + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [container, toc, transitionState]); + return { container, toc }; +} + +export const PrimarySidebar = ({ + sidebarRef, + nav, + footer, + headings, + mobileOnly, +}: { + sidebarRef?: React.RefObject; + nav?: SiteManifest['nav']; + headings?: Heading[]; + footer?: React.ReactNode; + mobileOnly?: boolean; +}) => { + const top = useThemeTop(); + const grid = useGridSystemProvider(); + const footerRef = useRef(null); + const [open] = useNavOpen(); + const config = useSiteManifest(); + + useEffect(() => { + setTimeout(() => { + if (!footerRef.current) return; + footerRef.current.style.opacity = '1'; + footerRef.current.style.transform = 'none'; + }, 500); + }, [footerRef]); + if (!config) return null; + + return ( +
+
+
+ {nav && ( + + )} + {nav && headings &&
} + {headings && ( + + )} +
+ {footer && ( +
+ {footer} +
+ )} +
+
+ ); +}; diff --git a/packages/site/src/components/Navigation/TableOfContents.tsx b/packages/site/src/components/Navigation/TableOfContents.tsx deleted file mode 100644 index d2f2fa541..000000000 --- a/packages/site/src/components/Navigation/TableOfContents.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import classNames from 'classnames'; -import { useNavigation } from '@remix-run/react'; -import { - useNavOpen, - useSiteManifest, - useGridSystemProvider, - useThemeTop, -} from '@myst-theme/providers'; -import { getProjectHeadings } from '@myst-theme/common'; -import { Toc } from './TableOfContentsItems.js'; - -export function useTocHeight(top = 0, inset = 0) { - const container = useRef(null); - const toc = useRef(null); - const transitionState = useNavigation().state; - const setHeight = () => { - if (!container.current || !toc.current) return; - const height = container.current.offsetHeight - window.scrollY; - const div = toc.current.firstChild as HTMLDivElement; - if (div) div.style.height = `min(calc(100vh - ${top}px), ${height + inset}px)`; - const nav = toc.current.querySelector('nav'); - if (nav) nav.style.opacity = height > 150 ? '1' : '0'; - }; - useEffect(() => { - setHeight(); - setTimeout(setHeight, 100); // Some lag sometimes - const handleScroll = () => setHeight(); - window.addEventListener('scroll', handleScroll); - return () => { - window.removeEventListener('scroll', handleScroll); - }; - }, [container, toc, transitionState]); - return { container, toc }; -} - -export const TableOfContents = ({ - projectSlug, - tocRef, - footer, -}: { - tocRef?: React.RefObject; - projectSlug?: string; - footer?: React.ReactNode; -}) => { - const top = useThemeTop(); - const grid = useGridSystemProvider(); - const footerRef = useRef(null); - const [open] = useNavOpen(); - const config = useSiteManifest(); - useEffect(() => { - setTimeout(() => { - if (!footerRef.current) return; - footerRef.current.style.opacity = '1'; - footerRef.current.style.transform = 'none'; - }, 500); - }, [footerRef]); - if (!config) return null; - const headings = getProjectHeadings(config, projectSlug, { - addGroups: false, - }); - if (!headings) return null; - return ( -
-
- - {footer && ( -
- {footer} -
- )} -
-
- ); -}; - -export const InlineTableOfContents = ({ - projectSlug, - tocRef, - className = 'flex-grow overflow-y-auto max-w-[350px]', -}: { - projectSlug?: string; - className?: string; - tocRef?: React.RefObject; -}) => { - const config = useSiteManifest(); - if (!config) return null; - const headings = getProjectHeadings(config, projectSlug, { - addGroups: false, - }); - if (!headings) return null; - return ( - - ); -}; diff --git a/packages/site/src/components/Navigation/TopNav.tsx b/packages/site/src/components/Navigation/TopNav.tsx index 245c7e2fc..d77292ebf 100644 --- a/packages/site/src/components/Navigation/TopNav.tsx +++ b/packages/site/src/components/Navigation/TopNav.tsx @@ -13,46 +13,10 @@ import { import { LoadingBar } from './Loading.js'; import { HomeLink } from './HomeLink.js'; import { ActionMenu } from './ActionMenu.js'; +import { ExternalOrInternalLink } from './Link.js'; export const DEFAULT_NAV_HEIGHT = 60; -function ExternalOrInternalLink({ - to, - className, - children, - nav, - prefetch = 'intent', -}: { - to: string; - className?: string | ((props: { isActive: boolean }) => string); - children: React.ReactNode; - nav?: boolean; - prefetch?: 'intent' | 'render' | 'none'; -}) { - const Link = useLinkProvider(); - const NavLink = useNavLinkProvider(); - const staticClass = typeof className === 'function' ? className({ isActive: false }) : className; - if (to.startsWith('http') || to.startsWith('mailto:')) { - return ( - - {children} - - ); - } - if (nav) { - return ( - - {children} - - ); - } - return ( - - {children} - - ); -} - export function NavItem({ item }: { item: SiteNavItem }) { const NavLink = useNavLinkProvider(); if (!('children' in item)) { diff --git a/packages/site/src/components/Navigation/index.tsx b/packages/site/src/components/Navigation/index.tsx index 52700ea94..bebe3b2e2 100644 --- a/packages/site/src/components/Navigation/index.tsx +++ b/packages/site/src/components/Navigation/index.tsx @@ -1,7 +1,8 @@ export { ThemeButton } from './ThemeButton.js'; export { TopNav, NavItems, NavItem, DEFAULT_NAV_HEIGHT } from './TopNav.js'; -export { Navigation } from './Navigation.js'; -export { TableOfContents, InlineTableOfContents, useTocHeight } from './TableOfContents.js'; +export { Navigation, PrimaryNavigation, ConfigurablePrimaryNavigation } from './Navigation.js'; +export { PrimarySidebar, useSidebarHeight } from './PrimarySidebar.js'; +export { InlineTableOfContents } from './InlineTableOfContents.js'; export { LoadingBar } from './Loading.js'; export { ActionMenu } from './ActionMenu.js'; export { HomeLink } from './HomeLink.js'; diff --git a/packages/site/src/pages/Article.tsx b/packages/site/src/pages/Article.tsx index 704503c4a..beee678ea 100644 --- a/packages/site/src/pages/Article.tsx +++ b/packages/site/src/pages/Article.tsx @@ -57,7 +57,7 @@ export const ArticlePage = React.memo(function ({ )} {compute?.enabled && diff --git a/themes/book/app/components/ArticlePage.tsx b/themes/book/app/components/ArticlePage.tsx index 2c70eb624..4438ff782 100644 --- a/themes/book/app/components/ArticlePage.tsx +++ b/themes/book/app/components/ArticlePage.tsx @@ -87,15 +87,15 @@ export const ArticlePage = React.memo(function ({ )} {!hide_outline && (
- +
)} {compute?.enabled && diff --git a/themes/book/app/routes/$.tsx b/themes/book/app/routes/$.tsx index a2a2cc869..83a068515 100644 --- a/themes/book/app/routes/$.tsx +++ b/themes/book/app/routes/$.tsx @@ -8,8 +8,8 @@ import { getProject, isFlatSite, type PageLoader } from '@myst-theme/common'; import { KatexCSS, useOutlineHeight, - useTocHeight, - Navigation, + useSidebarHeight, + PrimaryNavigation, TopNav, getMetaTagsForArticle, ArticlePageCatchBoundary, @@ -83,12 +83,12 @@ export function ArticlePageAndNavigation({ inset?: number; }) { const top = useThemeTop(); - const { container, toc } = useTocHeight(top, inset); + const { container, toc } = useSidebarHeight(top, inset); return ( - - + } projectSlug={projectSlug} @@ -97,7 +97,8 @@ export function ArticlePageAndNavigation({
{children}
@@ -106,7 +107,6 @@ export function ArticlePageAndNavigation({ ); } - export default function Page() { const { container } = useOutlineHeight(); const data = useLoaderData() as { page: PageLoader; project: ManifestProject };