204 lines
No EOL
5.5 KiB
Text
204 lines
No EOL
5.5 KiB
Text
---
|
||
interface TOCItem {
|
||
id: string;
|
||
text: string;
|
||
level: number;
|
||
}
|
||
interface Props {
|
||
items: TOCItem[];
|
||
}
|
||
const { items } = Astro.props;
|
||
---
|
||
|
||
{items.length > 0 && (
|
||
<aside class="toc-container" id="toc-sidebar">
|
||
<nav class="toc-nav">
|
||
<h3 class="toc-title">Оглавление</h3>
|
||
<ul class="toc-list">
|
||
{items.map((item) => (
|
||
<li class={`toc-item level-${item.level}`} data-toc-target={item.id}>
|
||
<a href={`#${item.id}`} class="toc-link">
|
||
<span class="toc-text">{item.text}</span>
|
||
</a>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</nav>
|
||
</aside>
|
||
)}
|
||
|
||
<style is:global>
|
||
.toc-container {
|
||
width: 100%;
|
||
margin-top: 1.5rem;
|
||
transition: margin-top 0.3s ease;
|
||
}
|
||
.toc-container.no-top-margin {
|
||
margin-top: 0 !important;
|
||
}
|
||
.toc-nav {
|
||
background: #ffffff;
|
||
border-radius: 1.25rem;
|
||
padding: 1.5rem;
|
||
border: 1px solid #e2e8f0;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.03);
|
||
}
|
||
.toc-title {
|
||
color: #1e3050;
|
||
font-size: 1.1rem;
|
||
font-weight: 800;
|
||
margin: 0 0 1.25rem;
|
||
padding-bottom: 0.75rem;
|
||
border-bottom: 2px solid #eac26e;
|
||
text-align: center;
|
||
}
|
||
|
||
.toc-list {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
counter-reset: toc-h2;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
/* Номера только для H2 (level-1/2), H3 без номеров */
|
||
.toc-item.level-1, .toc-item.level-2 { counter-reset: toc-h3; counter-increment: toc-h2; }
|
||
.toc-item.level-3 { padding-left: 1.25rem; counter-increment: toc-h3; }
|
||
.toc-item.level-3 .toc-link::before { content: none; }
|
||
|
||
/* h2 = level 1 в HTML */
|
||
.toc-item.level-1, .toc-item.level-2 {
|
||
font-weight: 600;
|
||
}
|
||
.toc-item.level-1 .toc-link, .toc-item.level-2 .toc-link {
|
||
font-size: 0.95rem;
|
||
padding: 0.6rem 0.75rem;
|
||
}
|
||
|
||
.toc-link {
|
||
display: flex;
|
||
padding: 0.5rem 0.75rem;
|
||
color: #64748b;
|
||
text-decoration: none;
|
||
font-size: 0.9rem;
|
||
line-height: 1.4;
|
||
border-radius: 0.5rem;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.toc-link::before {
|
||
content: "";
|
||
display: inline-block;
|
||
width: 1.5rem;
|
||
color: #1e3050;
|
||
font-weight: 700;
|
||
margin-right: 0.6rem;
|
||
flex-shrink: 0;
|
||
text-align: center;
|
||
}
|
||
.toc-item.level-1 .toc-link::before, .toc-item.level-2 .toc-link::before { content: counter(toc-h2) "."; }
|
||
/* H3 без номеров */
|
||
|
||
.toc-link:hover { color: #1e3050; background: #f1f5f9; }
|
||
|
||
.toc-item.active .toc-link {
|
||
color: #eac26e;
|
||
font-weight: 700;
|
||
background: rgba(234, 194, 110, 0.1);
|
||
}
|
||
.toc-item.active .toc-link::before { color: #eac26e; }
|
||
</style>
|
||
|
||
<script>
|
||
function setupTOC() {
|
||
const toc = document.getElementById('toc-sidebar');
|
||
if (!toc) return;
|
||
|
||
let lastScrollY = window.scrollY;
|
||
let ticking = false;
|
||
|
||
const updateMargin = () => {
|
||
const currentScrollY = window.scrollY;
|
||
// При скролле ВНИЗ - убрать отступ
|
||
if (currentScrollY > lastScrollY) {
|
||
toc.classList.add('no-top-margin');
|
||
} else {
|
||
// При скролле ВВЕРХ или начальная позиция - вернуть отступ
|
||
toc.classList.remove('no-top-margin');
|
||
}
|
||
lastScrollY = currentScrollY;
|
||
ticking = false;
|
||
};
|
||
|
||
window.addEventListener('scroll', () => {
|
||
if (!ticking) {
|
||
requestAnimationFrame(() => updateMargin());
|
||
ticking = true;
|
||
}
|
||
});
|
||
|
||
const items = toc.querySelectorAll('.toc-item');
|
||
const links = toc.querySelectorAll<HTMLAnchorElement>('.toc-link');
|
||
const HEADER_OFFSET = 120; // Высота хедера + запас
|
||
|
||
// --- 1. ПЛАВНЫЙ СКРОЛЛ ПРИ КЛИКЕ ---
|
||
links.forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
const id = link.getAttribute('href')?.substring(1);
|
||
const target = document.getElementById(id || '');
|
||
|
||
if (target) {
|
||
const elementPosition = target.getBoundingClientRect().top;
|
||
const offsetPosition = elementPosition + window.pageYOffset - HEADER_OFFSET;
|
||
|
||
window.scrollTo({
|
||
top: offsetPosition,
|
||
behavior: "smooth"
|
||
});
|
||
|
||
// Обновляем URL
|
||
history.pushState(null, '', `#${id}`);
|
||
}
|
||
});
|
||
});
|
||
|
||
// --- 2. ПОДСВЕТКА ПРИ СКРОЛЛЕ (SCROLL SPY) ---
|
||
const headingElements = Array.from(items)
|
||
.map(item => {
|
||
const id = item.getAttribute('data-toc-target');
|
||
return id ? document.getElementById(id) : null;
|
||
})
|
||
.filter((el): el is HTMLElement => el !== null);
|
||
|
||
const observerOptions = {
|
||
root: null,
|
||
rootMargin: '-120px 0px -70% 0px', // Зона срабатывания: верхняя часть экрана
|
||
threshold: 0
|
||
};
|
||
|
||
const observer = new IntersectionObserver((entries) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
const activeId = entry.target.id;
|
||
|
||
items.forEach(item => {
|
||
if (item.getAttribute('data-toc-target') === activeId) {
|
||
item.classList.add('active');
|
||
} else {
|
||
item.classList.remove('active');
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}, observerOptions);
|
||
|
||
headingElements.forEach(heading => observer.observe(heading));
|
||
}
|
||
|
||
// Запуск для обычной загрузки и для Astro ViewTransitions
|
||
document.addEventListener('DOMContentLoaded', setupTOC);
|
||
document.addEventListener('astro:page-load', setupTOC);
|
||
</script> |