@myst-theme/site 1.1.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myst-theme/site",
3
- "version": "1.1.0",
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.0",
29
- "@myst-theme/diagrams": "^1.1.0",
30
- "@myst-theme/frontmatter": "^1.1.0",
31
- "@myst-theme/providers": "^1.1.0",
32
- "@myst-theme/search": "^1.1.0",
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.0",
40
+ "myst-demo": "^1.1.2",
41
41
  "myst-spec-ext": "^1.8.1",
42
- "myst-to-react": "^1.1.0",
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",
@@ -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';
@@ -18,6 +19,21 @@ const SELECTOR = [1, 2, 3, 4].map((n) => `main h${n}`).join(', ');
18
19
 
19
20
  const onClient = typeof document !== 'undefined';
20
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
+
21
37
  export type Heading = {
22
38
  element: HTMLHeadingElement;
23
39
  id: string;
@@ -148,13 +164,24 @@ const useIntersectionObserver = (elements: Element[], options?: Record<string, a
148
164
 
149
165
  if (!onClient) return { observer };
150
166
  useEffect(() => {
167
+ // We want a list of all header elements on screen to loop through, but:
168
+ // IntersectionObserver returns elements that have *changed state*, not the full list of elements on screen.
169
+ // So we maintain a set of on-screen elements and add/remove as new intersection events happen.
151
170
  const cb: IntersectionObserverCallback = (entries) => {
152
- setIntersecting(entries.filter((e) => e.isIntersecting).map((e) => e.target));
171
+ setIntersecting((prev) => {
172
+ const next = new Set(prev);
173
+ // Add or remove from our set based on intersection state
174
+ entries.forEach((e) => {
175
+ if (e.isIntersecting) next.add(e.target);
176
+ else next.delete(e.target);
177
+ });
178
+ return arrayIfChanged(prev, Array.from(next));
179
+ });
153
180
  };
154
181
  const o = new IntersectionObserver(cb, options ?? {});
155
182
  setObserver(o);
156
183
  return () => o.disconnect();
157
- }, []);
184
+ }, [options]);
158
185
 
159
186
  // Changes to the DOM mean we need to update our intersection observer
160
187
  useEffect(() => {
@@ -163,12 +190,18 @@ const useIntersectionObserver = (elements: Element[], options?: Record<string, a
163
190
  }
164
191
  // Observe all heading elements
165
192
  const toWatch = elements;
166
- toWatch.map((e) => observer.observe(e));
193
+ toWatch.forEach((e) => {
194
+ observer.observe(e);
195
+ });
167
196
  // Cleanup afterwards
168
197
  return () => {
169
- toWatch.map((e) => observer.unobserve(e));
198
+ toWatch.forEach((e) => {
199
+ observer.unobserve(e);
200
+ });
201
+ // Ensure we don't keep stale references when elements are removed/unobserved.
202
+ setIntersecting((prev) => prev.filter((e) => !toWatch.includes(e)));
170
203
  };
171
- }, [elements]);
204
+ }, [elements, observer]);
172
205
 
173
206
  return { observer, intersecting };
174
207
  };
