@xingwangzhe/stalux 1.57.0 → 1.58.0

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.57.0",
3
+ "version": "1.58.0",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -22,6 +22,8 @@
22
22
  "engines": {
23
23
  "bun": ">=1.4.0"
24
24
  },
25
+ "// browserslist": "现代基线,与 src/internal/modern-css.ts 的 lightningcss targets 保持一致;当前构建不使用 autoprefixer/postcss,此字段用于声明目标并约束后续工具链。",
26
+ "browserslist": ["chrome >= 154", "firefox >= 157", "safari >= 26.6"],
25
27
  "bin": {
26
28
  "stalux": ".release/cli.mjs"
27
29
  },
@@ -110,17 +112,7 @@
110
112
  "@lucide/astro": "^1.71.0",
111
113
  "@microsoft/clarity": "^1.0.2",
112
114
  "@pagefind/component-ui": "^1.6.0",
113
- "@swup/a11y-plugin": "^5.2.1",
114
- "@swup/body-class-plugin": "^3.3.0",
115
- "@swup/fragment-plugin": "^1.3.1",
116
- "@swup/head-plugin": "^2.3.1",
117
- "@swup/js-plugin": "^3.2.0",
118
- "@swup/parallel-plugin": "^0.4.0",
119
- "@swup/preload-plugin": "^3.2.12",
120
- "@swup/progress-plugin": "^3.2.0",
121
- "@swup/route-name-plugin": "^4.1.0",
122
- "@swup/scripts-plugin": "^2.1.0",
123
- "@swup/scroll-plugin": "^4.0.0",
115
+ "@swup/astro": "^1.8.0",
124
116
  "@takumi-rs/core": "^2.20.0",
125
117
  "@w3c/webmcp-polyfill": "^0.1.0",
126
118
  "@waline/client": "^3.16.2",
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { compactStylesPlugin } from "./internal/compact-styles.ts";
4
4
  import { modernCssTargets } from "./internal/modern-css.ts";
5
5
  import { styleNames } from "./internal/style-names.generated.ts";
6
6
  import { swupDebugPlugin } from "./internal/swup-debug-plugin.ts";
