@xingwangzhe/stalux 1.29.5 → 1.29.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.
Files changed (40) hide show
  1. package/README.md +23 -3
  2. package/package.json +4 -2
  3. package/src/components/stalux/analytics/clarity.astro +1 -1
  4. package/src/components/stalux/analytics/google.astro +1 -1
  5. package/src/components/stalux/analytics/umami.astro +1 -1
  6. package/src/components/stalux/archives/archivesList.astro +27 -17
  7. package/src/components/stalux/categories/categoriesList.astro +2 -2
  8. package/src/components/stalux/common/verCount.astro +1 -1
  9. package/src/components/stalux/footer/FooterBadges.astro +14 -3
  10. package/src/components/stalux/layout/head.astro +8 -4
  11. package/src/components/stalux/links/linkList.astro +1 -1
  12. package/src/components/stalux/tags/tagsCloud.astro +22 -12
  13. package/src/components/stalux/words/wordList.astro +1 -1
  14. package/src/index.ts +67 -5
  15. package/src/layouts/Stalux.astro +0 -5
  16. package/src/scripts/analytics-runtime.ts +166 -0
  17. package/src/scripts/core-runtime.ts +0 -1
  18. package/src/scripts/date-time.ts +9 -5
  19. package/src/scripts/navigation.ts +48 -25
  20. package/src/scripts/runtime-counter.ts +2 -3
  21. package/src/scripts/search-dialog.ts +24 -0
  22. package/src/scripts/tags-cloud.ts +63 -29
  23. package/src/scripts/visible-clock.ts +24 -0
  24. package/src/scripts/waline.ts +1 -1
  25. package/src/styles/components/archives/archivesList.module.css +15 -87
  26. package/src/styles/components/categories/categoriesCard.module.css +3 -8
  27. package/src/styles/components/categories/categoriesList.module.css +7 -8
  28. package/src/styles/components/links/linkCard.module.css +0 -2
  29. package/src/styles/components/links/linkList.module.css +3 -3
  30. package/src/styles/components/posts/postAuthor.module.css +0 -1
  31. package/src/styles/components/posts/postCard.module.css +3 -12
  32. package/src/styles/components/tags/tagHeader.module.css +31 -31
  33. package/src/styles/components/tags/tagsCloud.module.css +34 -50
  34. package/src/utils/content-index.ts +1 -1
  35. package/src/utils/feed.ts +1 -1
  36. package/src/scripts/after-load.ts +0 -27
  37. package/src/scripts/clarity.ts +0 -48
  38. package/src/scripts/google-analytics.ts +0 -90
  39. package/src/scripts/umami.ts +0 -28
  40. package/src/scripts/vercount.ts +0 -38
@@ -1,7 +1,7 @@
1
1
  import { createClientLogger } from "./logger";
2
2
 
3
3
  import { registerPageLifecycle } from "./page-runtime";
4
- import { upgradeAndOpenSearchDialog } from "./search-dialog";
4
+ import { ensureSearchStyles, upgradeAndOpenSearchDialog } from "./search-dialog";
5
5
 
6
6
  const logger = createClientLogger("navigation");
7
7
 
