@xingwangzhe/stalux 1.46.2 → 1.46.5
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 +3 -2
- package/src/components/stalux/archives/archivesList.astro +1 -1
- package/src/components/stalux/categories/categoriesList.astro +6 -1
- package/src/components/stalux/common/Weather.astro +1 -1
- package/src/components/stalux/common/date.astro +1 -1
- package/src/components/stalux/footer/FooterBeian.astro +2 -0
- package/src/components/stalux/layout/head.astro +1 -1
- package/src/components/stalux/links/linkList.astro +1 -1
- package/src/components/stalux/posts/postContent.astro +5 -1
- package/src/components/stalux/posts/postList.astro +6 -1
- package/src/components/stalux/posts/postRight.astro +11 -4
- package/src/components/stalux/words/wordList.astro +11 -8
- package/src/index.ts +1 -1
- package/src/internal/compact-styles.ts +1 -1
- package/src/internal/font-slices.ts +3 -3
- package/src/internal/page-output-cache.ts +6 -7
- package/src/internal/social-card-renderer.ts +3 -5
- package/src/pages/og/[image].[format].ts +1 -1
- package/src/schemas/config.ts +5 -5
- package/src/scripts/analytics-runtime.ts +1 -1
- package/src/scripts/archive-descriptions.ts +1 -1
- package/src/scripts/back-to-top.ts +7 -6
- package/src/scripts/background.ts +2 -2
- package/src/scripts/countdown.ts +1 -1
- package/src/scripts/date-time.ts +1 -1
- package/src/scripts/list-reveal.ts +39 -18
- package/src/scripts/module-motion.ts +5 -3
- package/src/scripts/navigation-font.ts +4 -1
- package/src/scripts/navigation.ts +1 -1
- package/src/scripts/page-runtime.ts +6 -4
- package/src/scripts/random-posts.ts +2 -2
- package/src/scripts/route-motion.ts +67 -56
- package/src/scripts/route-out.ts +12 -14
- package/src/scripts/runtime-counter.ts +1 -1
- package/src/scripts/shell-hydration.ts +60 -0
- package/src/scripts/smooth-scroll.ts +4 -4
- package/src/scripts/swup-internals.d.ts +31 -0
- package/src/scripts/swup-router.ts +26 -11
- package/src/scripts/waline.ts +1 -1
- package/src/scripts/weather.ts +148 -82
- package/src/scripts/webmcp.ts +24 -20
- package/src/utils/ai-discovery.ts +1 -1
- package/src/utils/cc.ts +2 -2
- package/src/utils/collections-stats.ts +4 -2
- package/src/utils/dayjs.ts +2 -2
- package/src/utils/i18n.ts +2 -2
- package/src/utils/seo-document.ts +4 -4
- package/src/utils/taxonomy.ts +10 -12
|
@@ -37,17 +37,17 @@ interface HeadingMotion {
|
|
|
37
37
|
// Legacy heading cues remain available to integrations. The router requests
|
|
38
38
|
// one content animation; it never stacks a second animation on the heading.
|
|
39
39
|
const headingMotions: Record<Exclude<RouteMotionPreset, "none">, HeadingMotion> = {
|
|
40
|
-
glide: { opacity: 0.86, duration:
|
|
41
|
-
article: { opacity: 0.86, duration:
|
|
42
|
-
enter: { opacity: 0.86, duration:
|
|
43
|
-
up: { opacity: 0.9, duration:
|
|
44
|
-
taxonomy: { opacity: 0.85, duration:
|
|
45
|
-
previous: { opacity: 0.9, duration:
|
|
46
|
-
next: { opacity: 0.9, duration:
|
|
47
|
-
panel: { opacity: 0.9, duration:
|
|
48
|
-
home: { opacity: 0.9, duration:
|
|
49
|
-
restore: { opacity: 0.96, duration:
|
|
50
|
-
error: { opacity: 0.9, duration:
|
|
40
|
+
glide: { opacity: 0.86, duration: 50 },
|
|
41
|
+
article: { opacity: 0.86, duration: 50 },
|
|
42
|
+
enter: { opacity: 0.86, duration: 70, y: 6 },
|
|
43
|
+
up: { opacity: 0.9, duration: 60, y: -4 },
|
|
44
|
+
taxonomy: { opacity: 0.85, duration: 60 },
|
|
45
|
+
previous: { opacity: 0.9, duration: 70, x: -8 },
|
|
46
|
+
next: { opacity: 0.9, duration: 70, x: 8 },
|
|
47
|
+
panel: { opacity: 0.9, duration: 60, y: 4 },
|
|
48
|
+
home: { opacity: 0.9, duration: 60, y: 4 },
|
|
49
|
+
restore: { opacity: 0.96, duration: 40 },
|
|
50
|
+
error: { opacity: 0.9, duration: 40 },
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
const MAIN = "main[data-stalux-route]";
|
|
@@ -55,41 +55,50 @@ const ARTICLE = "article.stalux-article";
|
|
|
55
55
|
// Authored per page type by the theme, not inferred from layout: the article
|
|
56
56
|
// body, archive list, tag cloud, post list, word list, link list, home block.
|
|
57
57
|
const CONTENT = "[data-stalux-motion-content]";
|
|
58
|
+
// The one node a page nominates as its cue stage. Preferred over measuring,
|
|
59
|
+
// because measuring an article column forces a full layout of a document that
|
|
60
|
+
// is tens of thousands of pixels tall, before it has ever been painted.
|
|
61
|
+
const STAGE = "[data-stalux-motion-stage]";
|
|
58
62
|
// A persist node marks the three-column article shell. Those sidebars keep
|
|
59
63
|
// their identity across visits and must never travel with a route cue.
|
|
60
64
|
const PERSIST = "[data-swup-persist]";
|
|
61
65
|
|
|
62
66
|
interface StageMotion {
|
|
63
67
|
opacity: number;
|
|
64
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Enter offset in px. Positive y enters from below, positive x from the right.
|
|
70
|
+
* Zero means a pure cross-fade: the column is replaced in place.
|
|
71
|
+
*/
|
|
65
72
|
distance: number;
|
|
66
73
|
axis: "x" | "y";
|
|
67
74
|
duration: number;
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
// One entry per route kind, because the components are not interchangeable: an
|
|
71
|
-
// article opens from below
|
|
72
|
-
//
|
|
78
|
+
// article opens from below and going back up to an index pulls away upward.
|
|
79
|
+
//
|
|
80
|
+
// Article-to-article is the exception: the reader keeps the same shell (sidebars,
|
|
81
|
+
// header) and only the column changes, so the column fades in place instead of
|
|
82
|
+
// travelling. Sliding a 28,000px column sideways made the whole page look like it
|
|
83
|
+
// jerked, and the shell already tells the reader where they are.
|
|
73
84
|
const stageMotions: Record<Exclude<RouteMotionPreset, "none">, StageMotion> = {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
previous: { opacity: 0, distance: -36, axis: "x", duration: 300 },
|
|
77
|
-
next: { opacity: 0, distance: 36, axis: "x", duration: 300 },
|
|
85
|
+
previous: { opacity: 0, distance: 0, axis: "x", duration: 150 },
|
|
86
|
+
next: { opacity: 0, distance: 0, axis: "x", duration: 150 },
|
|
78
87
|
// Entering a different article without a reading-direction trigger.
|
|
79
|
-
article: { opacity: 0, distance:
|
|
88
|
+
article: { opacity: 0, distance: 0, axis: "y", duration: 150 },
|
|
80
89
|
// Reaching an article from any list or the home block.
|
|
81
|
-
enter: { opacity: 0, distance: 56, axis: "y", duration:
|
|
82
|
-
home: { opacity: 0, distance: 48, axis: "y", duration:
|
|
90
|
+
enter: { opacity: 0, distance: 56, axis: "y", duration: 170 },
|
|
91
|
+
home: { opacity: 0, distance: 48, axis: "y", duration: 160 },
|
|
83
92
|
// Leaving an article for the archive or a taxonomy index.
|
|
84
|
-
up: { opacity: 0, distance: -44, axis: "y", duration:
|
|
93
|
+
up: { opacity: 0, distance: -44, axis: "y", duration: 150 },
|
|
85
94
|
// Moving between two taxonomy detail pages.
|
|
86
|
-
taxonomy: { opacity: 0, distance: 36, axis: "y", duration:
|
|
95
|
+
taxonomy: { opacity: 0, distance: 36, axis: "y", duration: 140 },
|
|
87
96
|
// About / links / words: single-panel pages.
|
|
88
|
-
panel: { opacity: 0, distance: 44, axis: "y", duration:
|
|
89
|
-
glide: { opacity: 0, distance: 40, axis: "y", duration:
|
|
97
|
+
panel: { opacity: 0, distance: 44, axis: "y", duration: 150 },
|
|
98
|
+
glide: { opacity: 0, distance: 40, axis: "y", duration: 140 },
|
|
90
99
|
// History traversal restores what the reader already saw.
|
|
91
|
-
restore: { opacity: 0, distance: -36, axis: "y", duration:
|
|
92
|
-
error: { opacity: 0, distance: 28, axis: "y", duration:
|
|
100
|
+
restore: { opacity: 0, distance: -36, axis: "y", duration: 140 },
|
|
101
|
+
error: { opacity: 0, distance: 28, axis: "y", duration: 130 },
|
|
93
102
|
};
|
|
94
103
|
|
|
95
104
|
const laidOut = (element: HTMLElement): boolean => {
|
|
@@ -100,14 +109,17 @@ const laidOut = (element: HTMLElement): boolean => {
|
|
|
100
109
|
/**
|
|
101
110
|
* The single node a route cue may animate for the current document.
|
|
102
111
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
112
|
+
* A page can nominate its stage; only when it does not is the largest authored
|
|
113
|
+
* content stage measured. Measuring is the fallback, not the rule: on an article
|
|
114
|
+
* the column is tens of thousands of pixels tall and measuring it forces a full
|
|
115
|
+
* layout of the incoming document at the exact moment the entry cue needs to
|
|
116
|
+
* start. Sidebars (persisted shells), the header and the footer never qualify.
|
|
107
117
|
*/
|
|
108
118
|
export function getRouteStage(): HTMLElement | null {
|
|
109
119
|
const main = document.querySelector<HTMLElement>(MAIN);
|
|
110
120
|
if (!main?.isConnected) return null;
|
|
121
|
+
const authored = main.querySelector<HTMLElement>(STAGE);
|
|
122
|
+
if (authored?.isConnected && !authored.closest(PERSIST)) return authored;
|
|
111
123
|
const candidates: HTMLElement[] = [];
|
|
112
124
|
const article = main.querySelector<HTMLElement>(ARTICLE);
|
|
113
125
|
if (article) candidates.push(article);
|
|
@@ -156,7 +168,7 @@ function completion() {
|
|
|
156
168
|
|
|
157
169
|
/** A route transition never captures a long article, list, or off-screen heading. */
|
|
158
170
|
export function getRouteMotionTarget(focus: HTMLElement | null): HTMLElement | null {
|
|
159
|
-
if (!focus?.isConnected
|
|
171
|
+
if (!(focus?.isConnected && focus.hasAttribute("data-stalux-motion-focus"))) return null;
|
|
160
172
|
const visible = (element: HTMLElement, maxHeight: number) => {
|
|
161
173
|
const rect = element.getBoundingClientRect();
|
|
162
174
|
return (
|
|
@@ -177,17 +189,17 @@ export function getRouteMotionTarget(focus: HTMLElement | null): HTMLElement | n
|
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
const regionMotions: Record<Exclude<RouteMotionPreset, "none">, HeadingMotion> = {
|
|
180
|
-
glide: { opacity: 0.72, duration:
|
|
181
|
-
article: { opacity: 0.72, duration:
|
|
182
|
-
enter: { opacity: 0.72, duration:
|
|
183
|
-
up: { opacity: 0.8, duration:
|
|
184
|
-
taxonomy: { opacity: 0.72, duration:
|
|
185
|
-
previous: { opacity: 0.76, duration:
|
|
186
|
-
next: { opacity: 0.76, duration:
|
|
187
|
-
panel: { opacity: 0.76, duration:
|
|
188
|
-
home: { opacity: 0.76, duration:
|
|
192
|
+
glide: { opacity: 0.72, duration: 80 },
|
|
193
|
+
article: { opacity: 0.72, duration: 90, y: 4 },
|
|
194
|
+
enter: { opacity: 0.72, duration: 90, y: 6 },
|
|
195
|
+
up: { opacity: 0.8, duration: 80, y: -4 },
|
|
196
|
+
taxonomy: { opacity: 0.72, duration: 80, x: 6 },
|
|
197
|
+
previous: { opacity: 0.76, duration: 90, x: -8 },
|
|
198
|
+
next: { opacity: 0.76, duration: 90, x: 8 },
|
|
199
|
+
panel: { opacity: 0.76, duration: 80, y: 4 },
|
|
200
|
+
home: { opacity: 0.76, duration: 90, y: 4 },
|
|
189
201
|
restore: { opacity: 1, duration: 0 },
|
|
190
|
-
error: { opacity: 0.85, duration:
|
|
202
|
+
error: { opacity: 0.85, duration: 50 },
|
|
191
203
|
};
|
|
192
204
|
|
|
193
205
|
/** Share Swup's actual content animation with background and analytics consumers. */
|
|
@@ -281,19 +293,18 @@ export function prepareRouteMotion(event: RouteMotionEvent): RouteMotion {
|
|
|
281
293
|
spec.axis === "x"
|
|
282
294
|
? `translate3d(${distance}px, 0, 0)`
|
|
283
295
|
: `translate3d(0, ${distance}px, 0)`;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
);
|
|
296
|
+
// A zero distance is a cross-fade in place. Authoring a
|
|
297
|
+
// translate3d(0,0,0) would put a 28,000px column on its own
|
|
298
|
+
// compositor layer for nothing.
|
|
299
|
+
const keyframes: PropertyIndexedKeyframes = { opacity: [spec.opacity, 1] };
|
|
300
|
+
if (distance !== 0) keyframes.transform = [from, "translate3d(0, 0, 0)"];
|
|
301
|
+
animation = stage.animate(keyframes, {
|
|
302
|
+
duration: mobile ? Math.round(spec.duration * 0.9) : spec.duration,
|
|
303
|
+
// Strong ease-out: quick arrival, soft settle. The
|
|
304
|
+
// built-in ease-out is too weak to read at this size.
|
|
305
|
+
easing: "cubic-bezier(0.23, 1, 0.32, 1)",
|
|
306
|
+
fill: "both",
|
|
307
|
+
});
|
|
297
308
|
} else {
|
|
298
309
|
const spec = options?.content ? regionMotions[preset] : headingMotions[preset];
|
|
299
310
|
const keyframes: PropertyIndexedKeyframes = { opacity: [spec.opacity, 1] };
|
|
@@ -306,7 +317,7 @@ export function prepareRouteMotion(event: RouteMotionEvent): RouteMotion {
|
|
|
306
317
|
];
|
|
307
318
|
}
|
|
308
319
|
animation = target.animate(keyframes, {
|
|
309
|
-
duration: mobile ? (options?.content ?
|
|
320
|
+
duration: mobile ? (options?.content ? 60 : 40) : spec.duration,
|
|
310
321
|
easing: "cubic-bezier(0.2, 0.7, 0.2, 1)",
|
|
311
322
|
fill: "both",
|
|
312
323
|
});
|
package/src/scripts/route-out.ts
CHANGED
|
@@ -16,8 +16,7 @@ export async function animateRouteOut(
|
|
|
16
16
|
const spec = getStageExit(preset);
|
|
17
17
|
const stage = spec ? getRouteStage() : null;
|
|
18
18
|
if (
|
|
19
|
-
!spec ||
|
|
20
|
-
!stage ||
|
|
19
|
+
!(spec && stage) ||
|
|
21
20
|
signal.aborted ||
|
|
22
21
|
document.hidden ||
|
|
23
22
|
typeof stage.animate !== "function" ||
|
|
@@ -28,6 +27,10 @@ export async function animateRouteOut(
|
|
|
28
27
|
const distance = mobile ? Math.round(spec.distance * 0.6) : spec.distance;
|
|
29
28
|
const to =
|
|
30
29
|
spec.axis === "x" ? `translate3d(${distance}px, 0, 0)` : `translate3d(0, ${distance}px, 0)`;
|
|
30
|
+
// Article-to-article has no distance: the column fades out in place, matching
|
|
31
|
+
// the entry fade, so the shell stays put and only the content disappears.
|
|
32
|
+
const keyframes: PropertyIndexedKeyframes = { opacity: [1, 0] };
|
|
33
|
+
if (distance !== 0) keyframes.transform = ["translate3d(0, 0, 0)", to];
|
|
31
34
|
let animation: Animation | undefined;
|
|
32
35
|
const cancel = () => {
|
|
33
36
|
const active = animation;
|
|
@@ -39,18 +42,13 @@ export async function animateRouteOut(
|
|
|
39
42
|
}
|
|
40
43
|
};
|
|
41
44
|
try {
|
|
42
|
-
animation = stage.animate(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// is the one place ease-in is correct; entrances use ease-out.
|
|
50
|
-
easing: "cubic-bezier(0.4, 0, 1, 1)",
|
|
51
|
-
fill: "forwards",
|
|
52
|
-
},
|
|
53
|
-
);
|
|
45
|
+
animation = stage.animate(keyframes, {
|
|
46
|
+
duration: mobile ? Math.round(spec.duration * 0.62) : Math.round(spec.duration * 0.58),
|
|
47
|
+
// Strong ease-in: the outgoing document accelerates away. An exit
|
|
48
|
+
// is the one place ease-in is correct; entrances use ease-out.
|
|
49
|
+
easing: "cubic-bezier(0.4, 0, 1, 1)",
|
|
50
|
+
fill: "forwards",
|
|
51
|
+
});
|
|
54
52
|
signal.addEventListener("abort", cancel, { once: true });
|
|
55
53
|
await animation.finished;
|
|
56
54
|
// The stage node survives the swap, so this filled effect is what holds
|
|
@@ -4,7 +4,7 @@ import { observeVisibleClock } from "./visible-clock";
|
|
|
4
4
|
registerPageLifecycle("runtime-counter", () => {
|
|
5
5
|
const container = document.querySelector<HTMLElement>("[data-stalux-buildtime]");
|
|
6
6
|
const counter = document.getElementById("runtime-counter");
|
|
7
|
-
if (!container
|
|
7
|
+
if (!(container && counter)) return;
|
|
8
8
|
const start = new Date(container.dataset.staluxBuildtime ?? "");
|
|
9
9
|
const format =
|
|
10
10
|
container.dataset.staluxRuntimeFormat ?? "{days}d {hours}h {minutes}m {seconds}s";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep a shell's frame, refresh only the parts that describe the page.
|
|
3
|
+
*
|
|
4
|
+
* A `data-swup-persist` node is kept whole by Swup: that is what makes the
|
|
5
|
+
* sidebars, the header and the footer survive a visit. For a container whose
|
|
6
|
+
* *frame* is stable but whose *contents* belong to the page (the article's TOC,
|
|
7
|
+
* tag list and category list), keeping the node whole would show the previous
|
|
8
|
+
* article's headings. Marking it `data-stalux-shell-slot` says: keep this node,
|
|
9
|
+
* take the children from the page we are navigating to.
|
|
10
|
+
*
|
|
11
|
+
* Doing it here, synchronously after the swap and before `astro:after-swap`, means
|
|
12
|
+
* every page module that mounts later sees the refreshed lists and nothing has to
|
|
13
|
+
* be measured or re-rendered a second time.
|
|
14
|
+
*/
|
|
15
|
+
const SLOT = "[data-stalux-shell-slot]";
|
|
16
|
+
|
|
17
|
+
/** Persist nodes that existed before this swap; only their slots need refilling. */
|
|
18
|
+
const retained = new WeakSet<Element>();
|
|
19
|
+
|
|
20
|
+
/** Call before the content is replaced, while the old document is still live. */
|
|
21
|
+
export function markRetainedShells(): void {
|
|
22
|
+
for (const element of document.querySelectorAll("[data-swup-persist]")) retained.add(element);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function hydrateShells(next: Document | undefined): number {
|
|
26
|
+
if (!next || typeof next.querySelector !== "function") return 0;
|
|
27
|
+
let hydrated = 0;
|
|
28
|
+
for (const live of document.querySelectorAll<HTMLElement>(SLOT)) {
|
|
29
|
+
const slot = live.getAttribute?.("data-stalux-shell-slot");
|
|
30
|
+
if (!slot) continue;
|
|
31
|
+
// A slot that arrived with the incoming document already holds the right
|
|
32
|
+
// children; replacing them again would dirty layout for nothing.
|
|
33
|
+
const shell = live.closest("[data-swup-persist]");
|
|
34
|
+
if (!(shell && retained.has(shell))) continue;
|
|
35
|
+
const incoming = next.querySelector<HTMLElement>(`[data-stalux-shell-slot="${slot}"]`);
|
|
36
|
+
if (!incoming) continue;
|
|
37
|
+
// Two articles often share the same tags or the same category. Writing
|
|
38
|
+
// identical children back would dirty layout for a subtree that is
|
|
39
|
+
// already correct.
|
|
40
|
+
if (live.innerHTML === incoming.innerHTML) continue;
|
|
41
|
+
// The incoming nodes belong to another document; importNode adopts them.
|
|
42
|
+
const fresh = document.importNode(incoming, true);
|
|
43
|
+
live.replaceChildren(...fresh.childNodes);
|
|
44
|
+
// These lists are new content even though their frame is old, so the
|
|
45
|
+
// unroll cue still applies to them.
|
|
46
|
+
for (const group of live.querySelectorAll<HTMLElement>("[data-stalux-reveal]"))
|
|
47
|
+
group.setAttribute("data-stalux-shell-fresh", "");
|
|
48
|
+
hydrated++;
|
|
49
|
+
}
|
|
50
|
+
// A shell that is itself new (first visit to an article, or any move between
|
|
51
|
+
// page types) brings fresh lists with it: they are content, not a kept frame.
|
|
52
|
+
for (const group of document.querySelectorAll<HTMLElement>(
|
|
53
|
+
"[data-swup-persist] [data-stalux-reveal]",
|
|
54
|
+
)) {
|
|
55
|
+
const shell = group.closest("[data-swup-persist]");
|
|
56
|
+
if (shell && !retained.has(shell) && !group.hasAttribute("data-stalux-shell-fresh"))
|
|
57
|
+
group.setAttribute("data-stalux-shell-fresh", "");
|
|
58
|
+
}
|
|
59
|
+
return hydrated;
|
|
60
|
+
}
|
|
@@ -12,9 +12,9 @@ export interface PageGlide {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/** Bound the pre-swap return-to-top so a 6000px article does not add a second. */
|
|
15
|
-
const GLIDE_MIN_MS =
|
|
16
|
-
const GLIDE_MAX_MS =
|
|
17
|
-
const GLIDE_SAFETY_MS =
|
|
15
|
+
const GLIDE_MIN_MS = 80;
|
|
16
|
+
const GLIDE_MAX_MS = 180;
|
|
17
|
+
const GLIDE_SAFETY_MS = 380;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Walk the current document back to the top before the container is replaced.
|
|
@@ -42,7 +42,7 @@ export function glideToTop(): PageGlide {
|
|
|
42
42
|
|
|
43
43
|
// Longer documents take proportionally longer, but the ceiling keeps the
|
|
44
44
|
// whole cue inside the same budget as the content fade.
|
|
45
|
-
const duration = Math.min(GLIDE_MAX_MS, GLIDE_MIN_MS + startTop /
|
|
45
|
+
const duration = Math.min(GLIDE_MAX_MS, GLIDE_MIN_MS + startTop / 20);
|
|
46
46
|
let frame: number | undefined;
|
|
47
47
|
let settled = false;
|
|
48
48
|
const release = () => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type restoration for swup members we depend on but the package omits.
|
|
3
|
+
*
|
|
4
|
+
* `Visit.abort()` exists in swup 4.10.0's source (`src/modules/Visit.ts`) and is
|
|
5
|
+
* marked `/** @internal *\/`, so it is stripped from the published
|
|
6
|
+
* `dist/types/modules/Visit.d.ts`. The router genuinely needs it: superseding a
|
|
7
|
+
* visit mid-navigation means marking the old one aborted, and no public API does
|
|
8
|
+
* that.
|
|
9
|
+
*
|
|
10
|
+
* Declaring it here is the honest alternative to casting at the call site: the
|
|
11
|
+
* compiler now checks the name and the signature everywhere, and there is exactly
|
|
12
|
+
* one place to revisit when swup changes. Callers still guard with
|
|
13
|
+
* `typeof visit.abort === "function"` so a future removal degrades to a no-op
|
|
14
|
+
* instead of throwing in the middle of a navigation.
|
|
15
|
+
*
|
|
16
|
+
* Recheck on every swup upgrade:
|
|
17
|
+
* grep -n "@internal" node_modules/swup/src/modules/Visit.ts
|
|
18
|
+
* grep -n "abort" node_modules/swup/dist/types/modules/Visit.d.ts
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export {};
|
|
22
|
+
|
|
23
|
+
declare module "swup" {
|
|
24
|
+
interface Visit {
|
|
25
|
+
/**
|
|
26
|
+
* Mark this visit aborted. Internal in 4.10.0; see the note above.
|
|
27
|
+
* Not a public API: do not call it outside the router adapter.
|
|
28
|
+
*/
|
|
29
|
+
abort(): void;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -12,6 +12,7 @@ import "./navigation-client";
|
|
|
12
12
|
import { prepareNavigationScripts } from "./navigation-scripts";
|
|
13
13
|
import { prepareRouteMotion, type RouteMotion } from "./route-motion";
|
|
14
14
|
import { animateRouteOut } from "./route-out";
|
|
15
|
+
import { hydrateShells, markRetainedShells } from "./shell-hydration";
|
|
15
16
|
import { cancelPageScroll, glideToTop, type PageGlide, scrollPage } from "./smooth-scroll";
|
|
16
17
|
|
|
17
18
|
interface NavigationContext {
|
|
@@ -35,9 +36,11 @@ type Position = { top: number; left: number; focus?: string };
|
|
|
35
36
|
const MAIN = "main[data-stalux-route]";
|
|
36
37
|
|
|
37
38
|
function abortObsoleteVisit(visit: Visit): void {
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
39
|
+
// `abort()` is @internal in swup 4.10.0 and stripped from the published
|
|
40
|
+
// types; swup-internals.d.ts restores the declaration, so this is checked
|
|
41
|
+
// like any other member. The guard keeps a future removal from throwing in
|
|
42
|
+
// the middle of a navigation: every caller returns right after this anyway.
|
|
43
|
+
if (typeof visit.abort === "function") visit.abort();
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
function dispatchNavigation(name: string, context: NavigationContext, nextDocument?: Document) {
|
|
@@ -88,9 +91,9 @@ export function installSwupRouter(): Swup {
|
|
|
88
91
|
window.addEventListener(
|
|
89
92
|
"popstate",
|
|
90
93
|
(event) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
// hasUAVisualTransition is part of the DOM lib's PopStateEvent, so
|
|
95
|
+
// this reads the event directly; the listener is typed for popstate.
|
|
96
|
+
browserMotion = event.hasUAVisualTransition === true;
|
|
94
97
|
},
|
|
95
98
|
{ capture: true },
|
|
96
99
|
);
|
|
@@ -181,8 +184,10 @@ export function installSwupRouter(): Swup {
|
|
|
181
184
|
left: window.scrollX,
|
|
182
185
|
focus,
|
|
183
186
|
});
|
|
184
|
-
if (historyPositions.size > 64)
|
|
185
|
-
historyPositions.
|
|
187
|
+
if (historyPositions.size > 64) {
|
|
188
|
+
const oldest = historyPositions.keys().next();
|
|
189
|
+
if (!oldest.done) historyPositions.delete(oldest.value);
|
|
190
|
+
}
|
|
186
191
|
const from = new URL(visit.from.url + (visit.from.hash ?? ""), window.location.origin);
|
|
187
192
|
const to = new URL(visit.to.url + (visit.to.hash ?? ""), window.location.origin);
|
|
188
193
|
const navigationType = visit.history.popstate ? "traverse" : visit.history.action;
|
|
@@ -261,7 +266,7 @@ export function installSwupRouter(): Swup {
|
|
|
261
266
|
// missing document on an aborted visit is not a failed navigation.
|
|
262
267
|
if (context?.controller.signal.aborted || visit.done || visit !== swup.visit) return;
|
|
263
268
|
const next = visit.to.document;
|
|
264
|
-
if (!context
|
|
269
|
+
if (!(context && next && replace)) throw new Error("Missing Swup document");
|
|
265
270
|
if (context.controller.signal.aborted) return;
|
|
266
271
|
// Two cues overlap on the outgoing document: the content stage leaving,
|
|
267
272
|
// and the page walking back to the top. Persisted sidebars, the header
|
|
@@ -283,6 +288,9 @@ export function installSwupRouter(): Swup {
|
|
|
283
288
|
html.dataset.staluxVtPreset = context.preset;
|
|
284
289
|
context.motion = prepareRouteMotion(dispatchNavigation("astro:before-swap", context, next));
|
|
285
290
|
if (context.controller.signal.aborted) return;
|
|
291
|
+
// Remember which shells exist now: after the swap only those slots need
|
|
292
|
+
// refilling, because only those nodes came from the previous document.
|
|
293
|
+
markRetainedShells();
|
|
286
294
|
await replace(visit, args);
|
|
287
295
|
// The stage node survives the swap, so the filled exit effect still holds
|
|
288
296
|
// the incoming document at zero opacity. It is released inside
|
|
@@ -306,6 +314,10 @@ export function installSwupRouter(): Swup {
|
|
|
306
314
|
}
|
|
307
315
|
html.dataset.staluxRoute = getRouteKind(context.to.pathname);
|
|
308
316
|
visibleHistoryIndex = Number(window.history.state?.index ?? visibleHistoryIndex);
|
|
317
|
+
// Kept shells whose contents belong to the page (the article sidebars'
|
|
318
|
+
// lists) take their children from the incoming document now, before any
|
|
319
|
+
// page module mounts, so nothing needs to re-render or re-measure later.
|
|
320
|
+
hydrateShells(next);
|
|
309
321
|
dispatchNavigation("astro:after-swap", context, next);
|
|
310
322
|
// The entry cue has to own the first painted frame of the new document.
|
|
311
323
|
// content:scroll is an await and a rAF away, and running the incoming
|
|
@@ -419,8 +431,11 @@ export function installSwupRouter(): Swup {
|
|
|
419
431
|
}
|
|
420
432
|
});
|
|
421
433
|
swup.hooks.on("cache:set", () => {
|
|
422
|
-
while (swup.cache.size > 32)
|
|
423
|
-
swup.cache.
|
|
434
|
+
while (swup.cache.size > 32) {
|
|
435
|
+
const oldest = swup.cache.all.keys().next();
|
|
436
|
+
if (oldest.done) break;
|
|
437
|
+
swup.cache.delete(oldest.value);
|
|
438
|
+
}
|
|
424
439
|
});
|
|
425
440
|
// Parsed DOM, not window.load: slow third-party resources cannot hold the
|
|
426
441
|
// first page's interactions hostage. Page-runtime dedupes the early mount.
|
package/src/scripts/waline.ts
CHANGED
|
@@ -10,7 +10,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
10
10
|
registerPageLifecycle("waline", () => {
|
|
11
11
|
const container = document.querySelector<HTMLElement>("#waline-container");
|
|
12
12
|
const commentElement = document.querySelector<HTMLElement>("#waline-comment");
|
|
13
|
-
if (!container
|
|
13
|
+
if (!(container && commentElement)) return;
|
|
14
14
|
|
|
15
15
|
let decoded: unknown;
|
|
16
16
|
try {
|