@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,59 @@
1
+ ---
2
+ import type { CollectionEntry } from "astro:content";
3
+ import IconEdit from "../../assets/icons/IconEdit.svg";
4
+ import { SITE } from "@/config";
5
+
6
+ type Props = {
7
+ hideEditPost?: CollectionEntry<"blog">["data"]["hideEditPost"];
8
+ class?: string;
9
+ post: CollectionEntry<"blog">;
10
+ };
11
+
12
+ const { hideEditPost, post, class: className = "" } = Astro.props;
13
+
14
+ const editHref = `${SITE.editPost.url}${post.filePath}`;
15
+ const historyHref = SITE.editPost.url
16
+ ? `${SITE.editPost.url.replace("/edit/", "/commits/").replace(/\/$/, "")}/${post.filePath}`
17
+ : "";
18
+ const showEditPost =
19
+ SITE.editPost.enabled && !hideEditPost && editHref.trim() !== "";
20
+ ---
21
+
22
+ {
23
+ showEditPost && (
24
+ <span class:list={["flex justify-baseline gap-3", className]}>
25
+ <a
26
+ href={editHref}
27
+ target="_blank"
28
+ rel="noopener noreferrer"
29
+ class="flex items-center gap-1.5 opacity-80 hover:text-accent"
30
+ >
31
+ <IconEdit class="inline-block" />
32
+ <span>{SITE.editPost.text}</span>
33
+ </a>
34
+ {historyHref && (
35
+ <a
36
+ href={historyHref}
37
+ target="_blank"
38
+ rel="noopener noreferrer"
39
+ class="flex items-center gap-1.5 opacity-80 hover:text-accent"
40
+ title="Version history"
41
+ >
42
+ <svg
43
+ class="inline-block size-5"
44
+ viewBox="0 0 24 24"
45
+ fill="none"
46
+ stroke="currentColor"
47
+ stroke-width="2"
48
+ stroke-linecap="round"
49
+ stroke-linejoin="round"
50
+ >
51
+ <circle cx="12" cy="12" r="10" />
52
+ <polyline points="12 6 12 12 16 14" />
53
+ </svg>
54
+ <span>History</span>
55
+ </a>
56
+ )}
57
+ </span>
58
+ )
59
+ }
@@ -0,0 +1,260 @@
1
+ ---
2
+ /**
3
+ * FloatingTOC - A floating table of contents that appears on the right side
4
+ * Features:
5
+ * - Vertical list style
6
+ * - Highlights active section while scrolling
7
+ * - Smooth scrolling
8
+ * - Idle state: Minimalist bars
9
+ * - Hover state: Full text
10
+ * - Auto-expand active section
11
+ */
12
+ import { type TocHeading, buildNestedHeadings } from "../../utils/toc";
13
+
14
+ type Props = {
15
+ headings: TocHeading[];
16
+ };
17
+
18
+ const { headings } = Astro.props;
19
+
20
+ const nestedHeadings = buildNestedHeadings(headings);
21
+ ---
22
+
23
+ <aside
24
+ class="floating-toc-container group/container fixed top-32 left-[calc(50%+28rem)] z-20 hidden flex-col items-start gap-2 xl:flex"
25
+ aria-label="文章目录"
26
+ data-floating-toc
27
+ >
28
+ {/* Invisible Hover Area: Extends coverage to make hover more stable */}
29
+ <div class="absolute -inset-8 z-[-1] min-w-[200px]"></div>
30
+
31
+ <div
32
+ class="floating-toc-panel relative flex w-auto flex-col items-start gap-1 bg-transparent p-0"
33
+ >
34
+ <nav class="toc-nav max-h-[70vh] pr-1 pl-1">
35
+ <ul class="flex flex-col items-start gap-3">
36
+ {
37
+ nestedHeadings.map(heading => (
38
+ <li
39
+ class="toc-item flex flex-col items-start"
40
+ data-slug={heading.slug}
41
+ >
42
+ <a
43
+ href={`#${heading.slug}`}
44
+ class="toc-link group/link relative block flex min-h-[16px] items-center transition-all duration-300"
45
+ data-toc-slug={heading.slug}
46
+ >
47
+ {/* Bar: Visible in Idle, transitions to 0 width in Container Hover */}
48
+ <span class="toc-bar block h-1 w-[48px] rounded-sm bg-muted transition-all duration-300 group-hover/container:w-0 group-hover/container:opacity-0 group-hover/link:bg-accent group-data-[active=true]/link:bg-accent" />
49
+
50
+ {/* Text: Hidden in Idle, Visible in Container Hover */}
51
+ <span class="toc-text text-muted-foreground pointer-events-none absolute left-0 -translate-x-2 text-sm whitespace-nowrap opacity-0 transition-all duration-300 group-hover/container:pointer-events-auto group-hover/container:translate-x-0 group-hover/container:opacity-100 group-hover/link:text-accent group-data-[active=true]/link:font-medium group-data-[active=true]/link:text-accent">
52
+ {heading.text}
53
+ </span>
54
+ </a>
55
+
56
+ {heading.children.length > 0 && (
57
+ <ul class="toc-sublist mt-3 hidden flex-col items-start gap-2 border-l border-border/0 pl-1">
58
+ {heading.children.map((subHeading: TocHeading) => (
59
+ <li data-slug={subHeading.slug} class="flex justify-start">
60
+ <a
61
+ href={`#${subHeading.slug}`}
62
+ class="toc-link group/link relative block flex min-h-[16px] items-center transition-all duration-300"
63
+ data-toc-slug={subHeading.slug}
64
+ >
65
+ {/* Sub-item Bar: shorter width */}
66
+ <span class="toc-bar block h-1 w-[38px] rounded-sm bg-muted transition-all duration-300 group-hover/container:w-0 group-hover/container:opacity-0 group-hover/link:bg-accent group-data-[active=true]/link:bg-accent" />
67
+
68
+ {/* Sub-item Text */}
69
+ <span class="toc-text text-muted-foreground pointer-events-none absolute left-0 -translate-x-2 text-xs whitespace-nowrap opacity-0 transition-all duration-300 group-hover/container:pointer-events-auto group-hover/container:translate-x-0 group-hover/container:opacity-100 group-hover/link:text-accent group-data-[active=true]/link:font-medium group-data-[active=true]/link:text-accent">
70
+ {subHeading.text}
71
+ </span>
72
+ </a>
73
+ </li>
74
+ ))}
75
+ </ul>
76
+ )}
77
+ </li>
78
+ ))
79
+ }
80
+ </ul>
81
+ </nav>
82
+ </div>
83
+ </aside>
84
+
85
+ <style>
86
+ /* Ensure sublists are hidden by default */
87
+ .toc-sublist {
88
+ display: none;
89
+ }
90
+
91
+ /* Show sublist when parent is active */
92
+ .toc-item.active .toc-sublist {
93
+ display: flex;
94
+ animation: fadeIn 0.3s ease;
95
+ }
96
+
97
+ /* Also show sublist if a child is active */
98
+ .toc-item:has(.toc-sublist .toc-link.active) .toc-sublist {
99
+ display: flex;
100
+ }
101
+
102
+ @keyframes fadeIn {
103
+ from {
104
+ opacity: 0;
105
+ transform: translateY(-2px);
106
+ }
107
+ to {
108
+ opacity: 1;
109
+ transform: translateY(0);
110
+ }
111
+ }
112
+
113
+ /* Scrollbar hiding */
114
+ .toc-nav {
115
+ scrollbar-width: none; /* Firefox */
116
+ -ms-overflow-style: none; /* IE/Edge */
117
+ }
118
+ .toc-nav::-webkit-scrollbar {
119
+ display: none; /* Chrome/Safari/Opera */
120
+ }
121
+ </style>
122
+
123
+ <script>
124
+ function initFloatingTOC() {
125
+ const tocContainer = document.querySelector("[data-floating-toc]");
126
+ if (!tocContainer) return;
127
+
128
+ const tocLinks =
129
+ tocContainer.querySelectorAll<HTMLAnchorElement>("a[data-toc-slug]");
130
+ const tocItems = tocContainer.querySelectorAll(".toc-item");
131
+ const indicator = tocContainer.querySelector<HTMLElement>(".toc-progress");
132
+
133
+ // Intersection Observer
134
+ const observerOptions = {
135
+ root: null,
136
+ rootMargin: "-100px 0px -66% 0px",
137
+ threshold: 0,
138
+ };
139
+
140
+ const headingElements = new Map<string, Element>();
141
+
142
+ tocLinks.forEach(link => {
143
+ const slug = link.dataset.tocSlug;
144
+ if (slug) {
145
+ const heading = document.getElementById(slug);
146
+ if (heading) {
147
+ headingElements.set(slug, heading);
148
+ }
149
+ }
150
+ });
151
+
152
+ const observer = new IntersectionObserver(entries => {
153
+ entries.forEach(entry => {
154
+ if (entry.isIntersecting) {
155
+ setActive(entry.target.id);
156
+ }
157
+ });
158
+ }, observerOptions);
159
+
160
+ headingElements.forEach(element => {
161
+ observer.observe(element);
162
+ });
163
+
164
+ function setActive(activeSlug: string) {
165
+ // 1. Update Active Links
166
+ tocLinks.forEach(link => {
167
+ const isActive = link.dataset.tocSlug === activeSlug;
168
+ if (isActive) {
169
+ link.setAttribute("data-active", "true");
170
+ link.classList.add("active");
171
+ } else {
172
+ link.removeAttribute("data-active");
173
+ link.classList.remove("active");
174
+ }
175
+ });
176
+
177
+ // 2. Update Active Items (for sublist expansion)
178
+ tocItems.forEach(item => {
179
+ const slug = item.getAttribute("data-slug");
180
+ const hasActiveChild = item.querySelector(`.toc-link.active`);
181
+
182
+ // If this item is active OR has an active child, mark as active
183
+ if (slug === activeSlug || hasActiveChild) {
184
+ item.classList.add("active");
185
+ } else {
186
+ // Only remove if it's not the parent of the active item
187
+ // Actually, we re-evaluate all.
188
+ // Simpler: Just remove active from all, then add to the one matching activeSlug's parent h2
189
+ item.classList.remove("active");
190
+ }
191
+ });
192
+
193
+ // Re-apply active to parent of currently active link
194
+ if (activeSlug && tocContainer) {
195
+ const activeLink = tocContainer.querySelector(
196
+ `.toc-link[data-toc-slug="${activeSlug}"]`
197
+ );
198
+ if (activeLink) {
199
+ const parentItem = activeLink.closest(".toc-item");
200
+ if (parentItem) {
201
+ parentItem.classList.add("active");
202
+ }
203
+ }
204
+ }
205
+ if (indicator) {
206
+ const winScroll =
207
+ document.body.scrollTop || document.documentElement.scrollTop;
208
+ const height =
209
+ document.documentElement.scrollHeight -
210
+ document.documentElement.clientHeight;
211
+ const scrolled = (winScroll / height) * 100;
212
+ indicator.style.height = `${scrolled}%`;
213
+ }
214
+ }
215
+
216
+ // Scroll Progress Update (Continuous)
217
+ document.addEventListener("scroll", () => {
218
+ if (indicator) {
219
+ const winScroll =
220
+ document.body.scrollTop || document.documentElement.scrollTop;
221
+ const height =
222
+ document.documentElement.scrollHeight -
223
+ document.documentElement.clientHeight;
224
+ const scrolled = (winScroll / height) * 100;
225
+ indicator.style.height = `${scrolled}%`;
226
+ }
227
+ });
228
+
229
+ // Smooth Scroll Click Handler
230
+ tocLinks.forEach(link => {
231
+ link.addEventListener("click", e => {
232
+ e.preventDefault();
233
+ const slug = link.dataset.tocSlug;
234
+ if (slug) {
235
+ const heading = document.getElementById(slug);
236
+ if (heading) {
237
+ history.pushState(null, "", `#${slug}`);
238
+ const headerOffset = 80;
239
+ const elementPosition = heading.getBoundingClientRect().top;
240
+ const offsetPosition =
241
+ elementPosition + window.pageYOffset - headerOffset;
242
+ window.scrollTo({
243
+ top: offsetPosition,
244
+ behavior: "smooth",
245
+ });
246
+ setActive(slug);
247
+ }
248
+ }
249
+ });
250
+ });
251
+
252
+ // Cleanup
253
+ document.addEventListener("astro:before-swap", () => {
254
+ observer.disconnect();
255
+ });
256
+ }
257
+
258
+ document.addEventListener("astro:page-load", initFloatingTOC);
259
+ initFloatingTOC();
260
+ </script>
@@ -0,0 +1,223 @@
1
+ ---
2
+ /**
3
+ * InlineTOC - An expandable table of contents below the title
4
+ * Features:
5
+ * - Expand/collapse toggle
6
+ * - Nested heading structure
7
+ * - Click to navigate
8
+ */
9
+ import { type TocHeading, buildNestedHeadings } from "../../utils/toc";
10
+
11
+ type Props = {
12
+ headings: TocHeading[];
13
+ };
14
+
15
+ const { headings } = Astro.props;
16
+
17
+ const nestedHeadings = buildNestedHeadings(headings);
18
+ ---
19
+
20
+ <div
21
+ class="inline-toc bg-card/50 mt-8 mb-8 rounded-lg border border-border backdrop-blur-sm"
22
+ data-inline-toc
23
+ >
24
+ <button
25
+ type="button"
26
+ class="inline-toc-toggle flex w-full items-center justify-between rounded-lg px-4 py-3 text-sm font-medium text-foreground transition-colors hover:bg-muted/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
27
+ aria-expanded="false"
28
+ aria-controls="inline-toc-content"
29
+ data-toc-toggle
30
+ >
31
+ <div class="flex items-center gap-2">
32
+ <svg
33
+ class="text-muted-foreground h-4 w-4"
34
+ viewBox="0 0 24 24"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ stroke-width="2"
38
+ >
39
+ <path d="M3 12h18M3 6h18M3 18h18"></path>
40
+ </svg>
41
+ <span>目录</span>
42
+ <span
43
+ class="text-muted-foreground rounded-md bg-muted px-1.5 py-0.5 text-xs"
44
+ >
45
+ {nestedHeadings.length} 章节
46
+ </span>
47
+ </div>
48
+ <svg
49
+ class="text-muted-foreground h-4 w-4 transition-transform duration-200"
50
+ viewBox="0 0 24 24"
51
+ fill="none"
52
+ stroke="currentColor"
53
+ stroke-width="2"
54
+ data-toggle-icon
55
+ >
56
+ <path d="M6 9l6 6 6-6"></path>
57
+ </svg>
58
+ </button>
59
+
60
+ <div
61
+ id="inline-toc-content"
62
+ class="inline-toc-content grid overflow-hidden transition-[grid-template-rows] duration-300 ease-out"
63
+ style="grid-template-rows: 0fr;"
64
+ data-toc-content
65
+ >
66
+ <nav class="bg-card/30 min-h-0 border-t border-border">
67
+ <ul class="space-y-1 px-4 py-2">
68
+ {
69
+ nestedHeadings.map(heading => (
70
+ <li>
71
+ <div class="group flex items-center justify-between">
72
+ <a
73
+ href={`#${heading.slug}`}
74
+ class="inline-toc-link text-muted-foreground block flex-1 truncate rounded-md px-2 py-1.5 text-sm font-medium transition-colors hover:bg-accent/5 hover:text-accent"
75
+ data-toc-slug={heading.slug}
76
+ data-toc-depth={heading.depth}
77
+ >
78
+ {heading.text}
79
+ </a>
80
+ {heading.children.length > 0 && (
81
+ <button
82
+ type="button"
83
+ class="text-muted-foreground rounded-md p-1 transition-colors hover:text-accent"
84
+ aria-label="Toggle subheadings"
85
+ onclick={`this.parentElement.nextElementSibling.classList.toggle('hidden'); this.querySelector('svg').classList.toggle('rotate-180')`}
86
+ >
87
+ <svg
88
+ class="h-3.5 w-3.5 transition-transform duration-200"
89
+ viewBox="0 0 24 24"
90
+ fill="none"
91
+ stroke="currentColor"
92
+ stroke-width="2"
93
+ >
94
+ <path d="M6 9l6 6 6-6" />
95
+ </svg>
96
+ </button>
97
+ )}
98
+ </div>
99
+
100
+ {heading.children.length > 0 && (
101
+ <ul class="mt-1 ml-2 hidden space-y-1 border-l border-border/50 pl-4">
102
+ {heading.children.map((subHeading: TocHeading) => (
103
+ <li>
104
+ <a
105
+ href={`#${subHeading.slug}`}
106
+ class="inline-toc-link text-muted-foreground block truncate rounded-md px-2 py-1 text-xs transition-colors hover:bg-accent/5 hover:text-accent"
107
+ data-toc-slug={subHeading.slug}
108
+ data-toc-depth={subHeading.depth}
109
+ >
110
+ {subHeading.text}
111
+ </a>
112
+ </li>
113
+ ))}
114
+ </ul>
115
+ )}
116
+ </li>
117
+ ))
118
+ }
119
+ </ul>
120
+ </nav>
121
+ </div>
122
+ </div>
123
+
124
+ <style>
125
+ .inline-toc-toggle[aria-expanded="true"] [data-toggle-icon] {
126
+ transform: rotate(180deg);
127
+ }
128
+
129
+ .inline-toc-content[data-expanded="true"] {
130
+ grid-template-rows: 1fr;
131
+ }
132
+
133
+ .inline-toc-link.active {
134
+ color: var(--accent);
135
+ background-color: color-mix(in srgb, var(--accent) 10%, transparent);
136
+ }
137
+ </style>
138
+
139
+ <script>
140
+ function initInlineTOC() {
141
+ // Only initialize once per container
142
+ const tocContainers = document.querySelectorAll("[data-inline-toc]");
143
+
144
+ tocContainers.forEach(tocContainer => {
145
+ if (tocContainer.hasAttribute("data-initialized")) return;
146
+ tocContainer.setAttribute("data-initialized", "true");
147
+
148
+ const toggle = tocContainer.querySelector("[data-toc-toggle]");
149
+ const content = tocContainer.querySelector("[data-toc-content]");
150
+ const links = tocContainer.querySelectorAll(".inline-toc-link");
151
+
152
+ if (!toggle || !content) return;
153
+
154
+ // Toggle expand/collapse
155
+ const handleToggle = (e: Event) => {
156
+ e.preventDefault();
157
+ e.stopPropagation();
158
+ const isExpanded = toggle.getAttribute("aria-expanded") === "true";
159
+ toggle.setAttribute("aria-expanded", isExpanded ? "false" : "true");
160
+ content.setAttribute("data-expanded", isExpanded ? "false" : "true");
161
+ (content as HTMLElement).style.gridTemplateRows = isExpanded
162
+ ? "0fr"
163
+ : "1fr";
164
+ };
165
+
166
+ toggle.addEventListener("click", handleToggle);
167
+
168
+ // Track active section (optional for InlineTOC, but good for consistency)
169
+ const observerOptions = {
170
+ root: null,
171
+ rootMargin: "-80px 0px -70% 0px",
172
+ threshold: 0,
173
+ };
174
+
175
+ const headingElements = new Map();
176
+
177
+ links.forEach(link => {
178
+ const slug = (link as HTMLElement).dataset.tocSlug;
179
+ if (slug) {
180
+ const heading = document.getElementById(slug);
181
+ if (heading) {
182
+ headingElements.set(slug, heading);
183
+ }
184
+ }
185
+ });
186
+
187
+ const observer = new IntersectionObserver(entries => {
188
+ entries.forEach(entry => {
189
+ const slug = entry.target.id;
190
+
191
+ if (entry.isIntersecting) {
192
+ updateActiveLink(slug);
193
+ }
194
+ });
195
+ }, observerOptions);
196
+
197
+ headingElements.forEach(element => {
198
+ observer.observe(element);
199
+ });
200
+
201
+ function updateActiveLink(activeSlug: string) {
202
+ links.forEach(link => {
203
+ const isActive = (link as HTMLElement).dataset.tocSlug === activeSlug;
204
+ link.classList.toggle("active", isActive);
205
+ });
206
+ }
207
+
208
+ // Clean up observer when navigating away (Astro specific)
209
+ document.addEventListener(
210
+ "astro:before-swap",
211
+ () => {
212
+ observer.disconnect();
213
+ },
214
+ { once: true }
215
+ );
216
+ });
217
+ }
218
+
219
+ // Handle initial load and view transitions
220
+ document.addEventListener("astro:page-load", initInlineTOC);
221
+ // Also run on DOMContentLoaded for first paint if not using View Transitions or as fallback
222
+ document.addEventListener("DOMContentLoaded", initInlineTOC);
223
+ </script>