@myst-theme/site 0.9.10 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myst-theme/site",
3
- "version": "0.9.10",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -10,35 +10,39 @@
10
10
  "license": "MIT",
11
11
  "sideEffects": false,
12
12
  "scripts": {
13
+ "clean": "rimraf dist",
13
14
  "compile": "tsc --noEmit",
14
15
  "lint": "eslint src/**/*.ts*",
15
- "lint:format": "prettier --check \"src/**/*.{ts,tsx,md}\""
16
+ "lint:format": "prettier --check \"src/**/*.{ts,tsx,md}\"",
17
+ "dev": "npm-run-all --parallel \"build:* -- --watch\"",
18
+ "build:esm": "tsc",
19
+ "build": "npm-run-all -l clean -p build:esm"
16
20
  },
17
21
  "dependencies": {
18
22
  "@headlessui/react": "^1.7.15",
19
23
  "@heroicons/react": "^2.0.18",
20
- "@myst-theme/common": "^0.9.10",
21
- "@myst-theme/diagrams": "^0.9.10",
22
- "@myst-theme/frontmatter": "^0.9.10",
23
- "@myst-theme/jupyter": "^0.9.10",
24
- "@myst-theme/providers": "^0.9.10",
24
+ "@myst-theme/common": "^0.11.0",
25
+ "@myst-theme/diagrams": "^0.11.0",
26
+ "@myst-theme/frontmatter": "^0.11.0",
27
+ "@myst-theme/jupyter": "^0.11.0",
28
+ "@myst-theme/providers": "^0.11.0",
25
29
  "@radix-ui/react-collapsible": "^1.0.3",
26
30
  "classnames": "^2.3.2",
27
31
  "lodash.throttle": "^4.1.1",
28
- "myst-common": "^1.5.0",
29
- "myst-config": "^1.5.0",
30
- "myst-demo": "^0.9.10",
31
- "myst-spec-ext": "^1.5.0",
32
- "myst-to-react": "^0.9.10",
32
+ "myst-common": "^1.6.0",
33
+ "myst-config": "^1.6.0",
34
+ "myst-demo": "^0.11.0",
35
+ "myst-spec-ext": "^1.6.0",
36
+ "myst-to-react": "^0.11.0",
33
37
  "nbtx": "^0.2.3",
34
38
  "node-cache": "^5.1.2",
35
39
  "node-fetch": "^2.6.11",
36
- "thebe-react": "^0.4.6",
40
+ "thebe-react": "0.4.7",
37
41
  "unist-util-select": "^4.0.1"
38
42
  },
