@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,306 @@
1
+ ---
2
+ // PostActions - Like and Bookmark functionality
3
+ // Uses localStorage for persistence on static sites
4
+ ---
5
+
6
+ <div
7
+ class="post-actions mt-6 flex flex-wrap items-center justify-center gap-4"
8
+ data-pagefind-ignore
9
+ >
10
+ {/* Like Button */}
11
+ <button
12
+ id="like-btn"
13
+ class="action-btn group text-muted-foreground flex items-center gap-2 rounded-full border border-border bg-background/50 px-4 py-2 text-sm transition-all hover:border-accent hover:text-accent"
14
+ aria-label="Like this post"
15
+ aria-pressed="false"
16
+ >
17
+ <svg
18
+ class="size-5 transition-transform group-hover:scale-110"
19
+ viewBox="0 0 24 24"
20
+ fill="none"
21
+ stroke="currentColor"
22
+ stroke-width="2"
23
+ stroke-linecap="round"
24
+ stroke-linejoin="round"
25
+ >
26
+ <path
27
+ d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
28
+ ></path>
29
+ </svg>
30
+ <span class="like-text">喜欢</span>
31
+ <span id="like-count" class="rounded-full bg-muted px-2 text-xs">0</span>
32
+ </button>
33
+
34
+ {/* Bookmark Button */}
35
+ <button
36
+ id="bookmark-btn"
37
+ class="action-btn group text-muted-foreground flex items-center gap-2 rounded-full border border-border bg-background/50 px-4 py-2 text-sm transition-all hover:border-accent hover:text-accent"
38
+ aria-label="Bookmark this post"
39
+ aria-pressed="false"
40
+ >
41
+ <svg
42
+ class="size-5 transition-transform group-hover:scale-110"
43
+ viewBox="0 0 24 24"
44
+ fill="none"
45
+ stroke="currentColor"
46
+ stroke-width="2"
47
+ stroke-linecap="round"
48
+ stroke-linejoin="round"
49
+ >
50
+ <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
51
+ </svg>
52
+ <span class="bookmark-text">收藏</span>
53
+ </button>
54
+ </div>
55
+
56
+ <script is:inline>
57
+ const LIKES_KEY = "blog_likes";
58
+ const BOOKMARKS_KEY = "blog_bookmarks";
59
+
60
+ function getPostId() {
61
+ return window.location.pathname.replace(/\/$/, "");
62
+ }
63
+
64
+ function getLikes() {
65
+ try {
66
+ const stored = localStorage.getItem(LIKES_KEY);
67
+ return stored ? JSON.parse(stored) : {};
68
+ } catch {
69
+ return {};
70
+ }
71
+ }
72
+
73
+ function getBookmarks() {
74
+ try {
75
+ const stored = localStorage.getItem(BOOKMARKS_KEY);
76
+ return stored ? JSON.parse(stored) : [];
77
+ } catch {
78
+ return [];
79
+ }
80
+ }
81
+
82
+ function saveLikes(likes) {
83
+ localStorage.setItem(LIKES_KEY, JSON.stringify(likes));
84
+ }
85
+
86
+ function saveBookmarks(bookmarks) {
87
+ localStorage.setItem(BOOKMARKS_KEY, JSON.stringify(bookmarks));
88
+ }
89
+
90
+ function isLiked(postId) {
91
+ const likedPosts = localStorage.getItem("blog_liked_posts");
92
+ if (likedPosts) {
93
+ try {
94
+ const posts = JSON.parse(likedPosts);
95
+ return posts.includes(postId);
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+ return false;
101
+ }
102
+
103
+ function setLiked(postId, liked) {
104
+ let posts = [];
105
+
106
+ const stored = localStorage.getItem("blog_liked_posts");
107
+ if (stored) {
108
+ try {
109
+ posts = JSON.parse(stored);
110
+ } catch {
111
+ posts = [];
112
+ }
113
+ }
114
+ if (liked && !posts.includes(postId)) {
115
+ posts.push(postId);
116
+ } else if (!liked) {
117
+ posts = posts.filter(id => id !== postId);
118
+ }
119
+ localStorage.setItem("blog_liked_posts", JSON.stringify(posts));
120
+ }
121
+
122
+ function isBookmarked(postId) {
123
+ return getBookmarks().includes(postId);
124
+ }
125
+
126
+ function initPostActions() {
127
+ const likeBtn = document.getElementById("like-btn");
128
+ const bookmarkBtn = document.getElementById("bookmark-btn");
129
+ const likeCount = document.getElementById("like-count");
130
+ const likeText = likeBtn?.querySelector(".like-text");
131
+ const bookmarkText = bookmarkBtn?.querySelector(".bookmark-text");
132
+
133
+ if (!likeBtn || !bookmarkBtn || !likeCount) return;
134
+
135
+ const postId = getPostId();
136
+
137
+ // Initialize like state
138
+ const likes = getLikes();
139
+ const currentLikes = likes[postId] || 0;
140
+ likeCount.textContent = String(currentLikes);
141
+
142
+ const userLiked = isLiked(postId);
143
+ if (userLiked) {
144
+ likeBtn.classList.add(
145
+ "liked",
146
+ "border-accent",
147
+ "text-accent",
148
+ "bg-accent/10"
149
+ );
150
+ likeBtn.setAttribute("aria-pressed", "true");
151
+ if (likeText) likeText.textContent = "已喜欢";
152
+ // Fill heart
153
+ const heart = likeBtn.querySelector("svg path");
154
+ if (heart) {
155
+ heart.setAttribute("fill", "currentColor");
156
+ }
157
+ }
158
+
159
+ // Initialize bookmark state
160
+ if (isBookmarked(postId)) {
161
+ bookmarkBtn.classList.add(
162
+ "bookmarked",
163
+ "border-accent",
164
+ "text-accent",
165
+ "bg-accent/10"
166
+ );
167
+ bookmarkBtn.setAttribute("aria-pressed", "true");
168
+ if (bookmarkText) bookmarkText.textContent = "已收藏";
169
+ // Fill bookmark
170
+ const bookmark = bookmarkBtn.querySelector("svg path");
171
+ if (bookmark) {
172
+ bookmark.setAttribute("fill", "currentColor");
173
+ }
174
+ }
175
+
176
+ // Like button click handler
177
+ likeBtn.addEventListener("click", () => {
178
+ const likes = getLikes();
179
+ const userLiked = isLiked(postId);
180
+
181
+ if (userLiked) {
182
+ // Unlike
183
+ likes[postId] = Math.max(0, (likes[postId] || 1) - 1);
184
+ setLiked(postId, false);
185
+ likeBtn.classList.remove(
186
+ "liked",
187
+ "border-accent",
188
+ "text-accent",
189
+ "bg-accent/10"
190
+ );
191
+ likeBtn.setAttribute("aria-pressed", "false");
192
+ if (likeText) likeText.textContent = "喜欢";
193
+ // Unfill heart
194
+ const heart = likeBtn.querySelector("svg path");
195
+ if (heart) {
196
+ heart.setAttribute("fill", "none");
197
+ }
198
+ } else {
199
+ // Like
200
+ likes[postId] = (likes[postId] || 0) + 1;
201
+ setLiked(postId, true);
202
+ likeBtn.classList.add(
203
+ "liked",
204
+ "border-accent",
205
+ "text-accent",
206
+ "bg-accent/10"
207
+ );
208
+ likeBtn.setAttribute("aria-pressed", "true");
209
+ if (likeText) likeText.textContent = "已喜欢";
210
+ // Fill heart
211
+ const heart = likeBtn.querySelector("svg path");
212
+ if (heart) {
213
+ heart.setAttribute("fill", "currentColor");
214
+ }
215
+ // Add animation
216
+ likeBtn.classList.add("animate-pulse");
217
+ setTimeout(() => likeBtn.classList.remove("animate-pulse"), 300);
218
+ }
219
+
220
+ saveLikes(likes);
221
+ likeCount.textContent = String(likes[postId] || 0);
222
+ });
223
+
224
+ // Bookmark button click handler
225
+ bookmarkBtn.addEventListener("click", () => {
226
+ const bookmarks = getBookmarks();
227
+ const isCurrentlyBookmarked = bookmarks.includes(postId);
228
+
229
+ if (isCurrentlyBookmarked) {
230
+ // Remove bookmark
231
+ const newBookmarks = bookmarks.filter(id => id !== postId);
232
+ saveBookmarks(newBookmarks);
233
+ bookmarkBtn.classList.remove(
234
+ "bookmarked",
235
+ "border-accent",
236
+ "text-accent",
237
+ "bg-accent/10"
238
+ );
239
+ bookmarkBtn.setAttribute("aria-pressed", "false");
240
+ if (bookmarkText) bookmarkText.textContent = "收藏";
241
+ // Unfill bookmark
242
+ const bookmark = bookmarkBtn.querySelector("svg path");
243
+ if (bookmark) {
244
+ bookmark.setAttribute("fill", "none");
245
+ }
246
+ } else {
247
+ // Add bookmark
248
+ bookmarks.push(postId);
249
+ saveBookmarks(bookmarks);
250
+ bookmarkBtn.classList.add(
251
+ "bookmarked",
252
+ "border-accent",
253
+ "text-accent",
254
+ "bg-accent/10"
255
+ );
256
+ bookmarkBtn.setAttribute("aria-pressed", "true");
257
+ if (bookmarkText) bookmarkText.textContent = "已收藏";
258
+ // Fill bookmark
259
+ const bookmark = bookmarkBtn.querySelector("svg path");
260
+ if (bookmark) {
261
+ bookmark.setAttribute("fill", "currentColor");
262
+ }
263
+ // Add animation
264
+ bookmarkBtn.classList.add("animate-pulse");
265
+ setTimeout(() => bookmarkBtn.classList.remove("animate-pulse"), 300);
266
+ }
267
+ });
268
+ }
269
+
270
+ // Initialize on page load
271
+ initPostActions();
272
+
273
+ // Re-initialize after page transitions
274
+ document.addEventListener("astro:page-load", initPostActions);
275
+ </script>
276
+
277
+ <style is:global>
278
+ .post-actions .action-btn {
279
+ transition: all 0.2s ease;
280
+ }
281
+
282
+ .post-actions .action-btn:hover {
283
+ transform: translateY(-2px);
284
+ }
285
+
286
+ .post-actions .action-btn:active {
287
+ transform: translateY(0);
288
+ }
289
+
290
+ .post-actions .action-btn.liked svg,
291
+ .post-actions .action-btn.bookmarked svg {
292
+ animation: heartbeat 0.3s ease;
293
+ }
294
+
295
+ @keyframes heartbeat {
296
+ 0% {
297
+ transform: scale(1);
298
+ }
299
+ 50% {
300
+ transform: scale(1.2);
301
+ }
302
+ 100% {
303
+ transform: scale(1);
304
+ }
305
+ }
306
+ </style>
@@ -0,0 +1,60 @@
1
+ ---
2
+ import type { CollectionEntry } from "astro:content";
3
+ import { getLocalizedPostPath } from "../../utils/getPath";
4
+ import Datetime from "../../components/ui/Datetime.astro";
5
+
6
+ interface Props {
7
+ posts: CollectionEntry<"blog">[];
8
+ lang?: string;
9
+ title?: string;
10
+ }
11
+
12
+ const {
13
+ posts,
14
+ lang = "zh",
15
+ title = lang === "zh" ? "相关推荐" : "Related Posts",
16
+ } = Astro.props;
17
+ ---
18
+
19
+ {
20
+ posts.length > 0 && (
21
+ <div class="mt-8" data-pagefind-ignore>
22
+ <h3 class="mb-4 flex items-center gap-2 text-lg font-semibold text-foreground">
23
+ <svg
24
+ class="size-5 text-accent"
25
+ viewBox="0 0 24 24"
26
+ fill="none"
27
+ stroke="currentColor"
28
+ stroke-width="2"
29
+ stroke-linecap="round"
30
+ stroke-linejoin="round"
31
+ >
32
+ <path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z" />
33
+ </svg>
34
+ {title}
35
+ </h3>
36
+ <div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
37
+ {posts.map(post => (
38
+ <a
39
+ href={getLocalizedPostPath(lang, post.id)}
40
+ class="group flex flex-col rounded-lg border border-border bg-background/50 p-4 transition-all duration-200 hover:border-accent/50 hover:bg-muted/30 hover:shadow-sm"
41
+ >
42
+ <h4 class="line-clamp-2 text-sm font-medium text-foreground transition-colors group-hover:text-accent">
43
+ {post.data.title}
44
+ </h4>
45
+ <p class="text-muted-foreground mt-1.5 line-clamp-2 text-xs">
46
+ {post.data.description}
47
+ </p>
48
+ <div class="mt-auto pt-2">
49
+ <Datetime
50
+ pubDatetime={post.data.pubDatetime}
51
+ modDatetime={post.data.modDatetime}
52
+ lang={lang}
53
+ />
54
+ </div>
55
+ </a>
56
+ ))}
57
+ </div>
58
+ </div>
59
+ )
60
+ }
@@ -0,0 +1,176 @@
1
+ ---
2
+ import type { CollectionEntry } from "astro:content";
3
+ import { getCollection } from "astro:content";
4
+ import { getPostsByLang } from "../../utils/getPostsByLang";
5
+ import { getSeriesByName } from "../../utils/getSeriesData";
6
+ import { getLocalizedPostPath } from "../../utils/getPath";
7
+ import { t } from "../../utils/i18n";
8
+
9
+ interface Props {
10
+ post: CollectionEntry<"blog">;
11
+ lang?: string;
12
+ }
13
+
14
+ const { post, lang = "zh" } = Astro.props;
15
+ const series = post.data.series;
16
+
17
+ if (!series) return;
18
+
19
+ const allPosts = await getCollection("blog", ({ data }) => !data.draft);
20
+ const langPosts = getPostsByLang(allPosts, lang);
21
+ const seriesInfo = getSeriesByName(langPosts, series.name);
22
+
23
+ if (!seriesInfo || seriesInfo.totalPosts < 2) return;
24
+
25
+ const currentOrder = series.order;
26
+ const seriesSlug = seriesInfo.slug;
27
+ const progressPct = (currentOrder / seriesInfo.totalPosts) * 100;
28
+ const orderStr = String(currentOrder).padStart(2, "0");
29
+ const totalStr = String(seriesInfo.totalPosts).padStart(2, "0");
30
+ ---
31
+
32
+ <nav
33
+ class="series-nav my-6 rounded-xl border p-4 sm:p-5"
34
+ aria-label={`${t("series.title", lang)}: ${series.name}`}
35
+ >
36
+ {/* Header */}
37
+ <div class="mb-3 flex items-center justify-between gap-2">
38
+ <a
39
+ href={`/${lang}/series/${encodeURIComponent(seriesSlug)}/`}
40
+ class="series-link flex items-center gap-2 text-sm font-bold"
41
+ >
42
+ <svg class="series-icon size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
43
+ <path d="M4 6h16M4 12h16M4 18h10" />
44
+ <circle cx="20" cy="18" r="2" fill="currentColor" stroke="none" />
45
+ </svg>
46
+ <span>{series.name}</span>
47
+ </a>
48
+ <span class="series-progress font-mono text-xs tracking-wider">
49
+ {orderStr}<span class="mx-0.5 opacity-40">/</span>{totalStr}
50
+ </span>
51
+ </div>
52
+
53
+ {/* Progress bar */}
54
+ <div class="series-track relative mb-4 h-[3px] w-full overflow-visible rounded-full">
55
+ <div class="series-fill absolute inset-y-0 left-0 rounded-full" style={`width: ${progressPct}%`}>
56
+ <span class="series-pulse-dot"></span>
57
+ </div>
58
+ </div>
59
+
60
+ {/* Post list */}
61
+ <ol class="space-y-0.5">
62
+ {
63
+ seriesInfo.posts.map((p, idx) => {
64
+ const isCurrent = p.id === post.id;
65
+ const href = getLocalizedPostPath(lang, p.id, p.filePath);
66
+ const num = String(idx + 1).padStart(2, "0");
67
+
68
+ return (
69
+ <li>
70
+ {isCurrent ? (
71
+ <span class="series-item series-item--active flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm font-medium">
72
+ <span class="font-mono text-xs opacity-70">{num}</span>
73
+ <span class="truncate">{p.data.title}</span>
74
+ </span>
75
+ ) : (
76
+ <a
77
+ href={href}
78
+ class="series-item flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm"
79
+ >
80
+ <span class="font-mono text-xs opacity-40">{num}</span>
81
+ <span class="truncate">{p.data.title}</span>
82
+ </a>
83
+ )}
84
+ </li>
85
+ );
86
+ })
87
+ }
88
+ </ol>
89
+ </nav>
90
+
91
+ <style>
92
+ .series-nav {
93
+ background: linear-gradient(135deg, var(--series-bg-from), var(--series-bg-to));
94
+ border-color: var(--series-border);
95
+ box-shadow: 0 0 15px var(--series-glow);
96
+ transition:
97
+ background-color 0.35s cubic-bezier(0.4, 0, 0.2, 1),
98
+ border-color 0.35s cubic-bezier(0.4, 0, 0.2, 1),
99
+ box-shadow 0.35s cubic-bezier(0.4, 0, 0.2, 1),
100
+ color 0.35s cubic-bezier(0.4, 0, 0.2, 1);
101
+ }
102
+
103
+ .series-link {
104
+ color: var(--foreground);
105
+ transition: color 0.2s;
106
+ }
107
+ .series-link:hover {
108
+ color: var(--accent);
109
+ }
110
+
111
+ .series-icon {
112
+ color: var(--accent);
113
+ }
114
+
115
+ .series-progress {
116
+ color: var(--foreground-soft);
117
+ }
118
+
119
+ /* Track */
120
+ .series-track {
121
+ background-color: var(--muted);
122
+ }
123
+ .series-fill {
124
+ background-color: var(--accent);
125
+ transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
126
+ }
127
+
128
+ /* Pulse dot */
129
+ .series-pulse-dot {
130
+ position: absolute;
131
+ right: -4px;
132
+ top: 50%;
133
+ transform: translateY(-50%);
134
+ width: 8px;
135
+ height: 8px;
136
+ border-radius: 50%;
137
+ background-color: var(--series-pulse);
138
+ animation: pulse-glow 2s ease-in-out infinite;
139
+ }
140
+
141
+ @keyframes pulse-glow {
142
+ 0%, 100% {
143
+ box-shadow: 0 0 4px 1px var(--series-glow);
144
+ opacity: 1;
145
+ }
146
+ 50% {
147
+ box-shadow: 0 0 10px 4px var(--series-glow);
148
+ opacity: 0.7;
149
+ }
150
+ }
151
+
152
+ /* List items */
153
+ .series-item {
154
+ color: var(--foreground-soft);
155
+ transition:
156
+ color 0.2s,
157
+ background-color 0.2s;
158
+ }
159
+ a.series-item:hover {
160
+ color: var(--accent);
161
+ background-color: var(--muted);
162
+ }
163
+
164
+ .series-item--active {
165
+ color: var(--accent);
166
+ background-color: color-mix(in srgb, var(--accent) 8%, transparent);
167
+ border-left: 2px solid var(--accent);
168
+ padding-left: calc(0.75rem - 2px);
169
+ }
170
+
171
+ @media (prefers-reduced-motion: reduce) {
172
+ .series-pulse-dot {
173
+ animation: none;
174
+ }
175
+ }
176
+ </style>
@@ -0,0 +1,26 @@
1
+ ---
2
+ import { SHARE_LINKS } from "@/constants";
3
+ import LinkButton from "../../components/ui/LinkButton.astro";
4
+
5
+ const URL = Astro.url;
6
+ ---
7
+
8
+ {
9
+ SHARE_LINKS.length > 0 && (
10
+ <div class="flex flex-none flex-col items-center justify-center gap-1 md:items-start">
11
+ <span class="italic">Share this post on:</span>
12
+ <div class="text-center">
13
+ {SHARE_LINKS.map(social => (
14
+ <LinkButton
15
+ href={`${social.href + URL}`}
16
+ class="scale-90 p-2 hover:rotate-6 sm:p-1"
17
+ title={social.linkTitle}
18
+ >
19
+ <social.icon class="inline-block size-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110" />
20
+ <span class="sr-only">{social.linkTitle}</span>
21
+ </LinkButton>
22
+ ))}
23
+ </div>
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,37 @@
1
+ ---
2
+ import IconChevronLeft from "../../assets/icons/IconChevronLeft.svg";
3
+ import LinkButton from "../../components/ui/LinkButton.astro";
4
+ import { SITE } from "@/config";
5
+ ---
6
+
7
+ {
8
+ SITE.showBackButton && (
9
+ <div class="app-layout flex items-center justify-start">
10
+ <LinkButton
11
+ id="back-button"
12
+ href="/"
13
+ class="focus-outline -ms-2 mt-8 mb-2 hover:text-foreground/75"
14
+ >
15
+ <IconChevronLeft class="inline-block size-6 rtl:rotate-180" />
16
+ <span>Go back</span>
17
+ </LinkButton>
18
+ </div>
19
+ )
20
+ }
21
+
22
+ <script>
23
+ /* Update Search Praam */
24
+ function updateGoBackUrl() {
25
+ const backButton: HTMLAnchorElement | null =
26
+ document.querySelector("#back-button");
27
+
28
+ const backUrl = sessionStorage.getItem("backUrl");
29
+
30
+ if (backUrl && backButton) {
31
+ backButton.href = backUrl;
32
+ }
33
+ }
34
+
35
+ document.addEventListener("astro:page-load", updateGoBackUrl);
36
+ updateGoBackUrl();
37
+ </script>