@@ -5,25 +5,52 @@ interface Props {
5
5
slug: string ;
6
6
text: string ;
7
7
}[];
8
+ maxDepth? : number ;
9
+ className? : string ;
8
10
}
9
11
10
- const { headings } = Astro .props ;
12
+ const {
13
+ headings,
14
+ maxDepth = 3 ,
15
+ className = ' '
16
+ } = Astro .props ;
11
17
12
- const filteredHeadings = headings .filter (heading => heading .depth <= 3 );
18
+ const filteredHeadings = headings .filter (heading => heading .depth <= maxDepth );
19
+
20
+ const depthClasses = {
21
+ 1 : ' pl-0' ,
22
+ 2 : ' pl-4' ,
23
+ 3 : ' pl-8' ,
24
+ 4 : ' pl-12'
25
+ };
13
26
---
14
27
15
- <nav class =" toc hidden lg:block" aria-label =" Table of Contents" >
16
- <div class =" sticky top-8 max-h-[calc(100vh-4rem)] overflow-y-auto" >
17
- <h2 class =" text-sm font-medium text-gray-500 dark:text-gray-400 mb-4" >Table of Contents</h2 >
28
+ <nav
29
+ class:list ={ [
30
+ ' toc hidden lg:block' ,
31
+ className
32
+ ]}
33
+ aria-label =" Table of Contents"
34
+ >
35
+ <div class =" sticky top-8 bottom-16 max-h-[calc(100vh-8rem)] overflow-y-auto pb-8 pr-2" >
36
+ <h2 class =" text-sm font-medium text-gray-500 dark:text-gray-400 mb-4" >
37
+ Table of Contents
38
+ </h2 >
18
39
<ul class =" space-y-2 text-sm" >
19
40
{ filteredHeadings .map (heading => (
20
- <li class = { ` pl-${( heading .depth - 1 ) * 4 } ` } >
21
- <a
41
+ <li class = { depthClasses [ heading .depth ] } >
42
+ <a
22
43
href = { ` #${heading .slug } ` }
23
- class = { `
24
- block py-1 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200
25
- ${heading .depth === 1 ? ' font-medium' : ' font-normal' }
26
- ` }
44
+ class :list = { [
45
+ ' toc-item block py-1 transition-colors duration-150' ,
46
+ ' text-gray-600 dark:text-gray-400' ,
47
+ ' hover:text-gray-900 dark:hover:text-gray-200' ,
48
+ {
49
+ ' font-medium' : heading .depth === 1 ,
50
+ ' font-normal' : heading .depth > 1
51
+ }
52
+ ]}
53
+ data-heading = { heading .slug }
27
54
>
28
55
{ heading .text }
29
56
</a >
@@ -35,50 +62,116 @@ const filteredHeadings = headings.filter(heading => heading.depth <= 3);
35
62
36
63
<style >
37
64
.toc {
38
- width: 16rem;
39
- position: fixed;
40
- left: max(2rem, calc((100vw - 60ch - 28rem) / 2));
41
- transform: translateX(-100%);
42
- padding-right: 2rem;
65
+ width: clamp(14rem, 16rem, 20vw) ;
66
+ position: fixed;
67
+ left: max(2rem, calc((100vw - 60ch - 28rem) / 2));
68
+ transform: translateX(-100%);
69
+ padding-right: 2rem;
43
70
}
44
71
45
72
@media (max-width: 1300px) {
46
- .toc {
47
- left: 2rem;
48
- transform: none;
49
- padding-right: 1rem;
50
- width: 14rem;
51
- }
73
+ .toc {
74
+ left: 2rem;
75
+ transform: none;
76
+ padding-right: 1rem;
77
+ width: 14rem;
78
+ }
52
79
}
53
80
54
81
@media (max-width: 1024px) {
55
- .toc {
56
- display: none;
57
- }
82
+ .toc {
83
+ display: none;
84
+ }
85
+ }
86
+
87
+ .toc div::-webkit-scrollbar {
88
+ width: 6px;
89
+ }
90
+
91
+ .toc div::-webkit-scrollbar-track {
92
+ background: transparent;
93
+ }
94
+
95
+ .toc div::-webkit-scrollbar-thumb {
96
+ background-color: rgb(156 163 175 / 0.2);
97
+ border-radius: 3px;
98
+ }
99
+
100
+ :global(.dark) .toc div::-webkit-scrollbar-thumb {
101
+ background-color: rgb(75 85 99 / 0.3);
102
+ }
103
+
104
+ .toc div::-webkit-scrollbar-thumb:hover {
105
+ background-color: rgb(156 163 175 / 0.3);
106
+ }
107
+
108
+ :global(.dark) .toc div::-webkit-scrollbar-thumb:hover {
109
+ background-color: rgb(75 85 99 / 0.4);
110
+ }
111
+
112
+ /* Active state styling */
113
+ .toc-item.active {
114
+ @apply text-blue-600 dark:text-blue-400 font-medium;
58
115
}
59
116
</style >
60
117
61
118
<script >
62
- const observerOptions = {
63
- root: null,
64
- rootMargin: '0px',
65
- threshold: 1.0
66
- };
119
+ function updateActiveHeading() {
120
+ const headings = Array.from(document.querySelectorAll('h1[id], h2[id], h3[id]'));
121
+ const tocItems = document.querySelectorAll('.toc-item');
67
122
68
- const observer = new IntersectionObserver(entries => {
69
- entries.forEach(entry => {
70
- const id = entry.target.getAttribute('id');
71
- if (entry.isIntersecting) {
72
- document.querySelector(`nav.toc a[href="#${id}"]`)
73
- ?.classList.add('text-blue-600', 'dark:text-blue-400');
74
- } else {
75
- document.querySelector(`nav.toc a[href="#${id}"]`)
76
- ?.classList.remove('text-blue-600', 'dark:text-blue-400');
123
+ const observer = new IntersectionObserver(
124
+ (entries) => {
125
+ const visibleHeadings = entries
126
+ .filter(entry => entry.isIntersecting)
127
+ .sort((a, b) => {
128
+ return a.target.getBoundingClientRect().top - b.target.getBoundingClientRect().top;
129
+ });
130
+
131
+ if (visibleHeadings.length > 0) {
132
+ const activeHeading = visibleHeadings[0];
133
+ tocItems.forEach(item => item.classList.remove('active'));
134
+
135
+ const activeItem = document.querySelector(
136
+ `.toc-item[data-heading="${activeHeading.target.id}"]`
137
+ );
138
+ if (activeItem) {
139
+ activeItem.classList.add('active');
140
+ }
141
+ }
142
+ },
143
+ {
144
+ rootMargin: '-64px 0px -70% 0px',
145
+ threshold: [0, 1]
77
146
}
78
- });
79
- }, observerOptions);
147
+ );
80
148
81
- document.querySelectorAll('h1[id], h2[id], h3[id]').forEach((header) => {
82
- observer.observe(header);
149
+ headings.forEach(heading => observer.observe(heading));
150
+ return () => observer.disconnect();
151
+ }
152
+
153
+ updateActiveHeading();
154
+ document.addEventListener('astro:after-swap', updateActiveHeading);
155
+
156
+ document.querySelectorAll('.toc-item').forEach(link => {
157
+ link.addEventListener('click', (e) => {
158
+ e.preventDefault();
159
+ const targetId = link.getAttribute('href')?.slice(1);
160
+ if (targetId) {
161
+ const targetElement = document.getElementById(targetId);
162
+ if (targetElement) {
163
+ const headerOffset = 32;
164
+ const elementPosition = targetElement.getBoundingClientRect().top;
165
+ const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
166
+
167
+ window.scrollTo({
168
+ top: offsetPosition,
169
+ behavior: 'smooth'
170
+ });
171
+
172
+ history.pushState(null, '', `#${targetId}`);
173
+ }
174
+ }
175
+ });
83
176
});
84
177
</script >
0 commit comments