39
43
  "peerDependencies": {
40
- "@remix-run/node": "^1.17 || ^2.0",
41
- "@remix-run/react": "^1.17 || ^2.0",
44
+ "@remix-run/node": "^1.19",
45
+ "@remix-run/react": "^1.19",
42
46
  "@types/react": "^16.8 || ^17.0 || ^18.0",
43
47
  "@types/react-dom": "^16.8 || ^17.0 || ^18.0",
44
48
  "react": "^16.8 || ^17.0 || ^18.0",
@@ -85,7 +85,7 @@ export function ArticleHeader({
85
85
  frontmatter={rest}
86
86
  authorStyle="list"
87
87
  className={classNames('flex-grow', {
88
- 'pt-4 px-6': frontmatter?.banner,
88
+ 'pt-6 px-6': frontmatter?.banner,
89
89
  ...positionFrontmatter,
90
90
  })}
91
91
  hideBadges
@@ -0,0 +1,25 @@
1
+ import { useSiteManifest } from '@myst-theme/providers';
2
+ import { getProjectHeadings } from '@myst-theme/common';
3
+ import { Toc } from './TableOfContentsItems.js';
4
+
5
+ export const InlineTableOfContents = ({
6
+ projectSlug,
7
+ sidebarRef,
8
+ className = 'flex-grow overflow-y-auto max-w-[350px]',
9
+ }: {
10
+ projectSlug?: string;
11
+ className?: string;
12
+ sidebarRef?: React.RefObject<HTMLElement>;
13
+ }) => {
14
+ const config = useSiteManifest();
15
+ if (!config) return null;
16
+ const headings = getProjectHeadings(config, projectSlug, {
17
+ addGroups: false,
18
+ });
19
+ if (!headings) return null;
20
+ return (
21
+ <nav aria-label="Table of Contents" className={className} ref={sidebarRef}>
22
+ <Toc headings={headings} />
23
+ </nav>
24
+ );
25
+ };
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { useLinkProvider, useNavLinkProvider } from '@myst-theme/providers';
3
+
4
+ export function ExternalOrInternalLink({
5
+ to,
6
+ className,
7
+ children,
8
+ nav,
9
+ onClick,
10
+ prefetch = 'intent',
11
+ }: {
12
+ to: string;
13
+ className?: string | ((props: { isActive: boolean }) => string);
14
+ children: React.ReactNode;
15
+ nav?: boolean;
16
+ onClick?: () => void;
17
+ prefetch?: 'intent' | 'render' | 'none';
18
+ }) {
19
+ const Link = useLinkProvider();
20
+ const NavLink = useNavLinkProvider();
21
+ const staticClass = typeof className === 'function' ? className({ isActive: false }) : className;
22
+ if (to.startsWith('http') || to.startsWith('mailto:')) {
23
+ return (
24
+ <a
25
+ href={to}
26
+ target="_blank"
27
+ rel="noopener noreferrer"
28
+ className={staticClass}
29
+ onClick={onClick}
30
+ >
31
+ {children}
32
+ </a>
33
+ );
34
+ }
35
+ if (nav) {
36
+ return (
37
+ <NavLink prefetch={prefetch} to={to} className={className} onClick={onClick}>
38
+ {children}
39
+ </NavLink>
40
+ );
41
+ }
42
+ return (
43
+ <Link prefetch={prefetch} to={to} className={staticClass} onClick={onClick}>
44
+ {children}
45
+ </Link>
46
+ );
47
+ }
@@ -1,37 +1,106 @@
1
- import { useNavOpen, useThemeTop } from '@myst-theme/providers';
2
- import { TableOfContents } from './TableOfContents.js';
1
+ import { useNavOpen, useSiteManifest, useThemeTop } from '@myst-theme/providers';
2
+ import { PrimarySidebar } from './PrimarySidebar.js';
3
+ import type { Heading } from '@myst-theme/common';
4
+ import { getProjectHeadings } from '@myst-theme/common';
5
+ import type { SiteManifest } from 'myst-config';
3
6
 
4
- export function Navigation({
7
+ /**
8
+ * PrimaryNavigation will load nav links and headers from the site manifest and display
9
+ * them in a mobile-friendly format.
10
+ */
11
+ export const PrimaryNavigation = ({
5
12
  children,
6
13
  projectSlug,
7
- tocRef,
14
+ sidebarRef,
8
15
  hide_toc,
16
+ mobileOnly,
9
17
  footer,
10
18
  }: {
11
19
  children?: React.ReactNode;
12
20
  projectSlug?: string;
13
- tocRef?: React.RefObject<HTMLDivElement>;
21
+ sidebarRef?: React.RefObject<HTMLDivElement>;
14
22
  hide_toc?: boolean;
23
+ mobileOnly?: boolean;
15
24
  footer?: React.ReactNode;
16
- }) {
25
+ }) => {
26
+ const config = useSiteManifest();
27
+ if (!config) return null;
28
+
29
+ const headings = getProjectHeadings(config, projectSlug, {
30
+ addGroups: false,
31
+ });
32
+
33
+ const { nav } = config;
34
+
35
+ return (
36
+ <ConfigurablePrimaryNavigation
37
+ children={children}
38
+ sidebarRef={sidebarRef}
39
+ hide_toc={hide_toc}
40
+ mobileOnly={mobileOnly}
41
+ nav={nav}
42
+ headings={headings}
43
+ footer={footer}
44
+ />
45
+ );
46
+ };
47
+
48
+ /**
49
+ @deprecated use PrimaryNavigation instead
50
+ */
51
+ export const Navigation = PrimaryNavigation;
52
+
53
+ /**
54
+ * ConfigurablePrimaryNavigation will display a mobile-friendly navigation sidebar based on the
55
+ * nav, headings, and footer provided by the caller. Use this in situations where the PrimaryNavigation
56
+ * component may pick up the wrong SiteManifest.
57
+ */
58
+ export const ConfigurablePrimaryNavigation = ({
59
+ children,
60
+ sidebarRef,
61
+ hide_toc,
62
+ mobileOnly,
63
+ nav,
64
+ headings,
65
+ footer,
66
+ }: {
67
+ children?: React.ReactNode;
68
+ sidebarRef?: React.RefObject<HTMLDivElement>;
69
+ hide_toc?: boolean;
70
+ mobileOnly?: boolean;
71
+ nav?: SiteManifest['nav'];
72
+ headings?: Heading[];
73
+ footer?: React.ReactNode;
74
+ }) => {
17
75
  const [open, setOpen] = useNavOpen();
18
76
  const top = useThemeTop();
77
+
19
78
  if (children)
20
79
  console.warn(
21
80
  `Including children in Navigation can break keyboard accessbility and is deprecated. Please move children to the page component.`,
22
81
  );
82
+
83
+ // the logic on the following line looks wrong, this will return `null` or `<></>`
84
+ // we should just return `null` if `hide_toc` is true?
23
85
  if (hide_toc) return children ? null : <>{children}</>;
86
+
24
87
  return (
25
88
  <>
26
- {open && (
89
+ {open && !mobileOnly && headings && (
27
90
  <div
28
91
  className="fixed inset-0 z-30 bg-black opacity-50"
29
92
  style={{ marginTop: top }}
30
93
  onClick={() => setOpen(false)}
31
94
  ></div>
32
95
  )}
33
- <TableOfContents tocRef={tocRef} projectSlug={projectSlug} footer={footer} />
96
+ <PrimarySidebar
97
+ sidebarRef={sidebarRef}
98
+ nav={nav}
99
+ headings={headings}
100
+ footer={footer}
101
+ mobileOnly={mobileOnly}
102
+ />
34
103
  {children}
35
104
  </>
36
105
  );
37
- }
106
+ };
@@ -0,0 +1,201 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useNavigation } from '@remix-run/react';
4
+ import {
5
+ useNavOpen,
6
+ useSiteManifest,
7
+ useGridSystemProvider,
8
+ useThemeTop,
9
+ } from '@myst-theme/providers';
10
+ import type { Heading } from '@myst-theme/common';
11
+ import { Toc } from './TableOfContentsItems.js';
12
+ import { ExternalOrInternalLink } from './Link.js';
13
+ import type { SiteManifest, SiteNavItem } from 'myst-config';
14
+ import * as Collapsible from '@radix-ui/react-collapsible';
15
+ import { ChevronRightIcon } from '@heroicons/react/24/solid';
16
+
17
+ export function SidebarNavItem({ item }: { item: SiteNavItem }) {
18
+ if (!item.children?.length) {
19
+ return (
20
+ <ExternalOrInternalLink
21
+ nav
22
+ to={item.url ?? ''}
23
+ className={classNames(
24
+ 'p-2 my-1 rounded-lg',
25
+ 'hover:bg-slate-300/30',
26
+ 'block break-words focus:outline outline-blue-200 outline-2 rounded',
27
+ )}
28
+ >
29
+ {item.title}
30
+ </ExternalOrInternalLink>
31
+ );
32
+ }
33
+ const [open, setOpen] = React.useState(false);
34
+ return (
35
+ <Collapsible.Root className="w-full" open={open} onOpenChange={setOpen}>
36
+ <div
37
+ className={classNames(
38
+ 'flex flex-row w-full gap-2 px-2 my-1 text-left rounded-lg outline-none',
39
+ 'hover:bg-slate-300/30',
40
+ )}
41
+ >
42
+ <ExternalOrInternalLink
43
+ nav
44
+ to={item.url ?? ''}
45
+ className={classNames('py-2 grow', {})}
46
+ onClick={() => setOpen(!open)}
47
+ >
48
+ {item.title}
49
+ </ExternalOrInternalLink>
50
+ <Collapsible.Trigger asChild>
51
+ <button
52
+ className="self-center flex-none rounded-md group hover:bg-slate-300/30 focus:outline outline-blue-200 outline-2"
53
+ aria-label="Open Folder"
54
+ >
55
+ <ChevronRightIcon
56
+ className="transition-transform duration-300 group-data-[state=open]:rotate-90 text-text-slate-700 dark:text-slate-100"
57
+ height="1.5rem"
58
+ width="1.5rem"
59
+ />
60
+ </button>
61
+ </Collapsible.Trigger>
62
+ </div>
63
+ <Collapsible.Content className="pl-3 pr-[2px] collapsible-content">
64
+ {item.children.map((action) => (
65
+ <ExternalOrInternalLink
66
+ nav
67
+ key={action.url}
68
+ to={action.url || ''}
69
+ className={classNames(
70
+ 'p-2 my-1 rounded-lg',
71
+ 'hover:bg-slate-300/30',
72
+ 'block break-words focus:outline outline-blue-200 outline-2 rounded',
73
+ )}
74
+ >
75
+ {action.title}
76
+ </ExternalOrInternalLink>
77
+ ))}
78
+ </Collapsible.Content>
79
+ </Collapsible.Root>
80
+ );
81
+ }
82
+
83
+ export function SidebarNav({ nav }: { nav?: SiteManifest['nav'] }) {
84
+ if (!nav) return null;
85
+ return (
86
+ <div className="w-full px-1 dark:text-white">
87
+ {nav.map((item) => {
88
+ return <SidebarNavItem key={'url' in item ? item.url : item.title} item={item} />;
89
+ })}
90
+ </div>
91
+ );
92
+ }
93
+
94
+ export function useSidebarHeight<T extends HTMLElement = HTMLElement>(top = 0, inset = 0) {
95
+ const container = useRef<T>(null);
96
+ const toc = useRef<HTMLDivElement>(null);
97
+ const transitionState = useNavigation().state;
98
+ const setHeight = () => {
99
+ if (!container.current || !toc.current) return;
100
+ const height = container.current.offsetHeight - window.scrollY;
101
+ const div = toc.current.firstChild as HTMLDivElement;
102
+ if (div) div.style.height = `min(calc(100vh - ${top}px), ${height + inset}px)`;
103
+ const nav = toc.current.querySelector('nav');
104
+ if (nav) nav.style.opacity = height > 150 ? '1' : '0';
105
+ };
106
+ useEffect(() => {
107
+ setHeight();
108
+ setTimeout(setHeight, 100); // Some lag sometimes
109
+ const handleScroll = () => setHeight();
110
+ window.addEventListener('scroll', handleScroll);
111
+ return () => {
112
+ window.removeEventListener('scroll', handleScroll);
113
+ };
114
+ }, [container, toc, transitionState]);
115
+ return { container, toc };
116
+ }
117
+
118
+ export const PrimarySidebar = ({
119
+ sidebarRef,
120
+ nav,
121
+ footer,
122
+ headings,
123
+ mobileOnly,
124
+ }: {
125
+ sidebarRef?: React.RefObject<HTMLElement>;
126
+ nav?: SiteManifest['nav'];
127
+ headings?: Heading[];
128
+ footer?: React.ReactNode;
129
+ mobileOnly?: boolean;
130
+ }) => {
131
+ const top = useThemeTop();
132
+ const grid = useGridSystemProvider();
133
+ const footerRef = useRef<HTMLDivElement>(null);
134
+ const [open] = useNavOpen();
135
+ const config = useSiteManifest();
136
+
137
+ useEffect(() => {
138
+ setTimeout(() => {
139
+ if (!footerRef.current) return;
140
+ footerRef.current.style.opacity = '1';
141
+ footerRef.current.style.transform = 'none';
142
+ }, 500);
143
+ }, [footerRef]);
144
+ if (!config) return null;
145
+
146
+ return (
147
+ <div
148
+ ref={sidebarRef as any}
149
+ className={classNames(
150
+ 'fixed',
151
+ `xl:${grid}`, // for example, xl:article-grid
152
+ 'grid-gap xl:w-screen xl:pointer-events-none overflow-auto max-xl:min-w-[300px]',
153
+ { hidden: !open, 'z-30': open, 'z-10': !open },
154
+ )}
155
+ style={{ top }}
156
+ >
157
+ <div
158
+ className={classNames(
159
+ 'pointer-events-auto',
160
+ 'xl:col-margin-left flex-col',
161
+ 'overflow-hidden',
162
+ {
163
+ flex: open,
164
+ 'bg-white dark:bg-stone-900': open, // just apply when open, so that theme can transition
165
+ 'hidden xl:flex': !open && !mobileOnly,
166
+ hidden: !open && mobileOnly,
167
+ 'lg:hidden': mobileOnly && !headings,
168
+ },
169
+ )}
170
+ >
171
+ <div className="flex-grow py-6 overflow-y-auto">
172
+ {nav && (
173
+ <nav
174
+ aria-label="Navigation"
175
+ className="overflow-y-hidden transition-opacity ml-3 xl:ml-0 mr-3 max-w-[350px] lg:hidden"
176
+ >
177
+ <SidebarNav nav={nav} />
178
+ </nav>
179
+ )}
180
+ {nav && headings && <div className="my-3 border-b-2 lg:hidden" />}
181
+ {headings && (
182
+ <nav
183
+ aria-label="Table of Contents"
184
+ className="flex-grow overflow-y-hidden transition-opacity ml-3 xl:ml-0 mr-3 max-w-[350px]"
185
+ >
186
+ <Toc headings={headings} />
187
+ </nav>
188
+ )}
189
+ </div>
190
+ {footer && (
191
+ <div
192
+ className="flex-none py-6 transition-all duration-700 translate-y-6 opacity-0"
193
+ ref={footerRef}
194
+ >
195
+ {footer}
196
+ </div>
197
+ )}
198
+ </div>
199
+ </div>
200
+ );
201
+ };
@@ -13,46 +13,10 @@ import {
13
13
  import { LoadingBar } from './Loading.js';
14
14
  import { HomeLink } from './HomeLink.js';
15
15
  import { ActionMenu } from './ActionMenu.js';
16
+ import { ExternalOrInternalLink } from './Link.js';
16
17
 
17
18
  export const DEFAULT_NAV_HEIGHT = 60;
18
19
 
19
- function ExternalOrInternalLink({
20
- to,
21
- className,
22
- children,
23
- nav,
24
- prefetch = 'intent',
25
- }: {
26
- to: string;
27
- className?: string | ((props: { isActive: boolean }) => string);
28
- children: React.ReactNode;
29
- nav?: boolean;
30
- prefetch?: 'intent' | 'render' | 'none';
31
- }) {
32
- const Link = useLinkProvider();
33
- const NavLink = useNavLinkProvider();
34
- const staticClass = typeof className === 'function' ? className({ isActive: false }) : className;
35
- if (to.startsWith('http') || to.startsWith('mailto:')) {
36
- return (
37
- <a href={to} target="_blank" rel="noopener noreferrer" className={staticClass}>
38
- {children}
39
- </a>
40
- );
41
- }
42
- if (nav) {
43
- return (
44
- <NavLink prefetch={prefetch} to={to} className={className}>
45
- {children}
46
- </NavLink>
47
- );
48
- }
49
- return (
50
- <Link prefetch={prefetch} to={to} className={staticClass}>
51
- {children}
52
- </Link>
53
- );
54
- }
55
-
56
20
  export function NavItem({ item }: { item: SiteNavItem }) {
57
21
  const NavLink = useNavLinkProvider();
58
22
  if (!('children' in item)) {
@@ -143,26 +107,28 @@ export function NavItems({ nav }: { nav?: SiteManifest['nav'] }) {
143
107
  );
144
108
  }
145
109
 
146
- export function TopNav() {
110
+ export function TopNav({ hideToc }: { hideToc?: boolean }) {
147
111
  const [open, setOpen] = useNavOpen();
148
112
  const config = useSiteManifest();
149
113
  const { title, nav, actions } = config ?? {};
150
114
  const { logo, logo_dark, logo_text } = config?.options ?? {};
151
115
  return (
152
- <div className="bg-white/80 backdrop-blur dark:bg-stone-900/80 shadow dark:shadow-stone-700 p-3 md:px-8 fixed w-screen top-0 z-30 h-[60px]">
153
- <nav className="flex items-center justify-between flex-wrap max-w-[1440px] mx-auto">
154
- <div className="flex flex-row xl:min-w-[19.5rem] mr-2 sm:mr-7 justify-start items-center">
155
- <div className="block xl:hidden">
156
- <button
157
- className="flex items-center border-stone-400 text-stone-800 hover:text-stone-900 dark:text-stone-200 hover:dark:text-stone-100"
158
- onClick={() => {
159
- setOpen(!open);
160
- }}
161
- >
162
- <MenuIcon width="2rem" height="2rem" className="m-1" />
163
- <span className="sr-only">Open Menu</span>
164
- </button>
165
- </div>
116
+ <div className="bg-white/80 backdrop-blur dark:bg-stone-900/80 shadow dark:shadow-stone-700 p-3 md:px-8 sticky w-screen top-0 z-30 h-[60px]">
117
+ <nav className="flex items-center justify-between flex-nowrap max-w-[1440px] mx-auto">
118
+ <div className="flex flex-row xl:min-w-[19.5rem] mr-2 sm:mr-7 justify-start items-center shrink-0">
119
+ {!hideToc && (
120
+ <div className="block xl:hidden">
121
+ <button
122
+ className="flex items-center border-stone-400 text-stone-800 hover:text-stone-900 dark:text-stone-200 hover:dark:text-stone-100"
123
+ onClick={() => {
124
+ setOpen(!open);
125
+ }}
126
+ >
127
+ <MenuIcon width="2rem" height="2rem" className="m-1" />
128
+ <span className="sr-only">Open Menu</span>
129
+ </button>
130
+ </div>
131
+ )}
166
132
  <HomeLink name={title} logo={logo} logoDark={logo_dark} logoText={logo_text} />
167
133
  </div>
168
134
  <div className="flex items-center flex-grow w-auto">
@@ -1,7 +1,8 @@
1
1
  export { ThemeButton } from './ThemeButton.js';
2
2
  export { TopNav, NavItems, NavItem, DEFAULT_NAV_HEIGHT } from './TopNav.js';
3
- export { Navigation } from './Navigation.js';
4
- export { TableOfContents, InlineTableOfContents, useTocHeight } from './TableOfContents.js';
3
+ export { Navigation, PrimaryNavigation, ConfigurablePrimaryNavigation } from './Navigation.js';
4
+ export { PrimarySidebar, useSidebarHeight } from './PrimarySidebar.js';
5
+ export { InlineTableOfContents } from './InlineTableOfContents.js';
5
6
  export { LoadingBar } from './Loading.js';
6
7
  export { ActionMenu } from './ActionMenu.js';
7
8
  export { HomeLink } from './HomeLink.js';
@@ -1,10 +1,10 @@
1
- import type { NodeRenderer } from '@myst-theme/providers';
1
+ import type { NodeRenderers } from '@myst-theme/providers';
2
2
  import { DEFAULT_RENDERERS } from 'myst-to-react';
3
3
  import { MystDemoRenderer } from 'myst-demo';
4
4
  import { MermaidNodeRenderer } from '@myst-theme/diagrams';
5
5
  import OUTPUT_RENDERERS from '@myst-theme/jupyter';
6
6
 
7
- export const renderers: Record<string, NodeRenderer> = {
7
+ export const renderers: NodeRenderers = {
8
8
  ...DEFAULT_RENDERERS,
9
9
  myst: MystDemoRenderer,
10
10
  mermaid: MermaidNodeRenderer,
@@ -7,8 +7,6 @@ import {
7
7
  FrontmatterParts,
8
8
  BackmatterParts,
9
9
  } from '../components/index.js';
10
- import { ErrorDocumentNotFound } from './ErrorDocumentNotFound.js';
11
- import { ErrorProjectNotFound } from './ErrorProjectNotFound.js';
12
10
  import type { PageLoader } from '@myst-theme/common';
13
11
  import { copyNode, type GenericParent } from 'myst-common';
14
12
  import { SourceFileKind } from 'myst-spec-ext';
@@ -57,7 +55,7 @@ export const ArticlePage = React.memo(function ({
57
55
  <FrontmatterBlock
58
56
  kind={article.kind}
59
57
  frontmatter={{ ...article.frontmatter, downloads }}
60
- className="pt-5 mb-8"
58
+ className="mb-8 pt-9"
61
59
  />
62
60
  )}
63
61
  {compute?.enabled &&
@@ -80,11 +78,3 @@ export const ArticlePage = React.memo(function ({
80
78
  </ReferencesProvider>
81
79
  );
82
80
  });
83
-
84
- export function ProjectPageCatchBoundary() {
85
- return <ErrorProjectNotFound />;
86
- }
87
-
88
- export function ArticlePageCatchBoundary() {
89
- return <ErrorDocumentNotFound />;
90
- }
@@ -0,0 +1,14 @@
1
+ export type ErrorResponse = {
2
+ status: number;
3
+ statusText: string;
4
+ data: any;
5
+ };
6
+ export function ErrorUnhandled({ error }: { error: ErrorResponse }) {
7
+ return (
8
+ <>
9
+ <h1>Unexpected Error Occurred</h1>
10
+ <p>Status: {error.status}</p>
11
+ <p>{error.data.message}</p>
12
+ </>
13
+ );
14
+ }
@@ -1,6 +1,6 @@
1
1
  import type { SiteManifest } from 'myst-config';
2
2
  import type { SiteLoader } from '@myst-theme/common';
3
- import type { NodeRenderer } from '@myst-theme/providers';
3
+ import type { NodeRenderers } from '@myst-theme/providers';
4
4
  import { BaseUrlProvider, SiteProvider, Theme, ThemeProvider } from '@myst-theme/providers';
5
5
  import {
6
6
  Links,
@@ -12,10 +12,13 @@ import {
12
12
  useLoaderData,
13
13
  Link,
14
14
  NavLink,
15
+ useRouteError,
16
+ isRouteErrorResponse,
15
17
  } from '@remix-run/react';
16
18
  import { DEFAULT_NAV_HEIGHT, renderers as defaultRenderers } from '../components/index.js';
17
19
  import { Analytics } from '../seo/index.js';
18
20
  import { Error404 } from './Error404.js';
21
+ import { ErrorUnhandled } from './ErrorUnhandled.js';
19
22
  import classNames from 'classnames';
20
23
 
21
24
  export function Document({
@@ -37,7 +40,7 @@ export function Document({
37
40
  staticBuild?: boolean;
38
41
  baseurl?: string;
39
42
  top?: number;
40
- renderers?: Record<string, NodeRenderer>;
43
+ renderers?: NodeRenderers;
41
44
  }) {
42
45
  const links = staticBuild
43
46
  ? {
@@ -86,12 +89,13 @@ export function App() {
86
89
  );
87
90
  }
88
91
 
89
- export function AppCatchBoundary() {
92
+ export function AppErrorBoundary() {
93
+ const error = useRouteError();
90
94
  return (
91
95
  <Document theme={Theme.light}>
92
96
  <article className="article">
93
97
  <main className="article-grid subgrid-gap col-screen">
94
- <Error404 />
98
+ {isRouteErrorResponse(error) ? <Error404 /> : <ErrorUnhandled error={error as any} />}
95
99
  </main>
96
100
  </article>
97
101
  </Document>
@@ -1,5 +1,6 @@
1
1
  export { ErrorProjectNotFound } from './ErrorProjectNotFound.js';
2
2
  export { ErrorDocumentNotFound } from './ErrorDocumentNotFound.js';
3
3
  export { Error404 } from './Error404.js';
4
- export { ArticlePage, ArticlePageCatchBoundary, ProjectPageCatchBoundary } from './Article.js';
5
- export { App, Document, AppCatchBoundary } from './Root.js';
4
+ export { ErrorUnhandled } from './ErrorUnhandled.js';
5
+ export { ArticlePage } from './Article.js';
6
+ export { App, Document, AppErrorBoundary } from './Root.js';
package/src/seo/meta.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { HtmlMetaDescriptor, V2_MetaDescriptor } from '@remix-run/react';
1
+ import type { V2_MetaDescriptor } from '@remix-run/react';
2
2
 
3
3
  export type SocialSite = {
4
4
  title: string;
@@ -17,15 +17,15 @@ export type SocialArticle = {
17
17
  keywords?: string[];
18
18
  };
19
19
 
20
- function allDefined(meta: Record<string, string | null | undefined>): HtmlMetaDescriptor {
21
- return Object.fromEntries(Object.entries(meta).filter(([, v]) => v)) as HtmlMetaDescriptor;
20
+ function allDefined(meta: Record<string, string | null | undefined>): V2_MetaDescriptor {
21
+ return Object.fromEntries(Object.entries(meta).filter(([, v]) => v)) as V2_MetaDescriptor;
22
22
  }
23
23
 
24
24
  export function getMetaTagsForSite_V1({
25
25
  title,
26
26
  description,
27
27
  twitter,
28
- }: SocialSite): HtmlMetaDescriptor {
28
+ }: SocialSite): V2_MetaDescriptor {
29
29
  const meta = {
30
30
  title,
31
31
  description,
@@ -60,7 +60,7 @@ export function getMetaTagsForArticle_V1({
60
60
  image,
61
61
  twitter,
62
62
  keywords,
63
- }: SocialArticle): HtmlMetaDescriptor {
63
+ }: SocialArticle): V2_MetaDescriptor {
64
64
  const meta = {
65
65
  title,
66
66
  description,
@@ -1,126 +0,0 @@
1
- import React, { useEffect, useRef } from 'react';
2
- import classNames from 'classnames';
3
- import { useNavigation } from '@remix-run/react';
4
- import {
5
- useNavOpen,
6
- useSiteManifest,
7
- useGridSystemProvider,
8
- useThemeTop,
9
- } from '@myst-theme/providers';
10
- import { getProjectHeadings } from '@myst-theme/common';
11
- import { Toc } from './TableOfContentsItems.js';
12
-
13
- export function useTocHeight<T extends HTMLElement = HTMLElement>(top = 0, inset = 0) {
14
- const container = useRef<T>(null);
15
- const toc = useRef<HTMLDivElement>(null);
16
- const transitionState = useNavigation().state;
17
- const setHeight = () => {
18
- if (!container.current || !toc.current) return;
19
- const height = container.current.offsetHeight - window.scrollY;
20
- const div = toc.current.firstChild as HTMLDivElement;
21
- if (div) div.style.height = `min(calc(100vh - ${top}px), ${height + inset}px)`;
22
- const nav = toc.current.querySelector('nav');
23
- if (nav) nav.style.opacity = height > 150 ? '1' : '0';
24
- };
25
- useEffect(() => {
26
- setHeight();
27
- setTimeout(setHeight, 100); // Some lag sometimes
28
- const handleScroll = () => setHeight();
29
- window.addEventListener('scroll', handleScroll);
30
- return () => {
31
- window.removeEventListener('scroll', handleScroll);
32
- };
33
- }, [container, toc, transitionState]);
34
- return { container, toc };
35
- }
36
-
37
- export const TableOfContents = ({
38
- projectSlug,
39
- tocRef,
40
- footer,
41
- }: {
42
- tocRef?: React.RefObject<HTMLElement>;
43
- projectSlug?: string;
44
- footer?: React.ReactNode;
45
- }) => {
46
- const top = useThemeTop();
47
- const grid = useGridSystemProvider();
48
- const footerRef = useRef<HTMLDivElement>(null);
49
- const [open] = useNavOpen();
50
- const config = useSiteManifest();
51
- useEffect(() => {
52
- setTimeout(() => {
53
- if (!footerRef.current) return;
54
- footerRef.current.style.opacity = '1';
55
- footerRef.current.style.transform = 'none';
56
- }, 500);
57
- }, [footerRef]);
58
- if (!config) return null;
59
- const headings = getProjectHeadings(config, projectSlug, {
60
- addGroups: false,
61
- });
62
- if (!headings) return null;
63
- return (
64
- <div
65
- ref={tocRef as any}
66
- className={classNames(
67
- 'fixed',
68
- `xl:${grid}`, // for example, xl:article-grid
69
- 'grid-gap xl:w-screen xl:pointer-events-none overflow-auto max-xl:min-w-[300px]',
70
- { hidden: !open, 'z-30': open, 'z-10': !open },
71
- )}
72
- style={{ top }}
73
- >
74
- <div
75
- className={classNames(
76
- 'pointer-events-auto',
77
- 'xl:col-margin-left flex-col',
78
- 'overflow-hidden',
79
- // 'border-r border-stone-200 dark:border-stone-700',
80
- {
81
- flex: open,
82
- 'bg-white dark:bg-stone-900': open, // just apply when open, so that theme can transition
83
- 'hidden xl:flex': !open,
84
- },
85
- )}
86
- >
87
- <nav
88
- aria-label="Table of Contents"
89
- className="flex-grow overflow-y-auto transition-opacity mt-6 pb-3 ml-3 xl:ml-0 mr-3 max-w-[350px]"
90
- >
91
- <Toc headings={headings} />
92
- </nav>
93
- {footer && (
94
- <div
95
- className="flex-none py-4 transition-all duration-700 translate-y-6 opacity-0"
96
- ref={footerRef}
97
- >
98
- {footer}
99
- </div>
100
- )}
101
- </div>
102
- </div>
103
- );
104
- };
105
-
106
- export const InlineTableOfContents = ({
107
- projectSlug,
108
- tocRef,
109
- className = 'flex-grow overflow-y-auto max-w-[350px]',
110
- }: {
111
- projectSlug?: string;
112
- className?: string;
113
- tocRef?: React.RefObject<HTMLElement>;
114
- }) => {
115
- const config = useSiteManifest();
116
- if (!config) return null;
117
- const headings = getProjectHeadings(config, projectSlug, {
118
- addGroups: false,
119
- });
120
- if (!headings) return null;
121
- return (
122
- <nav aria-label="Table of Contents" className={className} ref={tocRef}>
123
- <Toc headings={headings} />
124
- </nav>
125
- );
126
- };