nicholai-work-2026/src/components/TableOfContents.astro
Nicholai 2ca66ccc6d Update theme variable usage and add ThemeToggle component
- Refactor component styles to use CSS custom properties for colors and backgrounds.
- Replace hard‑coded Tailwind classes with theme variables across BlogCard, BlogFilters, Footer, GridOverlay, Navigation, PostNavigation, ReadingProgress, RelatedPosts, TableOfContents, ThemeToggle, sections, layouts, pages, and global.css.
- Add ThemeToggle component for user‑controlled theme switching.
- Update global styles to define new theme variables.
- Ensure all components respect theme changes and maintain accessibility.

Hubert The Eunuch
2025-12-18 17:08:52 -07:00

122 lines
3.5 KiB
Plaintext

---
interface Props {
headings: Array<{
depth: number;
slug: string;
text: string;
}>;
class?: string;
}
const { headings, class: className = '' } = Astro.props;
// Filter to only H2 and H3 headings
const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
---
{tocHeadings.length > 0 && (
<nav class:list={['toc', className]} data-toc aria-label="Table of contents">
<div class="flex items-center gap-3 mb-6">
<span class="text-[10px] font-mono text-[var(--theme-text-muted)] uppercase tracking-widest font-bold">
/// CONTENTS
</span>
<span class="h-px flex-grow bg-[var(--theme-border-primary)]"></span>
</div>
<ul class="space-y-3">
{tocHeadings.map((heading) => (
<li>
<a
href={`#${heading.slug}`}
data-toc-link={heading.slug}
class:list={[
'toc-link block text-sm transition-all duration-300 hover:text-[var(--theme-text-primary)]',
heading.depth === 2
? 'text-[var(--theme-text-secondary)] font-medium'
: 'text-[var(--theme-text-muted)] pl-4 text-xs',
]}
>
<span class="flex items-center gap-2">
{heading.depth === 2 && (
<span class="w-1.5 h-1.5 bg-[var(--theme-text-subtle)] toc-indicator transition-colors duration-300"></span>
)}
{heading.text}
</span>
</a>
</li>
))}
</ul>
</nav>
)}
<script>
function initTableOfContents() {
const tocLinks = document.querySelectorAll('[data-toc-link]');
if (tocLinks.length === 0) return;
const headings = Array.from(tocLinks).map((link) => {
const slug = (link as HTMLElement).dataset.tocLink;
return document.getElementById(slug || '');
}).filter(Boolean) as HTMLElement[];
let currentActive: Element | null = null;
function updateActiveLink() {
const scrollY = window.scrollY;
const offset = 150; // Offset for when to activate
let activeHeading: HTMLElement | null = null;
for (const heading of headings) {
const rect = heading.getBoundingClientRect();
const top = rect.top + scrollY;
if (scrollY >= top - offset) {
activeHeading = heading;
}
}
if (activeHeading && currentActive !== activeHeading) {
// Remove active state from all links
tocLinks.forEach((link) => {
link.classList.remove('text-brand-accent', 'text-[var(--theme-text-primary)]');
link.classList.add('text-[var(--theme-text-secondary)]');
const indicator = link.querySelector('.toc-indicator');
if (indicator) {
indicator.classList.remove('bg-brand-accent');
indicator.classList.add('bg-[var(--theme-text-subtle)]');
}
});
// Add active state to current link
const activeLink = document.querySelector(`[data-toc-link="${activeHeading.id}"]`);
if (activeLink) {
activeLink.classList.remove('text-[var(--theme-text-secondary)]');
activeLink.classList.add('text-brand-accent');
const indicator = activeLink.querySelector('.toc-indicator');
if (indicator) {
indicator.classList.remove('bg-[var(--theme-text-subtle)]');
indicator.classList.add('bg-brand-accent');
}
}
currentActive = activeHeading;
}
}
window.addEventListener('scroll', updateActiveLink, { passive: true });
updateActiveLink();
}
// Initialize on page load
initTableOfContents();
// Re-initialize on Astro page transitions
document.addEventListener('astro:page-load', initTableOfContents);
</script>
<style>
.toc-link:hover .toc-indicator {
background-color: var(--color-brand-accent);
}
</style>