@xingwangzhe/stalux 1.21.3 → 1.21.6

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.3",
3
+ "version": "1.21.6",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -84,7 +84,6 @@
84
84
  "@waline/client": "^3.15.2",
85
85
  "@xingwangzhe/satteri-mermaid": "0.7.3",
86
86
  "@xingwangzhe/satteri-photoswipe": "^0.2.2",
87
- "@xingwangzhe/stalux": "1.21.0",
88
87
  "@xingwangzhe/tags-cloud": "^1.2.5",
89
88
  "animejs": "^4.5.0",
90
89
  "astro-expressive-code": "^0.44.1",
@@ -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")}
@@ -91,10 +91,12 @@ const navs = navsConfig?.items ?? [];
91
91
  const searchBtn = document.querySelector(
92
92
  '[data-ref="stalux-search-btn"]',
93
93
  );
94
- searchBtn?.addEventListener("click", (e) => {
94
+ searchBtn?.addEventListener("click", async (e) => {
95
95
  e.preventDefault();
96
96
  closeNav();
97
- document.getElementById('search')?.open();
97
+ await import("@pagefind/component-ui");
98
+ await import("@pagefind/component-ui/css");
99
+ document.getElementById("search")?.open();
98
100
  });
99
101
 
100
102
  // 汉堡菜单交互
@@ -140,17 +142,20 @@ const navs = navsConfig?.items ?? [];
140
142
  if (!navList || !btnLeft || !btnRight) return;
141
143
 
142
144
  function updateButtons() {
143
- const maxScrollLeft = Math.max(0, navList.scrollWidth - navList.clientWidth);
144
- const hasOverflow = maxScrollLeft > 4;
145
- const scrollLeft = Math.min(Math.max(navList.scrollLeft, 0), maxScrollLeft);
146
-
147
- if (navList.scrollLeft !== scrollLeft) {
148
- navList.scrollLeft = scrollLeft;
149
- }
150
-
151
- navList.toggleAttribute("data-overflow", hasOverflow);
152
- btnLeft.classList.toggle("visible", hasOverflow && scrollLeft > 4);
153
- btnRight.classList.toggle("visible", hasOverflow && scrollLeft < maxScrollLeft - 4);
145
+ const navWidth = navList.clientWidth;
146
+ const itemCount = navList.children.length;
147
+ const baseFontSize = Number.parseFloat(getComputedStyle(navList).fontSize) || 16;
148
+ const minFontSize = 12;
149
+ const gap = Number.parseFloat(getComputedStyle(navList).gap) || 0;
150
+ const estimatedWidth = [...navList.children].reduce(
151
+ (total, item) => total + item.getBoundingClientRect().width,
152
+ 0,
153
+ ) + gap * Math.max(0, itemCount - 1);
154
+ const scale = estimatedWidth > navWidth ? Math.max(minFontSize / baseFontSize, navWidth / estimatedWidth) : 1;
155
+ navList.style.setProperty("--nav-scale", String(scale));
156
+ navList.removeAttribute("data-overflow");
157
+ btnLeft.removeAttribute("data-visible");
158
+ btnRight.removeAttribute("data-visible");
154
159
  }
155
160
 
156
161
  btnLeft.addEventListener("click", () => {
@@ -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));
@@ -19,10 +19,10 @@ import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, get
19
19
 
20
20
  <Stalux title={siteTitle} description={siteDescription} fontSubsetId="home">
21
21
  <div class={styles.homeBody}>
22
- <h1 class={styles.homeTitle}>{siteTitle}</h1>
23
22
  <div class={styles.homeNavs}>
24
23
  <Navs />
25
24
  </div>
25
+ <h1 class={styles.homeTitle}>{siteTitle}</h1>
26
26
  <h2 class="sr-only" style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;">{t("ai.about")}</h2>
27
27
  <Author />
28
28
  <TextType />
@@ -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
  });
@@ -1,4 +1,10 @@
1
1
  /* 导航容器 — 相对定位,为滚动按钮提供锚点 */
