@astro-minimax/core 0.1.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.
- package/README.md +29 -0
- package/package.json +41 -0
- package/src/assets/icons/IconArchive.svg +1 -0
- package/src/assets/icons/IconArrowLeft.svg +1 -0
- package/src/assets/icons/IconArrowNarrowUp.svg +1 -0
- package/src/assets/icons/IconArrowRight.svg +1 -0
- package/src/assets/icons/IconArticle.svg +1 -0
- package/src/assets/icons/IconBrandX.svg +1 -0
- package/src/assets/icons/IconCalendar.svg +1 -0
- package/src/assets/icons/IconChevronLeft.svg +1 -0
- package/src/assets/icons/IconChevronRight.svg +1 -0
- package/src/assets/icons/IconEdit.svg +1 -0
- package/src/assets/icons/IconFacebook.svg +1 -0
- package/src/assets/icons/IconGitHub.svg +1 -0
- package/src/assets/icons/IconHash.svg +1 -0
- package/src/assets/icons/IconHome.svg +1 -0
- package/src/assets/icons/IconLinkedin.svg +1 -0
- package/src/assets/icons/IconMail.svg +1 -0
- package/src/assets/icons/IconMenuDeep.svg +1 -0
- package/src/assets/icons/IconMoon.svg +1 -0
- package/src/assets/icons/IconPinterest.svg +1 -0
- package/src/assets/icons/IconProject.svg +1 -0
- package/src/assets/icons/IconRss.svg +1 -0
- package/src/assets/icons/IconSearch.svg +1 -0
- package/src/assets/icons/IconSeries.svg +1 -0
- package/src/assets/icons/IconSunHigh.svg +1 -0
- package/src/assets/icons/IconTag.svg +1 -0
- package/src/assets/icons/IconTelegram.svg +1 -0
- package/src/assets/icons/IconUser.svg +1 -0
- package/src/assets/icons/IconWhatsapp.svg +1 -0
- package/src/assets/icons/IconX.svg +1 -0
- package/src/components/ai/AIChatWidget.astro +377 -0
- package/src/components/blog/Comments.astro +527 -0
- package/src/components/blog/Copyright.astro +152 -0
- package/src/components/blog/EditPost.astro +59 -0
- package/src/components/blog/FloatingTOC.astro +260 -0
- package/src/components/blog/InlineTOC.astro +223 -0
- package/src/components/blog/PostActions.astro +306 -0
- package/src/components/blog/RelatedPosts.astro +60 -0
- package/src/components/blog/SeriesNav.astro +176 -0
- package/src/components/blog/ShareLinks.astro +26 -0
- package/src/components/nav/BackButton.astro +37 -0
- package/src/components/nav/BackToTopButton.astro +223 -0
- package/src/components/nav/Breadcrumb.astro +57 -0
- package/src/components/nav/FloatingActions.astro +206 -0
- package/src/components/nav/Footer.astro +107 -0
- package/src/components/nav/Header.astro +252 -0
- package/src/components/nav/Pagination.astro +45 -0
- package/src/components/social/Socials.astro +19 -0
- package/src/components/social/Sponsors.astro +34 -0
- package/src/components/social/Sponsorship.astro +44 -0
- package/src/components/ui/Alert.astro +28 -0
- package/src/components/ui/Card.astro +206 -0
- package/src/components/ui/Collapse.astro +82 -0
- package/src/components/ui/ColorPreview.astro +29 -0
- package/src/components/ui/Datetime.astro +61 -0
- package/src/components/ui/GithubCard.astro +191 -0
- package/src/components/ui/LinkButton.astro +21 -0
- package/src/components/ui/Tag.astro +37 -0
- package/src/components/ui/TagCloud.astro +69 -0
- package/src/components/ui/Timeline.astro +39 -0
- package/src/layouts/AboutLayout.astro +24 -0
- package/src/layouts/Layout.astro +329 -0
- package/src/layouts/Main.astro +42 -0
- package/src/layouts/PostDetails.astro +445 -0
- package/src/plugins/rehype-autolink-headings.ts +46 -0
- package/src/plugins/rehype-external-links.ts +35 -0
- package/src/plugins/rehype-table-scroll.ts +35 -0
- package/src/plugins/remark-add-zoomable.ts +28 -0
- package/src/plugins/remark-reading-time.ts +18 -0
- package/src/plugins/shiki-transformers.ts +212 -0
- package/src/scripts/lightbox.ts +63 -0
- package/src/scripts/reading-position.ts +56 -0
- package/src/scripts/theme-utils.ts +19 -0
- package/src/scripts/theme.ts +179 -0
- package/src/scripts/web-vitals.ts +96 -0
- package/src/styles/code-blocks.css +194 -0
- package/src/styles/components.css +252 -0
- package/src/styles/global.css +403 -0
- package/src/styles/typography.css +149 -0
- package/src/types.ts +89 -0
- package/src/utils/generateOgImages.ts +38 -0
- package/src/utils/getCategoryPath.ts +23 -0
- package/src/utils/getPath.ts +52 -0
- package/src/utils/getPostsByCategory.ts +17 -0
- package/src/utils/getPostsByGroupCondition.ts +25 -0
- package/src/utils/getPostsByLang.ts +27 -0
- package/src/utils/getPostsByTag.ts +10 -0
- package/src/utils/getReadingTime.ts +33 -0
- package/src/utils/getRelatedPosts.ts +59 -0
- package/src/utils/getSeriesData.ts +57 -0
- package/src/utils/getSortedPosts.ts +18 -0
- package/src/utils/getTagsWithCount.ts +38 -0
- package/src/utils/getUniqueCategories.ts +81 -0
- package/src/utils/getUniqueTags.ts +23 -0
- package/src/utils/i18n.ts +249 -0
- package/src/utils/loadGoogleFont.ts +38 -0
- package/src/utils/og-templates/post.js +229 -0
- package/src/utils/og-templates/site.js +128 -0
- package/src/utils/pathUtils.ts +17 -0
- package/src/utils/postFilter.ts +11 -0
- package/src/utils/slugify.ts +23 -0
- package/src/utils/toc.ts +27 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
---
|
|
2
|
+
import IconArrowNarrowUp from "../../assets/icons/IconArrowNarrowUp.svg";
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<!-- There can't be a filter on parent element, or it will break `fixed` -->
|
|
6
|
+
<div class="back-to-top-wrapper hidden lg:block">
|
|
7
|
+
<div
|
|
8
|
+
id="back-to-top-btn"
|
|
9
|
+
class="back-to-top-btn hide flex items-center overflow-hidden rounded-2xl transition"
|
|
10
|
+
onclick="backToTop()"
|
|
11
|
+
>
|
|
12
|
+
<button aria-label="Back to Top" class="h-[3rem] w-[3rem] rounded-full">
|
|
13
|
+
<IconArrowNarrowUp class="mx-auto" />
|
|
14
|
+
</button>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<style lang="stylus">
|
|
19
|
+
.back-to-top-wrapper
|
|
20
|
+
width: 3rem
|
|
21
|
+
height: 3rem
|
|
22
|
+
position: absolute
|
|
23
|
+
right: 0
|
|
24
|
+
top: 0
|
|
25
|
+
pointer-events: none
|
|
26
|
+
|
|
27
|
+
.back-to-top-btn
|
|
28
|
+
color: #394E6A
|
|
29
|
+
font-size: 1.75rem
|
|
30
|
+
font-weight: bold
|
|
31
|
+
position: fixed
|
|
32
|
+
bottom: 2rem
|
|
33
|
+
right: 2rem
|
|
34
|
+
opacity: 1
|
|
35
|
+
cursor: pointer
|
|
36
|
+
pointer-events: auto
|
|
37
|
+
z-index: 1000
|
|
38
|
+
transition: all 0.3s ease
|
|
39
|
+
border-radius: 50%
|
|
40
|
+
background-color: #ffffff
|
|
41
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15)
|
|
42
|
+
border: none
|
|
43
|
+
width: 3rem
|
|
44
|
+
height: 3rem
|
|
45
|
+
svg
|
|
46
|
+
font-size: 1.25rem
|
|
47
|
+
&.hide
|
|
48
|
+
transform: scale(0.9)
|
|
49
|
+
opacity: 0
|
|
50
|
+
pointer-events: none
|
|
51
|
+
&:active
|
|
52
|
+
transform: scale(0.9)
|
|
53
|
+
&:hover
|
|
54
|
+
transform: scale(1.05)
|
|
55
|
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2)
|
|
56
|
+
background-color: #D2D7DE
|
|
57
|
+
border-color: none
|
|
58
|
+
|
|
59
|
+
/* 暗色主题样式 */
|
|
60
|
+
:global(.dark) .back-to-top-btn
|
|
61
|
+
background-color: #2B2D38
|
|
62
|
+
border: none
|
|
63
|
+
color: #ffffff
|
|
64
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15)
|
|
65
|
+
|
|
66
|
+
&:hover
|
|
67
|
+
background-color: #3a3d4a
|
|
68
|
+
border-color: none
|
|
69
|
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2)
|
|
70
|
+
|
|
71
|
+
/* 响应式调整 */
|
|
72
|
+
@media (max-width: 1024px)
|
|
73
|
+
.back-to-top-btn
|
|
74
|
+
right: 1rem
|
|
75
|
+
bottom: 10rem
|
|
76
|
+
border-radius: 50%
|
|
77
|
+
|
|
78
|
+
@media (max-width: 768px)
|
|
79
|
+
.back-to-top-btn
|
|
80
|
+
right: 0.75rem
|
|
81
|
+
bottom: 6rem
|
|
82
|
+
width: 2.75rem
|
|
83
|
+
height: 2.75rem
|
|
84
|
+
font-size: 1.5rem
|
|
85
|
+
border-radius: 50%
|
|
86
|
+
svg
|
|
87
|
+
font-size: 1.25rem
|
|
88
|
+
|
|
89
|
+
@media (max-width: 480px)
|
|
90
|
+
.back-to-top-btn
|
|
91
|
+
right: 0.5rem
|
|
92
|
+
bottom: 4rem
|
|
93
|
+
width: 2.5rem
|
|
94
|
+
height: 2.5rem
|
|
95
|
+
font-size: 1.25rem
|
|
96
|
+
border-radius: 50%
|
|
97
|
+
svg
|
|
98
|
+
font-size: 1rem
|
|
99
|
+
|
|
100
|
+
/* 高缩放比例适配 */
|
|
101
|
+
@media (min-resolution: 2dppx)
|
|
102
|
+
.back-to-top-btn
|
|
103
|
+
right: max(0.5rem, 2rem - 2vw)
|
|
104
|
+
bottom: max(9rem, 10rem - 5vh)
|
|
105
|
+
|
|
106
|
+
/* 超小屏幕适配 */
|
|
107
|
+
@media (max-width: 360px)
|
|
108
|
+
.back-to-top-btn
|
|
109
|
+
right: 0.25rem
|
|
110
|
+
bottom: 5rem
|
|
111
|
+
width: 2rem
|
|
112
|
+
height: 2rem
|
|
113
|
+
font-size: 1rem
|
|
114
|
+
border-radius: 50%
|
|
115
|
+
svg
|
|
116
|
+
font-size: 0.875rem
|
|
117
|
+
|
|
118
|
+
/* 横屏模式适配 */
|
|
119
|
+
@media (orientation: landscape) and (max-height: 500px)
|
|
120
|
+
.back-to-top-btn
|
|
121
|
+
bottom: 8rem
|
|
122
|
+
right: 0.5rem
|
|
123
|
+
|
|
124
|
+
/* 确保按钮始终在可视区域内 */
|
|
125
|
+
.back-to-top-btn
|
|
126
|
+
/* 防止按钮被裁剪 */
|
|
127
|
+
min-width: 1.25rem
|
|
128
|
+
min-height: 1.25rem
|
|
129
|
+
/* 确保按钮有足够的点击区域 */
|
|
130
|
+
padding: 0.25rem
|
|
131
|
+
/* 防止按钮超出屏幕 */
|
|
132
|
+
max-width: 3rem
|
|
133
|
+
max-height: 3rem
|
|
134
|
+
/* 确保圆角在所有尺寸下都正确 */
|
|
135
|
+
border-radius: 50% !important
|
|
136
|
+
|
|
137
|
+
/* 高DPI屏幕优化 */
|
|
138
|
+
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)
|
|
139
|
+
.back-to-top-btn
|
|
140
|
+
/* 确保在高DPI屏幕上清晰显示 */
|
|
141
|
+
image-rendering: -webkit-optimize-contrast
|
|
142
|
+
image-rendering: crisp-edges
|
|
143
|
+
|
|
144
|
+
/* 极低分辨率适配 */
|
|
145
|
+
@media (max-width: 320px)
|
|
146
|
+
.back-to-top-btn
|
|
147
|
+
right: 0.125rem
|
|
148
|
+
bottom: 4.5rem
|
|
149
|
+
width: 1.75rem
|
|
150
|
+
height: 1.75rem
|
|
151
|
+
font-size: 0.875rem
|
|
152
|
+
border-radius: 50%
|
|
153
|
+
svg
|
|
154
|
+
font-size: 0.75rem
|
|
155
|
+
|
|
156
|
+
/* 确保按钮在容器内正确显示 */
|
|
157
|
+
.back-to-top-wrapper
|
|
158
|
+
/* 确保容器不会裁剪内容 */
|
|
159
|
+
overflow: visible
|
|
160
|
+
/* 防止容器影响按钮定位 */
|
|
161
|
+
z-index: 999
|
|
162
|
+
|
|
163
|
+
/* 按钮激活状态 */
|
|
164
|
+
.back-to-top-btn:active
|
|
165
|
+
transform: scale(0.95)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
</style>
|
|
169
|
+
|
|
170
|
+
<script is:raw is:inline>
|
|
171
|
+
window.backToTop = function () {
|
|
172
|
+
window.scroll({ top: 0, behavior: "smooth" });
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// 响应式返回顶部按钮管理器
|
|
176
|
+
if (typeof window.BackToTopManager === "undefined") {
|
|
177
|
+
window.BackToTopManager = class BackToTopManager {
|
|
178
|
+
constructor() {
|
|
179
|
+
this.button = document.getElementById("back-to-top-btn");
|
|
180
|
+
this.wrapper = document.querySelector(".back-to-top-wrapper");
|
|
181
|
+
this.init();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
init() {
|
|
185
|
+
if (!this.button || !this.wrapper) return;
|
|
186
|
+
|
|
187
|
+
this.setupScrollListener();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setupScrollListener() {
|
|
191
|
+
const updateVisibility = () => {
|
|
192
|
+
const scrollTop =
|
|
193
|
+
window.pageYOffset || document.documentElement.scrollTop;
|
|
194
|
+
|
|
195
|
+
// 当滚动超过200px时显示按钮
|
|
196
|
+
if (scrollTop > 200) {
|
|
197
|
+
this.button.classList.remove("hide");
|
|
198
|
+
} else {
|
|
199
|
+
this.button.classList.add("hide");
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
window.addEventListener("scroll", updateVisibility, { passive: true });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 移除resize监听和位置更新逻辑,因为CSS媒体查询已经处理了响应式定位
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 页面加载完成后初始化
|
|
211
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
212
|
+
new BackToTopManager();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// 如果页面已经加载完成,立即初始化
|
|
216
|
+
if (document.readyState === "loading") {
|
|
217
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
218
|
+
new BackToTopManager();
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
new BackToTopManager();
|
|
222
|
+
}
|
|
223
|
+
</script>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Remove current url path and remove trailing slash if exists
|
|
3
|
+
const currentUrlPath = Astro.url.pathname.replace(/\/+$/, "");
|
|
4
|
+
|
|
5
|
+
// Get url array from path
|
|
6
|
+
// eg: /tags/tailwindcss => ['tags', 'tailwindcss']
|
|
7
|
+
const breadcrumbList = currentUrlPath.split("/").slice(1);
|
|
8
|
+
|
|
9
|
+
// if breadcrumb is Home > Posts > 1 <etc>
|
|
10
|
+
// replace Posts with Posts (page number)
|
|
11
|
+
if (breadcrumbList[0] === "posts") {
|
|
12
|
+
breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// if breadcrumb is Home > Tags > [tag] > [page] <etc>
|
|
16
|
+
// replace [tag] > [page] with [tag] (page number)
|
|
17
|
+
if (breadcrumbList[0] === "tags" && !isNaN(Number(breadcrumbList[2]))) {
|
|
18
|
+
breadcrumbList.splice(
|
|
19
|
+
1,
|
|
20
|
+
3,
|
|
21
|
+
`${breadcrumbList[1]} ${Number(breadcrumbList[2]) === 1 ? "" : "(page " + breadcrumbList[2] + ")"}`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<nav class="app-layout mt-8 mb-1" aria-label="breadcrumb">
|
|
27
|
+
<ul
|
|
28
|
+
class="font-light [&>li]:inline [&>li:not(:last-child)>a]:hover:opacity-100"
|
|
29
|
+
>
|
|
30
|
+
<li>
|
|
31
|
+
<a href="/" class="opacity-80">Home</a>
|
|
32
|
+
<span aria-hidden="true" class="opacity-80">»</span>
|
|
33
|
+
</li>
|
|
34
|
+
{
|
|
35
|
+
breadcrumbList.map((breadcrumb, index) =>
|
|
36
|
+
index + 1 === breadcrumbList.length ? (
|
|
37
|
+
<li>
|
|
38
|
+
<span
|
|
39
|
+
class:list={["capitalize opacity-75", { lowercase: index > 0 }]}
|
|
40
|
+
aria-current="page"
|
|
41
|
+
>
|
|
42
|
+
{/* make the last part lowercase in Home > Tags > some-tag */}
|
|
43
|
+
{decodeURIComponent(breadcrumb)}
|
|
44
|
+
</span>
|
|
45
|
+
</li>
|
|
46
|
+
) : (
|
|
47
|
+
<li>
|
|
48
|
+
<a href={`/${breadcrumb}/`} class="capitalize opacity-70">
|
|
49
|
+
{breadcrumb}
|
|
50
|
+
</a>
|
|
51
|
+
<span aria-hidden="true">»</span>
|
|
52
|
+
</li>
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
</ul>
|
|
57
|
+
</nav>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
---
|
|
2
|
+
import IconArrowNarrowUp from "../../assets/icons/IconArrowNarrowUp.svg";
|
|
3
|
+
import IconMoon from "../../assets/icons/IconMoon.svg";
|
|
4
|
+
import IconSunHigh from "../../assets/icons/IconSunHigh.svg";
|
|
5
|
+
import { SITE } from "@/config";
|
|
6
|
+
|
|
7
|
+
const aiEnabled = SITE.ai?.enabled ?? false;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<div
|
|
11
|
+
class="fixed right-6 bottom-6 z-50 flex flex-col items-center gap-3"
|
|
12
|
+
id="floating-actions"
|
|
13
|
+
role="group"
|
|
14
|
+
aria-label="Page actions"
|
|
15
|
+
>
|
|
16
|
+
<!-- AI Assistant -->
|
|
17
|
+
{
|
|
18
|
+
aiEnabled && (
|
|
19
|
+
<button
|
|
20
|
+
id="ai-chat-toggle-fab"
|
|
21
|
+
class="fab-btn flex size-11 items-center justify-center rounded-full border border-accent bg-accent text-background shadow-md transition-all duration-200 hover:scale-105 hover:shadow-lg active:scale-95"
|
|
22
|
+
aria-label="AI Assistant"
|
|
23
|
+
title="AI Assistant"
|
|
24
|
+
>
|
|
25
|
+
<svg
|
|
26
|
+
class="size-5"
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
fill="none"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
stroke-width="2"
|
|
31
|
+
stroke-linecap="round"
|
|
32
|
+
stroke-linejoin="round"
|
|
33
|
+
>
|
|
34
|
+
<path d="M12 8V4H8" />
|
|
35
|
+
<rect width="16" height="12" x="4" y="8" rx="2" />
|
|
36
|
+
<path d="M2 14h2" />
|
|
37
|
+
<path d="M20 14h2" />
|
|
38
|
+
<path d="M15 13v2" />
|
|
39
|
+
<path d="M9 13v2" />
|
|
40
|
+
</svg>
|
|
41
|
+
</button>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
<!-- Reading Mode (only on article pages) -->
|
|
46
|
+
<button
|
|
47
|
+
id="reading-mode-btn"
|
|
48
|
+
class="fab-btn hidden size-11 items-center justify-center rounded-full border border-border bg-background/90 shadow-md backdrop-blur-sm transition-all duration-200 hover:shadow-lg active:scale-95"
|
|
49
|
+
aria-label="Toggle reading mode"
|
|
50
|
+
aria-pressed="false"
|
|
51
|
+
title="Toggle reading mode"
|
|
52
|
+
>
|
|
53
|
+
<svg
|
|
54
|
+
id="reading-icon-off"
|
|
55
|
+
class="text-muted-foreground size-5"
|
|
56
|
+
viewBox="0 0 24 24"
|
|
57
|
+
fill="none"
|
|
58
|
+
stroke="currentColor"
|
|
59
|
+
stroke-width="2"
|
|
60
|
+
stroke-linecap="round"
|
|
61
|
+
stroke-linejoin="round"
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
>
|
|
64
|
+
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
|
|
65
|
+
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
|
|
66
|
+
</svg>
|
|
67
|
+
<svg
|
|
68
|
+
id="reading-icon-on"
|
|
69
|
+
class="hidden size-5 text-accent"
|
|
70
|
+
viewBox="0 0 24 24"
|
|
71
|
+
fill="none"
|
|
72
|
+
stroke="currentColor"
|
|
73
|
+
stroke-width="2"
|
|
74
|
+
stroke-linecap="round"
|
|
75
|
+
stroke-linejoin="round"
|
|
76
|
+
aria-hidden="true"
|
|
77
|
+
>
|
|
78
|
+
<path d="M18 6 6 18"></path>
|
|
79
|
+
<path d="m6 6 12 12"></path>
|
|
80
|
+
</svg>
|
|
81
|
+
</button>
|
|
82
|
+
|
|
83
|
+
<!-- Font Size Controls (visible in reading mode) -->
|
|
84
|
+
<div id="font-controls" class="hidden flex-col items-center gap-1.5">
|
|
85
|
+
<button
|
|
86
|
+
id="font-increase-btn"
|
|
87
|
+
class="text-muted-foreground size-9 rounded-full border border-border bg-background/90 text-xs font-bold shadow-sm backdrop-blur-sm transition-all hover:border-accent hover:text-accent active:scale-95"
|
|
88
|
+
aria-label="Increase font size"
|
|
89
|
+
title="Increase font size">A+</button
|
|
90
|
+
>
|
|
91
|
+
<button
|
|
92
|
+
id="font-decrease-btn"
|
|
93
|
+
class="text-muted-foreground size-9 rounded-full border border-border bg-background/90 text-xs font-bold shadow-sm backdrop-blur-sm transition-all hover:border-accent hover:text-accent active:scale-95"
|
|
94
|
+
aria-label="Decrease font size"
|
|
95
|
+
title="Decrease font size">A-</button
|
|
96
|
+
>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Theme Toggle -->
|
|
100
|
+
{
|
|
101
|
+
SITE.lightAndDarkMode && (
|
|
102
|
+
<button
|
|
103
|
+
id="theme-btn"
|
|
104
|
+
class="fab-btn group relative size-11 rounded-full border border-border bg-background/90 shadow-md backdrop-blur-sm hover:shadow-lg"
|
|
105
|
+
title="Toggle theme"
|
|
106
|
+
aria-label="auto"
|
|
107
|
+
aria-live="polite"
|
|
108
|
+
>
|
|
109
|
+
<IconMoon class="icon-moon text-muted-foreground absolute top-1/2 left-1/2 size-5 -translate-x-1/2 -translate-y-1/2 group-hover:text-foreground" />
|
|
110
|
+
<IconSunHigh class="icon-sun text-muted-foreground absolute top-1/2 left-1/2 size-5 -translate-x-1/2 -translate-y-1/2 group-hover:text-foreground" />
|
|
111
|
+
</button>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
<!-- Back to Top -->
|
|
116
|
+
<button
|
|
117
|
+
id="back-to-top-btn"
|
|
118
|
+
class="fab-btn pointer-events-none size-11 rounded-full border border-border bg-background/90 opacity-0 shadow-md backdrop-blur-sm transition-all duration-200 hover:shadow-lg active:scale-95"
|
|
119
|
+
aria-label="Back to Top"
|
|
120
|
+
title="Back to Top"
|
|
121
|
+
>
|
|
122
|
+
<IconArrowNarrowUp class="text-muted-foreground mx-auto size-5" />
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<script>
|
|
127
|
+
function initFloatingActions() {
|
|
128
|
+
const backToTopBtn = document.getElementById("back-to-top-btn");
|
|
129
|
+
const readingModeBtn = document.getElementById("reading-mode-btn");
|
|
130
|
+
const fontControls = document.getElementById("font-controls");
|
|
131
|
+
const fontIncBtn = document.getElementById("font-increase-btn");
|
|
132
|
+
const fontDecBtn = document.getElementById("font-decrease-btn");
|
|
133
|
+
const iconOff = document.getElementById("reading-icon-off");
|
|
134
|
+
const iconOn = document.getElementById("reading-icon-on");
|
|
135
|
+
const article = document.querySelector("#article") as HTMLElement | null;
|
|
136
|
+
|
|
137
|
+
const isArticlePage = !!document.querySelector("article#article");
|
|
138
|
+
|
|
139
|
+
if (readingModeBtn) {
|
|
140
|
+
if (isArticlePage) {
|
|
141
|
+
readingModeBtn.classList.remove("hidden");
|
|
142
|
+
readingModeBtn.classList.add("flex");
|
|
143
|
+
} else {
|
|
144
|
+
readingModeBtn.classList.add("hidden");
|
|
145
|
+
readingModeBtn.classList.remove("flex");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
readingModeBtn.addEventListener("click", () => {
|
|
149
|
+
const active = document.body.classList.toggle("reading-mode");
|
|
150
|
+
readingModeBtn.setAttribute("aria-pressed", String(active));
|
|
151
|
+
|
|
152
|
+
if (active) {
|
|
153
|
+
readingModeBtn.classList.add("border-accent", "bg-accent/10");
|
|
154
|
+
readingModeBtn.classList.remove("bg-background/90");
|
|
155
|
+
iconOff?.classList.add("hidden");
|
|
156
|
+
iconOn?.classList.remove("hidden");
|
|
157
|
+
fontControls?.classList.remove("hidden");
|
|
158
|
+
fontControls?.classList.add("flex");
|
|
159
|
+
} else {
|
|
160
|
+
readingModeBtn.classList.remove("border-accent", "bg-accent/10");
|
|
161
|
+
readingModeBtn.classList.add("bg-background/90");
|
|
162
|
+
iconOff?.classList.remove("hidden");
|
|
163
|
+
iconOn?.classList.add("hidden");
|
|
164
|
+
fontControls?.classList.add("hidden");
|
|
165
|
+
fontControls?.classList.remove("flex");
|
|
166
|
+
if (article) article.style.fontSize = "";
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let fontSize = 1;
|
|
172
|
+
fontIncBtn?.addEventListener("click", () => {
|
|
173
|
+
if (!article || fontSize >= 1.5) return;
|
|
174
|
+
fontSize = Math.min(fontSize + 0.1, 1.5);
|
|
175
|
+
article.style.fontSize = `${fontSize}em`;
|
|
176
|
+
});
|
|
177
|
+
fontDecBtn?.addEventListener("click", () => {
|
|
178
|
+
if (!article || fontSize <= 0.8) return;
|
|
179
|
+
fontSize = Math.max(fontSize - 0.1, 0.8);
|
|
180
|
+
article.style.fontSize = `${fontSize}em`;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!backToTopBtn) return;
|
|
184
|
+
|
|
185
|
+
const updateVisibility = () => {
|
|
186
|
+
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
|
187
|
+
if (scrollTop > 300) {
|
|
188
|
+
backToTopBtn.classList.remove("opacity-0", "pointer-events-none");
|
|
189
|
+
backToTopBtn.classList.add("opacity-100");
|
|
190
|
+
} else {
|
|
191
|
+
backToTopBtn.classList.add("opacity-0", "pointer-events-none");
|
|
192
|
+
backToTopBtn.classList.remove("opacity-100");
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
backToTopBtn.addEventListener("click", () => {
|
|
197
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
window.addEventListener("scroll", updateVisibility, { passive: true });
|
|
201
|
+
updateVisibility();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
initFloatingActions();
|
|
205
|
+
document.addEventListener("astro:after-swap", initFloatingActions);
|
|
206
|
+
</script>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
import { SITE } from "@/config";
|
|
4
|
+
import { t } from "../../utils/i18n";
|
|
5
|
+
|
|
6
|
+
const currentYear = new Date().getFullYear();
|
|
7
|
+
|
|
8
|
+
// Calculate running days
|
|
9
|
+
const startDate = new Date(SITE.startDate);
|
|
10
|
+
const today = new Date();
|
|
11
|
+
const runningDays = Math.floor(
|
|
12
|
+
(today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
noMarginTop?: boolean;
|
|
17
|
+
lang?: string;
|
|
18
|
+
} & HTMLAttributes<"footer">;
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
noMarginTop = false,
|
|
22
|
+
lang = "zh",
|
|
23
|
+
class: className,
|
|
24
|
+
...attrs
|
|
25
|
+
} = Astro.props;
|
|
26
|
+
|
|
27
|
+
const copyrightText = t("footer.copyright", lang)
|
|
28
|
+
.replace("{year}", String(currentYear))
|
|
29
|
+
.replace("{author}", SITE.author);
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<footer
|
|
33
|
+
class:list={["app-layout max-w-5xl", { "mt-auto": !noMarginTop }, className]}
|
|
34
|
+
{...attrs}
|
|
35
|
+
>
|
|
36
|
+
<div
|
|
37
|
+
class:list={[
|
|
38
|
+
"py-6 sm:py-4",
|
|
39
|
+
"border-t border-border",
|
|
40
|
+
"flex flex-col items-center justify-center",
|
|
41
|
+
]}
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
class="flex flex-col items-center justify-center text-sm whitespace-nowrap text-neutral-600 sm:flex-row dark:text-neutral-400"
|
|
45
|
+
>
|
|
46
|
+
<!-- Column 1: Avatar + Hi, {author} -->
|
|
47
|
+
<div class="px-4">
|
|
48
|
+
<a
|
|
49
|
+
href={`/${lang}/`}
|
|
50
|
+
class="flex items-center gap-2 transition-colors hover:text-accent"
|
|
51
|
+
>
|
|
52
|
+
<img
|
|
53
|
+
src="/favicon.ico"
|
|
54
|
+
alt={`${SITE.author}'s avatar`}
|
|
55
|
+
class="h-5 w-5 rounded-full"
|
|
56
|
+
/>
|
|
57
|
+
<span>{t("footer.hi", lang)}, {SITE.author}</span>
|
|
58
|
+
</a>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<span class="hidden px-2 text-neutral-500 sm:inline dark:text-neutral-500"
|
|
62
|
+
>|</span
|
|
63
|
+
>
|
|
64
|
+
|
|
65
|
+
<!-- Column 2: Running days -->
|
|
66
|
+
<div class="px-4">
|
|
67
|
+
<span
|
|
68
|
+
>{t("footer.running", lang)}
|
|
69
|
+
{runningDays}
|
|
70
|
+
{t("footer.days", lang)}</span
|
|
71
|
+
>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<span class="hidden px-2 text-neutral-500 sm:inline dark:text-neutral-500"
|
|
75
|
+
>|</span
|
|
76
|
+
>
|
|
77
|
+
|
|
78
|
+
<!-- Column 3: Copyright -->
|
|
79
|
+
<div class="px-4">
|
|
80
|
+
<span>{copyrightText}</span>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<span class="hidden px-2 text-neutral-500 sm:inline dark:text-neutral-500"
|
|
84
|
+
>|</span
|
|
85
|
+
>
|
|
86
|
+
|
|
87
|
+
<!-- Column 4: Sitemap -->
|
|
88
|
+
<div class="px-4">
|
|
89
|
+
<a
|
|
90
|
+
href="/sitemap-index.xml"
|
|
91
|
+
class="transition-colors hover:text-accent"
|
|
92
|
+
>
|
|
93
|
+
{t("footer.sitemap", lang)}
|
|
94
|
+
</a>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<span class="hidden px-2 text-neutral-500 sm:inline dark:text-neutral-500"
|
|
98
|
+
>|</span
|
|
99
|
+
>
|
|
100
|
+
|
|
101
|
+
<!-- Column 5: RSS -->
|
|
102
|
+
<div class="px-4">
|
|
103
|
+
<a href="/rss.xml" class="transition-colors hover:text-accent"> RSS </a>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</footer>
|