@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.
Files changed (103) hide show
  1. package/README.md +29 -0
  2. package/package.json +41 -0
  3. package/src/assets/icons/IconArchive.svg +1 -0
  4. package/src/assets/icons/IconArrowLeft.svg +1 -0
  5. package/src/assets/icons/IconArrowNarrowUp.svg +1 -0
  6. package/src/assets/icons/IconArrowRight.svg +1 -0
  7. package/src/assets/icons/IconArticle.svg +1 -0
  8. package/src/assets/icons/IconBrandX.svg +1 -0
  9. package/src/assets/icons/IconCalendar.svg +1 -0
  10. package/src/assets/icons/IconChevronLeft.svg +1 -0
  11. package/src/assets/icons/IconChevronRight.svg +1 -0
  12. package/src/assets/icons/IconEdit.svg +1 -0
  13. package/src/assets/icons/IconFacebook.svg +1 -0
  14. package/src/assets/icons/IconGitHub.svg +1 -0
  15. package/src/assets/icons/IconHash.svg +1 -0
  16. package/src/assets/icons/IconHome.svg +1 -0
  17. package/src/assets/icons/IconLinkedin.svg +1 -0
  18. package/src/assets/icons/IconMail.svg +1 -0
  19. package/src/assets/icons/IconMenuDeep.svg +1 -0
  20. package/src/assets/icons/IconMoon.svg +1 -0
  21. package/src/assets/icons/IconPinterest.svg +1 -0
  22. package/src/assets/icons/IconProject.svg +1 -0
  23. package/src/assets/icons/IconRss.svg +1 -0
  24. package/src/assets/icons/IconSearch.svg +1 -0
  25. package/src/assets/icons/IconSeries.svg +1 -0
  26. package/src/assets/icons/IconSunHigh.svg +1 -0
  27. package/src/assets/icons/IconTag.svg +1 -0
  28. package/src/assets/icons/IconTelegram.svg +1 -0
  29. package/src/assets/icons/IconUser.svg +1 -0
  30. package/src/assets/icons/IconWhatsapp.svg +1 -0
  31. package/src/assets/icons/IconX.svg +1 -0
  32. package/src/components/ai/AIChatWidget.astro +377 -0
  33. package/src/components/blog/Comments.astro +527 -0
  34. package/src/components/blog/Copyright.astro +152 -0
  35. package/src/components/blog/EditPost.astro +59 -0
  36. package/src/components/blog/FloatingTOC.astro +260 -0
  37. package/src/components/blog/InlineTOC.astro +223 -0
  38. package/src/components/blog/PostActions.astro +306 -0
  39. package/src/components/blog/RelatedPosts.astro +60 -0
  40. package/src/components/blog/SeriesNav.astro +176 -0
  41. package/src/components/blog/ShareLinks.astro +26 -0
  42. package/src/components/nav/BackButton.astro +37 -0
  43. package/src/components/nav/BackToTopButton.astro +223 -0
  44. package/src/components/nav/Breadcrumb.astro +57 -0
  45. package/src/components/nav/FloatingActions.astro +206 -0
  46. package/src/components/nav/Footer.astro +107 -0
  47. package/src/components/nav/Header.astro +252 -0
  48. package/src/components/nav/Pagination.astro +45 -0
  49. package/src/components/social/Socials.astro +19 -0
  50. package/src/components/social/Sponsors.astro +34 -0
  51. package/src/components/social/Sponsorship.astro +44 -0
  52. package/src/components/ui/Alert.astro +28 -0
  53. package/src/components/ui/Card.astro +206 -0
  54. package/src/components/ui/Collapse.astro +82 -0
  55. package/src/components/ui/ColorPreview.astro +29 -0
  56. package/src/components/ui/Datetime.astro +61 -0
  57. package/src/components/ui/GithubCard.astro +191 -0
  58. package/src/components/ui/LinkButton.astro +21 -0
  59. package/src/components/ui/Tag.astro +37 -0
  60. package/src/components/ui/TagCloud.astro +69 -0
  61. package/src/components/ui/Timeline.astro +39 -0
  62. package/src/layouts/AboutLayout.astro +24 -0
  63. package/src/layouts/Layout.astro +329 -0
  64. package/src/layouts/Main.astro +42 -0
  65. package/src/layouts/PostDetails.astro +445 -0
  66. package/src/plugins/rehype-autolink-headings.ts +46 -0
  67. package/src/plugins/rehype-external-links.ts +35 -0
  68. package/src/plugins/rehype-table-scroll.ts +35 -0
  69. package/src/plugins/remark-add-zoomable.ts +28 -0
  70. package/src/plugins/remark-reading-time.ts +18 -0
  71. package/src/plugins/shiki-transformers.ts +212 -0
  72. package/src/scripts/lightbox.ts +63 -0
  73. package/src/scripts/reading-position.ts +56 -0
  74. package/src/scripts/theme-utils.ts +19 -0
  75. package/src/scripts/theme.ts +179 -0
  76. package/src/scripts/web-vitals.ts +96 -0
  77. package/src/styles/code-blocks.css +194 -0
  78. package/src/styles/components.css +252 -0
  79. package/src/styles/global.css +403 -0
  80. package/src/styles/typography.css +149 -0
  81. package/src/types.ts +89 -0
  82. package/src/utils/generateOgImages.ts +38 -0
  83. package/src/utils/getCategoryPath.ts +23 -0
  84. package/src/utils/getPath.ts +52 -0
  85. package/src/utils/getPostsByCategory.ts +17 -0
  86. package/src/utils/getPostsByGroupCondition.ts +25 -0
  87. package/src/utils/getPostsByLang.ts +27 -0
  88. package/src/utils/getPostsByTag.ts +10 -0
  89. package/src/utils/getReadingTime.ts +33 -0
  90. package/src/utils/getRelatedPosts.ts +59 -0
  91. package/src/utils/getSeriesData.ts +57 -0
  92. package/src/utils/getSortedPosts.ts +18 -0
  93. package/src/utils/getTagsWithCount.ts +38 -0
  94. package/src/utils/getUniqueCategories.ts +81 -0
  95. package/src/utils/getUniqueTags.ts +23 -0
  96. package/src/utils/i18n.ts +249 -0
  97. package/src/utils/loadGoogleFont.ts +38 -0
  98. package/src/utils/og-templates/post.js +229 -0
  99. package/src/utils/og-templates/site.js +128 -0
  100. package/src/utils/pathUtils.ts +17 -0
  101. package/src/utils/postFilter.ts +11 -0
  102. package/src/utils/slugify.ts +23 -0
  103. 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">&raquo;</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">&raquo;</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>