@myst-theme/site 1.1.1 → 1.1.2
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 +8 -8
- package/src/components/DocumentOutline.tsx +18 -3
- package/src/components/Navigation/HomeLink.tsx +5 -2
- package/src/components/Navigation/Loading.tsx +1 -1
- package/src/components/Navigation/Navigation.tsx +6 -1
- package/src/components/Navigation/PrimarySidebar.tsx +44 -15
- package/src/components/Navigation/Search.tsx +1 -1
- package/src/components/Navigation/TopNav.tsx +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myst-theme/site",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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.1.
|
|
29
|
-
"@myst-theme/diagrams": "^1.1.
|
|
30
|
-
"@myst-theme/frontmatter": "^1.1.
|
|
31
|
-
"@myst-theme/providers": "^1.1.
|
|
32
|
-
"@myst-theme/search": "^1.1.
|
|
28
|
+
"@myst-theme/common": "^1.1.2",
|
|
29
|
+
"@myst-theme/diagrams": "^1.1.2",
|
|
30
|
+
"@myst-theme/frontmatter": "^1.1.2",
|
|
31
|
+
"@myst-theme/providers": "^1.1.2",
|
|
32
|
+
"@myst-theme/search": "^1.1.2",
|
|
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.1.
|
|
40
|
+
"myst-demo": "^1.1.2",
|
|
41
41
|
"myst-spec-ext": "^1.8.1",
|
|
42
|
-
"myst-to-react": "^1.1.
|
|
42
|
+
"myst-to-react": "^1.1.2",
|
|
43
43
|
"nbtx": "^0.2.3",
|
|
44
44
|
"node-cache": "^5.1.2",
|
|
45
45
|
"node-fetch": "^2.6.11",
|
|
@@ -19,6 +19,21 @@ const SELECTOR = [1, 2, 3, 4].map((n) => `main h${n}`).join(', ');
|
|
|
19
19
|
|
|
20
20
|
const onClient = typeof document !== 'undefined';
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Returns `next` if it differs from `prev`, otherwise returns `prev` unchanged.
|
|
24
|
+
* This prevents unnecessary React state updates (and re-renders) when the
|
|
25
|
+
* array contents haven't actually changed - important for breaking feedback
|
|
26
|
+
* loops with IntersectionObserver and MutationObserver.
|
|
27
|
+
*/
|
|
28
|
+
function arrayIfChanged<T>(prev: T[], next: T[]): T[] {
|
|
29
|
+
if (prev === next) return prev;
|
|
30
|
+
if (prev.length !== next.length) return next;
|
|
31
|
+
for (let i = 0; i < prev.length; i++) {
|
|
32
|
+
if (prev[i] !== next[i]) return next;
|
|
33
|
+
}
|
|
34
|
+
return prev;
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
export type Heading = {
|
|
23
38
|
element: HTMLHeadingElement;
|
|
24
39
|
id: string;
|
|
@@ -160,7 +175,7 @@ const useIntersectionObserver = (elements: Element[], options?: Record<string, a
|
|
|
160
175
|
if (e.isIntersecting) next.add(e.target);
|
|
161
176
|
else next.delete(e.target);
|
|
162
177
|
});
|
|
163
|
-
return Array.from(next);
|
|
178
|
+
return arrayIfChanged(prev, Array.from(next));
|
|
164
179
|
});
|
|
165
180
|
};
|
|
166
181
|
const o = new IntersectionObserver(cb, options ?? {});
|
|
@@ -208,7 +223,7 @@ export function useHeaders(selector: string, maxdepth: number) {
|
|
|
208
223
|
const onMutation = useCallback(
|
|
209
224
|
throttle(
|
|
210
225
|
() => {
|
|
211
|
-
setElements(getHeaders(selector));
|
|
226
|
+
setElements((prev) => arrayIfChanged(prev, getHeaders(selector)));
|
|
212
227
|
},
|
|
213
228
|
500,
|
|
214
229
|
// Trailing updates help ensure we eventually process the last DOM mutation burst.
|
|
@@ -369,7 +384,7 @@ function useMarginOccluder() {
|
|
|
369
384
|
.flat()
|
|
370
385
|
.join(', ');
|
|
371
386
|
const marginElements = mainElementRef.current.querySelectorAll(selector);
|
|
372
|
-
setElements(Array.from(marginElements));
|
|
387
|
+
setElements((prev) => arrayIfChanged(prev, Array.from(marginElements)));
|
|
373
388
|
},
|
|
374
389
|
500,
|
|
375
390
|
// Trailing updates help ensure we eventually process the last DOM mutation burst.
|
|
@@ -5,18 +5,21 @@ export function HomeLink({
|
|
|
5
5
|
logo,
|
|
6
6
|
logoDark,
|
|
7
7
|
logoText,
|
|
8
|
+
logoAlt,
|
|
8
9
|
name,
|
|
9
10
|
url,
|
|
10
11
|
}: {
|
|
11
12
|
logo?: string;
|
|
12
13
|
logoDark?: string;
|
|
13
14
|
logoText?: string;
|
|
15
|
+
logoAlt?: string;
|
|
14
16
|
name?: string;
|
|
15
17
|
url?: string;
|
|
16
18
|
}) {
|
|
17
19
|
const Link = useLinkProvider();
|
|
18
20
|
const baseurl = useBaseurl();
|
|
19
21
|
const nothingSet = !logo && !logoText;
|
|
22
|
+
const altText = logoAlt ?? logoText ?? name;
|
|
20
23
|
return (
|
|
21
24
|
<Link
|
|
22
25
|
className="myst-home-link flex items-center ml-3 dark:text-white w-fit md:ml-5 xl:ml-7"
|
|
@@ -32,14 +35,14 @@ export function HomeLink({
|
|
|
32
35
|
<img
|
|
33
36
|
src={logo}
|
|
34
37
|
className={classNames('h-9', { 'dark:hidden': !!logoDark })}
|
|
35
|
-
alt={
|
|
38
|
+
alt={altText}
|
|
36
39
|
height="2.25rem"
|
|
37
40
|
></img>
|
|
38
41
|
{logoDark && (
|
|
39
42
|
<img
|
|
40
43
|
src={logoDark}
|
|
41
44
|
className="hidden h-9 dark:block"
|
|
42
|
-
alt={
|
|
45
|
+
alt={altText}
|
|
43
46
|
height="2.25rem"
|
|
44
47
|
></img>
|
|
45
48
|
)}
|
|
@@ -48,7 +48,7 @@ export function LoadingBar() {
|
|
|
48
48
|
return (
|
|
49
49
|
<div
|
|
50
50
|
className={classNames(
|
|
51
|
-
'myst-loading-bar w-
|
|
51
|
+
'myst-loading-bar w-full h-[2px] bg-blue-500 absolute left-0 bottom-0 transition-transform',
|
|
52
52
|
{
|
|
53
53
|
'myst-loading-bar-progress animate-load scale-x-40': isLoading,
|
|
54
54
|
'scale-x-100': !isLoading,
|
|
@@ -91,13 +91,18 @@ export const ConfigurablePrimaryNavigation = ({
|
|
|
91
91
|
return (
|
|
92
92
|
<>
|
|
93
93
|
{open && !mobileOnly && headings && (
|
|
94
|
+
// Darkened backdrop behind the open sidebar on mobile.
|
|
94
95
|
<div
|
|
95
|
-
|
|
96
|
+
// It follows the same top-offset rules as the sidebar: header offset on desktop,
|
|
97
|
+
// full-screen from top on mobile.
|
|
98
|
+
className="myst-navigation-overlay fixed inset-0 max-xl:z-40 xl:z-30 bg-black opacity-50 max-xl:!mt-0"
|
|
96
99
|
style={{ marginTop: top }}
|
|
100
|
+
// Clicking the backdrop is the primary escape path for closing the sidebar.
|
|
97
101
|
onClick={() => setOpen(false)}
|
|
98
102
|
></div>
|
|
99
103
|
)}
|
|
100
104
|
<PrimarySidebar
|
|
105
|
+
// The actual sidebar panel is here; the overlay backdrop is above.
|
|
101
106
|
sidebarRef={sidebarRef}
|
|
102
107
|
nav={nav}
|
|
103
108
|
headings={headings}
|
|
@@ -99,24 +99,38 @@ export function SidebarNav({ nav }: { nav?: SiteManifest['nav'] }) {
|
|
|
99
99
|
);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Manages the sidebar's position and height on scroll.
|
|
104
|
+
*
|
|
105
|
+
* On wide screen, the sidebar is position:fixed and needs JS to
|
|
106
|
+
* grow/shrink/move based on other elements as we scroll
|
|
107
|
+
*
|
|
108
|
+
* On non-wide, the sidebar is a full-screen overlay — CSS handles everything.
|
|
109
|
+
*/
|
|
102
110
|
export function useSidebarHeight<T extends HTMLElement = HTMLElement>(top = 0, inset = 0) {
|
|
103
111
|
const container = useRef<T>(null);
|
|
104
112
|
const toc = useRef<HTMLDivElement>(null);
|
|
105
113
|
const transitionState = useNavigation().state;
|
|
106
114
|
const wide = useIsWide();
|
|
107
115
|
const { bannerState } = useBannerState();
|
|
108
|
-
const totalTop = top + bannerState.height;
|
|
109
|
-
|
|
110
116
|
const setHeight = () => {
|
|
111
117
|
if (!container.current || !toc.current) return;
|
|
112
|
-
const height = container.current.offsetHeight - window.scrollY;
|
|
113
118
|
const div = toc.current.firstChild as HTMLDivElement;
|
|
114
|
-
if (div)
|
|
115
|
-
div.style.height = wide
|
|
116
|
-
? `min(calc(100vh - ${totalTop}px), ${height + inset}px)`
|
|
117
|
-
: `calc(100vh - ${totalTop}px)`;
|
|
118
|
-
if (div) div.style.height = `min(calc(100vh - ${totalTop}px), ${height + inset}px)`;
|
|
119
119
|
const nav = toc.current.querySelector('nav');
|
|
120
|
+
if (!wide) {
|
|
121
|
+
// On mobile, clear any stale inline styles so CSS can handle sizing.
|
|
122
|
+
if (div) div.style.height = '';
|
|
123
|
+
if (nav) nav.style.opacity = '';
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// As the banner scrolls out of view, slide the sidebar up to stay
|
|
127
|
+
// just below the sticky TopNav.
|
|
128
|
+
const effectiveBannerHeight = Math.max(0, bannerState.height - window.scrollY);
|
|
129
|
+
const effectiveTop = top + effectiveBannerHeight;
|
|
130
|
+
toc.current.style.top = `${effectiveTop}px`;
|
|
131
|
+
// Sidebar height: fill the viewport but don't extend past the article.
|
|
132
|
+
const height = Math.max(0, container.current.offsetHeight - window.scrollY);
|
|
133
|
+
if (div) div.style.height = `min(calc(100vh - ${effectiveTop}px), ${height + inset}px)`;
|
|
120
134
|
if (nav) nav.style.opacity = height > 150 ? '1' : '0';
|
|
121
135
|
};
|
|
122
136
|
useEffect(() => {
|
|
@@ -127,7 +141,7 @@ export function useSidebarHeight<T extends HTMLElement = HTMLElement>(top = 0, i
|
|
|
127
141
|
return () => {
|
|
128
142
|
window.removeEventListener('scroll', handleScroll);
|
|
129
143
|
};
|
|
130
|
-
}, [container, toc, transitionState, wide,
|
|
144
|
+
}, [container, toc, transitionState, wide, top, bannerState.height]);
|
|
131
145
|
return { container, toc };
|
|
132
146
|
}
|
|
133
147
|
|
|
@@ -152,6 +166,8 @@ export const PrimarySidebar = ({
|
|
|
152
166
|
const footerRef = useRef<HTMLDivElement>(null);
|
|
153
167
|
const [open] = useNavOpen();
|
|
154
168
|
const config = useSiteManifest();
|
|
169
|
+
// Applies layout and whitespacing to a few sidebar sections
|
|
170
|
+
const sidebarSectionInsetClass = 'ml-3 xl:ml-0 mr-3 max-w-[350px]';
|
|
155
171
|
|
|
156
172
|
useEffect(() => {
|
|
157
173
|
setTimeout(() => {
|
|
@@ -169,9 +185,13 @@ export const PrimarySidebar = ({
|
|
|
169
185
|
'myst-primary-sidebar',
|
|
170
186
|
'fixed',
|
|
171
187
|
`xl:${grid}`, // for example, xl:article-grid
|
|
172
|
-
|
|
188
|
+
// Base sidebar layout and scrolling behavior
|
|
189
|
+
'grid-gap xl:w-full xl:pointer-events-none overflow-auto',
|
|
190
|
+
// Mobile modal behavior: cap width, pin to top, and fill viewport height
|
|
191
|
+
'max-xl:w-[75vw] max-xl:max-w-[350px] max-xl:!top-0 max-xl:h-screen',
|
|
173
192
|
{ 'lg:hidden': nav && hide_toc },
|
|
174
|
-
|
|
193
|
+
// raise sidebar above content when open
|
|
194
|
+
{ hidden: !open, 'max-xl:z-40 xl:z-30': open, 'z-10': !open },
|
|
175
195
|
)}
|
|
176
196
|
style={{ top: top + bannerState.height }}
|
|
177
197
|
>
|
|
@@ -180,7 +200,7 @@ export const PrimarySidebar = ({
|
|
|
180
200
|
'myst-primary-sidebar-pointer',
|
|
181
201
|
'pointer-events-auto',
|
|
182
202
|
'xl:col-margin-left flex-col',
|
|
183
|
-
'overflow-hidden',
|
|
203
|
+
'overflow-hidden max-xl:h-full',
|
|
184
204
|
{
|
|
185
205
|
flex: open,
|
|
186
206
|
'bg-white dark:bg-stone-900': open, // just apply when open, so that theme can transition
|
|
@@ -194,7 +214,10 @@ export const PrimarySidebar = ({
|
|
|
194
214
|
{nav && (
|
|
195
215
|
<nav
|
|
196
216
|
aria-label="Navigation"
|
|
197
|
-
className=
|
|
217
|
+
className={classNames(
|
|
218
|
+
'myst-primary-sidebar-topnav overflow-y-hidden transition-opacity lg:hidden',
|
|
219
|
+
sidebarSectionInsetClass,
|
|
220
|
+
)}
|
|
198
221
|
>
|
|
199
222
|
<SidebarNav nav={nav} />
|
|
200
223
|
</nav>
|
|
@@ -203,7 +226,10 @@ export const PrimarySidebar = ({
|
|
|
203
226
|
{headings && (
|
|
204
227
|
<nav
|
|
205
228
|
aria-label="Table of Contents"
|
|
206
|
-
className=
|
|
229
|
+
className={classNames(
|
|
230
|
+
'myst-primary-sidebar-toc flex-grow overflow-y-hidden transition-opacity',
|
|
231
|
+
sidebarSectionInsetClass,
|
|
232
|
+
)}
|
|
207
233
|
>
|
|
208
234
|
<Toc headings={headings} />
|
|
209
235
|
</nav>
|
|
@@ -211,7 +237,10 @@ export const PrimarySidebar = ({
|
|
|
211
237
|
</div>
|
|
212
238
|
{footer && (
|
|
213
239
|
<div
|
|
214
|
-
className=
|
|
240
|
+
className={classNames(
|
|
241
|
+
'myst-primary-sidebar-footer flex-none py-6 transition-all duration-700 translate-y-6 opacity-0',
|
|
242
|
+
sidebarSectionInsetClass,
|
|
243
|
+
)}
|
|
215
244
|
ref={footerRef}
|
|
216
245
|
>
|
|
217
246
|
{footer}
|
|
@@ -632,7 +632,7 @@ export function Search({ debounceTime = 500, charLimit = 64 }: SearchProps) {
|
|
|
632
632
|
<Dialog.Portal>
|
|
633
633
|
<Dialog.Overlay className="fixed inset-0 bg-[#656c85cc] z-[1000]" />
|
|
634
634
|
<Dialog.Content
|
|
635
|
-
className="myst-search-dialog fixed flex flex-col top-0 bg-white dark:bg-stone-900 z-[1001] h-screen w-
|
|
635
|
+
className="myst-search-dialog fixed flex flex-col top-0 bg-white dark:bg-stone-900 z-[1001] h-screen w-full sm:left-1/2 sm:-translate-x-1/2 sm:w-[90vw] sm:max-w-screen-sm sm:h-auto sm:max-h-[var(--content-max-height)] sm:top-[var(--content-top)] sm:rounded-md p-4 text-gray-900 dark:text-white"
|
|
636
636
|
// Store state as CSS variables so that we can set the style with tailwind variants
|
|
637
637
|
style={
|
|
638
638
|
{
|
|
@@ -117,9 +117,9 @@ export function TopNav({ hideToc, hideSearch }: { hideToc?: boolean; hideSearch?
|
|
|
117
117
|
const [open, setOpen] = useNavOpen();
|
|
118
118
|
const config = useSiteManifest();
|
|
119
119
|
const { title, nav, actions } = config ?? {};
|
|
120
|
-
const { logo, logo_dark, logo_text, logo_url } = config?.options ?? {};
|
|
120
|
+
const { logo, logo_dark, logo_text, logo_url, logo_alt } = config?.options ?? {};
|
|
121
121
|
return (
|
|
122
|
-
<div className="myst-top-nav bg-white/80 backdrop-blur dark:bg-stone-900/80 shadow dark:shadow-stone-700 p-3 md:px-8 sticky w-
|
|
122
|
+
<div className="myst-top-nav bg-white/80 backdrop-blur dark:bg-stone-900/80 shadow dark:shadow-stone-700 p-3 md:px-8 sticky w-full top-0 z-30 h-[60px]">
|
|
123
123
|
<nav className="myst-top-nav-bar flex items-center justify-between flex-nowrap max-w-[1440px] mx-auto">
|
|
124
124
|
<div className="flex flex-row xl:min-w-[19.5rem] mr-2 sm:mr-7 justify-start items-center shrink-0">
|
|
125
125
|
{
|
|
@@ -145,6 +145,7 @@ export function TopNav({ hideToc, hideSearch }: { hideToc?: boolean; hideSearch?
|
|
|
145
145
|
logo={logo}
|
|
146
146
|
logoDark={logo_dark}
|
|
147
147
|
logoText={logo_text}
|
|
148
|
+
logoAlt={logo_alt}
|
|
148
149
|
url={logo_url}
|
|
149
150
|
/>
|
|
150
151
|
</div>
|