Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/trueberryless/the-thinking-hub. The central knowledge hub for 8 key Computer Science thinking modes. the-thinking-hub.felixs.dev
Something went wrong. Try again.
9.7 kB · 387 lines
Astro
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388---import { themeConfig } from '@/config'import type { TOCProps } from '@/types'
const { toc = [] }: TOCProps = Astro.props---
<div class="toc-container" id="toc"> <nav class="toc-nav"> <ul class="toc-list" id="toc-list"> <!-- Back to top link --> <li class="toc-item toc-level-0"> <a href="#" class="toc-link toc-title" title="Back to top" data-text="Back to top"> Back to top </a> </li>
<!-- TOC items --> {toc.map((item) => ( <li class={`toc-item toc-level-${item.level}`}> <a href={`#${item.id}`} class="toc-link" title={item.text} data-text={item.text}> {item.text} </a> </li> ))} </ul> </nav></div>
<script is:inline define:vars={{ contentWidth: themeConfig.general.contentWidth, centeredLayout: themeConfig.general.centeredLayout, toc: themeConfig.post.toc }}> ;(function () { // Core state const state = { container: null, links: null, headings: null, titleLink: null, headingMap: new Map(), positions: [], scrollTimeout: null, hasContent: false }
// Initialize DOM elements function initElements() { state.container = document.querySelector('.toc-container') if (!state.container) return false
state.links = document.querySelectorAll('.toc-link') state.headings = document.querySelectorAll('h1, h2') state.titleLink = document.querySelector('.toc-link.toc-title')
// Build heading map state.headingMap.clear() state.links.forEach((link) => { const href = link.getAttribute('href') if (href?.startsWith('#')) { state.headingMap.set(href.substring(1), link) } })
return true }
// Check if content exists function checkContent() { if (!state.container || !state.links) return
const tocItems = Array.from(state.links).filter( (link) => !link.classList.contains('toc-title') ) state.hasContent = tocItems.length > 0
if (!state.hasContent) { state.container.style.display = 'none' } }
// Cache heading positions function cachePositions() { if (!state.headings?.length) return
const scrollTop = window.pageYOffset state.positions = Array.from(state.headings) .filter((_, index) => !(index === 0 && state.headings[0].tagName === 'H1')) .map((heading) => ({ id: heading.id, offsetTop: heading.getBoundingClientRect().top + scrollTop })) }
// Adjust TOC position function adjustPosition() { if (!state.container || !centeredLayout || !state.hasContent) { if (state.container) state.container.style.display = 'none' return }
const pageWidth = window.innerWidth const contentWidthValue = Math.max(parseFloat(contentWidth), 25) const margin = (pageWidth - contentWidthValue * 16) / 2 const baseMinSpace = 11 * 16 // Base minimum space needed const minSpace = toc ? baseMinSpace + 52 : baseMinSpace + 12
if (margin >= minSpace) { state.container.style.display = 'block' state.container.classList.add('fixed-position') const leftPosition = toc ? margin - 176 - 40 : margin - 176 state.container.style.left = `${leftPosition}px` } else { state.container.style.display = 'none' state.container.classList.remove('fixed-position') state.container.style.left = '' } }
// Handle click events function handleClick(e) { const link = e.target.closest('.toc-link') if (!link) return
e.preventDefault()
if (link.classList.contains('toc-title')) { window.scrollTo({ top: 0, behavior: 'smooth' }) history.pushState(null, null, '#') } else { const href = link.getAttribute('href') if (href?.startsWith('#')) { const target = document.getElementById(href.substring(1)) if (target) { const rect = target.getBoundingClientRect() const scrollTop = window.pageYOffset || document.documentElement.scrollTop const offset = rect.top + scrollTop - 96 window.scrollTo({ top: offset, behavior: 'smooth' }) history.pushState(null, null, href) } } } }
// Update active state function updateActive() { if (!state.links?.length || !state.positions.length) return
const scrollTop = window.pageYOffset + 100 let currentActive = null
// Find current active heading for (let i = state.positions.length - 1; i >= 0; i--) { if (scrollTop >= state.positions[i].offsetTop) { currentActive = state.positions[i].id break } }
// Update active state state.links.forEach((link) => link.classList.remove('active'))
if (currentActive && state.headingMap.has(currentActive)) { state.headingMap.get(currentActive).classList.add('active') } else if (state.titleLink) { state.titleLink.classList.add('active') } }
// Initialize function init(retryCount = 0) { const maxRetries = 5
if (initElements()) { checkContent() adjustPosition() cachePositions()
if (state.container) { state.container.removeEventListener('click', handleClick) state.container.addEventListener('click', handleClick) }
updateActive() } else if (retryCount < maxRetries) { setTimeout(() => init(retryCount + 1), 100) } }
// Event handlers function handleScroll() { if (state.scrollTimeout) { cancelAnimationFrame(state.scrollTimeout) } state.scrollTimeout = requestAnimationFrame(updateActive) }
function handleResize() { adjustPosition() requestAnimationFrame(cachePositions) }
// Cleanup function cleanup() { if (state.scrollTimeout) { cancelAnimationFrame(state.scrollTimeout) state.scrollTimeout = null }
Object.assign(state, { container: null, links: null, headings: null, titleLink: null, headingMap: new Map(), positions: [], hasContent: false }) }
// Event listeners document.addEventListener('astro:page-load', () => { cleanup() init() })
document.addEventListener('astro:after-swap', () => { cleanup() init() })
// Fallback for when Astro transitions are disabled document.addEventListener('DOMContentLoaded', () => { if (!state.container || !state.hasContent) { init() } })
window.addEventListener('resize', handleResize) window.addEventListener('scroll', handleScroll) })()</script>
<style is:inline> .toc-container { width: 12rem; position: relative; left: -0.175em; opacity: 0; transition: opacity 0.2s ease-out; display: none; }
.toc-container.fixed-position { opacity: 1; position: fixed; top: 12rem; margin-top: 0; padding-left: 1rem; z-index: 10; left: auto; }
.toc-nav { font-family: var(--sans); }
.toc-list, .toc-list li, .toc-item { list-style: none; margin: 0; padding: 0; }
.prose .toc-container .toc-list { margin-left: 0 !important; padding-left: 0 !important; }
.prose .toc-container .toc-list li { margin: 0 !important; padding: 0 !important; }
.toc-item::before, .toc-item::marker { display: none; }
.toc-link { display: block; color: transparent; text-decoration: none; position: relative; padding-left: 0; height: 1.125rem; width: 100%; min-height: 1rem; font-size: 0; line-height: 1.125rem; text-indent: 2rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; transition: color 0.2s ease-out, font-size 0.2s ease-out, text-indent 0.2s ease-out; cursor: pointer; }
.toc-link::after { content: attr(data-text); position: absolute; left: -0.5rem; top: 0; font-family: var(--sans); font-size: var(--font-size-s); letter-spacing: var(--spacing-m); line-height: 1.125rem; color: var(--text-primary); opacity: 0; transition: opacity 0.2s ease-out, left 0.2s ease-out; pointer-events: none; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; max-width: 100%; }
.toc-link:hover::after { opacity: 1; left: -0.75rem; }
.toc-level-0 .toc-link:hover::after { opacity: 0; }
.toc-level-1 .toc-link:hover::before, .toc-level-2 .toc-link:hover::before, .toc-level-3 .toc-link:hover::before { width: 0.75rem; transition: width 0.1s ease-out; }
.toc-link.active { color: var(--text-primary); }
/* Horizontal line indicators */ .toc-level-0 .toc-link::before, .toc-level-1 .toc-link::before, .toc-level-2 .toc-link::before, .toc-level-3 .toc-link::before { content: ''; position: absolute; left: 0; top: 50%; width: 2.5rem; height: 1px; background-color: var(--text-tertiary); transform: translateY(-50%); opacity: 0.4; transition: all 0.1s ease-out; }
.toc-link:hover::before, .toc-link.active::before { opacity: 0.8; background-color: var(--text-primary); }
/* Hide on mobile */ @media (max-width: 768px) { .toc-container { display: none !important; } }</style>