@xingwangzhe/stalux 1.43.1 → 1.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +5 -3
  2. package/package.json +4 -2
  3. package/public/background/pattern-1.min.webp +0 -0
  4. package/public/background/pattern-10.min.webp +0 -0
  5. package/public/background/pattern-11.min.webp +0 -0
  6. package/public/background/pattern-12.min.webp +0 -0
  7. package/public/background/pattern-13.min.webp +0 -0
  8. package/public/background/pattern-14.min.webp +0 -0
  9. package/public/background/pattern-15.min.webp +0 -0
  10. package/public/background/pattern-16.min.webp +0 -0
  11. package/public/background/pattern-17.min.webp +0 -0
  12. package/public/background/pattern-18.min.webp +0 -0
  13. package/public/background/pattern-19.min.webp +0 -0
  14. package/public/background/pattern-2.min.webp +0 -0
  15. package/public/background/pattern-20.min.webp +0 -0
  16. package/public/background/pattern-21.min.webp +0 -0
  17. package/public/background/pattern-22.min.webp +0 -0
  18. package/public/background/pattern-23.min.webp +0 -0
  19. package/public/background/pattern-24.min.webp +0 -0
  20. package/public/background/pattern-25.min.webp +0 -0
  21. package/public/background/pattern-26.min.webp +0 -0
  22. package/public/background/pattern-27.min.webp +0 -0
  23. package/public/background/pattern-28.min.webp +0 -0
  24. package/public/background/pattern-29.min.webp +0 -0
  25. package/public/background/pattern-3.min.webp +0 -0
  26. package/public/background/pattern-30.min.webp +0 -0
  27. package/public/background/pattern-31.min.webp +0 -0
  28. package/public/background/pattern-32.min.webp +0 -0
  29. package/public/background/pattern-33.min.webp +0 -0
  30. package/public/background/pattern-34.min.webp +0 -0
  31. package/public/background/pattern-35.min.webp +0 -0
  32. package/public/background/pattern-36.min.webp +0 -0
  33. package/public/background/pattern-37.min.webp +0 -0
  34. package/public/background/pattern-38.min.webp +0 -0
  35. package/public/background/pattern-39.min.webp +0 -0
  36. package/public/background/pattern-4.min.webp +0 -0
  37. package/public/background/pattern-40.min.webp +0 -0
  38. package/public/background/pattern-41.min.webp +0 -0
  39. package/public/background/pattern-42.min.webp +0 -0
  40. package/public/background/pattern-5.min.webp +0 -0
  41. package/public/background/pattern-6.min.webp +0 -0
  42. package/public/background/pattern-7.min.webp +0 -0
  43. package/public/background/pattern-8.min.webp +0 -0
  44. package/public/background/pattern-9.min.webp +0 -0
  45. package/src/components/stalux/archives/archivesList.astro +8 -2
  46. package/src/components/stalux/archives/toArchive.astro +1 -1
  47. package/src/components/stalux/categories/categoriesCard.astro +1 -1
  48. package/src/components/stalux/common/author.astro +1 -1
  49. package/src/components/stalux/footer/FooterBadges.astro +5 -1
  50. package/src/components/stalux/layout/PageHeading.astro +2 -2
  51. package/src/components/stalux/layout/footer.astro +5 -1
  52. package/src/components/stalux/links/linkCard.astro +7 -1
  53. package/src/components/stalux/links/socialMedia.astro +1 -0
  54. package/src/components/stalux/posts/ArticleSummary.astro +6 -1
  55. package/src/components/stalux/posts/PostTaxonomy.astro +2 -1
  56. package/src/components/stalux/posts/WalineComment.astro +1 -0
  57. package/src/components/stalux/posts/postCC.astro +1 -1
  58. package/src/components/stalux/posts/postCard.astro +4 -1
  59. package/src/components/stalux/posts/postContent.astro +1 -1
  60. package/src/components/stalux/posts/postLeft.astro +2 -2
  61. package/src/components/stalux/posts/postNavigation.astro +5 -1
  62. package/src/components/stalux/posts/postRight.astro +5 -1
  63. package/src/components/stalux/posts/postToc.astro +2 -2
  64. package/src/components/stalux/tags/tagHeader.astro +1 -1
  65. package/src/components/stalux/tags/tagsCloud.astro +3 -1
  66. package/src/components/stalux/words/wordCard.astro +5 -1
  67. package/src/internal/public-assets.ts +3 -3
  68. package/src/internal/style-names.generated.ts +20 -22
  69. package/src/layouts/PostLayout.astro +8 -0
  70. package/src/layouts/Stalux.astro +3 -1
  71. package/src/pages/404.astro +2 -1
  72. package/src/pages/index.astro +5 -1
  73. package/src/scripts/background.ts +54 -113
  74. package/src/scripts/list-reveal.ts +81 -0
  75. package/src/scripts/module-motion.ts +73 -0
  76. package/src/scripts/random-posts.ts +85 -47
  77. package/src/scripts/route-motion.ts +44 -24
  78. package/src/scripts/route-out.ts +11 -4
  79. package/src/scripts/smooth-scroll.ts +87 -0
  80. package/src/scripts/swup-router.ts +23 -15
  81. package/src/scripts/tags-cloud.ts +8 -3
  82. package/src/styles/base.css +2 -2
  83. package/src/styles/generated-article.css +1 -1
  84. package/src/styles/generated-links.css +1 -1
  85. package/src/styles/generated-waline.css +1 -1
  86. package/src/styles/generated-words.css +1 -1
  87. package/src/styles/generated.css +1 -1
  88. package/src/styles/integrations.css +1 -17
  89. package/src/styles/module-motion.css +174 -0
  90. package/src/styles/motion.css +2 -26
  91. package/src/utils/route-motion-policy.ts +5 -1
