Fork of Chiri for Astro for my blog
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add toc component; fix footnote and back button positions

the3ash 5075638d fdfbf8e7

+325 -6
+15 -5
src/components/features/FootnoteScroll.astro
··· 1 1 <script> 2 - document.addEventListener('astro:page-load', () => { 2 + function bindFootnoteEvents() { 3 3 const footnoteLinks = document.querySelectorAll('[data-footnote-ref], [data-footnote-backref]') 4 4 5 5 footnoteLinks.forEach((link) => { ··· 11 11 const target = document.querySelector(href) 12 12 if (!target) return 13 13 14 - // Calculate scroll position with offset (6 * 16px = 96px from top) 15 - const targetPosition = target.getBoundingClientRect().top + window.scrollY - 6 * 16 14 + // Use fixed offset of 128px 15 + const offset = 128 16 + 17 + const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset 18 + 16 19 window.scrollTo({ 17 - top: targetPosition, 18 - behavior: 'smooth' 20 + top: targetPosition 19 21 }) 20 22 }) 21 23 }) 24 + } 25 + 26 + document.addEventListener('astro:page-load', () => { 27 + bindFootnoteEvents() 28 + }) 29 + 30 + document.addEventListener('DOMContentLoaded', () => { 31 + bindFootnoteEvents() 22 32 }) 23 33 </script>
+1 -1
src/components/ui/BackButton.astro
··· 35 35 const pageWidth = window.innerWidth 36 36 const contentWidthValue = parseFloat(contentWidth) 37 37 const margin = (pageWidth - contentWidthValue * 16) / 2 38 - const minSpace = 9 * 16 // Minimum space needed 38 + const minSpace = 11 * 16 // Minimum space needed 39 39 40 40 // Position button fixed on the left if there's enough space 41 41 if (margin >= minSpace) {
+304
src/components/ui/TableOfContents.astro
··· 1 + --- 2 + import { SITE } from '@/config' 3 + --- 4 + 5 + <div class="toc-container" id="toc"> 6 + <nav class="toc-nav"> 7 + <ul class="toc-list" id="toc-list"></ul> 8 + </nav> 9 + </div> 10 + 11 + <script 12 + is:inline 13 + define:vars={{ contentWidth: SITE.contentWidth, centeredLayout: SITE.centeredLayout }} 14 + > 15 + ;(function () { 16 + // TOC positioning logic (similar to BackButton) 17 + function adjustTOCPosition() { 18 + const toc = document.querySelector('.toc-container') 19 + if (!toc) return 20 + 21 + // If not using centered layout, hide TOC 22 + if (!centeredLayout) { 23 + toc.style.display = 'none' 24 + return 25 + } 26 + 27 + // Calculate available margin space for positioning 28 + const pageWidth = window.innerWidth 29 + const contentWidthValue = parseFloat(contentWidth) 30 + const margin = (pageWidth - contentWidthValue * 16) / 2 31 + const minSpace = 11 * 16 // Minimum space needed 32 + 33 + // Show and position TOC fixed on the left if there's enough space 34 + if (margin >= minSpace) { 35 + toc.style.display = 'block' 36 + toc.classList.add('fixed-position') 37 + toc.style.left = `${margin - minSpace}px` 38 + } else { 39 + toc.style.display = 'none' 40 + toc.classList.remove('fixed-position') 41 + toc.style.left = '' 42 + } 43 + } 44 + 45 + // Extract headings and paragraphs and create TOC 46 + function createTOC() { 47 + const tocList = document.getElementById('toc-list') 48 + if (!tocList) return 49 + 50 + const tocItems = [] 51 + 52 + // Get all content elements in document order 53 + const allElements = Array.from(document.querySelectorAll('h1, h2, h3')) 54 + 55 + // Process elements in document order 56 + allElements.forEach((element, index) => { 57 + if (element.tagName === 'H1' || element.tagName === 'H2' || element.tagName === 'H3') { 58 + // Skip the main title (first h1) 59 + if (index === 0 && element.tagName === 'H1') return 60 + 61 + const level = parseInt(element.tagName.charAt(1)) 62 + if (level > 3) return // Only include h1, h2, h3 63 + 64 + const text = element.textContent || '' 65 + const id = element.id || `heading-${index}` 66 + element.id = id 67 + 68 + tocItems.push({ 69 + level, 70 + text, 71 + id, 72 + element 73 + }) 74 + } 75 + }) 76 + 77 + // Clear existing TOC 78 + tocList.innerHTML = '' 79 + 80 + // Add title link at the top 81 + const titleLi = document.createElement('li') 82 + titleLi.className = 'toc-item toc-level-0' 83 + 84 + const titleLink = document.createElement('a') 85 + titleLink.href = '#' 86 + titleLink.className = 'toc-link toc-title' 87 + titleLink.setAttribute('title', 'Back to top') 88 + titleLink.setAttribute('data-text', 'Back to top') 89 + 90 + // Add click handler to scroll to top 91 + titleLink.addEventListener('click', (e) => { 92 + e.preventDefault() 93 + window.scrollTo({ top: 0, behavior: 'smooth' }) 94 + // Update URL without page jump 95 + history.pushState(null, null, '#') 96 + }) 97 + 98 + titleLi.appendChild(titleLink) 99 + tocList.appendChild(titleLi) 100 + 101 + // Create TOC items 102 + tocItems.forEach((item) => { 103 + const li = document.createElement('li') 104 + li.className = `toc-item toc-level-${item.level}` 105 + 106 + const link = document.createElement('a') 107 + link.href = `#${item.id}` 108 + link.className = 'toc-link' 109 + link.setAttribute('title', item.text) // Add title for accessibility 110 + link.setAttribute('data-text', item.text) // Add data-text for CSS content 111 + link.textContent = item.text // Add text content for hover display 112 + 113 + link.addEventListener('click', (e) => { 114 + e.preventDefault() 115 + const target = document.getElementById(item.id) 116 + if (target) { 117 + const rect = target.getBoundingClientRect() 118 + const scrollTop = window.pageYOffset || document.documentElement.scrollTop 119 + const offset = rect.top + scrollTop - 96 // 6rem = 96px 120 + window.scrollTo({ top: offset, behavior: 'smooth' }) 121 + history.pushState(null, null, `#${item.id}`) 122 + } 123 + }) 124 + 125 + li.appendChild(link) 126 + tocList.appendChild(li) 127 + }) 128 + 129 + updateActiveTOCItem() 130 + } 131 + 132 + // Update active TOC item based on scroll position 133 + function updateActiveTOCItem() { 134 + const tocLinks = document.querySelectorAll('.toc-link') 135 + const headings = document.querySelectorAll('h1, h2, h3') 136 + 137 + let currentActive = null 138 + const scrollTop = window.pageYOffset + 100 // Offset for better detection 139 + 140 + // Check headings 141 + headings.forEach((heading, index) => { 142 + if (index === 0 && heading.tagName === 'H1') return 143 + 144 + const rect = heading.getBoundingClientRect() 145 + const offsetTop = rect.top + window.pageYOffset 146 + 147 + if (scrollTop >= offsetTop) { 148 + currentActive = heading.id 149 + } 150 + }) 151 + 152 + tocLinks.forEach((link) => { 153 + link.classList.remove('active') 154 + if (link.getAttribute('href') === `#${currentActive}`) { 155 + link.classList.add('active') 156 + } 157 + }) 158 + 159 + // If no active item found and we're at the top, activate the title link 160 + if (!currentActive && window.pageYOffset < 200) { 161 + const titleLink = document.querySelector('.toc-link.toc-title') 162 + if (titleLink) { 163 + titleLink.classList.add('active') 164 + } 165 + } 166 + } 167 + 168 + function initTOC() { 169 + adjustTOCPosition() 170 + createTOC() 171 + } 172 + 173 + document.addEventListener('astro:page-load', () => { 174 + initTOC() 175 + }) 176 + 177 + document.addEventListener('DOMContentLoaded', () => { 178 + initTOC() 179 + }) 180 + 181 + window.addEventListener('resize', adjustTOCPosition) 182 + window.addEventListener('scroll', updateActiveTOCItem) 183 + })() 184 + </script> 185 + 186 + <style is:global> 187 + .toc-container { 188 + width: 128px; 189 + position: relative; 190 + left: -0.175em; 191 + } 192 + 193 + .toc-nav { 194 + font-family: var(--font-serif); 195 + font-size: var(--font-size-s); 196 + line-height: 1.5; 197 + } 198 + 199 + .toc-list { 200 + list-style: none; 201 + list-style-type: none; 202 + margin: 0 !important; 203 + padding: 0 !important; 204 + } 205 + 206 + .toc-list li { 207 + margin: 0 !important; 208 + padding: 0 !important; 209 + } 210 + 211 + .toc-item { 212 + list-style: none; 213 + list-style-type: none; 214 + } 215 + 216 + .toc-item::before { 217 + display: none; 218 + } 219 + 220 + .toc-item::marker { 221 + display: none; 222 + } 223 + 224 + .toc-link { 225 + display: block; 226 + color: transparent; 227 + text-decoration: none; 228 + transition: all 0.2s ease-out; 229 + position: relative; 230 + padding-left: 0; 231 + height: 1.125rem; 232 + width: 100%; 233 + min-height: 1rem; 234 + font-size: 0; 235 + line-height: 1.125rem; 236 + text-indent: 2rem; 237 + overflow: hidden; 238 + white-space: nowrap; 239 + text-overflow: ellipsis; 240 + } 241 + 242 + .toc-link:hover { 243 + font-family: var(--font-sans); 244 + font-size: var(--font-size-s); 245 + letter-spacing: var(--spacing-m); 246 + text-indent: 1.125rem; 247 + text-decoration: none; 248 + } 249 + 250 + .toc-level-1 .toc-link:hover::before, 251 + .toc-level-2 .toc-link:hover::before, 252 + .toc-level-3 .toc-link:hover::before { 253 + width: 0.75rem; 254 + } 255 + 256 + .toc-level-0 .toc-link:hover::after, 257 + .toc-level-1 .toc-link:hover::after, 258 + .toc-level-2 .toc-link:hover::after, 259 + .toc-level-3 .toc-link:hover::after { 260 + opacity: 1; 261 + } 262 + 263 + .toc-link.active { 264 + color: var(--text-primary); 265 + } 266 + 267 + /* Horizontal line indicators */ 268 + .toc-level-0 .toc-link::before, 269 + .toc-level-1 .toc-link::before, 270 + .toc-level-2 .toc-link::before, 271 + .toc-level-3 .toc-link::before { 272 + content: ''; 273 + position: absolute; 274 + left: 0; 275 + top: 50%; 276 + width: 2.5rem; 277 + height: 1px; 278 + background-color: var(--text-tertiary); 279 + transform: translateY(-50%); 280 + opacity: 0.4; 281 + } 282 + 283 + .toc-link:hover::before, 284 + .toc-link.active::before { 285 + opacity: 0.8; 286 + background-color: var(--text-primary); 287 + } 288 + 289 + /* Fixed positioning */ 290 + .toc-container.fixed-position { 291 + position: fixed; 292 + top: 12rem; /* Position below BackButton */ 293 + margin-top: 0; 294 + padding-left: 1rem; 295 + z-index: 10; 296 + } 297 + 298 + /* Hide on mobile and when space is limited */ 299 + @media (max-width: 768px) { 300 + .toc-container { 301 + display: none !important; 302 + } 303 + } 304 + </style>
+1
src/config.ts
··· 21 21 22 22 // POST SETTINGS /////////////////////////////////////////////////////////////////////////////////////// 23 23 readingTime: false, // Show reading time in posts 24 + toc: true, // Show table of contents 24 25 imageViewer: true, // Enable image viewer 25 26 copyCode: false // Enable copy button in code blocks 26 27 }
+2
src/content/posts/theme-guide.md
··· 53 53 54 54 // Show reading time in posts 55 55 readingTime: false, 56 + // Show table of contents 57 + toc: true, 56 58 // Enable image viewer 57 59 imageViewer: true, 58 60 // Enable copy button in code blocks
+2
src/layouts/PostLayout.astro
··· 6 6 import BaseHead from '@/components/layout/BaseHead.astro' 7 7 import Footer from '@/components/layout/Footer.astro' 8 8 import BackButton from '@/components/ui/BackButton.astro' 9 + import TableOfContents from '@/components/ui/TableOfContents.astro' 9 10 import GradientMask from '@/components/ui/GradientMask.astro' 10 11 import ImageViewer from '@/components/ui/ImageViewer.astro' 11 12 import GitHubCard from '@/components/ui/GitHubCard.astro' ··· 34 35 <div class="prose"> 35 36 <GradientMask /> 36 37 <BackButton /> 38 + {SITE.toc && <TableOfContents />} 37 39 <div class="title"> 38 40 <h1>{title}</h1> 39 41 <div class="date">