@myst-theme/site 0.0.17
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 +53 -0
- package/src/components/Bibliography.tsx +51 -0
- package/src/components/ContentBlocks.tsx +20 -0
- package/src/components/ContentReload.tsx +66 -0
- package/src/components/DocumentOutline.tsx +187 -0
- package/src/components/ExternalOrInternalLink.tsx +30 -0
- package/src/components/FooterLinksBlock.tsx +39 -0
- package/src/components/Navigation/Loading.tsx +59 -0
- package/src/components/Navigation/Navigation.tsx +31 -0
- package/src/components/Navigation/TableOfContents.tsx +155 -0
- package/src/components/Navigation/ThemeButton.tsx +25 -0
- package/src/components/Navigation/TopNav.tsx +249 -0
- package/src/components/Navigation/index.tsx +5 -0
- package/src/components/index.ts +8 -0
- package/src/components/renderers.ts +7 -0
- package/src/hooks/index.ts +23 -0
- package/src/index.ts +8 -0
- package/src/loaders/cdn.server.ts +145 -0
- package/src/loaders/errors.server.ts +20 -0
- package/src/loaders/index.ts +5 -0
- package/src/loaders/links.ts +8 -0
- package/src/loaders/theme.server.ts +47 -0
- package/src/loaders/utils.ts +145 -0
- package/src/pages/Article.tsx +31 -0
- package/src/pages/ErrorDocumentNotFound.tsx +10 -0
- package/src/pages/ErrorProjectNotFound.tsx +10 -0
- package/src/pages/ErrorSiteNotFound.tsx +30 -0
- package/src/pages/Root.tsx +91 -0
- package/src/pages/index.ts +5 -0
- package/src/seo/analytics.tsx +34 -0
- package/src/seo/index.ts +4 -0
- package/src/seo/meta.ts +57 -0
- package/src/seo/robots.ts +24 -0
- package/src/seo/sitemap.ts +216 -0
- package/src/store.ts +21 -0
- package/src/types.ts +49 -0
- package/src/utils.ts +5 -0
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@myst-theme/site",
|
|
3
|
+
"version": "0.0.17",
|
|
4
|
+
"main": "./src/index.ts",
|
|
5
|
+
"types": "./src/index.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"src"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"scripts": {
|
|
12
|
+
"compile": "tsc",
|
|
13
|
+
"lint": "eslint src/**/*.ts*",
|
|
14
|
+
"lint:format": "prettier --check \"src/**/*.{ts,tsx,md}\""
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@curvenote/blocks": "^1.5.16",
|
|
18
|
+
"@curvenote/connect": "0.0.6",
|
|
19
|
+
"@curvenote/icons": "^0.0.3",
|
|
20
|
+
"@curvenote/nbtx": "^0.1.11",
|
|
21
|
+
"@curvenote/runtime": "^0.2.9",
|
|
22
|
+
"@curvenote/ui-providers": "^0.0.16",
|
|
23
|
+
"@headlessui/react": "^1.6.6",
|
|
24
|
+
"@heroicons/react": "^2.0.12",
|
|
25
|
+
"@myst-theme/frontmatter": "^0.1.19",
|
|
26
|
+
"classnames": "^2.3.2",
|
|
27
|
+
"lodash.throttle": "^4.1.1",
|
|
28
|
+
"myst-common": "^0.0.11",
|
|
29
|
+
"myst-config": "^0.0.6",
|
|
30
|
+
"myst-demo": "^0.1.19",
|
|
31
|
+
"myst-to-react": "^0.1.19",
|
|
32
|
+
"mystjs": "^0.0.15",
|
|
33
|
+
"node-cache": "^5.1.2",
|
|
34
|
+
"node-fetch": "^2.6.7",
|
|
35
|
+
"unist-util-select": "^4.0.1"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@remix-run/node": "^1.7.5",
|
|
39
|
+
"@remix-run/react": "^1.7.5",
|
|
40
|
+
"@types/react": "^16.8 || ^17.0 || ^18.0",
|
|
41
|
+
"@types/react-dom": "^16.8 || ^17.0 || ^18.0",
|
|
42
|
+
"react": "^16.8 || ^17.0 || ^18.0",
|
|
43
|
+
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/lodash.throttle": "^4.1.7",
|
|
47
|
+
"@types/node-fetch": "^2.6.2",
|
|
48
|
+
"eslint": "^8.21.0",
|
|
49
|
+
"eslint-config-curvenote": "latest",
|
|
50
|
+
"tsconfig": "latest",
|
|
51
|
+
"typescript": "latest"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useReferences } from '@myst-theme/providers';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
|
|
4
|
+
const HIDE_OVER_N_REFERENCES = 5;
|
|
5
|
+
|
|
6
|
+
export function Bibliography() {
|
|
7
|
+
const references = useReferences();
|
|
8
|
+
const { order, data } = references?.cite ?? {};
|
|
9
|
+
const filtered = order?.filter((l) => l);
|
|
10
|
+
const [hidden, setHidden] = useState(true);
|
|
11
|
+
if (!filtered || !data || filtered.length === 0) return null;
|
|
12
|
+
const refs = hidden ? filtered.slice(0, HIDE_OVER_N_REFERENCES) : filtered;
|
|
13
|
+
return (
|
|
14
|
+
<section>
|
|
15
|
+
{filtered.length > HIDE_OVER_N_REFERENCES && (
|
|
16
|
+
<button
|
|
17
|
+
onClick={() => setHidden(!hidden)}
|
|
18
|
+
className="float-right text-xs p-1 px-2 border rounded hover:border-blue-500 dark:hover:border-blue-400"
|
|
19
|
+
>
|
|
20
|
+
{hidden ? 'Show All' : 'Collapse'}
|
|
21
|
+
</button>
|
|
22
|
+
)}
|
|
23
|
+
<header className="text-lg font-semibold text-stone-900 dark:text-white">References</header>
|
|
24
|
+
<div className="text-xs mb-8 pl-3 text-stone-500 dark:text-stone-300">
|
|
25
|
+
<ol>
|
|
26
|
+
{refs.map((label) => {
|
|
27
|
+
const { html } = data[label];
|
|
28
|
+
return (
|
|
29
|
+
<li
|
|
30
|
+
key={label}
|
|
31
|
+
className="break-words"
|
|
32
|
+
id={`cite-${label}`}
|
|
33
|
+
dangerouslySetInnerHTML={{ __html: html || '' }}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
})}
|
|
37
|
+
{filtered.length > HIDE_OVER_N_REFERENCES && (
|
|
38
|
+
<li className="list-none text-center">
|
|
39
|
+
<button
|
|
40
|
+
onClick={() => setHidden(!hidden)}
|
|
41
|
+
className="p-2 border rounded hover:border-blue-500 dark:hover:border-blue-400"
|
|
42
|
+
>
|
|
43
|
+
{hidden ? `Show all ${filtered.length} references` : 'Collapse references'}
|
|
44
|
+
</button>
|
|
45
|
+
</li>
|
|
46
|
+
)}
|
|
47
|
+
</ol>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useParse, DEFAULT_RENDERERS } from 'myst-to-react';
|
|
2
|
+
import type { Parent } from 'myst-spec';
|
|
3
|
+
import { useNodeRenderers } from '@myst-theme/providers';
|
|
4
|
+
|
|
5
|
+
function Block({ id, node }: { id: string; node: Parent }) {
|
|
6
|
+
const renderers = useNodeRenderers() ?? DEFAULT_RENDERERS;
|
|
7
|
+
const children = useParse(node, renderers);
|
|
8
|
+
return <div id={id}>{children}</div>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ContentBlocks({ mdast }: { mdast: Parent }) {
|
|
12
|
+
const blocks = mdast.children as Parent[];
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
{blocks.map((node, index) => {
|
|
16
|
+
return <Block key={(node as any).key} id={`${index}`} node={node} />;
|
|
17
|
+
})}
|
|
18
|
+
</>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'myst';
|
|
4
|
+
|
|
5
|
+
async function mystLiveReloadConnect(config: { onOpen?: () => void; port?: string | number }) {
|
|
6
|
+
if (!config.port || (window as any).mystLiveReloadConnected) return;
|
|
7
|
+
(window as any).mystLiveReloadConnected = true;
|
|
8
|
+
setTimeout(() => {
|
|
9
|
+
const myst = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
|
|
10
|
+
if (myst.scroll) {
|
|
11
|
+
window.scrollTo(0, myst.scroll);
|
|
12
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
13
|
+
}
|
|
14
|
+
}, 30);
|
|
15
|
+
console.log(`🔊 Listening to live content changes on port ${config.port}`);
|
|
16
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
17
|
+
const host = location.hostname;
|
|
18
|
+
const socketPath = `${protocol}//${host}:${config.port}/socket`;
|
|
19
|
+
const ws = new WebSocket(socketPath);
|
|
20
|
+
|
|
21
|
+
ws.onmessage = (message) => {
|
|
22
|
+
const event = JSON.parse(message.data);
|
|
23
|
+
if (event.type === 'LOG') {
|
|
24
|
+
console.log(event.message);
|
|
25
|
+
}
|
|
26
|
+
if (event.type === 'RELOAD') {
|
|
27
|
+
console.log('🚀 Reloading window ...');
|
|
28
|
+
console.log(`📌 Keeping scroll for page at ${window.scrollY}`);
|
|
29
|
+
const myst = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
|
|
30
|
+
myst.scroll = window.scrollY;
|
|
31
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(myst));
|
|
32
|
+
window.location.reload();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
ws.onopen = () => {
|
|
36
|
+
if (config && typeof config.onOpen === 'function') {
|
|
37
|
+
config.onOpen();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
ws.onclose = () => {
|
|
41
|
+
console.log('MyST content server web socket closed. Reconnecting...');
|
|
42
|
+
setTimeout(
|
|
43
|
+
() =>
|
|
44
|
+
mystLiveReloadConnect({
|
|
45
|
+
...config,
|
|
46
|
+
onOpen: () => window.location.reload(),
|
|
47
|
+
}),
|
|
48
|
+
1000,
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
ws.onerror = (error: any) => {
|
|
52
|
+
console.log('MyST content server web socket error:');
|
|
53
|
+
console.error(error);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Inspired by the LiveReload component in Remix
|
|
58
|
+
export const ContentReload =
|
|
59
|
+
process.env.NODE_ENV !== 'development'
|
|
60
|
+
? () => null
|
|
61
|
+
: ({ port }: { port?: string | number }) => {
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
mystLiveReloadConnect({ port });
|
|
64
|
+
}, []);
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import throttle from 'lodash.throttle';
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
const SELECTOR = [1, 2, 3, 4, 5, 6].map((n) => `main h${n}`).join(', ');
|
|
6
|
+
const HIGHLIGHT_CLASS = 'highlight';
|
|
7
|
+
|
|
8
|
+
const onClient = typeof document !== 'undefined';
|
|
9
|
+
|
|
10
|
+
type Heading = {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
titleHTML: string;
|
|
14
|
+
level: number;
|
|
15
|
+
};
|
|
16
|
+
type Props = {
|
|
17
|
+
headings: Heading[];
|
|
18
|
+
activeId?: string;
|
|
19
|
+
highlight?: () => void;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* This renders an item in the table of contents list.
|
|
23
|
+
* scrollIntoView is used to ensure that when a user clicks on an item, it will smoothly scroll.
|
|
24
|
+
*/
|
|
25
|
+
const Headings = ({ headings, activeId, highlight }: Props) => (
|
|
26
|
+
<ul className="text-slate-400 text-sm leading-6">
|
|
27
|
+
{headings.map((heading) => (
|
|
28
|
+
<li
|
|
29
|
+
key={heading.id}
|
|
30
|
+
className={classNames('border-l-2', {
|
|
31
|
+
'text-blue-600': heading.id === activeId,
|
|
32
|
+
'border-l-gray-300 dark:border-l-gray-50 hover:border-l-blue-500':
|
|
33
|
+
heading.id !== activeId,
|
|
34
|
+
'border-l-blue-500': heading.id === activeId,
|
|
35
|
+
'bg-blue-50 dark:bg-slate-800': heading.id === activeId,
|
|
36
|
+
})}
|
|
37
|
+
>
|
|
38
|
+
<a
|
|
39
|
+
className={classNames('block p-1 pl-2 text-slate-800 dark:text-slate-100', {
|
|
40
|
+
'text-blue-600 dark:text-white font-semibold': heading.id === activeId,
|
|
41
|
+
'pr-2': heading.id !== activeId,
|
|
42
|
+
'pl-3': heading.level === 2,
|
|
43
|
+
'pl-4': heading.level === 3,
|
|
44
|
+
'pl-5': heading.level === 4,
|
|
45
|
+
'pl-6': heading.level === 5,
|
|
46
|
+
'pl-7': heading.level === 6,
|
|
47
|
+
})}
|
|
48
|
+
href={`#${heading.id}`}
|
|
49
|
+
onClick={(e) => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
const el = document.querySelector(`#${heading.id}`);
|
|
52
|
+
if (!el) return;
|
|
53
|
+
getHeaders().forEach((h) => {
|
|
54
|
+
h.classList.remove(HIGHLIGHT_CLASS);
|
|
55
|
+
});
|
|
56
|
+
el.classList.add(HIGHLIGHT_CLASS);
|
|
57
|
+
highlight?.();
|
|
58
|
+
el.scrollIntoView({ behavior: 'smooth' });
|
|
59
|
+
}}
|
|
60
|
+
// Note that the title can have math in it!
|
|
61
|
+
dangerouslySetInnerHTML={{ __html: heading.titleHTML }}
|
|
62
|
+
/>
|
|
63
|
+
</li>
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
function getHeaders(): HTMLHeadingElement[] {
|
|
69
|
+
const headers = Array.from(document.querySelectorAll(SELECTOR)).filter((e) => {
|
|
70
|
+
const parent = e.closest('.exclude-from-outline');
|
|
71
|
+
return !(e.classList.contains('title') || parent);
|
|
72
|
+
});
|
|
73
|
+
return headers as HTMLHeadingElement[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function useHeaders() {
|
|
77
|
+
if (!onClient) return { activeId: '', headings: [] };
|
|
78
|
+
const onScreen = useRef<Set<HTMLHeadingElement>>(new Set());
|
|
79
|
+
const [activeId, setActiveId] = useState<string>();
|
|
80
|
+
const headingsSet = useRef<Set<HTMLHeadingElement>>(new Set());
|
|
81
|
+
|
|
82
|
+
const highlight = useCallback(() => {
|
|
83
|
+
const current = [...onScreen.current];
|
|
84
|
+
const highlighted = current.reduce((a, b) => {
|
|
85
|
+
if (a) return a;
|
|
86
|
+
if (b.classList.contains('highlight')) return b.id;
|
|
87
|
+
return null;
|
|
88
|
+
}, null as string | null);
|
|
89
|
+
const active = [...onScreen.current].sort((a, b) => a.offsetTop - b.offsetTop)[0];
|
|
90
|
+
if (highlighted || active) setActiveId(highlighted || active.id);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const { observer } = useIntersectionObserver(highlight, onScreen.current);
|
|
94
|
+
const [elements, setElements] = useState<HTMLHeadingElement[]>([]);
|
|
95
|
+
|
|
96
|
+
const render = throttle(() => setElements(getHeaders()), 500);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
// We have to look at the document changes for reloads/mutations
|
|
99
|
+
const main = document.querySelector('main');
|
|
100
|
+
const mutations = new MutationObserver(render);
|
|
101
|
+
// Fire when added to the dom
|
|
102
|
+
render();
|
|
103
|
+
if (main) {
|
|
104
|
+
mutations.observe(main, { attributes: true, childList: true, subtree: true });
|
|
105
|
+
}
|
|
106
|
+
return () => mutations.disconnect();
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
// Re-observe all elements when the observer changes
|
|
111
|
+
Array.from(elements).map((e) => observer.current?.observe(e));
|
|
112
|
+
}, [observer]);
|
|
113
|
+
|
|
114
|
+
elements.forEach((e) => {
|
|
115
|
+
if (headingsSet.current.has(e)) return;
|
|
116
|
+
observer.current?.observe(e);
|
|
117
|
+
headingsSet.current.add(e);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const headings: Heading[] = elements
|
|
121
|
+
.map((heading) => {
|
|
122
|
+
return {
|
|
123
|
+
level: Number(heading.tagName.slice(1)),
|
|
124
|
+
id: heading.id,
|
|
125
|
+
text: heading.querySelector('.heading-text'),
|
|
126
|
+
};
|
|
127
|
+
})
|
|
128
|
+
.filter((h) => !!h.text)
|
|
129
|
+
.map(({ level, text, id }) => {
|
|
130
|
+
const { innerText: title, innerHTML: titleHTML } = text as HTMLSpanElement;
|
|
131
|
+
return { title, titleHTML, id, level };
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return { activeId, highlight, headings };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const useIntersectionObserver = (highlight: () => void, onScreen: Set<HTMLHeadingElement>) => {
|
|
138
|
+
const observer = useRef<IntersectionObserver | null>(null);
|
|
139
|
+
if (!onClient) return { observer };
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const callback: IntersectionObserverCallback = (entries) => {
|
|
142
|
+
entries.forEach((entry) => {
|
|
143
|
+
onScreen[entry.isIntersecting ? 'add' : 'delete'](entry.target as HTMLHeadingElement);
|
|
144
|
+
});
|
|
145
|
+
highlight();
|
|
146
|
+
};
|
|
147
|
+
const o = new IntersectionObserver(callback);
|
|
148
|
+
observer.current = o;
|
|
149
|
+
return () => o.disconnect();
|
|
150
|
+
}, [highlight, onScreen]);
|
|
151
|
+
return { observer };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const DocumentOutline = ({
|
|
155
|
+
top,
|
|
156
|
+
height,
|
|
157
|
+
className = 'document-outline',
|
|
158
|
+
}: {
|
|
159
|
+
top?: number;
|
|
160
|
+
height?: number;
|
|
161
|
+
className?: string;
|
|
162
|
+
}) => {
|
|
163
|
+
const { activeId, headings, highlight } = useHeaders();
|
|
164
|
+
if (height && height < 50) return null;
|
|
165
|
+
if (headings.length <= 1) return <nav suppressHydrationWarning />;
|
|
166
|
+
return (
|
|
167
|
+
<nav
|
|
168
|
+
aria-label="Document Outline"
|
|
169
|
+
suppressHydrationWarning
|
|
170
|
+
className={classNames('not-prose transition-opacity', className)}
|
|
171
|
+
style={{
|
|
172
|
+
top: top ?? 0,
|
|
173
|
+
height:
|
|
174
|
+
typeof document === 'undefined' || (height && height > window.innerHeight)
|
|
175
|
+
? undefined
|
|
176
|
+
: height,
|
|
177
|
+
opacity: height && height > 300 ? undefined : 0,
|
|
178
|
+
pointerEvents: height && height > 300 ? undefined : 'none',
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<div className="text-slate-900 mb-4 text-sm leading-6 dark:text-slate-100 uppercase">
|
|
182
|
+
In this article
|
|
183
|
+
</div>
|
|
184
|
+
{onClient && <Headings headings={headings} activeId={activeId} highlight={highlight} />}
|
|
185
|
+
</nav>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Link } from '@remix-run/react';
|
|
2
|
+
|
|
3
|
+
export function ExternalOrInternalLink({
|
|
4
|
+
to,
|
|
5
|
+
className,
|
|
6
|
+
isStatic,
|
|
7
|
+
prefetch = 'intent',
|
|
8
|
+
title,
|
|
9
|
+
children,
|
|
10
|
+
}: {
|
|
11
|
+
to: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
isStatic?: boolean;
|
|
14
|
+
prefetch?: 'intent' | 'render' | 'none';
|
|
15
|
+
title?: string;
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
}) {
|
|
18
|
+
if (to.startsWith('http') || isStatic) {
|
|
19
|
+
return (
|
|
20
|
+
<a href={to} className={className} target="_blank" rel="noopener noreferrer" title={title}>
|
|
21
|
+
{children}
|
|
22
|
+
</a>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return (
|
|
26
|
+
<Link to={to} className={className} prefetch={prefetch} title={title}>
|
|
27
|
+
{children}
|
|
28
|
+
</Link>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { Link } from '@remix-run/react';
|
|
3
|
+
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
|
|
4
|
+
import type { FooterLinks, NavigationLink } from '../types';
|
|
5
|
+
import { useUrlbase, withUrlbase } from '@myst-theme/providers';
|
|
6
|
+
|
|
7
|
+
const FooterLink = ({ title, url, group, right }: NavigationLink & { right?: boolean }) => {
|
|
8
|
+
const urlbase = useUrlbase();
|
|
9
|
+
return (
|
|
10
|
+
<Link
|
|
11
|
+
prefetch="intent"
|
|
12
|
+
className="group flex-1 p-4 block border font-normal hover:border-blue-600 dark:hover:border-blue-400 no-underline hover:text-blue-600 dark:hover:text-blue-400 text-gray-600 dark:text-gray-100 border-gray-200 dark:border-gray-500 rounded shadow-sm hover:shadow-lg dark:shadow-neutral-700"
|
|
13
|
+
to={withUrlbase(url, urlbase)}
|
|
14
|
+
>
|
|
15
|
+
<div className="flex align-middle h-full">
|
|
16
|
+
{right && (
|
|
17
|
+
<ArrowLeftIcon className="w-6 h-6 self-center transition-transform group-hover:-translate-x-1" />
|
|
18
|
+
)}
|
|
19
|
+
<div className={classNames('flex-grow', { 'text-right': right })}>
|
|
20
|
+
<div className="text-xs text-gray-500 dark:text-gray-400">{group || ' '}</div>
|
|
21
|
+
{title}
|
|
22
|
+
</div>
|
|
23
|
+
{!right && (
|
|
24
|
+
<ArrowRightIcon className="w-6 h-6 self-center transition-transform group-hover:translate-x-1" />
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
</Link>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function FooterLinksBlock({ links }: { links?: FooterLinks }) {
|
|
32
|
+
if (!links) return null;
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex space-x-4 my-10">
|
|
35
|
+
{links.navigation?.prev && <FooterLink {...links.navigation?.prev} right />}
|
|
36
|
+
{links.navigation?.next && <FooterLink {...links.navigation?.next} />}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useTransition } from '@remix-run/react';
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Show a loading progess bad if the load takes more than 150ms
|
|
7
|
+
*/
|
|
8
|
+
function useLoading() {
|
|
9
|
+
const transitionState = useTransition().state;
|
|
10
|
+
const ref = useMemo<{ start?: NodeJS.Timeout; finish?: NodeJS.Timeout }>(() => ({}), []);
|
|
11
|
+
const [showLoading, setShowLoading] = useState(false);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (transitionState === 'loading') {
|
|
15
|
+
ref.start = setTimeout(() => {
|
|
16
|
+
setShowLoading(true);
|
|
17
|
+
}, 150);
|
|
18
|
+
} else {
|
|
19
|
+
if (ref.start) {
|
|
20
|
+
// We have stoped loading in <150ms
|
|
21
|
+
clearTimeout(ref.start);
|
|
22
|
+
delete ref.start;
|
|
23
|
+
setShowLoading(false);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
ref.finish = setTimeout(() => {
|
|
27
|
+
setShowLoading(false);
|
|
28
|
+
}, 150);
|
|
29
|
+
}
|
|
30
|
+
return () => {
|
|
31
|
+
if (ref.start) {
|
|
32
|
+
clearTimeout(ref.start);
|
|
33
|
+
delete ref.start;
|
|
34
|
+
}
|
|
35
|
+
if (ref.finish) {
|
|
36
|
+
clearTimeout(ref.finish);
|
|
37
|
+
delete ref.finish;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}, [transitionState]);
|
|
41
|
+
|
|
42
|
+
return { showLoading, isLoading: transitionState === 'loading' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function LoadingBar() {
|
|
46
|
+
const { isLoading, showLoading } = useLoading();
|
|
47
|
+
if (!showLoading) return null;
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
className={classNames(
|
|
51
|
+
'w-screen h-[2px] bg-blue-500 absolute left-0 bottom-0 transition-transform',
|
|
52
|
+
{
|
|
53
|
+
'animate-load scale-x-40': isLoading,
|
|
54
|
+
'scale-x-100': !isLoading,
|
|
55
|
+
},
|
|
56
|
+
)}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useNavOpen } from '@myst-theme/providers';
|
|
2
|
+
import { TableOfContents } from './TableOfContents';
|
|
3
|
+
|
|
4
|
+
export function Navigation({
|
|
5
|
+
children,
|
|
6
|
+
projectSlug,
|
|
7
|
+
top,
|
|
8
|
+
height,
|
|
9
|
+
hide_toc,
|
|
10
|
+
}: {
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
projectSlug?: string;
|
|
13
|
+
top?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
hide_toc?: boolean;
|
|
16
|
+
}) {
|
|
17
|
+
const [open, setOpen] = useNavOpen();
|
|
18
|
+
if (hide_toc) return <>{children}</>;
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
{open && (
|
|
22
|
+
<div
|
|
23
|
+
className="fixed inset-0 bg-black opacity-50 z-30"
|
|
24
|
+
onClick={() => setOpen(false)}
|
|
25
|
+
></div>
|
|
26
|
+
)}
|
|
27
|
+
{children}
|
|
28
|
+
<TableOfContents projectSlug={projectSlug} top={top} height={height} />
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
}
|