@@ -347,6 +347,11 @@ function scheduleSwupRecovery() {
347347}
348348
349349function setClickOutsideToClose(panel: string, ignores: string[]) {
350+ const registry = (window as unknown as { __clickOutsideRegistry?: Set<string> }).__clickOutsideRegistry
351+ ?? ((window as unknown as { __clickOutsideRegistry: Set<string> }).__clickOutsideRegistry = new Set<string>());
352+ if (registry.has(panel)) return;
353+ registry.add(panel);
354+
350355 document.addEventListener("click", event => {
351356 const panelDom = document.getElementById(panel);
352357 if (!panelDom) {
@@ -377,6 +382,8 @@ function loadHue() {
377382}
378383
379384let bodyScrollbarInitialized = false;
385+ let bodyScrollbarScheduled = false;
386+ const enableBodyScrollbar = false;
380387let katexObserver: IntersectionObserver | undefined;
381388const katexObserverOptions = {
382389 root: null,
@@ -419,28 +426,69 @@ function ensureKatexObserver() {
419426 }, katexObserverOptions);
420427}
421428
422- function initCustomScrollbar() {
429+ function initBodyScrollbar() {
430+ if (!enableBodyScrollbar) return;
423431 const bodyElement = document.body;
424- if (!bodyElement) return;
425- if (!bodyScrollbarInitialized) {
426- OverlayScrollbars(
427- // docs say that a initialization to the body element would affect native functionality like window.scrollTo
428- // but just leave it here for now
429- {
430- target: bodyElement,
431- cancel: {
432- nativeScrollbarsOverlaid: true, // don't initialize the overlay scrollbar if there is a native one
433- }
434- }, {
432+ if (!bodyElement || bodyScrollbarInitialized ) return;
433+
434+ OverlayScrollbars(
435+ {
436+ target: bodyElement,
437+ cancel: {
438+ // Avoid replacing native overlay scrollbars (macOS, etc.)
439+ nativeScrollbarsOverlaid: true,
440+ },
441+ },
442+ {
435443 scrollbars: {
436444 theme: 'scrollbar-base scrollbar-auto py-1',
437445 autoHide: 'move',
438446 autoHideDelay: 500,
439447 autoHideSuspend: false,
440448 },
441- });
442- bodyScrollbarInitialized = true;
449+ }
450+ );
451+
452+ bodyScrollbarInitialized = true;
453+ }
454+
455+ function scheduleBodyScrollbarInit() {
456+ if (bodyScrollbarInitialized || bodyScrollbarScheduled) return;
457+ bodyScrollbarScheduled = true;
458+
459+ const run = () => {
460+ bodyScrollbarScheduled = false;
461+ initBodyScrollbar();
462+ };
463+
464+ // Defer body-level DOM mutation until after the initial hydration window.
465+ if (document.readyState === 'complete') {
466+ requestAnimationFrame(run);
467+ return;
468+ }
469+
470+ window.addEventListener(
471+ 'load',
472+ () => {
473+ run();
474+ },
475+ { once: true }
476+ );
477+
478+ const requestIdle = (window as Window & {
479+ requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => void;
480+ }).requestIdleCallback;
481+ if (requestIdle) {
482+ requestIdle(run, { timeout: 1500 });
483+ return;
443484 }
485+ setTimeout(run, 0);
486+ }
487+
488+ function initCustomScrollbar() {
489+ const bodyElement = document.body;
490+ if (!bodyElement) return;
491+ scheduleBodyScrollbarInit();
444492
445493 ensureKatexObserver();
446494 const katexElements = document.querySelectorAll('.katex-display') as NodeListOf<HTMLElement>;
@@ -472,9 +520,31 @@ function init() {
472520}
473521
474522/* Load settings when entering the site */
475- init();
523+ if (document.readyState === 'loading') {
524+ document.addEventListener('DOMContentLoaded', init, { once: true });
525+ } else {
526+ init();
527+ }
528+
529+ function syncNavbarForRoute(isHome: boolean) {
530+ const navbar = document.getElementById('navbar');
531+ if (!navbar) return;
532+ navbar.setAttribute('data-is-home', String(isHome));
533+ if (navbar.getAttribute('data-transparent-mode') === 'semifull') {
534+ // Non-home pages should always render the blurred state immediately.
535+ navbar.classList.toggle('scrolled', !isHome || window.scrollY > 8);
536+ }
537+ }
476538
477539const setup = () => {
540+ // In dev, swup's cached HTML can get out of sync with hot-updated JS and break islands.
541+ if (import.meta.env.DEV) {
542+ try {
543+ window.swup?.cache?.empty?.();
544+ } catch (error) {
545+ console.warn('Failed to clear swup cache in dev:', error);
546+ }
547+ }
478548 // TODO: temp solution to change the height of the banner
479549/*
480550 window.swup.hooks.on('animation:out:start', () => {
@@ -511,19 +581,20 @@ const setup = () => {
511581 updateScrollThresholds();
512582 requestScrollUpdate();
513583 });
514- window.swup.hooks.on('visit:error', (visit: {to: {url: string}}) => {
515- clearAnimationFlags();
516- window.location.href = visit.to.url;
517- });
518584 window.swup.hooks.on('visit:start', (visit: {to: {url: string}}) => {
585+ if (import.meta.env.DEV) {
586+ window.swup?.cache?.empty?.();
587+ }
519588 scheduleSwupRecovery();
520589 // change banner height immediately when a link is clicked
521590 const bodyElement = document.querySelector('body')
522- if (pathsEqual(visit.to.url, url('/'))) {
591+ const isHomeTarget = pathsEqual(visit.to.url, url('/'));
592+ if (isHomeTarget) {
523593 bodyElement!.classList.add('lg:is-home');
524594 } else {
525595 bodyElement!.classList.remove('lg:is-home');
526596 }
597+ syncNavbarForRoute(isHomeTarget);
527598 updateScrollThresholds();
528599 requestScrollUpdate();
529600
@@ -545,6 +616,7 @@ const setup = () => {
545616 if (heightExtend) {
546617 heightExtend.classList.remove('hidden')
547618 }
619+ syncNavbarForRoute(document.body.classList.contains('lg:is-home'));
548620 refreshScrollTargets();
549621 updateScrollThresholds();
550622 requestScrollUpdate();
0 commit comments