7
+ import { SWUP_OPTIONS } from "./internal/swup-options.ts";
7
8
  /**
8
9
  * Stalux — Astro 博客主题集成入口
9
10
  *
@@ -18,6 +19,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
18
19
  import { createRequire } from "node:module";
19
20
  import path from "node:path";
20
21
  import sitemap from "@astrojs/sitemap";
22
+ import swup from "@swup/astro";
21
23
  import { mermaidHast, mermaidMdast } from "@xingwangzhe/satteri-mermaid";
22
24
  import { photoswipe } from "@xingwangzhe/satteri-photoswipe";
23
25
  import type { AstroIntegration } from "astro";
@@ -181,17 +183,20 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
181
183
  // Vite re-optimization would otherwise reload the
182
184
  // entire document midway through a Swup navigation.
183
185
  include: [
184
- "swup",
185
- "@swup/a11y-plugin",
186
- "@swup/body-class-plugin",
187
- "@swup/fragment-plugin",
188
- "@swup/parallel-plugin",
189
- "@swup/route-name-plugin",
190
- "@swup/scripts-plugin",
191
- "@swup/head-plugin",
192
- "@swup/js-plugin",
193
- "@swup/preload-plugin",
194
- "@swup/scroll-plugin",
186
+ // The router and its plugins are assembled by
187
+ // @swup/astro and imported through its client
188
+ // entry points, so those are the specifiers Vite
189
+ // has to see up front.
190
+ "@swup/astro/serialise",
191
+ "@swup/astro/client/Swup",
192
+ "@swup/astro/client/SwupA11yPlugin",
193
+ "@swup/astro/client/SwupBodyClassPlugin",
194
+ "@swup/astro/client/SwupFragmentPlugin",
195
+ "@swup/astro/client/SwupHeadPlugin",
196
+ "@swup/astro/client/SwupPreloadPlugin",
197
+ "@swup/astro/client/SwupProgressPlugin",
198
+ "@swup/astro/client/SwupRouteNamePlugin",
199
+ "@swup/astro/client/SwupScrollPlugin",
195
200
  "@microsoft/clarity",
196
201
  "@pagefind/component-ui",
197
202
  "@waline/client",
@@ -245,6 +250,12 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
245
250
  // 2. 注入全局 CSS
246
251
  injectScript("page-ssr", `import "${srcDir}/styles/generated.css";`);
247
252
  injectScript("page", `import "${srcDir}/scripts/core-runtime.ts";`);
253
+ // 路由层:@swup/astro 负责装配实例与插件,这里只挂三个护栏
254
+ // (脚本去重执行、过期 visit 中止、过期样式剪枝)。
255
+ injectScript(
256
+ "page",
257
+ `import { installRouteGuards } from "${srcDir}/scripts/route-guards.ts"; installRouteGuards();`,
258
+ );
248
259
 
249
260
  // 3. 注入所有页面路由(仅插件模式,源码模式下使用文件路由)
250
261
  if (isPluginMode) {
@@ -679,8 +690,10 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
679
690
  };
680
691
 
681
692
  // 打包内置集成:Astro 的 integrations 配置支持嵌套数组并自动展平(schema 里 val.flat(Infinity)),
682
- // 因此这里返回 [coreIntegration, sitemap, expressiveCode],让消费方一行 `integrations: [stalux()]` 即可完成配置。
683
- const bundled: AstroIntegration[] = [coreIntegration];
693
+ // 因此这里返回 [swup, coreIntegration, sitemap, expressiveCode],让消费方一行 `integrations: [stalux()]` 即可完成配置。
694
+ // 路由器必须排在第一位:@swup/astro 先注入初始化脚本,主题的护栏脚本才能在同一轮
695
+ // 模块求值里直接拿到 window.swup。
696
+ const bundled: AstroIntegration[] = [swup(SWUP_OPTIONS), coreIntegration];
684
697
 
685
698
  if (opt.sitemap !== false) {
686
699
  const userSitemap = opt.sitemap === true ? undefined : opt.sitemap;
@@ -1,5 +1,6 @@
1
1
  // Modern CSS output floor for the 1.35 release. Older fallback expansions are unnecessary.
2
2
  // Acceptance uses current stable browser engines; targets intentionally retain native nesting.
3
+ // Keep in sync with the `browserslist` field in package.json (documentation for other tools).
3
4
  export const modernCssTargets = {
4
5
  chrome: 154 << 16,
5
6
  firefox: 157 << 16,
@@ -247,101 +247,94 @@ export const styleNames: { classes: Record<string, string>; variables: Record<st
247
247
  "--border-thick": "--sh",
248
248
  "--border-thin": "--si",
249
249
  "--card-bg": "--sj",
250
- "--card-border": "--sk",
251
- "--card-box-shadow": "--sl",
252
- "--card-box-shadow-hover": "--sm",
253
- "--category-bg": "--sn",
254
- "--category-bg-hover": "--so",
255
- "--cloud-base-diameter": "--sp",
256
- "--cloud-density": "--sq",
257
- "--date-bg": "--sr",
258
- "--date-border": "--ss",
259
- "--dot": "--st",
260
- "--easing-bounce": "--su",
261
- "--font-size-base": "--sv",
262
- "--font-size-small": "--sw",
263
- "--font-size-smaller": "--sx",
264
- "--font-size-subtitle": "--sy",
265
- "--font-size-tiny": "--sz",
266
- "--muted-text": "--s10",
267
- "--nav-scale": "--s11",
268
- "--radius-full": "--s12",
269
- "--radius-lg": "--s13",
270
- "--radius-md": "--s14",
271
- "--radius-sm": "--s15",
272
- "--shadow-dark": "--s16",
273
- "--shadow-lg": "--s17",
274
- "--shadow-md": "--s18",
275
- "--shadow-raised-hover": "--s19",
276
- "--shadow-sidebar": "--s1a",
277
- "--shadow-sm": "--s1b",
278
- "--shadow-xl": "--s1c",
279
- "--space-2xl": "--s1d",
280
- "--space-2xs": "--s1e",
281
- "--space-3xl": "--s1f",
282
- "--space-lg": "--s1g",
283
- "--space-md": "--s1h",
284
- "--space-sm": "--s1i",
285
- "--space-xl": "--s1j",
286
- "--space-xs": "--s1k",
287
- "--stalux-accent-color": "--s1l",
288
- "--stalux-accent-rgb": "--s1m",
289
- "--stalux-bg-color": "--s1n",
290
- "--stalux-bg-filter": "--s1o",
291
- "--stalux-nav-offset": "--s1p",
292
- "--stalux-scrollbar-thumb": "--s1q",
293
- "--stalux-scrollbar-thumb-hover": "--s1r",
294
- "--stalux-scrollbar-track": "--s1s",
295
- "--stalux-site-header-height": "--s1t",
296
- "--stalux-site-header-min-height": "--s1u",
297
- "--stalux-text-shadow": "--s1v",
298
- "--surface-glass": "--s1w",
299
- "--surface-glass-shadow": "--s1x",
300
- "--tag-bg": "--s1y",
301
- "--tag-bg-hover": "--s1z",
302
- "--tl-line": "--s20",
303
- "--tl-pad": "--s21",
304
- "--transition-fast": "--s22",
305
- "--transition-medium": "--s23",
306
- "--transition-normal": "--s24",
307
- "--transition-slow": "--s25",
308
- "--transition-slower": "--s26",
309
- "--transition-slowest": "--s27",
310
- "--un-rotate": "--s28",
311
- "--un-rotate-x": "--s29",
312
- "--un-rotate-y": "--s2a",
313
- "--un-rotate-z": "--s2b",
314
- "--un-scale-x": "--s2c",
315
- "--un-scale-y": "--s2d",
316
- "--un-scale-z": "--s2e",
317
- "--un-skew-x": "--s2f",
318
- "--un-skew-y": "--s2g",
319
- "--un-translate-x": "--s2h",
320
- "--un-translate-y": "--s2i",
321
- "--un-translate-z": "--s2j",
322
- "--updated-bg": "--s2k",
323
- "--updated-border": "--s2l",
324
- "--white-05p": "--s2m",
325
- "--white-10p": "--s2n",
326
- "--white-15p": "--s2o",
327
- "--white-20p": "--s2p",
328
- "--white-30p": "--s2q",
329
- "--white-50p": "--s2r",
330
- "--white-60p": "--s2s",
331
- "--white-70p": "--s2t",
332
- "--white-75p": "--s2u",
333
- "--white-78p": "--s2v",
334
- "--white-80p": "--s2w",
335
- "--white-85p": "--s2x",
336
- "--white-90p": "--s2y",
337
- "--white-95p": "--s2z",
338
- "--white-full": "--s30"
250
+ "--card-box-shadow": "--sk",
251
+ "--card-box-shadow-hover": "--sl",
252
+ "--category-bg": "--sm",
253
+ "--category-bg-hover": "--sn",
254
+ "--cloud-base-diameter": "--so",
255
+ "--cloud-density": "--sp",
256
+ "--date-bg": "--sq",
257
+ "--date-border": "--sr",
258
+ "--dot": "--ss",
259
+ "--font-size-base": "--st",
260
+ "--font-size-small": "--su",
261
+ "--font-size-smaller": "--sv",
262
+ "--font-size-subtitle": "--sw",
263
+ "--font-size-tiny": "--sx",
264
+ "--muted-text": "--sy",
265
+ "--nav-scale": "--sz",
266
+ "--radius-full": "--s10",
267
+ "--radius-lg": "--s11",
268
+ "--radius-md": "--s12",
269
+ "--radius-sm": "--s13",
270
+ "--shadow-dark": "--s14",
271
+ "--shadow-lg": "--s15",
272
+ "--shadow-md": "--s16",
273
+ "--shadow-sidebar": "--s17",
274
+ "--shadow-sm": "--s18",
275
+ "--shadow-xl": "--s19",
276
+ "--space-2xl": "--s1a",
277
+ "--space-2xs": "--s1b",
278
+ "--space-3xl": "--s1c",
279
+ "--space-lg": "--s1d",
280
+ "--space-md": "--s1e",
281
+ "--space-sm": "--s1f",
282
+ "--space-xl": "--s1g",
283
+ "--space-xs": "--s1h",
284
+ "--stalux-accent-color": "--s1i",
285
+ "--stalux-accent-rgb": "--s1j",
286
+ "--stalux-bg-color": "--s1k",
287
+ "--stalux-bg-filter": "--s1l",
288
+ "--stalux-nav-offset": "--s1m",
289
+ "--stalux-scrollbar-thumb": "--s1n",
290
+ "--stalux-scrollbar-thumb-hover": "--s1o",
291
+ "--stalux-scrollbar-track": "--s1p",
292
+ "--stalux-site-header-height": "--s1q",
293
+ "--stalux-site-header-min-height": "--s1r",
294
+ "--stalux-text-shadow": "--s1s",
295
+ "--surface-glass": "--s1t",
296
+ "--surface-glass-shadow": "--s1u",
297
+ "--tag-bg": "--s1v",
298
+ "--tag-bg-hover": "--s1w",
299
+ "--tl-line": "--s1x",
300
+ "--tl-pad": "--s1y",
301
+ "--transition-fast": "--s1z",
302
+ "--transition-medium": "--s20",
303
+ "--transition-normal": "--s21",
304
+ "--transition-slow": "--s22",
305
+ "--un-rotate": "--s23",
306
+ "--un-rotate-x": "--s24",
307
+ "--un-rotate-y": "--s25",
308
+ "--un-rotate-z": "--s26",
309
+ "--un-scale-x": "--s27",
310
+ "--un-scale-y": "--s28",
311
+ "--un-scale-z": "--s29",
312
+ "--un-skew-x": "--s2a",
313
+ "--un-skew-y": "--s2b",
314
+ "--un-translate-x": "--s2c",
315
+ "--un-translate-y": "--s2d",
316
+ "--un-translate-z": "--s2e",
317
+ "--updated-bg": "--s2f",
318
+ "--updated-border": "--s2g",
319
+ "--white-05p": "--s2h",
320
+ "--white-10p": "--s2i",
321
+ "--white-15p": "--s2j",
322
+ "--white-20p": "--s2k",
323
+ "--white-30p": "--s2l",
324
+ "--white-50p": "--s2m",
325
+ "--white-60p": "--s2n",
326
+ "--white-70p": "--s2o",
327
+ "--white-75p": "--s2p",
328
+ "--white-78p": "--s2q",
329
+ "--white-80p": "--s2r",
330
+ "--white-85p": "--s2s",
331
+ "--white-90p": "--s2t",
332
+ "--white-95p": "--s2u",
333
+ "--white-full": "--s2v"
339
334
  },
340
335
  "animations": {
341
- "spin": "k0",
342
- "stalux-cursor-blink": "k1",
343
- "stalux-link-avatar-spin": "k2",
344
- "stalux-random-spin": "k3",
345
- "stalux-social-stalux-social-arrive": "k4"
336
+ "stalux-cursor-blink": "k0",
337
+ "stalux-random-spin": "k1",
338
+ "stalux-social-stalux-social-arrive": "k2"
346
339
  }
347
340
  };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Swup configuration handed to `@swup/astro`.
3
+ *
4
+ * `@swup/astro` owns construction of the Swup instance and all plugin
5
+ * assembly; the theme only describes *which* behaviour it wants. Anything that
6
+ * cannot be expressed here lives in `src/scripts/route-guards.ts`.
7
+ */
8
+ import type { Options, Route } from "@swup/astro";
9
+
10
+ /** Named routes shared by the router and the route classifier. */
11
+ export const SWUP_ROUTES: Route[] = [
12
+ { name: "home", path: "/" },
13
+ { name: "article", path: "/posts/:post" },
14
+ { name: "archive", path: "/archives" },
15
+ { name: "tags-index", path: "/tags" },
16
+ { name: "tags-detail", path: "/tags/:tag" },
17
+ { name: "categories-index", path: "/categories" },
18
+ { name: "categories-detail", path: "/categories/:category" },
19
+ ...["about", "links", "words"].map((name) => ({ name, path: `/${name}` })),
20
+ { name: "not-found", path: "/404" },
21
+ ];
22
+
23
+ /**
24
+ * Real files are never pages. `@swup/astro` builds `ignoreVisit` itself and
25
+ * only lets `ignore` extend it, so this list is the whole hook.
26
+ */
27
+ export const SWUP_IGNORE_FILES = /\.(?:md|json|xml|txt|html?|pdf|png|webp|avif|svg|jpe?g)$/iu;
28
+
29
+ /**
30
+ * Paths the theme renders as soft-navigable pages.
31
+ *
32
+ * `@swup/astro` cannot express "only these routes" as a predicate: `ignore`
33
+ * accepts regexes, and a regex is the only form that survives serialisation
34
+ * into the init script. Keeping the alternation here also keeps the rule next
35
+ * to `SWUP_ROUTES` above, which is the same set of pages.
36
+ */
37
+ const SOFT_NAV_PATHS =
38
+ /^\/(?:|archives|links|words|about|404|tags(?:\/[^/?#]+)?|categories(?:\/[^/?#]+)?|posts\/[^/?#]+)\/?(?:[?#].*)?$/u;
39
+
40
+ /** Every path the theme itself renders; anything else is left to the browser. */
41
+ export const SWUP_IGNORE_UNKNOWN_ROUTES = new RegExp(`^(?!${SOFT_NAV_PATHS.source})`, "u");
42
+
43
+ /**
44
+ * `@swup/astro` types most plugin options as booleans, but forwards any object
45
+ * it is given straight into the plugin constructor. Two of these need their
46
+ * real plugin options (body-class prefix, head persistence, scroll behaviour),
47
+ * so the object form is used deliberately and the cast is made once here.
48
+ */
49
+ export const SWUP_OPTIONS = {
50
+ containers: ["#swup-main"],
51
+ // No route animation: no theme, and no `[class*="transition-"]` selector to
52
+ // wait on. The swap is a plain, synchronous DOM replacement.
53
+ theme: false,
54
+ animationClass: false,
55
+ // The init script must run while the first document is still parsing; on
56
+ // idle the first click can arrive before the router is listening.
57
+ loadOnIdle: false,
58
+ // The guard script needs the instance; route-prefetch already reads it.
59
+ globalInstance: true,
60
+ // Page scripts are re-executed by the theme's own once-per-session runner
61
+ // (see route-guards.ts). The plugin's whole-document pass would replay
62
+ // analytics SDKs on every visit.
63
+ reloadScripts: false,
64
+ cache: true,
65
+ native: false,
66
+ morph: false,
67
+ forms: false,
68
+ preload: { hover: true, visible: false },
69
+ progress: { delay: 80, transition: 200 },
70
+ accessibility: true,
71
+ updateBodyClass: { prefix: "page-", attributes: [/^data-stalux-(?!bg-)/u] },
72
+ updateHead: {
73
+ awaitAssets: true,
74
+ // Keep old CSS readable while the incoming sheet loads; route-guards
75
+ // removes the styles the new page does not use once it is in place.
76
+ persistTags: "link[rel=stylesheet], script[src], style",
77
+ },
78
+ routes: SWUP_ROUTES,
79
+ fragments: [
80
+ {
81
+ from: ["/tags/:slug", "/categories/:slug"],
82
+ to: ["/tags/:slug", "/categories/:slug"],
83
+ containers: ["#swup-taxonomy"],
84
+ scroll: true,
85
+ },
86
+ {
87
+ from: "/posts/:post",
88
+ to: "/posts/:post",
89
+ containers: ["#swup-article"],
90
+ scroll: true,
91
+ },
92
+ ],
93
+ smoothScrolling: {
94
+ doScrollingRightAway: false,
95
+ animateScroll: { betweenPages: false, samePageWithHash: true, samePage: true },
96
+ },
97
+ ignore: [SWUP_IGNORE_FILES, SWUP_IGNORE_UNKNOWN_ROUTES],
98
+ } as unknown as Partial<Options>;
@@ -74,7 +74,6 @@ const routeKind = getRouteKind(Astro.url.pathname);
74
74
  />
75
75
  <script>
76
76
  import "../scripts/background.ts";
77
- import "../scripts/view-transitions.ts";
78
77
  // Every page type has at least one list, so the unroll cue is registered
79
78
  // once in the shared layout instead of per page. Page-runtime dedupes it
80
79
  // against the article and archive components that also request it.
@@ -1,7 +1,6 @@
1
1
  /** Async native SDKs keep live DOM events out of worker proxies during Astro swaps. */
2
2
 
3
3
  import { createClientLogger } from "./logger";
4
- import { prepareRouteMotion, type RouteMotionEvent } from "./route-motion";
5
4
 
6
5
  const logger = createClientLogger("analytics");
7
6
 
@@ -32,7 +31,6 @@ const state = window.__staluxAnalyticsRuntime;
32
31
  state.scripts ??= new Map();
33
32
 
34
33
  type GooglePage = {
35
- ready: Promise<unknown>;
36
34
  payload: {
37
35
  page_location: string;
38
36
  page_path: string;
@@ -41,7 +39,6 @@ type GooglePage = {
41
39
  };
42
40
  };
43
41
  const googlePages: GooglePage[] = [];
44
- let transitionReady: Promise<unknown> = Promise.resolve();
45
42
  let googleScheduled = false;
46
43
  let googleSchedule = 0;
47
44
  let googleTimer: ReturnType<typeof setTimeout> | undefined;
@@ -77,9 +74,9 @@ function scheduleGooglePages(): void {
77
74
  // A burst of visits stays FIFO, with a separate task for each SDK call.
78
75
  scheduleGooglePages();
79
76
  };
80
- // A queued visit can outlive its own transition. Also wait for whichever
81
- // incoming snapshot is current when this task is scheduled.
82
- void Promise.all([page.ready, transitionReady]).then(() => {
77
+ // A queued visit can outlive the page it belongs to; only the paint
78
+ // ordering of the current document matters here.
79
+ void Promise.resolve(page).then(() => {
83
80
  if (schedule !== googleSchedule) return;
84
81
  // Let the routed content paint after native rendering resumes and its own
85
82
  // motion is installed. The timer also covers throttled/background frames.
@@ -336,7 +333,6 @@ function trackGooglePage(): void {
336
333
  // Capture the visit before yielding: another navigation must not replace its
337
334
  // URL, title or virtual referrer while the SDK is waiting for its task.
338
335
  googlePages.push({
339
- ready: transitionReady,
340
336
  payload: {
341
337
  page_location: window.location.href,
342
338
  page_path: window.location.pathname + window.location.search,
@@ -439,11 +435,10 @@ if (state.installed && (!state.native || state.revision !== 4)) {
439
435
  state.native = true;
440
436
  state.revision = 4;
441
437
  state.installed = true;
442
- document.addEventListener("astro:before-swap", (event) => {
438
+ document.addEventListener("astro:before-swap", () => {
443
439
  pendingEarlyView++;
444
440
  earlyViewPending = false;
445
441
  viewedEarly = false;
446
- transitionReady = prepareRouteMotion(event as Event & RouteMotionEvent).ready;
447
442
  // Invalidate an older page's frames, timer and posted task before this
448
443
  // incoming snapshot starts. Its captured visit remains in the FIFO.
449
444
  cancelGoogleSchedule();
@@ -11,10 +11,6 @@ const MAX_ITEMS = 16;
11
11
  const STAGGER_MS = 22;
12
12
  const MAX_DELAY_MS = 200;
13
13
  const DURATION_MS = 190;
14
- // When a route cue is already moving the page body, the sheet waits for it to
15
- // mostly settle before unrolling. Starting both at once makes the rows wipe
16
- // open while the whole page is still semi-transparent, which reads as a smudge.
17
- const ROUTE_LEAD_IN_MS = 75;
18
14
  // Strong ease-out: the row arrives fast and settles softly. The built-in
19
15
  // ease-out is too weak to read at this size.
20
16
  const EASING = "cubic-bezier(0.23, 1, 0.32, 1)";
@@ -59,9 +55,7 @@ function canReveal(): boolean {
59
55
  if (typeof IntersectionObserver !== "function") return false;
60
56
  if (typeof document === "undefined") return false;
61
57
  if (document.hidden) return false;
62
- if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
63
- // Returning to a history entry restores what the reader already saw.
64
- return document.documentElement.dataset.staluxVtPreset !== "restore";
58
+ return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
65
59
  }
66
60
 
67
61
  function groups(): Element[] {
@@ -102,11 +96,6 @@ function staggerOf(index: number): number {
102
96
  return Math.min(index * STAGGER_MS, MAX_DELAY_MS);
103
97
  }
104
98
 
105
- function leadIn(): number {
106
- // A route cue leaves its preset on the document element while it runs.
107
- return document.documentElement.dataset.staluxVtPreset ? ROUTE_LEAD_IN_MS : 0;
108
- }
109
-
110
99
  /**
111
100
  * Hold every incoming row on the wipe's first frame the moment the router has
112
101
  * its new document, before the browser paints it.
@@ -201,7 +190,6 @@ export function mountListReveal(): (() => void) | undefined {
201
190
  };
202
191
  const reveal = (container: Element) => {
203
192
  if (document.hidden || preference.matches) return;
204
- const lead = leadIn();
205
193
  for (const [index, row] of rows(container).entries()) {
206
194
  if (typeof row.animate !== "function") continue;
207
195
  // Prefer the effect created during the swap: it already owns the
@@ -216,7 +204,7 @@ export function mountListReveal(): (() => void) | undefined {
216
204
  primed?.animation ??
217
205
  row.animate(UNROLL_KEYFRAMES, {
218
206
  duration: DURATION_MS,
219
- delay: lead + staggerOf(index),
207
+ delay: staggerOf(index),
220
208
  easing: EASING,
221
209
  // Hidden during the stagger delay instead of popping in.
222
210
  fill: "backwards",
@@ -224,7 +212,7 @@ export function mountListReveal(): (() => void) | undefined {
224
212
  if (primed) {
225
213
  // The stagger is counted from now, not from the swap.
226
214
  primed.animation.effect?.updateTiming({
227
- delay: lead + primed.stagger,
215
+ delay: primed.stagger,
228
216
  });
229
217
  primed.animation.play();
230
218
  }
@@ -27,13 +27,12 @@ export function mountModuleMotion(): (() => void) | undefined {
27
27
  // so the observer's target needs no assertion.
28
28
  const element = entry.target;
29
29
  observer.unobserve(element);
30
- // The router owns a single bounded lead region. Do not layer an
31
- // observer entry over that motion or make long comments slide.
30
+ // The router owns the swap. Do not layer an observer entry over
31
+ // it or make long comments slide.
32
32
  if (
33
33
  document.hidden ||
34
34
  motionPreference.matches ||
35
35
  document.documentElement.classList.contains("is-changing") ||
36
- document.documentElement.dataset.staluxVtPreset ||
37
36
  // A persisted node kept its identity across the visit; it was
38
37
  // never rebuilt, so re-entering it would be a second cue.
39
38
  element.closest("[data-swup-persist]") ||
@@ -1,4 +1,3 @@
1
- import SwupScriptsPlugin from "@swup/scripts-plugin";
2
1
  import { createClientLogger } from "./logger";
3
2
 
4
3
  const logger = createClientLogger("navigation-scripts");
@@ -28,12 +27,18 @@ const javascript = new Set([
28
27
  ]);
29
28
 
30
29
  /**
31
- * Scripts Plugin owns script recreation. Astro's page-load event additionally
32
- * needs an ordered completion barrier and once-per-session module semantics.
33
- * Disable the plugin's automatic whole-document pass: that would also replay
34
- * analytics SDKs and scripts in retained shells on every navigation.
30
+ * The theme owns script execution instead of the swup scripts plugin: Astro's
31
+ * page-load event needs an ordered completion barrier and once-per-session
32
+ * semantics. The plugin's whole-document pass would also replay analytics SDKs
33
+ * and the scripts of retained shells on every visit.
35
34
  */
36
- export const navigationScripts = new SwupScriptsPlugin({ head: false, body: false });
35
+ function runScript(from: HTMLScriptElement): HTMLScriptElement {
36
+ const script = document.createElement("script");
37
+ for (const { name, value } of from.attributes) script.setAttribute(name, value);
38
+ script.textContent = from.textContent;
39
+ from.replaceWith(script);
40
+ return script;
41
+ }
37
42
 
38
43
  function describeScript(script: HTMLScriptElement, base: string) {
39
44
  const type = (script.getAttribute("type") ?? "").trim().toLowerCase();
@@ -112,7 +117,7 @@ export function prepareNavigationScripts(
112
117
  );
113
118
  if (copy) copy.replaceWith(inert);
114
119
  else (item.head ? document.head : document.body).append(inert);
115
- const script = navigationScripts.runScript(inert);
120
+ const script = runScript(inert);
116
121
  script.async = false;
117
122
  executedNodes.add(script);
118
123
  let observed = script;