Skip to content

Commit f9070e9

Browse files
committed
Focus reader on selected chapter
1 parent b54ad01 commit f9070e9

3 files changed

Lines changed: 144 additions & 9 deletions

File tree

assets/site.js

Lines changed: 136 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ const copy = {
328328
search: "搜索章节",
329329
top: "顶部",
330330
readerKicker: "Engineering Field Guide",
331+
chapterKicker: "章节片段",
331332
loading: "Loading guide...",
332333
loadError: "Could not load guide",
333334
pdf: "下载 PDF",
@@ -398,6 +399,7 @@ const copy = {
398399
search: "Search chapters",
399400
top: "Top",
400401
readerKicker: "Engineering Field Guide",
402+
chapterKicker: "Chapter Focus",
401403
loading: "Loading guide...",
402404
loadError: "Could not load guide",
403405
pdf: "Download PDF",
@@ -510,6 +512,8 @@ const state = {
510512
day: 1,
511513
headings: [],
512514
chapterTargets: [],
515+
chapterSections: [],
516+
focusedChapterIndex: null,
513517
observer: null,
514518
guidePromise: null,
515519
};
@@ -585,6 +589,7 @@ const els = {
585589
readerKicker: document.querySelector("#reader-kicker"),
586590
readerTitle: document.querySelector("#reader-title"),
587591
readerSummary: document.querySelector("#reader-summary"),
592+
readerMetrics: document.querySelector(".reader-metrics"),
588593
metricChapters: document.querySelector("#metric-chapters"),
589594
metricLanguages: document.querySelector("#metric-languages"),
590595
metricPdf: document.querySelector("#metric-pdf"),
@@ -620,6 +625,12 @@ function preferredDay() {
620625
return Number.isInteger(stored) && stored >= 1 && stored <= PLAN_DAY_COUNT ? stored : 1;
621626
}
622627

628+
function preferredChapter() {
629+
const params = new URLSearchParams(window.location.search);
630+
const requested = Number(params.get("chapter"));
631+
return Number.isInteger(requested) && requested >= 1 && requested <= CHAPTER_COUNT ? requested : null;
632+
}
633+
623634
function getStoredText(key) {
624635
try {
625636
return window.localStorage.getItem(key) || "";
@@ -824,6 +835,7 @@ function setLanguageChrome(lang) {
824835
els.metricChapters.innerHTML = `<strong>${CHAPTER_COUNT}</strong> ${doc.metrics.chapters}`;
825836
els.metricLanguages.innerHTML = `<strong>2</strong> ${doc.metrics.languages}`;
826837
els.metricPdf.innerHTML = `<strong>PDF</strong> ${doc.metrics.pdf}`;
838+
renderReaderHeader();
827839

828840
els.langButtons.forEach((button) => {
829841
const isActive = button.dataset.lang === lang;
@@ -858,10 +870,106 @@ function updateUrl() {
858870
params.set("lang", state.lang);
859871
params.set("day", String(state.day));
860872
params.set("view", state.mode);
861-
const hash = state.mode === "reader" ? window.location.hash : "";
873+
if (state.mode === "reader" && state.focusedChapterIndex) {
874+
params.set("chapter", String(state.focusedChapterIndex));
875+
} else {
876+
params.delete("chapter");
877+
}
878+
const hash = state.mode === "reader" && state.focusedChapterIndex ? window.location.hash : "";
862879
window.history.replaceState(null, "", `${window.location.pathname}?${params.toString()}${hash}`);
863880
}
864881

882+
function focusedChapterSection() {
883+
if (!state.focusedChapterIndex) return null;
884+
return state.chapterSections[state.focusedChapterIndex - 1] || null;
885+
}
886+
887+
function readerVisibleHeadings() {
888+
const headings = state.headings.filter((heading) => heading.depth === 2 || heading.depth === 3);
889+
const section = focusedChapterSection();
890+
if (!section) return headings;
891+
892+
const nodes = new Set(section.nodes);
893+
return headings.filter((heading) => nodes.has(heading.element));
894+
}
895+
896+
function renderReaderHeader() {
897+
const doc = docs[state.lang];
898+
const text = copy[state.lang];
899+
const section = focusedChapterSection();
900+
901+
if (section) {
902+
const chapter = chapters[state.lang][section.chapterIndex - 1];
903+
els.readerKicker.textContent = text.chapterKicker;
904+
els.readerTitle.textContent = section.text;
905+
els.readerSummary.textContent = chapter?.focus || doc.summary;
906+
els.readerMetrics.hidden = true;
907+
return;
908+
}
909+
910+
els.readerKicker.textContent = text.readerKicker;
911+
els.readerTitle.textContent = doc.title;
912+
els.readerSummary.textContent = doc.summary;
913+
els.readerMetrics.hidden = false;
914+
}
915+
916+
function updateReaderStatus() {
917+
const section = focusedChapterSection();
918+
els.status.textContent = section
919+
? `${docs[state.lang].status} · ${section.text}`
920+
: `${docs[state.lang].status} · ${state.chapterTargets.length} ${docs[state.lang].sections}`;
921+
}
922+
923+
function applyReaderFocus() {
924+
let section = focusedChapterSection();
925+
if (state.focusedChapterIndex && !section) {
926+
state.focusedChapterIndex = null;
927+
section = null;
928+
}
929+
930+
const visibleNodes = new Set(section?.nodes || []);
931+
els.article.classList.toggle("is-chapter-focused", Boolean(section));
932+
els.article.querySelectorAll(".is-focused-chapter-start").forEach((heading) => {
933+
heading.classList.remove("is-focused-chapter-start");
934+
});
935+
936+
[...els.article.children].forEach((node) => {
937+
node.hidden = Boolean(section) && !visibleNodes.has(node);
938+
});
939+
940+
if (section) {
941+
section.element.classList.add("is-focused-chapter-start");
942+
}
943+
944+
renderReaderHeader();
945+
updateReaderStatus();
946+
}
947+
948+
function setReaderFocus(chapterIndex) {
949+
const next = Number(chapterIndex);
950+
state.focusedChapterIndex = Number.isInteger(next) && next >= 1 && next <= CHAPTER_COUNT ? next : null;
951+
applyReaderFocus();
952+
renderToc();
953+
observeHeadings();
954+
updateReadingProgress();
955+
}
956+
957+
function currentHashTarget() {
958+
try {
959+
return decodeURIComponent(window.location.hash.slice(1));
960+
} catch {
961+
return window.location.hash.slice(1);
962+
}
963+
}
964+
965+
function syncReaderHashToFocus() {
966+
const section = focusedChapterSection();
967+
if (state.mode !== "reader" || !section) return;
968+
if (currentHashTarget() !== section.id) {
969+
window.location.hash = section.id;
970+
}
971+
}
972+
865973
function getDayTaskDefs(day, lang = state.lang) {
866974
const text = copy[lang];
867975
const schedule = getDaySchedule(day, lang);
@@ -1381,12 +1489,14 @@ async function loadGuide(lang, shouldUpdateUrl = true) {
13811489
els.article.classList.remove("loading");
13821490

13831491
postProcessArticle();
1492+
applyReaderFocus();
1493+
syncReaderHashToFocus();
13841494
renderToc();
13851495
observeHeadings();
13861496
updateReadingProgress();
13871497
window.lucide?.createIcons();
13881498

1389-
els.status.textContent = `${docs[lang].status} · ${state.chapterTargets.length} ${docs[lang].sections}`;
1499+
updateReaderStatus();
13901500

13911501
if (shouldUpdateUrl) updateUrl();
13921502

@@ -1457,14 +1567,29 @@ function postProcessArticle() {
14571567
});
14581568
mermaid.run({ querySelector: ".mermaid" });
14591569
}
1570+
1571+
const articleChildren = [...els.article.children];
1572+
state.chapterSections = state.chapterTargets
1573+
.map((target, index) => {
1574+
const start = articleChildren.indexOf(target.element);
1575+
const nextTarget = state.chapterTargets[index + 1];
1576+
const end = nextTarget ? articleChildren.indexOf(nextTarget.element) : articleChildren.length;
1577+
if (start < 0) return null;
1578+
1579+
return {
1580+
...target,
1581+
chapterIndex: index + 1,
1582+
nodes: articleChildren.slice(start, end > start ? end : articleChildren.length),
1583+
};
1584+
})
1585+
.filter(Boolean);
14601586
}
14611587

14621588
function renderToc() {
14631589
const query = els.search.value.trim().toLowerCase();
14641590
els.toc.innerHTML = "";
14651591

1466-
state.headings
1467-
.filter((heading) => heading.depth === 2 || heading.depth === 3)
1592+
readerVisibleHeadings()
14681593
.forEach((heading) => {
14691594
const item = document.createElement("li");
14701595
const link = document.createElement("a");
@@ -1502,7 +1627,7 @@ function observeHeadings() {
15021627
},
15031628
);
15041629

1505-
state.headings.forEach((heading) => state.observer.observe(heading.element));
1630+
readerVisibleHeadings().forEach((heading) => state.observer.observe(heading.element));
15061631
}
15071632

15081633
function updateReadingProgress() {
@@ -1517,11 +1642,11 @@ async function openChapter(chapterIndex) {
15171642
await state.guidePromise.catch(() => {});
15181643
}
15191644

1520-
setMode("reader");
1521-
15221645
const target = state.chapterTargets[chapterIndex - 1];
15231646
if (!target) return;
15241647

1648+
setReaderFocus(chapterIndex);
1649+
setMode("reader");
15251650
window.location.hash = target.id;
15261651
window.setTimeout(() => {
15271652
target.element.scrollIntoView({ behavior: "smooth", block: "start" });
@@ -1548,6 +1673,9 @@ document.addEventListener("click", (event) => {
15481673

15491674
const modeButton = event.target.closest("button[data-mode]");
15501675
if (modeButton) {
1676+
if (modeButton.dataset.mode === "reader") {
1677+
setReaderFocus(null);
1678+
}
15511679
setMode(modeButton.dataset.mode);
15521680
return;
15531681
}
@@ -1624,6 +1752,7 @@ window.addEventListener("resize", updateReadingProgress);
16241752
state.lang = preferredLanguage();
16251753
state.mode = preferredMode();
16261754
state.day = preferredDay();
1755+
state.focusedChapterIndex = state.mode === "reader" ? preferredChapter() : null;
16271756
setLanguageChrome(state.lang);
16281757
renderPlanner();
16291758
renderSchedulePlan();

assets/styles.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,12 @@ button:disabled {
10511051
scroll-margin-top: 5.4rem;
10521052
}
10531053

1054+
.prose.is-chapter-focused h2.is-focused-chapter-start {
1055+
margin-top: 0;
1056+
padding-top: 0;
1057+
border-top: 0;
1058+
}
1059+
10541060
.prose h1,
10551061
.prose h2,
10561062
.prose h3,

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
<meta name="twitter:card" content="summary" />
2020
<link rel="canonical" href="https://lling0000.github.io/Vibe_coding_guide/" />
2121
<title>Vibe Coding Guide | AI Coding 工程工作流手册</title>
22-
<link rel="stylesheet" href="./assets/styles.css?v=20260617-plan" />
22+
<link rel="stylesheet" href="./assets/styles.css?v=20260617-chapter" />
2323
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
2424
<script defer src="https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js"></script>
2525
<script defer src="https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.min.js"></script>
2626
<script defer src="https://cdn.jsdelivr.net/npm/lucide@0.475.0/dist/umd/lucide.min.js"></script>
27-
<script defer src="./assets/site.js?v=20260617-plan"></script>
27+
<script defer src="./assets/site.js?v=20260617-chapter"></script>
2828
</head>
2929
<body data-mode="planner">
3030
<div class="site-progress" aria-hidden="true">

0 commit comments

Comments
 (0)