mirror of
https://github.com/reonokiy/blog.nokiy.net.git
synced 2025-06-16 03:32:51 +02:00
237 lines
6.5 KiB
Text
237 lines
6.5 KiB
Text
---
|
|
import type { MarkdownHeading } from 'astro'
|
|
import TocIcon from '@/assets/icons/toc-icon.svg'
|
|
import { ui } from '@/i18n/ui'
|
|
import { getPageInfo } from '@/utils/page'
|
|
|
|
interface Props {
|
|
headings: MarkdownHeading[]
|
|
}
|
|
|
|
const { currentLang } = getPageInfo(Astro.url.pathname)
|
|
const currentUI = ui[currentLang as keyof typeof ui]
|
|
|
|
const { headings = [] } = Astro.props
|
|
const filteredHeadings = headings.filter(heading =>
|
|
heading.depth >= 2
|
|
&& heading.depth <= 4,
|
|
)
|
|
---
|
|
|
|
{filteredHeadings.length > 0 && (
|
|
<div class="mb-4 uno-round-border bg-secondary/5 2xl:(fixed left-0 top-43.5 max-w-[min(calc(50vw-38rem),13rem)] border-none bg-transparent)">
|
|
<input
|
|
type="checkbox"
|
|
id="toc-toggle"
|
|
class="accordion-toggle"
|
|
hidden
|
|
/>
|
|
<div class="relative h-12 w-full bg-transparent">
|
|
<label
|
|
for="toc-toggle"
|
|
class="absolute inset-0 flex cursor-pointer items-center 2xl:(static flex c-secondary/40 transition-colors ease-out hover:c-secondary/80)"
|
|
>
|
|
<span class="toc-title">
|
|
{currentUI.toc}
|
|
</span>
|
|
|
|
<TocIcon
|
|
aria-hidden="true"
|
|
class="ml-2 hidden aspect-square w-4.2 2xl:(mt-4 block origin-center active:scale-90)"
|
|
fill="currentColor"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
{/* Expandable content wrapper */}
|
|
<div class="accordion-wrapper">
|
|
<nav
|
|
id="toc-content"
|
|
class="accordion-content"
|
|
aria-label="Table of Contents"
|
|
>
|
|
<ul class="toc-list">
|
|
{filteredHeadings.map(heading => (
|
|
<li
|
|
class:list={{
|
|
'ml-0': heading.depth === 2,
|
|
'ml-4': heading.depth === 3,
|
|
'ml-8': heading.depth === 4,
|
|
}}
|
|
>
|
|
<a
|
|
href={`#${heading.slug}`}
|
|
class:list={[
|
|
{ 'toc-link-h2': heading.depth === 2 },
|
|
{ 'toc-link-h3': heading.depth === 3 },
|
|
{ 'toc-link-h4': heading.depth === 4 },
|
|
]}
|
|
>
|
|
{heading.text}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<!-- Override heti default styles >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
|
|
<style>
|
|
.toc-title {
|
|
--at-apply: 'font-semibold ml-4 select-none 2xl:hidden';
|
|
}
|
|
.toc-list {
|
|
--at-apply: 'list-none pl-0 space-y-2 mt-1 mb-4 2xl:space-y-1.2';
|
|
}
|
|
.toc-link-h2, .toc-link-h3, .toc-link-h4 {
|
|
--at-apply: 'text-sm no-underline font-normal text-balance select-none 2xl:(text-3.2 c-secondary/60 transition-colors ease-in hover:(c-secondary font-medium))';
|
|
}
|
|
|
|
/* Initial collapsed state with zero height grid row */
|
|
.accordion-wrapper {
|
|
--at-apply: 'grid rows-[0fr] duration-300 ease-in-out';
|
|
}
|
|
.accordion-content {
|
|
--at-apply: 'max-h-66 overflow-hidden pl-4 pr-6 2xl:(max-h-[calc(100vh-21.5rem)] pl-2)';
|
|
}
|
|
|
|
/* When toggle is checked, expand the wrapper to show content */
|
|
.accordion-toggle:checked ~ .accordion-wrapper {
|
|
grid-template-rows: 1fr;
|
|
}
|
|
.accordion-toggle:checked ~ .accordion-wrapper .accordion-content {
|
|
--at-apply: 'overflow-y-auto';
|
|
}
|
|
#toc-content::-webkit-scrollbar {
|
|
--at-apply: 'w-1.25 lg:w-1.5';
|
|
}
|
|
#toc-content::-webkit-scrollbar-thumb {
|
|
--at-apply: 'rounded-full bg-secondary/15';
|
|
}
|
|
#toc-content::-webkit-scrollbar-thumb:hover {
|
|
--at-apply: 'bg-secondary/25';
|
|
}
|
|
|
|
@media (min-width: 1536px) {
|
|
.accordion-wrapper {
|
|
grid-template-rows: 1fr;
|
|
}
|
|
.accordion-toggle:checked ~ .accordion-wrapper {
|
|
grid-template-rows: 0fr;
|
|
}
|
|
.accordion-content {
|
|
--at-apply: 'overflow-y-auto';
|
|
}
|
|
.accordion-toggle:checked ~ .accordion-wrapper .accordion-content {
|
|
--at-apply: 'overflow-hidden';
|
|
}
|
|
.toc-link-active {
|
|
--at-apply: 'c-secondary font-medium';
|
|
}
|
|
#toc-content {
|
|
--at-apply: 'scrollbar-hidden';
|
|
}
|
|
#toc-content::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<!-- TOC Highlight Script >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
|
|
<script>
|
|
function setupTOCHighlight() {
|
|
const tocContent = document.getElementById('toc-content')
|
|
if (!tocContent)
|
|
return
|
|
|
|
const tocLinks = tocContent.querySelectorAll('a')
|
|
if (tocLinks.length === 0)
|
|
return
|
|
|
|
// Check if in large screen (2xl) mode
|
|
let is2xl = window.innerWidth >= 1536
|
|
|
|
// Build mapping from heading IDs to TOC links
|
|
const headingMap = new Map<string, HTMLAnchorElement>()
|
|
tocLinks.forEach((link) => {
|
|
const id = link.getAttribute('href')?.substring(1)
|
|
if (id)
|
|
headingMap.set(id, link as HTMLAnchorElement)
|
|
})
|
|
|
|
let currentActiveLink: HTMLAnchorElement | null = null
|
|
|
|
function highlightLink(link: HTMLAnchorElement) {
|
|
if (link === currentActiveLink)
|
|
return
|
|
|
|
if (currentActiveLink) {
|
|
currentActiveLink.classList.remove('toc-link-active')
|
|
}
|
|
|
|
link.classList.add('toc-link-active')
|
|
currentActiveLink = link
|
|
|
|
// Only scroll the TOC in large screen (2xl) mode
|
|
if (is2xl) {
|
|
scrollLinkToCenter(link)
|
|
}
|
|
}
|
|
|
|
// Scroll the link to center position
|
|
function scrollLinkToCenter(link: HTMLAnchorElement) {
|
|
link.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center',
|
|
})
|
|
}
|
|
|
|
// Intersection observer callback
|
|
const intersectionCallback = (entries: IntersectionObserverEntry[]) => {
|
|
const visibleHeading = entries.find(entry => entry.isIntersecting)?.target
|
|
|
|
if (visibleHeading && visibleHeading.id) {
|
|
const link = headingMap.get(visibleHeading.id)
|
|
if (link)
|
|
highlightLink(link)
|
|
}
|
|
}
|
|
|
|
// Create the intersection observer
|
|
const headingObserver = new IntersectionObserver(intersectionCallback, {
|
|
rootMargin: '0% 0% -66% 0%',
|
|
threshold: [0.4],
|
|
})
|
|
|
|
// Observe all heading elements
|
|
document.querySelectorAll('h2, h3, h4').forEach((heading) => {
|
|
if (heading.id && heading.id !== 'footnotes') {
|
|
headingObserver.observe(heading)
|
|
}
|
|
})
|
|
|
|
// Highlight the first TOC item by default
|
|
if (tocLinks.length > 0) {
|
|
highlightLink(tocLinks[0] as HTMLAnchorElement)
|
|
}
|
|
|
|
// Listen for window resize events
|
|
window.addEventListener('resize', () => {
|
|
const newIs2xl = window.innerWidth >= 1536
|
|
if (is2xl !== newIs2xl) {
|
|
// Update screen size flag
|
|
is2xl = newIs2xl
|
|
|
|
if (currentActiveLink && is2xl) {
|
|
scrollLinkToCenter(currentActiveLink)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
setupTOCHighlight()
|
|
document.addEventListener('astro:after-swap', setupTOCHighlight)
|
|
</script>
|
|
|