@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingwangzhe/stalux",
|
|
3
|
-
"version": "1.46.
|
|
3
|
+
"version": "1.46.5",
|
|
4
4
|
"description": "A powerful, modern Astro blog theme — use as template or install as plugin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"knip": "knip",
|
|
92
92
|
"verify:build": "node scripts/verify-build.ts",
|
|
93
93
|
"validate": "bun run styles:build && bun run check && bun run node:check && bun run astro:check && bun run knip && bun run test:coverage && bun run build && bun run build:cli",
|
|
94
|
-
"prepare": "git config core.hooksPath scripts/git-hooks",
|
|
94
|
+
"prepare": "sh -c 'if [ -d scripts/git-hooks ] && git rev-parse --git-dir >/dev/null 2>&1; then git config core.hooksPath scripts/git-hooks; fi'",
|
|
95
95
|
"dev:debug": "STALUX_DEBUG=1 astro dev --verbose",
|
|
96
96
|
"build:debug": "STALUX_DEBUG=1 astro build --timings --verbose && node scripts/verify-build.ts",
|
|
97
97
|
"styles:build": "node scripts/build-styles.ts && node scripts/build-style-names.ts",
|
|
@@ -145,6 +145,7 @@
|
|
|
145
145
|
"@unocss/astro": "^66.11.0",
|
|
146
146
|
"@vitest/coverage-v8": "^5.0.4",
|
|
147
147
|
"astro": "^7.4.2",
|
|
148
|
+
"happy-dom": "^21.0.4",
|
|
148
149
|
"knip": "^6.41.0",
|
|
149
150
|
"sharp": "0.35.5",
|
|
150
151
|
"sonda": "^0.14.0",
|
|
@@ -56,7 +56,7 @@ const archiveYears = [...archivesByYearMonth.entries()]
|
|
|
56
56
|
}));
|
|
57
57
|
---
|
|
58
58
|
|
|
59
|
-
<section class="stalux-archive-container" data-stalux-motion-content>
|
|
59
|
+
<section class="stalux-archive-container" data-stalux-motion-content data-stalux-motion-stage>
|
|
60
60
|
<div class="stalux-archive-page-header">
|
|
61
61
|
<div class="stalux-archive-timeline-container">
|
|
62
62
|
{archiveYears.length > 0 ? (
|
|
@@ -7,7 +7,12 @@ import { getCategoryCountList } from "@utils/collections-stats";
|
|
|
7
7
|
const categoryList = await getCategoryCountList(Astro.logger);
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
<ul
|
|
10
|
+
<ul
|
|
11
|
+
class="stalux-categories"
|
|
12
|
+
data-stalux-motion-content
|
|
13
|
+
data-stalux-motion-stage
|
|
14
|
+
data-stalux-reveal
|
|
15
|
+
>
|
|
11
16
|
{categoryList.map((category) => (
|
|
12
17
|
<li data-stalux-reveal-item>
|
|
13
18
|
<CategoriesCard category={category} />
|
|
@@ -31,7 +31,7 @@ const zh = (site.lang || "zh-CN").startsWith("zh");
|
|
|
31
31
|
{zh ? "访客 IP 与位置加载中…" : "Loading visitor IP and location…"}
|
|
32
32
|
</p>
|
|
33
33
|
)}
|
|
34
|
-
<p class="stalux-weather-status" data-weather-status
|
|
34
|
+
<p class="stalux-weather-status" data-weather-status />
|
|
35
35
|
<button type="button" data-weather-retry hidden>
|
|
36
36
|
{zh ? "重试" : "Retry"}
|
|
37
37
|
</button>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
(() => {
|
|
8
8
|
const dateElement = document.querySelector('[data-ref="date"]');
|
|
9
9
|
const timeElement = document.querySelector('[data-ref="time"]');
|
|
10
|
-
if (!dateElement
|
|
10
|
+
if (!(dateElement && timeElement)) return;
|
|
11
11
|
const now = new Date();
|
|
12
12
|
const pad = (value) => String(value).padStart(2, "0");
|
|
13
13
|
const lang = document.documentElement.lang || "zh-CN";
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { toMachineDateTime } from "@utils/semantic-time";
|
|
19
19
|
import { buildSeoDocument, type SeoPageInput } from "@utils/seo-document";
|
|
20
20
|
import { SEO } from "astro-seo";
|
|
21
|
-
import packageJson from "../../../../package.json";
|
|
21
|
+
import packageJson from "../../../../package.json" with { type: "json" };
|
|
22
22
|
|
|
23
23
|
const configEntries = await getCollection("config");
|
|
24
24
|
const siteConfig = getSiteData(configEntries);
|
|
@@ -10,7 +10,7 @@ const linksConfig = getLinksData(configEntries);
|
|
|
10
10
|
const links = linksConfig?.sites ?? [];
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
<ul class="stalux-links" data-stalux-motion-content data-stalux-reveal>
|
|
13
|
+
<ul class="stalux-links" data-stalux-motion-content data-stalux-motion-stage data-stalux-reveal>
|
|
14
14
|
{links.map((link) => (
|
|
15
15
|
<li class="stalux-link-list-item" data-stalux-reveal-item>
|
|
16
16
|
<LinkCard link={link} />
|
|
@@ -38,7 +38,11 @@ const exportMd = Boolean(promoteConfig?.export_md) && !isAboutPage;
|
|
|
38
38
|
const mdSourceUrl = exportMd ? `${Astro.url.pathname.replace(/\/$/, "")}.md` : "";
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
-
<article
|
|
41
|
+
<article
|
|
42
|
+
class="stalux-article"
|
|
43
|
+
data-stalux-motion-stage
|
|
44
|
+
data-stalux-gallery={props.hasImage ? "true" : undefined}
|
|
45
|
+
>
|
|
42
46
|
<!-- Article Header -->
|
|
43
47
|
<header class="stalux-post-article-header" data-stalux-motion-region>
|
|
44
48
|
<h1 class="stalux-post-title" data-stalux-motion-focus>
|
|
@@ -11,7 +11,12 @@ const props = Astro.props as Props;
|
|
|
11
11
|
const posts = props.posts ?? (await getCollection("posts", ({ data }) => !data.draft));
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
<div
|
|
14
|
+
<div
|
|
15
|
+
data-stalux="postlist"
|
|
16
|
+
class="stalux-post-list-container"
|
|
17
|
+
data-stalux-motion-content
|
|
18
|
+
data-stalux-motion-stage
|
|
19
|
+
>
|
|
15
20
|
<ul class="post-list" data-stalux-reveal>
|
|
16
21
|
{posts.map((post, index) => (
|
|
17
22
|
<li data-stalux-reveal-item>
|
|
@@ -14,7 +14,14 @@ interface Props {
|
|
|
14
14
|
const { headings = [], tags = [], categories = [] } = Astro.props;
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
<!--
|
|
18
|
+
The right column is a shell. Between two articles it keeps its node, its
|
|
19
|
+
scroll position and its frame; only the three article-specific lists inside
|
|
20
|
+
the slots are refreshed (see scripts/shell-hydration.ts). Rebuilding the whole
|
|
21
|
+
aside on every article change made the reader watch the sidebar disappear and
|
|
22
|
+
reflow, and it cost a full layout of the new article to do it.
|
|
23
|
+
-->
|
|
24
|
+
<aside class="stalux-sidebar-right" data-stalux-scroll-pane data-swup-persist="post-sidebar-right">
|
|
18
25
|
<div
|
|
19
26
|
class="stalux-sidebar-container"
|
|
20
27
|
data-stalux-module="sidebar"
|
|
@@ -23,13 +30,13 @@ const { headings = [], tags = [], categories = [] } = Astro.props;
|
|
|
23
30
|
<PostDate />
|
|
24
31
|
<Weather />
|
|
25
32
|
</div>
|
|
26
|
-
<div class="stalux-sidebar-container">
|
|
33
|
+
<div class="stalux-sidebar-container" data-stalux-shell-slot="toc">
|
|
27
34
|
<PostToc headings={headings} />
|
|
28
35
|
</div>
|
|
29
|
-
<div class="stalux-sidebar-container">
|
|
36
|
+
<div class="stalux-sidebar-container" data-stalux-shell-slot="tags">
|
|
30
37
|
<PostTaxonomy kind="tag" items={tags} />
|
|
31
38
|
</div>
|
|
32
|
-
<div class="stalux-sidebar-container">
|
|
39
|
+
<div class="stalux-sidebar-container" data-stalux-shell-slot="categories">
|
|
33
40
|
<PostTaxonomy kind="category" items={categories} />
|
|
34
41
|
</div>
|
|
35
42
|
</aside>
|
|
@@ -19,14 +19,17 @@ const sorted = [...entries].sort((a, b) => {
|
|
|
19
19
|
});
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
<ul
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
<ul
|
|
23
|
+
class="stalux-word-list-list"
|
|
24
|
+
data-stalux-motion-content
|
|
25
|
+
data-stalux-motion-stage
|
|
26
|
+
data-stalux-reveal
|
|
27
|
+
>
|
|
28
|
+
{sorted.map((entry) => (
|
|
29
|
+
<li class="stalux-word-list-item" data-stalux-reveal-item>
|
|
30
|
+
<WordCard entry={entry} timezone={timezone} />
|
|
31
|
+
</li>
|
|
32
|
+
))}
|
|
30
33
|
</ul>
|
|
31
34
|
|
|
32
35
|
<script src="../../../scripts/word-list.ts"></script>
|
package/src/index.ts
CHANGED
|
@@ -678,7 +678,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
678
678
|
const postsDir = path.resolve(process.cwd(), contentDir, "posts");
|
|
679
679
|
if (existsSync(postsDir)) {
|
|
680
680
|
for (const filename of readdirSync(postsDir)) {
|
|
681
|
-
if (!filename.endsWith(".md")
|
|
681
|
+
if (!(filename.endsWith(".md") || filename.endsWith(".mdx"))) {
|
|
682
682
|
continue;
|
|
683
683
|
}
|
|
684
684
|
const source = readFileSync(path.join(postsDir, filename), "utf8");
|
|
@@ -80,7 +80,7 @@ function markupEdits(value: string, names: Names): Edit[] {
|
|
|
80
80
|
if (["pre", "code", "script", "style"].includes(node.tagName)) return;
|
|
81
81
|
for (const attr of node.attrs) {
|
|
82
82
|
const loc = node.sourceCodeLocation?.attrs?.[attr.name];
|
|
83
|
-
if (!loc
|
|
83
|
+
if (!(loc && ["class", "style", "data-toc-active-class"].includes(attr.name)))
|
|
84
84
|
continue;
|
|
85
85
|
const raw = value.slice(loc.startOffset, loc.endOffset);
|
|
86
86
|
const offset = loc.startOffset + raw.indexOf("=") + 1;
|
|
@@ -258,7 +258,7 @@ function isHiddenElement(attrs: Array<{ name: string; value: string }>): boolean
|
|
|
258
258
|
const CONTROL_CHARACTER = /\p{C}/u;
|
|
259
259
|
function addGlyphs(text: string, output: Set<string>): void {
|
|
260
260
|
for (const char of text)
|
|
261
|
-
if (!output.has(char)
|
|
261
|
+
if (!(output.has(char) || CONTROL_CHARACTER.test(char))) output.add(char);
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
function collectBodyText(node: HtmlNode, output: Set<string>, skipSharedComponents = false): void {
|
|
@@ -410,7 +410,7 @@ function preparePageFont(
|
|
|
410
410
|
excludedChars: ReadonlySet<string> = new Set(),
|
|
411
411
|
) {
|
|
412
412
|
const body = findBody(document);
|
|
413
|
-
if (!body) return
|
|
413
|
+
if (!body) return;
|
|
414
414
|
|
|
415
415
|
const glyphs = new Set<string>();
|
|
416
416
|
addGlyphs(RESERVED_GLYPHS, glyphs);
|
|
@@ -425,7 +425,7 @@ function preparePageFont(
|
|
|
425
425
|
const chars = [...glyphs].sort(
|
|
426
426
|
(a, b) => (a.codePointAt(0) as number) - (b.codePointAt(0) as number),
|
|
427
427
|
);
|
|
428
|
-
if (chars.length === 0) return
|
|
428
|
+
if (chars.length === 0) return;
|
|
429
429
|
|
|
430
430
|
let prepared = preparedFonts.get(fontBuffer);
|
|
431
431
|
if (!prepared) {
|
|
@@ -59,14 +59,13 @@ export async function createPageOutputCache(
|
|
|
59
59
|
for (const [asset, expected] of record.assets) {
|
|
60
60
|
if ((await assetDigest(asset)) !== expected) return undefined;
|
|
61
61
|
}
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
if (
|
|
63
|
+
record.font &&
|
|
64
|
+
(!/^page-[a-f\d]+\.woff2$/u.test(record.font.filename) ||
|
|
65
65
|
typeof record.font.sourcePath !== "string" ||
|
|
66
|
-
!(await fs.stat(record.font.sourcePath)).isFile()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
66
|
+
!(await fs.stat(record.font.sourcePath)).isFile())
|
|
67
|
+
)
|
|
68
|
+
return undefined;
|
|
70
69
|
return { html: record.html, font: record.font };
|
|
71
70
|
} catch {
|
|
72
71
|
// Missing, truncated, or invalid cache entries are rebuilt from source.
|
|
@@ -87,7 +87,7 @@ async function loadCoverUncached(cover: string, paths: SocialCardPaths): Promise
|
|
|
87
87
|
if (cachedStat && Date.now() - cachedStat.mtimeMs < 86_400_000)
|
|
88
88
|
return readFile(remoteCache);
|
|
89
89
|
const response = await fetch(url, { signal: AbortSignal.timeout(8000) });
|
|
90
|
-
if (!response.ok
|
|
90
|
+
if (!(response.ok && response.headers.get("content-type")?.startsWith("image/")))
|
|
91
91
|
throw new Error(`cover returned HTTP ${response.status} or non-image content`);
|
|
92
92
|
const chunks: Uint8Array[] = [];
|
|
93
93
|
let size = 0;
|
|
@@ -100,8 +100,7 @@ async function loadCoverUncached(cover: string, paths: SocialCardPaths): Promise
|
|
|
100
100
|
bytes = Buffer.concat(chunks);
|
|
101
101
|
const dimensions = imageSize(bytes);
|
|
102
102
|
if (
|
|
103
|
-
!dimensions.width ||
|
|
104
|
-
!dimensions.height ||
|
|
103
|
+
!(dimensions.width && dimensions.height) ||
|
|
105
104
|
dimensions.width * dimensions.height > 32_000_000
|
|
106
105
|
)
|
|
107
106
|
throw new Error("invalid cover dimensions");
|
|
@@ -110,8 +109,7 @@ async function loadCoverUncached(cover: string, paths: SocialCardPaths): Promise
|
|
|
110
109
|
}
|
|
111
110
|
const dimensions = imageSize(bytes);
|
|
112
111
|
if (
|
|
113
|
-
!dimensions.width ||
|
|
114
|
-
!dimensions.height ||
|
|
112
|
+
!(dimensions.width && dimensions.height) ||
|
|
115
113
|
dimensions.width * dimensions.height > 32_000_000
|
|
116
114
|
)
|
|
117
115
|
throw new Error("invalid or oversized cover dimensions");
|
|
@@ -15,7 +15,7 @@ export async function getStaticPaths() {
|
|
|
15
15
|
return posts
|
|
16
16
|
.filter(
|
|
17
17
|
(post) =>
|
|
18
|
-
!post.data["og:image"]?.trim()
|
|
18
|
+
!(post.data["og:image"]?.trim() && isSocialImageUrl(post.data["og:image"].trim())),
|
|
19
19
|
)
|
|
20
20
|
.flatMap((post) => {
|
|
21
21
|
const card = createSocialCardData(post.data, site, author);
|
package/src/schemas/config.ts
CHANGED
|
@@ -164,7 +164,7 @@ export const commentSchema = z.object({
|
|
|
164
164
|
.object({
|
|
165
165
|
serverURL: z.url("comment.waline.serverURL must be a valid URL").optional(),
|
|
166
166
|
lang: z.string().optional().default("zh-CN"),
|
|
167
|
-
locale: z.
|
|
167
|
+
locale: z.unknown().optional(),
|
|
168
168
|
emoji: z
|
|
169
169
|
.array(z.string())
|
|
170
170
|
.optional()
|
|
@@ -181,10 +181,10 @@ export const commentSchema = z.object({
|
|
|
181
181
|
dark: z.union([z.string(), z.boolean()]).optional().default(true),
|
|
182
182
|
noCopyright: z.boolean().optional().default(false),
|
|
183
183
|
commentSorting: z.string().optional().default("latest"),
|
|
184
|
-
imageUploader: z.
|
|
185
|
-
highlighter: z.
|
|
186
|
-
texRenderer: z.
|
|
187
|
-
search: z.
|
|
184
|
+
imageUploader: z.unknown().optional(),
|
|
185
|
+
highlighter: z.unknown().optional(),
|
|
186
|
+
texRenderer: z.unknown().optional(),
|
|
187
|
+
search: z.unknown().optional(),
|
|
188
188
|
wordLimit: z.number().optional().default(200),
|
|
189
189
|
pageSize: z.number().optional().default(10),
|
|
190
190
|
})
|
|
@@ -244,7 +244,7 @@ function trackGooglePage(): void {
|
|
|
244
244
|
|
|
245
245
|
function trackUmamiPage(): void {
|
|
246
246
|
const config = document.body?.dataset;
|
|
247
|
-
if (!config?.staluxUmamiId
|
|
247
|
+
if (!(config?.staluxUmamiId && config.staluxUmamiUrl)) return;
|
|
248
248
|
// Snapshot now: a slow SDK must not attribute every queued visit to the last URL.
|
|
249
249
|
const payload = {
|
|
250
250
|
website: config.staluxUmamiId,
|
|
@@ -36,7 +36,7 @@ export function getArchiveDescriptionPosition(
|
|
|
36
36
|
export function mountArchiveDescriptions() {
|
|
37
37
|
const root = document.querySelector<HTMLElement>(".stalux-archive-container");
|
|
38
38
|
const tooltip = document.querySelector<HTMLElement>("[data-archive-tooltip]");
|
|
39
|
-
if (!root
|
|
39
|
+
if (!(root && tooltip)) return;
|
|
40
40
|
const usePopover =
|
|
41
41
|
typeof tooltip.showPopover === "function" && typeof tooltip.hidePopover === "function";
|
|
42
42
|
if (usePopover) tooltip.setAttribute("popover", "manual");
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
declare global {
|
|
2
|
+
interface Window {
|
|
3
|
+
__STALUX_BACK_TO_TOP__?: boolean;
|
|
4
|
+
}
|
|
3
5
|
}
|
|
4
6
|
|
|
5
7
|
const SENTINEL_SELECTOR = "[data-stalux-scroll-sentinel]";
|
|
@@ -14,12 +16,11 @@ const BUTTON_SELECTOR = "[data-stalux-back-to-top]";
|
|
|
14
16
|
* not reached `ready`.
|
|
15
17
|
*/
|
|
16
18
|
export function installBackToTop(): void {
|
|
17
|
-
|
|
18
|
-
if (host.__STALUX_BACK_TO_TOP__) return;
|
|
19
|
+
if (window.__STALUX_BACK_TO_TOP__) return;
|
|
19
20
|
const sentinel = document.querySelector(SENTINEL_SELECTOR);
|
|
20
21
|
const button = document.querySelector<HTMLButtonElement>(BUTTON_SELECTOR);
|
|
21
|
-
if (!sentinel
|
|
22
|
-
|
|
22
|
+
if (!(sentinel && button)) return;
|
|
23
|
+
window.__STALUX_BACK_TO_TOP__ = true;
|
|
23
24
|
|
|
24
25
|
button.addEventListener("click", () => {
|
|
25
26
|
window.scrollTo({
|
|
@@ -34,7 +34,7 @@ export function installSessionBackground(): void {
|
|
|
34
34
|
if (connection?.saveData) return;
|
|
35
35
|
const a = document.querySelector<HTMLElement>('[data-stalux-background="a"]');
|
|
36
36
|
const b = document.querySelector<HTMLElement>('[data-stalux-background="b"]');
|
|
37
|
-
if (!a
|
|
37
|
+
if (!(a && b) || a.style.backgroundImage || b.style.backgroundImage) return;
|
|
38
38
|
const custom = Boolean(window.__STALUX_BG_URLS__?.length);
|
|
39
39
|
const urls = window.__STALUX_BG_URLS__?.length
|
|
40
40
|
? window.__STALUX_BG_URLS__
|
|
@@ -50,7 +50,7 @@ export function installSessionBackground(): void {
|
|
|
50
50
|
() => {
|
|
51
51
|
// These two shell nodes retain their identities during Swup visits.
|
|
52
52
|
// No navigation event can start another image or reset this result.
|
|
53
|
-
if (!a.isConnected
|
|
53
|
+
if (!(a.isConnected && b.isConnected)) return;
|
|
54
54
|
// Original SVGs have only a viewBox: auto sizes them as contain.
|
|
55
55
|
// Raster files have intrinsic dimensions, so retain that sizing
|
|
56
56
|
// explicitly. Custom URLs keep the existing auto semantics.
|
package/src/scripts/countdown.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { registerPageLifecycle } from "./page-runtime";
|
|
|
3
3
|
registerPageLifecycle("not-found-countdown", () => {
|
|
4
4
|
const el = document.getElementById("countdown");
|
|
5
5
|
const template = el?.dataset.countdownTemplate;
|
|
6
|
-
if (!el
|
|
6
|
+
if (!(el && template)) return;
|
|
7
7
|
let seconds = 5;
|
|
8
8
|
const timer = window.setInterval(() => {
|
|
9
9
|
seconds -= 1;
|
package/src/scripts/date-time.ts
CHANGED
|
@@ -25,7 +25,7 @@ function toDisplayDate(date: Date, lang: string): string {
|
|
|
25
25
|
registerPageLifecycle("date-time", () => {
|
|
26
26
|
const dateElement = document.querySelector<HTMLTimeElement>('[data-ref="date"]');
|
|
27
27
|
const timeElement = document.querySelector<HTMLTimeElement>('[data-ref="time"]');
|
|
28
|
-
if (!dateElement
|
|
28
|
+
if (!(dateElement && timeElement)) return;
|
|
29
29
|
const lang = document.documentElement.lang || "zh-CN";
|
|
30
30
|
let lastDate = "";
|
|
31
31
|
const update = () => {
|
|
@@ -4,13 +4,17 @@ const ITEM = ":scope > [data-stalux-reveal-item]";
|
|
|
4
4
|
// never a queue long enough to keep the reader waiting for the last item.
|
|
5
5
|
const MAX_CONTAINERS = 48;
|
|
6
6
|
const MAX_ITEMS = 16;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
// The whole unroll budget is lead-in + max delay + row duration. At the previous
|
|
8
|
+
// 150 + 400 + 380 the tail alone ran 930ms after the swap, which readers report
|
|
9
|
+
// as the page still moving a second later. Every number here is half of that, so
|
|
10
|
+
// the same sheet reads in one motion without the wait.
|
|
11
|
+
const STAGGER_MS = 22;
|
|
12
|
+
const MAX_DELAY_MS = 200;
|
|
13
|
+
const DURATION_MS = 190;
|
|
10
14
|
// When a route cue is already moving the page body, the sheet waits for it to
|
|
11
15
|
// mostly settle before unrolling. Starting both at once makes the rows wipe
|
|
12
16
|
// open while the whole page is still semi-transparent, which reads as a smudge.
|
|
13
|
-
const ROUTE_LEAD_IN_MS =
|
|
17
|
+
const ROUTE_LEAD_IN_MS = 75;
|
|
14
18
|
// Strong ease-out: the row arrives fast and settles softly. The built-in
|
|
15
19
|
// ease-out is too weak to read at this size.
|
|
16
20
|
const EASING = "cubic-bezier(0.23, 1, 0.32, 1)";
|
|
@@ -46,7 +50,10 @@ interface Held {
|
|
|
46
50
|
stagger: number;
|
|
47
51
|
timer: ReturnType<typeof setTimeout>;
|
|
48
52
|
}
|
|
49
|
-
|
|
53
|
+
// Rows and groups are only ever queried, measured and animated, all of which
|
|
54
|
+
// Element already provides. Typing them as HTMLElement forced a cast on the
|
|
55
|
+
// observer's target for no gain.
|
|
56
|
+
const held = new Map<Element, Held>();
|
|
50
57
|
|
|
51
58
|
function canReveal(): boolean {
|
|
52
59
|
if (typeof IntersectionObserver !== "function") return false;
|
|
@@ -57,14 +64,22 @@ function canReveal(): boolean {
|
|
|
57
64
|
return document.documentElement.dataset.staluxVtPreset !== "restore";
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
function groups():
|
|
61
|
-
return [...document.querySelectorAll<
|
|
62
|
-
.filter(
|
|
67
|
+
function groups(): Element[] {
|
|
68
|
+
return [...document.querySelectorAll<Element>(CONTAINER)]
|
|
69
|
+
.filter(
|
|
70
|
+
(element) =>
|
|
71
|
+
// A retained shell kept its identity, so its list was already
|
|
72
|
+
// read: cueing it again would be a second animation. Contents
|
|
73
|
+
// that shell-hydration just replaced are new content and are
|
|
74
|
+
// explicitly marked as such.
|
|
75
|
+
element.hasAttribute("data-stalux-shell-fresh") ||
|
|
76
|
+
!element.closest("[data-swup-persist]"),
|
|
77
|
+
)
|
|
63
78
|
.slice(0, MAX_CONTAINERS);
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
function rows(group:
|
|
67
|
-
return [...group.querySelectorAll<
|
|
81
|
+
function rows(group: Element): Element[] {
|
|
82
|
+
return [...group.querySelectorAll<Element>(ITEM)].slice(0, MAX_ITEMS);
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
/**
|
|
@@ -76,7 +91,7 @@ function rows(group: HTMLElement): HTMLElement[] {
|
|
|
76
91
|
* the entry cue needed. Groups further down the page keep the authored visible
|
|
77
92
|
* state and start their wipe when they scroll in, which never flashed anyway.
|
|
78
93
|
*/
|
|
79
|
-
function nearViewport(group:
|
|
94
|
+
function nearViewport(group: Element): boolean {
|
|
80
95
|
const rect = group.getBoundingClientRect();
|
|
81
96
|
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
82
97
|
const margin = window.innerHeight * 0.5;
|
|
@@ -130,6 +145,11 @@ export function primeListReveal(): void {
|
|
|
130
145
|
fill: "backwards",
|
|
131
146
|
});
|
|
132
147
|
animation.pause();
|
|
148
|
+
// A paused effect only ever settles by rejecting when the hold is
|
|
149
|
+
// released, and nothing awaits it yet: without a handler that is an
|
|
150
|
+
// unhandled rejection in the console. `reveal` attaches the real
|
|
151
|
+
// handler when it takes the effect over.
|
|
152
|
+
void animation.finished.catch(() => undefined);
|
|
133
153
|
const timer = setTimeout(() => {
|
|
134
154
|
// The visit was abandoned: release the row so the authored
|
|
135
155
|
// visible state returns rather than staying clipped.
|
|
@@ -179,7 +199,7 @@ export function mountListReveal(): (() => void) | undefined {
|
|
|
179
199
|
for (const animation of active) animation.cancel();
|
|
180
200
|
active.clear();
|
|
181
201
|
};
|
|
182
|
-
const reveal = (container:
|
|
202
|
+
const reveal = (container: Element) => {
|
|
183
203
|
if (document.hidden || preference.matches) return;
|
|
184
204
|
const lead = leadIn();
|
|
185
205
|
for (const [index, row] of rows(container).entries()) {
|
|
@@ -226,7 +246,7 @@ export function mountListReveal(): (() => void) | undefined {
|
|
|
226
246
|
for (const entry of entries) {
|
|
227
247
|
if (!entry.isIntersecting) continue;
|
|
228
248
|
observer.unobserve(entry.target);
|
|
229
|
-
reveal(entry.target
|
|
249
|
+
reveal(entry.target);
|
|
230
250
|
}
|
|
231
251
|
},
|
|
232
252
|
{ threshold: 0.05 },
|
|
@@ -249,13 +269,14 @@ export function mountListReveal(): (() => void) | undefined {
|
|
|
249
269
|
// before the browser paints, which is the only moment where the incoming rows
|
|
250
270
|
// can be held without a visible flash. Registered once per document even when
|
|
251
271
|
// both the shared layout and the article layout import this module.
|
|
252
|
-
|
|
253
|
-
|
|
272
|
+
declare global {
|
|
273
|
+
interface Window {
|
|
274
|
+
__staluxListRevealPrime?: boolean;
|
|
275
|
+
}
|
|
254
276
|
}
|
|
255
277
|
if (typeof document !== "undefined" && typeof window !== "undefined") {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
host.__staluxListRevealPrime = true;
|
|
278
|
+
if (!window.__staluxListRevealPrime) {
|
|
279
|
+
window.__staluxListRevealPrime = true;
|
|
259
280
|
document.addEventListener("astro:after-swap", primeListReveal);
|
|
260
281
|
}
|
|
261
282
|
}
|
|
@@ -11,7 +11,7 @@ export function mountModuleMotion(): (() => void) | undefined {
|
|
|
11
11
|
typeof IntersectionObserver !== "function"
|
|
12
12
|
)
|
|
13
13
|
return;
|
|
14
|
-
const elements = [...document.querySelectorAll<
|
|
14
|
+
const elements = [...document.querySelectorAll<Element>(SELECTOR)].slice(0, MAX_REGIONS);
|
|
15
15
|
if (!elements.length) return;
|
|
16
16
|
const active = new Set<Animation>();
|
|
17
17
|
const motionPreference = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
@@ -23,7 +23,9 @@ export function mountModuleMotion(): (() => void) | undefined {
|
|
|
23
23
|
(entries) => {
|
|
24
24
|
for (const entry of entries) {
|
|
25
25
|
if (!entry.isIntersecting) continue;
|
|
26
|
-
|
|
26
|
+
// Element already covers everything used below (closest, animate),
|
|
27
|
+
// so the observer's target needs no assertion.
|
|
28
|
+
const element = entry.target;
|
|
27
29
|
observer.unobserve(element);
|
|
28
30
|
// The router owns a single bounded lead region. Do not layer an
|
|
29
31
|
// observer entry over that motion or make long comments slide.
|
|
@@ -44,7 +46,7 @@ export function mountModuleMotion(): (() => void) | undefined {
|
|
|
44
46
|
const animation = element.animate(
|
|
45
47
|
{ opacity: [0.88, 1] },
|
|
46
48
|
{
|
|
47
|
-
duration:
|
|
49
|
+
duration: 70,
|
|
48
50
|
easing: "cubic-bezier(0.2, 0.7, 0.2, 1)",
|
|
49
51
|
},
|
|
50
52
|
);
|
|
@@ -16,7 +16,10 @@ function warmFont(href: string): Promise<FontFace> {
|
|
|
16
16
|
});
|
|
17
17
|
const ready = face.load();
|
|
18
18
|
fonts.set(href, { face, ready });
|
|
19
|
-
if (fonts.size > MAX_FONTS)
|
|
19
|
+
if (fonts.size > MAX_FONTS) {
|
|
20
|
+
const oldest = fonts.keys().next();
|
|
21
|
+
if (!oldest.done) fonts.delete(oldest.value);
|
|
22
|
+
}
|
|
20
23
|
void ready.catch(() => {
|
|
21
24
|
if (fonts.get(href)?.ready === ready) fonts.delete(href);
|
|
22
25
|
});
|
|
@@ -38,7 +38,7 @@ registerPageLifecycle("navigation", () => {
|
|
|
38
38
|
const navOverlay = document.querySelector<HTMLElement>("[data-ref='stalux-nav-overlay']");
|
|
39
39
|
const buttonLeft = document.querySelector<HTMLElement>("[data-ref='stalux-nav-scroll-left']");
|
|
40
40
|
const buttonRight = document.querySelector<HTMLElement>("[data-ref='stalux-nav-scroll-right']");
|
|
41
|
-
if (!navList
|
|
41
|
+
if (!(navList && buttonLeft && buttonRight)) return;
|
|
42
42
|
|
|
43
43
|
const controller = new AbortController();
|
|
44
44
|
const listenerOptions = { signal: controller.signal };
|
|
@@ -12,12 +12,14 @@ interface RuntimeRegistration {
|
|
|
12
12
|
unregister: PageDisposer;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
declare global {
|
|
16
|
+
interface Window {
|
|
17
|
+
__staluxPageRuntime?: Map<string, RuntimeRegistration>;
|
|
18
|
+
}
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface PageRuntimeEnvironment {
|
|
20
|
-
host:
|
|
22
|
+
host: Pick<Window, "__staluxPageRuntime">;
|
|
21
23
|
target: RuntimeTarget;
|
|
22
24
|
queueTask: (callback: () => void) => void;
|
|
23
25
|
}
|
|
@@ -25,7 +27,7 @@ export interface PageRuntimeEnvironment {
|
|
|
25
27
|
function getBrowserEnvironment(): PageRuntimeEnvironment | undefined {
|
|
26
28
|
if (typeof window === "undefined" || typeof document === "undefined") return undefined;
|
|
27
29
|
return {
|
|
28
|
-
host: window
|
|
30
|
+
host: window,
|
|
29
31
|
target: document,
|
|
30
32
|
queueTask: queueMicrotask,
|
|
31
33
|
};
|
|
@@ -10,7 +10,7 @@ function shuffle<T extends object>(items: T[]): T[] {
|
|
|
10
10
|
const j = (crypto.getRandomValues(bytes)[0] ?? 0) % (i + 1);
|
|
11
11
|
const current = result[i];
|
|
12
12
|
const target = result[j];
|
|
13
|
-
if (!current
|
|
13
|
+
if (!(current && target)) throw new RangeError("Shuffle index is out of bounds");
|
|
14
14
|
result[i] = target;
|
|
15
15
|
result[j] = current;
|
|
16
16
|
}
|
|
@@ -35,7 +35,7 @@ function render(posts: { abbrlink: string; title: string }[]) {
|
|
|
35
35
|
export function mountRandomPosts(): (() => void) | undefined {
|
|
36
36
|
const container = document.querySelector<HTMLElement>("[data-stalux-random-posts]");
|
|
37
37
|
const list = container?.querySelector<HTMLElement>("#random-posts-list");
|
|
38
|
-
if (!container
|
|
38
|
+
if (!(container && list)) return;
|
|
39
39
|
const controller = new AbortController();
|
|
40
40
|
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
41
41
|
const fallback = container.dataset.loadError ?? "Failed to load posts";
|