@myst-theme/site 0.9.9 → 0.10.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 +19 -15
- package/src/components/Headers.tsx +1 -1
- package/src/components/Navigation/InlineTableOfContents.tsx +25 -0
- package/src/components/Navigation/Link.tsx +47 -0
- package/src/components/Navigation/Navigation.tsx +78 -9
- package/src/components/Navigation/PrimarySidebar.tsx +201 -0
- package/src/components/Navigation/TopNav.tsx +18 -52
- package/src/components/Navigation/index.tsx +3 -2
- package/src/pages/Article.tsx +1 -1
- package/src/components/Navigation/TableOfContents.tsx +0 -126
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myst-theme/site",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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.
|
|
21
|
-
"@myst-theme/diagrams": "^0.
|
|
22
|
-
"@myst-theme/frontmatter": "^0.
|
|
23
|
-
"@myst-theme/jupyter": "^0.
|
|
24
|
-
"@myst-theme/providers": "^0.
|
|
24
|
+
"@myst-theme/common": "^0.10.0",
|
|
25
|
+
"@myst-theme/diagrams": "^0.10.0",
|
|
26
|
+
"@myst-theme/frontmatter": "^0.10.0",
|
|
27
|
+
"@myst-theme/jupyter": "^0.10.0",
|
|
28
|
+
"@myst-theme/providers": "^0.10.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.
|
|
29
|
-
"myst-config": "^1.
|
|
30
|
-
"myst-demo": "^0.
|
|
31
|
-
"myst-spec-ext": "^1.
|
|
32
|
-
"myst-to-react": "^0.
|
|
32
|
+
"myst-common": "^1.5.0",
|
|
33
|
+
"myst-config": "^1.5.0",
|
|
34
|
+
"myst-demo": "^0.10.0",
|
|
35
|
+
"myst-spec-ext": "^1.5.0",
|
|
36
|
+
"myst-to-react": "^0.10.0",
|
|
33
37
|
"nbtx": "^0.2.3",
|
|
34
38
|
"node-cache": "^5.1.2",
|
|
35
39
|
"node-fetch": "^2.6.11",
|
|
36
|
-
"thebe-react": "
|
|
40
|
+
"thebe-react": "0.4.7",
|
|
37
41
|
"unist-util-select": "^4.0.1"
|
|
38
42
|
},
|
|
39
43
|
"peerDependencies": {
|
|
40
|
-
"@remix-run/node": "^1.
|
|
41
|
-
"@remix-run/react": "^1.
|
|
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",
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
14
|
+
sidebarRef,
|
|
8
15
|
hide_toc,
|
|
16
|
+
mobileOnly,
|
|
9
17
|
footer,
|
|
10
18
|
}: {
|
|
11
19
|
children?: React.ReactNode;
|
|
12
20
|
projectSlug?: string;
|
|
13
|
-
|
|
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
|
-
<
|
|
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
|
|
153
|
-
<nav className="flex items-center justify-between flex-
|
|
154
|
-
<div className="flex flex-row xl:min-w-[19.5rem] mr-2 sm:mr-7 justify-start items-center">
|
|
155
|
-
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 {
|
|
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';
|
package/src/pages/Article.tsx
CHANGED
|
@@ -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
|
-
};
|