@xingwangzhe/stalux 1.55.2 → 1.55.4

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": "@xingwangzhe/stalux",
3
- "version": "1.55.2",
3
+ "version": "1.55.4",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -438,25 +438,20 @@ export function installSwupRouter(): Swup {
438
438
  // then watch it jump to the entry offset and slide back, which reads as
439
439
  // the page shaking. Put the scroll at its final position and start the
440
440
  // cue in this same synchronous block, before the scripts run, so the
441
- // compositor already owns the cue while the main thread is busy. A visit
442
- // with a hash or a history traversal keeps the deferred path, because its
443
- // scroll is not decided yet.
444
- if (
445
- // A history traversal restores a position that is only known later,
446
- // and a hash target is decided by the anchor.
447
- !visit.history.popstate &&
448
- context.navigationType !== "traverse" &&
449
- !context.to.hash &&
450
- context.preset !== "none" &&
451
- // ScrollPlugin owns visit.scroll; a bare visit has no reset flag.
452
- visit.scroll?.reset !== false
453
- ) {
454
- const position = scroll.getCachedScrollPositions?.(visit.to.url)?.window;
455
- const top = position?.top ?? 0;
456
- const left = position?.left ?? 0;
457
- if (window.scrollY !== top || window.scrollX !== left)
458
- window.scrollTo({ top, left, behavior: "instant" });
459
- startMotion(visit);
441
+ // compositor already owns the cue while the main thread is busy. Hash
442
+ // targets and history traversals are settled in content:scroll below.
443
+ // Entering a new document from a link always starts at its top. The
444
+ // motion preset (reduced motion / hidden tab) and the ScrollPlugin's
445
+ // reset flag are visual concerns, not navigation ones; neither may opt
446
+ // out of the reset. A cached offset belongs to history traversals only
447
+ // (handled in content:scroll) and must never pull a fresh page back
448
+ // into the middle of the previous article.
449
+ const startsFresh =
450
+ !visit.history.popstate && context.navigationType !== "traverse" && !context.to.hash;
451
+ if (startsFresh) {
452
+ if (window.scrollY !== 0 || window.scrollX !== 0)
453
+ window.scrollTo({ top: 0, left: 0, behavior: "instant" });
454
+ if (context.preset !== "none" && visit.scroll?.reset !== false) startMotion(visit);
460
455
  }
461
456
  context.scriptsReady = context.executeScripts?.(context.controller.signal);
462
457
  });
@@ -464,15 +459,16 @@ export function installSwupRouter(): Swup {
464
459
  swup.hooks.replace(
465
460
  "content:scroll",
466
461
  async (visit, args, defaultScroll) => {
462
+ const context = contexts.get(visit);
467
463
  const saved = visit.history.popstate
468
464
  ? historyPositions.get(Number(window.history.state?.index))
469
465
  : undefined;
470
466
  if (visit.history.popstate) {
471
- // ScrollPlugin defers unanimated popstate to the browser, but Swup
472
- // uses manual restoration even for reduced motion and native swipes.
473
- const position = saved ??
474
- scroll.getCachedScrollPositions(visit.to.url)?.window ?? { top: 0, left: 0 };
475
- window.scrollTo({ top: position.top, left: position.left, behavior: "instant" });
467
+ // Every route change starts at the top. Restoring the exact
468
+ // mid-article offset on back/forward reads as "the navigation
469
+ // never scrolled"; keep only the focused link so keyboard users
470
+ // retain their place in the navigation.
471
+ window.scrollTo({ top: 0, left: 0, behavior: "instant" });
476
472
  scroll.restoreScrollContainers(visit.to.url);
477
473
  if (saved?.focus) {
478
474
  const link = [...document.querySelectorAll<HTMLAnchorElement>("a[href]")].find(
@@ -487,6 +483,14 @@ export function installSwupRouter(): Swup {
487
483
  // ScrollPlugin scrolls in rAF; finish it before heading visibility checks.
488
484
  if (!document.hidden)
489
485
  await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
486
+ // That deferred frame may have restored a cached mid-article offset.
487
+ // A link navigation always wins: land the new document at its top.
488
+ if (
489
+ context?.navigationType !== "traverse" &&
490
+ !context?.to.hash &&
491
+ (window.scrollY !== 0 || window.scrollX !== 0)
492
+ )
493
+ window.scrollTo({ top: 0, left: 0, behavior: "instant" });
490
494
  startMotion(visit);
491
495
  return true;
492
496
  },
@@ -1,8 +1,36 @@
1
1
  import { initTocScrollSpy } from "../utils/toc-scrollspy";
2
2
  import { registerPageLifecycle } from "./page-runtime";
3
3
 
4
+ /**
5
+ * 目录高亮初始化会读取全部标题位置,强制一次完整布局:线上文章页实测
6
+ * 226ms 长任务(JS 仅 21ms,其余是 Style & Layout),正好落在 TBT 统计窗口内。
7
+ * 而读者进入文章后通常先看开头,滚到需要高亮的时机远晚于首屏,
8
+ * 因此把它放到空闲时段;2s 兜底保证页面持续繁忙时仍会初始化。
9
+ */
4
10
  registerPageLifecycle("toc-scrollspy", () => {
5
11
  const container = document.querySelector<HTMLElement>("[data-toc-active-class]");
6
12
  const activeClass = container?.dataset.tocActiveClass;
7
- return activeClass ? initTocScrollSpy(activeClass) : undefined;
13
+ if (!activeClass) return;
14
+ let cleanup: (() => void) | undefined;
15
+ let disposed = false;
16
+ const run = () => {
17
+ if (!disposed) cleanup = initTocScrollSpy(activeClass);
18
+ };
19
+ // 通过可选属性读取,避免 TS 把初始化值收窄成「必定存在」而报常真判断。
20
+ const idle = (
21
+ globalThis as {
22
+ requestIdleCallback?: (
23
+ callback: IdleRequestCallback,
24
+ options?: IdleRequestOptions,
25
+ ) => number;
26
+ }
27
+ ).requestIdleCallback;
28
+ const handle = idle ? idle(run, { timeout: 2000 }) : setTimeout(run, 300);
29
+ return () => {
30
+ disposed = true;
31
+ // requestIdleCallback 与 cancelIdleCallback 在支持后者的实现里总是成对出现。
32
+ if (idle) globalThis.cancelIdleCallback(handle as number);
33
+ else clearTimeout(handle as unknown as ReturnType<typeof setTimeout>);
34
+ cleanup?.();
35
+ };
8
36
  });