@xingwangzhe/stalux 1.56.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 +4 -12
- package/src/components/stalux/archives/archivesList.astro +1 -1
- package/src/components/stalux/categories/categoriesCard.astro +1 -1
- package/src/components/stalux/posts/PostTaxonomy.astro +1 -1
- package/src/components/stalux/posts/postCard.astro +3 -3
- package/src/components/stalux/tags/tagHeader.astro +2 -2
- package/src/components/stalux/tags/tagsCloud.astro +2 -2
- package/src/index.ts +39 -13
- package/src/internal/html-output.ts +18 -0
- package/src/internal/modern-css.ts +1 -0
- package/src/internal/route-hrefs.ts +47 -0
- package/src/internal/social-card-data.ts +1 -1
- package/src/internal/style-names.generated.ts +87 -94
- package/src/internal/swup-options.ts +98 -0
- package/src/layouts/Stalux.astro +0 -1
- package/src/pages/404.astro +1 -1
- package/src/pages/api/posts.json.ts +1 -1
- package/src/pages/atom.xml.ts +2 -0
- package/src/pages/posts/[post].md.ts +2 -2
- package/src/pages/rss.xml.ts +2 -0
- package/src/scripts/analytics-runtime.ts +4 -9
- package/src/scripts/list-reveal.ts +3 -15
- package/src/scripts/module-motion.ts +2 -3
- package/src/scripts/navigation-scripts.ts +12 -7
- package/src/scripts/route-guards.ts +200 -0
- package/src/scripts/swup-internals.d.ts +15 -0
- package/src/styles/base.css +0 -8
- package/src/styles/generated-prose.css +1 -1
- package/src/styles/generated-waline.css +1 -1
- package/src/styles/generated.css +1 -1
- package/src/styles/integrations.css +14 -16
- package/src/styles/math.css +3 -1
- package/src/styles/motion.css +1 -22
- package/src/utils/accent-color.ts +52 -0
- package/src/utils/ai-discovery.ts +1 -1
- package/src/utils/feed.ts +1 -1
- package/src/utils/public-routes.ts +14 -4
- package/src/utils/seo-document.ts +3 -2
- package/src/scripts/route-motion.ts +0 -340
- package/src/scripts/route-out.ts +0 -63
- package/src/scripts/smooth-scroll.ts +0 -139
- package/src/scripts/swup-router.ts +0 -580
- package/src/scripts/view-transitions.ts +0 -4
- package/src/utils/route-motion-policy.ts +0 -72
|
@@ -1,580 +0,0 @@
|
|
|
1
|
-
import SwupA11yPlugin from "@swup/a11y-plugin";
|
|
2
|
-
import SwupBodyClassPlugin from "@swup/body-class-plugin";
|
|
3
|
-
import SwupFragmentPlugin from "@swup/fragment-plugin";
|
|
4
|
-
import SwupHeadPlugin from "@swup/head-plugin";
|
|
5
|
-
import SwupJsPlugin from "@swup/js-plugin";
|
|
6
|
-
import SwupParallelPlugin from "@swup/parallel-plugin";
|
|
7
|
-
import SwupPreloadPlugin from "@swup/preload-plugin";
|
|
8
|
-
import SwupProgressPlugin from "@swup/progress-plugin";
|
|
9
|
-
import SwupRouteNamePlugin from "@swup/route-name-plugin";
|
|
10
|
-
import SwupScrollPlugin from "@swup/scroll-plugin";
|
|
11
|
-
import Swup, { type Visit } from "swup";
|
|
12
|
-
import { getRouteKind } from "../utils/public-routes";
|
|
13
|
-
import { getRouteMotionPreset, type RouteMotionPreset } from "../utils/route-motion-policy";
|
|
14
|
-
import { installBackToTop } from "./back-to-top";
|
|
15
|
-
import { prepareNavigationFont } from "./navigation-font";
|
|
16
|
-
import "./navigation-client";
|
|
17
|
-
import { navigationScripts, prepareNavigationScripts } from "./navigation-scripts";
|
|
18
|
-
import { prepareRouteMotion, type RouteMotion } from "./route-motion";
|
|
19
|
-
import { animateRouteOut } from "./route-out";
|
|
20
|
-
import { hydrateShells, markRetainedShells } from "./shell-hydration";
|
|
21
|
-
import { cancelPageScroll, glideToTop, type PageGlide, scrollPage } from "./smooth-scroll";
|
|
22
|
-
|
|
23
|
-
interface NavigationContext {
|
|
24
|
-
controller: AbortController;
|
|
25
|
-
from: URL;
|
|
26
|
-
to: URL;
|
|
27
|
-
direction: string;
|
|
28
|
-
navigationType: "push" | "replace" | "traverse";
|
|
29
|
-
sourceElement?: Element;
|
|
30
|
-
preset: RouteMotionPreset;
|
|
31
|
-
browserMotion: boolean;
|
|
32
|
-
motion?: RouteMotion;
|
|
33
|
-
executeScripts?: (signal?: AbortSignal) => Promise<void>;
|
|
34
|
-
scriptsReady?: Promise<void>;
|
|
35
|
-
outgoingReady?: Promise<void>;
|
|
36
|
-
incomingDocument?: Document;
|
|
37
|
-
removeOutgoing?: () => void;
|
|
38
|
-
obsoleteStyles: Element[];
|
|
39
|
-
pruneObsoleteStyles?: () => void;
|
|
40
|
-
glide?: PageGlide;
|
|
41
|
-
releaseOutgoing?: () => void;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type Position = { top: number; left: number; focus?: string };
|
|
45
|
-
const MAIN = "#swup-main";
|
|
46
|
-
|
|
47
|
-
function abortObsoleteVisit(visit: Visit): void {
|
|
48
|
-
// `abort()` is @internal in swup 4.10.0 and stripped from the published
|
|
49
|
-
// types; swup-internals.d.ts restores the declaration, so this is checked
|
|
50
|
-
// like any other member. The guard keeps a future removal from throwing in
|
|
51
|
-
// the middle of a navigation: every caller returns right after this anyway.
|
|
52
|
-
if (typeof visit.abort === "function") visit.abort();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function dispatchNavigation(name: string, context: NavigationContext, nextDocument?: Document) {
|
|
56
|
-
const event = Object.assign(new Event(name, { cancelable: true }), {
|
|
57
|
-
from: context.from,
|
|
58
|
-
to: context.to,
|
|
59
|
-
direction: context.direction,
|
|
60
|
-
navigationType: context.navigationType,
|
|
61
|
-
sourceElement: context.sourceElement,
|
|
62
|
-
signal: context.controller.signal,
|
|
63
|
-
...(nextDocument ? { newDocument: nextDocument } : {}),
|
|
64
|
-
});
|
|
65
|
-
document.dispatchEvent(event);
|
|
66
|
-
return event;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function clearMotionState(): void {
|
|
70
|
-
delete document.documentElement.dataset.staluxMotionPending;
|
|
71
|
-
for (const key of ["staluxVtFrom", "staluxVtTo", "staluxVtDirection", "staluxVtPreset"])
|
|
72
|
-
delete document.documentElement.dataset[key];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** The exit fill is released by whichever path finishes first; never twice. */
|
|
76
|
-
function once(effect: () => void): () => void {
|
|
77
|
-
let done = false;
|
|
78
|
-
return () => {
|
|
79
|
-
if (done) return;
|
|
80
|
-
done = true;
|
|
81
|
-
effect();
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Astro renders pages; Swup alone owns their client navigation and animation. */
|
|
86
|
-
export function installSwupRouter(): Swup {
|
|
87
|
-
if (window.swup) return window.swup;
|
|
88
|
-
const contexts = new WeakMap<Visit, NavigationContext>();
|
|
89
|
-
const historyPositions = new Map<number, Position>();
|
|
90
|
-
let authoredStyles = [...document.head.querySelectorAll("style,link[rel=stylesheet]")];
|
|
91
|
-
let activeContext: NavigationContext | undefined;
|
|
92
|
-
let visibleHistoryIndex = Number(window.history.state?.index ?? 1);
|
|
93
|
-
let browserMotion = false;
|
|
94
|
-
// Swup waits for the target document before it starts any animation, so a
|
|
95
|
-
// slow fetch is a window with no visual response at all. The official
|
|
96
|
-
// plugin waits 300ms before showing its bar, which leaves most of that
|
|
97
|
-
// window empty; 80ms keeps a cached/preloaded visit invisible (it finishes
|
|
98
|
-
// first) while a real network round trip gets an immediate, theme-coloured
|
|
99
|
-
// cue. The bar itself is compositor-only: a 3px transform on one element.
|
|
100
|
-
const progress = new SwupProgressPlugin({ delay: 80, transition: 200 });
|
|
101
|
-
const scroll = new SwupScrollPlugin({
|
|
102
|
-
doScrollingRightAway: false,
|
|
103
|
-
animateScroll: { betweenPages: false, samePageWithHash: true, samePage: true },
|
|
104
|
-
scrollFunction: scrollPage,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
window.addEventListener(
|
|
108
|
-
"popstate",
|
|
109
|
-
(event) => {
|
|
110
|
-
// hasUAVisualTransition is part of the DOM lib's PopStateEvent, so
|
|
111
|
-
// this reads the event directly; the listener is typed for popstate.
|
|
112
|
-
browserMotion = event.hasUAVisualTransition === true;
|
|
113
|
-
},
|
|
114
|
-
{ capture: true },
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
const swup = new Swup({
|
|
118
|
-
native: false,
|
|
119
|
-
containers: [MAIN],
|
|
120
|
-
animationSelector: false,
|
|
121
|
-
animateHistoryBrowsing: true,
|
|
122
|
-
timeout: 15_000,
|
|
123
|
-
ignoreVisit: (href, { el }) => {
|
|
124
|
-
const url = new URL(href, window.location.href);
|
|
125
|
-
const target =
|
|
126
|
-
el?.getAttribute("target") ??
|
|
127
|
-
document.querySelector("base[target]")?.getAttribute("target");
|
|
128
|
-
return (
|
|
129
|
-
getRouteKind(url.pathname) === "other" ||
|
|
130
|
-
/\.(?:md|json|xml|txt|html?|pdf|png|webp|avif|svg|jpe?g)$/iu.test(url.pathname) ||
|
|
131
|
-
Boolean(el?.closest("[data-no-swup],[data-astro-reload],[rel~=external]")) ||
|
|
132
|
-
Boolean(target && target !== "_self")
|
|
133
|
-
);
|
|
134
|
-
},
|
|
135
|
-
plugins: [
|
|
136
|
-
navigationScripts,
|
|
137
|
-
new SwupBodyClassPlugin({
|
|
138
|
-
prefix: "page-",
|
|
139
|
-
attributes: [/^data-stalux-(?!bg-)/u],
|
|
140
|
-
}),
|
|
141
|
-
new SwupRouteNamePlugin({
|
|
142
|
-
routes: [
|
|
143
|
-
{ name: "home", path: "/" },
|
|
144
|
-
{ name: "article", path: "/posts/:post" },
|
|
145
|
-
{ name: "archive", path: "/archives" },
|
|
146
|
-
{ name: "tags-index", path: "/tags" },
|
|
147
|
-
{ name: "tags-detail", path: "/tags/:tag" },
|
|
148
|
-
{ name: "categories-index", path: "/categories" },
|
|
149
|
-
{ name: "categories-detail", path: "/categories/:category" },
|
|
150
|
-
...["about", "links", "words"].map((name) => ({ name, path: "/" + name })),
|
|
151
|
-
{ name: "not-found", path: "/404" },
|
|
152
|
-
],
|
|
153
|
-
}),
|
|
154
|
-
new SwupFragmentPlugin({
|
|
155
|
-
rules: [
|
|
156
|
-
{
|
|
157
|
-
from: ["/tags/:slug", "/categories/:slug"],
|
|
158
|
-
to: ["/tags/:slug", "/categories/:slug"],
|
|
159
|
-
containers: ["#swup-taxonomy"],
|
|
160
|
-
scroll: true,
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
from: "/posts/:post",
|
|
164
|
-
to: "/posts/:post",
|
|
165
|
-
containers: ["#swup-article"],
|
|
166
|
-
scroll: true,
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
}),
|
|
170
|
-
new SwupParallelPlugin({ containers: [MAIN, "#swup-article", "#swup-taxonomy"] }),
|
|
171
|
-
progress,
|
|
172
|
-
new SwupHeadPlugin({
|
|
173
|
-
awaitAssets: true,
|
|
174
|
-
// Keep old CSS readable until replacement, then prune authored
|
|
175
|
-
// orphans. Styles inserted by live SDKs remain available.
|
|
176
|
-
persistTags: (el) => el.matches("script[src],style,link[rel=stylesheet]"),
|
|
177
|
-
}),
|
|
178
|
-
scroll,
|
|
179
|
-
new SwupA11yPlugin({ headingSelector: "[data-stalux-motion-focus]" }),
|
|
180
|
-
new SwupPreloadPlugin({
|
|
181
|
-
// The initial document is already present; avoid fetching it twice.
|
|
182
|
-
preloadInitialPage: false,
|
|
183
|
-
preloadHoveredLinks: true,
|
|
184
|
-
preloadVisibleLinks: false,
|
|
185
|
-
throttle: 2,
|
|
186
|
-
}),
|
|
187
|
-
new SwupJsPlugin({
|
|
188
|
-
animations: [
|
|
189
|
-
{
|
|
190
|
-
from: "(.*)",
|
|
191
|
-
to: "(.*)",
|
|
192
|
-
out: async () => {},
|
|
193
|
-
in: async (_done, { visit }) => {
|
|
194
|
-
const context = contexts.get(visit);
|
|
195
|
-
if (!context?.motion) return;
|
|
196
|
-
context.motion.start(
|
|
197
|
-
document.querySelector<HTMLElement>("[data-stalux-motion-focus]"),
|
|
198
|
-
{
|
|
199
|
-
preset: context.preset,
|
|
200
|
-
content: true,
|
|
201
|
-
browserMotion: context.browserMotion,
|
|
202
|
-
skip: context.controller.signal.aborted,
|
|
203
|
-
releaseOutgoing: context.releaseOutgoing,
|
|
204
|
-
},
|
|
205
|
-
);
|
|
206
|
-
await Promise.all([context.motion.finished, context.outgoingReady]);
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
],
|
|
210
|
-
}),
|
|
211
|
-
],
|
|
212
|
-
});
|
|
213
|
-
if (import.meta.env.DEV) {
|
|
214
|
-
void import("virtual:stalux-swup-debug").then(({ default: Debug }) => {
|
|
215
|
-
if (Debug) swup.use(new Debug());
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
window.swup = swup;
|
|
219
|
-
document.documentElement.dataset.staluxRouter = "swup";
|
|
220
|
-
document.documentElement.dataset.staluxRoute = getRouteKind(window.location.pathname);
|
|
221
|
-
installBackToTop();
|
|
222
|
-
|
|
223
|
-
// Set this in the first synchronous before hook, even when a superseding
|
|
224
|
-
// click skips the later context handler. Swup's page:load promise must settle
|
|
225
|
-
// before an obsolete visit is stopped at the transition boundary.
|
|
226
|
-
swup.hooks.before(
|
|
227
|
-
"visit:start",
|
|
228
|
-
(visit) => {
|
|
229
|
-
visit.animation.wait = true;
|
|
230
|
-
},
|
|
231
|
-
{ priority: -1000 },
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
swup.hooks.on("visit:start", (visit) => {
|
|
235
|
-
cancelPageScroll();
|
|
236
|
-
const focused = document.activeElement;
|
|
237
|
-
const focus =
|
|
238
|
-
focused instanceof HTMLAnchorElement
|
|
239
|
-
? (focused.getAttribute("href") ?? undefined)
|
|
240
|
-
: undefined;
|
|
241
|
-
historyPositions.set(visibleHistoryIndex, {
|
|
242
|
-
top: window.scrollY,
|
|
243
|
-
left: window.scrollX,
|
|
244
|
-
focus,
|
|
245
|
-
});
|
|
246
|
-
if (historyPositions.size > 64) {
|
|
247
|
-
const oldest = historyPositions.keys().next();
|
|
248
|
-
if (!oldest.done) historyPositions.delete(oldest.value);
|
|
249
|
-
}
|
|
250
|
-
const from = new URL(visit.from.url + (visit.from.hash ?? ""), window.location.origin);
|
|
251
|
-
const to = new URL(visit.to.url + (visit.to.hash ?? ""), window.location.origin);
|
|
252
|
-
const navigationType = visit.history.popstate ? "traverse" : visit.history.action;
|
|
253
|
-
const context: NavigationContext = {
|
|
254
|
-
controller: new AbortController(),
|
|
255
|
-
from,
|
|
256
|
-
to,
|
|
257
|
-
navigationType,
|
|
258
|
-
direction: visit.history.direction === "backwards" ? "back" : "forward",
|
|
259
|
-
sourceElement: visit.trigger.el,
|
|
260
|
-
browserMotion: browserMotion && visit.history.popstate,
|
|
261
|
-
preset: getRouteMotionPreset({
|
|
262
|
-
from,
|
|
263
|
-
to,
|
|
264
|
-
navigationType,
|
|
265
|
-
intent:
|
|
266
|
-
visit.trigger.el
|
|
267
|
-
?.closest("[data-stalux-motion]")
|
|
268
|
-
?.getAttribute("data-stalux-motion") ?? undefined,
|
|
269
|
-
reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches,
|
|
270
|
-
browserMotion: browserMotion && visit.history.popstate,
|
|
271
|
-
hidden: document.hidden,
|
|
272
|
-
}),
|
|
273
|
-
obsoleteStyles: [],
|
|
274
|
-
};
|
|
275
|
-
browserMotion = false;
|
|
276
|
-
contexts.set(visit, context);
|
|
277
|
-
activeContext = context;
|
|
278
|
-
visit.animation.wait = true;
|
|
279
|
-
if (context.preset === "none" || visit.history.popstate) visit.animation.parallel = false;
|
|
280
|
-
visit.a11y.focus = visit.history.popstate ? false : { selector: MAIN, wait: true };
|
|
281
|
-
if (dispatchNavigation("astro:before-preparation", context).defaultPrevented)
|
|
282
|
-
visit.ignore();
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
swup.hooks.before("visit:transition", (visit) => {
|
|
286
|
-
const context = contexts.get(visit);
|
|
287
|
-
// renderPage updates the URL before content:replace. A revived stale
|
|
288
|
-
// visit must stop here as well as before the later head/DOM work.
|
|
289
|
-
if (!context || context.controller.signal.aborted || visit !== swup.visit)
|
|
290
|
-
abortObsoleteVisit(visit);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
swup.hooks.before("content:insert", (visit, { containers }) => {
|
|
294
|
-
const context = contexts.get(visit);
|
|
295
|
-
if (!context) return;
|
|
296
|
-
context.motion = prepareRouteMotion(
|
|
297
|
-
dispatchNavigation("astro:before-swap", context, context.incomingDocument),
|
|
298
|
-
);
|
|
299
|
-
markRetainedShells();
|
|
300
|
-
const animations: Animation[] = [];
|
|
301
|
-
for (const { previous } of containers) {
|
|
302
|
-
const rect = previous.getBoundingClientRect();
|
|
303
|
-
previous.inert = true;
|
|
304
|
-
// The incoming document owns IDs and focus as soon as it is inserted.
|
|
305
|
-
// Persisted shells are moved by Swup and retain their original IDs.
|
|
306
|
-
for (const element of [previous, ...previous.querySelectorAll("[id]")]) {
|
|
307
|
-
if (!element.closest("[data-swup-persist]")) element.removeAttribute("id");
|
|
308
|
-
}
|
|
309
|
-
Object.assign(previous.style, {
|
|
310
|
-
position: "fixed",
|
|
311
|
-
top: rect.top + "px",
|
|
312
|
-
left: rect.left + "px",
|
|
313
|
-
width: rect.width + "px",
|
|
314
|
-
height: rect.height + "px",
|
|
315
|
-
// Clip the visible slice without truncating a deeply scrolled article.
|
|
316
|
-
clipPath: `inset(${Math.max(0, -rect.top)}px 0 ${Math.max(0, rect.bottom - window.innerHeight)}px 0)`,
|
|
317
|
-
overflow: "hidden",
|
|
318
|
-
pointerEvents: "none",
|
|
319
|
-
zIndex: "1",
|
|
320
|
-
});
|
|
321
|
-
animations.push(
|
|
322
|
-
previous.animate(
|
|
323
|
-
{ opacity: [1, 0], transform: ["translateY(0)", "translateY(-12px)"] },
|
|
324
|
-
{ duration: 160, easing: "ease-out", fill: "forwards" },
|
|
325
|
-
),
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
const remove = () => {
|
|
329
|
-
for (const animation of animations) animation.cancel();
|
|
330
|
-
for (const { previous } of containers) previous.remove();
|
|
331
|
-
};
|
|
332
|
-
context.removeOutgoing = once(remove);
|
|
333
|
-
context.controller.signal.addEventListener("abort", context.removeOutgoing, { once: true });
|
|
334
|
-
context.outgoingReady = Promise.all(
|
|
335
|
-
animations.map((animation) => animation.finished.catch(() => {})),
|
|
336
|
-
).then(() => context.removeOutgoing?.());
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
swup.hooks.before(
|
|
340
|
-
"content:replace",
|
|
341
|
-
async (visit) => {
|
|
342
|
-
const context = contexts.get(visit);
|
|
343
|
-
// Swup 4 can resume a superseded async visit:start after abort. Stop
|
|
344
|
-
// its entire remaining hook chain before HeadPlugin touches metadata.
|
|
345
|
-
if (!context || context.controller.signal.aborted || visit !== swup.visit) {
|
|
346
|
-
abortObsoleteVisit(visit);
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
const next = visit.to.document;
|
|
350
|
-
if (!next) return;
|
|
351
|
-
context.executeScripts = prepareNavigationScripts(next, { baseUrl: context.to.href });
|
|
352
|
-
await prepareNavigationFont(next, context.controller.signal);
|
|
353
|
-
if (context.controller.signal.aborted || visit !== swup.visit) {
|
|
354
|
-
abortObsoleteVisit(visit);
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
context.incomingDocument = next.cloneNode(true) as Document;
|
|
358
|
-
const incoming = [...next.head.querySelectorAll("style,link[rel=stylesheet]")];
|
|
359
|
-
context.obsoleteStyles = authoredStyles.filter(
|
|
360
|
-
(el) => !incoming.some((other) => el.isEqualNode(other)),
|
|
361
|
-
);
|
|
362
|
-
dispatchNavigation("astro:after-preparation", context, next);
|
|
363
|
-
},
|
|
364
|
-
{ priority: -100 },
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
// Assets are ready before old interactions are disposed. Replacement errors
|
|
368
|
-
// propagate to Swup's full-document fallback instead of leaving a dead page.
|
|
369
|
-
swup.hooks.replace("content:replace", async (visit, args, replace) => {
|
|
370
|
-
const context = contexts.get(visit);
|
|
371
|
-
// Swup can release an obsolete visit's parsed document while its async
|
|
372
|
-
// hook chain is still unwinding. Cancellation precedes validation: a
|
|
373
|
-
// missing document on an aborted visit is not a failed navigation.
|
|
374
|
-
if (context?.controller.signal.aborted || visit.done || visit !== swup.visit) return;
|
|
375
|
-
const next = visit.to.document;
|
|
376
|
-
if (!(context && next && replace)) throw new Error("Missing Swup document");
|
|
377
|
-
if (context.controller.signal.aborted) return;
|
|
378
|
-
// Two cues overlap on the outgoing document: the content stage leaving,
|
|
379
|
-
// and the page walking back to the top. Persisted sidebars, the header
|
|
380
|
-
// and the frame are never part of either one.
|
|
381
|
-
if (
|
|
382
|
-
!visit.animation.parallel &&
|
|
383
|
-
context.navigationType !== "traverse" &&
|
|
384
|
-
context.preset !== "none"
|
|
385
|
-
) {
|
|
386
|
-
const outgoing = animateRouteOut(context.preset, context.controller.signal);
|
|
387
|
-
if (!context.to.hash) context.glide = glideToTop();
|
|
388
|
-
await context.glide?.promise;
|
|
389
|
-
context.releaseOutgoing = once(await outgoing);
|
|
390
|
-
if (context.controller.signal.aborted || visit !== swup.visit) {
|
|
391
|
-
context.releaseOutgoing();
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
const html = document.documentElement;
|
|
396
|
-
html.dataset.staluxVtFrom = getRouteKind(context.from.pathname);
|
|
397
|
-
html.dataset.staluxVtTo = getRouteKind(context.to.pathname);
|
|
398
|
-
html.dataset.staluxVtDirection = context.direction;
|
|
399
|
-
html.dataset.staluxVtPreset = context.preset;
|
|
400
|
-
if (context.controller.signal.aborted) return;
|
|
401
|
-
if (!visit.animation.parallel) {
|
|
402
|
-
context.motion = prepareRouteMotion(
|
|
403
|
-
dispatchNavigation("astro:before-swap", context, next),
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
// Remember which shells exist now: after the swap only those slots need
|
|
407
|
-
// refilling, because only those nodes came from the previous document.
|
|
408
|
-
markRetainedShells();
|
|
409
|
-
await replace(visit, args);
|
|
410
|
-
// The stage node survives the swap, so the filled exit effect still holds
|
|
411
|
-
// the incoming document at zero opacity. It is released inside
|
|
412
|
-
// motion.start, in the same synchronous block as the entry cue: releasing
|
|
413
|
-
// it here would paint the new document fully visible for one frame.
|
|
414
|
-
if (context.controller.signal.aborted) return;
|
|
415
|
-
// ParallelPlugin keeps the outgoing container visible while it fades out
|
|
416
|
-
// and only removes it at visit:end / content:remove. Its page-scoped CSS
|
|
417
|
-
// must outlive that DOM: pruning here paints the outgoing (and already
|
|
418
|
-
// overlapping) content unstyled for the whole overlap. `awaitAssets`
|
|
419
|
-
// only covers the incoming stylesheets, not the outgoing ones.
|
|
420
|
-
const pruneObsoleteStyles = once(() => {
|
|
421
|
-
for (const element of context.obsoleteStyles) element.remove();
|
|
422
|
-
});
|
|
423
|
-
context.pruneObsoleteStyles = pruneObsoleteStyles;
|
|
424
|
-
if (context.outgoingReady) void context.outgoingReady.then(pruneObsoleteStyles);
|
|
425
|
-
else pruneObsoleteStyles();
|
|
426
|
-
const incomingStyles = [...next.head.querySelectorAll("style,link[rel=stylesheet]")];
|
|
427
|
-
authoredStyles = [...document.head.querySelectorAll("style,link[rel=stylesheet]")].filter(
|
|
428
|
-
(el) => incomingStyles.some((other) => el.isEqualNode(other)),
|
|
429
|
-
);
|
|
430
|
-
// Main alone is replaced: header/footer/sentinel and background layers
|
|
431
|
-
// keep their node identities. Never reset live background state to SSR 0.
|
|
432
|
-
html.dataset.staluxRoute = visit.to.route ?? getRouteKind(context.to.pathname);
|
|
433
|
-
document.querySelector(MAIN)?.setAttribute("data-stalux-route", html.dataset.staluxRoute);
|
|
434
|
-
visibleHistoryIndex = Number(window.history.state?.index ?? visibleHistoryIndex);
|
|
435
|
-
// Kept shells whose contents belong to the page (the article sidebars'
|
|
436
|
-
// lists) take their children from the incoming document now, before any
|
|
437
|
-
// page module mounts, so nothing needs to re-render or re-measure later.
|
|
438
|
-
hydrateShells(context.incomingDocument ?? next);
|
|
439
|
-
dispatchNavigation("astro:after-swap", context, next);
|
|
440
|
-
// The entry cue has to own the first painted frame of the new document.
|
|
441
|
-
// content:scroll is an await and a rAF away, and running the incoming
|
|
442
|
-
// scripts can block the main thread for hundreds of milliseconds: the
|
|
443
|
-
// reader would see the finished page for one or more frames and only
|
|
444
|
-
// then watch it jump to the entry offset and slide back, which reads as
|
|
445
|
-
// the page shaking. Put the scroll at its final position and start the
|
|
446
|
-
// cue in this same synchronous block, before the scripts run, so the
|
|
447
|
-
// compositor already owns the cue while the main thread is busy. Hash
|
|
448
|
-
// targets and history traversals are settled in content:scroll below.
|
|
449
|
-
// Entering a new document from a link always starts at its top. The
|
|
450
|
-
// motion preset (reduced motion / hidden tab) and the ScrollPlugin's
|
|
451
|
-
// reset flag are visual concerns, not navigation ones; neither may opt
|
|
452
|
-
// out of the reset. A cached offset belongs to history traversals only
|
|
453
|
-
// (handled in content:scroll) and must never pull a fresh page back
|
|
454
|
-
// into the middle of the previous article.
|
|
455
|
-
const startsFresh =
|
|
456
|
-
!visit.history.popstate && context.navigationType !== "traverse" && !context.to.hash;
|
|
457
|
-
if (startsFresh) {
|
|
458
|
-
if (window.scrollY !== 0 || window.scrollX !== 0)
|
|
459
|
-
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
460
|
-
if (context.preset !== "none" && visit.scroll?.reset !== false) startMotion(visit);
|
|
461
|
-
}
|
|
462
|
-
context.scriptsReady = context.executeScripts?.(context.controller.signal);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
swup.hooks.replace(
|
|
466
|
-
"content:scroll",
|
|
467
|
-
async (visit, args, defaultScroll) => {
|
|
468
|
-
const context = contexts.get(visit);
|
|
469
|
-
const saved = visit.history.popstate
|
|
470
|
-
? historyPositions.get(Number(window.history.state?.index))
|
|
471
|
-
: undefined;
|
|
472
|
-
if (visit.history.popstate) {
|
|
473
|
-
// Every route change starts at the top. Restoring the exact
|
|
474
|
-
// mid-article offset on back/forward reads as "the navigation
|
|
475
|
-
// never scrolled"; keep only the focused link so keyboard users
|
|
476
|
-
// retain their place in the navigation.
|
|
477
|
-
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
478
|
-
scroll.restoreScrollContainers(visit.to.url);
|
|
479
|
-
if (saved?.focus) {
|
|
480
|
-
const link = [...document.querySelectorAll<HTMLAnchorElement>("a[href]")].find(
|
|
481
|
-
(el) => el.getAttribute("href") === saved.focus,
|
|
482
|
-
);
|
|
483
|
-
link?.focus({ preventScroll: true });
|
|
484
|
-
}
|
|
485
|
-
startMotion(visit);
|
|
486
|
-
return true;
|
|
487
|
-
}
|
|
488
|
-
await defaultScroll?.(visit, args);
|
|
489
|
-
// ScrollPlugin scrolls in rAF; finish it before heading visibility checks.
|
|
490
|
-
if (!document.hidden)
|
|
491
|
-
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
492
|
-
// That deferred frame may have restored a cached mid-article offset.
|
|
493
|
-
// A link navigation always wins: land the new document at its top.
|
|
494
|
-
if (
|
|
495
|
-
context?.navigationType !== "traverse" &&
|
|
496
|
-
!context?.to.hash &&
|
|
497
|
-
(window.scrollY !== 0 || window.scrollX !== 0)
|
|
498
|
-
)
|
|
499
|
-
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
500
|
-
startMotion(visit);
|
|
501
|
-
return true;
|
|
502
|
-
},
|
|
503
|
-
{ priority: 10 },
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
function startMotion(visit: Visit): void {
|
|
507
|
-
const context = contexts.get(visit);
|
|
508
|
-
if (!context || context !== activeContext || visit !== swup.visit) return;
|
|
509
|
-
delete document.documentElement.dataset.staluxMotionPending;
|
|
510
|
-
context?.motion?.start(document.querySelector<HTMLElement>("[data-stalux-motion-focus]"), {
|
|
511
|
-
preset: context.preset,
|
|
512
|
-
content: true,
|
|
513
|
-
skip: !visit.animation.animate || context.controller.signal.aborted,
|
|
514
|
-
browserMotion: context.browserMotion,
|
|
515
|
-
releaseOutgoing: context.releaseOutgoing,
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
swup.hooks.on("page:view", async (visit) => {
|
|
520
|
-
const context = contexts.get(visit);
|
|
521
|
-
if (!context || context.controller.signal.aborted) return;
|
|
522
|
-
await context.scriptsReady;
|
|
523
|
-
if (context.controller.signal.aborted) return;
|
|
524
|
-
dispatchNavigation("astro:page-load", context);
|
|
525
|
-
if (!visit.animation.animate)
|
|
526
|
-
context.motion?.start(null, { skip: true, releaseOutgoing: context.releaseOutgoing });
|
|
527
|
-
});
|
|
528
|
-
swup.hooks.replace("link:self", () => undefined);
|
|
529
|
-
|
|
530
|
-
const cancel = (visit: Visit) => {
|
|
531
|
-
// A cancelled/failed visit never reaches page:view. Clear its delayed
|
|
532
|
-
// indicator without allowing a stale visit to hide a newer one.
|
|
533
|
-
if (visit === swup.visit) progress.stopShowingProgress();
|
|
534
|
-
const context = contexts.get(visit);
|
|
535
|
-
context?.controller.abort();
|
|
536
|
-
context?.removeOutgoing?.();
|
|
537
|
-
// An aborted visit never reaches visit:end, so its outgoing CSS has to
|
|
538
|
-
// be released here or it would linger in head forever.
|
|
539
|
-
context?.pruneObsoleteStyles?.();
|
|
540
|
-
context?.glide?.cancel();
|
|
541
|
-
// An aborted visit keeps no exit fill: without this the stage would stay
|
|
542
|
-
// transparent until the next successful navigation.
|
|
543
|
-
context?.releaseOutgoing?.();
|
|
544
|
-
context?.motion?.cancel();
|
|
545
|
-
if (context === activeContext) {
|
|
546
|
-
clearMotionState();
|
|
547
|
-
document.documentElement.removeAttribute("aria-busy");
|
|
548
|
-
activeContext = undefined;
|
|
549
|
-
}
|
|
550
|
-
};
|
|
551
|
-
swup.hooks.on("visit:abort", cancel);
|
|
552
|
-
swup.hooks.on("visit:fail", cancel);
|
|
553
|
-
swup.hooks.on("visit:end", (visit) => {
|
|
554
|
-
const context = contexts.get(visit);
|
|
555
|
-
// Safety net for paths where the exit animation never settles. The prune
|
|
556
|
-
// is idempotent, so a completed outgoingReady does not remove twice.
|
|
557
|
-
context?.pruneObsoleteStyles?.();
|
|
558
|
-
context?.glide?.cancel();
|
|
559
|
-
context?.releaseOutgoing?.();
|
|
560
|
-
context?.motion?.start(null, { skip: true });
|
|
561
|
-
if (activeContext === context) {
|
|
562
|
-
clearMotionState();
|
|
563
|
-
activeContext = undefined;
|
|
564
|
-
}
|
|
565
|
-
});
|
|
566
|
-
swup.hooks.on("cache:set", () => {
|
|
567
|
-
while (swup.cache.size > 32) {
|
|
568
|
-
const oldest = swup.cache.all.keys().next();
|
|
569
|
-
if (oldest.done) break;
|
|
570
|
-
swup.cache.delete(oldest.value);
|
|
571
|
-
}
|
|
572
|
-
});
|
|
573
|
-
// Parsed DOM, not window.load: slow third-party resources cannot hold the
|
|
574
|
-
// first page's interactions hostage. Page-runtime dedupes the early mount.
|
|
575
|
-
const initialLoad = () => document.dispatchEvent(new Event("astro:page-load"));
|
|
576
|
-
if (document.readyState === "loading")
|
|
577
|
-
document.addEventListener("DOMContentLoaded", initialLoad, { once: true });
|
|
578
|
-
else queueMicrotask(initialLoad);
|
|
579
|
-
return swup;
|
|
580
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { getRouteKind, normalizePublicPath } from "./public-routes";
|
|
2
|
-
|
|
3
|
-
export type RouteMotionPreset =
|
|
4
|
-
| "none"
|
|
5
|
-
| "glide"
|
|
6
|
-
| "article"
|
|
7
|
-
| "panel"
|
|
8
|
-
| "enter"
|
|
9
|
-
| "up"
|
|
10
|
-
| "taxonomy"
|
|
11
|
-
| "previous"
|
|
12
|
-
| "next"
|
|
13
|
-
| "home"
|
|
14
|
-
| "restore"
|
|
15
|
-
| "error";
|
|
16
|
-
|
|
17
|
-
interface MotionContext {
|
|
18
|
-
from: URL;
|
|
19
|
-
to: URL;
|
|
20
|
-
navigationType?: string;
|
|
21
|
-
intent?: string;
|
|
22
|
-
reducedMotion?: boolean;
|
|
23
|
-
browserMotion?: boolean;
|
|
24
|
-
hidden?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Choose a visual hint, never a navigation or scroll operation. */
|
|
28
|
-
export function getRouteMotionPreset(context: MotionContext): RouteMotionPreset {
|
|
29
|
-
const { from, to } = context;
|
|
30
|
-
const source = getRouteKind(from.pathname);
|
|
31
|
-
const target = getRouteKind(to.pathname);
|
|
32
|
-
if (
|
|
33
|
-
context.hidden ||
|
|
34
|
-
context.reducedMotion ||
|
|
35
|
-
context.browserMotion ||
|
|
36
|
-
from.origin !== to.origin ||
|
|
37
|
-
source === "other" ||
|
|
38
|
-
target === "other" ||
|
|
39
|
-
(normalizePublicPath(from.pathname) === normalizePublicPath(to.pathname) &&
|
|
40
|
-
from.search === to.search)
|
|
41
|
-
) {
|
|
42
|
-
return "none";
|
|
43
|
-
}
|
|
44
|
-
// Both directions restore an existing history entry. A push to a parent
|
|
45
|
-
// route is not a history traversal, and an article's previous/next is not back.
|
|
46
|
-
if (context.navigationType === "traverse") return "restore";
|
|
47
|
-
if (context.navigationType === "replace") return "glide";
|
|
48
|
-
if (target === "not-found") return "error";
|
|
49
|
-
if (source === "not-found") return "glide";
|
|
50
|
-
if (target === "home") return "home";
|
|
51
|
-
if (source === "article" && target === "article") {
|
|
52
|
-
return context.intent === "previous" || context.intent === "next"
|
|
53
|
-
? context.intent
|
|
54
|
-
: "article";
|
|
55
|
-
}
|
|
56
|
-
if (target === "article") return "enter";
|
|
57
|
-
if (target === "about" || target === "links" || target === "words") return "panel";
|
|
58
|
-
if (target === "tags-detail" || target === "categories-detail") {
|
|
59
|
-
if (source === target) return "taxonomy";
|
|
60
|
-
if (source === "tags-detail" || source === "categories-detail") return "glide";
|
|
61
|
-
return "enter";
|
|
62
|
-
}
|
|
63
|
-
if (
|
|
64
|
-
(source === "article" &&
|
|
65
|
-
(target === "archive" || target === "tags-index" || target === "categories-index")) ||
|
|
66
|
-
(source === "tags-detail" && target === "tags-index") ||
|
|
67
|
-
(source === "categories-detail" && target === "categories-index")
|
|
68
|
-
) {
|
|
69
|
-
return "up";
|
|
70
|
-
}
|
|
71
|
-
return source === "home" ? "enter" : "glide";
|
|
72
|
-
}
|