tfrere HF Staff commited on
Commit
03c7c65
·
1 Parent(s): 35f4ea0

fix: match TOC style and collapse behavior to original template

Browse files

- Grid columns: 260px / 680px / 260px (matching original)
- Desktop TOC: exact same styles (border-left, font-size 13px, title
14px 600, H2 links bold 700, hover underline solid muted, active
underline, margin-top 12px)
- Auto-collapse: sub-sections fold/unfold based on active heading,
disabled on mobile (all expanded), with height transition 200ms
- Mobile sidebar: same styles as original template
- Class names aligned: .table-of-contents (not .toc)

Made-with: Cursor

Files changed (1) hide show
  1. backend/src/publisher/html-renderer.ts +225 -103
backend/src/publisher/html-renderer.ts CHANGED
@@ -88,14 +88,14 @@ body {
88
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
89
  }
90
 
91
- /* Grid: TOC | Article | (empty) */
92
  .content-grid {
93
  max-width: 1280px;
94
  margin: 0 auto;
95
  padding: 0 24px;
96
  margin-top: 40px;
97
  display: grid;
98
- grid-template-columns: 240px minmax(0, 720px) 1fr;
99
  gap: 32px;
100
  align-items: start;
101
  }
@@ -160,30 +160,64 @@ body {
160
  z-index: 100;
161
  }
162
 
163
- /* ---------- Table of Contents ---------- */
164
- .toc {
165
  position: sticky;
166
  top: 32px;
167
- max-height: calc(100vh - 64px);
168
- overflow-y: auto;
169
  }
170
 
171
- .toc .toc-title {
172
  font-weight: 600;
173
  font-size: 14px;
174
  margin-bottom: 8px;
175
  color: var(--text-primary);
176
  }
177
 
178
- .toc nav { border-left: 1px solid var(--border); padding-left: 16px; font-size: 13px; }
179
- .toc nav ul { margin: 0 0 4px; padding-left: 1em; }
180
- .toc nav li { list-style: none; margin: 0.25em 0; }
181
- .toc nav a { color: var(--text-secondary); text-decoration: none; transition: color 0.15s; }
182
- .toc nav > ul > li > a { font-weight: 600; color: var(--text-primary); }
183
- .toc nav a:hover { color: var(--accent-light); }
184
- .toc nav a.active { color: var(--accent-light); text-decoration: underline; text-underline-offset: 2px; }
185
 
