@myst-theme/site 1.0.1 → 1.1.1
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": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@headlessui/react": "^1.7.15",
|
|
27
27
|
"@heroicons/react": "^2.0.18",
|
|
28
|
-
"@myst-theme/common": "^1.
|
|
29
|
-
"@myst-theme/diagrams": "^1.
|
|
30
|
-
"@myst-theme/frontmatter": "^1.
|
|
31
|
-
"@myst-theme/providers": "^1.
|
|
32
|
-
"@myst-theme/search": "^1.
|
|
28
|
+
"@myst-theme/common": "^1.1.1",
|
|
29
|
+
"@myst-theme/diagrams": "^1.1.1",
|
|
30
|
+
"@myst-theme/frontmatter": "^1.1.1",
|
|
31
|
+
"@myst-theme/providers": "^1.1.1",
|
|
32
|
+
"@myst-theme/search": "^1.1.1",
|
|
33
33
|
"@radix-ui/react-collapsible": "^1.0.3",
|
|
34
34
|
"@radix-ui/react-dialog": "^1.0.3",
|
|
35
35
|
"@radix-ui/react-visually-hidden": "^1.1.0",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"lodash.throttle": "^4.1.1",
|
|
38
38
|
"myst-common": "^1.8.1",
|
|
39
39
|
"myst-config": "^1.7.9",
|
|
40
|
-
"myst-demo": "^1.
|
|
40
|
+
"myst-demo": "^1.1.1",
|
|
41
41
|
"myst-spec-ext": "^1.8.1",
|
|
42
|
-
"myst-to-react": "^1.
|
|
42
|
+
"myst-to-react": "^1.1.1",
|
|
43
43
|
"nbtx": "^0.2.3",
|
|
44
44
|
"node-cache": "^5.1.2",
|
|
45
45
|
"node-fetch": "^2.6.11",
|
|
@@ -2,12 +2,13 @@ import {
|
|
|
2
2
|
useBaseurl,
|
|
3
3
|
useNavLinkProvider,
|
|
4
4
|
useSiteManifest,
|
|
5
|
+
useThemeTop,
|
|
5
6
|
withBaseurl,
|
|
6
7
|
} from '@myst-theme/providers';
|
|
7
8
|
import { useNavigation } from '@remix-run/react';
|
|
8
9
|
import classNames from 'classnames';
|
|
9
10
|
import throttle from 'lodash.throttle';
|
|
10
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
11
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
11
12
|
import type { RefObject } from 'react';
|
|
12
13
|
import { DocumentChartBarIcon } from '@heroicons/react/24/outline';
|
|
13
14
|
import { ChevronRightIcon } from '@heroicons/react/24/solid';
|
|
@@ -148,13 +149,24 @@ const useIntersectionObserver = (elements: Element[], options?: Record<string, a
|
|
|
148
149
|
|
|
149
150
|
if (!onClient) return { observer };
|
|
150
151
|
useEffect(() => {
|
|
152
|
+
// We want a list of all header elements on screen to loop through, but:
|
|
153
|
+
// IntersectionObserver returns elements that have *changed state*, not the full list of elements on screen.
|
|
154
|
+
// So we maintain a set of on-screen elements and add/remove as new intersection events happen.
|
|
151
155
|
const cb: IntersectionObserverCallback = (entries) => {
|
|
152
|
-
setIntersecting(
|
|
156
|
+
setIntersecting((prev) => {
|
|
157
|
+
const next = new Set(prev);
|
|
158
|
+
// Add or remove from our set based on intersection state
|
|
159
|
+
entries.forEach((e) => {
|
|
160
|
+
if (e.isIntersecting) next.add(e.target);
|
|
161
|
+
else next.delete(e.target);
|
|
162
|
+
});
|
|
163
|
+
return Array.from(next);
|
|
164
|
+
});
|
|
153
165
|
};
|
|
154
166
|
const o = new IntersectionObserver(cb, options ?? {});
|
|
155
167
|
setObserver(o);
|
|
156
168
|
return () => o.disconnect();
|
|
157
|
-
}, []);
|
|
169
|
+
}, [options]);
|
|
158
170
|
|
|
159
171
|
// Changes to the DOM mean we need to update our intersection observer
|
|
160
172
|
useEffect(() => {
|
|
@@ -163,12 +175,18 @@ const useIntersectionObserver = (elements: Element[], options?: Record<string, a
|
|
|
163
175
|
}
|
|
164
176
|
// Observe all heading elements
|
|
165
177
|
const toWatch = elements;
|
|
166
|
-
toWatch.
|
|
178
|
+
toWatch.forEach((e) => {
|
|
179
|
+
observer.observe(e);
|
|
180
|
+
});
|
|
167
181
|
// Cleanup afterwards
|
|
168
182
|
return () => {
|
|
169
|
-
toWatch.
|
|
183
|
+
toWatch.forEach((e) => {
|
|
184
|
+
observer.unobserve(e);
|
|
185
|
+
});
|
|
186
|
+
// Ensure we don't keep stale references when elements are removed/unobserved.
|
|
187
|
+
setIntersecting((prev) => prev.filter((e) => !toWatch.includes(e)));
|
|
170
188
|
};
|
|
171
|
-
}, [elements]);
|
|
189
|
+
}, [elements, observer]);
|
|
172
190
|
|
|
173
191
|
return { observer, intersecting };
|
|
174
192
|
};
|
|
@@ -177,6 +195,7 @@ const useIntersectionObserver = (elements: Element[], options?: Record<string, a
|
|
|
177
195
|
* Keep track of which headers are visible, and which header is active
|
|
178
196
|
*/
|
|
179
197
|
export function useHeaders(selector: string, maxdepth: number) {
|
|
198
|
+
const topOffset = useThemeTop();
|
|
180
199
|
if (!onClient) return { activeId: '', headings: [] };
|
|
181
200
|
// Keep track of main manually for now
|
|
182
201
|
const mainElementRef = useRef<HTMLElement | null>(null);
|
|
@@ -192,7 +211,8 @@ export function useHeaders(selector: string, maxdepth: number) {
|
|
|
192
211
|
setElements(getHeaders(selector));
|
|
193
212
|
},
|
|
194
213
|
500,
|
|
195
|
-
|
|
214
|
+
// Trailing updates help ensure we eventually process the last DOM mutation burst.
|
|
215
|
+
{ trailing: true },
|
|
196
216
|
),
|
|
197
217
|
[selector],
|
|
198
218
|
);
|
|
@@ -210,6 +230,9 @@ export function useHeaders(selector: string, maxdepth: number) {
|
|
|
210
230
|
const [activeId, setActiveId] = useState<string>();
|
|
211
231
|
|
|
212
232
|
useEffect(() => {
|
|
233
|
+
// Use the theme's top offset (navbar height) + a bit of padding for filtering active headings to avoid over-shooting the header.
|
|
234
|
+
const OFFSET_PX = topOffset - 10;
|
|
235
|
+
// Prefer a heading marked as highlighted (e.g. focus/anchor) if one is currently intersecting.
|
|
213
236
|
const highlighted = intersecting!.reduce(
|
|
214
237
|
(a, b) => {
|
|
215
238
|
if (a) return a;
|
|
@@ -218,11 +241,28 @@ export function useHeaders(selector: string, maxdepth: number) {
|
|
|
218
241
|
},
|
|
219
242
|
null as string | null,
|
|
220
243
|
);
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
244
|
+
const intersectingElements = intersecting as HTMLElement[];
|
|
245
|
+
// Choose the heading closest to the navbar offset line within a viewport window under it.
|
|
246
|
+
// Using a window avoids a case where the active header is off screen the next header is way at the bottom of the screen.
|
|
247
|
+
let bestInActiveHeaderWindow: { el: HTMLElement; distance: number } | undefined;
|
|
248
|
+
const ACTIVE_WINDOW_PX = window.innerHeight * 0.33;
|
|
249
|
+
for (const el of intersectingElements) {
|
|
250
|
+
const distance = el.getBoundingClientRect().top - OFFSET_PX;
|
|
251
|
+
if (
|
|
252
|
+
// Only keep things under the navbar line
|
|
253
|
+
distance >= 0 &&
|
|
254
|
+
// Only keep things over the active window size
|
|
255
|
+
distance <= ACTIVE_WINDOW_PX &&
|
|
256
|
+
// Now if it's closer to the navbar line than the active element, update active
|
|
257
|
+
(!bestInActiveHeaderWindow || distance < bestInActiveHeaderWindow.distance)
|
|
258
|
+
) {
|
|
259
|
+
bestInActiveHeaderWindow = { el, distance };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// If nothing is below the navbar line, keep the current active heading.
|
|
263
|
+
const active = bestInActiveHeaderWindow?.el;
|
|
264
|
+
if (highlighted || active) setActiveId(highlighted || active?.id);
|
|
265
|
+
}, [intersecting, topOffset]);
|
|
226
266
|
|
|
227
267
|
const [headings, setHeadings] = useState<Heading[]>([]);
|
|
228
268
|
useEffect(() => {
|
|
@@ -292,6 +332,8 @@ export function useOutlineHeight<T extends HTMLElement = HTMLElement>(
|
|
|
292
332
|
function useMarginOccluder() {
|
|
293
333
|
const [occluded, setOccluded] = useState(false);
|
|
294
334
|
const [elements, setElements] = useState<Element[]>([]);
|
|
335
|
+
// Memoize options so `useIntersectionObserver(..., options)` doesn't recreate the observer each render.
|
|
336
|
+
const intersectionOptions = useMemo(() => ({ rootMargin: '0px 0px -33% 0px' }), []);
|
|
295
337
|
|
|
296
338
|
// Keep track of main manually for now
|
|
297
339
|
const mainElementRef = useRef<HTMLElement | null>(null);
|
|
@@ -330,7 +372,8 @@ function useMarginOccluder() {
|
|
|
330
372
|
setElements(Array.from(marginElements));
|
|
331
373
|
},
|
|
332
374
|
500,
|
|
333
|
-
|
|
375
|
+
// Trailing updates help ensure we eventually process the last DOM mutation burst.
|
|
376
|
+
{ trailing: true },
|
|
334
377
|
),
|
|
335
378
|
[],
|
|
336
379
|
);
|
|
@@ -343,7 +386,7 @@ function useMarginOccluder() {
|
|
|
343
386
|
// Trigger initial update
|
|
344
387
|
useEffect(onMutation, []);
|
|
345
388
|
// Keep tabs of margin elements on screen
|
|
346
|
-
const { intersecting } = useIntersectionObserver(elements,
|
|
389
|
+
const { intersecting } = useIntersectionObserver(elements, intersectionOptions);
|
|
347
390
|
useEffect(() => {
|
|
348
391
|
setOccluded(intersecting!.length > 0);
|
|
349
392
|
}, [intersecting]);
|
|
@@ -401,6 +444,7 @@ export const DocumentOutline = ({
|
|
|
401
444
|
className={classNames(
|
|
402
445
|
'myst-outline not-prose overflow-y-auto',
|
|
403
446
|
'transition-opacity duration-700', // Animation on load
|
|
447
|
+
'bg-white/95 dark:bg-stone-900/95 backdrop-blur-sm rounded-lg p-2 -m-2', // Solid background to avoid overlap with margin content
|
|
404
448
|
className,
|
|
405
449
|
)}
|
|
406
450
|
style={{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { useLinkProvider, useNavLinkProvider } from '@myst-theme/providers';
|
|
2
|
+
import { isExternalUrl, useLinkProvider, useNavLinkProvider } from '@myst-theme/providers';
|
|
3
3
|
|
|
4
4
|
export function ExternalOrInternalLink({
|
|
5
5
|
to,
|
|
@@ -19,7 +19,7 @@ export function ExternalOrInternalLink({
|
|
|
19
19
|
const Link = useLinkProvider();
|
|
20
20
|
const NavLink = useNavLinkProvider();
|
|
21
21
|
const staticClass = typeof className === 'function' ? className({ isActive: false }) : className;
|
|
22
|
-
if (
|
|
22
|
+
if (isExternalUrl(to)) {
|
|
23
23
|
return (
|
|
24
24
|
<a
|
|
25
25
|
href={to}
|
|
@@ -174,7 +174,7 @@ function SearchShortcut() {
|
|
|
174
174
|
return (
|
|
175
175
|
<div
|
|
176
176
|
aria-hidden
|
|
177
|
-
className="myst-search-shortcut items-center hidden mx-1 font-mono text-sm text-gray-600 sm:flex gap-x-1"
|
|
177
|
+
className="myst-search-shortcut items-center hidden mx-1 font-mono text-sm text-gray-600 dark:text-gray-300 sm:flex gap-x-1"
|
|
178
178
|
>
|
|
179
179
|
<kbd
|
|
180
180
|
className={classNames(
|
|
@@ -507,7 +507,7 @@ function SearchForm({
|
|
|
507
507
|
<form onSubmit={onSubmit}>
|
|
508
508
|
<div className="relative flex w-full h-10 flow-row gap-x-1 ">
|
|
509
509
|
<label id={searchLabelID} htmlFor={searchInputID}>
|
|
510
|
-
<MagnifyingGlassIcon className="absolute text-gray-600 inset-y-0 start-0 h-10 w-10 p-2.5 aspect-square flex items-center pointer-events-none" />
|
|
510
|
+
<MagnifyingGlassIcon className="absolute text-gray-600 dark:text-gray-300 inset-y-0 start-0 h-10 w-10 p-2.5 aspect-square flex items-center pointer-events-none" />
|
|
511
511
|
<span className="hidden">Search query</span>
|
|
512
512
|
</label>
|
|
513
513
|
<input
|
|
@@ -563,7 +563,7 @@ const SearchPlaceholderButton = forwardRef<
|
|
|
563
563
|
className={classNames(
|
|
564
564
|
'myst-search-bar',
|
|
565
565
|
className,
|
|
566
|
-
'flex items-center h-10 aspect-square sm:w-64 text-left text-gray-600',
|
|
566
|
+
'flex items-center h-10 aspect-square sm:w-64 text-left text-gray-600 dark:text-gray-300',
|
|
567
567
|
'border border-gray-300 dark:border-gray-600',
|
|
568
568
|
'rounded-lg bg-gray-50 dark:bg-gray-700',
|
|
569
569
|
{
|
|
@@ -172,7 +172,7 @@ const NestedToc = ({ heading }: { heading: NestedHeading }) => {
|
|
|
172
172
|
<Collapsible.Root className="w-full" open={open} onOpenChange={setOpen}>
|
|
173
173
|
<div
|
|
174
174
|
className={classNames(
|
|
175
|
-
'myst-toc-item flex flex-row w-full gap-2
|
|
175
|
+
'myst-toc-item flex flex-row w-full gap-2 pl-2 my-1 text-left rounded-lg outline-none',
|
|
176
176
|
{
|
|
177
177
|
'myst-toc-item-exact bg-blue-300/30': exact,
|
|
178
178
|
'hover:bg-slate-300/30': !exact,
|
|
@@ -189,7 +189,7 @@ const NestedToc = ({ heading }: { heading: NestedHeading }) => {
|
|
|
189
189
|
/>
|
|
190
190
|
<Collapsible.Trigger asChild>
|
|
191
191
|
<button
|
|
192
|
-
className="self-center flex-none rounded-md group hover:bg-slate-300/30 focus:outline outline-blue-200 outline-2"
|
|
192
|
+
className="self-stretch flex items-center flex-none px-1 rounded-l-md group hover:bg-slate-300/30 focus-visible:outline outline-blue-200 outline-2"
|
|
193
193
|
aria-label="Open Folder"
|
|
194
194
|
>
|
|
195
195
|
<ChevronRightIcon
|