package/README.md CHANGED
@@ -18,9 +18,9 @@ Standalone images written as `![Image description](url)` receive a `figure` and
18
18
 
19
19
  Code blocks default to the single `dark-plus` theme, with system color-scheme switching and theme selectors disabled. To opt into another theme or switching behavior, explicitly override `themes`, `useDarkModeMediaQuery`, and `themeCssSelector` through `expressiveCode`. This only changes generated code styles and adds no telemetry.
20
20
 
21
- Swup owns soft navigation and transition timing. Astro renders the static documents; the browser runtime replaces only `main[data-stalux-route]`, keeping the header, footer, scroll sentinel and two background layers alive. The incoming visible heading receives a brief opacity or directional hint selected by the route policy: entering an article can move 6px vertically, explicit previous/next article links use opposite 8px horizontal hints, and returning to a list uses a 4px upward hint. Other article links and taxonomy changes fade without inventing a reading direction. Desktop effects last 80–140ms; phones use an 80ms fade without translation. The article body and long lists stay immediately readable. Reduced motion and browser-provided history gestures skip content motion; same-page anchors use their scrolling behavior and same-URL clicks preserve the reading position. Standalone HTML and exported data use document navigation. See the [Swup transition architecture and route policy](docs/view-transitions-swup.md).
21
+ Swup owns soft navigation and motion timing. Astro renders static documents; only `main[data-stalux-route]` is replaced, while navigation, footer, scroll sentinel and background retain their DOM identities. After destination assets are ready, a visible height-bounded heading or introductory region receives a short exit and entry: up to 6px vertically for reading entry, 8px horizontally for explicit previous/next, 4px vertically for a neutral article change, and 6px horizontally within one taxonomy dimension. Cross-category/tag changes use a neutral fade. Desktop entry lasts 100–180ms with an 80ms exit; phones use a 120ms pure entry fade. Long documents remain directly readable without whole-main motion or hiding. History restoration, reduced motion, native browser gestures and same-page anchors skip page motion. Independent HTML and exported data use document navigation. See [Modules and view transitions](docs/MOTION.md) for the complete inventory and timing contract.
22
22
 