2
+ nav {
3
+ width: 100%;
4
+ min-width: 0;
5
+ max-width: 100%;
6
+ }
7
+
2
8
  .navContainer {
3
9
  position: relative;
4
10
  width: 100%;
@@ -13,30 +19,22 @@
13
19
  gap: 2rem;
14
20
  align-items: center;
15
21
  justify-content: center;
22
+ transform: scale(var(--nav-scale, 1));
23
+ transform-origin: center;
16
24
  min-width: 0;
17
25
  max-width: 100%;
18
26
  box-sizing: border-box;
19
27
  transition: transform 0.3s ease;
20
28
  /* 当导航项总宽度超过视口时允许横向滚动,防止右侧项被裁剪 */
21
- overflow-x: auto;
22
- scroll-behavior: smooth;
23
- -webkit-overflow-scrolling: touch;
24
- /* 隐藏滚动条但保留滚动功能 */
25
- scrollbar-width: none;
26
- -ms-overflow-style: none;
27
- }
28
-
29
- .navList[data-overflow] {
30
- justify-content: flex-start;
31
- padding-inline: 44px;
32
- }
33
-
34
- .navList::-webkit-scrollbar {
35
- display: none;
29
+ flex-wrap: wrap;
30
+ width: 100%;
31
+ overflow: visible;
32
+ justify-content: center;
36
33
  }
37
34
 
38
35
  /* 滚动按钮 — 仅当内容溢出时显示 */
39
36
  .scrollBtn {
37
+ display: none;
40
38
  position: absolute;
41
39
  top: 50%;
42
40
  transform: translateY(-50%);
@@ -49,7 +47,6 @@
49
47
  backdrop-filter: blur(8px);
50
48
  color: var(--white-full);
51
49
  cursor: pointer;
52
- display: flex;
53
50
  align-items: center;
54
51
  justify-content: center;
55
52
  opacity: 0;
@@ -63,7 +60,7 @@
63
60
  background: var(--black-50p);
64
61
  }
65
62
 
66
- .scrollBtn.visible {
63
+ .scrollBtn[data-visible] {
67
64
  opacity: 1;
68
65
  pointer-events: auto;
69
66
  }
@@ -77,7 +74,7 @@
77
74
  }
78
75
 
79
76
  /* 导航容器 hover 时显示滚动按钮 */
80
- .navContainer:hover .scrollBtn.visible {
77
+ .navContainer:hover .scrollBtn[data-visible] {
81
78
  opacity: 1;
82
79
  }
83
80
 
@@ -183,20 +180,16 @@
183
180
  }
184
181
  }
185
182
 
186
- /* 较窄屏幕:隐藏文字,只显示图标 */
183
+ /* 中小屏幕:保留文字,通过横向滚动展示完整导航 */
187
184
  @media (max-width: 900px) {
188
- .navText {
189
- display: none;
190
- }
191
-
192
185
  .navItem {
193
- font-size: 1em;
194
- margin-right: 0.5em;
195
- margin-left: 0.5em;
186
+ font-size: 0.95em;
187
+ margin-right: 0.25em;
188
+ margin-left: 0.25em;
196
189
  }
197
190
 
198
191
  .navList {
199
- gap: 0.75rem;
192
+ gap: 0.5rem;
200
193
  }
201
194
  }
202
195
 
@@ -204,12 +197,19 @@
204
197
  @media (max-width: 600px) {
205
198
  .navToggle {
206
199
  display: block;
200
+ position: fixed;
201
+ top: 0.75rem;
202
+ left: 0.75rem;
207
203
  }
208
204
 
209
205
  .navContainer {
210
206
  overflow: visible;
211
207
  }
212
208
 
209
+ nav {
210
+ width: auto;
211
+ }
212
+
213
213
  .navList {
214
214
  position: fixed;
215
215
  top: 0;
@@ -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;
@@ -13,10 +14,21 @@
13
14
  display: inline-flex;
14
15
  align-items: center;
15
16
  justify-content: center;
17
+ text-align: center;
16
18
  border-radius: 50%;
17
19
  background-color: var(--white-15p);
18
20
  box-sizing: border-box;
19
21
  overflow: hidden;
20
22
  }
23
+ a,
24
+ .icon-container,
25
+ .custom-icon {
26
+ display: inline-flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ width: 100%;
30
+ height: 100%;
31
+ line-height: 1;
32
+ }
21
33
  }
22
34
  }
@@ -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);
@@ -4,6 +4,7 @@
4
4
  align-items: center;
5
5
  min-height: 100vh;
6
6
  padding-bottom: var(--space-3xl);
7
+ position: relative;
7
8
  }
8
9
 
9
10
  .homeTitle {
@@ -16,6 +17,11 @@
16
17
 
17
18
  .homeNavs {
18
19
  display: flex;
20
+ width: 100%;
21
+ order: -1;
22
+ min-width: 0;
23
+ max-width: 100%;
24
+ justify-content: center;
19
25
  }
20
26
 
21
27
  #showPostList {
@@ -44,6 +50,10 @@
44
50
  align-items: center;
45
51
  gap: 0.5em;
46
52
  }
53
+
54
+ .homeNavs :global(nav) {
55
+ width: 100%;
56
+ }
47
57
  #showPostList {
48
58
  margin-top: 2em;
49
59
  font-size: 1.1em;