@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,82 @@
1
+ ---
2
+ interface Props {
3
+ class?: string;
4
+ title: string;
5
+ }
6
+
7
+ const { class: className = "", title, ...props } = Astro.props;
8
+ ---
9
+
10
+ <collapse-component class="group/expand">
11
+ <div
12
+ class:list={[
13
+ "my-4 rounded-xl border px-3 sm:px-4 group-[.expanded]/expand:bg-muted",
14
+ className,
15
+ ]}
16
+ {...props}
17
+ >
18
+ <slot name="before" />
19
+ <div
20
+ class="group/highlight expand-title sticky top-0 z-20 flex cursor-pointer items-center justify-between py-1.5 group-[.expanded]/expand:bg-muted sm:py-2"
21
+ >
22
+ <p
23
+ class="m-0 transition-colors group-hover/highlight:text-accent"
24
+ >
25
+ {title}
26
+ </p>
27
+ <div class="expand-button">
28
+ <svg
29
+ xmlns="http://www.w3.org/2000/svg"
30
+ width="16"
31
+ height="16"
32
+ viewBox="0 0 24 24"
33
+ fill="none"
34
+ stroke-width="2.5"
35
+ stroke-linecap="round"
36
+ stroke-linejoin="round"
37
+ class="my-1 stroke-foreground-soft transition-all duration-300 group-hover/highlight:stroke-accent group-[.expanded]/expand:-rotate-90"
38
+ >
39
+ <line
40
+ x1="5"
41
+ y1="12"
42
+ x2="19"
43
+ y2="12"
44
+ class="translate-x-1 scale-x-100 duration-300 ease-in-out group-[.expanded]/expand:translate-x-4 group-[.expanded]/expand:scale-x-0"
45
+ ></line>
46
+ <polyline
47
+ points="12 5 19 12 12 19"
48
+ class="translate-x-1 duration-300 ease-in-out group-[.expanded]/expand:translate-x-0"
49
+ ></polyline>
50
+ </svg>
51
+ </div>
52
+ </div>
53
+ <div
54
+ class="expand-content grid opacity-0 transition-all duration-300 ease-in-out group-[.expanded]/expand:mb-3 group-[.expanded]/expand:opacity-100 sm:group-[.expanded]/expand:mb-4"
55
+ >
56
+ <div class="overflow-hidden">
57
+ <slot />
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </collapse-component>
62
+
63
+ <script>
64
+ class CollapseElement extends HTMLElement {
65
+ connectedCallback() {
66
+ const expandTitle = this.querySelector(".expand-title");
67
+ expandTitle?.addEventListener("click", () => {
68
+ this.classList.toggle("expanded");
69
+ });
70
+ }
71
+ }
72
+ customElements.define("collapse-component", CollapseElement);
73
+ </script>
74
+
75
+ <style>
76
+ .expand-content {
77
+ grid-template-rows: 0fr;
78
+ }
79
+ .expanded .expand-content {
80
+ grid-template-rows: 1fr;
81
+ }
82
+ </style>
@@ -0,0 +1,29 @@
1
+ ---
2
+ /**
3
+ * Color preview component
4
+ * Usage: <ColorPreview colors={["#FF6B6B", "#4ECDC4"]} />
5
+ */
6
+
7
+ interface Props {
8
+ colors: string[];
9
+ }
10
+
11
+ const { colors } = Astro.props;
12
+ ---
13
+
14
+ <div class="color-preview my-4 flex flex-wrap gap-3">
15
+ {
16
+ colors.map(color => (
17
+ <div class="flex items-center gap-2">
18
+ <span
19
+ class="color-swatch h-6 w-6 rounded-md border border-border shadow-sm"
20
+ style={`background-color: ${color}`}
21
+ title={color}
22
+ />
23
+ <code class="text-sm text-neutral-600 dark:text-neutral-400">
24
+ {color}
25
+ </code>
26
+ </div>
27
+ ))
28
+ }
29
+ </div>
@@ -0,0 +1,61 @@
1
+ ---
2
+ import dayjs from "dayjs";
3
+ import utc from "dayjs/plugin/utc";
4
+ import timezone from "dayjs/plugin/timezone";
5
+ import IconCalendar from "../../assets/icons/IconCalendar.svg";
6
+ import { SITE } from "@/config";
7
+ import { t } from "../../utils/i18n";
8
+
9
+ dayjs.extend(utc);
10
+ dayjs.extend(timezone);
11
+
12
+ type Props = {
13
+ class?: string;
14
+ size?: "sm" | "lg";
15
+ pubDatetime: string | Date;
16
+ timezone?: string;
17
+ modDatetime?: string | Date | null;
18
+ lang?: string;
19
+ };
20
+
21
+ const {
22
+ pubDatetime,
23
+ modDatetime,
24
+ size = "sm",
25
+ class: className = "",
26
+ timezone: postTimezone,
27
+ lang,
28
+ } = Astro.props;
29
+
30
+ const currentLang = lang ?? SITE.lang;
31
+ const isModified = modDatetime && modDatetime > pubDatetime;
32
+
33
+ const datetime = dayjs(isModified ? modDatetime : pubDatetime).tz(
34
+ postTimezone || SITE.timezone
35
+ );
36
+
37
+ const date =
38
+ currentLang === "zh"
39
+ ? datetime.format("YYYY 年 M 月 D 日")
40
+ : datetime.format("D MMM, YYYY");
41
+ ---
42
+
43
+ <div class:list={["flex items-center gap-x-2 opacity-80", className]}>
44
+ <IconCalendar
45
+ class:list={[
46
+ "inline-block size-6 min-w-5.5",
47
+ { "scale-90": size === "sm" },
48
+ ]}
49
+ />
50
+ {
51
+ isModified && (
52
+ <span class:list={["text-sm", { "sm:text-base": size === "lg" }]}>
53
+ {t("post.updated", currentLang)}
54
+ </span>
55
+ )
56
+ }
57
+ <time
58
+ class:list={["text-sm", { "sm:text-base": size === "lg" }]}
59
+ datetime={datetime.toISOString()}>{date}</time
60
+ >
61
+ </div>
@@ -0,0 +1,191 @@
1
+ ---
2
+ interface Props {
3
+ repo: string;
4
+ }
5
+
6
+ const { repo: repoRaw } = Astro.props;
7
+ const repo = repoRaw.replace(/^https:\/\/github\.com\//, "");
8
+ const [owner, repoName] = repo.split("/");
9
+ ---
10
+
11
+ <github-card class="not-prose loading" data-repo={repo}>
12
+ <a
13
+ href={`https://github.com/${repo}`}
14
+ target="_blank"
15
+ rel="noopener noreferrer"
16
+ class="group flex flex-col gap-y-2 rounded-xl border px-4 py-3 transition-colors hover:bg-muted sm:px-5 sm:py-4"
17
+ >
18
+ <div class="flex items-center justify-between gap-x-2">
19
+ <div
20
+ class="flex min-w-0 flex-1 items-center gap-x-2 text-foreground group-hover:text-accent"
21
+ >
22
+ <div
23
+ id="gh-avatar"
24
+ class="load-block me-2 size-7 flex-shrink-0 rounded-full bg-cover sm:size-8"
25
+ >
26
+ </div>
27
+ <div class="min-w-0 flex-1">
28
+ <div class="flex items-center gap-x-1 max-sm:flex-wrap">
29
+ <span class="truncate text-base transition-colors sm:text-lg"
30
+ >{owner}</span
31
+ >
32
+ <span class="text-foreground-soft max-sm:hidden">/</span>
33
+ <span
34
+ class="truncate text-base font-medium transition-colors sm:text-lg max-sm:w-full"
35
+ >{repoName}</span
36
+ >
37
+ </div>
38
+ </div>
39
+ </div>
40
+ <div class="flex-shrink-0 rounded-full bg-card p-1">
41
+ <svg
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ width="22"
44
+ height="22"
45
+ viewBox="0 0 24 24"
46
+ fill="currentColor"
47
+ >
48
+ <path
49
+ d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.161 22 16.416 22 12c0-5.523-4.477-10-10-10z"
50
+ ></path>
51
+ </svg>
52
+ </div>
53
+ </div>
54
+ <p id="gh-description" class="load-block text-sm sm:text-base"
55
+ >Loading...</p
56
+ >
57
+ <div class="flex items-center justify-between gap-x-2">
58
+ <div
59
+ class="load-block flex flex-wrap items-center gap-x-3 gap-y-1 sm:gap-x-5"
60
+ >
61
+ <div class="flex items-center gap-x-1.5 sm:gap-x-2">
62
+ <svg
63
+ xmlns="http://www.w3.org/2000/svg"
64
+ width="18"
65
+ height="18"
66
+ viewBox="0 0 24 24"
67
+ fill="none"
68
+ stroke="currentColor"
69
+ stroke-width="2"
70
+ stroke-linecap="round"
71
+ stroke-linejoin="round"
72
+ >
73
+ <polygon
74
+ points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
75
+ ></polygon>
76
+ </svg>
77
+ <span id="gh-stars" class="text-sm leading-tight sm:text-base"
78
+ >???</span
79
+ >
80
+ </div>
81
+ <div class="flex items-center gap-x-1.5 sm:gap-x-2">
82
+ <svg
83
+ xmlns="http://www.w3.org/2000/svg"
84
+ width="18"
85
+ height="18"
86
+ viewBox="0 0 24 24"
87
+ fill="none"
88
+ stroke="currentColor"
89
+ stroke-width="2"
90
+ stroke-linecap="round"
91
+ stroke-linejoin="round"
92
+ >
93
+ <circle cx="18" cy="18" r="3"></circle>
94
+ <circle cx="6" cy="6" r="3"></circle>
95
+ <path d="M13 6h3a2 2 0 0 1 2 2v7"></path>
96
+ <line x1="6" y1="9" x2="6" y2="21"></line>
97
+ </svg>
98
+ <span id="gh-forks" class="text-sm leading-tight sm:text-base"
99
+ >???</span
100
+ >
101
+ </div>
102
+ </div>
103
+ <span id="gh-language" class="load-block text-sm leading-tight sm:text-base"
104
+ >?????</span
105
+ >
106
+ </div>
107
+ </a>
108
+ </github-card>
109
+
110
+ <style>
111
+ @keyframes pulsate {
112
+ 0% {
113
+ opacity: 1;
114
+ }
115
+ 50% {
116
+ opacity: 0.4;
117
+ }
118
+ to {
119
+ opacity: 1;
120
+ }
121
+ }
122
+ .loading .load-block {
123
+ color: transparent;
124
+ border-radius: 0.375rem;
125
+ background-color: var(--card);
126
+ animation: pulsate 2s infinite linear;
127
+ user-select: none;
128
+ }
129
+ .loading .load-block:nth-child(2) {
130
+ animation-delay: 1s;
131
+ }
132
+ :not(.loading) #gh-avatar {
133
+ background-color: var(--card);
134
+ }
135
+ </style>
136
+
137
+ <script>
138
+ interface GithubRepoData {
139
+ stargazers_count: number;
140
+ forks: number;
141
+ language: string;
142
+ owner: { avatar_url: string };
143
+ license?: { spdx_id: string };
144
+ description: string;
145
+ }
146
+
147
+ class GithubCardElement extends HTMLElement {
148
+ async connectedCallback() {
149
+ const repo = this.dataset.repo;
150
+ if (!repo) return;
151
+ try {
152
+ const res = await fetch(`https://api.github.com/repos/${repo}`, {
153
+ referrerPolicy: "no-referrer",
154
+ });
155
+ if (!res.ok) return;
156
+ const data = (await res.json()) as GithubRepoData;
157
+
158
+ this.setText("#gh-stars", this.fmt(data.stargazers_count));
159
+ this.setText("#gh-forks", this.fmt(data.forks));
160
+ this.setText("#gh-language", data.language || "N/A");
161
+ this.setText(
162
+ "#gh-description",
163
+ data.description?.replace(/:[a-zA-Z0-9_]+:/g, "") ||
164
+ "No description"
165
+ );
166
+
167
+ const avatar = this.querySelector("#gh-avatar") as HTMLElement;
168
+ if (avatar)
169
+ avatar.style.backgroundImage = `url(${data.owner.avatar_url})`;
170
+
171
+ this.classList.remove("loading");
172
+ } catch {
173
+ this.setText("#gh-description", "Failed to fetch data");
174
+ }
175
+ }
176
+
177
+ private setText(sel: string, text: string) {
178
+ const el = this.querySelector(sel) as HTMLElement;
179
+ if (el) el.textContent = text;
180
+ }
181
+
182
+ private fmt(n: number) {
183
+ return Intl.NumberFormat("en-us", {
184
+ notation: "compact",
185
+ maximumFractionDigits: 1,
186
+ }).format(n);
187
+ }
188
+ }
189
+
190
+ customElements.define("github-card", GithubCardElement);
191
+ </script>
@@ -0,0 +1,21 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+
4
+ type Props = { disabled?: boolean } & HTMLAttributes<"a">;
5
+
6
+ const { disabled, class: className, ...attrs } = Astro.props;
7
+
8
+ const Button = disabled ? "span" : "a";
9
+ ---
10
+
11
+ <Button
12
+ aria-disabled={disabled}
13
+ class:list={[
14
+ "group inline-flex items-center gap-1",
15
+ { "hover:text-accent": !disabled },
16
+ className,
17
+ ]}
18
+ {...attrs}
19
+ >
20
+ <slot />
21
+ </Button>
@@ -0,0 +1,37 @@
1
+ ---
2
+ import IconHash from "../../assets/icons/IconHash.svg";
3
+
4
+ type Props = {
5
+ tag: string;
6
+ tagName: string;
7
+ size?: "sm" | "lg";
8
+ lang?: string;
9
+ };
10
+
11
+ const { tag, tagName, size = "lg", lang } = Astro.props;
12
+ const tagHref = lang ? `/${lang}/tags/${tag}/` : `/tags/${tag}/`;
13
+ ---
14
+
15
+ <li>
16
+ <a
17
+ href={tagHref}
18
+ transition:name={tag}
19
+ class:list={[
20
+ "flex items-center gap-0.5",
21
+ "border-b-2 border-dashed border-foreground",
22
+ "hover:-mt-0.5 hover:border-accent hover:text-accent",
23
+ "focus-visible:border-none focus-visible:text-accent",
24
+ { "text-sm": size === "sm" },
25
+ { "text-lg": size === "lg" },
26
+ ]}
27
+ >
28
+ <IconHash
29
+ class:list={[
30
+ "opacity-80",
31
+ { "size-5": size === "lg" },
32
+ { "size-4": size === "sm" },
33
+ ]}
34
+ />
35
+ {tagName}
36
+ </a>
37
+ </li>
@@ -0,0 +1,69 @@
1
+ ---
2
+ import IconHash from "../../assets/icons/IconHash.svg";
3
+
4
+ type Props = {
5
+ tag: string;
6
+ tagName: string;
7
+ count: number;
8
+ maxCount: number;
9
+ minCount: number;
10
+ lang?: string;
11
+ };
12
+
13
+ const { tag, tagName, count, maxCount, minCount, lang = "zh" } = Astro.props;
14
+
15
+ // Calculate relative size based on count
16
+ const getTagSize = (currentCount: number, max: number, min: number): string => {
17
+ if (max === min) return "text-base";
18
+
19
+ // Normalize to 0-1 range
20
+ const normalized = (currentCount - min) / (max - min);
21
+
22
+ // Map to size classes with more granular steps
23
+ if (normalized > 0.9) return "text-3xl font-bold";
24
+ if (normalized > 0.8) return "text-2xl font-semibold";
25
+ if (normalized > 0.7) return "text-xl font-semibold";
26
+ if (normalized > 0.6) return "text-lg font-medium";
27
+ if (normalized > 0.5) return "text-base font-medium";
28
+ if (normalized > 0.4) return "text-sm font-medium";
29
+ if (normalized > 0.3) return "text-sm";
30
+ if (normalized > 0.2) return "text-xs";
31
+ return "text-xs";
32
+ };
33
+
34
+ // Calculate opacity based on count
35
+ const getTagOpacity = (
36
+ currentCount: number,
37
+ max: number,
38
+ min: number
39
+ ): string => {
40
+ if (max === min) return "opacity-90";
41
+
42
+ const normalized = (currentCount - min) / (max - min);
43
+
44
+ if (normalized > 0.7) return "opacity-100";
45
+ if (normalized > 0.5) return "opacity-90";
46
+ if (normalized > 0.3) return "opacity-80";
47
+ return "opacity-70";
48
+ };
49
+
50
+ const sizeClass = getTagSize(count, maxCount, minCount);
51
+ const opacityClass = getTagOpacity(count, maxCount, minCount);
52
+ ---
53
+
54
+ <a
55
+ href={`/${lang}/tags/${tag}/`}
56
+ transition:name={tag}
57
+ class:list={[
58
+ "inline-flex items-center gap-0.5 rounded-md px-2 py-1 transition-all duration-300",
59
+ "hover:scale-105 hover:bg-accent/10 hover:text-accent",
60
+ "focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-dashed",
61
+ sizeClass,
62
+ opacityClass,
63
+ ]}
64
+ title={`${count} post${count > 1 ? "s" : ""}`}
65
+ >
66
+ <IconHash class="size-4 opacity-70" />
67
+ <span>{tagName}</span>
68
+ <span class="text-xs opacity-60">({count})</span>
69
+ </a>
@@ -0,0 +1,39 @@
1
+ ---
2
+ export interface TimelineEvent {
3
+ date: string;
4
+ content: string;
5
+ }
6
+
7
+ interface Props {
8
+ class?: string;
9
+ events: TimelineEvent[];
10
+ }
11
+
12
+ const { class: className, events, ...props } = Astro.props;
13
+ ---
14
+
15
+ <div class={className} {...props}>
16
+ <ul class="ps-0 sm:ps-2">
17
+ {
18
+ events.map((event, index) => (
19
+ <li class="group relative flex list-none gap-x-3 rounded-full ps-0 sm:gap-x-2">
20
+ <span class="z-10 my-2 ms-2 h-3 w-3 min-w-3 rounded-full border-2 border-foreground-soft transition-transform group-hover:scale-125" />
21
+ {index !== events.length - 1 && (
22
+ <span
23
+ class="absolute start-[12px] top-[20px] w-1 bg-border"
24
+ style={{ height: "calc(100% - 4px)" }}
25
+ />
26
+ )}
27
+ <div class="flex gap-2 max-sm:flex-col">
28
+ <samp class="w-fit grow-0 rounded-md py-1 text-sm max-sm:bg-card max-sm:px-2 sm:min-w-[82px] sm:text-right">
29
+ {event.date}
30
+ </samp>
31
+ <div>
32
+ <Fragment set:html={event.content} />
33
+ </div>
34
+ </div>
35
+ </li>
36
+ ))
37
+ }
38
+ </ul>
39
+ </div>
@@ -0,0 +1,24 @@
1
+ ---
2
+ import type { MarkdownLayoutProps } from "astro";
3
+ import Header from "../components/nav/Header.astro";
4
+ import Footer from "../components/nav/Footer.astro";
5
+ import Breadcrumb from "../components/nav/Breadcrumb.astro";
6
+ import Layout from "./Layout.astro";
7
+ import { SITE } from "@/config";
8
+
9
+ type Props = MarkdownLayoutProps<{ title: string }>;
10
+
11
+ const { frontmatter } = Astro.props;
12
+ ---
13
+
14
+ <Layout title={`${frontmatter.title} | ${SITE.title}`}>
15
+ <Header />
16
+ <Breadcrumb />
17
+ <main id="main-content" class="app-layout">
18
+ <section id="about" class="app-prose mb-28 max-w-app prose-img:border-0">
19
+ <h1 class="text-2xl tracking-wider sm:text-3xl">{frontmatter.title}</h1>
20
+ <slot />
21
+ </section>
22
+ </main>
23
+ <Footer />
24
+ </Layout>