23
- The runtime uses Swup's Head, Accessibility, Preload, Scroll and JS plugins with Stalux lifecycle handlers. It waits for incoming stylesheets and gives the destination font a bounded 150ms head start, disposes old interactions before replacement, loads first-seen page scripts, then dispatches the Astro-compatible page lifecycle events used by comments, counters, tag clouds and WebMCP. Backgrounds decode independently and crossfade after the content animation finishes; stale navigation work cannot replace the current background. History positions are restored by the Swup runtime, including reduced-motion visits. This requires no new configuration or telemetry. Animation duration describes the effect itself, not total navigation time or a measured performance improvement.
23
+ The runtime uses Swup's Head, Accessibility, Preload, Scroll and JS plugins with Stalux lifecycle handlers. It waits for incoming stylesheets and gives the destination font a bounded 150ms head start, disposes old interactions before replacement, loads first-seen page scripts, then dispatches the Astro-compatible page lifecycle events used by comments, counters, tag clouds and WebMCP. Each full document randomly selects and asynchronously decodes one background, revealing it from the stable dark canvas over 200ms. Swup soft navigations retain that image without selecting, decoding, or crossfading another SVG. Reduced motion reveals it immediately; saveData keeps the dark canvas without requesting decorative SVGs. Custom `window.__STALUX_BG_URLS__` remains supported, and slow or failed decoding never blocks reading. History positions are restored by the Swup runtime, including reduced-motion visits. This requires no new configuration or telemetry. Animation duration describes the effect itself, not total navigation time or a measured performance improvement.
24
24
 
25
25
  ---
26
26
 
@@ -371,4 +371,6 @@ og:image: /images/custom-social-card.png
371
371
 
372
372
  Local covers are read from the final `publicDir`. Remote covers are requested by the build machine, with an 8-second timeout and an 8 MiB limit (remote bytes are cached for 24 hours); unreadable or undecodable artwork emits a warning and falls back to a title card. No remote image-generation service is used. WebP and PNG outputs are cached in `stalux-social-cards/` under Astro's cache directory and copied into each build output. Explicit sharing images are fetched by social platforms, so they must be publicly accessible images. Platforms maintain their own preview caches and may not refresh an existing share immediately after deployment.
373
373
 
374
- Content transitions follow Fuwari’s fade and vertical movement: keep the old content readable until destination assets are ready, fade it out over 140ms, then reveal the new content over 220ms (180ms on phones). The header and background persist. History restores the reading position without an outgoing animation. Same-page anchors use interruptible native smooth scrolling; reduced motion uses instant positioning. Cross-page scroll resets happen while content is hidden. No configuration is required.
374
+ Motion is specified for 55 visible semantic modules in [Modules and view transitions](docs/MOTION.md). Swup animates one visible, height-bounded heading or introductory region after destination assets are ready, with cues for reading, returning, filtering and previous/next articles. Long documents stay readable and the header, footer and dark canvas remain stable. Phones use a short fade; history restores the reading position; same-page anchors use interruptible native scrolling. Module hover, focus, press and asynchronous update feedback complement bounded secondary entry cues. Reduced motion and print remain static and readable. No new configuration is required.
375
+
376
+ Built-in background artwork uses committed lossless WebP companions. Ratio-only SVG sizing is preserved with `contain`; custom background URLs retain their existing sizing. Regenerate companions after changing the SVG originals with `bun run backgrounds:build`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.43.1",
3
+ "version": "1.45.0",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -98,7 +98,8 @@
98
98
  "styles:watch": "node scripts/build-styles.ts --watch",
99
99
  "benchmark:performance": "node scripts/benchmark-performance.ts",
100
100
  "benchmark:builds": "python3 scripts/benchmark-builds.py",
101
- "analyze": "SONDA=1 bun run build"
101
+ "analyze": "SONDA=1 bun run build",
102
+ "backgrounds:build": "bun run scripts/build-backgrounds.ts"
102
103
  },
