Skip to content

Commit 14c68b3

Browse files
committed
Adds table of contents
1 parent 7aecf04 commit 14c68b3

4 files changed

Lines changed: 225 additions & 3 deletions

File tree

content/mapping-the-flood-cn.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "绘测洪流"
33
date: 2025-01-01
44
layout: "viz-article"
5-
description: "AI智能体的扩散,2025–2026"
5+
description: "AI智能体的扩散"
66
tags: ["ai", "agents", "research", "autonomy"]
77
---
88

content/mapping-the-flood-es.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "Cartografía del Diluvio"
33
date: 2025-01-01
44
layout: "viz-article"
5-
description: "La Proliferación de los Agentes de IA, 2025–2026"
5+
description: "La Proliferación de los Agentes de IA"
66
tags: ["ia", "agentes", "investigación", "autonomía"]
77
---
88

content/mapping-the-flood.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "Mapping the Flood"
33
date: 2025-01-01
44
layout: "viz-article"
5-
description: "The Proliferation of AI Agents, 2025–2026"
5+
description: "The Proliferation of AI Agents"
66
tags: ["ai", "agents", "research", "autonomy"]
77
---
88

layouts/_default/viz-article.html

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
{{- $scrollLabel := "Scroll" }}
99
{{- if eq $lang "es" }}{{ $scrollLabel = "Bajar" }}{{ end }}
1010
{{- if eq $lang "cn" }}{{ $scrollLabel = "向下" }}{{ end }}
11+
{{- $tocLabel := "Table of contents" }}
12+
{{- if eq $lang "es" }}{{ $tocLabel = "Índice" }}{{ end }}
13+
{{- if eq $lang "cn" }}{{ $tocLabel = "目录" }}{{ end }}
1114
<html lang="{{ $htmlLang }}">
1215
<head>
1316
<meta charset="UTF-8" />
@@ -378,6 +381,103 @@
378381
/* Back-reference arrow */
379382
.article-inner .footnote-backref { margin-left: 4px; }
380383

384+
/* ─────────────────────────────────────────────────────────────
385+
TABLE OF CONTENTS — fixed left gutter, non-intrusive
386+
Visible only on viewports wide enough (≥ 1180px) to have
387+
left gutter space beside the 720px centred article column.
388+
───────────────────────────────────────────────────────────── */
389+
#toc-nav {
390+
position: fixed;
391+
/* Vertically centred in viewport */
392+
top: 50%;
393+
transform: translateY(-50%);
394+
/* Left-gutter: gap from article left edge ≈ 16px */
395+
left: max(12px, calc(50vw - 576px));
396+
width: 200px;
397+
max-height: 76vh;
398+
overflow-y: auto;
399+
overflow-x: hidden;
400+
scrollbar-width: none;
401+
z-index: 49;
402+
/* Hidden until article is fully in view */
403+
opacity: 0;
404+
pointer-events: none;
405+
transition: opacity 0.55s ease;
406+
}
407+
#toc-nav::-webkit-scrollbar { display: none; }
408+
409+
/* Revealed by JS once article is fully scrolled into view */
410+
#toc-nav.toc-visible {
411+
opacity: 1;
412+
pointer-events: auto;
413+
}
414+
415+
#toc-nav ul {
416+
list-style: none;
417+
margin: 0;
418+
padding: 0;
419+
}
420+
#toc-nav li {
421+
margin: 0;
422+
padding: 0;
423+
}
424+
425+
/* Base link style — h2 chapters (primary nav items) */
426+
.toc-link {
427+
display: block;
428+
font-family: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
429+
font-size: 11px;
430+
line-height: 1.45;
431+
font-weight: 400;
432+
color: rgba(230, 237, 243, 0.28);
433+
text-decoration: none;
434+
padding: 2.5px 4px 2.5px 11px;
435+
border-left: 1.5px solid transparent;
436+
/* Single-line with ellipsis for chapters */
437+
white-space: nowrap;
438+
overflow: hidden;
439+
text-overflow: ellipsis;
440+
transition: color 0.18s ease, border-color 0.18s ease;
441+
}
442+
.toc-link:hover {
443+
color: rgba(230, 237, 243, 0.78);
444+
text-decoration: none;
445+
}
446+
.toc-link.toc-active {
447+
color: rgba(154, 180, 255, 0.92);
448+
border-left-color: rgba(154, 180, 255, 0.48);
449+
font-weight: 500;
450+
}
451+
452+
/* h1 PART headings — dim uppercase section-divider labels */
453+
.toc-link.toc-h1 {
454+
font-size: 9px;
455+
font-weight: 700;
456+
letter-spacing: 0.09em;
457+
text-transform: uppercase;
458+
color: rgba(154, 180, 255, 0.20);
459+
padding-left: 0;
460+
border-left: none;
461+
margin-top: 10px;
462+
/* Allow wrapping so long translated PART titles fit */
463+
white-space: normal;
464+
overflow: visible;
465+
text-overflow: clip;
466+
line-height: 1.3;
467+
}
468+
.toc-link.toc-h1:hover {
469+
color: rgba(154, 180, 255, 0.52);
470+
}
471+
.toc-link.toc-h1.toc-active {
472+
color: rgba(154, 180, 255, 0.52);
473+
border-left: none;
474+
}
475+
476+
/* Hide entirely on viewports too narrow for the left gutter */
477+
@media (max-width: 1180px) {
478+
#toc-nav { display: none; }
479+
}
480+
381481
/* ─────────────────────────────────────────────────────────────
382482
KEYFRAMES
383483
───────────────────────────────────────────────────────────── */
@@ -461,6 +561,12 @@ <h1>{{ .Title }}</h1>
461561
</div>
462562
</div>
463563

