@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myst-theme/site",
3
- "version": "1.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.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",
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.1",
40
+ "myst-demo": "^1.1.2",
41
41
  "myst-spec-ext": "^1.8.1",
42
- "myst-to-react": "^1.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={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}
@@ -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>