103
104
  "dependencies": {
104
105
  "@astrojs/compiler": "^4.0.0",
@@ -145,6 +146,7 @@
145
146
  "@vitest/coverage-v8": "^5.0.4",
146
147
  "astro": "^7.4.2",
147
148
  "knip": "^6.41.0",
149
+ "sharp": "0.35.5",
148
150
  "sonda": "^0.14.0",
149
151
  "typescript": "npm:@typescript/typescript6@6.0.2",
150
152
  "unocss": "^66.11.0",
@@ -77,9 +77,12 @@ const archiveYears = [...archivesByYearMonth.entries()]
77
77
  { length: Math.ceil(monthPosts.length / 12) },
78
78
  (_, index) => monthPosts.slice(index * 12, (index + 1) * 12),
79
79
  ).map((group) => (
80
- <ul class="stalux-archive-post-list">
80
+ <ul class="stalux-archive-post-list" data-stalux-reveal>
81
81
  {group.map((post) => (
82
- <li class="stalux-archive-post-item">
82
+ <li
83
+ class="stalux-archive-post-item"
84
+ data-stalux-reveal-item
85
+ >
83
86
  <time
84
87
  class="stalux-archive-post-date"
85
88
  datetime={toMachineDateTime(
@@ -127,7 +130,10 @@ const archiveYears = [...archivesByYearMonth.entries()]
127
130
 
128
131
  <script>
129
132
  import { mountArchiveDescriptions } from "../../../scripts/archive-descriptions";
133
+ import { mountListReveal } from "../../../scripts/list-reveal";
130
134
  import { registerPageLifecycle } from "../../../scripts/page-runtime";
131
135
 
132
136
  registerPageLifecycle("archive-descriptions", mountArchiveDescriptions);
137
+ // The archive cascade ships only with the archive markup it animates.
138
+ registerPageLifecycle("list-reveal", mountListReveal);
133
139
  </script>
@@ -10,5 +10,5 @@ const { t } = createTranslator(siteConfig.lang || "zh-CN");
10
10
  ---
11
11
 
12
12
  <div id={"stalux-to-archive-archs"}>
13
- <a href="/archives" data-astro-prefetch>{t("archives.viewAll")}</a>
13
+ <a data-stalux-control="button" href="/archives" data-astro-prefetch>{t("archives.viewAll")}</a>
14
14
  </div>
@@ -21,7 +21,7 @@ const { t } = createTranslator((siteConfig.lang as string) || "zh-CN");
21
21
  ---
22
22
 
23
23
  <div class="stalux-category-shell">
24
- <article class="stalux-category-card">
24
+ <article class="stalux-category-card" data-stalux-control="category-card">
25
25
  <a
26
26
  href={`/categories/${props.category.name}/`}
27
27
  class="stalux-categoryheader a-none"
@@ -21,7 +21,7 @@ const navigationHref = isHomePage ? "/about" : "/";
21
21
  <figure
22
22
  class={`stalux-author-figure ${props.iscolum ? "stalux-author-figure-column stalux-u-flex-col" : ""}`}
23
23
  >
24
- <a href={navigationHref} class="a-none" data-astro-prefetch>
24
+ <a data-stalux-control="avatar" href={navigationHref} class="a-none" data-astro-prefetch>
25
25
  <Image src={authorConfig.avatar} alt={authorConfig.name} width={100} height={100} />
26
26
  </a>
27
27
  <figcaption>{authorConfig.name}</figcaption>
@@ -36,7 +36,11 @@ function badgeImg(opts: BadgeOptions) {
36
36
  if (isBadgeGroup(item)) {
37
37
  const groupId = `badge-group-${idx}`;
38
38
  return (
39
- <details class="stalux-badge-group stalux-u-w-full badge-group" id={groupId}>
39
+ <details
40
+ data-stalux-module="badge-group"
41
+ class="stalux-badge-group stalux-u-w-full badge-group"
42
+ id={groupId}
43
+ >
40
44
  <summary class="stalux-badge-group-summary stalux-u-flex stalux-u-items-center stalux-u-justify-center">
41
45
  <span class="stalux-badge-group-arrow">&#9654;</span>
42
46
  <span class="stalux-badge-group-title">{item.title}</span>
@@ -7,7 +7,7 @@ interface Props {
7
7
  const { heading, description } = Astro.props;
8
8
  ---
9
9
 
10
- <>
10
+ <div data-stalux-motion-region>
11
11
  <h1 class="stalux-page-title stalux-u-text-center" data-stalux-motion-focus>
12
12
  {heading}
13
13
  </h1>
@@ -16,4 +16,4 @@ const { heading, description } = Astro.props;
16
16
  {description}
17
17
  </div>
18
18
  )}
19
- </>
19
+ </div>
@@ -51,7 +51,11 @@ const totalWordCount = await getTotalWordCount(Astro.logger);
51
51
  const formattedWordCount = formatWordCount(totalWordCount, siteConfig.lang || "zh-CN");
52
52
  ---
53
53
 
54
- <footer class="stalux-footer stalux-u-w-full stalux-u-text-center" data-stalux-font-group="footer">
54
+ <footer
55
+ class="stalux-footer stalux-u-w-full stalux-u-text-center"
56
+ data-stalux-font-group="footer"
57
+ data-stalux-module="footer"
58
+ >
55
59
  <div class="stalux-footer-content stalux-u-flex stalux-u-flex-col">
56
60
  <!-- 版权信息 -->
57
61
  {footerConfig?.copyright?.enabled && (
@@ -29,7 +29,13 @@ const desc = link.description ?? "";
29
29
  const icon = link.icon ?? link.avatar ?? "/favicon.ico";
30
30
  ---
31
31
 
32
- <a class="stalux-link-shell a-none" href={href} target="_blank" rel="noopener">
32
+ <a
33
+ data-stalux-control="link-card"
34
+ class="stalux-link-shell a-none"
35
+ href={href}
36
+ target="_blank"
37
+ rel="noopener"
38
+ >
33
39
  <div class="stalux-link-card">
34
40
  <div class="stalux-link-card-avatar">
35
41
  <img
@@ -40,6 +40,7 @@ function getIconByTitle(iconName: string): SimpleIcon | undefined {
40
40
  return (
41
41
  <li class="social_icon">
42
42
  <a
43
+ data-stalux-control="social"
43
44
  href={item.link}
44
45
  target="_blank"
45
46
  aria-label={item.icon}
@@ -9,7 +9,12 @@ const summary = description?.trim();
9
9
  ---
10
10
 
11
11
  {summary && (
12
- <aside class="stalux-article-summary" data-stalux-motion-content aria-label={label}>
12
+ <aside
13
+ class="stalux-article-summary"
14
+ data-stalux-motion-content
15
+ data-stalux-module="summary"
16
+ aria-label={label}
17
+ >
13
18
  <p class="stalux-article-summary-label">{label}</p>
14
19
  <p class="stalux-article-summary-text">{summary}</p>
15
20
  </aside>
@@ -46,12 +46,13 @@ const cfg = {
46
46
  <nav class="stalux-post-taxonomy-container" aria-label={title ?? cfg.heading}>
47
47
  <h3 class="stalux-post-taxonomy-title">{title ?? cfg.heading}</h3>
48
48
  {items.length > 0 ? (
49
- <div class={cfg.listClass}>
49
+ <div class={cfg.listClass} data-stalux-reveal>
50
50
  {items.map((item) => (
51
51
  <a
52
52
  href={`${cfg.prefix}${item}/`}
53
53
  class={`${cfg.itemClass} a-none`}
54
54
  data-astro-prefetch
55
+ data-stalux-reveal-item
55
56
  >
56
57
  {item}
57
58
  </a>
@@ -44,6 +44,7 @@ const walineConfig = {
44
44
  {walineConfig.serverURL && (
45
45
  <section
46
46
  id="waline-container"
47
+ data-stalux-module="comments"
47
48
  class="waline-comment-container"
48
49
  aria-label={t("comment.title")}
49
50
  data-waline-config={
@@ -26,7 +26,7 @@ const licenseLink = buildCCLink(cc);
26
26
  const licenseName = buildCCName(cc, t);
27
27
  ---
28
28
 
29
- <div class="stalux-license-box">
29
+ <div class="stalux-license-box" data-stalux-module="license">
30
30
  <div class="stalux-license-content">
31
31
  <p class="stalux-license-title">{title}</p>
32
32
  <p class="stalux-license-author">{t("post.author")}{author}</p>
@@ -19,7 +19,10 @@ const timezone = siteConfig.timezone;
19
19
  const description = post.data.desc || "";
20
20
  ---
21
21
 
22
- <article class="stalux-post-card-shell block h-full text-inherit no-underline">
22
+ <article
23
+ data-stalux-control="post-card"
24
+ class="stalux-post-card-shell block h-full text-inherit no-underline"
25
+ >
23
26
  <div class="stalux-post-card stalux-u-flex stalux-u-flex-col stalux-u-w-full stalux-u-min-w-0">
24
27
  {post.data.cover && (
25
28
  <a
@@ -40,7 +40,7 @@ const mdSourceUrl = exportMd ? `${Astro.url.pathname.replace(/\/$/, "")}.md` : "
40
40
 
41
41
  <article class="stalux-article" data-stalux-gallery={props.hasImage ? "true" : undefined}>
42
42
  <!-- Article Header -->
43
- <header class="stalux-post-article-header">
43
+ <header class="stalux-post-article-header" data-stalux-motion-region>
44
44
  <h1 class="stalux-post-title" data-stalux-motion-focus>
45
45
  {props.title}
46
46
  </h1>
@@ -5,8 +5,8 @@ import PostAuthor from "@components/stalux/posts/postAuthor.astro";
5
5
  import RandomPost from "@components/stalux/posts/randomPost.astro";
6
6
  ---
7
7
 
8
- <aside class="stalux-sidebar">
9
- <div class="stalux-sidebar-container">
8
+ <aside class="stalux-sidebar" data-swup-persist="post-sidebar-left">
9
+ <div class="stalux-sidebar-container" data-stalux-module="sidebar">
10
10
  <PostAuthor />
11
11
  </div>
12
12
  <div class="stalux-sidebar-container">
@@ -19,7 +19,11 @@ const { t } = createTranslator((siteConfig.lang as string) || "zh-CN");
19
19
  ---
20
20
 
21
21
  {(props.prev || props.next) && (
22
- <nav class="stalux-pagination" aria-label={t("post.navigation")}>
22
+ <nav
23
+ data-stalux-module="post-navigation"
24
+ class="stalux-pagination"
25
+ aria-label={t("post.navigation")}
26
+ >
23
27
  <div class="stalux-pagination-inner">
24
28
  {props.prev ? (
25
29
  <a
@@ -15,7 +15,11 @@ const { headings = [], tags = [], categories = [] } = Astro.props;
15
15
  ---
16
16
 
17
17
  <aside class="stalux-sidebar-right" data-stalux-scroll-pane>
18
- <div class="stalux-sidebar-container">
18
+ <div
19
+ class="stalux-sidebar-container"
20
+ data-stalux-module="sidebar"
21
+ data-swup-persist="post-sidebar-clock"
22
+ >
19
23
  <PostDate />
20
24
  <Weather />
21
25
  </div>
@@ -32,9 +32,9 @@ const tocItems = headings
32
32
  <nav data-toc-active-class="stalux-toc-active" aria-label={t("taxonomy.toc")}>
33
33
  <h3 class="stalux-toc-title">{title}</h3>
34
34
  {tocItems.length > 0 ? (
35
- <ul class="stalux-toc-list">
35
+ <ul class="stalux-toc-list" data-stalux-reveal>
36
36
  {tocItems.map((heading) => (
37
- <li>
37
+ <li data-stalux-reveal-item>
38
38
  <a
39
39
  class={`stalux-toc-item ${({ 1: "stalux-toc-depth-1", 2: "stalux-toc-depth-2", 3: "stalux-toc-depth-3", 4: "stalux-toc-depth-4", 5: "stalux-toc-depth-5", 6: "stalux-toc-depth-6" } as Record<number, string>)[heading.depth] || ""} a-none`}
40
40
  href={`#${heading.slug}`}
@@ -27,7 +27,7 @@ if (typeKey === "category" || typeKey === "categories" || typeKey === "分类")
27
27
  }
28
28
  ---
29
29
 
30
- <header class="stalux-tag-header">
30
+ <header class="stalux-tag-header" data-stalux-motion-region>
31
31
  <h1 class="stalux-tag-page-title" data-stalux-motion-focus>
32
32
  <span class="stalux-tag-category-icon">
33
33
  <svg
@@ -22,7 +22,7 @@ const tagLinks = tagList.map((t) => ({
22
22
  ---
23
23
 
24
24
  <!-- 移动端:SSR 卡片 -->
25
- <div class="stalux-cloud-cards" id="tags-cards" data-stalux-motion-content>
25
+ <div class="stalux-cloud-cards" id="tags-cards" data-stalux-tag-cards data-stalux-motion-content>
26
26
  {tagList.length > 0 ? (
27
27
  tagGroups.map((group) => (
28
28
  <div class="stalux-cloud-tag-group">
@@ -32,6 +32,7 @@ const tagLinks = tagList.map((t) => ({
32
32
  class="stalux-cloud-tag-card"
33
33
  data-stalux-card
34
34
  data-stalux-tag-card
35
+ data-stalux-control="tag-card"
35
36
  data-count={count}
36
37
  data-astro-prefetch
37
38
  >
@@ -49,6 +50,7 @@ const tagLinks = tagList.map((t) => ({
49
50
  <!-- 桌面端:Canvas 标签云 -->
50
51
  <div
51
52
  class="stalux-cloud-canvas-frame"
53
+ data-stalux-tag-canvas
52
54
  style={{ "--cloud-density": cloudDensity }}
53
55
  data-stalux-motion-content
54
56
  >
@@ -19,7 +19,11 @@ const { source, link, sourceDate, date, abbrlink } = entry.data;
19
19
  const machineDate = date && timezone ? toMachineDateTime(date, timezone) : date;
20
20
  ---
21
21
 
22
- <article class="stalux-word-card" id={abbrlink ? `word-${abbrlink}` : undefined}>
22
+ <article
23
+ data-stalux-module="word"
24
+ class="stalux-word-card"
25
+ id={abbrlink ? `word-${abbrlink}` : undefined}
26
+ >
23
27
  <Quote class="stalux-word-mark" size={28} stroke-width={1.4} aria-hidden="true" />
24
28
  <div class="stalux-word-body stalux-prose" data-pagefind-body>
25
29
  <Content />
@@ -2,11 +2,11 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "
2
2
  import path from "node:path";
3
3
 
4
4
  // ---------------------------------------------------------------------------
5
- // 背景 SVG 判别式同步
5
+ // 背景资源判别式同步
6
6
  // ---------------------------------------------------------------------------
7
7
 
8
8
  /**
9
- * 把主题包的 public/background/*.svg 同步到用户项目 public/background/。
9
+ * 把主题包的 public/background/ SVG 原图及 WebP 同步到用户 public/background/。
10
10
  *
11
11
  * 判别式(非无条件覆盖):
12
12
  * - 同名文件:内容一致则跳过(幂等);内容不同才覆盖(主题更新了 SVG)
@@ -26,7 +26,7 @@ export function syncBackgroundSvgs(
26
26
  let copied = 0;
27
27
  let skipped = 0;
28
28
  for (const name of readdirSync(srcBg)) {
29
- if (!name.endsWith(".svg")) continue;
29
+ if (!/\.(?:svg|webp)$/u.test(name)) continue;
30
30
  const srcFile = path.join(srcBg, name);
31
31
  const destFile = path.join(destBg, name);
32
32
  if (existsSync(destFile)) {
@@ -333,27 +333,25 @@ export const styleNames: { classes: Record<string, string>; variables: Record<st
333
333
  "--white-full": "--s2x"
334
334
  },
335
335
  "animations": {
336
- "fadeInUpComment": "k0",
337
- "spin": "k1",
338
- "stalux-article-enter": "k2",
339
- "stalux-article-enter-back": "k3",
340
- "stalux-cursor-blink": "k4",
341
- "stalux-link-avatar-spin": "k5",
342
- "stalux-page-enter": "k6",
343
- "stalux-page-enter-back": "k7",
344
- "stalux-page-exit": "k8",
345
- "stalux-random-spin": "k9",
346
- "stalux-sidebar-left-back-in": "ka",
347
- "stalux-sidebar-left-back-out": "kb",
348
- "stalux-sidebar-left-in": "kc",
349
- "stalux-sidebar-left-out": "kd",
350
- "stalux-sidebar-right-back-in": "ke",
351
- "stalux-sidebar-right-back-out": "kf",
352
- "stalux-sidebar-right-in": "kg",
353
- "stalux-sidebar-right-out": "kh",
354
- "stalux-social-stalux-social-arrive": "ki",
355
- "stalux-taxonomy-enter": "kj",
356
- "stalux-taxonomy-enter-back": "kk",
357
- "stalux-word-word-glow": "kl"
336
+ "spin": "k0",
337
+ "stalux-article-enter": "k1",
338
+ "stalux-article-enter-back": "k2",
339
+ "stalux-cursor-blink": "k3",
340
+ "stalux-link-avatar-spin": "k4",
341
+ "stalux-page-enter": "k5",
342
+ "stalux-page-enter-back": "k6",
343
+ "stalux-page-exit": "k7",
344
+ "stalux-random-spin": "k8",
345
+ "stalux-sidebar-left-back-in": "k9",
346
+ "stalux-sidebar-left-back-out": "ka",
347
+ "stalux-sidebar-left-in": "kb",
348
+ "stalux-sidebar-left-out": "kc",
349
+ "stalux-sidebar-right-back-in": "kd",
350
+ "stalux-sidebar-right-back-out": "ke",
351
+ "stalux-sidebar-right-in": "kf",
352
+ "stalux-sidebar-right-out": "kg",
353
+ "stalux-social-stalux-social-arrive": "kh",
354
+ "stalux-taxonomy-enter": "ki",
355
+ "stalux-taxonomy-enter-back": "kj"
358
356
  }
359
357
  };
@@ -92,3 +92,11 @@ const walineConfig = walineSection?.serverURL
92
92
  <PostRight headings={headings} tags={tags} categories={categories} />
93
93
  </div>
94
94
  </div>
95
+
96
+ <script>
97
+ import { mountListReveal } from "../scripts/list-reveal";
98
+ import { registerPageLifecycle } from "../scripts/page-runtime";
99
+
100
+ // Article sidebars cascade their lists; the script ships with article pages only.
101
+ registerPageLifecycle("list-reveal", mountListReveal);
102
+ </script>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import "../styles/module-motion.css";
2
3
  import { getCollection } from "astro:content";
3
4
  import Footer from "@components/stalux/layout/footer.astro";
4
5
  import Head from "@components/stalux/layout/head.astro";
@@ -41,7 +42,7 @@ const htmlLang = siteConfig.lang || "zh-CN";
41
42
  // 背景 SVG 以静态 URL 硬编码在 background.ts(纯客户端),
42
43
  // 集成在 config:done 时把 SVG 复制到用户项目最终的 publicDir/background/。
43
44
  // SSR 不渲染背景图,默认露出 .bg-layer 的灰墙底色(--stalux-bg-color),
44
- // 客户端运行时随机选图;不经过 import.meta.glob 资源管线,
45
+ // 每个完整文档首次客户端随机选图,同一 Swup 会话复用;不经过 import.meta.glob 资源管线,
45
46
  // 保证增量构建依赖图 hash 稳定。
46
47
  const llmPromoteRaw = promoteConfig?.llm_promote ?? "";
47
48
  const llmPromote = llmPromoteRaw
@@ -73,6 +74,7 @@ const routeKind = getRouteKind(Astro.url.pathname);
73
74
  />
74
75
  <script>
75
76
  import "../scripts/background.ts";
77
+ import "../scripts/module-motion.ts";
76
78
  import "../scripts/view-transitions.ts";
77
79
  </script>
78
80
  {aiConfig?.conformance !== "disabled" && <script>import "../scripts/webmcp.ts";</script>}
@@ -17,7 +17,7 @@ const { t } = createTranslator(siteConfig.lang || "zh-CN");
17
17
  class="stalux-404-error-container stalux-u-flex stalux-u-items-center stalux-u-justify-center stalux-u-text-center"
18
18
  data-pagefind-ignore
19
19
  >
20
- <div class="stalux-404-error-content">
20
+ <div class="stalux-404-error-content" data-stalux-motion-region>
21
21
  <h1 data-stalux-motion-focus>404</h1>
22
22
  <p data-stalux-motion-content>{t("page404.pageNotFound")}</p>
23
23
  <p class="agent-recovery" data-stalux-motion-content>
@@ -38,6 +38,7 @@ const { t } = createTranslator(siteConfig.lang || "zh-CN");
38
38
  <a
39
39
  href="/"
40
40
  class="stalux-404-home-btn home-btn"
41
+ data-stalux-control="button"
41
42
  data-astro-prefetch
42
43
  data-stalux-motion-content
43
44
  >{t("page404.backHome")}</a
@@ -20,7 +20,11 @@ import { getSiteData } from "@utils/config-utils";
20
20
 
21
21
  <Stalux title={siteTitle} description={siteDescription}>
22
22
  <div class="stalux-home-body stalux-u-flex stalux-u-items-center stalux-u-flex-col">
23
- <h1 class="stalux-home-title stalux-u-text-center" data-stalux-motion-focus>
23
+ <h1
24
+ class="stalux-home-title stalux-u-text-center"
25
+ data-stalux-motion-region
26
+ data-stalux-motion-focus
27
+ >
24
28
  {siteTitle}
25
29
  </h1>
26
30
  <h2