564+
<!-- ═══════════════════════════════════════════════════════════
565+
TABLE OF CONTENTS — built by JS, fixed to the left gutter.
566+
Language-agnostic: reads headings from the rendered DOM.
567+
═══════════════════════════════════════════════════════════ -->
568+
<nav id="toc-nav" role="navigation" aria-label="{{ $tocLabel }}"></nav>
569+
464570

465571
<!-- ═══════════════════════════════════════════════════════════
466572
CANVAS ANIMATION — particle swarm visualization
@@ -988,5 +1094,121 @@ <h1>{{ .Title }}</h1>
9881094
})();
9891095
</script>
9901096

1097+
1098+
<!-- ═══════════════════════════════════════════════════════════
1099+
TABLE OF CONTENTS — scroll-driven, language-agnostic.
1100+
Builds itself from the rendered heading elements so it works
1101+
for English, Spanish, and Chinese with zero extra markup.
1102+
═══════════════════════════════════════════════════════════ -->
1103+
<script>
1104+
(() => {
1105+
'use strict';
1106+
1107+
const tocNav = document.getElementById('toc-nav');
1108+
const articleInner = document.querySelector('#article-wrap .article-inner');
1109+
if (!tocNav || !articleInner) return;
1110+
1111+
/* ── Build TOC list from h1 + h2 headings ──────────────── */
1112+
const ul = document.createElement('ul');
1113+
tocNav.appendChild(ul);
1114+
1115+
// Collect every h1 and h2 in document order
1116+
const allHeadings = [...articleInner.querySelectorAll('h1, h2')];
1117+
const items = []; // { el, id, link }
1118+
let firstH1Skipped = false;
1119+
1120+
allHeadings.forEach(el => {
1121+
// Hugo goldmark always adds an id to headings; skip any that don't have one
1122+
if (!el.id) return;
1123+
1124+
const tag = el.tagName.toLowerCase(); // 'h1' or 'h2'
1125+
const text = el.textContent.trim();
1126+
1127+
// Skip the document-title h1 (first h1 in .article-inner)
1128+
// It duplicates the title already shown in the fullscreen viz
1129+
if (tag === 'h1' && !firstH1Skipped) {
1130+
firstH1Skipped = true;
1131+
return;
1132+
}
1133+
1134+
const a = document.createElement('a');
1135+
a.href = '#' + el.id;
1136+
a.textContent = text;
1137+
a.title = text; // full text tooltip when truncated
1138+
a.className = 'toc-link toc-' + tag;
1139+
a.dataset.tocId = el.id;
1140+
1141+
// Smooth-scroll on click without changing the URL hash abruptly
1142+
a.addEventListener('click', e => {
1143+
e.preventDefault();
1144+
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
1145+
});
1146+
1147+
const li = document.createElement('li');
1148+
li.appendChild(a);
1149+
ul.appendChild(li);
1150+
items.push({ el, id: el.id, link: a });
1151+
});
1152+
1153+
/* ── Active-section tracking ───────────────────────────── */
1154+
let rafPending = false;
1155+
let lastActiveId = null;
1156+
1157+
function updateToc() {
1158+
rafPending = false;
1159+
1160+
const scrollY = window.scrollY;
1161+
const vh = window.innerHeight;
1162+
1163+
// TOC is only shown after the viz has fully scrolled away
1164+
if (scrollY >= vh) {
1165+
tocNav.classList.add('toc-visible');
1166+
} else {
1167+
tocNav.classList.remove('toc-visible');
1168+
return; // nothing more to do while in the viz
1169+
}
1170+
1171+
// Active item = the last heading whose top edge is at or above
1172+
// 15% down the viewport (a comfortable reading-ahead threshold)
1173+
const threshold = Math.min(120, vh * 0.15);
1174+
let activeId = null;
1175+
1176+
for (const { el, id } of items) {
1177+
if (el.getBoundingClientRect().top <= threshold) activeId = id;
1178+
}
1179+
1180+
// Only update DOM if the active section actually changed
1181+
if (activeId === lastActiveId) return;
1182+
lastActiveId = activeId;
1183+
1184+
let activeLink = null;
1185+
items.forEach(({ id, link }) => {
1186+
const on = (id === activeId);
1187+
link.classList.toggle('toc-active', on);
1188+
if (on) activeLink = link;
1189+
});
1190+
1191+
// Auto-scroll the TOC panel so the active item stays visible
1192+
if (activeLink) {
1193+
const navBox = tocNav.getBoundingClientRect();
1194+
const lnkBox = activeLink.getBoundingClientRect();
1195+
if (lnkBox.top < navBox.top + 20 || lnkBox.bottom > navBox.bottom - 20) {
1196+
activeLink.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
1197+
}
1198+
}
1199+
}
1200+
1201+
window.addEventListener('scroll', () => {
1202+
if (!rafPending) {
1203+
rafPending = true;
1204+
requestAnimationFrame(updateToc);
1205+
}
1206+
}, { passive: true });
1207+
1208+
// Handle the case where the page is loaded/refreshed mid-scroll
1209+
updateToc();
1210+
})();
1211+
</script>
1212+
9911213
</body>
9921214
</html>

0 commit comments

Comments
 (0)