@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.
- package/README.md +23 -3
- package/package.json +4 -2
- package/src/components/stalux/analytics/clarity.astro +1 -1
- package/src/components/stalux/analytics/google.astro +1 -1
- package/src/components/stalux/analytics/umami.astro +1 -1
- package/src/components/stalux/archives/archivesList.astro +27 -17
- package/src/components/stalux/categories/categoriesList.astro +2 -2
- package/src/components/stalux/common/verCount.astro +1 -1
- package/src/components/stalux/footer/FooterBadges.astro +14 -3
- package/src/components/stalux/layout/head.astro +8 -4
- package/src/components/stalux/links/linkList.astro +1 -1
- package/src/components/stalux/tags/tagsCloud.astro +22 -12
- package/src/components/stalux/words/wordList.astro +1 -1
- package/src/index.ts +67 -5
- package/src/layouts/Stalux.astro +0 -5
- package/src/scripts/analytics-runtime.ts +166 -0
- package/src/scripts/core-runtime.ts +0 -1
- package/src/scripts/date-time.ts +9 -5
- package/src/scripts/navigation.ts +48 -25
- package/src/scripts/runtime-counter.ts +2 -3
- package/src/scripts/search-dialog.ts +24 -0
- package/src/scripts/tags-cloud.ts +63 -29
- package/src/scripts/visible-clock.ts +24 -0
- package/src/scripts/waline.ts +1 -1
- package/src/styles/components/archives/archivesList.module.css +15 -87
- package/src/styles/components/categories/categoriesCard.module.css +3 -8
- package/src/styles/components/categories/categoriesList.module.css +7 -8
- package/src/styles/components/links/linkCard.module.css +0 -2
- package/src/styles/components/links/linkList.module.css +3 -3
- package/src/styles/components/posts/postAuthor.module.css +0 -1
- package/src/styles/components/posts/postCard.module.css +3 -12
- package/src/styles/components/tags/tagHeader.module.css +31 -31
- package/src/styles/components/tags/tagsCloud.module.css +34 -50
- package/src/utils/content-index.ts +1 -1
- package/src/utils/feed.ts +1 -1
- package/src/scripts/after-load.ts +0 -27
- package/src/scripts/clarity.ts +0 -48
- package/src/scripts/google-analytics.ts +0 -90
- package/src/scripts/umami.ts +0 -28
- 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
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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",
|
|
136
|
+
window.addEventListener("resize", scheduleResize, {
|
|
116
137
|
passive: true,
|
|
117
138
|
signal: controller.signal,
|
|
118
139
|
});
|
|
119
140
|
|
|
120
|
-
|
|
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 {
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
}
|
package/src/scripts/waline.ts
CHANGED
|
@@ -54,7 +54,7 @@ registerPageLifecycle("waline", () => {
|
|
|
54
54
|
(entries) => {
|
|
55
55
|
if (entries.some((entry) => entry.isIntersecting)) load();
|
|
56
56
|
},
|
|
57
|
-
{ rootMargin:
|
|
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
|
-
|
|
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
|
-
.
|
|
14
|
-
|
|
13
|
+
.categoriesList > li {
|
|
14
|
+
content-visibility: auto;
|
|
15
|
+
contain-intrinsic-block-size: auto 180px;
|
|
16
|
+
overflow-clip-margin: 30px;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
@
|
|
18
|
-
|
|
19
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
40
|
+
@media (480px <= width <= 767px) {
|
|
41
41
|
.list {
|
|
42
42
|
grid-template-columns: repeat(2, 1fr);
|
|
43
43
|
gap: 16px;
|
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
16
|
+
& .categoryIcon {
|
|
17
|
+
margin-right: 0.8rem;
|
|
18
|
+
color: var(--accent-80p);
|
|
19
|
+
}
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
21
|
+
& .categoryName {
|
|
22
|
+
color: var(--accent-90p);
|
|
23
|
+
font-weight: 500;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
27
|
+
& .pageDescription {
|
|
28
|
+
font-size: 1rem;
|
|
29
|
+
color: var(--white-70p);
|
|
30
|
+
margin-bottom: 0.5rem;
|
|
31
|
+
}
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
40
|
+
&:hover {
|
|
41
|
+
color: var(--accent-90p);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
44
|
}
|