@xingwangzhe/stalux 1.42.0 → 1.43.1
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/README.md +7 -5
- package/package.json +7 -1
- package/src/components/stalux/archives/archivesList.astro +0 -2
- package/src/components/stalux/archives/toArchive.astro +1 -5
- package/src/components/stalux/common/Typewriter.astro +1 -7
- package/src/components/stalux/common/author.astro +1 -6
- package/src/components/stalux/common/date.astro +1 -11
- package/src/components/stalux/layout/PageHeading.astro +2 -11
- package/src/components/stalux/layout/footer.astro +1 -6
- package/src/components/stalux/layout/head.astro +0 -2
- package/src/components/stalux/links/socialMedia.astro +1 -10
- package/src/components/stalux/posts/ArticleSummary.astro +2 -8
- package/src/components/stalux/posts/PostTaxonomy.astro +3 -14
- package/src/components/stalux/posts/postCC.astro +2 -2
- package/src/components/stalux/posts/postCard.astro +1 -5
- package/src/components/stalux/posts/postContent.astro +4 -23
- package/src/components/stalux/posts/postLeft.astro +4 -22
- package/src/components/stalux/posts/postRight.astro +5 -27
- package/src/components/stalux/posts/postToc.astro +3 -10
- package/src/components/stalux/posts/randomPost.astro +0 -1
- package/src/components/stalux/tags/tagHeader.astro +1 -5
- package/src/index.ts +20 -1
- package/src/internal/style-names.generated.ts +59 -69
- package/src/layouts/PostLayout.astro +1 -5
- package/src/layouts/Stalux.astro +3 -19
- package/src/pages/index.astro +1 -5
- package/src/scripts/navigation-client.ts +13 -0
- package/src/scripts/navigation-scripts.ts +191 -0
- package/src/scripts/route-motion.ts +82 -11
- package/src/scripts/route-out.ts +33 -0
- package/src/scripts/smooth-scroll.ts +52 -0
- package/src/scripts/swup-router.ts +381 -0
- package/src/scripts/tags-cloud.ts +1 -1
- package/src/scripts/view-transitions.ts +3 -233
- package/src/scripts/word-list.ts +9 -3
- package/src/styles/generated-words.css +1 -1
- package/src/styles/generated.css +1 -1
- package/src/styles/motion.css +7 -211
- package/src/utils/toc-scrollspy.ts +21 -14
- package/src/scripts/archive-reveal.ts +0 -76
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import SwupA11yPlugin from "@swup/a11y-plugin";
|
|
2
|
+
import SwupHeadPlugin from "@swup/head-plugin";
|
|
3
|
+
import SwupJsPlugin from "@swup/js-plugin";
|
|
4
|
+
import SwupPreloadPlugin from "@swup/preload-plugin";
|
|
5
|
+
import SwupScrollPlugin from "@swup/scroll-plugin";
|
|
6
|
+
import Swup, { type Visit } from "swup";
|
|
7
|
+
import { getRouteKind } from "../utils/public-routes";
|
|
8
|
+
import { getRouteMotionPreset, type RouteMotionPreset } from "../utils/route-motion-policy";
|
|
9
|
+
import { installBackToTop } from "./back-to-top";
|
|
10
|
+
import { prepareNavigationFont } from "./navigation-font";
|
|
11
|
+
import "./navigation-client";
|
|
12
|
+
import { prepareNavigationScripts } from "./navigation-scripts";
|
|
13
|
+
import { prepareRouteMotion, type RouteMotion } from "./route-motion";
|
|
14
|
+
import { animateRouteOut } from "./route-out";
|
|
15
|
+
import { cancelPageScroll, scrollPage } from "./smooth-scroll";
|
|
16
|
+
|
|
17
|
+
interface NavigationContext {
|
|
18
|
+
controller: AbortController;
|
|
19
|
+
from: URL;
|
|
20
|
+
to: URL;
|
|
21
|
+
direction: string;
|
|
22
|
+
navigationType: "push" | "replace" | "traverse";
|
|
23
|
+
sourceElement?: Element;
|
|
24
|
+
preset: RouteMotionPreset;
|
|
25
|
+
browserMotion: boolean;
|
|
26
|
+
motion?: RouteMotion;
|
|
27
|
+
executeScripts?: (signal?: AbortSignal) => Promise<void>;
|
|
28
|
+
scriptsReady?: Promise<void>;
|
|
29
|
+
obsoleteStyles: Element[];
|
|
30
|
+
releaseOutgoing?: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Position = { top: number; left: number; focus?: string };
|
|
34
|
+
const MAIN = "main[data-stalux-route]";
|
|
35
|
+
|
|
36
|
+
function abortObsoleteVisit(visit: Visit): void {
|
|
37
|
+
// Swup 4.10 ships abort() but strips @internal members from its .d.ts.
|
|
38
|
+
// Pin core to that audited version; recheck this adapter when upgrading it.
|
|
39
|
+
(visit as Visit & { abort: () => void }).abort();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function dispatchNavigation(name: string, context: NavigationContext, nextDocument?: Document) {
|
|
43
|
+
const event = Object.assign(new Event(name, { cancelable: true }), {
|
|
44
|
+
from: context.from,
|
|
45
|
+
to: context.to,
|
|
46
|
+
direction: context.direction,
|
|
47
|
+
navigationType: context.navigationType,
|
|
48
|
+
sourceElement: context.sourceElement,
|
|
49
|
+
signal: context.controller.signal,
|
|
50
|
+
...(nextDocument ? { newDocument: nextDocument } : {}),
|
|
51
|
+
});
|
|
52
|
+
document.dispatchEvent(event);
|
|
53
|
+
return event;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clearMotionState(): void {
|
|
57
|
+
delete document.documentElement.dataset.staluxMotionPending;
|
|
58
|
+
for (const key of ["staluxVtFrom", "staluxVtTo", "staluxVtDirection", "staluxVtPreset"])
|
|
59
|
+
delete document.documentElement.dataset[key];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Astro renders pages; Swup alone owns their client navigation and animation. */
|
|
63
|
+
export function installSwupRouter(): Swup {
|
|
64
|
+
if (window.swup) return window.swup;
|
|
65
|
+
const contexts = new WeakMap<Visit, NavigationContext>();
|
|
66
|
+
const historyPositions = new Map<number, Position>();
|
|
67
|
+
let authoredStyles = [...document.head.querySelectorAll("style,link[rel=stylesheet]")];
|
|
68
|
+
let activeContext: NavigationContext | undefined;
|
|
69
|
+
let visibleHistoryIndex = Number(window.history.state?.index ?? 1);
|
|
70
|
+
let browserMotion = false;
|
|
71
|
+
const scroll = new SwupScrollPlugin({
|
|
72
|
+
doScrollingRightAway: false,
|
|
73
|
+
animateScroll: { betweenPages: false, samePageWithHash: true, samePage: true },
|
|
74
|
+
scrollFunction: scrollPage,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
window.addEventListener(
|
|
78
|
+
"popstate",
|
|
79
|
+
(event) => {
|
|
80
|
+
browserMotion =
|
|
81
|
+
(event as PopStateEvent & { hasUAVisualTransition?: boolean })
|
|
82
|
+
.hasUAVisualTransition === true;
|
|
83
|
+
},
|
|
84
|
+
{ capture: true },
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const swup = new Swup({
|
|
88
|
+
native: false,
|
|
89
|
+
containers: [MAIN],
|
|
90
|
+
animationSelector: false,
|
|
91
|
+
animateHistoryBrowsing: true,
|
|
92
|
+
timeout: 15_000,
|
|
93
|
+
ignoreVisit: (href, { el }) => {
|
|
94
|
+
const url = new URL(href, window.location.href);
|
|
95
|
+
const target =
|
|
96
|
+
el?.getAttribute("target") ??
|
|
97
|
+
document.querySelector("base[target]")?.getAttribute("target");
|
|
98
|
+
return (
|
|
99
|
+
getRouteKind(url.pathname) === "other" ||
|
|
100
|
+
/\.(?:md|json|xml|txt|html?|pdf|png|webp|avif|svg|jpe?g)$/i.test(url.pathname) ||
|
|
101
|
+
Boolean(el?.closest("[data-no-swup],[data-astro-reload],[rel~=external]")) ||
|
|
102
|
+
Boolean(target && target !== "_self")
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
plugins: [
|
|
106
|
+
new SwupHeadPlugin({
|
|
107
|
+
awaitAssets: true,
|
|
108
|
+
// Keep old CSS readable until replacement, then prune authored
|
|
109
|
+
// orphans. Styles inserted by live SDKs remain available.
|
|
110
|
+
persistTags: (el) => el.matches("script[src],style,link[rel=stylesheet]"),
|
|
111
|
+
}),
|
|
112
|
+
scroll,
|
|
113
|
+
new SwupA11yPlugin({ headingSelector: "[data-stalux-motion-focus]" }),
|
|
114
|
+
new SwupPreloadPlugin({
|
|
115
|
+
preloadHoveredLinks: true,
|
|
116
|
+
preloadVisibleLinks: false,
|
|
117
|
+
throttle: 2,
|
|
118
|
+
}),
|
|
119
|
+
new SwupJsPlugin({
|
|
120
|
+
animations: [
|
|
121
|
+
{
|
|
122
|
+
from: "(.*)",
|
|
123
|
+
to: "(.*)",
|
|
124
|
+
out: async () => {},
|
|
125
|
+
in: async (_done, { visit }) => {
|
|
126
|
+
const context = contexts.get(visit);
|
|
127
|
+
if (!context?.motion) return;
|
|
128
|
+
context.motion.start(document.querySelector<HTMLElement>(MAIN), {
|
|
129
|
+
preset: context.preset,
|
|
130
|
+
content: true,
|
|
131
|
+
browserMotion: context.browserMotion,
|
|
132
|
+
skip: context.controller.signal.aborted,
|
|
133
|
+
});
|
|
134
|
+
await context.motion.finished;
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
}),
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
window.swup = swup;
|
|
142
|
+
document.documentElement.dataset.staluxRouter = "swup";
|
|
143
|
+
document.documentElement.dataset.staluxRoute = getRouteKind(window.location.pathname);
|
|
144
|
+
installBackToTop();
|
|
145
|
+
|
|
146
|
+
// Set this in the first synchronous before hook, even when a superseding
|
|
147
|
+
// click skips the later context handler. Swup's page:load promise must settle
|
|
148
|
+
// before an obsolete visit is stopped at the transition boundary.
|
|
149
|
+
swup.hooks.before(
|
|
150
|
+
"visit:start",
|
|
151
|
+
(visit) => {
|
|
152
|
+
visit.animation.wait = true;
|
|
153
|
+
},
|
|
154
|
+
{ priority: -1000 },
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
swup.hooks.on("visit:start", (visit) => {
|
|
158
|
+
cancelPageScroll();
|
|
159
|
+
const focused = document.activeElement;
|
|
160
|
+
const focus =
|
|
161
|
+
focused instanceof HTMLAnchorElement
|
|
162
|
+
? (focused.getAttribute("href") ?? undefined)
|
|
163
|
+
: undefined;
|
|
164
|
+
historyPositions.set(visibleHistoryIndex, {
|
|
165
|
+
top: window.scrollY,
|
|
166
|
+
left: window.scrollX,
|
|
167
|
+
focus,
|
|
168
|
+
});
|
|
169
|
+
if (historyPositions.size > 64)
|
|
170
|
+
historyPositions.delete(historyPositions.keys().next().value as number);
|
|
171
|
+
const from = new URL(visit.from.url + (visit.from.hash ?? ""), window.location.origin);
|
|
172
|
+
const to = new URL(visit.to.url + (visit.to.hash ?? ""), window.location.origin);
|
|
173
|
+
const navigationType = visit.history.popstate ? "traverse" : visit.history.action;
|
|
174
|
+
const context: NavigationContext = {
|
|
175
|
+
controller: new AbortController(),
|
|
176
|
+
from,
|
|
177
|
+
to,
|
|
178
|
+
navigationType,
|
|
179
|
+
direction: visit.history.direction === "backwards" ? "back" : "forward",
|
|
180
|
+
sourceElement: visit.trigger.el,
|
|
181
|
+
browserMotion: browserMotion && visit.history.popstate,
|
|
182
|
+
preset: getRouteMotionPreset({
|
|
183
|
+
from,
|
|
184
|
+
to,
|
|
185
|
+
navigationType,
|
|
186
|
+
intent:
|
|
187
|
+
visit.trigger.el
|
|
188
|
+
?.closest("[data-stalux-motion]")
|
|
189
|
+
?.getAttribute("data-stalux-motion") ?? undefined,
|
|
190
|
+
reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches,
|
|
191
|
+
browserMotion: browserMotion && visit.history.popstate,
|
|
192
|
+
hidden: document.hidden,
|
|
193
|
+
}),
|
|
194
|
+
obsoleteStyles: [],
|
|
195
|
+
};
|
|
196
|
+
browserMotion = false;
|
|
197
|
+
contexts.set(visit, context);
|
|
198
|
+
activeContext = context;
|
|
199
|
+
visit.animation.wait = true;
|
|
200
|
+
visit.a11y.focus = visit.history.popstate ? false : { selector: MAIN, wait: true };
|
|
201
|
+
if (dispatchNavigation("astro:before-preparation", context).defaultPrevented)
|
|
202
|
+
visit.ignore();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
swup.hooks.before("visit:transition", (visit) => {
|
|
206
|
+
const context = contexts.get(visit);
|
|
207
|
+
// renderPage updates the URL before content:replace. A revived stale
|
|
208
|
+
// visit must stop here as well as before the later head/DOM work.
|
|
209
|
+
if (!context || context.controller.signal.aborted || visit !== swup.visit)
|
|
210
|
+
abortObsoleteVisit(visit);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
swup.hooks.before(
|
|
214
|
+
"content:replace",
|
|
215
|
+
async (visit) => {
|
|
216
|
+
const context = contexts.get(visit);
|
|
217
|
+
// Swup 4 can resume a superseded async visit:start after abort. Stop
|
|
218
|
+
// its entire remaining hook chain before HeadPlugin touches metadata.
|
|
219
|
+
if (!context || context.controller.signal.aborted || visit !== swup.visit) {
|
|
220
|
+
abortObsoleteVisit(visit);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const next = visit.to.document;
|
|
224
|
+
if (!next) return;
|
|
225
|
+
context.executeScripts = prepareNavigationScripts(next, { baseUrl: context.to.href });
|
|
226
|
+
await prepareNavigationFont(next, context.controller.signal);
|
|
227
|
+
if (context.controller.signal.aborted || visit !== swup.visit) {
|
|
228
|
+
abortObsoleteVisit(visit);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const incoming = [...next.head.querySelectorAll("style,link[rel=stylesheet]")];
|
|
232
|
+
context.obsoleteStyles = authoredStyles.filter(
|
|
233
|
+
(el) => !incoming.some((other) => el.isEqualNode(other)),
|
|
234
|
+
);
|
|
235
|
+
dispatchNavigation("astro:after-preparation", context, next);
|
|
236
|
+
},
|
|
237
|
+
{ priority: -100 },
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Assets are ready before old interactions are disposed. Replacement errors
|
|
241
|
+
// propagate to Swup's full-document fallback instead of leaving a dead page.
|
|
242
|
+
swup.hooks.replace("content:replace", async (visit, args, replace) => {
|
|
243
|
+
const context = contexts.get(visit);
|
|
244
|
+
// Swup can release an obsolete visit's parsed document while its async
|
|
245
|
+
// hook chain is still unwinding. Cancellation precedes validation: a
|
|
246
|
+
// missing document on an aborted visit is not a failed navigation.
|
|
247
|
+
if (context?.controller.signal.aborted || visit.done || visit !== swup.visit) return;
|
|
248
|
+
const next = visit.to.document;
|
|
249
|
+
if (!context || !next || !replace) throw new Error("Missing Swup document");
|
|
250
|
+
if (context.controller.signal.aborted) return;
|
|
251
|
+
// Keep the old page readable until HTML, styles and fonts are ready.
|
|
252
|
+
if (context.navigationType !== "traverse" && context.preset !== "none") {
|
|
253
|
+
context.releaseOutgoing = await animateRouteOut(
|
|
254
|
+
document.querySelector<HTMLElement>(MAIN),
|
|
255
|
+
context.controller.signal,
|
|
256
|
+
);
|
|
257
|
+
if (context.controller.signal.aborted || visit !== swup.visit) {
|
|
258
|
+
context.releaseOutgoing();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const html = document.documentElement;
|
|
263
|
+
html.dataset.staluxVtFrom = getRouteKind(context.from.pathname);
|
|
264
|
+
html.dataset.staluxVtTo = getRouteKind(context.to.pathname);
|
|
265
|
+
html.dataset.staluxVtDirection = context.direction;
|
|
266
|
+
html.dataset.staluxVtPreset = context.preset;
|
|
267
|
+
context.motion = prepareRouteMotion(dispatchNavigation("astro:before-swap", context, next));
|
|
268
|
+
if (context.controller.signal.aborted) return;
|
|
269
|
+
if (context.navigationType !== "traverse" && context.preset !== "none")
|
|
270
|
+
html.dataset.staluxMotionPending = "";
|
|
271
|
+
await replace(visit, args);
|
|
272
|
+
context.releaseOutgoing?.();
|
|
273
|
+
if (context.controller.signal.aborted) return;
|
|
274
|
+
for (const element of context.obsoleteStyles) element.remove();
|
|
275
|
+
const incomingStyles = [...next.head.querySelectorAll("style,link[rel=stylesheet]")];
|
|
276
|
+
authoredStyles = [...document.head.querySelectorAll("style,link[rel=stylesheet]")].filter(
|
|
277
|
+
(el) => incomingStyles.some((other) => el.isEqualNode(other)),
|
|
278
|
+
);
|
|
279
|
+
// Main alone is replaced: header/footer/sentinel and background layers
|
|
280
|
+
// keep their node identities. Never reset live background state to SSR 0.
|
|
281
|
+
for (const { name, value } of [...next.body.attributes]) {
|
|
282
|
+
if (
|
|
283
|
+
name.startsWith("data-stalux-") &&
|
|
284
|
+
name !== "data-stalux-bg-index" &&
|
|
285
|
+
name !== "data-stalux-bg-layer"
|
|
286
|
+
)
|
|
287
|
+
document.body.setAttribute(name, value);
|
|
288
|
+
}
|
|
289
|
+
html.dataset.staluxRoute = getRouteKind(context.to.pathname);
|
|
290
|
+
visibleHistoryIndex = Number(window.history.state?.index ?? visibleHistoryIndex);
|
|
291
|
+
dispatchNavigation("astro:after-swap", context, next);
|
|
292
|
+
context.scriptsReady = context.executeScripts?.(context.controller.signal);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
swup.hooks.replace(
|
|
296
|
+
"content:scroll",
|
|
297
|
+
async (visit, args, defaultScroll) => {
|
|
298
|
+
const saved = visit.history.popstate
|
|
299
|
+
? historyPositions.get(Number(window.history.state?.index))
|
|
300
|
+
: undefined;
|
|
301
|
+
if (visit.history.popstate) {
|
|
302
|
+
// ScrollPlugin defers unanimated popstate to the browser, but Swup
|
|
303
|
+
// uses manual restoration even for reduced motion and native swipes.
|
|
304
|
+
const position = saved ??
|
|
305
|
+
scroll.getCachedScrollPositions(visit.to.url)?.window ?? { top: 0, left: 0 };
|
|
306
|
+
window.scrollTo({ top: position.top, left: position.left, behavior: "instant" });
|
|
307
|
+
scroll.restoreScrollContainers(visit.to.url);
|
|
308
|
+
if (saved?.focus) {
|
|
309
|
+
const link = [...document.querySelectorAll<HTMLAnchorElement>("a[href]")].find(
|
|
310
|
+
(el) => el.getAttribute("href") === saved.focus,
|
|
311
|
+
);
|
|
312
|
+
link?.focus({ preventScroll: true });
|
|
313
|
+
}
|
|
314
|
+
startMotion(visit);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
await defaultScroll?.(visit, args);
|
|
318
|
+
// ScrollPlugin scrolls in rAF; finish it before heading visibility checks.
|
|
319
|
+
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
320
|
+
startMotion(visit);
|
|
321
|
+
return true;
|
|
322
|
+
},
|
|
323
|
+
{ priority: 10 },
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
function startMotion(visit: Visit): void {
|
|
327
|
+
const context = contexts.get(visit);
|
|
328
|
+
if (!context || context !== activeContext || visit !== swup.visit) return;
|
|
329
|
+
delete document.documentElement.dataset.staluxMotionPending;
|
|
330
|
+
context?.motion?.start(document.querySelector<HTMLElement>(MAIN), {
|
|
331
|
+
preset: context.preset,
|
|
332
|
+
content: true,
|
|
333
|
+
skip: !visit.animation.animate || context.controller.signal.aborted,
|
|
334
|
+
browserMotion: context.browserMotion,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
swup.hooks.on("page:view", async (visit) => {
|
|
339
|
+
const context = contexts.get(visit);
|
|
340
|
+
if (!context || context.controller.signal.aborted) return;
|
|
341
|
+
await context.scriptsReady;
|
|
342
|
+
if (context.controller.signal.aborted) return;
|
|
343
|
+
dispatchNavigation("astro:page-load", context);
|
|
344
|
+
if (!visit.animation.animate) context.motion?.start(null, { skip: true });
|
|
345
|
+
});
|
|
346
|
+
swup.hooks.replace("link:self", () => undefined);
|
|
347
|
+
|
|
348
|
+
const cancel = (visit: Visit) => {
|
|
349
|
+
const context = contexts.get(visit);
|
|
350
|
+
context?.controller.abort();
|
|
351
|
+
context?.releaseOutgoing?.();
|
|
352
|
+
context?.motion?.cancel();
|
|
353
|
+
if (context === activeContext) {
|
|
354
|
+
clearMotionState();
|
|
355
|
+
document.documentElement.removeAttribute("aria-busy");
|
|
356
|
+
activeContext = undefined;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
swup.hooks.on("visit:abort", cancel);
|
|
360
|
+
swup.hooks.on("visit:fail", cancel);
|
|
361
|
+
swup.hooks.on("visit:end", (visit) => {
|
|
362
|
+
const context = contexts.get(visit);
|
|
363
|
+
context?.releaseOutgoing?.();
|
|
364
|
+
context?.motion?.start(null, { skip: true });
|
|
365
|
+
if (activeContext === context) {
|
|
366
|
+
clearMotionState();
|
|
367
|
+
activeContext = undefined;
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
swup.hooks.on("cache:set", () => {
|
|
371
|
+
while (swup.cache.size > 32)
|
|
372
|
+
swup.cache.delete(swup.cache.all.keys().next().value as string);
|
|
373
|
+
});
|
|
374
|
+
// Parsed DOM, not window.load: slow third-party resources cannot hold the
|
|
375
|
+
// first page's interactions hostage. Page-runtime dedupes the early mount.
|
|
376
|
+
const initialLoad = () => document.dispatchEvent(new Event("astro:page-load"));
|
|
377
|
+
if (document.readyState === "loading")
|
|
378
|
+
document.addEventListener("DOMContentLoaded", initialLoad, { once: true });
|
|
379
|
+
else queueMicrotask(initialLoad);
|
|
380
|
+
return swup;
|
|
381
|
+
}
|
|
@@ -1,234 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import { installBackToTop } from "./back-to-top";
|
|
4
|
-
import { createClientLogger } from "./logger";
|
|
5
|
-
import { prepareNavigationFont } from "./navigation-font";
|
|
6
|
-
import { prepareRouteMotion, type RouteMotion } from "./route-motion";
|
|
1
|
+
// Keep the layout entry stable while Swup owns navigation and animation.
|
|
2
|
+
import { installSwupRouter } from "./swup-router";
|
|
7
3
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
interface TransitionWindow extends Window {
|
|
11
|
-
__STALUX_VT_LISTENER__?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function setRouteState(pathname: string): void {
|
|
15
|
-
const html = document.documentElement;
|
|
16
|
-
const route = getRouteKind(pathname);
|
|
17
|
-
logger.debug(`route state=${route}`);
|
|
18
|
-
if (html.dataset.staluxRoute !== route) html.dataset.staluxRoute = route;
|
|
19
|
-
const main = document.querySelector("main[data-stalux-route]");
|
|
20
|
-
if (main && main.getAttribute("data-stalux-route") !== route) {
|
|
21
|
-
main.setAttribute("data-stalux-route", route);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface NavigationEvent {
|
|
26
|
-
from: URL;
|
|
27
|
-
to: URL;
|
|
28
|
-
direction: string;
|
|
29
|
-
navigationType?: string;
|
|
30
|
-
loader: () => Promise<void>;
|
|
31
|
-
newDocument: Document;
|
|
32
|
-
signal: AbortSignal;
|
|
33
|
-
viewTransition?: ViewTransition;
|
|
34
|
-
sourceElement?: Element;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function setNavigationState(
|
|
38
|
-
event: NavigationEvent,
|
|
39
|
-
preset: RouteMotionPreset,
|
|
40
|
-
html = document.documentElement,
|
|
41
|
-
): void {
|
|
42
|
-
const from = getRouteKind(new URL(event.from, window.location.href).pathname);
|
|
43
|
-
const to = getRouteKind(new URL(event.to, window.location.href).pathname);
|
|
44
|
-
html.dataset.staluxVtFrom = from;
|
|
45
|
-
html.dataset.staluxVtTo = to;
|
|
46
|
-
html.dataset.staluxVtDirection = event.direction;
|
|
47
|
-
html.dataset.staluxVtPreset = preset;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function clearNavigationState(): void {
|
|
51
|
-
const html = document.documentElement;
|
|
52
|
-
delete html.dataset.staluxVtFrom;
|
|
53
|
-
delete html.dataset.staluxVtTo;
|
|
54
|
-
delete html.dataset.staluxVtDirection;
|
|
55
|
-
delete html.dataset.staluxVtPreset;
|
|
56
|
-
delete html.dataset.staluxVtPhase;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function preserveHeaderMetrics(nextDocument: Document): void {
|
|
60
|
-
const selector = '[data-astro-transition-persist="stalux-site-header"]';
|
|
61
|
-
if (!document.querySelector(selector) || !nextDocument.querySelector(selector)) return;
|
|
62
|
-
// The header DOM persists. Keep its measured dimensions through Astro's root
|
|
63
|
-
// attribute swap instead of invalidating inherited layout variables twice.
|
|
64
|
-
for (const property of ["--stalux-nav-offset", "--stalux-site-header-height"]) {
|
|
65
|
-
const value = document.documentElement.style.getPropertyValue(property);
|
|
66
|
-
if (value && !nextDocument.documentElement.style.getPropertyValue(property)) {
|
|
67
|
-
nextDocument.documentElement.style.setProperty(property, value);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const transitionWindow = window as TransitionWindow;
|
|
73
|
-
if (!transitionWindow.__STALUX_VT_LISTENER__) {
|
|
74
|
-
transitionWindow.__STALUX_VT_LISTENER__ = true;
|
|
75
|
-
let pendingNavigation = 0;
|
|
76
|
-
let swappedNavigation = 0;
|
|
77
|
-
|
|
78
|
-
// A current-page link is not a page replacement. Intercept before Astro's
|
|
79
|
-
// delegated handler, but keep propagation for menu cleanup and click tracking.
|
|
80
|
-
// During preparation, Astro must receive it to cancel an incoming route.
|
|
81
|
-
// Hash changes, explicit reloads, downloads and modified/new-tab clicks keep
|
|
82
|
-
// their native behavior.
|
|
83
|
-
document.addEventListener(
|
|
84
|
-
"click",
|
|
85
|
-
(event) => {
|
|
86
|
-
if (
|
|
87
|
-
pendingNavigation !== 0 ||
|
|
88
|
-
event.defaultPrevented ||
|
|
89
|
-
event.button !== 0 ||
|
|
90
|
-
event.metaKey ||
|
|
91
|
-
event.ctrlKey ||
|
|
92
|
-
event.shiftKey ||
|
|
93
|
-
event.altKey
|
|
94
|
-
)
|
|
95
|
-
return;
|
|
96
|
-
const anchor =
|
|
97
|
-
event.target instanceof Element
|
|
98
|
-
? event.target.closest<HTMLAnchorElement>("a[href]")
|
|
99
|
-
: null;
|
|
100
|
-
if (
|
|
101
|
-
!anchor ||
|
|
102
|
-
anchor.href !== window.location.href ||
|
|
103
|
-
anchor.href.includes("#") ||
|
|
104
|
-
anchor.hasAttribute("download") ||
|
|
105
|
-
anchor.hasAttribute("data-astro-reload") ||
|
|
106
|
-
anchor.relList.contains("external")
|
|
107
|
-
)
|
|
108
|
-
return;
|
|
109
|
-
const target =
|
|
110
|
-
anchor.getAttribute("target") ??
|
|
111
|
-
document.querySelector("base[target]")?.getAttribute("target");
|
|
112
|
-
if (!target || target === "_self") event.preventDefault();
|
|
113
|
-
},
|
|
114
|
-
{ capture: true },
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
let generation = 0;
|
|
118
|
-
let pendingBrowserMotion = false;
|
|
119
|
-
let preset: RouteMotionPreset = "none";
|
|
120
|
-
let motion: RouteMotion | undefined;
|
|
121
|
-
window.addEventListener(
|
|
122
|
-
"popstate",
|
|
123
|
-
(event) => {
|
|
124
|
-
pendingBrowserMotion =
|
|
125
|
-
(event as PopStateEvent & { hasUAVisualTransition?: boolean })
|
|
126
|
-
.hasUAVisualTransition === true;
|
|
127
|
-
},
|
|
128
|
-
{ capture: true },
|
|
129
|
-
);
|
|
130
|
-
document.addEventListener("astro:before-preparation", (event) => {
|
|
131
|
-
const navigation = event as unknown as NavigationEvent;
|
|
132
|
-
const current = ++generation;
|
|
133
|
-
pendingNavigation = current;
|
|
134
|
-
const cancelPreparation = () => {
|
|
135
|
-
if (current !== generation) return;
|
|
136
|
-
pendingNavigation = 0;
|
|
137
|
-
clearNavigationState();
|
|
138
|
-
};
|
|
139
|
-
motion?.cancel();
|
|
140
|
-
clearNavigationState();
|
|
141
|
-
// CSS moves only explicitly marked content, never the root or sidebars.
|
|
142
|
-
// Long article geometry does not need to be measured or captured.
|
|
143
|
-
preset = getRouteMotionPreset({
|
|
144
|
-
from: new URL(navigation.from, window.location.href),
|
|
145
|
-
to: new URL(navigation.to, window.location.href),
|
|
146
|
-
navigationType: navigation.navigationType,
|
|
147
|
-
intent:
|
|
148
|
-
navigation.sourceElement
|
|
149
|
-
?.closest("[data-stalux-motion]")
|
|
150
|
-
?.getAttribute("data-stalux-motion") ?? undefined,
|
|
151
|
-
reducedMotion: window.matchMedia?.("(prefers-reduced-motion: reduce)").matches,
|
|
152
|
-
browserMotion: pendingBrowserMotion && navigation.navigationType === "traverse",
|
|
153
|
-
hidden: document.hidden,
|
|
154
|
-
});
|
|
155
|
-
pendingBrowserMotion = false;
|
|
156
|
-
setNavigationState(navigation, preset);
|
|
157
|
-
navigation.signal.addEventListener("abort", cancelPreparation, { once: true });
|
|
158
|
-
if (navigation.signal.aborted) cancelPreparation();
|
|
159
|
-
queueMicrotask(() => {
|
|
160
|
-
if (event.defaultPrevented) cancelPreparation();
|
|
161
|
-
});
|
|
162
|
-
const loader = navigation.loader;
|
|
163
|
-
navigation.loader = async () => {
|
|
164
|
-
try {
|
|
165
|
-
await loader();
|
|
166
|
-
if (event.defaultPrevented) cancelPreparation();
|
|
167
|
-
else {
|
|
168
|
-
await prepareNavigationFont(navigation.newDocument, navigation.signal);
|
|
169
|
-
}
|
|
170
|
-
} catch (error) {
|
|
171
|
-
cancelPreparation();
|
|
172
|
-
throw error;
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
document.addEventListener("astro:before-swap", (event) => {
|
|
178
|
-
const navigation = event as unknown as NavigationEvent;
|
|
179
|
-
if (navigation.signal.aborted) return;
|
|
180
|
-
const current = generation;
|
|
181
|
-
swappedNavigation = current;
|
|
182
|
-
preserveHeaderMetrics(navigation.newDocument);
|
|
183
|
-
// Astro replaces the root attributes during swap. Carry state into the
|
|
184
|
-
// new document; CSS keyframes read these data-* attributes to pick their
|
|
185
|
-
// own direction rather than letting Astro's default `fade` pair apply.
|
|
186
|
-
setNavigationState(navigation, preset, navigation.newDocument.documentElement);
|
|
187
|
-
if (preset !== "none") {
|
|
188
|
-
navigation.newDocument.documentElement.dataset.staluxVtPhase = "prepared";
|
|
189
|
-
}
|
|
190
|
-
// This release uses CSS/native VT only. Settle the existing installation
|
|
191
|
-
// gate without starting a second WAAPI effect; analytics/background keep
|
|
192
|
-
// the same shared ready/finished semantics and their existing listeners.
|
|
193
|
-
motion = prepareRouteMotion(navigation);
|
|
194
|
-
motion.start(null, { skip: true });
|
|
195
|
-
// Preparing the incoming content paused prevents a one-frame flash at
|
|
196
|
-
// its final position. Release it with the heading's native animation;
|
|
197
|
-
// page scripts and Astro's scroll restoration keep their normal order.
|
|
198
|
-
void Promise.resolve(navigation.viewTransition?.ready).then(
|
|
199
|
-
() => {
|
|
200
|
-
if (
|
|
201
|
-
current === generation &&
|
|
202
|
-
!navigation.signal.aborted &&
|
|
203
|
-
document.documentElement.dataset.staluxVtPhase === "prepared"
|
|
204
|
-
) {
|
|
205
|
-
document.documentElement.dataset.staluxVtPhase = "running";
|
|
206
|
-
}
|
|
207
|
-
},
|
|
208
|
-
() => {},
|
|
209
|
-
);
|
|
210
|
-
const finish = () => {
|
|
211
|
-
if (current === generation) clearNavigationState();
|
|
212
|
-
};
|
|
213
|
-
// Native ready starts the animation. Keep selectors until it finishes,
|
|
214
|
-
// including fallback/skip and abort; an older navigation cannot clear a
|
|
215
|
-
// newer one's state.
|
|
216
|
-
void motion.finished.then(finish, finish);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
document.addEventListener("astro:after-swap", () => {
|
|
220
|
-
setRouteState(window.location.pathname);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
document.addEventListener("astro:page-load", () => {
|
|
224
|
-
// A previous route's scripts can finish while a newer loader is pending.
|
|
225
|
-
if (swappedNavigation === generation) pendingNavigation = 0;
|
|
226
|
-
setRouteState(window.location.pathname);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// Footer button and scroll sentinel persist across soft navigations, so the
|
|
230
|
-
// BackToTop observer is installed once per document.
|
|
231
|
-
installBackToTop();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
setRouteState(window.location.pathname);
|
|
4
|
+
installSwupRouter();
|
package/src/scripts/word-list.ts
CHANGED
|
@@ -7,8 +7,14 @@ function scrollToWord(hash: string): void {
|
|
|
7
7
|
highlighted.removeAttribute("data-glow");
|
|
8
8
|
}
|
|
9
9
|
element.dataset.glow = "";
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
11
|
+
element.scrollIntoView({ behavior: reducedMotion ? "instant" : "smooth", block: "center" });
|
|
12
|
+
// Keep Swup's source/index so back from another page can restore this entry.
|
|
13
|
+
history.replaceState(
|
|
14
|
+
{ ...history.state, url: `${location.pathname}${location.search}${hash}` },
|
|
15
|
+
"",
|
|
16
|
+
hash,
|
|
17
|
+
);
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
registerPageLifecycle("word-list", () => {
|
|
@@ -17,7 +23,7 @@ registerPageLifecycle("word-list", () => {
|
|
|
17
23
|
let firstFrame = 0;
|
|
18
24
|
let secondFrame = 0;
|
|
19
25
|
|
|
20
|
-
if (location.hash.startsWith("#word-")) {
|
|
26
|
+
if (location.hash.startsWith("#word-") && !window.swup?.visit.history.popstate) {
|
|
21
27
|
firstFrame = requestAnimationFrame(() => {
|
|
22
28
|
firstFrame = 0;
|
|
23
29
|
secondFrame = requestAnimationFrame(() => {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
@layer theme, base, components, utilities;
|
|
2
|
-
@layer components{.stalux-word-card{transition:border-color var(--transition-normal), box-shadow var(--transition-normal);box-sizing:border-box;border-radius:var(--radius-lg,12px);background:var(--surface-glass);width:100%;min-width:0;box-shadow:var(--card-box-shadow);flex-direction:column;gap:
|
|
2
|
+
@layer components{.stalux-word-card{transition:border-color var(--transition-normal), box-shadow var(--transition-normal);box-sizing:border-box;border-radius:var(--radius-lg,12px);background:var(--surface-glass);width:100%;min-width:0;box-shadow:var(--card-box-shadow);flex-direction:column;gap:1.5rem;padding:2rem 2rem 1.25rem;display:flex;position:relative}.stalux-word-date{font-variant-numeric:tabular-nums;color:var(--white-50p)}.stalux-word-footer{border-top:1px solid var(--white-10p);color:var(--white-70p);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.75rem;margin-top:auto;padding-top:1rem;font-size:.85rem;display:flex}.stalux-word-source-link{text-decoration-color:var(--accent-50p);text-underline-offset:.15em;transition:color var(--transition-fast);color:var(--accent-90p);text-decoration-line:underline}.stalux-word-body{min-width:0;color:var(--white-90p);font-size:1.1rem;line-height:1.9;position:relative}.stalux-word-list-list{flex-direction:column;align-items:center;gap:1.5rem;margin:2rem auto;padding:0;list-style-type:none;display:flex}.stalux-word-list-item{width:100%;max-width:900px;display:flex}.stalux-word-source-meta{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:.5rem;display:flex}.stalux-word-source-date{color:var(--white-70p)}.stalux-word-source-text{color:var(--white-90p)}.stalux-word-card{border-left:2px solid var(--accent-50p);background:linear-gradient(135deg, rgb(var(--stalux-accent-rgb) / .06), transparent 55%), #121418eb;overflow-wrap:anywhere;scroll-margin-top:6rem}.stalux-word-mark{color:var(--accent-60p);opacity:.45;pointer-events:none;position:absolute;top:1.5rem;right:1.5rem}.stalux-word-body{padding-right:2.5rem}.stalux-word-footer{backdrop-filter:none;text-align:left;background:0 0;padding:.75rem 0 0}.stalux-word-source-meta{text-align:right}.stalux-word-source-link{align-items:center;min-height:44px;display:inline-flex}.stalux-word-permalink{min-height:44px;color:var(--white-70p);font-variant-numeric:tabular-nums;align-items:center;display:inline-flex}.stalux-word-permalink:focus-visible,.stalux-word-source-link:focus-visible{outline:2px solid var(--stalux-accent-color);outline-offset:4px;border-radius:4px}.stalux-word-source-meta{min-width:0;margin-left:auto}.stalux-word-body>:first-child{margin-top:0}.stalux-word-body>:last-child{margin-bottom:0}.stalux-word-body .expressive-code{max-width:100%;overflow-x:auto}.stalux-word-card:target,.stalux-word-card[data-glow]{box-shadow:0 0 20px var(--accent-40p), 0 0 60px var(--accent-20p), var(--shadow-lg);animation:2s ease-out stalux-word-word-glow}.stalux-word-body h1,.stalux-word-body h2,.stalux-word-body h3,.stalux-word-body h4,.stalux-word-body h5,.stalux-word-body h6{margin:.5rem 0;font-size:1.1em}.stalux-word-body p{margin:.5rem 0}.stalux-word-body blockquote{border-left:3px solid var(--accent-60p);background:var(--white-05p);margin:.5rem 0;padding:.6rem 1rem}.stalux-word-body pre{white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:visible}.stalux-word-body code{overflow-wrap:anywhere}@media (hover:hover) and (pointer:fine){.stalux-word-card:hover{border-color:var(--accent-80p);box-shadow:var(--card-box-shadow-hover)}.stalux-word-source-link:hover{color:var(--accent-90p)}}@media (width<=768px){.stalux-word-card{padding:1.25rem}.stalux-word-list-list{gap:1rem;margin:1.5rem auto}.stalux-word-list-item{max-width:100%}}}@layer utilities,theme,base;
|
|
3
3
|
/*$vite$:1*/
|