186
- /* Mobile TOC toggle */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  .toc-mobile-toggle {
188
  display: none;
189
  position: fixed;
@@ -199,10 +233,12 @@ body {
199
  cursor: pointer;
200
  align-items: center;
201
  justify-content: center;
202
- box-shadow: var(--shadow-sm);
 
203
  }
 
204
 
205
- /* Mobile backdrop */
206
  .toc-mobile-backdrop {
207
  display: none;
208
  position: fixed;
@@ -215,7 +251,7 @@ body {
215
  }
216
  .toc-mobile-backdrop.open { opacity: 1; pointer-events: auto; }
217
 
218
- /* Mobile sidebar */
219
  .toc-mobile-sidebar {
220
  display: none;
221
  position: fixed;
@@ -238,11 +274,12 @@ body {
238
  border-bottom: 1px solid var(--border);
239
  flex-shrink: 0;
240
  }
241
- .toc-mobile-sidebar__title { font-weight: 700; font-size: 15px; }
242
  .toc-mobile-sidebar__close {
243
  background: none; border: none; color: var(--text-tertiary);
244
  cursor: pointer; padding: 6px; border-radius: 6px;
245
  display: flex; align-items: center; justify-content: center;
 
246
  }
247
  .toc-mobile-sidebar__close:hover { color: var(--text-primary); background: var(--bg-secondary); }
248
 
@@ -250,23 +287,32 @@ body {
250
  flex: 1;
251
  overflow-y: auto;
252
  -webkit-overflow-scrolling: touch;
253
- padding: 0.75rem 1rem 2rem;
 
 
 
 
 
 
 
 
 
 
 
254
  }
255
- .toc-mobile-sidebar__body nav ul { margin: 0 0 4px; padding-left: 1em; }
256
- .toc-mobile-sidebar__body nav li { list-style: none; margin: 0.35em 0; }
257
- .toc-mobile-sidebar__body nav a { color: var(--text-secondary); text-decoration: none; font-size: 14px; line-height: 1.5; }
258
- .toc-mobile-sidebar__body nav > ul > li > a { font-weight: 600; color: var(--text-primary); }
259
- .toc-mobile-sidebar__body nav a:hover { color: var(--accent-light); }
260
- .toc-mobile-sidebar__body nav a.active { color: var(--accent-light); text-decoration: underline; }
261
 
262
- /* Responsive: collapse grid below 1100px */
263
  @media (max-width: 1100px) {
264
  .content-grid {
 
265
  display: block;
266
  padding: 0 16px;
267
- margin-top: 1rem;
268
  }
269
- .toc { display: none; }
270
  .toc-mobile-toggle { display: flex; }
271
  .toc-mobile-backdrop { display: block; }
272
  .toc-mobile-sidebar { display: flex; }
@@ -308,7 +354,7 @@ dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; bo
308
 
309
  /* Print */
310
  @media print {
311
- .toc, .toc-mobile-toggle, .toc-mobile-backdrop, .toc-mobile-sidebar, .theme-toggle { display: none !important; }
312
  .content-grid { display: block !important; }
313
  }
314
  </style>
@@ -336,10 +382,10 @@ dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; bo
336
  </aside>
337
 
338
  <section class="content-grid">
339
- <div class="toc" aria-label="Table of Contents">
340
- <div class="toc-title">Table of Contents</div>
341
  <div id="toc-placeholder"></div>
342
- </div>
343
 
344
  <article>
345
  <header class="article-header">
@@ -361,17 +407,16 @@ dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; bo
361
 
362
  <script>
363
  (function() {
364
- // Theme toggle
365
  window.toggleTheme = function() {
366
- var html = document.documentElement;
367
- var next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
368
- html.setAttribute('data-theme', next);
369
  localStorage.setItem('theme', next);
370
  };
371
  var saved = localStorage.getItem('theme');
372
  if (saved) document.documentElement.setAttribute('data-theme', saved);
373
 
374
- // Image lightbox
375
  document.querySelectorAll('.tiptap img').forEach(function(img) {
376
  img.style.cursor = 'zoom-in';
377
  img.addEventListener('click', function() {
@@ -379,140 +424,217 @@ dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; bo
379
  document.getElementById('lightbox').showModal();
380
  });
381
  });
382
- document.getElementById('lightbox').addEventListener('click', function(e) {
383
- if (e.target === this) this.close();
384
- });
385
 
386
- // Glossary tabindex
387
- document.querySelectorAll('[data-type="glossary"]').forEach(function(el) {
388
- el.setAttribute('tabindex', '0');
389
- });
390
 
391
  // ---- Table of Contents ----
392
  var holder = document.getElementById('toc-placeholder');
393
  var holderMobile = document.getElementById('toc-mobile-placeholder');
394
  var articleRoot = document.querySelector('.content-grid article');
395
  if (!articleRoot) return;
396
-
397
  var headings = articleRoot.querySelectorAll('h2, h3, h4');
398
  if (!headings.length) return;
399
  var headingsArr = Array.from(headings);
400
 
401
- // Ensure unique IDs
402
  var usedIds = {};
403
- var slugify = function(s) {
404
- return String(s || '').toLowerCase().trim().replace(/\\s+/g, '_').replace(/[^a-z0-9_-]/g, '');
405
- };
406
  headingsArr.forEach(function(h) {
407
- var id = (h.id || '').trim() || slugify(h.textContent) || 'section';
408
- var candidate = id;
409
- var n = 2;
410
- while (usedIds[candidate]) { candidate = id + '-' + (n++); }
411
- if (h.id !== candidate) h.id = candidate;
412
- usedIds[candidate] = true;
413
  });
414
 
415
- // Build nested nav
416
  var nav = document.createElement('nav');
417
  var ulStack = [document.createElement('ul')];
418
  nav.appendChild(ulStack[0]);
419
- var levelOf = function(tag) { return tag === 'H2' ? 2 : tag === 'H3' ? 3 : 4; };
420
- var prev = 2;
421
  headingsArr.forEach(function(h) {
422
  var lvl = levelOf(h.tagName);
423
- while (lvl > prev) {
424
- var ul = document.createElement('ul');
425
- var last = ulStack[ulStack.length - 1].lastElementChild;
426
- if (last) last.appendChild(ul);
427
- ulStack.push(ul);
428
- prev++;
429
- }
430
  while (lvl < prev) { ulStack.pop(); prev--; }
431
  var li = document.createElement('li');
432
  var a = document.createElement('a');
433
- a.href = '#' + h.id;
434
- a.textContent = h.textContent;
435
  li.appendChild(a);
436
- ulStack[ulStack.length - 1].appendChild(li);
 
437
  });
438
 
439
  if (holder) holder.appendChild(nav);
440
  var navClone = nav.cloneNode(true);
441
  if (holderMobile) holderMobile.appendChild(navClone);
442
 
443
- // Active section tracking
 
 
 
444
  var allLinks = [].concat(
445
  holder ? Array.from(holder.querySelectorAll('a')) : [],
446
  holderMobile ? Array.from(holderMobile.querySelectorAll('a')) : []
447
  );
448
- var OFFSET = 80;
449
- var lastScrollTime = 0;
450
 
451
- var onScroll = function() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  var now = performance.now();
453
  if (now - lastScrollTime < 50) return;
454
  lastScrollTime = now;
455
  requestAnimationFrame(function() {
456
- var activeId = null;
457
  for (var i = headingsArr.length - 1; i >= 0; i--) {
458
  if (headingsArr[i].getBoundingClientRect().top - OFFSET <= 0) {
459
- activeId = headingsArr[i].id;
460
- break;
461
  }
462
  }
463
  allLinks.forEach(function(l) {
464
- if (activeId && l.getAttribute('href') === '#' + activeId) {
465
- l.classList.add('active');
466
- } else {
467
- l.classList.remove('active');
468
- }
469
  });
 
 
 
 
 
470
  });
471
- };
472
  window.addEventListener('scroll', onScroll, { passive: true });
473
  onScroll();
474
 
475
- // Mobile sidebar
476
  var sidebar = document.querySelector('.toc-mobile-sidebar');
477
  var backdrop = document.querySelector('.toc-mobile-backdrop');
478
  var toggleBtn = document.querySelector('.toc-mobile-toggle');
479
  var closeBtn = document.querySelector('.toc-mobile-sidebar__close');
480
 
481
- var openSidebar = function() {
482
- sidebar.classList.add('open');
483
- backdrop.classList.add('open');
484
- toggleBtn.setAttribute('aria-expanded', 'true');
485
  document.body.style.overflow = 'hidden';
486
  requestAnimationFrame(function() {
487
  var active = sidebar.querySelector('a.active');
488
  if (active) {
489
  var body = sidebar.querySelector('.toc-mobile-sidebar__body');
490
- if (body) body.scrollTop = Math.max(0, active.offsetTop - body.offsetTop - body.clientHeight / 3);
491
  }
492
  });
493
- };
494
- var closeSidebar = function() {
495
- sidebar.classList.remove('open');
496
- backdrop.classList.remove('open');
497
- toggleBtn.setAttribute('aria-expanded', 'false');
498
  document.body.style.overflow = '';
499
- };
500
-
501
  if (toggleBtn) toggleBtn.addEventListener('click', openSidebar);
502
  if (closeBtn) closeBtn.addEventListener('click', closeSidebar);
503
  if (backdrop) backdrop.addEventListener('click', closeSidebar);
504
- if (holderMobile) holderMobile.addEventListener('click', function(e) {
505
- if (e.target.closest && e.target.closest('a')) closeSidebar();
506
- });
507
- document.addEventListener('keydown', function(e) {
508
- if (e.key === 'Escape' && sidebar.classList.contains('open')) closeSidebar();
509
- });
510
 
511
- // Handle initial hash navigation
512
  if (window.location.hash) {
513
  var target = document.querySelector(window.location.hash);
514
- if (target) setTimeout(function() { target.scrollIntoView({ block: 'start' }); }, 100);
515
  }
 
 
 
 
 
516
  })();
517
  </script>
518
  </body>
 
88
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
89
  }
90
 
91
+ /* Grid: TOC | Article | (empty right) */
92
  .content-grid {
93
  max-width: 1280px;
94
  margin: 0 auto;
95
  padding: 0 24px;
96
  margin-top: 40px;
97
  display: grid;
98
+ grid-template-columns: 260px minmax(0, 680px) 260px;
99
  gap: 32px;
100
  align-items: start;
101
  }
 
160
  z-index: 100;
161
  }
162
 
163
+ /* ---------- Table of Contents (desktop sticky) ---------- */
164
+ .table-of-contents {
165
  position: sticky;
166
  top: 32px;
167
+ margin-top: 12px;
 
168
  }
169
 
170
+ .table-of-contents .title {
171
  font-weight: 600;
172
  font-size: 14px;
173
  margin-bottom: 8px;
174
  color: var(--text-primary);
175
  }
176
 
177
+ .table-of-contents nav {
178
+ border-left: 1px solid var(--border);
179
+ padding-left: 16px;
180
+ font-size: 13px;
181
+ }
 
 
182
 
183
+ .table-of-contents nav ul {
184
+ margin: 0 0 6px;
185
+ padding-left: 1em;
186
+ }
187
+
188
+ .table-of-contents nav li {
189
+ list-style: none;
190
+ margin: 0.25em 0;
191
+ }
192
+
193
+ .table-of-contents nav a,
194
+ .table-of-contents nav a:link,
195
+ .table-of-contents nav a:visited {
196
+ color: var(--text-primary);
197
+ text-decoration: none;
198
+ border-bottom: none;
199
+ }
200
+
201
+ .table-of-contents nav > ul > li > a {
202
+ font-weight: 700;
203
+ }
204
+
205
+ .table-of-contents nav a:hover {
206
+ text-decoration: underline solid var(--text-tertiary);
207
+ }
208
+
209
+ .table-of-contents nav a.active {
210
+ text-decoration: underline;
211
+ }
212
+
213
+ /* Collapsible sub-sections */
214
+ .table-of-contents nav.toc-collapsible li > ul,
215
+ .toc-mobile-sidebar nav.toc-collapsible li > ul {
216
+ overflow: hidden;
217
+ transition: height 200ms ease;
218
+ }
219
+
220
+ /* ---------- Mobile TOC toggle ---------- */
221
  .toc-mobile-toggle {
222
  display: none;
223
  position: fixed;
 
233
  cursor: pointer;
234
  align-items: center;
235
  justify-content: center;
236
+ box-shadow: 0 2px 12px rgba(0,0,0,.08);
237
+ transition: transform 150ms ease, box-shadow 150ms ease;
238
  }
239
+ .toc-mobile-toggle:active { transform: scale(0.92); }
240
 
241
+ /* ---------- Mobile backdrop ---------- */
242
  .toc-mobile-backdrop {
243
  display: none;
244
  position: fixed;
 
251
  }
252
  .toc-mobile-backdrop.open { opacity: 1; pointer-events: auto; }
253
 
254
+ /* ---------- Mobile sidebar ---------- */
255
  .toc-mobile-sidebar {
256
  display: none;
257
  position: fixed;
 
274
  border-bottom: 1px solid var(--border);
275
  flex-shrink: 0;
276
  }
277
+ .toc-mobile-sidebar__title { font-weight: 700; font-size: 15px; color: var(--text-primary); }
278
  .toc-mobile-sidebar__close {
279
  background: none; border: none; color: var(--text-tertiary);
280
  cursor: pointer; padding: 6px; border-radius: 6px;
281
  display: flex; align-items: center; justify-content: center;
282
+ transition: color 150ms ease, background 150ms ease;
283
  }
284
  .toc-mobile-sidebar__close:hover { color: var(--text-primary); background: var(--bg-secondary); }
285
 
 
287
  flex: 1;
288
  overflow-y: auto;
289
  -webkit-overflow-scrolling: touch;
290
+ padding: 0.75rem 1rem 1.5rem;
291
+ }
292
+ .toc-mobile-sidebar nav ul { margin: 0 0 6px; padding-left: 1em; }
293
+ .toc-mobile-sidebar nav li { list-style: none; margin: 0.35em 0; }
294
+ .toc-mobile-sidebar nav a,
295
+ .toc-mobile-sidebar nav a:link,
296
+ .toc-mobile-sidebar nav a:visited {
297
+ color: var(--text-primary);
298
+ text-decoration: none;
299
+ border-bottom: none;
300
+ font-size: 14px;
301
+ line-height: 1.5;
302
  }
