@xingwangzhe/stalux 1.46.1 → 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.
Files changed (48) hide show
  1. package/package.json +3 -2
  2. package/src/components/stalux/archives/archivesList.astro +1 -1
  3. package/src/components/stalux/categories/categoriesList.astro +6 -1
  4. package/src/components/stalux/common/Weather.astro +1 -1
  5. package/src/components/stalux/common/date.astro +1 -1
  6. package/src/components/stalux/footer/FooterBeian.astro +2 -0
  7. package/src/components/stalux/layout/head.astro +1 -1
  8. package/src/components/stalux/links/linkList.astro +1 -1
  9. package/src/components/stalux/posts/postContent.astro +5 -1
  10. package/src/components/stalux/posts/postList.astro +6 -1
  11. package/src/components/stalux/posts/postRight.astro +11 -4
  12. package/src/components/stalux/words/wordList.astro +11 -8
  13. package/src/index.ts +1 -1
  14. package/src/internal/compact-styles.ts +1 -1
  15. package/src/internal/font-slices.ts +3 -3
  16. package/src/internal/page-output-cache.ts +6 -7
  17. package/src/internal/social-card-renderer.ts +3 -5
  18. package/src/pages/og/[image].[format].ts +1 -1
  19. package/src/schemas/config.ts +5 -5
  20. package/src/scripts/analytics-runtime.ts +1 -1
  21. package/src/scripts/archive-descriptions.ts +1 -1
  22. package/src/scripts/back-to-top.ts +7 -6
  23. package/src/scripts/background.ts +2 -2
  24. package/src/scripts/countdown.ts +1 -1
  25. package/src/scripts/date-time.ts +1 -1
  26. package/src/scripts/list-reveal.ts +214 -40
  27. package/src/scripts/module-motion.ts +5 -3
  28. package/src/scripts/navigation-font.ts +4 -1
  29. package/src/scripts/navigation.ts +1 -1
  30. package/src/scripts/page-runtime.ts +6 -4
  31. package/src/scripts/random-posts.ts +2 -2
  32. package/src/scripts/route-motion.ts +67 -56
  33. package/src/scripts/route-out.ts +12 -14
  34. package/src/scripts/runtime-counter.ts +1 -1
  35. package/src/scripts/shell-hydration.ts +60 -0
  36. package/src/scripts/smooth-scroll.ts +4 -4
  37. package/src/scripts/swup-internals.d.ts +31 -0
  38. package/src/scripts/swup-router.ts +53 -11
  39. package/src/scripts/waline.ts +1 -1
  40. package/src/scripts/weather.ts +148 -82
  41. package/src/scripts/webmcp.ts +24 -20
  42. package/src/utils/ai-discovery.ts +1 -1
  43. package/src/utils/cc.ts +2 -2
  44. package/src/utils/collections-stats.ts +4 -2
  45. package/src/utils/dayjs.ts +2 -2
  46. package/src/utils/i18n.ts +2 -2
  47. package/src/utils/seo-document.ts +4 -4
  48. 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.1",
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 class="stalux-categories" data-stalux-motion-content data-stalux-reveal>
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></p>
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 || !timeElement) return;
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";
@@ -35,6 +35,8 @@ const showBeian = !!(icp || pubsec);
35
35
  <img
36
36
  src="https://beian.mps.gov.cn/img/logo01.dd7ff50e.png"
37
37
  alt={t("footer.securityIcon")}
38
+ width="15"
39
+ height="15"
38
40
  />
39
41
  {pubsec}
40
42
  </a>
@@ -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 class="stalux-article" data-stalux-gallery={props.hasImage ? "true" : undefined}>
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 data-stalux="postlist" class="stalux-post-list-container" data-stalux-motion-content>
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
- <aside class="stalux-sidebar-right" data-stalux-scroll-pane>
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 class="stalux-word-list-list" data-stalux-motion-content data-stalux-reveal>
23
- {sorted.map((entry) => {
24
- return (
25
- <li class="stalux-word-list-item" data-stalux-reveal-item>
26
- <WordCard entry={entry} timezone={timezone} />
27
- </li>
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") && !filename.endsWith(".mdx")) {
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 || !["class", "style", "data-toc-active-class"].includes(attr.name))
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) && !CONTROL_CHARACTER.test(char)) output.add(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 undefined;
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 undefined;
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 (record.font) {
63
- if (
64
- !/^page-[a-f\d]+\.woff2$/u.test(record.font.filename) ||
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
- return undefined;
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 || !response.headers.get("content-type")?.startsWith("image/"))
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() || !isSocialImageUrl(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);
@@ -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.any().optional(),
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.any().optional(),
185
- highlighter: z.any().optional(),
186
- texRenderer: z.any().optional(),
187
- search: z.any().optional(),
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 || !config.staluxUmamiUrl) return;
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 || !tooltip) return;
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
- interface BackToTopWindow extends Window {
2
- __STALUX_BACK_TO_TOP__?: boolean;
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
- const host = window as BackToTopWindow;
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 || !button) return;
22
- host.__STALUX_BACK_TO_TOP__ = true;
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 || !b || a.style.backgroundImage || b.style.backgroundImage) return;
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 || !b.isConnected) return;
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.
@@ -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 || !template) return;
6
+ if (!(el && template)) return;
7
7
  let seconds = 5;
8
8
  const timer = window.setInterval(() => {
9
9
  seconds -= 1;
@@ -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 || !timeElement) return;
28
+ if (!(dateElement && timeElement)) return;
29
29
  const lang = document.documentElement.lang || "zh-CN";
30
30
  let lastDate = "";
31
31
  const update = () => {