@xingwangzhe/stalux 1.52.2 → 1.54.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 +8 -1
- package/src/components/stalux/posts/postContent.astro +1 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +7 -0
- package/src/internal/swup-debug-plugin.ts +25 -0
- package/src/layouts/Stalux.astro +2 -2
- package/src/pages/categories/[category].astro +4 -2
- package/src/pages/tags/[tag].astro +4 -2
- package/src/scripts/navigation-scripts.ts +129 -162
- package/src/scripts/swup-router.ts +120 -16
- package/src/styles/module-motion.css +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingwangzhe/stalux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.54.0",
|
|
4
4
|
"description": "A powerful, modern Astro blog theme — use as template or install as plugin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -112,9 +112,15 @@
|
|
|
112
112
|
"@microsoft/clarity": "1.0.2",
|
|
113
113
|
"@pagefind/component-ui": "^1.6.0",
|
|
114
114
|
"@swup/a11y-plugin": "^5.2.1",
|
|
115
|
+
"@swup/body-class-plugin": "^3.3.0",
|
|
116
|
+
"@swup/fragment-plugin": "^1.3.1",
|
|
115
117
|
"@swup/head-plugin": "^2.3.1",
|
|
116
118
|
"@swup/js-plugin": "^3.2.0",
|
|
119
|
+
"@swup/parallel-plugin": "^0.4.0",
|
|
117
120
|
"@swup/preload-plugin": "^3.2.12",
|
|
121
|
+
"@swup/progress-plugin": "^3.2.0",
|
|
122
|
+
"@swup/route-name-plugin": "^4.1.0",
|
|
123
|
+
"@swup/scripts-plugin": "^2.1.0",
|
|
118
124
|
"@swup/scroll-plugin": "^4.0.0",
|
|
119
125
|
"@takumi-rs/core": "2.19.0",
|
|
120
126
|
"@waline/client": "^3.16.2",
|
|
@@ -140,6 +146,7 @@
|
|
|
140
146
|
"devDependencies": {
|
|
141
147
|
"@astrojs/check": "^0.9.12",
|
|
142
148
|
"@biomejs/biome": "^2.6.5",
|
|
149
|
+
"@swup/debug-plugin": "^4.1.0",
|
|
143
150
|
"@types/bun": "1.4.4",
|
|
144
151
|
"@types/css-tree": "^3.2.0",
|
|
145
152
|
"@unocss/astro": "^66.12.0",
|
package/src/env.d.ts
CHANGED
|
@@ -12,3 +12,8 @@ declare module "virtual:stalux-social-card-paths" {
|
|
|
12
12
|
const paths: import("./internal/social-card-renderer").SocialCardPaths;
|
|
13
13
|
export default paths;
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
declare module "virtual:stalux-swup-debug" {
|
|
17
|
+
const Debug: (new () => import("swup").Plugin) | null;
|
|
18
|
+
export default Debug;
|
|
19
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { forEachBounded } from "./internal/bounded-tasks.ts";
|
|
|
3
3
|
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
|
+
import { swupDebugPlugin } from "./internal/swup-debug-plugin.ts";
|
|
6
7
|
/**
|
|
7
8
|
* Stalux — Astro 博客主题集成入口
|
|
8
9
|
*
|
|
@@ -182,6 +183,11 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
182
183
|
include: [
|
|
183
184
|
"swup",
|
|
184
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",
|
|
185
191
|
"@swup/head-plugin",
|
|
186
192
|
"@swup/js-plugin",
|
|
187
193
|
"@swup/preload-plugin",
|
|
@@ -216,6 +222,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
216
222
|
? `export default ${JSON.stringify(socialCardPaths)}`
|
|
217
223
|
: undefined,
|
|
218
224
|
},
|
|
225
|
+
swupDebugPlugin(command === "dev"),
|
|
219
226
|
searchStylePlugin(command === "dev", config.base),
|
|
220
227
|
webMcpMutationsPlugin(),
|
|
221
228
|
...(command === "build"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import type { Plugin } from "vite";
|
|
3
|
+
|
|
4
|
+
/** Keep a source-only devDependency optional in published consumer projects. */
|
|
5
|
+
export function swupDebugPlugin(development: boolean): Plugin {
|
|
6
|
+
const id = "virtual:stalux-swup-debug";
|
|
7
|
+
let entry: string | undefined;
|
|
8
|
+
if (development) {
|
|
9
|
+
try {
|
|
10
|
+
entry = createRequire(import.meta.url).resolve("@swup/debug-plugin");
|
|
11
|
+
} catch {
|
|
12
|
+
// Consumers can install @swup/debug-plugin as their own devDependency.
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
name: "stalux:swup-debug",
|
|
17
|
+
resolveId: (source) => (source === id ? `\0${id}` : undefined),
|
|
18
|
+
load: (source) =>
|
|
19
|
+
source === `\0${id}`
|
|
20
|
+
? entry
|
|
21
|
+
? `export { default } from ${JSON.stringify(entry)};`
|
|
22
|
+
: "export default null;"
|
|
23
|
+
: undefined,
|
|
24
|
+
};
|
|
25
|
+
}
|
package/src/layouts/Stalux.astro
CHANGED
|
@@ -86,7 +86,7 @@ const routeKind = getRouteKind(Astro.url.pathname);
|
|
|
86
86
|
</script>
|
|
87
87
|
{aiConfig?.conformance !== "disabled" && <script>import "../scripts/webmcp.ts";</script>}
|
|
88
88
|
<body
|
|
89
|
-
class=
|
|
89
|
+
class={`stalux-root dark page-${routeKind}`}
|
|
90
90
|
data-stalux-bg-layer="a"
|
|
91
91
|
data-stalux-bg-index="0"
|
|
92
92
|
data-stalux-site-title={siteConfig.title}
|
|
@@ -116,7 +116,7 @@ const routeKind = getRouteKind(Astro.url.pathname);
|
|
|
116
116
|
aria-hidden="true"
|
|
117
117
|
></div>
|
|
118
118
|
<div class="bg-layer bg-b" data-stalux-background="b" aria-hidden="true"></div>
|
|
119
|
-
<main class="stalux-main" data-stalux-route={routeKind}>
|
|
119
|
+
<main id="swup-main" class="stalux-main" data-stalux-route={routeKind}>
|
|
120
120
|
<slot />
|
|
121
121
|
</main>
|
|
122
122
|
<Footer />
|
|
@@ -32,6 +32,8 @@ const props = Astro.props as Props;
|
|
|
32
32
|
count: props.count,
|
|
33
33
|
})}
|
|
34
34
|
>
|
|
35
|
-
<
|
|
36
|
-
|
|
35
|
+
<div id="swup-taxonomy">
|
|
36
|
+
<TagHeader type={t("taxonomy.category")} name={props.category} count={props.count} />
|
|
37
|
+
<PostList posts={props.posts} />
|
|
38
|
+
</div>
|
|
37
39
|
</Stalux>
|
|
@@ -34,6 +34,8 @@ const props = Astro.props as Props;
|
|
|
34
34
|
})}
|
|
35
35
|
noindex={props.count < 3}
|
|
36
36
|
>
|
|
37
|
-
<
|
|
38
|
-
|
|
37
|
+
<div id="swup-taxonomy">
|
|
38
|
+
<TagHeader type={t("taxonomy.tag")} name={props.tag} count={props.count} />
|
|
39
|
+
<PostList posts={props.posts} />
|
|
40
|
+
</div>
|
|
39
41
|
</Stalux>
|
|
@@ -1,84 +1,10 @@
|
|
|
1
|
+
import SwupScriptsPlugin from "@swup/scripts-plugin";
|
|
1
2
|
import { createClientLogger } from "./logger";
|
|
2
3
|
|
|
3
4
|
const logger = createClientLogger("navigation-scripts");
|
|
4
5
|
const executions = new Map<string, Promise<void>>();
|
|
5
|
-
const executedNodes = new WeakSet<HTMLScriptElement>();
|
|
6
6
|
let seeded = false;
|
|
7
|
-
|
|
8
|
-
interface NavigationScript {
|
|
9
|
-
attributes: { name: string; value: string }[];
|
|
10
|
-
content: string;
|
|
11
|
-
source?: string;
|
|
12
|
-
type: string;
|
|
13
|
-
key: string;
|
|
14
|
-
rerun: boolean;
|
|
15
|
-
head: boolean;
|
|
16
|
-
original: HTMLScriptElement;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const JAVASCRIPT_TYPES = new Set([
|
|
20
|
-
"",
|
|
21
|
-
"module",
|
|
22
|
-
"text/javascript",
|
|
23
|
-
"application/javascript",
|
|
24
|
-
"text/ecmascript",
|
|
25
|
-
"application/ecmascript",
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
function describeScript(script: HTMLScriptElement, base: string): NavigationScript | undefined {
|
|
29
|
-
const type = (script.getAttribute("type") ?? "").trim().toLowerCase();
|
|
30
|
-
if (!JAVASCRIPT_TYPES.has(type) || script.hasAttribute("data-swup-ignore-script")) return;
|
|
31
|
-
if (script.hasAttribute("nomodule") && "noModule" in document.createElement("script")) return;
|
|
32
|
-
const rawSource = script.getAttribute("src");
|
|
33
|
-
let source: string | undefined;
|
|
34
|
-
if (rawSource !== null) {
|
|
35
|
-
try {
|
|
36
|
-
source = new URL(rawSource, base).href;
|
|
37
|
-
} catch {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const content = script.textContent ?? "";
|
|
42
|
-
return {
|
|
43
|
-
attributes: [...script.attributes].map(({ name, value }) => ({ name, value })),
|
|
44
|
-
content,
|
|
45
|
-
source,
|
|
46
|
-
type,
|
|
47
|
-
key: source ? `src:${source}` : `inline:${type}:${content}`,
|
|
48
|
-
rerun: script.hasAttribute("data-astro-rerun"),
|
|
49
|
-
head: script.parentElement?.tagName === "HEAD",
|
|
50
|
-
original: script,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function seedCurrentScripts(): void {
|
|
55
|
-
if (seeded) return;
|
|
56
|
-
seeded = true;
|
|
57
|
-
for (const script of document.querySelectorAll<HTMLScriptElement>("script")) {
|
|
58
|
-
const descriptor = describeScript(script, document.baseURI);
|
|
59
|
-
if (descriptor) {
|
|
60
|
-
executions.set(descriptor.key, Promise.resolve());
|
|
61
|
-
executedNodes.add(script);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function waitForScript(script: HTMLScriptElement, timeoutMs: number): Promise<boolean> {
|
|
67
|
-
return new Promise((resolve) => {
|
|
68
|
-
let timer: ReturnType<typeof setTimeout>;
|
|
69
|
-
const finish = (loaded: boolean) => {
|
|
70
|
-
clearTimeout(timer);
|
|
71
|
-
script.removeEventListener("load", loadedHandler);
|
|
72
|
-
script.removeEventListener("error", failedHandler);
|
|
73
|
-
resolve(loaded);
|
|
74
|
-
};
|
|
75
|
-
const loadedHandler = () => finish(true);
|
|
76
|
-
const failedHandler = () => finish(false);
|
|
77
|
-
script.addEventListener("load", loadedHandler, { once: true });
|
|
78
|
-
script.addEventListener("error", failedHandler, { once: true });
|
|
79
|
-
timer = setTimeout(failedHandler, timeoutMs);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
7
|
+
const executedNodes = new WeakSet<HTMLScriptElement>();
|
|
82
8
|
|
|
83
9
|
function abortable(promise: Promise<void>, signal?: AbortSignal): Promise<void> {
|
|
84
10
|
if (!signal) return promise;
|
|
@@ -92,100 +18,141 @@ function abortable(promise: Promise<void>, signal?: AbortSignal): Promise<void>
|
|
|
92
18
|
void promise.then(finish, finish);
|
|
93
19
|
});
|
|
94
20
|
}
|
|
21
|
+
const javascript = new Set([
|
|
22
|
+
"",
|
|
23
|
+
"module",
|
|
24
|
+
"text/javascript",
|
|
25
|
+
"application/javascript",
|
|
26
|
+
"text/ecmascript",
|
|
27
|
+
"application/ecmascript",
|
|
28
|
+
]);
|
|
95
29
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
describeScript(candidate, document.baseURI)?.key === descriptor.key,
|
|
104
|
-
);
|
|
105
|
-
const existing = executions.get(descriptor.key);
|
|
106
|
-
if (existing && !descriptor.rerun) {
|
|
107
|
-
for (const copy of inertCopies) copy.remove();
|
|
108
|
-
return existing;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const script = document.createElement("script");
|
|
112
|
-
// Dynamic scripts are async by default. Preserve document order, including
|
|
113
|
-
// the module barrier used below, before copying the page's attributes.
|
|
114
|
-
script.async = false;
|
|
115
|
-
for (const { name, value } of descriptor.attributes) {
|
|
116
|
-
if (name !== "src" && name !== "data-astro-exec") script.setAttribute(name, value);
|
|
117
|
-
}
|
|
118
|
-
if (descriptor.source) script.setAttribute("src", descriptor.source);
|
|
119
|
-
script.textContent = descriptor.content;
|
|
120
|
-
script.setAttribute("data-astro-exec", "");
|
|
121
|
-
executedNodes.add(script);
|
|
122
|
-
|
|
123
|
-
let observed = script;
|
|
124
|
-
if (!descriptor.source && descriptor.type === "module") {
|
|
125
|
-
// Inline module load events are not portable. Astro uses the same
|
|
126
|
-
// ordered external module barrier to wait for deferred inline modules.
|
|
127
|
-
observed = document.createElement("script");
|
|
128
|
-
observed.type = "module";
|
|
129
|
-
observed.async = false;
|
|
130
|
-
observed.src = "data:application/javascript,";
|
|
131
|
-
observed.setAttribute("data-swup-ignore-script", "");
|
|
132
|
-
if (script.nonce) observed.nonce = script.nonce;
|
|
133
|
-
}
|
|
134
|
-
const loaded =
|
|
135
|
-
descriptor.source || descriptor.type === "module"
|
|
136
|
-
? waitForScript(observed, timeoutMs)
|
|
137
|
-
: Promise.resolve(true);
|
|
138
|
-
const execution = loaded.then((success) => {
|
|
139
|
-
if (observed !== script) observed.remove();
|
|
140
|
-
if (!success) {
|
|
141
|
-
// Failed fetches may succeed on a subsequent visit. Do not turn a
|
|
142
|
-
// transient offline response into a session-long disabled feature.
|
|
143
|
-
if (executions.get(descriptor.key) === execution) executions.delete(descriptor.key);
|
|
144
|
-
script.remove();
|
|
145
|
-
logger.warn(`page script failed to load: ${descriptor.source ?? "inline module"}`);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
executions.set(descriptor.key, execution);
|
|
30
|
+
/**
|
|
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.
|
|
35
|
+
*/
|
|
36
|
+
export const navigationScripts = new SwupScriptsPlugin({ head: false, body: false });
|
|
149
37
|
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
38
|
+
function describeScript(script: HTMLScriptElement, base: string) {
|
|
39
|
+
const type = (script.getAttribute("type") ?? "").trim().toLowerCase();
|
|
40
|
+
if (!javascript.has(type) || script.hasAttribute("data-swup-ignore-script")) return;
|
|
41
|
+
if (script.hasAttribute("nomodule")) return;
|
|
42
|
+
const source = script.getAttribute("src");
|
|
43
|
+
const src = source === null ? undefined : new URL(source, base).href;
|
|
44
|
+
return {
|
|
45
|
+
script,
|
|
46
|
+
src,
|
|
47
|
+
type,
|
|
48
|
+
key: src ? `src:${src}` : `inline:${type}:${script.textContent ?? ""}`,
|
|
49
|
+
rerun:
|
|
50
|
+
script.hasAttribute("data-astro-rerun") ||
|
|
51
|
+
script.hasAttribute("data-swup-reload-script"),
|
|
52
|
+
head: script.parentElement?.tagName === "HEAD",
|
|
53
|
+
};
|
|
163
54
|
}
|
|
164
55
|
|
|
165
|
-
/**
|
|
166
|
-
* Capture both head and body before Swup replaces the document's containers.
|
|
167
|
-
* Call the returned function after replacement, before astro:page-load. Bundled
|
|
168
|
-
* modules and ordinary inline scripts retain Astro's once-per-session behavior;
|
|
169
|
-
* data-astro-rerun opts a script into every visit. SDK scripts already present in
|
|
170
|
-
* the first document are never reinserted merely because head metadata changed.
|
|
171
|
-
*/
|
|
56
|
+
/** Capture before Parallel moves incoming containers out of the parsed document. */
|
|
172
57
|
export function prepareNavigationScripts(
|
|
173
|
-
|
|
58
|
+
next: Document,
|
|
174
59
|
options: { timeoutMs?: number; baseUrl?: string } = {},
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
60
|
+
) {
|
|
61
|
+
if (!seeded) {
|
|
62
|
+
seeded = true;
|
|
63
|
+
for (const script of document.querySelectorAll<HTMLScriptElement>("script")) {
|
|
64
|
+
const item = describeScript(script, document.baseURI);
|
|
65
|
+
if (item) {
|
|
66
|
+
executions.set(item.key, Promise.resolve());
|
|
67
|
+
executedNodes.add(script);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const scripts = [...next.querySelectorAll<HTMLScriptElement>("script")]
|
|
72
|
+
.map((script) =>
|
|
73
|
+
describeScript(script, options.baseUrl ?? next.baseURI ?? document.baseURI),
|
|
74
|
+
)
|
|
75
|
+
.filter((item) => item !== undefined)
|
|
76
|
+
.map((item) => ({
|
|
77
|
+
...item,
|
|
78
|
+
original: item.script,
|
|
79
|
+
script: item.script.cloneNode(true) as HTMLScriptElement,
|
|
80
|
+
}));
|
|
81
|
+
let execution: Promise<void> | undefined;
|
|
82
|
+
return (signal?: AbortSignal): Promise<void> => {
|
|
83
|
+
execution ??= (async () => {
|
|
84
|
+
for (const item of scripts) {
|
|
185
85
|
if (signal?.aborted) return;
|
|
186
|
-
|
|
86
|
+
const existing = executions.get(item.key);
|
|
87
|
+
if (existing && !item.rerun) {
|
|
88
|
+
for (const copy of document.querySelectorAll<HTMLScriptElement>("script")) {
|
|
89
|
+
if (
|
|
90
|
+
!executedNodes.has(copy) &&
|
|
91
|
+
describeScript(copy, document.baseURI)?.key === item.key
|
|
92
|
+
)
|
|
93
|
+
copy.remove();
|
|
94
|
+
}
|
|
95
|
+
await abortable(existing, signal);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const inert = item.script.cloneNode(true) as HTMLScriptElement;
|
|
99
|
+
if (item.src) inert.setAttribute("src", item.src);
|
|
100
|
+
inert.async = false;
|
|
101
|
+
inert.setAttribute("data-astro-exec", "");
|
|
102
|
+
// Reuse HeadPlugin's inert clone, or the inserted body script.
|
|
103
|
+
// Scripts outside replaced fragments are staged only if new.
|
|
104
|
+
const copy =
|
|
105
|
+
(item.original.isConnected && item.original.ownerDocument === document
|
|
106
|
+
? item.original
|
|
107
|
+
: undefined) ??
|
|
108
|
+
[...document.querySelectorAll<HTMLScriptElement>("script")].find(
|
|
109
|
+
(script) =>
|
|
110
|
+
!executedNodes.has(script) &&
|
|
111
|
+
describeScript(script, document.baseURI)?.key === item.key,
|
|
112
|
+
);
|
|
113
|
+
if (copy) copy.replaceWith(inert);
|
|
114
|
+
else (item.head ? document.head : document.body).append(inert);
|
|
115
|
+
const script = navigationScripts.runScript(inert);
|
|
116
|
+
script.async = false;
|
|
117
|
+
executedNodes.add(script);
|
|
118
|
+
let observed = script;
|
|
119
|
+
if (!item.src && item.type === "module") {
|
|
120
|
+
observed = document.createElement("script");
|
|
121
|
+
observed.type = "module";
|
|
122
|
+
observed.async = false;
|
|
123
|
+
observed.src = "data:application/javascript,";
|
|
124
|
+
observed.setAttribute("data-swup-ignore-script", "");
|
|
125
|
+
if (script.nonce) observed.nonce = script.nonce;
|
|
126
|
+
document.body.append(observed);
|
|
127
|
+
}
|
|
128
|
+
const ready =
|
|
129
|
+
item.src || item.type === "module"
|
|
130
|
+
? new Promise<void>((resolve) => {
|
|
131
|
+
const finish = (failed = false) => {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
observed.removeEventListener("load", loaded);
|
|
134
|
+
observed.removeEventListener("error", failedHandler);
|
|
135
|
+
if (observed !== script) observed.remove();
|
|
136
|
+
if (failed) {
|
|
137
|
+
executions.delete(item.key);
|
|
138
|
+
script.remove();
|
|
139
|
+
logger.warn(
|
|
140
|
+
`page script failed to load: ${item.src ?? "inline module"}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
resolve();
|
|
144
|
+
};
|
|
145
|
+
const loaded = () => finish();
|
|
146
|
+
const failedHandler = () => finish(true);
|
|
147
|
+
const timer = setTimeout(failedHandler, options.timeoutMs ?? 15_000);
|
|
148
|
+
observed.addEventListener("load", loaded, { once: true });
|
|
149
|
+
observed.addEventListener("error", failedHandler, { once: true });
|
|
150
|
+
})
|
|
151
|
+
: Promise.resolve();
|
|
152
|
+
executions.set(item.key, ready);
|
|
153
|
+
await abortable(ready, signal);
|
|
187
154
|
}
|
|
188
155
|
})();
|
|
189
|
-
return
|
|
156
|
+
return execution;
|
|
190
157
|
};
|
|
191
158
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import SwupA11yPlugin from "@swup/a11y-plugin";
|
|
2
|
+
import SwupBodyClassPlugin from "@swup/body-class-plugin";
|
|
3
|
+
import SwupFragmentPlugin from "@swup/fragment-plugin";
|
|
2
4
|
import SwupHeadPlugin from "@swup/head-plugin";
|
|
3
5
|
import SwupJsPlugin from "@swup/js-plugin";
|
|
6
|
+
import SwupParallelPlugin from "@swup/parallel-plugin";
|
|
4
7
|
import SwupPreloadPlugin from "@swup/preload-plugin";
|
|
8
|
+
import SwupProgressPlugin from "@swup/progress-plugin";
|
|
9
|
+
import SwupRouteNamePlugin from "@swup/route-name-plugin";
|
|
5
10
|
import SwupScrollPlugin from "@swup/scroll-plugin";
|
|
6
11
|
import Swup, { type Visit } from "swup";
|
|
7
12
|
import { getRouteKind } from "../utils/public-routes";
|
|
@@ -9,7 +14,7 @@ import { getRouteMotionPreset, type RouteMotionPreset } from "../utils/route-mot
|
|
|
9
14
|
import { installBackToTop } from "./back-to-top";
|
|
10
15
|
import { prepareNavigationFont } from "./navigation-font";
|
|
11
16
|
import "./navigation-client";
|
|
12
|
-
import { prepareNavigationScripts } from "./navigation-scripts";
|
|
17
|
+
import { navigationScripts, prepareNavigationScripts } from "./navigation-scripts";
|
|
13
18
|
import { prepareRouteMotion, type RouteMotion } from "./route-motion";
|
|
14
19
|
import { animateRouteOut } from "./route-out";
|
|
15
20
|
import { hydrateShells, markRetainedShells } from "./shell-hydration";
|
|
@@ -27,13 +32,16 @@ interface NavigationContext {
|
|
|
27
32
|
motion?: RouteMotion;
|
|
28
33
|
executeScripts?: (signal?: AbortSignal) => Promise<void>;
|
|
29
34
|
scriptsReady?: Promise<void>;
|
|
35
|
+
outgoingReady?: Promise<void>;
|
|
36
|
+
incomingDocument?: Document;
|
|
37
|
+
removeOutgoing?: () => void;
|
|
30
38
|
obsoleteStyles: Element[];
|
|
31
39
|
glide?: PageGlide;
|
|
32
40
|
releaseOutgoing?: () => void;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
type Position = { top: number; left: number; focus?: string };
|
|
36
|
-
const MAIN = "main
|
|
44
|
+
const MAIN = "#swup-main";
|
|
37
45
|
|
|
38
46
|
function abortObsoleteVisit(visit: Visit): void {
|
|
39
47
|
// `abort()` is @internal in swup 4.10.0 and stripped from the published
|
|
@@ -82,6 +90,7 @@ export function installSwupRouter(): Swup {
|
|
|
82
90
|
let activeContext: NavigationContext | undefined;
|
|
83
91
|
let visibleHistoryIndex = Number(window.history.state?.index ?? 1);
|
|
84
92
|
let browserMotion = false;
|
|
93
|
+
const progress = new SwupProgressPlugin();
|
|
85
94
|
const scroll = new SwupScrollPlugin({
|
|
86
95
|
doScrollingRightAway: false,
|
|
87
96
|
animateScroll: { betweenPages: false, samePageWithHash: true, samePage: true },
|
|
@@ -117,6 +126,42 @@ export function installSwupRouter(): Swup {
|
|
|
117
126
|
);
|
|
118
127
|
},
|
|
119
128
|
plugins: [
|
|
129
|
+
navigationScripts,
|
|
130
|
+
new SwupBodyClassPlugin({
|
|
131
|
+
prefix: "page-",
|
|
132
|
+
attributes: [/^data-stalux-(?!bg-)/u],
|
|
133
|
+
}),
|
|
134
|
+
new SwupRouteNamePlugin({
|
|
135
|
+
routes: [
|
|
136
|
+
{ name: "home", path: "/" },
|
|
137
|
+
{ name: "article", path: "/posts/:post" },
|
|
138
|
+
{ name: "archive", path: "/archives" },
|
|
139
|
+
{ name: "tags-index", path: "/tags" },
|
|
140
|
+
{ name: "tags-detail", path: "/tags/:tag" },
|
|
141
|
+
{ name: "categories-index", path: "/categories" },
|
|
142
|
+
{ name: "categories-detail", path: "/categories/:category" },
|
|
143
|
+
...["about", "links", "words"].map((name) => ({ name, path: "/" + name })),
|
|
144
|
+
{ name: "not-found", path: "/404" },
|
|
145
|
+
],
|
|
146
|
+
}),
|
|
147
|
+
new SwupFragmentPlugin({
|
|
148
|
+
rules: [
|
|
149
|
+
{
|
|
150
|
+
from: ["/tags/:slug", "/categories/:slug"],
|
|
151
|
+
to: ["/tags/:slug", "/categories/:slug"],
|
|
152
|
+
containers: ["#swup-taxonomy"],
|
|
153
|
+
scroll: true,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
from: "/posts/:post",
|
|
157
|
+
to: "/posts/:post",
|
|
158
|
+
containers: ["#swup-article"],
|
|
159
|
+
scroll: true,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
}),
|
|
163
|
+
new SwupParallelPlugin({ containers: [MAIN, "#swup-article", "#swup-taxonomy"] }),
|
|
164
|
+
progress,
|
|
120
165
|
new SwupHeadPlugin({
|
|
121
166
|
awaitAssets: true,
|
|
122
167
|
// Keep old CSS readable until replacement, then prune authored
|
|
@@ -151,13 +196,18 @@ export function installSwupRouter(): Swup {
|
|
|
151
196
|
releaseOutgoing: context.releaseOutgoing,
|
|
152
197
|
},
|
|
153
198
|
);
|
|
154
|
-
await context.motion.finished;
|
|
199
|
+
await Promise.all([context.motion.finished, context.outgoingReady]);
|
|
155
200
|
},
|
|
156
201
|
},
|
|
157
202
|
],
|
|
158
203
|
}),
|
|
159
204
|
],
|
|
160
205
|
});
|
|
206
|
+
if (import.meta.env.DEV) {
|
|
207
|
+
void import("virtual:stalux-swup-debug").then(({ default: Debug }) => {
|
|
208
|
+
if (Debug) swup.use(new Debug());
|
|
209
|
+
});
|
|
210
|
+
}
|
|
161
211
|
window.swup = swup;
|
|
162
212
|
document.documentElement.dataset.staluxRouter = "swup";
|
|
163
213
|
document.documentElement.dataset.staluxRoute = getRouteKind(window.location.pathname);
|
|
@@ -219,6 +269,7 @@ export function installSwupRouter(): Swup {
|
|
|
219
269
|
contexts.set(visit, context);
|
|
220
270
|
activeContext = context;
|
|
221
271
|
visit.animation.wait = true;
|
|
272
|
+
if (context.preset === "none" || visit.history.popstate) visit.animation.parallel = false;
|
|
222
273
|
visit.a11y.focus = visit.history.popstate ? false : { selector: MAIN, wait: true };
|
|
223
274
|
if (dispatchNavigation("astro:before-preparation", context).defaultPrevented)
|
|
224
275
|
visit.ignore();
|
|
@@ -232,6 +283,52 @@ export function installSwupRouter(): Swup {
|
|
|
232
283
|
abortObsoleteVisit(visit);
|
|
233
284
|
});
|
|
234
285
|
|
|
286
|
+
swup.hooks.before("content:insert", (visit, { containers }) => {
|
|
287
|
+
const context = contexts.get(visit);
|
|
288
|
+
if (!context) return;
|
|
289
|
+
context.motion = prepareRouteMotion(
|
|
290
|
+
dispatchNavigation("astro:before-swap", context, context.incomingDocument),
|
|
291
|
+
);
|
|
292
|
+
markRetainedShells();
|
|
293
|
+
const animations: Animation[] = [];
|
|
294
|
+
for (const { previous } of containers) {
|
|
295
|
+
const rect = previous.getBoundingClientRect();
|
|
296
|
+
previous.inert = true;
|
|
297
|
+
// The incoming document owns IDs and focus as soon as it is inserted.
|
|
298
|
+
// Persisted shells are moved by Swup and retain their original IDs.
|
|
299
|
+
for (const element of [previous, ...previous.querySelectorAll("[id]")]) {
|
|
300
|
+
if (!element.closest("[data-swup-persist]")) element.removeAttribute("id");
|
|
301
|
+
}
|
|
302
|
+
Object.assign(previous.style, {
|
|
303
|
+
position: "fixed",
|
|
304
|
+
top: rect.top + "px",
|
|
305
|
+
left: rect.left + "px",
|
|
306
|
+
width: rect.width + "px",
|
|
307
|
+
height: rect.height + "px",
|
|
308
|
+
// Clip the visible slice without truncating a deeply scrolled article.
|
|
309
|
+
clipPath: `inset(${Math.max(0, -rect.top)}px 0 ${Math.max(0, rect.bottom - window.innerHeight)}px 0)`,
|
|
310
|
+
overflow: "hidden",
|
|
311
|
+
pointerEvents: "none",
|
|
312
|
+
zIndex: "1",
|
|
313
|
+
});
|
|
314
|
+
animations.push(
|
|
315
|
+
previous.animate(
|
|
316
|
+
{ opacity: [1, 0], transform: ["translateY(0)", "translateY(-12px)"] },
|
|
317
|
+
{ duration: 160, easing: "ease-out", fill: "forwards" },
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const remove = () => {
|
|
322
|
+
for (const animation of animations) animation.cancel();
|
|
323
|
+
for (const { previous } of containers) previous.remove();
|
|
324
|
+
};
|
|
325
|
+
context.removeOutgoing = once(remove);
|
|
326
|
+
context.controller.signal.addEventListener("abort", context.removeOutgoing, { once: true });
|
|
327
|
+
context.outgoingReady = Promise.all(
|
|
328
|
+
animations.map((animation) => animation.finished.catch(() => {})),
|
|
329
|
+
).then(() => context.removeOutgoing?.());
|
|
330
|
+
});
|
|
331
|
+
|
|
235
332
|
swup.hooks.before(
|
|
236
333
|
"content:replace",
|
|
237
334
|
async (visit) => {
|
|
@@ -250,6 +347,7 @@ export function installSwupRouter(): Swup {
|
|
|
250
347
|
abortObsoleteVisit(visit);
|
|
251
348
|
return;
|
|
252
349
|
}
|
|
350
|
+
context.incomingDocument = next.cloneNode(true) as Document;
|
|
253
351
|
const incoming = [...next.head.querySelectorAll("style,link[rel=stylesheet]")];
|
|
254
352
|
context.obsoleteStyles = authoredStyles.filter(
|
|
255
353
|
(el) => !incoming.some((other) => el.isEqualNode(other)),
|
|
@@ -273,7 +371,11 @@ export function installSwupRouter(): Swup {
|
|
|
273
371
|
// Two cues overlap on the outgoing document: the content stage leaving,
|
|
274
372
|
// and the page walking back to the top. Persisted sidebars, the header
|
|
275
373
|
// and the frame are never part of either one.
|
|
276
|
-
if (
|
|
374
|
+
if (
|
|
375
|
+
!visit.animation.parallel &&
|
|
376
|
+
context.navigationType !== "traverse" &&
|
|
377
|
+
context.preset !== "none"
|
|
378
|
+
) {
|
|
277
379
|
const outgoing = animateRouteOut(context.preset, context.controller.signal);
|
|
278
380
|
if (!context.to.hash) context.glide = glideToTop();
|
|
279
381
|
await context.glide?.promise;
|
|
@@ -288,8 +390,12 @@ export function installSwupRouter(): Swup {
|
|
|
288
390
|
html.dataset.staluxVtTo = getRouteKind(context.to.pathname);
|
|
289
391
|
html.dataset.staluxVtDirection = context.direction;
|
|
290
392
|
html.dataset.staluxVtPreset = context.preset;
|
|
291
|
-
context.motion = prepareRouteMotion(dispatchNavigation("astro:before-swap", context, next));
|
|
292
393
|
if (context.controller.signal.aborted) return;
|
|
394
|
+
if (!visit.animation.parallel) {
|
|
395
|
+
context.motion = prepareRouteMotion(
|
|
396
|
+
dispatchNavigation("astro:before-swap", context, next),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
293
399
|
// Remember which shells exist now: after the swap only those slots need
|
|
294
400
|
// refilling, because only those nodes came from the previous document.
|
|
295
401
|
markRetainedShells();
|
|
@@ -306,20 +412,13 @@ export function installSwupRouter(): Swup {
|
|
|
306
412
|
);
|
|
307
413
|
// Main alone is replaced: header/footer/sentinel and background layers
|
|
308
414
|
// keep their node identities. Never reset live background state to SSR 0.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
name.startsWith("data-stalux-") &&
|
|
312
|
-
name !== "data-stalux-bg-index" &&
|
|
313
|
-
name !== "data-stalux-bg-layer"
|
|
314
|
-
)
|
|
315
|
-
document.body.setAttribute(name, value);
|
|
316
|
-
}
|
|
317
|
-
html.dataset.staluxRoute = getRouteKind(context.to.pathname);
|
|
415
|
+
html.dataset.staluxRoute = visit.to.route ?? getRouteKind(context.to.pathname);
|
|
416
|
+
document.querySelector(MAIN)?.setAttribute("data-stalux-route", html.dataset.staluxRoute);
|
|
318
417
|
visibleHistoryIndex = Number(window.history.state?.index ?? visibleHistoryIndex);
|
|
319
418
|
// Kept shells whose contents belong to the page (the article sidebars'
|
|
320
419
|
// lists) take their children from the incoming document now, before any
|
|
321
420
|
// page module mounts, so nothing needs to re-render or re-measure later.
|
|
322
|
-
hydrateShells(next);
|
|
421
|
+
hydrateShells(context.incomingDocument ?? next);
|
|
323
422
|
dispatchNavigation("astro:after-swap", context, next);
|
|
324
423
|
// The entry cue has to own the first painted frame of the new document.
|
|
325
424
|
// content:scroll is an await and a rAF away, and running the incoming
|
|
@@ -375,7 +474,8 @@ export function installSwupRouter(): Swup {
|
|
|
375
474
|
}
|
|
376
475
|
await defaultScroll?.(visit, args);
|
|
377
476
|
// ScrollPlugin scrolls in rAF; finish it before heading visibility checks.
|
|
378
|
-
|
|
477
|
+
if (!document.hidden)
|
|
478
|
+
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
379
479
|
startMotion(visit);
|
|
380
480
|
return true;
|
|
381
481
|
},
|
|
@@ -407,8 +507,12 @@ export function installSwupRouter(): Swup {
|
|
|
407
507
|
swup.hooks.replace("link:self", () => undefined);
|
|
408
508
|
|
|
409
509
|
const cancel = (visit: Visit) => {
|
|
510
|
+
// A cancelled/failed visit never reaches page:view. Clear its delayed
|
|
511
|
+
// indicator without allowing a stale visit to hide a newer one.
|
|
512
|
+
if (visit === swup.visit) progress.stopShowingProgress();
|
|
410
513
|
const context = contexts.get(visit);
|
|
411
514
|
context?.controller.abort();
|
|
515
|
+
context?.removeOutgoing?.();
|
|
412
516
|
context?.glide?.cancel();
|
|
413
517
|
// An aborted visit keeps no exit fill: without this the stage would stay
|
|
414
518
|
// transparent until the next successful navigation.
|
|
@@ -172,3 +172,16 @@ pagefind-modal dialog::backdrop {
|
|
|
172
172
|
animation: none !important;
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
+
|
|
176
|
+
/* The official plugin prepends its defaults; keep theme color authoritative. */
|
|
177
|
+
html .swup-progress-bar {
|
|
178
|
+
height: 3px;
|
|
179
|
+
background-color: var(--stalux-accent-color);
|
|
180
|
+
pointer-events: none;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@media (prefers-reduced-motion: reduce) {
|
|
184
|
+
html .swup-progress-bar {
|
|
185
|
+
transition: none;
|
|
186
|
+
}
|
|
187
|
+
}
|