-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsidebar.js
More file actions
116 lines (99 loc) · 3.95 KB
/
sidebar.js
File metadata and controls
116 lines (99 loc) · 3.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// 테마 초기화 (flash 최소화)
(function(){
const saved=localStorage.getItem('cs_viz_theme')||'light';
document.documentElement.setAttribute('data-theme',saved);
})();
(function () {
// 챕터 색상 가져오기
const navTitle = document.querySelector('.nav-chapter-title');
const chColor = navTitle ? getComputedStyle(navTitle).color : '#00ffaa';
// 페이지의 모든 section[id] 수집
const sections = Array.from(document.querySelectorAll('section[id]'));
if (!sections.length) return;
// 사이드바 생성
const sidebar = document.createElement('aside');
sidebar.className = 'sidebar';
let html = '<div class="sidebar-title">목차</div>';
sections.forEach((sec, i) => {
const h2 = sec.querySelector('h2');
const rawText = h2 ? h2.textContent.trim() : sec.id;
// 괄호 이하 부분 제거해서 짧게 표시
const label = rawText.replace(/\s*\(.*\)$/, '').replace(/\s*—.*$/, '').trim();
html += `<a class="sidebar-link" href="#${sec.id}" data-id="${sec.id}">
<span class="sidebar-num">${String(i + 1).padStart(2, '0')}</span>
<span class="sidebar-label">${label}</span>
</a>`;
});
sidebar.innerHTML = html;
document.body.prepend(sidebar);
document.body.classList.add('has-sidebar');
// 상단 nav의 .nav-sections 숨기기 (사이드바로 대체)
const navSections = document.querySelector('.nav-sections');
if (navSections) navSections.style.display = 'none';
// 오버레이 (모바일)
const overlay = document.createElement('div');
overlay.className = 'sidebar-overlay';
document.body.appendChild(overlay);
// 토글 버튼 (모바일)
const toggle = document.createElement('button');
toggle.className = 'sidebar-toggle';
toggle.setAttribute('aria-label', '목차 열기');
toggle.innerHTML = '≡';
document.body.appendChild(toggle);
function openSidebar() {
sidebar.classList.add('open');
overlay.classList.add('open');
toggle.innerHTML = '✕';
}
function closeSidebar() {
sidebar.classList.remove('open');
overlay.classList.remove('open');
toggle.innerHTML = '≡';
}
toggle.addEventListener('click', () =>
sidebar.classList.contains('open') ? closeSidebar() : openSidebar()
);
overlay.addEventListener('click', closeSidebar);
// 링크 클릭 시 모바일에서 닫기
sidebar.querySelectorAll('.sidebar-link').forEach(link => {
link.addEventListener('click', () => {
if (window.innerWidth <= 820) closeSidebar();
});
});
// IntersectionObserver: 현재 섹션 하이라이트
const links = sidebar.querySelectorAll('.sidebar-link');
function setActive(id) {
links.forEach(l => {
const isActive = l.dataset.id === id;
l.classList.toggle('active', isActive);
l.style.color = isActive ? chColor : '';
l.style.borderLeftColor = isActive ? chColor : '';
});
}
const observer = new IntersectionObserver(entries => {
// 화면에 보이는 섹션 중 가장 위에 있는 것을 활성화
const visible = entries
.filter(e => e.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
if (visible.length) setActive(visible[0].target.id);
}, {
rootMargin: '-54px 0px -40% 0px',
threshold: 0
});
sections.forEach(sec => observer.observe(sec));
// 초기 활성화
if (sections[0]) setActive(sections[0].id);
})();
// 테마 토글 버튼
(function(){
const btn=document.createElement('button');
btn.className='theme-toggle-btn';
function upd(){const dark=document.documentElement.getAttribute('data-theme')==='dark';btn.textContent=dark?'☀️':'🌙';btn.title=dark?'라이트 모드로 전환':'다크 모드로 전환';}
upd();
btn.addEventListener('click',()=>{
const next=document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark';
document.documentElement.setAttribute('data-theme',next);
localStorage.setItem('cs_viz_theme',next);upd();
});
document.body.appendChild(btn);
})();