303
+ .toc-mobile-sidebar nav > ul > li > a { font-weight: 700; }
304
+ .toc-mobile-sidebar nav a:hover { text-decoration: underline solid var(--text-tertiary); }
305
+ .toc-mobile-sidebar nav a.active { color: var(--accent-light); text-decoration: underline; }
 
 
 
306
 
307
+ /* ---------- Responsive: collapse below 1100px ---------- */
308
  @media (max-width: 1100px) {
309
  .content-grid {
310
+ overflow: hidden;
311
  display: block;
312
  padding: 0 16px;
313
+ margin-top: 0.5rem;
314
  }
315
+ .table-of-contents { position: static; display: none; }
316
  .toc-mobile-toggle { display: flex; }
317
  .toc-mobile-backdrop { display: block; }
318
  .toc-mobile-sidebar { display: flex; }
 
354
 
355
  /* Print */
356
  @media print {
357
+ .table-of-contents, .toc-mobile-toggle, .toc-mobile-backdrop, .toc-mobile-sidebar, .theme-toggle { display: none !important; }
358
  .content-grid { display: block !important; }
359
  }
360
  </style>
 
382
  </aside>
383
 
384
  <section class="content-grid">
385
+ <nav class="table-of-contents" aria-label="Table of Contents">
386
+ <div class="title">Table of Contents</div>
387
  <div id="toc-placeholder"></div>
388
+ </nav>
389
 
390
  <article>
391
  <header class="article-header">
 
407
 
408
  <script>
409
  (function() {
410
+ // Theme
411
  window.toggleTheme = function() {
412
+ var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
413
+ document.documentElement.setAttribute('data-theme', next);
 
414
  localStorage.setItem('theme', next);
415
  };
416
  var saved = localStorage.getItem('theme');
417
  if (saved) document.documentElement.setAttribute('data-theme', saved);
418
 
419
+ // Lightbox
420
  document.querySelectorAll('.tiptap img').forEach(function(img) {
421
  img.style.cursor = 'zoom-in';
422
  img.addEventListener('click', function() {
 
424
  document.getElementById('lightbox').showModal();
425
  });
426
  });
427
+ document.getElementById('lightbox').addEventListener('click', function(e) { if (e.target === this) this.close(); });
 
 
428
 
429
+ // Glossary
430
+ document.querySelectorAll('[data-type="glossary"]').forEach(function(el) { el.setAttribute('tabindex', '0'); });
 
 
431
 
432
  // ---- Table of Contents ----
433
  var holder = document.getElementById('toc-placeholder');
434
  var holderMobile = document.getElementById('toc-mobile-placeholder');
435
  var articleRoot = document.querySelector('.content-grid article');
436
  if (!articleRoot) return;
 
437
  var headings = articleRoot.querySelectorAll('h2, h3, h4');
438
  if (!headings.length) return;
439
  var headingsArr = Array.from(headings);
440
 
441
+ // Unique IDs
442
  var usedIds = {};
443
+ var slugify = function(s) { return String(s||'').toLowerCase().trim().replace(/\\s+/g,'_').replace(/[^a-z0-9_-]/g,''); };
 
 
444
  headingsArr.forEach(function(h) {
445
+ var id = (h.id||'').trim() || slugify(h.textContent) || 'section';
446
+ var c = id, n = 2;
447
+ while (usedIds[c]) c = id+'-'+(n++);
448
+ if (h.id !== c) h.id = c;
449
+ usedIds[c] = true;
 
450
  });
451
 
452
+ // Build nested nav with data-heading-idx
453
  var nav = document.createElement('nav');
454
  var ulStack = [document.createElement('ul')];
455
  nav.appendChild(ulStack[0]);
456
+ var levelOf = function(tag) { return tag==='H2'?2:tag==='H3'?3:4; };
457
+ var prev = 2, hIdx = 0;
458
  headingsArr.forEach(function(h) {
459
  var lvl = levelOf(h.tagName);
460
+ while (lvl > prev) { var ul = document.createElement('ul'); var last = ulStack[ulStack.length-1].lastElementChild; if (last) last.appendChild(ul); ulStack.push(ul); prev++; }
 
 
 
 
 
 
461
  while (lvl < prev) { ulStack.pop(); prev--; }
462
  var li = document.createElement('li');
463
  var a = document.createElement('a');
464
+ a.href = '#'+h.id; a.textContent = h.textContent;
 
465
  li.appendChild(a);
466
+ li.setAttribute('data-heading-idx', String(hIdx++));
467
+ ulStack[ulStack.length-1].appendChild(li);
468
  });
469
 
470
  if (holder) holder.appendChild(nav);
471
  var navClone = nav.cloneNode(true);
472
  if (holderMobile) holderMobile.appendChild(navClone);
473
 
474
+ // Mark navs as collapsible
475
+ nav.classList.add('toc-collapsible');
476
+ navClone.classList.add('toc-collapsible');
477
+
478
  var allLinks = [].concat(
479
  holder ? Array.from(holder.querySelectorAll('a')) : [],
480
  holderMobile ? Array.from(holderMobile.querySelectorAll('a')) : []
481
  );
 
 
482
 
483
+ // ---- Auto-collapse logic ----
484
+ var isMobile = window.matchMedia('(max-width: 1100px)');
485
+
486
+ function getItemsWithChildren(navEl) {
487
+ if (!navEl) return [];
488
+ return Array.from(navEl.querySelectorAll('li[data-heading-idx]')).filter(function(li) {
489
+ return li.querySelector(':scope > ul');
490
+ });
491
+ }
492
+
493
+ function getAncestorIndices(items, targetIdx) {
494
+ var toExpand = {};
495
+ var activeLi = null;
496
+ function find(li) {
497
+ if (Number(li.getAttribute('data-heading-idx')) === targetIdx) return li;
498
+ var ul = li.querySelector(':scope > ul');
499
+ if (!ul) return null;
500
+ var children = ul.querySelectorAll(':scope > li[data-heading-idx]');
501
+ for (var i = 0; i < children.length; i++) { var f = find(children[i]); if (f) return f; }
502
+ return null;
503
+ }
504
+ for (var i = 0; i < items.length; i++) { activeLi = find(items[i]); if (activeLi) break; }
505
+ if (!activeLi) return toExpand;
506
+ toExpand[targetIdx] = true;
507
+ var cur = activeLi;
508
+ while (cur) {
509
+ var parent = cur.parentElement ? cur.parentElement.closest('li[data-heading-idx]') : null;
510
+ if (parent) { toExpand[Number(parent.getAttribute('data-heading-idx'))] = true; cur = parent; }
511
+ else break;
512
+ }
513
+ return toExpand;
514
+ }
515
+
516
+ function applyCollapseState(navEl, activeIdx) {
517
+ if (!navEl) return;
518
+ var items = getItemsWithChildren(navEl);
519
+ var ancestors = getAncestorIndices(items, activeIdx);
520
+
521
+ items.forEach(function(li) {
522
+ var sub = li.querySelector(':scope > ul');
523
+ if (!sub) return;
524
+ var idx = Number(li.getAttribute('data-heading-idx'));
525
+ var allDesc = li.querySelectorAll('li[data-heading-idx]');
526
+ var related = [idx];
527
+ allDesc.forEach(function(d) { related.push(Number(d.getAttribute('data-heading-idx'))); });
528
+ var shouldExpand = related.some(function(i) { return ancestors[i]; });
529
+
530
+ if (shouldExpand) {
531
+ li.classList.remove('collapsed');
532
+ sub.style.height = 'auto';
533
+ } else {
534
+ li.classList.add('collapsed');
535
+ sub.style.height = '0px';
536
+ }
537
+ });
538
+ }
539
+
540
+ function expandAll(navEl) {
541
+ if (!navEl) return;
542
+ getItemsWithChildren(navEl).forEach(function(li) {
543
+ li.classList.remove('collapsed');
544
+ var sub = li.querySelector(':scope > ul');
545
+ if (sub) sub.style.height = 'auto';
546
+ });
547
+ }
548
+
549
+ // Initial state: collapse all except first section
550
+ function initCollapse() {
551
+ if (isMobile.matches) {
552
+ expandAll(holder ? holder.querySelector('nav') : null);
553
+ expandAll(holderMobile ? holderMobile.querySelector('nav') : null);
554
+ } else {
555
+ applyCollapseState(holder ? holder.querySelector('nav') : null, 0);
556
+ applyCollapseState(holderMobile ? holderMobile.querySelector('nav') : null, 0);
557
+ }
558
+ }
559
+ initCollapse();
560
+
561
+ isMobile.addEventListener('change', function() {
562
+ if (isMobile.matches) {
563
+ expandAll(holder ? holder.querySelector('nav') : null);
564
+ expandAll(holderMobile ? holderMobile.querySelector('nav') : null);
565
+ } else {
566
+ applyCollapseState(holder ? holder.querySelector('nav') : null, prevActiveIdx);
567
+ applyCollapseState(holderMobile ? holderMobile.querySelector('nav') : null, prevActiveIdx);
568
+ }
569
+ });
570
+
571
+ // ---- Scroll tracking ----
572
+ var OFFSET = 60, lastScrollTime = 0, prevActiveIdx = 0;
573
+
574
+ function onScroll() {
575
  var now = performance.now();
576
  if (now - lastScrollTime < 50) return;
577
  lastScrollTime = now;
578
  requestAnimationFrame(function() {
579
+ var activeIdx = -1, activeId = null;
580
  for (var i = headingsArr.length - 1; i >= 0; i--) {
581
  if (headingsArr[i].getBoundingClientRect().top - OFFSET <= 0) {
582
+ activeIdx = i; activeId = headingsArr[i].id; break;
 
583
  }
584
  }
585
  allLinks.forEach(function(l) {
586
+ if (activeId && l.getAttribute('href') === '#'+activeId) l.classList.add('active');
587
+ else l.classList.remove('active');
 
 
 
588
  });
589
+ if (activeIdx !== prevActiveIdx && activeIdx >= 0 && !isMobile.matches) {
590
+ prevActiveIdx = activeIdx;
591
+ applyCollapseState(holder ? holder.querySelector('nav') : null, activeIdx);
592
+ }
593
+ if (activeIdx >= 0) prevActiveIdx = activeIdx;
594
  });
595
+ }
596
  window.addEventListener('scroll', onScroll, { passive: true });
597
  onScroll();
598
 
599
+ // ---- Mobile sidebar ----
600
  var sidebar = document.querySelector('.toc-mobile-sidebar');
601
  var backdrop = document.querySelector('.toc-mobile-backdrop');
602
  var toggleBtn = document.querySelector('.toc-mobile-toggle');
603
  var closeBtn = document.querySelector('.toc-mobile-sidebar__close');
604
 
605
+ function openSidebar() {
606
+ sidebar.classList.add('open'); backdrop.classList.add('open');
607
+ toggleBtn.setAttribute('aria-expanded','true');
 
608
  document.body.style.overflow = 'hidden';
609
  requestAnimationFrame(function() {
610
  var active = sidebar.querySelector('a.active');
611
  if (active) {
612
  var body = sidebar.querySelector('.toc-mobile-sidebar__body');
613
+ if (body) body.scrollTop = Math.max(0, active.offsetTop - body.offsetTop - body.clientHeight/3);
614
  }
615
  });
616
+ }
617
+ function closeSidebar() {
618
+ sidebar.classList.remove('open'); backdrop.classList.remove('open');
619
+ toggleBtn.setAttribute('aria-expanded','false');
 
620
  document.body.style.overflow = '';
621
+ }
 
622
  if (toggleBtn) toggleBtn.addEventListener('click', openSidebar);
623
  if (closeBtn) closeBtn.addEventListener('click', closeSidebar);
624
  if (backdrop) backdrop.addEventListener('click', closeSidebar);
625
+ if (holderMobile) holderMobile.addEventListener('click', function(e) { if (e.target.closest && e.target.closest('a')) closeSidebar(); });
626
+ document.addEventListener('keydown', function(e) { if (e.key==='Escape' && sidebar.classList.contains('open')) closeSidebar(); });
 
 
 
 
627
 
628
+ // Hash navigation
629
  if (window.location.hash) {
630
  var target = document.querySelector(window.location.hash);
631
+ if (target) setTimeout(function() { target.scrollIntoView({block:'start'}); }, 100);
632
  }
633
+ window.addEventListener('popstate', function() {
634
+ var h = window.location.hash;
635
+ if (h) { var t = document.querySelector(h); if (t) t.scrollIntoView({block:'start'}); }
636
+ else window.scrollTo({top:0});
637
+ });
638
  })();
639
  </script>
640
  </body>