@xingwangzhe/stalux 1.21.5 → 1.21.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.21.5",
3
+ "version": "1.21.7",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -7,8 +7,3 @@
7
7
  <div data-pf-theme="dark">
8
8
  <pagefind-modal id="search"></pagefind-modal>
9
9
  </div>
10
-
11
- <script>
12
- import "@pagefind/component-ui";
13
- import "@pagefind/component-ui/css";
14
- </script>
@@ -93,7 +93,7 @@ function badgeImg(opts: Record<string, any>) {
93
93
  align-items: center;
94
94
  gap: 0.4rem;
95
95
  padding: 0.2rem 0;
96
- color: var(--white-40p);
96
+ color: var(--white-70p);
97
97
  font-size: 0.85rem;
98
98
  user-select: none;
99
99
  list-style: none;
@@ -116,12 +116,12 @@ function badgeImg(opts: Record<string, any>) {
116
116
  }
117
117
 
118
118
  .badge-group-title {
119
- color: var(--white-60p);
119
+ color: var(--white-80p);
120
120
  }
121
121
 
122
122
  .badge-group-hint {
123
123
  font-size: 0.7rem;
124
- color: var(--white-25p);
124
+ color: var(--white-70p);
125
125
  }
126
126
  .badge-group[open] .badge-group-hint[data-hint="collapsed"],
127
127
  .badge-group:not([open]) .badge-group-hint[data-hint="expanded"] {
@@ -27,6 +27,7 @@ const copyrightYear =
27
27
 
28
28
  <style>
29
29
  .copyright {
30
+ color: var(--white-70p);
30
31
  font-weight: 500;
31
32
  display: flex;
32
33
  gap: 0.3em;
@@ -42,6 +42,7 @@ const showThemeInfoSection = !!(showPoweredBy || showThemeInfo);
42
42
 
43
43
  <style>
44
44
  .theme-info {
45
+ color: var(--white-70p);
45
46
  font-size: 0.85rem;
46
47
  display: flex;
47
48
  justify-content: center;
@@ -62,7 +62,7 @@ const navs = navsConfig?.items ?? [];
62
62
  }
63
63
  <li class={styles.navItem}>
64
64
  <a
65
- href="javascript:void(0)"
65
+ href="#search"
66
66
  class={styles.navLink}
67
67
  data-ref="stalux-search-btn"
68
68
  aria-label={t("search.title")}
@@ -87,14 +87,23 @@ const navs = navsConfig?.items ?? [];
87
87
  </nav>
88
88
  <Search />
89
89
  <script>
90
+ const navWindow = window as Window & {
91
+ __STALUX_NAV_INITIALIZED__?: boolean;
92
+ };
93
+
90
94
  document.addEventListener("astro:page-load", () => {
95
+ if (navWindow.__STALUX_NAV_INITIALIZED__) return;
96
+ navWindow.__STALUX_NAV_INITIALIZED__ = true;
97
+
91
98
  const searchBtn = document.querySelector(
92
99
  '[data-ref="stalux-search-btn"]',
93
100
  );
94
- searchBtn?.addEventListener("click", (e) => {
101
+ searchBtn?.addEventListener("click", async (e) => {
95
102
  e.preventDefault();
96
103
  closeNav();
97
- document.getElementById('search')?.open();
104
+ await import("@pagefind/component-ui");
105
+ await import("@pagefind/component-ui/css");
106
+ document.getElementById("search")?.open();
98
107
  });
99
108
 
100
109
  // 汉堡菜单交互
@@ -139,7 +148,20 @@ const navs = navsConfig?.items ?? [];
139
148
  const btnRight = document.querySelector('[data-ref="stalux-nav-scroll-right"]');
140
149
  if (!navList || !btnLeft || !btnRight) return;
141
150
 
151
+ let frameId = 0;
152
+ let lastScale = "1";
153
+
154
+ function updateScrollButtons() {
155
+ const hasOverflow = navList.scrollWidth > navList.clientWidth;
156
+ const atStart = navList.scrollLeft <= 1;
157
+ const atEnd = navList.scrollLeft + navList.clientWidth >= navList.scrollWidth - 1;
158
+
159
+ btnLeft.toggleAttribute("data-visible", hasOverflow && !atStart);
160
+ btnRight.toggleAttribute("data-visible", hasOverflow && !atEnd);
161
+ }
162
+
142
163
  function updateButtons() {
164
+ frameId = 0;
143
165
  const navWidth = navList.clientWidth;
144
166
  const itemCount = navList.children.length;
145
167
  const baseFontSize = Number.parseFloat(getComputedStyle(navList).fontSize) || 16;
@@ -150,10 +172,17 @@ const navs = navsConfig?.items ?? [];
150
172
  0,
151
173
  ) + gap * Math.max(0, itemCount - 1);
152
174
  const scale = estimatedWidth > navWidth ? Math.max(minFontSize / baseFontSize, navWidth / estimatedWidth) : 1;
153
- navList.style.setProperty("--nav-scale", String(scale));
154
- navList.removeAttribute("data-overflow");
155
- btnLeft.removeAttribute("data-visible");
156
- btnRight.removeAttribute("data-visible");
175
+ const nextScale = String(scale);
176
+ if (nextScale !== lastScale) {
177
+ navList.style.setProperty("--nav-scale", nextScale);
178
+ lastScale = nextScale;
179
+ }
180
+ updateScrollButtons();
181
+ }
182
+
183
+ function scheduleUpdate() {
184
+ if (frameId) return;
185
+ frameId = requestAnimationFrame(updateButtons);
157
186
  }
158
187
 
159
188
  btnLeft.addEventListener("click", () => {
@@ -163,18 +192,16 @@ const navs = navsConfig?.items ?? [];
163
192
  btnRight.addEventListener("click", () => {
164
193
  navList.scrollBy({ left: 300, behavior: "smooth" });
165
194
  });
166
-
167
- // 监听滚动事件更新按钮状态
168
- navList.addEventListener("scroll", updateButtons);
195
+ navList.addEventListener("scroll", scheduleUpdate, { passive: true });
169
196
 
170
197
  // 用 ResizeObserver 监听容器尺寸变化(窗口缩窄/变宽时重新判断)
171
- const ro = new ResizeObserver(updateButtons);
198
+ const ro = new ResizeObserver(scheduleUpdate);
172
199
  ro.observe(navList);
173
200
  if (navList.parentElement) ro.observe(navList.parentElement);
174
- window.addEventListener("resize", updateButtons, { passive: true });
175
- window.addEventListener("load", updateButtons, { once: true });
201
+ window.addEventListener("resize", scheduleUpdate, { passive: true });
202
+ window.addEventListener("load", scheduleUpdate, { once: true });
176
203
 
177
204
  // 初始检测
178
- updateButtons();
205
+ scheduleUpdate();
179
206
  }
180
207
  </script>
@@ -44,7 +44,7 @@ const icon = link.icon ?? link.avatar ?? "/favicon.ico";
44
44
  />
45
45
  </div>
46
46
  <div class={styles.content}>
47
- <h3 class={styles.title}>{title}</h3>
47
+ <h2 class={styles.title}>{title}</h2>
48
48
  {desc && <p class={styles.description}>{desc}</p>}
49
49
  </div>
50
50
  </a>
@@ -8,9 +8,10 @@ import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, get
8
8
 
9
9
  interface Props {
10
10
  post: CollectionEntry<"posts">;
11
+ priority?: boolean;
11
12
  }
12
13
 
13
- const post = Astro.props.post;
14
+ const { post, priority = false } = Astro.props;
14
15
  const configEntries = await getCollection("config");
15
16
  const siteConfig = getSiteData(configEntries);
16
17
  const timezone = siteConfig.timezone;
@@ -19,16 +20,35 @@ const timezone = siteConfig.timezone;
19
20
  const description = post.data.desc || "";
20
21
  ---
21
22
 
22
- <a href={`/posts/${post.data.abbrlink}/`} class={style.post + " a-none"} data-astro-prefetch="viewport">
23
+ <article class={style.post}>
23
24
  <div class={style.postContainer}>
24
25
  {
25
26
  post.data.cover && (
26
- <div class={style.postCover}>
27
- <img src={post.data.cover} alt={post.data.title} loading="lazy" />
28
- </div>
27
+ <a
28
+ href={`/posts/${post.data.abbrlink}/`}
29
+ class="a-none"
30
+ data-astro-prefetch="viewport"
31
+ aria-label={post.data.title}
32
+ >
33
+ <div class={style.postCover}>
34
+ <img
35
+ src={post.data.cover}
36
+ alt={post.data.title}
37
+ loading={priority ? "eager" : "lazy"}
38
+ decoding="async"
39
+ fetchpriority={priority ? "high" : "auto"}
40
+ width="640"
41
+ height="360"
42
+ />
43
+ </div>
44
+ </a>
29
45
  )
30
46
  }
31
- <h2 class={style.postTitle}>{post.data.title}</h2>
47
+ <h2 class={style.postTitle}>
48
+ <a href={`/posts/${post.data.abbrlink}/`} class="a-none" data-astro-prefetch="viewport">
49
+ {post.data.title}
50
+ </a>
51
+ </h2>
32
52
  <div class={style.time}>
33
53
  {
34
54
  post.data.date && (
@@ -57,7 +77,7 @@ const description = post.data.desc || "";
57
77
  {
58
78
  post.data.tags?.map((tag) => (
59
79
  <a class="a-none" href={`/tags/${tag}`} data-astro-prefetch>
60
- <div class={style.postTag}>{tag}</div>
80
+ <span class={style.postTag}>{tag}</span>
61
81
  </a>
62
82
  ))
63
83
  }
@@ -66,18 +86,18 @@ const description = post.data.desc || "";
66
86
  {
67
87
  post.data.categories?.map((category) => (
68
88
  <a class="a-none" href={`/categories/${category}`} data-astro-prefetch>
69
- <div class={style.postCategory}>{category}</div>
89
+ <span class={style.postCategory}>{category}</span>
70
90
  </a>
71
91
  ))
72
92
  }
73
93
  </div>
74
94
  </div>
75
95
  <a
76
- href={`/posts/${post.data.abbrlink}`}
96
+ href={`/posts/${post.data.abbrlink}/`}
77
97
  class={style.readMore + " a-none"}
78
98
  data-astro-prefetch
79
99
  >
80
100
  Read More
81
101
  </a>
82
102
  </div>
83
- </a>
103
+ </article>
@@ -117,24 +117,30 @@ const mdSourceUrl = exportMd
117
117
  const container = document.querySelector("[data-pagefind-body]");
118
118
  if (!container) return;
119
119
 
120
- const links = container.querySelectorAll(GALLERY_SELECTOR);
121
- await Promise.all(
122
- [...links].map(async (link) => {
123
- const img = link.querySelector("img");
124
- if (!img) return;
125
-
126
- const tempImg = new Image();
127
- tempImg.src = img.currentSrc || img.src;
128
- try {
129
- await tempImg.decode();
130
- } catch {
131
- /* 解码失败时跳过尺寸写入 */
132
- return;
133
- }
134
- link.dataset.pswpWidth = String(tempImg.naturalWidth);
135
- link.dataset.pswpHeight = String(tempImg.naturalHeight);
136
- }),
137
- );
120
+ const links = [...container.querySelectorAll(GALLERY_SELECTOR)];
121
+ for (const link of links) {
122
+ const img = link.querySelector("img");
123
+ if (!img) continue;
124
+
125
+ const width = img.naturalWidth;
126
+ const height = img.naturalHeight;
127
+ if (width && height) {
128
+ link.dataset.pswpWidth = String(width);
129
+ link.dataset.pswpHeight = String(height);
130
+ continue;
131
+ }
132
+
133
+ const tempImg = new Image();
134
+ tempImg.src = img.currentSrc || img.src;
135
+ try {
136
+ await tempImg.decode();
137
+ } catch {
138
+ /* 解码失败时跳过尺寸写入 */
139
+ continue;
140
+ }
141
+ link.dataset.pswpWidth = String(tempImg.naturalWidth);
142
+ link.dataset.pswpHeight = String(tempImg.naturalHeight);
143
+ }
138
144
  }
139
145
 
140
146
  let lightbox = null;
@@ -150,7 +156,8 @@ const mdSourceUrl = exportMd
150
156
  lightbox = null;
151
157
  }
152
158
 
153
- await enrichPhotoSwipeData();
159
+ // 不阻塞文章首屏;已加载图片会立即写入尺寸,其余图片在后台补齐。
160
+ void enrichPhotoSwipeData();
154
161
 
155
162
  lightbox = new PhotoSwipeLightbox({
156
163
  gallery: "[data-pagefind-body]",
@@ -15,9 +15,9 @@ const posts = props.posts ?? (await getCollection("posts", ({ data }) => !data.d
15
15
  <div data-stalux="postlist" class={styles["container"]}>
16
16
  <ul class={styles["post-list"]}>
17
17
  {
18
- posts.map((post) => (
18
+ posts.map((post, index) => (
19
19
  <li>
20
- <PostCard post={post} />
20
+ <PostCard post={post} priority={index === 0} />
21
21
  </li>
22
22
  ))
23
23
  }
@@ -25,7 +25,12 @@ const sorted = [...entries].sort((a, b) => {
25
25
  return (
26
26
  <li class={styles.item}>
27
27
  {hash ? (
28
- <a href="javascript:void(0)" class="a-none" data-word-hash={hash}>
28
+ <a
29
+ href={hash}
30
+ class="a-none"
31
+ data-word-hash={hash}
32
+ aria-label={`Jump to ${entry.data.abbrlink}`}
33
+ >
29
34
  <WordCard entry={entry} timezone={timezone} />
30
35
  </a>
31
36
  ) : (
@@ -27,7 +27,7 @@ const { t } = createTranslator(siteConfig.lang || "zh-CN");
27
27
  </div>
28
28
 
29
29
  <script define:vars={{ countdownText: t("page404.countdown", { n: "{n}" }) }} is:inline>
30
- let timer: number | undefined;
30
+ let timer;
31
31
 
32
32
  function startCountdown() {
33
33
  if (timer !== undefined) window.clearInterval(timer);
@@ -40,7 +40,7 @@ const { t } = createTranslator(siteConfig.lang || "zh-CN");
40
40
  if (seconds <= 0) {
41
41
  if (timer !== undefined) window.clearInterval(timer);
42
42
  timer = undefined;
43
- const home = document.querySelector<HTMLAnchorElement>(".home-btn");
43
+ const home = document.querySelector(".home-btn");
44
44
  home?.click();
45
45
  } else {
46
46
  el.textContent = countdownText.replace("{n}", String(seconds));
@@ -56,13 +56,19 @@ export const featureFlagsHast = defineHastPlugin({
56
56
  filter: ["img"],
57
57
  visit(node, ctx) {
58
58
  ctx.data.astro.frontmatter.hasImage = true;
59
- // 添加懒加载和异步解码(封面图不在此处理,由 postCard.astro 控制)
59
+ // 第一张正文图片可能成为 LCP,优先加载;其余图片延迟加载。
60
+ const data = ctx.data as Record<string, unknown>;
61
+ const firstImage = !data.staluxFirstImageSeen;
62
+ data.staluxFirstImageSeen = true;
60
63
  if (!node.properties?.loading) {
61
- ctx.setProperty(node, "loading", "lazy");
64
+ ctx.setProperty(node, "loading", firstImage ? "eager" : "lazy");
62
65
  }
63
66
  if (!node.properties?.decoding) {
64
67
  ctx.setProperty(node, "decoding", "async");
65
68
  }
69
+ if (firstImage && !node.properties?.fetchpriority) {
70
+ ctx.setProperty(node, "fetchpriority", "high");
71
+ }
66
72
  },
67
73
  },
68
74
  });
@@ -3,6 +3,7 @@
3
3
  ul {
4
4
  display: flex;
5
5
  flex-wrap: wrap;
6
+ justify-content: center;
6
7
  gap: 1em;
7
8
  padding: 0;
8
9
  margin: 0;
@@ -1,3 +1,25 @@
1
+ .post {
2
+ display: block;
3
+ height: 100%;
4
+ color: inherit;
5
+ text-decoration: none;
6
+ }
7
+
8
+ .postTitle {
9
+ margin: 0;
10
+ line-height: 1.35;
11
+ }
12
+
13
+ .postTitle a {
14
+ color: inherit;
15
+ text-decoration: none;
16
+ }
17
+
18
+ .postDescription {
19
+ color: var(--white-70p);
20
+ line-height: 1.6;
21
+ }
22
+
1
23
  .postContainer {
2
24
  background-color: var(--card-bg);
3
25
  border-radius: var(--radius-lg);
@@ -37,7 +37,7 @@ export type RouteAnimation = {
37
37
  const easing = "cubic-bezier(0.25, 0.9, 0.2, 1)";
38
38
  const exitAnimation: TransitionAnimation = {
39
39
  name: "stalux-page-fade-out",
40
- duration: "0.32s",
40
+ duration: "0.16s",
41
41
  easing,
42
42
  fillMode: "both",
43
43
  };
@@ -48,7 +48,7 @@ function createRouteAnimation(name: string, backName = `${name}-back`): RouteAni
48
48
  old: exitAnimation,
49
49
  new: {
50
50
  name: `stalux-page-${name}-in`,
51
- duration: "0.42s",
51
+ duration: "0.2s",
52
52
  easing,
53
53
  fillMode: "both",
54
54
  },
@@ -57,7 +57,7 @@ function createRouteAnimation(name: string, backName = `${name}-back`): RouteAni
57
57
  old: exitAnimation,
58
58
  new: {
59
59
  name: `stalux-page-${backName}-in`,
60
- duration: "0.42s",
60
+ duration: "0.2s",
61
61
  easing,
62
62
  fillMode: "both",
63
63
  },