@myst-theme/site 0.1.35 → 0.1.37

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.1.35",
3
+ "version": "0.1.37",
4
4
  "main": "./src/index.ts",
5
5
  "types": "./src/index.ts",
6
6
  "files": [
@@ -16,20 +16,21 @@
16
16
  "dependencies": {
17
17
  "@headlessui/react": "^1.7.13",
18
18
  "@heroicons/react": "^2.0.14",
19
- "@myst-theme/diagrams": "^0.1.35",
20
- "@myst-theme/frontmatter": "^0.1.35",
21
- "@myst-theme/jupyter": "^0.1.35",
22
- "@myst-theme/providers": "^0.1.35",
19
+ "@myst-theme/diagrams": "^0.1.37",
20
+ "@myst-theme/frontmatter": "^0.1.37",
21
+ "@myst-theme/jupyter": "^0.1.37",
22
+ "@myst-theme/providers": "^0.1.37",
23
23
  "classnames": "^2.3.2",
24
24
  "lodash.throttle": "^4.1.1",
25
25
  "myst-common": "^0.0.16",
26
- "myst-config": "^0.0.12",
27
- "myst-demo": "^0.1.35",
28
- "myst-to-react": "^0.1.35",
26
+ "myst-config": "^0.0.14",
27
+ "myst-demo": "^0.1.37",
28
+ "myst-to-react": "^0.1.37",
29
29
  "nbtx": "^0.2.3",
30
30
  "node-cache": "^5.1.2",
31
31
  "node-fetch": "^2.6.7",
32
- "unist-util-select": "^4.0.1"
32
+ "unist-util-select": "^4.0.1",
33
+ "thebe-react": "^0.0.7"
33
34
  },
34
35
  "peerDependencies": {
35
36
  "@remix-run/node": "^1.12.0",
@@ -0,0 +1,124 @@
1
+ import { Spinner } from './Spinner';
2
+ import PlayCircleIcon from '@heroicons/react/24/outline/PlayCircleIcon';
3
+ import ArrowPathIcon from '@heroicons/react/24/outline/ArrowPathIcon';
4
+ import MinusCircleIcon from '@heroicons/react/24/outline/MinusCircleIcon';
5
+ import ArrowTopRightOnSquareIcon from '@heroicons/react/24/outline/ArrowTopRightOnSquareIcon';
6
+ import classNames from 'classnames';
7
+ import type { NotebookExecuteOptions } from 'thebe-react';
8
+ import { useThebeServer } from 'thebe-react';
9
+ import type { IThebeCellExecuteReturn } from 'thebe-core';
10
+ import { useMDASTNotebook, useNotebookCellExecution } from '@myst-theme/jupyter';
11
+
12
+ export function Run({
13
+ ready,
14
+ executing,
15
+ disabled,
16
+ execute,
17
+ }: {
18
+ ready: boolean;
19
+ executing: boolean;
20
+ disabled?: boolean;
21
+ execute: (
22
+ options?: NotebookExecuteOptions | undefined,
23
+ ) => Promise<(IThebeCellExecuteReturn | null)[]>;
24
+ }) {
25
+ return (
26
+ <div className="flex relative text-sm">
27
+ <button
28
+ className={classNames(
29
+ 'cursor-pointer text-gray-700 active:text-green-700 hover:opacity-100',
30
+ {
31
+ 'opacity-10 hover:opacity-10': executing,
32
+ 'opacity-60': !executing,
33
+ },
34
+ )}
35
+ disabled={disabled || !ready || executing}
36
+ onClick={() => execute()}
37
+ >
38
+ <PlayCircleIcon className="h-6 w-6 inline-block align-top" title="run all cells" />
39
+ </button>
40
+ {executing && (
41
+ <span className="absolute top-0 left-0 z-10 w-[22px] h-[22px] opacity-100">
42
+ <Spinner size={24} />
43
+ </span>
44
+ )}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export function Clear({
50
+ ready,
51
+ executing,
52
+ disabled,
53
+ clear,
54
+ }: {
55
+ ready: boolean;
56
+ executing: boolean;
57
+ disabled?: boolean;
58
+ clear: () => void;
59
+ }) {
60
+ return (
61
+ <button
62
+ className="flex cursor-pointer text-gray-700 active:text-green-700 opacity-60 hover:opacity-100"
63
+ disabled={disabled || !ready || executing}
64
+ onClick={() => clear()}
65
+ >
66
+ <MinusCircleIcon className="h-6 w-6 inline-block align-top" title="clear all outputs" />
67
+ </button>
68
+ );
69
+ }
70
+
71
+ export function RunCell({ id }: { id: string }) {
72
+ const exec = useNotebookCellExecution(id);
73
+ if (!exec?.ready) return null;
74
+ const { ready, executing, notebookIsExecuting, execute } = exec;
75
+ return (
76
+ <Run ready={ready} executing={executing} disabled={notebookIsExecuting} execute={execute} />
77
+ );
78
+ }
79
+
80
+ export function ClearCell({ id }: { id: string }) {
81
+ const exec = useNotebookCellExecution(id);
82
+ if (!exec?.ready) return null;
83
+ const { ready, executing, notebookIsExecuting, clear } = exec;
84
+ return <Clear ready={ready} executing={executing} disabled={notebookIsExecuting} clear={clear} />;
85
+ }
86
+
87
+ export function NotebookRunAll() {
88
+ const { ready: serverReady, server } = useThebeServer();
89
+ const exec = useMDASTNotebook();
90
+
91
+ if (!exec?.ready) return null;
92
+ const { ready, executing, executeAll, restart, clear } = exec;
93
+
94
+ const clickLaunchInJupyter = () => {
95
+ if (!serverReady || !server?.settings) return;
96
+ window.open(server.settings.baseUrl, '_blank');
97
+ };
98
+
99
+ return (
100
+ <div className="flex">
101
+ <div className="group flex relative space-x-1">
102
+ <Run ready={ready} executing={executing} execute={executeAll} />
103
+ <button
104
+ className="flex items-center cursor-pointer text-gray-700 active:text-green-700 opacity-60 hover:opacity-100"
105
+ disabled={!ready || executing}
106
+ onClick={() => restart()}
107
+ >
108
+ <ArrowPathIcon className="h-6 w-6" title="restart kernel" />
109
+ </button>
110
+ <Clear ready={ready} executing={executing} clear={clear} />
111
+ <button
112
+ className="flex items-center cursor-pointer text-gray-700 active:text-green-700 opacity-60 hover:opacity-100"
113
+ disabled={!ready}
114
+ onClick={clickLaunchInJupyter}
115
+ >
116
+ <ArrowTopRightOnSquareIcon
117
+ className="h-6 w-6 inline-block align-top"
118
+ title="launch in juptyer"
119
+ />
120
+ </button>
121
+ </div>
122
+ </div>
123
+ );
124
+ }
@@ -1,9 +1,28 @@
1
1
  import { useParse, DEFAULT_RENDERERS } from 'myst-to-react';
2
- import type { Parent } from 'myst-spec';
2
+ import type { GenericParent, SourceFileKind } from 'myst-common';
3
3
  import { useNodeRenderers } from '@myst-theme/providers';
4
4
  import classNames from 'classnames';
5
+ import { ClearCell, RunCell } from './ComputeControls';
5
6
 
6
- function Block({ id, node, className }: { id: string; node: Parent; className?: string }) {
7
+ function isACodeCell(node: GenericParent) {
8
+ return (
9
+ node.type === 'block' &&
10
+ node.children.length === 2 &&
11
+ node.children[0].type === 'code' &&
12
+ node.children[1].type === 'output'
13
+ );
14
+ }
15
+
16
+ function Block({
17
+ id,
18
+ node,
19
+ className,
20
+ }: {
21
+ id: string;
22
+ pageKind: SourceFileKind;
23
+ node: GenericParent;
24
+ className?: string;
25
+ }) {
7
26
  const renderers = useNodeRenderers() ?? DEFAULT_RENDERERS;
8
27
  const children = useParse(node, renderers);
9
28
  const subGrid = 'article-grid article-subgrid-gap col-screen';
@@ -13,23 +32,40 @@ function Block({ id, node, className }: { id: string; node: Parent; className?:
13
32
  (dataClassName && dataClassName.includes('col-')) || (className && className.includes('col-'));
14
33
  return (
15
34
  <div
35
+ key={id}
16
36
  id={id}
17
- className={classNames(className, dataClassName, {
37
+ className={classNames('relative group/block', className, dataClassName, {
18
38
  [subGrid]: !noSubGrid,
19
39
  })}
20
40
  >
21
41
  {children}
42
+ {isACodeCell(node) && (
43
+ <div className="hidden group-hover/block:flex md:flex-col absolute -top-[28px] md:top-0 right-0 md:-right-[28px] mt-8">
44
+ <RunCell id={id} />
45
+ <ClearCell id={id} />
46
+ </div>
47
+ )}
22
48
  </div>
23
49
  );
24
50
  }
25
51
 
26
- export function ContentBlocks({ mdast, className }: { mdast: Parent; className?: string }) {
27
- const blocks = mdast.children as Parent[];
52
+ export function ContentBlocks({
53
+ name,
54
+ pageKind,
55
+ mdast,
56
+ className,
57
+ }: {
58
+ name: string;
59
+ pageKind: SourceFileKind;
60
+ mdast: GenericParent;
61
+ className?: string;
62
+ }) {
63
+ const blocks = mdast.children as GenericParent[];
28
64
  return (
29
65
  <>
30
- {blocks.map((node, index) => {
31
- return <Block key={(node as any).key} id={`${index}`} node={node} className={className} />;
32
- })}
66
+ {blocks.map((node) => (
67
+ <Block key={node.key} id={node.key} pageKind={pageKind} node={node} className={className} />
68
+ ))}
33
69
  </>
34
70
  );
35
71
  }
@@ -1,3 +1,4 @@
1
+ import { useNavigation } from '@remix-run/react';
1
2
  import classNames from 'classnames';
2
3
  import throttle from 'lodash.throttle';
3
4
  import { useCallback, useEffect, useRef, useState } from 'react';
@@ -155,32 +156,51 @@ const useIntersectionObserver = (highlight: () => void, onScreen: Set<HTMLHeadin
155
156
  const DOC_OUTLINE_CLASS =
156
157
  'fixed z-10 bottom-0 right-[max(0px,calc(50%-45rem))] w-[14rem] lg:w-[18rem] py-10 px-4 lg:px-8 overflow-y-auto hidden lg:block';
157
158
 
159
+ export function useOutlineHeight<T extends HTMLElement = HTMLElement>() {
160
+ const container = useRef<T>(null);
161
+ const outline = useRef<T>(null);
162
+ const transitionState = useNavigation().state;
163
+ const setHeight = () => {
164
+ if (!container.current || !outline.current) return;
165
+ const height = container.current.offsetHeight - window.scrollY;
166
+ outline.current.style.display = height < 50 ? 'none' : '';
167
+ outline.current.style.height = height > window.innerHeight ? '' : `${height}px`;
168
+ outline.current.style.opacity = height && height > 300 ? '' : '0';
169
+ outline.current.style.pointerEvents = height && height > 300 ? '' : 'none';
170
+ };
171
+ useEffect(() => {
172
+ setHeight();
173
+ setTimeout(setHeight, 100); // Some lag sometimes
174
+ const handleScroll = () => setHeight();
175
+ window.addEventListener('scroll', handleScroll);
176
+ return () => {
177
+ window.removeEventListener('scroll', handleScroll);
178
+ };
179
+ }, [container, outline, transitionState]);
180
+ return { container, outline };
181
+ }
182
+
158
183
  export const DocumentOutline = ({
184
+ outlineRef,
159
185
  top,
160
- height,
161
186
  className = DOC_OUTLINE_CLASS,
162
187
  }: {
188
+ outlineRef?: React.RefObject<HTMLElement>;
163
189
  top?: number;
164
190
  height?: number;
165
191
  className?: string;
166
192
  }) => {
167
193
  const { activeId, headings, highlight } = useHeaders();
168
- if (height && height < 50) return null;
169
194
  if (headings.length <= 1) return <nav suppressHydrationWarning />;
170
195
  return (
171
196
  <nav
197
+ ref={outlineRef}
172
198
  aria-label="Document Outline"
173
199
  suppressHydrationWarning
174
200
  className={classNames('not-prose transition-opacity overflow-y-auto', className)}
175
201
  style={{
176
202
  top: top ?? 0,
177
- height:
178
- typeof document === 'undefined' || (height && height > window.innerHeight)
179
- ? undefined
180
- : height,
181
203
  maxHeight: `calc(100vh - ${(top ?? 0) + 20}px)`,
182
- opacity: height && height > 300 ? undefined : 0,
183
- pointerEvents: height && height > 300 ? undefined : 'none',
184
204
  }}
185
205
  >
186
206
  <div className="text-slate-900 mb-4 text-sm leading-6 dark:text-slate-100 uppercase">
@@ -0,0 +1,64 @@
1
+ import { useThebeCore, useThebeServer, useThebeSession } from 'thebe-react';
2
+ import PowerIcon from '@heroicons/react/24/outline/PowerIcon';
3
+ import { useHasNotebookProvider } from '@myst-theme/jupyter';
4
+ import { useNavigation } from '@remix-run/react';
5
+ import { useEffect, useState } from 'react';
6
+
7
+ export function EnableCompute({
8
+ canCompute,
9
+ children,
10
+ }: React.PropsWithChildren<{ canCompute: boolean }>) {
11
+ const { load, loading, core } = useThebeCore();
12
+ const { connect, connecting, ready: serverReady, error: serverError } = useThebeServer();
13
+ const {
14
+ start,
15
+ starting,
16
+ shutdown,
17
+ session,
18
+ ready: sessionReady,
19
+ error: sessionError,
20
+ } = useThebeSession();
21
+ const hasNotebookProvider = useHasNotebookProvider();
22
+ const navigation = useNavigation();
23
+ const [enabling, setEnabling] = useState(false);
24
+ const [enabled, setEnabled] = useState(false);
25
+ const busy = enabling || loading || connecting || starting;
26
+
27
+ useEffect(() => {
28
+ if (!enabling) return;
29
+ if (!core) return load();
30
+ if (!serverReady) return connect();
31
+ if (!sessionReady) start();
32
+ if (sessionReady) {
33
+ setEnabled(true);
34
+ setEnabling(false);
35
+ }
36
+ }, [enabling, core, serverReady, sessionReady]);
37
+
38
+ if (!canCompute || !hasNotebookProvider) return null;
39
+ let classes = 'flex text-center mr-1 cursor-pointer rounded-full';
40
+ const idleClasses = 'text-blue-700 hover:opacity-100 opacity-60';
41
+ const busyClasses = 'bg-yellow-700 text-yellow-700 opacity-100 font-semibold';
42
+ const readyClasses = 'bg-green-700 text-green-700 opacity-100 font-semibold';
43
+ const errorClasses = 'bg-red-700 text-red-700 opacity-100';
44
+
45
+ if (busy) classes += busyClasses;
46
+ else if (serverReady && sessionReady) classes += readyClasses;
47
+ else if (serverError || sessionError) classes += errorClasses;
48
+ else classes += idleClasses;
49
+
50
+ useEffect(() => {
51
+ if (navigation.state === 'loading') {
52
+ shutdown();
53
+ }
54
+ }, [shutdown, navigation]);
55
+
56
+ return (
57
+ <div className="flex mx-1 items-center">
58
+ <button className={classes} onClick={() => setEnabling(true)} disabled={enabling || enabled}>
59
+ <PowerIcon className="h-6 w-6 mx-1 inline-block align-top" title="enable compute" />
60
+ </button>
61
+ {enabled && <>{children}</>}
62
+ </div>
63
+ );
64
+ }
@@ -1,8 +1,10 @@
1
1
  import classNames from 'classnames';
2
2
  import { Link } from '@remix-run/react';
3
- import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
3
+ import ArrowLeftIcon from '@heroicons/react/24/outline/ArrowLeftIcon';
4
+ import ArrowRightIcon from '@heroicons/react/24/outline/ArrowRightIcon';
4
5
  import type { FooterLinks, NavigationLink } from '../types';
5
6
  import { useUrlbase, withUrlbase } from '@myst-theme/providers';
7
+ import { useEffect, useState } from 'react';
6
8
 
7
9
  const FooterLink = ({ title, url, group, right }: NavigationLink & { right?: boolean }) => {
8
10
  const urlbase = useUrlbase();
@@ -5,14 +5,14 @@ export function Navigation({
5
5
  children,
6
6
  projectSlug,
7
7
  top,
8
- height,
8
+ tocRef,
9
9
  hide_toc,
10
10
  footer,
11
11
  }: {
12
12
  children?: React.ReactNode;
13
13
  projectSlug?: string;
14
14
  top?: number;
15
- height?: number;
15
+ tocRef?: React.RefObject<HTMLDivElement>;
16
16
  hide_toc?: boolean;
17
17
  footer?: React.ReactNode;
18
18
  }) {
@@ -23,11 +23,12 @@ export function Navigation({
23
23
  {open && (
24
24
  <div
25
25
  className="fixed inset-0 bg-black opacity-50 z-30"
26
+ style={{ marginTop: top }}
26
27
  onClick={() => setOpen(false)}
27
28
  ></div>
28
29
  )}
29
30
  {children}
30
- <TableOfContents projectSlug={projectSlug} top={top} height={height} footer={footer} />
31
+ <TableOfContents tocRef={tocRef} projectSlug={projectSlug} top={top} footer={footer} />
31
32
  </>
32
33
  );
33
34
  }
@@ -1,6 +1,6 @@
1
- import React from 'react';
1
+ import React, { useEffect, useRef } from 'react';
2
2
  import classNames from 'classnames';
3
- import { NavLink, useParams, useLocation } from '@remix-run/react';
3
+ import { NavLink, useParams, useLocation, useNavigation } from '@remix-run/react';
4
4
  import type { SiteManifest } from 'myst-config';
5
5
  import { useNavOpen, useSiteManifest, useUrlbase, withUrlbase } from '@myst-theme/providers';
6
6
  import { getProjectHeadings } from '../../loaders';
@@ -102,14 +102,39 @@ const Headings = ({ folder, headings, sections }: Props) => {
102
102
  );
103
103
  };
104
104
 
105
+ export function useTocHeight<T extends HTMLElement = HTMLElement>(top?: number) {
106
+ const container = useRef<T>(null);
107
+ const toc = useRef<HTMLDivElement>(null);
108
+ const transitionState = useNavigation().state;
109
+ const setHeight = () => {
110
+ if (!container.current || !toc.current) return;
111
+ const height = container.current.offsetHeight - window.scrollY;
112
+ const div = toc.current.firstChild as HTMLDivElement;
113
+ const MAGIC_PADDING = 16; // I dunno, just go with it ...
114
+ if (div) div.style.height = `min(calc(100vh - ${top ?? 0}px), ${height + MAGIC_PADDING}px)`;
115
+ const nav = toc.current.querySelector('nav');
116
+ if (nav) nav.style.opacity = height > 150 ? '1' : '0';
117
+ };
118
+ useEffect(() => {
119
+ setHeight();
120
+ setTimeout(setHeight, 100); // Some lag sometimes
121
+ const handleScroll = () => setHeight();
122
+ window.addEventListener('scroll', handleScroll);
123
+ return () => {
124
+ window.removeEventListener('scroll', handleScroll);
125
+ };
126
+ }, [container, toc, transitionState]);
127
+ return { container, toc };
128
+ }
129
+
105
130
  export const TableOfContents = ({
106
131
  projectSlug,
107
132
  top,
108
- height,
133
+ tocRef,
109
134
  footer,
110
135
  }: {
111
136
  top?: number;
112
- height?: number;
137
+ tocRef?: React.RefObject<HTMLDivElement>;
113
138
  projectSlug?: string;
114
139
  footer?: React.ReactNode;
115
140
  }) => {
@@ -124,13 +149,13 @@ export const TableOfContents = ({
124
149
  if (!headings) return null;
125
150
  return (
126
151
  <div
152
+ ref={tocRef}
127
153
  className={classNames(
128
154
  'fixed xl:article-grid article-grid-gap xl:w-screen z-30 xl:pointer-events-none overflow-auto max-xl:min-w-[300px]',
129
155
  { hidden: !open },
130
156
  )}
131
157
  style={{
132
158
  top: top ?? 0,
133
- height: `min(calc(100vh - ${top ?? 0}px), ${height}px)`,
134
159
  }}
135
160
  >
136
161
  <div
@@ -149,7 +174,6 @@ export const TableOfContents = ({
149
174
  <nav
150
175
  aria-label="Table of Contents"
151
176
  className="flex-grow overflow-y-auto transition-opacity mt-6 pb-3 ml-3 xl:ml-0 mr-3"
152
- style={{ opacity: height && height > 150 ? undefined : 0 }}
153
177
  >
154
178
  <Headings folder={resolvedProjectSlug} headings={headings} sections={config?.projects} />
155
179
  </nav>
@@ -1,6 +1,6 @@
1
1
  import { useTheme } from '@myst-theme/providers';
2
- import { MoonIcon } from '@heroicons/react/24/solid';
3
- import { SunIcon } from '@heroicons/react/24/outline';
2
+ import MoonIcon from '@heroicons/react/24/solid/MoonIcon';
3
+ import SunIcon from '@heroicons/react/24/outline/SunIcon';
4
4
  import classNames from 'classnames';
5
5
 
6
6
  export function ThemeButton({ className = 'mx-3 h-8 w-8' }: { className?: string }) {
@@ -8,7 +8,7 @@ export function ThemeButton({ className = 'mx-3 h-8 w-8' }: { className?: string
8
8
  return (
9
9
  <button
10
10
  className={classNames(
11
- 'theme rounded-full border border-white border-solid overflow-hidden text-white hover:text-stone-500 hover:bg-white',
11
+ 'theme rounded-full border border-stone-700 dark:border-white hover:bg-neutral-100 border-solid overflow-hidden text-stone-700 dark:text-white hover:text-stone-500 dark:hover:text-neutral-800',
12
12
  className,
13
13
  )}
14
14
  title={`Change theme to ${isDark ? 'light' : 'dark'} mode.`}
@@ -2,11 +2,9 @@ import { Link, NavLink } from '@remix-run/react';
2
2
  import { Fragment } from 'react';
3
3
  import classNames from 'classnames';
4
4
  import { Menu, Transition } from '@headlessui/react';
5
- import {
6
- EllipsisVerticalIcon,
7
- Bars3Icon as MenuIcon,
8
- ChevronDownIcon,
9
- } from '@heroicons/react/24/solid';
5
+ import EllipsisVerticalIcon from '@heroicons/react/24/solid/EllipsisVerticalIcon';
6
+ import MenuIcon from '@heroicons/react/24/solid/Bars3Icon';
7
+ import ChevronDownIcon from '@heroicons/react/24/solid/ChevronDownIcon';
10
8
  import type { SiteManifest, SiteNavItem } from 'myst-config';
11
9
  import { ThemeButton } from './ThemeButton';
12
10
  import { useNavOpen, useSiteManifest } from '@myst-theme/providers';
@@ -58,7 +56,7 @@ function NavItem({ item }: { item: SiteNavItem }) {
58
56
  to={item.url ?? ''}
59
57
  className={({ isActive }) =>
60
58
  classNames(
61
- 'inline-flex items-center justify-center w-full mx-2 py-1 text-md font-medium text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75',
59
+ 'inline-flex items-center justify-center w-full mx-2 py-1 text-md font-medium dark:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75',
62
60
  {
63
61
  'border-b border-stone-200': isActive,
64
62
  },
@@ -73,12 +71,9 @@ function NavItem({ item }: { item: SiteNavItem }) {
73
71
  return (
74
72
  <Menu as="div" className="relative grow-0 inline-block mx-2">
75
73
  <div className="inline-block">
76
- <Menu.Button className="inline-flex items-center justify-center w-full mx-2 py-1 text-md font-medium text-white rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
74
+ <Menu.Button className="inline-flex items-center justify-center w-full mx-2 py-1 text-md font-medium text-stone-900 dark:text-white rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
77
75
  <span>{item.title}</span>
78
- <ChevronDownIcon
79
- className="w-5 h-5 ml-2 -mr-1 text-violet-200 hover:text-violet-100"
80
- aria-hidden="true"
81
- />
76
+ <ChevronDownIcon className="w-5 h-5 ml-2 -mr-1 text-violet-200 hover:text-violet-100" />
82
77
  </Menu.Button>
83
78
  </div>
84
79
  <Transition
@@ -184,11 +179,15 @@ function HomeLink({ logo, logoText, name }: { logo?: string; logoText?: string;
184
179
  const nothingSet = !logo && !logoText;
185
180
  return (
186
181
  <Link
187
- className="flex items-center text-white w-fit ml-3 md:ml-5 xl:ml-7"
182
+ className="flex items-center dark:text-white w-fit ml-3 md:ml-5 xl:ml-7"
188
183
  to="/"
189
184
  prefetch="intent"
190
185
  >
191
- {logo && <img src={logo} className="h-9 mr-3" alt={logoText || name} height="2.25rem"></img>}
186
+ {logo && (
187
+ <div className="dark:bg-white dark:rounded p-1 mr-3">
188
+ <img src={logo} className="h-9" alt={logoText || name} height="2.25rem"></img>
189
+ </div>
190
+ )}
192
191
  <span
193
192
  className={classNames('text-md sm:text-xl tracking-tight sm:mr-5', {
194
193
  'sr-only': !(logoText || nothingSet),
@@ -205,18 +204,18 @@ export function TopNav() {
205
204
  const config = useSiteManifest();
206
205
  const { logo, logo_text, logoText, actions, title, nav } = config ?? ({} as SiteManifest);
207
206
  return (
208
- <div className="bg-stone-700 p-3 md:px-8 fixed w-screen top-0 z-30 h-[60px]">
207
+ <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]">
209
208
  <nav className="flex items-center justify-between flex-wrap max-w-[1440px] mx-auto">
210
209
  <div className="flex flex-row xl:min-w-[19.5rem] mr-2 sm:mr-7 justify-start items-center">
211
210
  <div className="block xl:hidden">
212
211
  <button
213
- className="flex items-center text-stone-200 border-stone-400 hover:text-white"
212
+ className="flex items-center border-stone-400 text-stone-800 hover:text-stone-900 dark:text-stone-200 hover:dark:text-stone-100"
214
213
  onClick={() => {
215
214
  setOpen(!open);
216
215
  }}
217
216
  >
217
+ <MenuIcon className="h-8 w-8 p-1" />
218
218
  <span className="sr-only">Open Menu</span>
219
- <MenuIcon className="fill-current h-8 w-8 p-1" />
220
219
  </button>
221
220
  </div>
222
221
  <HomeLink name={title} logo={logo} logoText={logo_text || logoText} />
@@ -232,7 +231,7 @@ export function TopNav() {
232
231
  {actions?.map((action, index) => (
233
232
  <ExternalOrInternalLink
234
233
  key={action.url || index}
235
- className="inline-block text-md px-4 py-2 mx-1 leading-none border rounded text-white border-white hover:border-transparent hover:text-stone-500 hover:bg-white mt-0"
234
+ className="inline-block text-md px-4 py-2 mx-1 leading-none border rounded border-stone-700 dark:border-white text-stone-700 dark:text-white hover:text-stone-500 dark:hover:text-neutral-800 hover:bg-neutral-100 mt-0"
236
235
  to={action.url}
237
236
  >
238
237
  {action.title}
@@ -1,5 +1,5 @@
1
1
  export { ThemeButton } from './ThemeButton';
2
2
  export { TopNav, DEFAULT_NAV_HEIGHT } from './TopNav';
3
3
  export { Navigation } from './Navigation';
4
- export { TableOfContents } from './TableOfContents';
4
+ export { TableOfContents, useTocHeight } from './TableOfContents';
5
5
  export { LoadingBar } from './Loading';
@@ -0,0 +1,23 @@
1
+ export function Spinner({ size }: { size: number }) {
2
+ return (
3
+ <div role="status">
4
+ <svg
5
+ aria-hidden="true"
6
+ className={`w-[${size}px] h-[${size}px] mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-green-600`}
7
+ viewBox="0 0 100 101"
8
+ fill="none"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ >
11
+ <path
12
+ d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
13
+ fill="currentColor"
14
+ />
15
+ <path
16
+ d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
17
+ fill="currentFill"
18
+ />
19
+ </svg>
20
+ <span className="sr-only">Loading...</span>
21
+ </div>
22
+ );
23
+ }
@@ -1,5 +1,5 @@
1
1
  export { ContentBlocks } from './ContentBlocks';
2
- export { DocumentOutline } from './DocumentOutline';
2
+ export { DocumentOutline, useOutlineHeight } from './DocumentOutline';
3
3
  export { FooterLinksBlock } from './FooterLinksBlock';
4
4
  export { ContentReload } from './ContentReload';
5
5
  export { Bibliography } from './Bibliography';
package/src/index.ts CHANGED
@@ -4,4 +4,3 @@ export * from './loaders';
4
4
  export * from './components';
5
5
  export * from './pages';
6
6
  export * from './seo';
7
- export * from './hooks';
@@ -1,26 +1,54 @@
1
- import { ReferencesProvider } from '@myst-theme/providers';
1
+ import React from 'react';
2
+ import { ReferencesProvider, useComputeOptions } from '@myst-theme/providers';
2
3
  import { FrontmatterBlock } from '@myst-theme/frontmatter';
3
4
  import { Bibliography, ContentBlocks, FooterLinksBlock } from '../components';
4
5
  import { ErrorDocumentNotFound } from './ErrorDocumentNotFound';
5
6
  import { ErrorProjectNotFound } from './ErrorProjectNotFound';
6
7
  import type { PageLoader } from '../types';
8
+ import { ThebeSessionProvider } from 'thebe-react';
9
+ import type { GenericParent } from 'myst-common';
10
+ import { SourceFileKind } from 'myst-common';
11
+ import { EnableCompute } from '../components/EnableCompute';
12
+ import { NotebookRunAll } from '../components/ComputeControls';
13
+ import { NotebookProvider, BinderBadge } from '@myst-theme/jupyter';
7
14
 
8
- export function ArticlePage({ article }: { article: PageLoader }) {
9
- const { hide_title_block, hide_footer_links } = (article.frontmatter as any)?.design ?? {};
15
+ export const ArticlePage = React.memo(function ({ article }: { article: PageLoader }) {
16
+ const computeOptions = useComputeOptions();
17
+ const canCompute = computeOptions.canCompute && (article.frontmatter as any)?.thebe !== false;
18
+ const { hide_title_block, hide_footer_links, binder } =
19
+ (article.frontmatter as any)?.design ?? {};
20
+ const isJupyter = article?.kind && article.kind === SourceFileKind.Notebook;
10
21
  return (
11
22
  <ReferencesProvider
12
23
  references={{ ...article.references, article: article.mdast }}
13
24
  frontmatter={article.frontmatter}
14
25
  >
15
- {!hide_title_block && (
16
- <FrontmatterBlock kind={article.kind} frontmatter={article.frontmatter} />
17
- )}
18
- <ContentBlocks mdast={article.mdast} />
19
- <Bibliography />
20
- {!hide_footer_links && <FooterLinksBlock links={article.footer} />}
26
+ <ThebeSessionProvider start={false} name={article.slug}>
27
+ {!hide_title_block && (
28
+ <FrontmatterBlock kind={article.kind} frontmatter={article.frontmatter} />
29
+ )}
30
+ <NotebookProvider siteConfig={false} page={article}>
31
+ <div className="flex items-center">
32
+ <div className="flex-grow"></div>
33
+ {binder && <BinderBadge binder={binder} />}
34
+ {canCompute && isJupyter && (
35
+ <EnableCompute canCompute={true} key={article.slug}>
36
+ <NotebookRunAll />
37
+ </EnableCompute>
38
+ )}
39
+ </div>
40
+ <ContentBlocks
41
+ name={article.slug}
42
+ pageKind={article.kind}
43
+ mdast={article.mdast as GenericParent}
44
+ />
45
+ <Bibliography />
46
+ {!hide_footer_links && <FooterLinksBlock links={article.footer} />}
47
+ </NotebookProvider>
48
+ </ThebeSessionProvider>
21
49
  </ReferencesProvider>
22
50
  );
23
- }
51
+ });
24
52
 
25
53
  export function ProjectPageCatchBoundary() {
26
54
  return <ErrorProjectNotFound />;
@@ -16,6 +16,8 @@ import { ContentReload, renderers } from '../components';
16
16
  import { Analytics } from '../seo';
17
17
  import { ErrorSiteNotFound } from './ErrorSiteNotFound';
18
18
  import classNames from 'classnames';
19
+ import { ThebeCoreProvider } from 'thebe-react';
20
+ import { ConfiguredThebeServerProvider } from '@myst-theme/jupyter';
19
21
 
20
22
  export function Document({
21
23
  children,
@@ -47,7 +49,11 @@ export function Document({
47
49
  </head>
48
50
  <body className="m-0 transition-colors duration-500 bg-white dark:bg-stone-900">
49
51
  <ThemeProvider theme={theme} renderers={renderers} Link={RemixLink as any}>
50
- <SiteProvider config={config}>{children}</SiteProvider>
52
+ <ThebeCoreProvider>
53
+ <SiteProvider config={config}>
54
+ <ConfiguredThebeServerProvider>{children}</ConfiguredThebeServerProvider>
55
+ </SiteProvider>
56
+ </ThebeCoreProvider>
51
57
  </ThemeProvider>
52
58
  <ScrollRestoration />
53
59
  <Scripts />
@@ -1,23 +0,0 @@
1
- import { useTransition } from '@remix-run/react';
2
- import { useEffect, useRef, useState } from 'react';
3
-
4
- export function useNavigationHeight<T extends HTMLElement = HTMLElement>() {
5
- const ref = useRef<T>(null);
6
- const [height, setHeightState] = useState(1000);
7
- const transitionState = useTransition().state;
8
- const setHeight = () => {
9
- if (ref.current) {
10
- setHeightState(ref.current.offsetHeight - window.scrollY);
11
- }
12
- };
13
- useEffect(() => {
14
- setHeight();
15
- setTimeout(setHeight, 100); // Some lag sometimes
16
- const handleScroll = () => setHeight();
17
- window.addEventListener('scroll', handleScroll);
18
- return () => {
19
- window.removeEventListener('scroll', handleScroll);
20
- };
21
- }, [ref, transitionState]);
22
- return { ref, height };
23
- }