@@ -177,6 +210,7 @@ const useIntersectionObserver = (elements: Element[], options?: Record<string, a
177
210
  * Keep track of which headers are visible, and which header is active
178
211
  */
179
212
  export function useHeaders(selector: string, maxdepth: number) {
213
+ const topOffset = useThemeTop();
180
214
  if (!onClient) return { activeId: '', headings: [] };
181
215
  // Keep track of main manually for now
182
216
  const mainElementRef = useRef<HTMLElement | null>(null);
@@ -189,10 +223,11 @@ export function useHeaders(selector: string, maxdepth: number) {
189
223
  const onMutation = useCallback(
190
224
  throttle(
191
225
  () => {
192
- setElements(getHeaders(selector));
226
+ setElements((prev) => arrayIfChanged(prev, getHeaders(selector)));
193
227
  },
194
228
  500,
195
- { trailing: false },
229
+ // Trailing updates help ensure we eventually process the last DOM mutation burst.
230
+ { trailing: true },
196
231
  ),
197
232
  [selector],
198
233
  );
@@ -210,6 +245,9 @@ export function useHeaders(selector: string, maxdepth: number) {
210
245
  const [activeId, setActiveId] = useState<string>();
211
246
 
212
247
  useEffect(() => {
248
+ // Use the theme's top offset (navbar height) + a bit of padding for filtering active headings to avoid over-shooting the header.
249
+ const OFFSET_PX = topOffset - 10;
250
+ // Prefer a heading marked as highlighted (e.g. focus/anchor) if one is currently intersecting.
213
251
  const highlighted = intersecting!.reduce(
214
252
  (a, b) => {
215
253
  if (a) return a;
@@ -218,11 +256,28 @@ export function useHeaders(selector: string, maxdepth: number) {
218
256
  },
219
257
  null as string | null,
220
258
  );
221
- const active = [...(intersecting as HTMLElement[])].sort(
222
- (a, b) => a.offsetTop - b.offsetTop,
223
- )[0];
224
- if (highlighted || active) setActiveId(highlighted || active.id);
225
- }, [intersecting]);
259
+ const intersectingElements = intersecting as HTMLElement[];
260
+ // Choose the heading closest to the navbar offset line within a viewport window under it.
261
+ // Using a window avoids a case where the active header is off screen the next header is way at the bottom of the screen.
262
+ let bestInActiveHeaderWindow: { el: HTMLElement; distance: number } | undefined;
263
+ const ACTIVE_WINDOW_PX = window.innerHeight * 0.33;
264
+ for (const el of intersectingElements) {
265
+ const distance = el.getBoundingClientRect().top - OFFSET_PX;
266
+ if (
267
+ // Only keep things under the navbar line
268
+ distance >= 0 &&
269
+ // Only keep things over the active window size
270
+ distance <= ACTIVE_WINDOW_PX &&
271
+ // Now if it's closer to the navbar line than the active element, update active
272
+ (!bestInActiveHeaderWindow || distance < bestInActiveHeaderWindow.distance)
273
+ ) {
274
+ bestInActiveHeaderWindow = { el, distance };
275
+ }
276
+ }
277
+ // If nothing is below the navbar line, keep the current active heading.
278
+ const active = bestInActiveHeaderWindow?.el;
279
+ if (highlighted || active) setActiveId(highlighted || active?.id);
280
+ }, [intersecting, topOffset]);
226
281
 
227
282
  const [headings, setHeadings] = useState<Heading[]>([]);
228
283
  useEffect(() => {
@@ -292,6 +347,8 @@ export function useOutlineHeight<T extends HTMLElement = HTMLElement>(
292
347
  function useMarginOccluder() {
293
348
  const [occluded, setOccluded] = useState(false);
294
349
  const [elements, setElements] = useState<Element[]>([]);
350
+ // Memoize options so `useIntersectionObserver(..., options)` doesn't recreate the observer each render.
351
+ const intersectionOptions = useMemo(() => ({ rootMargin: '0px 0px -33% 0px' }), []);
295
352
 
296
353
  // Keep track of main manually for now
297
354
  const mainElementRef = useRef<HTMLElement | null>(null);
@@ -327,10 +384,11 @@ function useMarginOccluder() {
327
384
  .flat()
328
385
  .join(', ');
329
386
  const marginElements = mainElementRef.current.querySelectorAll(selector);
330
- setElements(Array.from(marginElements));
387
+ setElements((prev) => arrayIfChanged(prev, Array.from(marginElements)));
331
388
  },
332
389
  500,
333
- { trailing: false },
390
+ // Trailing updates help ensure we eventually process the last DOM mutation burst.
391
+ { trailing: true },
334
392
  ),
335
393
  [],
336
394
  );
@@ -343,7 +401,7 @@ function useMarginOccluder() {
343
401
  // Trigger initial update
344
402
  useEffect(onMutation, []);
345
403
  // Keep tabs of margin elements on screen
346
- const { intersecting } = useIntersectionObserver(elements, { rootMargin: '0px 0px -33% 0px' });
404
+ const { intersecting } = useIntersectionObserver(elements, intersectionOptions);
347
405
  useEffect(() => {
348
406
  setOccluded(intersecting!.length > 0);
349
407
  }, [intersecting]);
@@ -401,6 +459,7 @@ export const DocumentOutline = ({
401
459
  className={classNames(
402
460
  'myst-outline not-prose overflow-y-auto',
403
461
  'transition-opacity duration-700', // Animation on load
462
+ '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
463
  className,
405
464
  )}
406
465
  style={{
@@ -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={logoText || name}
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={logoText || name}
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-screen h-[2px] bg-blue-500 absolute left-0 bottom-0 transition-transform',
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
- className="myst-navigation-overlay fixed inset-0 z-30 bg-black opacity-50"
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, totalTop]);
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
- 'grid-gap xl:w-screen xl:pointer-events-none overflow-auto max-xl:min-w-[300px]',
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
- { hidden: !open, 'z-30': open, 'z-10': !open },
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="myst-primary-sidebar-topnav overflow-y-hidden transition-opacity ml-3 xl:ml-0 mr-3 max-w-[350px] lg:hidden"
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="myst-primary-sidebar-toc flex-grow overflow-y-hidden transition-opacity ml-3 xl:ml-0 mr-3 max-w-[350px]"
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="myst-primary-sidebar-footer flex-none py-6 transition-all duration-700 translate-y-6 opacity-0"
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}
@@ -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
  {
@@ -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-screen 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"
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-screen top-0 z-30 h-[60px]">
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>