@@ -12,7 +12,12 @@ interface SearchDialog extends HTMLElement {
12
12
 
13
13
  async function openSearchDialog(): Promise<void> {
14
14
  logger.debug("loading search UI");
15
- await import("@pagefind/component-ui");
15
+ await Promise.all([
16
+ import("@pagefind/component-ui"),
17
+ ensureSearchStyles(
18
+ document.querySelector<HTMLLinkElement>("link[data-stalux-search-style]"),
19
+ ),
20
+ ]);
16
21
  const dialog = document.querySelector<SearchDialog>("pagefind-modal#search");
17
22
  await upgradeAndOpenSearchDialog(
18
23
  {
@@ -35,6 +40,8 @@ registerPageLifecycle("navigation", () => {
35
40
  const listenerOptions = { signal: controller.signal };
36
41
  let frameId = 0;
37
42
  let lastScale = "1";
43
+ let needsScale = true;
44
+ const mobileNav = window.matchMedia("(max-width: 600px)");
38
45
 
39
46
  const closeNav = () => {
40
47
  navList.removeAttribute("data-state");
@@ -46,37 +53,51 @@ registerPageLifecycle("navigation", () => {
46
53
  navOverlay?.setAttribute("data-state", "show");
47
54
  document.body.style.overflow = "hidden";
48
55
  };
49
- const updateScrollButtons = () => {
50
- const hasOverflow = navList.scrollWidth > navList.clientWidth;
51
- const atStart = navList.scrollLeft <= 1;
52
- const atEnd = navList.scrollLeft + navList.clientWidth >= navList.scrollWidth - 1;
53
- buttonLeft.toggleAttribute("data-visible", hasOverflow && !atStart);
54
- buttonRight.toggleAttribute("data-visible", hasOverflow && !atEnd);
55
- };
56
56
  const updateButtons = () => {
57
57
  frameId = 0;
58
- const baseFontSize = Number.parseFloat(getComputedStyle(navList).fontSize) || 16;
59
- const gap = Number.parseFloat(getComputedStyle(navList).gap) || 0;
60
- const estimatedWidth =
61
- [...navList.children].reduce(
62
- (total, item) => total + item.getBoundingClientRect().width,
63
- 0,
64
- ) +
65
- gap * Math.max(0, navList.children.length - 1);
66
- const scale =
67
- estimatedWidth > navList.clientWidth
68
- ? Math.max(12 / baseFontSize, navList.clientWidth / estimatedWidth)
69
- : 1;
70
- const nextScale = String(scale);
58
+ // Read layout before writing styles or attributes. Scrolling does not
59
+ // change item sizes, so only resize events need to recalculate scale.
60
+ const { clientWidth, scrollWidth, scrollLeft } = navList;
61
+ let nextScale = lastScale;
62
+ if (needsScale) {
63
+ needsScale = false;
64
+ if (mobileNav.matches) {
65
+ nextScale = "1";
66
+ } else {
67
+ const style = getComputedStyle(navList);
68
+ const baseFontSize = Number.parseFloat(style.fontSize) || 16;
69
+ const gap = Number.parseFloat(style.gap) || 0;
70
+ let estimatedWidth = gap * Math.max(0, navList.children.length - 1);
71
+ for (const item of navList.children) {
72
+ // offsetWidth excludes the existing transform, avoiding
73
+ // scale feedback during repeated resize notifications.
74
+ if (item instanceof HTMLElement) estimatedWidth += item.offsetWidth;
75
+ }
76
+ nextScale = String(
77
+ estimatedWidth > clientWidth
78
+ ? Math.max(12 / baseFontSize, clientWidth / estimatedWidth)
79
+ : 1,
80
+ );
81
+ }
82
+ }
71
83
  if (nextScale !== lastScale) {
72
84
  navList.style.setProperty("--nav-scale", nextScale);
73
85
  lastScale = nextScale;
74
86
  }
75
- updateScrollButtons();
87
+ const hasOverflow = scrollWidth > clientWidth;
88
+ buttonLeft.toggleAttribute("data-visible", hasOverflow && scrollLeft > 1);
89
+ buttonRight.toggleAttribute(
90
+ "data-visible",
91
+ hasOverflow && scrollLeft + clientWidth < scrollWidth - 1,
92
+ );
76
93
  };
77
94
  const scheduleUpdate = () => {
78
95
  if (!frameId) frameId = requestAnimationFrame(updateButtons);
79
96
  };
97
+ const scheduleResize = () => {
98
+ needsScale = true;
99
+ scheduleUpdate();
100
+ };
80
101
 
81
102
  document.querySelector("[data-ref='stalux-search-btn']")?.addEventListener(
82
103
  "click",
@@ -112,12 +133,14 @@ registerPageLifecycle("navigation", () => {
112
133
  passive: true,
113
134
  signal: controller.signal,
114
135
  });
115
- window.addEventListener("resize", scheduleUpdate, {
136
+ window.addEventListener("resize", scheduleResize, {
116
137
  passive: true,
117
138
  signal: controller.signal,
118
139
  });
119
140
 
120
- const resizeObserver = new ResizeObserver(scheduleUpdate);
141
+ document.fonts.addEventListener("loadingdone", scheduleResize, listenerOptions);
142
+
143
+ const resizeObserver = new ResizeObserver(scheduleResize);
121
144
  resizeObserver.observe(navList);
122
145
  if (navList.parentElement) resizeObserver.observe(navList.parentElement);
123
146
  scheduleUpdate();
@@ -1,4 +1,5 @@
1
1
  import { registerPageLifecycle } from "./page-runtime";
2
+ import { observeVisibleClock } from "./visible-clock";
2
3
 
3
4
  registerPageLifecycle("runtime-counter", () => {
4
5
  const container = document.querySelector<HTMLElement>("[data-stalux-buildtime]");
@@ -17,7 +18,5 @@ registerPageLifecycle("runtime-counter", () => {
17
18
  .replace("{minutes}", String(Math.floor((total % 3600) / 60)))
18
19
  .replace("{seconds}", String(total % 60));
19
20
  };
20
- update();
21
- const timer = window.setInterval(update, 1000);
22
- return () => window.clearInterval(timer);
21
+ return observeVisibleClock(counter, update);
23
22
  });
@@ -7,6 +7,30 @@ export interface CustomElementRegistryLike<ElementType> {
7
7
  upgrade(element: ElementType): void;
8
8
  }
9
9
 
10
+ /** Promote the deferred stylesheet before showing a search dialog on a cold visit. */
11
+ export async function ensureSearchStyles(link: HTMLLinkElement | null): Promise<void> {
12
+ if (!link) return;
13
+ link.rel = "stylesheet";
14
+ link.media = "all";
15
+ if (link.sheet) return;
16
+ await new Promise<void>((resolve, reject) => {
17
+ const cleanup = () => {
18
+ link.removeEventListener("load", loaded);
19
+ link.removeEventListener("error", failed);
20
+ };
21
+ const loaded = () => {
22
+ cleanup();
23
+ resolve();
24
+ };
25
+ const failed = () => {
26
+ cleanup();
27
+ reject(new Error("Search stylesheet failed to load"));
28
+ };
29
+ link.addEventListener("load", loaded, { once: true });
30
+ link.addEventListener("error", failed, { once: true });
31
+ });
32
+ }
33
+
10
34
  export async function upgradeAndOpenSearchDialog<ElementType extends SearchDialogLike>(
11
35
  registry: CustomElementRegistryLike<ElementType>,
12
36
  dialog: ElementType | null,
@@ -1,4 +1,4 @@
1
- import { TagCloud } from "@xingwangzhe/tags-cloud";
1
+ import { navigate } from "astro:transitions/client";
2
2
  import { createClientLogger } from "./logger";
3
3
 
4
4
  import { registerPageLifecycle } from "./page-runtime";
@@ -36,35 +36,69 @@ function parseTags(value: string | undefined): LinkTag[] {
36
36
  }
37
37
 
38
38
  registerPageLifecycle("tags-cloud", () => {
39
- const mobile = window.matchMedia("(max-width: 768px)").matches;
40
- if (mobile) {
41
- logger.debug("mobile layout; cloud skipped");
42
- return;
43
- }
44
-
45
39
  const container = document.getElementById("tags-canvas");
46
40
  if (!container) return;
47
- const rawTags = parseTags(container.dataset.tags);
48
- if (rawTags.length === 0) return;
49
- const tags = rawTags.map((tag) => ({
50
- ...tag,
51
- onClick: () => {
52
- window.location.href = tag.url;
41
+ const desktop = window.matchMedia("(min-width: 769px)");
42
+ const controller = new AbortController();
43
+ let cloud: import("@xingwangzhe/tags-cloud").TagCloud | undefined;
44
+ let visible = false;
45
+ let loading = false;
46
+ let disposed = false;
47
+ const active = () => !disposed && desktop.matches && visible && !document.hidden;
48
+ const update = async () => {
49
+ if (!active()) {
50
+ // destroy also cancels the library's rAF; pause() only skips drawing.
51
+ cloud?.destroy();
52
+ cloud = undefined;
53
+ return;
54
+ }
55
+ if (cloud || loading) return;
56
+ loading = true;
57
+ try {
58
+ const { TagCloud } = await import("@xingwangzhe/tags-cloud");
59
+ if (!active()) return;
60
+ const tags = parseTags(container.dataset.tags).map((tag) => ({
61
+ ...tag,
62
+ onClick: () => {
63
+ void navigate(tag.url);
64
+ },
65
+ }));
66
+ if (!tags.length) return;
67
+ const width = container.clientWidth;
68
+ const baseRadius =
69
+ window.innerWidth < 1_024 ? 330 : window.innerWidth < 1_440 ? 420 : 480;
70
+ const radius = Math.min(
71
+ Math.round(baseRadius * Math.sqrt(Math.max(tags.length, 100) / 100)),
72
+ Math.round(width * 0.6),
73
+ );
74
+ cloud = new TagCloud(container, {
75
+ tags,
76
+ radius,
77
+ spinY: 0.15,
78
+ fontSize: 16,
79
+ color: "#ffffff",
80
+ fontFamily: "system-ui, -apple-system, sans-serif",
81
+ });
82
+ } catch (error) {
83
+ logger.warn(`tag cloud loading failed: ${String(error)}`);
84
+ } finally {
85
+ loading = false;
86
+ }
87
+ };
88
+ const observer = new IntersectionObserver(
89
+ (entries) => {
90
+ visible = entries.some((entry) => entry.isIntersecting);
91
+ void update();
53
92
  },
54
- }));
55
- const width = container.getBoundingClientRect().width;
56
- const baseRadius = window.innerWidth < 1_024 ? 330 : window.innerWidth < 1_440 ? 420 : 480;
57
- const densityScale = Math.sqrt(Math.max(tags.length, 100) / 100);
58
- const radius = Math.min(Math.round(baseRadius * densityScale), Math.round(width * 0.6));
59
- container.style.height = `${Math.round(radius * 2)}px`;
60
- const cloud = new TagCloud(container, {
61
- tags,
62
- radius,
63
- spinY: 0.15,
64
- fontSize: 16,
65
- color: "#ffffff",
66
- fontFamily: "system-ui, -apple-system, sans-serif",
67
- });
68
- logger.debug(`cloud initialized; tags=${tags.length}`);
69
- return () => cloud.destroy();
93
+ { rootMargin: "200px 0px" },
94
+ );
95
+ observer.observe(container);
96
+ desktop.addEventListener("change", update, { signal: controller.signal });
97
+ document.addEventListener("visibilitychange", update, { signal: controller.signal });
98
+ return () => {
99
+ disposed = true;
100
+ controller.abort();
101
+ observer.disconnect();
102
+ cloud?.destroy();
103
+ };
70
104
  });
@@ -0,0 +1,24 @@
1
+ /** Run a wall-clock update only while its element and document are visible. */
2
+ export function observeVisibleClock(element: Element, update: () => void): () => void {
3
+ const controller = new AbortController();
4
+ let visible = false;
5
+ let timer: ReturnType<typeof setInterval> | undefined;
6
+ const sync = () => {
7
+ if (timer !== undefined) clearInterval(timer);
8
+ timer = undefined;
9
+ if (!visible || document.hidden) return;
10
+ update();
11
+ timer = setInterval(update, 1_000);
12
+ };
13
+ const observer = new IntersectionObserver((entries) => {
14
+ visible = entries.some((entry) => entry.isIntersecting);
15
+ sync();
16
+ });
17
+ observer.observe(element);
18
+ document.addEventListener("visibilitychange", sync, { signal: controller.signal });
19
+ return () => {
20
+ controller.abort();
21
+ observer.disconnect();
22
+ if (timer !== undefined) clearInterval(timer);
23
+ };
24
+ }
@@ -54,7 +54,7 @@ registerPageLifecycle("waline", () => {
54
54
  (entries) => {
55
55
  if (entries.some((entry) => entry.isIntersecting)) load();
56
56
  },
57
- { rootMargin: "1000px 0px" },
57
+ { rootMargin: `${Math.min(400, Math.round(window.innerHeight * 0.5))}px 0px` },
58
58
  );
59
59
  observer.observe(container);
60
60
  } else {
@@ -9,7 +9,6 @@
9
9
  --tl-line: var(--radius-sm);
10
10
  --tl-pad: var(--space-xl);
11
11
  padding-left: var(--tl-pad);
12
- animation: contentReveal 220ms ease 360ms both;
13
12
  }
14
13
 
15
14
  .timelineContainer::before {
@@ -67,6 +66,10 @@
67
66
  }
68
67
 
69
68
  .monthSection {
69
+ content-visibility: auto;
70
+ contain-intrinsic-block-size: auto var(--archive-estimate, 600px);
71
+ margin-left: calc(-1 * var(--tl-pad));
72
+ padding-left: var(--tl-pad);
70
73
  margin-bottom: 2rem;
71
74
  position: relative;
72
75
  }
@@ -79,72 +82,15 @@
79
82
  background-color: var(--black-10p);
80
83
  border-radius: 0.5rem;
81
84
  width: 100%;
82
- animation: stalux-content-reveal 220ms ease 360ms both;
83
- }
84
-
85
- /* 懒加载指示器样式 */
86
- .lazyloadObserver {
87
- display: flex;
88
- flex-direction: column;
89
- align-items: center;
90
- padding: 2rem 0;
91
- opacity: 0.8;
92
- transition: opacity var(--transition-normal) ease;
93
- }
94
-
95
- .lazyloadObserver:hover {
96
- opacity: 1;
97
- }
98
-
99
- .loadingSpinner {
100
- display: flex;
101
- justify-content: center;
102
- margin-bottom: 0.5rem;
103
- }
104
-
105
- .loadingSpinner .dot {
106
- display: inline-block;
107
- width: 10px;
108
- height: 10px;
109
- margin: 0 3px;
110
- border-radius: 50%;
111
- background-color: var(--accent-80p);
112
- animation: dotPulse 1.4s infinite ease-in-out both;
113
- }
114
-
115
- .loadingSpinner .dot:nth-child(1) {
116
- animation-delay: -0.32s;
117
- }
118
-
119
- .loadingSpinner .dot:nth-child(2) {
120
- animation-delay: -0.16s;
121
- }
122
-
123
- .loadingSpinner .dot:nth-child(3) {
124
- animation-delay: 0s;
125
- }
126
-
127
- @keyframes dotPulse {
128
- 0%,
129
- 80%,
130
- 100% {
131
- transform: scale(0.3);
132
- }
133
- 40% {
134
- transform: scale(1);
135
- }
136
- }
137
-
138
- .loadingText {
139
- font-size: 0.9rem;
140
- color: var(--white-70p);
141
85
  }
142
86
 
143
87
  /* 文章列表和项目样式(静态 DOM)*/
144
88
  .postList {
89
+ content-visibility: auto;
90
+ contain-intrinsic-block-size: auto var(--archive-group-estimate, 576px);
145
91
  list-style: none;
146
- padding: 0;
147
- margin: 0;
92
+ padding: 0 0 0 var(--tl-pad);
93
+ margin: 0 0 0 calc(-1 * var(--tl-pad));
148
94
  }
149
95
 
150
96
  .postItem {
@@ -157,6 +103,13 @@
157
103
  align-items: center;
158
104
  }
159
105
 
106
+ @supports not (content-visibility: auto) {
107
+ .monthSection,
108
+ .postList {
109
+ contain-intrinsic-block-size: auto;
110
+ }
111
+ }
112
+
160
113
  .postItem::before {
161
114
  content: "";
162
115
  position: absolute;
@@ -202,10 +155,6 @@
202
155
  z-index: 1;
203
156
  }
204
157
 
205
- .monthBlock {
206
- margin: 0.5rem 0;
207
- }
208
-
209
158
  @media (max-width: 768px) {
210
159
  .archivesContainer {
211
160
  padding: 1.5rem 0.5rem 2rem 1.5rem;
@@ -215,14 +164,6 @@
215
164
  margin-bottom: 2rem;
216
165
  }
217
166
 
218
- .pageHeader .page-title {
219
- font-size: 2rem;
220
- }
221
-
222
- .pageHeader .page-description {
223
- font-size: 1rem;
224
- }
225
-
226
167
  .timelineContainer {
227
168
  --tl-pad: 1.5rem;
228
169
  }
@@ -246,10 +187,6 @@
246
187
  padding: 1.5rem 0.5rem 2rem 1.2rem;
247
188
  }
248
189
 
249
- .pageHeader .page-title {
250
- font-size: 1.8rem;
251
- }
252
-
253
190
  .timelineContainer {
254
191
  --tl-pad: 1.2rem;
255
192
  --tl-line: 3px;
@@ -264,12 +201,3 @@
264
201
  font-size: 1.1rem;
265
202
  }
266
203
  }
267
-
268
- @keyframes contentReveal {
269
- from {
270
- opacity: 0;
271
- }
272
- to {
273
- opacity: 1;
274
- }
275
- }
@@ -10,8 +10,7 @@
10
10
  box-shadow: var(--card-box-shadow);
11
11
  transition:
12
12
  transform 220ms ease,
13
- background-color 220ms ease,
14
- box-shadow 220ms ease;
13
+ background-color 220ms ease;
15
14
  overflow: hidden;
16
15
  height: fit-content;
17
16
  }
@@ -19,10 +18,7 @@
19
18
  .categoryCard::before {
20
19
  content: "";
21
20
  position: absolute;
22
- top: 0;
23
- left: 0;
24
- width: 100%;
25
- height: 100%;
21
+ inset: 0;
26
22
  background: linear-gradient(
27
23
  120deg,
28
24
  rgba(255, 255, 255, 0),
@@ -76,8 +72,7 @@
76
72
  transition:
77
73
  transform 240ms ease,
78
74
  background-color 240ms ease,
79
- color 240ms ease,
80
- box-shadow 240ms ease;
75
+ color 240ms ease;
81
76
  z-index: 2;
82
77
  transform-origin: center center;
83
78
  background: var(--black-15p);
@@ -10,15 +10,14 @@
10
10
  list-style: none;
11
11
  }
12
12
 
13
- .item {
14
- animation: contentReveal 220ms ease 360ms both;
13
+ .categoriesList > li {
14
+ content-visibility: auto;
15
+ contain-intrinsic-block-size: auto 180px;
16
+ overflow-clip-margin: 30px;
15
17
  }
16
18
 
17
- @keyframes contentReveal {
18
- from {
19
- opacity: 0;
20
- }
21
- to {
22
- opacity: 1;
19
+ @supports not (content-visibility: auto) {
20
+ .categoriesList > li {
21
+ contain-intrinsic-block-size: auto;
23
22
  }
24
23
  }
@@ -13,7 +13,6 @@
13
13
  overflow: visible;
14
14
  transition:
15
15
  transform 220ms ease,
16
- box-shadow 220ms ease,
17
16
  background-color 220ms ease,
18
17
  border-color 220ms ease;
19
18
  box-shadow: var(--card-box-shadow);
@@ -40,7 +39,6 @@
40
39
  transform-origin: center;
41
40
  transition:
42
41
  transform 220ms ease,
43
- box-shadow 220ms ease,
44
42
  border-color 220ms ease;
45
43
  }
46
44
 
@@ -23,21 +23,21 @@
23
23
  }
24
24
 
25
25
  /* 中等屏幕:4个卡片一行 */
26
- @media (max-width: 1399px) and (min-width: 1100px) {
26
+ @media (1100px <= width <= 1399px) {
27
27
  .list {
28
28
  grid-template-columns: repeat(4, 1fr);
29
29
  }
30
30
  }
31
31
 
32
32
  /* 小屏幕:3个卡片一行 */
33
- @media (max-width: 1099px) and (min-width: 768px) {
33
+ @media (768px <= width <= 1099px) {
34
34
  .list {
35
35
  grid-template-columns: repeat(3, 1fr);
36
36
  }
37
37
  }
38
38
 
39
39
  /* 平板:2个卡片一行 */
40
- @media (max-width: 767px) and (min-width: 480px) {
40
+ @media (480px <= width <= 767px) {
41
41
  .list {
42
42
  grid-template-columns: repeat(2, 1fr);
43
43
  gap: 16px;
@@ -17,7 +17,6 @@
17
17
  border-radius: var(--radius-lg);
18
18
  padding: var(--space-md);
19
19
  box-shadow: var(--card-box-shadow);
20
- transition: all var(--transition-normal) ease;
21
20
  }
22
21
 
23
22
  .authorBio {
@@ -37,9 +37,7 @@
37
37
  border-radius: var(--radius-lg);
38
38
  box-shadow: var(--card-box-shadow);
39
39
  padding: var(--space-lg);
40
- transition:
41
- transform 220ms cubic-bezier(0.22, 0.72, 0.22, 1),
42
- box-shadow 220ms ease;
40
+ transition: transform 220ms cubic-bezier(0.22, 0.72, 0.22, 1);
43
41
  cursor: pointer;
44
42
  display: flex;
45
43
  flex-direction: column;
@@ -66,8 +64,7 @@
66
64
  transition: transform var(--transition-fast) ease;
67
65
  }
68
66
 
69
- .postContainer:hover .postCover img,
70
- .postContainer:focus-within .postCover img {
67
+ .postContainer:is(:hover, :focus-within) .postCover img {
71
68
  transform: scale(1.05);
72
69
  }
73
70
 
@@ -79,13 +76,7 @@
79
76
  }
80
77
 
81
78
  /* 卡片悬停和焦点效果 */
82
- .postContainer {
83
- transition:
84
- transform 220ms cubic-bezier(0.22, 0.72, 0.22, 1),
85
- box-shadow 220ms ease;
86
- }
87
- .postContainer:hover,
88
- .postContainer:focus-within {
79
+ .postContainer:is(:hover, :focus-within) {
89
80
  transform: translateY(-3px);
90
81
  box-shadow: var(--card-box-shadow-hover);
91
82
  }
@@ -1,44 +1,44 @@
1
- /* 页面标题部分 */
1
+ /* Keep header selectors local with native nesting. */
2
2
  .pageHeader {
3
3
  margin: 3em;
4
4
  margin-bottom: 2.5rem;
5
5
  position: relative;
6
6
  padding-bottom: 1rem;
7
7
  border-bottom: 1px solid var(--white-10p);
8
- }
9
8
 
10
- .pageHeader .pageTitle {
11
- font-size: 1.8rem;
12
- margin-bottom: 0.5rem;
13
- display: flex;
14
- align-items: center;
15
- color: var(--white-full);
16
- }
9
+ & .pageTitle {
10
+ font-size: 1.8rem;
11
+ margin-bottom: 0.5rem;
12
+ display: flex;
13
+ align-items: center;
14
+ color: var(--white-full);
17
15
 
18
- .pageHeader .pageTitle .categoryIcon {
19
- margin-right: 0.8rem;
20
- color: var(--accent-80p);
21
- }
16
+ & .categoryIcon {
17
+ margin-right: 0.8rem;
18
+ color: var(--accent-80p);
19
+ }
22
20
 
23
- .pageHeader .pageTitle .categoryName {
24
- color: var(--accent-90p);
25
- font-weight: 500;
26
- }
21
+ & .categoryName {
22
+ color: var(--accent-90p);
23
+ font-weight: 500;
24
+ }
25
+ }
27
26
 
28
- .pageHeader .pageDescription {
29
- font-size: 1rem;
30
- color: var(--white-70p);
31
- margin-bottom: 0.5rem;
32
- }
27
+ & .pageDescription {
28
+ font-size: 1rem;
29
+ color: var(--white-70p);
30
+ margin-bottom: 0.5rem;
31
+ }
33
32
 
34
- .pageHeader .backLink {
35
- font-size: 0.9rem;
36
- color: var(--white-60p);
37
- text-decoration: none;
38
- display: inline-block;
39
- transition: all var(--transition-fast) ease;
40
- }
33
+ & .backLink {
34
+ font-size: 0.9rem;
35
+ color: var(--white-60p);
36
+ text-decoration: none;
37
+ display: inline-block;
38
+ transition: color var(--transition-fast) ease;
41
39
 
42
- .pageHeader .backLink:hover {
43
- color: var(--accent-90p);
40
+ &:hover {
41
+ color: var(--accent-90p);
42
+ }
43
+ }
44
44
  }