@myst-theme/site 0.11.0 → 0.13.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 +20 -12
- package/src/actions/index.ts +1 -0
- package/src/actions/theme.ts +8 -0
- package/src/components/DocumentOutline.tsx +254 -101
- package/src/components/Navigation/PrimarySidebar.tsx +7 -1
- package/src/components/Navigation/Search.tsx +630 -0
- package/src/components/Navigation/ThemeButton.tsx +7 -10
- package/src/components/Navigation/TopNav.tsx +4 -7
- package/src/components/index.ts +1 -0
- package/src/components/theme.tsx +19 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/theme.tsx +84 -0
- package/src/index.ts +2 -0
- package/src/loaders/theme.server.ts +3 -2
- package/src/pages/Root.tsx +75 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myst-theme/site",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -21,23 +21,31 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@headlessui/react": "^1.7.15",
|
|
23
23
|
"@heroicons/react": "^2.0.18",
|
|
24
|
-
"@myst-theme/common": "^0.
|
|
25
|
-
"@myst-theme/diagrams": "^0.
|
|
26
|
-
"@myst-theme/frontmatter": "^0.
|
|
27
|
-
"@myst-theme/jupyter": "^0.
|
|
28
|
-
"@myst-theme/providers": "^0.
|
|
24
|
+
"@myst-theme/common": "^0.13.0",
|
|
25
|
+
"@myst-theme/diagrams": "^0.13.0",
|
|
26
|
+
"@myst-theme/frontmatter": "^0.13.0",
|
|
27
|
+
"@myst-theme/jupyter": "^0.13.0",
|
|
28
|
+
"@myst-theme/providers": "^0.13.0",
|
|
29
|
+
"@myst-theme/search": "^0.13.0",
|
|
29
30
|
"@radix-ui/react-collapsible": "^1.0.3",
|
|
31
|
+
"@radix-ui/react-dialog": "^1.0.3",
|
|
32
|
+
"@radix-ui/react-radio-group": "^1.2.0",
|
|
33
|
+
"@radix-ui/react-roving-focus": "^1.1.0",
|
|
34
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
35
|
+
"@radix-ui/react-visually-hidden": "^1.1.0",
|
|
30
36
|
"classnames": "^2.3.2",
|
|
31
37
|
"lodash.throttle": "^4.1.1",
|
|
32
|
-
"myst-common": "^1.
|
|
33
|
-
"myst-config": "^1.
|
|
34
|
-
"myst-demo": "^0.
|
|
35
|
-
"myst-spec-ext": "^1.
|
|
36
|
-
"myst-to-react": "^0.
|
|
38
|
+
"myst-common": "^1.7.0",
|
|
39
|
+
"myst-config": "^1.7.0",
|
|
40
|
+
"myst-demo": "^0.13.0",
|
|
41
|
+
"myst-spec-ext": "^1.7.0",
|
|
42
|
+
"myst-to-react": "^0.13.0",
|
|
37
43
|
"nbtx": "^0.2.3",
|
|
38
44
|
"node-cache": "^5.1.2",
|
|
39
45
|
"node-fetch": "^2.6.11",
|
|
40
|
-
"
|
|
46
|
+
"react-merge-refs": "^2.1.1",
|
|
47
|
+
"string.prototype.matchall": "^4.0.11",
|
|
48
|
+
"thebe-react": "0.4.10",
|
|
41
49
|
"unist-util-select": "^4.0.1"
|
|
42
50
|
},
|
|
43
51
|
"peerDependencies": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './theme.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Theme } from '@myst-theme/common';
|
|
2
|
+
|
|
3
|
+
export function postThemeToAPI(theme: Theme) {
|
|
4
|
+
const xmlhttp = new XMLHttpRequest();
|
|
5
|
+
xmlhttp.open('POST', '/api/theme');
|
|
6
|
+
xmlhttp.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
|
|
7
|
+
xmlhttp.send(JSON.stringify({ theme }));
|
|
8
|
+
}
|
|
@@ -8,10 +8,12 @@ import { useNavigation } from '@remix-run/react';
|
|
|
8
8
|
import classNames from 'classnames';
|
|
9
9
|
import throttle from 'lodash.throttle';
|
|
10
10
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
11
|
+
import type { RefObject } from 'react';
|
|
11
12
|
import { DocumentChartBarIcon } from '@heroicons/react/24/outline';
|
|
13
|
+
import { ChevronRightIcon } from '@heroicons/react/24/solid';
|
|
14
|
+
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
12
15
|
|
|
13
16
|
const SELECTOR = [1, 2, 3, 4].map((n) => `main h${n}`).join(', ');
|
|
14
|
-
const HIGHLIGHT_CLASS = 'highlight';
|
|
15
17
|
|
|
16
18
|
const onClient = typeof document !== 'undefined';
|
|
17
19
|
|
|
@@ -25,15 +27,13 @@ export type Heading = {
|
|
|
25
27
|
|
|
26
28
|
type Props = {
|
|
27
29
|
headings: Heading[];
|
|
28
|
-
selector: string;
|
|
29
30
|
activeId?: string;
|
|
30
|
-
highlight?: () => void;
|
|
31
31
|
};
|
|
32
32
|
/**
|
|
33
33
|
* This renders an item in the table of contents list.
|
|
34
34
|
* scrollIntoView is used to ensure that when a user clicks on an item, it will smoothly scroll.
|
|
35
35
|
*/
|
|
36
|
-
const Headings = ({ headings, activeId
|
|
36
|
+
const Headings = ({ headings, activeId }: Props) => (
|
|
37
37
|
<ul className="text-sm leading-6 text-slate-400">
|
|
38
38
|
{headings.map((heading) => (
|
|
39
39
|
<li
|
|
@@ -62,11 +62,7 @@ const Headings = ({ headings, activeId, highlight, selector }: Props) => (
|
|
|
62
62
|
e.preventDefault();
|
|
63
63
|
const el = document.querySelector(`#${heading.id}`);
|
|
64
64
|
if (!el) return;
|
|
65
|
-
|
|
66
|
-
h.classList.remove(HIGHLIGHT_CLASS);
|
|
67
|
-
});
|
|
68
|
-
el.classList.add(HIGHLIGHT_CLASS);
|
|
69
|
-
highlight?.();
|
|
65
|
+
|
|
70
66
|
el.scrollIntoView({ behavior: 'smooth' });
|
|
71
67
|
history.replaceState(undefined, '', `#${heading.id}`);
|
|
72
68
|
}}
|
|
@@ -105,15 +101,111 @@ function getHeaders(selector: string): HTMLHeadingElement[] {
|
|
|
105
101
|
return headers as HTMLHeadingElement[];
|
|
106
102
|
}
|
|
107
103
|
|
|
104
|
+
type MutationCallback = (mutations: MutationRecord[], observer: MutationObserver) => void;
|
|
105
|
+
|
|
106
|
+
function useMutationObserver(
|
|
107
|
+
targetRef: RefObject<Element>,
|
|
108
|
+
callback: MutationCallback,
|
|
109
|
+
options: Record<string, any>,
|
|
110
|
+
) {
|
|
111
|
+
const [observer, setObserver] = useState<MutationObserver | null>(null);
|
|
112
|
+
|
|
113
|
+
if (!onClient) return { observer };
|
|
114
|
+
|
|
115
|
+
// Create observer
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const obs = new MutationObserver(callback);
|
|
118
|
+
setObserver(obs);
|
|
119
|
+
}, [callback, setObserver]);
|
|
120
|
+
|
|
121
|
+
// Setup observer
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!observer || !targetRef.current) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
observer.observe(targetRef.current, options);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.error(e);
|
|
131
|
+
}
|
|
132
|
+
return () => {
|
|
133
|
+
if (observer) {
|
|
134
|
+
observer.disconnect();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}, [observer]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const useIntersectionObserver = (elements: Element[], options?: Record<string, any>) => {
|
|
141
|
+
const [observer, setObserver] = useState<IntersectionObserver | null>(null);
|
|
142
|
+
const [intersecting, setIntersecting] = useState<Element[]>([]);
|
|
143
|
+
|
|
144
|
+
if (!onClient) return { observer };
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const cb: IntersectionObserverCallback = (entries) => {
|
|
147
|
+
setIntersecting(entries.filter((e) => e.isIntersecting).map((e) => e.target));
|
|
148
|
+
};
|
|
149
|
+
const o = new IntersectionObserver(cb, options ?? {});
|
|
150
|
+
setObserver(o);
|
|
151
|
+
return () => o.disconnect();
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
// Changes to the DOM mean we need to update our intersection observer
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (!observer) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Observe all heading elements
|
|
160
|
+
const toWatch = elements;
|
|
161
|
+
toWatch.map((e) => observer.observe(e));
|
|
162
|
+
// Cleanup afterwards
|
|
163
|
+
return () => {
|
|
164
|
+
toWatch.map((e) => observer.unobserve(e));
|
|
165
|
+
};
|
|
166
|
+
}, [elements]);
|
|
167
|
+
|
|
168
|
+
return { observer, intersecting };
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Keep track of which headers are visible, and which header is active
|
|
173
|
+
*/
|
|
108
174
|
export function useHeaders(selector: string, maxdepth: number) {
|
|
109
175
|
if (!onClient) return { activeId: '', headings: [] };
|
|
110
|
-
|
|
176
|
+
// Keep track of main manually for now
|
|
177
|
+
const mainElementRef = useRef<HTMLElement | null>(null);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
mainElementRef.current = document.querySelector('main');
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
// Track changes to the DOM
|
|
183
|
+
const [elements, setElements] = useState<HTMLHeadingElement[]>([]);
|
|
184
|
+
const onMutation = useCallback(
|
|
185
|
+
throttle(
|
|
186
|
+
() => {
|
|
187
|
+
setElements(getHeaders(selector));
|
|
188
|
+
},
|
|
189
|
+
500,
|
|
190
|
+
{ trailing: false },
|
|
191
|
+
),
|
|
192
|
+
[selector],
|
|
193
|
+
);
|
|
194
|
+
useMutationObserver(mainElementRef, onMutation, {
|
|
195
|
+
attributes: true,
|
|
196
|
+
childList: true,
|
|
197
|
+
subtree: true,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Trigger initial update
|
|
201
|
+
useEffect(onMutation, []);
|
|
202
|
+
|
|
203
|
+
// Watch intersections with headings
|
|
204
|
+
const { intersecting } = useIntersectionObserver(elements);
|
|
111
205
|
const [activeId, setActiveId] = useState<string>();
|
|
112
|
-
const headingsSet = useRef<Set<HTMLHeadingElement>>(new Set());
|
|
113
206
|
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const highlighted = current.reduce(
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
const highlighted = intersecting!.reduce(
|
|
117
209
|
(a, b) => {
|
|
118
210
|
if (a) return a;
|
|
119
211
|
if (b.classList.contains('highlight')) return b.id;
|
|
@@ -121,80 +213,43 @@ export function useHeaders(selector: string, maxdepth: number) {
|
|
|
121
213
|
},
|
|
122
214
|
null as string | null,
|
|
123
215
|
);
|
|
124
|
-
const active = [...
|
|
216
|
+
const active = [...(intersecting as HTMLElement[])].sort(
|
|
217
|
+
(a, b) => a.offsetTop - b.offsetTop,
|
|
218
|
+
)[0];
|
|
125
219
|
if (highlighted || active) setActiveId(highlighted || active.id);
|
|
126
|
-
}, []);
|
|
127
|
-
|
|
128
|
-
const { observer } = useIntersectionObserver(highlight, onScreen.current);
|
|
129
|
-
const [elements, setElements] = useState<HTMLHeadingElement[]>([]);
|
|
220
|
+
}, [intersecting]);
|
|
130
221
|
|
|
131
|
-
const
|
|
222
|
+
const [headings, setHeadings] = useState<Heading[]>([]);
|
|
132
223
|
useEffect(() => {
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
224
|
+
let minLevel = 10;
|
|
225
|
+
const thisHeadings: Heading[] = elements
|
|
226
|
+
.map((element) => {
|
|
227
|
+
return {
|
|
228
|
+
element,
|
|
229
|
+
level: Number(element.tagName.slice(1)),
|
|
230
|
+
id: element.id,
|
|
231
|
+
text: element.querySelector('.heading-text'),
|
|
232
|
+
};
|
|
233
|
+
})
|
|
234
|
+
.filter((h) => !!h.text)
|
|
235
|
+
.map(({ element, level, text, id }) => {
|
|
236
|
+
const { innerText: title, innerHTML: titleHTML } = cloneHeadingElement(
|
|
237
|
+
text as HTMLSpanElement,
|
|
238
|
+
);
|
|
239
|
+
minLevel = Math.min(minLevel, level);
|
|
240
|
+
return { element, title, titleHTML, id, level };
|
|
241
|
+
})
|
|
242
|
+
.filter((heading) => {
|
|
243
|
+
heading.level = heading.level - minLevel + 1;
|
|
244
|
+
return heading.level < maxdepth + 1;
|
|
245
|
+
});
|
|
148
246
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.map((element) => {
|
|
152
|
-
return {
|
|
153
|
-
element,
|
|
154
|
-
level: Number(element.tagName.slice(1)),
|
|
155
|
-
id: element.id,
|
|
156
|
-
text: element.querySelector('.heading-text'),
|
|
157
|
-
};
|
|
158
|
-
})
|
|
159
|
-
.filter((h) => !!h.text)
|
|
160
|
-
.map(({ element, level, text, id }) => {
|
|
161
|
-
const { innerText: title, innerHTML: titleHTML } = cloneHeadingElement(
|
|
162
|
-
text as HTMLSpanElement,
|
|
163
|
-
);
|
|
164
|
-
minLevel = Math.min(minLevel, level);
|
|
165
|
-
return { element, title, titleHTML, id, level };
|
|
166
|
-
})
|
|
167
|
-
.filter((heading) => {
|
|
168
|
-
heading.level = heading.level - minLevel + 1;
|
|
169
|
-
return heading.level < maxdepth + 1;
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
headings.forEach(({ element: e }) => {
|
|
173
|
-
if (headingsSet.current.has(e)) return;
|
|
174
|
-
observer.current?.observe(e);
|
|
175
|
-
headingsSet.current.add(e);
|
|
176
|
-
});
|
|
247
|
+
setHeadings(thisHeadings);
|
|
248
|
+
}, [elements]);
|
|
177
249
|
|
|
178
|
-
return { activeId,
|
|
250
|
+
return { activeId, headings };
|
|
179
251
|
}
|
|
180
252
|
|
|
181
|
-
const useIntersectionObserver = (highlight: () => void, onScreen: Set<HTMLHeadingElement>) => {
|
|
182
|
-
const observer = useRef<IntersectionObserver | null>(null);
|
|
183
|
-
if (!onClient) return { observer };
|
|
184
|
-
useEffect(() => {
|
|
185
|
-
const callback: IntersectionObserverCallback = (entries) => {
|
|
186
|
-
entries.forEach((entry) => {
|
|
187
|
-
onScreen[entry.isIntersecting ? 'add' : 'delete'](entry.target as HTMLHeadingElement);
|
|
188
|
-
});
|
|
189
|
-
highlight();
|
|
190
|
-
};
|
|
191
|
-
const o = new IntersectionObserver(callback);
|
|
192
|
-
observer.current = o;
|
|
193
|
-
return () => o.disconnect();
|
|
194
|
-
}, [highlight, onScreen]);
|
|
195
|
-
return { observer };
|
|
196
|
-
};
|
|
197
|
-
|
|
198
253
|
export function useOutlineHeight<T extends HTMLElement = HTMLElement>(
|
|
199
254
|
existingContainer?: React.RefObject<T>,
|
|
200
255
|
) {
|
|
@@ -226,6 +281,71 @@ export function useOutlineHeight<T extends HTMLElement = HTMLElement>(
|
|
|
226
281
|
return { container, outline };
|
|
227
282
|
}
|
|
228
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Determine whether the margin outline should be occluded by margin elements
|
|
286
|
+
*/
|
|
287
|
+
function useMarginOccluder() {
|
|
288
|
+
const [occluded, setOccluded] = useState(false);
|
|
289
|
+
const [elements, setElements] = useState<Element[]>([]);
|
|
290
|
+
|
|
291
|
+
// Keep track of main manually for now
|
|
292
|
+
const mainElementRef = useRef<HTMLElement | null>(null);
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
mainElementRef.current = document.querySelector('main');
|
|
295
|
+
}, []);
|
|
296
|
+
|
|
297
|
+
// Update list of margin elements
|
|
298
|
+
const onMutation = useCallback(
|
|
299
|
+
throttle(
|
|
300
|
+
() => {
|
|
301
|
+
if (!mainElementRef.current) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Watch margin elements, or their direct descendents (as some margin elements have height set to zero)
|
|
305
|
+
const classes = [
|
|
306
|
+
'col-margin-right',
|
|
307
|
+
'col-margin-right-inset',
|
|
308
|
+
'col-gutter-outset-right',
|
|
309
|
+
'col-screen-right',
|
|
310
|
+
'col-screen-inset-right',
|
|
311
|
+
'col-page-right',
|
|
312
|
+
'col-page-inset-right',
|
|
313
|
+
'col-body-outset-right',
|
|
314
|
+
'col-gutter-page-right',
|
|
315
|
+
// 'col-screen', // This is on everything!
|
|
316
|
+
'col-page',
|
|
317
|
+
'col-page-inset',
|
|
318
|
+
'col-body-outset',
|
|
319
|
+
];
|
|
320
|
+
const selector = classes
|
|
321
|
+
.map((cls) => [`.${cls}`, `.${cls} > *`])
|
|
322
|
+
.flat()
|
|
323
|
+
.join(', ');
|
|
324
|
+
const marginElements = mainElementRef.current.querySelectorAll(selector);
|
|
325
|
+
setElements(Array.from(marginElements));
|
|
326
|
+
},
|
|
327
|
+
500,
|
|
328
|
+
{ trailing: false },
|
|
329
|
+
),
|
|
330
|
+
[],
|
|
331
|
+
);
|
|
332
|
+
useMutationObserver(mainElementRef, onMutation, {
|
|
333
|
+
attributes: true,
|
|
334
|
+
childList: true,
|
|
335
|
+
subtree: true,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Trigger initial update
|
|
339
|
+
useEffect(onMutation, []);
|
|
340
|
+
// Keep tabs of margin elements on screen
|
|
341
|
+
const { intersecting } = useIntersectionObserver(elements, { rootMargin: '0px 0px -33% 0px' });
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
setOccluded(intersecting!.length > 0);
|
|
344
|
+
}, [intersecting]);
|
|
345
|
+
|
|
346
|
+
return { occluded };
|
|
347
|
+
}
|
|
348
|
+
|
|
229
349
|
export const DocumentOutline = ({
|
|
230
350
|
outlineRef,
|
|
231
351
|
top = 0,
|
|
@@ -233,6 +353,7 @@ export const DocumentOutline = ({
|
|
|
233
353
|
selector = SELECTOR,
|
|
234
354
|
children,
|
|
235
355
|
maxdepth = 4,
|
|
356
|
+
isMargin,
|
|
236
357
|
}: {
|
|
237
358
|
outlineRef?: React.RefObject<HTMLElement>;
|
|
238
359
|
top?: number;
|
|
@@ -241,31 +362,63 @@ export const DocumentOutline = ({
|
|
|
241
362
|
selector?: string;
|
|
242
363
|
children?: React.ReactNode;
|
|
243
364
|
maxdepth?: number;
|
|
365
|
+
isMargin: boolean;
|
|
244
366
|
}) => {
|
|
245
|
-
const { activeId, headings
|
|
367
|
+
const { activeId, headings } = useHeaders(selector, maxdepth);
|
|
368
|
+
const [open, setOpen] = useState(false);
|
|
369
|
+
|
|
370
|
+
// Keep track of changing occlusion
|
|
371
|
+
const { occluded } = useMarginOccluder();
|
|
372
|
+
|
|
373
|
+
// Handle transition between margin and non-margin
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
setOpen(true);
|
|
376
|
+
}, [isMargin]);
|
|
377
|
+
|
|
378
|
+
// Handle occlusion when outline is in margin
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
if (isMargin) {
|
|
381
|
+
setOpen(!occluded);
|
|
382
|
+
}
|
|
383
|
+
}, [occluded, isMargin]);
|
|
384
|
+
|
|
246
385
|
if (headings.length <= 1 || !onClient) {
|
|
247
386
|
return <nav suppressHydrationWarning>{children}</nav>;
|
|
248
387
|
}
|
|
388
|
+
|
|
249
389
|
return (
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
390
|
+
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
|
391
|
+
<nav
|
|
392
|
+
ref={outlineRef}
|
|
393
|
+
aria-label="Document Outline"
|
|
394
|
+
className={classNames(
|
|
395
|
+
'not-prose overflow-y-auto',
|
|
396
|
+
'transition-opacity duration-700', // Animation on load
|
|
397
|
+
className,
|
|
398
|
+
)}
|
|
399
|
+
style={{
|
|
400
|
+
top: top,
|
|
401
|
+
maxHeight: `calc(100vh - ${top + 20}px)`,
|
|
402
|
+
}}
|
|
403
|
+
>
|
|
404
|
+
<div className="flex flex-row gap-2 mb-4 text-sm leading-6 uppercase rounded-lg text-slate-900 dark:text-slate-100">
|
|
405
|
+
In this article
|
|
406
|
+
<Collapsible.Trigger asChild>
|
|
407
|
+
<button className="self-center flex-none rounded-md group hover:bg-slate-300/30 focus:outline outline-blue-200 outline-2">
|
|
408
|
+
<ChevronRightIcon
|
|
409
|
+
className="transition-transform duration-300 group-data-[state=open]:rotate-90 text-text-slate-700 dark:text-slate-100"
|
|
410
|
+
height="1.5rem"
|
|
411
|
+
width="1.5rem"
|
|
412
|
+
/>
|
|
413
|
+
</button>
|
|
414
|
+
</Collapsible.Trigger>
|
|
415
|
+
</div>
|
|
416
|
+
<Collapsible.Content className="CollapsibleContent">
|
|
417
|
+
<Headings headings={headings} activeId={activeId} />
|
|
418
|
+
{children}
|
|
419
|
+
</Collapsible.Content>
|
|
420
|
+
</nav>
|
|
421
|
+
</Collapsible.Root>
|
|
269
422
|
);
|
|
270
423
|
};
|
|
271
424
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useSiteManifest,
|
|
7
7
|
useGridSystemProvider,
|
|
8
8
|
useThemeTop,
|
|
9
|
+
useIsWide,
|
|
9
10
|
} from '@myst-theme/providers';
|
|
10
11
|
import type { Heading } from '@myst-theme/common';
|
|
11
12
|
import { Toc } from './TableOfContentsItems.js';
|
|
@@ -95,10 +96,15 @@ export function useSidebarHeight<T extends HTMLElement = HTMLElement>(top = 0, i
|
|
|
95
96
|
const container = useRef<T>(null);
|
|
96
97
|
const toc = useRef<HTMLDivElement>(null);
|
|
97
98
|
const transitionState = useNavigation().state;
|
|
99
|
+
const wide = useIsWide();
|
|
98
100
|
const setHeight = () => {
|
|
99
101
|
if (!container.current || !toc.current) return;
|
|
100
102
|
const height = container.current.offsetHeight - window.scrollY;
|
|
101
103
|
const div = toc.current.firstChild as HTMLDivElement;
|
|
104
|
+
if (div)
|
|
105
|
+
div.style.height = wide
|
|
106
|
+
? `min(calc(100vh - ${top}px), ${height + inset}px)`
|
|
107
|
+
: `calc(100vh - ${top}px)`;
|
|
102
108
|
if (div) div.style.height = `min(calc(100vh - ${top}px), ${height + inset}px)`;
|
|
103
109
|
const nav = toc.current.querySelector('nav');
|
|
104
110
|
if (nav) nav.style.opacity = height > 150 ? '1' : '0';
|
|
@@ -111,7 +117,7 @@ export function useSidebarHeight<T extends HTMLElement = HTMLElement>(top = 0, i
|
|
|
111
117
|
return () => {
|
|
112
118
|
window.removeEventListener('scroll', handleScroll);
|
|
113
119
|
};
|
|
114
|
-
}, [container, toc, transitionState]);
|
|
120
|
+
}, [container, toc, transitionState, wide]);
|
|
115
121
|
return { container, toc };
|
|
116
122
|
}
|
|
117
123
|
|