Skip to content

Commit 34fac02

Browse files
Add sidebar section jump links with mark-done tracking
- Inject .step-subnav placeholder after active step link in sidebar - After each step renders, scan h2 + Task/Step h3 headings from the markdown and build clickable jump links in the subnav - Each jump link has a small mark-done button (○ / ✓) that: * Toggles done state per section per step * Persists to localStorage (cli-workshop-section-done:{stepId}) * Restores on page load (checked state shown immediately) - Clicking a link smooth-scrolls the content pane to that heading - Completed sections show green ✓ and dimmed link text - CSS: .step-subnav, .subnav-item, .subnav-h3, .subnav-done-btn in styles.css Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1b27350 commit 34fac02

2 files changed

Lines changed: 140 additions & 1 deletion

File tree

docs/step.html

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,13 @@
9696
nav.innerHTML = STEPS.map(s => {
9797
const isCurrent = s.id === currentId;
9898
const isDone = done.includes(s.id) && !isCurrent;
99-
return `<a href="${buildStepURL(s.id)}" class="step-link${isCurrent ? ' active' : ''}${isDone ? ' done' : ''}">
99+
const link = `<a href="${buildStepURL(s.id)}" class="step-link${isCurrent ? ' active' : ''}${isDone ? ' done' : ''}">
100100
<span class="step-num">${isDone ? '✓' : s.number}</span>
101101
<span class="step-name">${s.title}</span>
102102
<span class="step-dur">${s.time}</span>
103103
</a>`;
104+
const subnav = isCurrent ? `<div class="step-subnav" id="stepSubnav"></div>` : '';
105+
return link + subnav;
104106
}).join('');
105107
const pct = Math.round(done.length / STEPS.length * 100);
106108
document.getElementById('progressFill').style.width = pct + '%';
@@ -191,6 +193,7 @@
191193
fixCheckboxes();
192194
rewriteLinks(stepId);
193195
initOsTabs();
196+
buildSubnav(stepId);
194197
markDone(stepId);
195198
document.getElementById('mainContent').scrollTop = 0;
196199
buildSidebar(stepId); // refresh to show done
@@ -209,6 +212,82 @@
209212
}
210213

211214
const OS_TAB_KEY = 'cli-workshop-os-tab';
215+
const SECTION_DONE_PREFIX = 'cli-workshop-section-done:';
216+
217+
function getSectionDone(stepId) {
218+
try { return JSON.parse(localStorage.getItem(SECTION_DONE_PREFIX + stepId) || '[]'); } catch { return []; }
219+
}
220+
221+
function toggleSectionDone(stepId, anchor, btn) {
222+
let done = getSectionDone(stepId);
223+
const wasDone = done.includes(anchor);
224+
if (wasDone) {
225+
done = done.filter(a => a !== anchor);
226+
} else {
227+
done.push(anchor);
228+
}
229+
try { localStorage.setItem(SECTION_DONE_PREFIX + stepId, JSON.stringify(done)); } catch {}
230+
const item = btn.closest('.subnav-item');
231+
if (!wasDone) {
232+
item.classList.add('done');
233+
btn.classList.add('done');
234+
btn.textContent = '✓';
235+
btn.title = 'Mark undone';
236+
} else {
237+
item.classList.remove('done');
238+
btn.classList.remove('done');
239+
btn.textContent = '○';
240+
btn.title = 'Mark done';
241+
}
242+
}
243+
244+
function scrollToSection(anchor) {
245+
const el = document.getElementById(anchor);
246+
const container = document.getElementById('mainContent');
247+
if (el && container) {
248+
const elTop = el.getBoundingClientRect().top;
249+
const containerTop = container.getBoundingClientRect().top;
250+
container.scrollBy({ top: elTop - containerTop - 80, behavior: 'smooth' });
251+
}
252+
return false;
253+
}
254+
255+
function buildSubnav(stepId) {
256+
const subnavEl = document.getElementById('stepSubnav');
257+
if (!subnavEl) return;
258+
259+
const saved = getSectionDone(stepId);
260+
const headings = [...document.querySelectorAll('.markdown h2, .markdown h3')];
261+
262+
const items = headings.filter(h => {
263+
const text = h.textContent.trim();
264+
if (text.startsWith('✅')) return false;
265+
if (h.tagName === 'H2') return true;
266+
if (h.tagName === 'H3' && /^(Task|Step)\s*\d+/i.test(text)) return true;
267+
return false;
268+
});
269+
270+
if (!items.length) { subnavEl.style.display = 'none'; return; }
271+
272+
items.forEach(h => {
273+
if (!h.id) {
274+
h.id = h.textContent.trim().toLowerCase().replace(/[^\w]+/g, '-').replace(/^-|-$/g, '');
275+
}
276+
});
277+
278+
subnavEl.innerHTML = items.map(h => {
279+
const anchor = h.id;
280+
const isH3 = h.tagName === 'H3';
281+
const isDone = saved.includes(anchor);
282+
const label = h.textContent.trim();
283+
return `<div class="subnav-item${isH3 ? ' subnav-h3' : ''}${isDone ? ' done' : ''}">
284+
<a onclick="return scrollToSection('${anchor}')" href="#">${label}</a>
285+
<button class="subnav-done-btn${isDone ? ' done' : ''}"
286+
onclick="toggleSectionDone('${stepId}','${anchor}',this)"
287+
title="${isDone ? 'Mark undone' : 'Mark done'}">${isDone ? '✓' : '○'}</button>
288+
</div>`;
289+
}).join('');
290+
}
212291

213292
function switchTab(btn, tabName) {
214293
const group = btn.closest('.tab-group');

docs/styles.css

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,66 @@ footer {
384384
.step-link.done .step-num { background: var(--accent); border-color: var(--accent); color: #fff; }
385385
.step-name { flex: 1; }
386386
.step-dur { font-size: 11px; color: var(--text-subtle); font-family: var(--font-mono); }
387+
388+
/* === Section jump subnav (shown under active step) === */
389+
.step-subnav {
390+
margin: 0 0 6px 36px;
391+
padding: 0;
392+
border-left: 1px solid var(--border-subtle);
393+
}
394+
.subnav-item {
395+
display: flex;
396+
align-items: center;
397+
gap: 2px;
398+
padding-right: 8px;
399+
}
400+
.subnav-item > a {
401+
flex: 1;
402+
font-size: 11.5px;
403+
color: var(--text-subtle);
404+
text-decoration: none !important;
405+
padding: 3px 0 3px 10px;
406+
line-height: 1.3;
407+
transition: color 0.15s;
408+
cursor: pointer;
409+
display: block;
410+
}
411+
.subnav-item > a:hover { color: var(--text); text-decoration: none !important; }
412+
.subnav-item.done > a {
413+
color: var(--accent-light);
414+
opacity: 0.65;
415+
}
416+
.subnav-h3 > a {
417+
padding-left: 20px;
418+
font-size: 11px;
419+
}
420+
.subnav-done-btn {
421+
background: none;
422+
border: 1px solid transparent;
423+
border-radius: 3px;
424+
width: 18px; height: 18px;
425+
display: flex; align-items: center; justify-content: center;
426+
font-size: 10px;
427+
color: var(--text-subtle);
428+
cursor: pointer;
429+
flex-shrink: 0;
430+
opacity: 0.4;
431+
transition: all 0.15s;
432+
padding: 0;
433+
line-height: 1;
434+
font-family: var(--font-mono);
435+
}
436+
.subnav-done-btn:hover {
437+
opacity: 1;
438+
border-color: var(--border);
439+
color: var(--text);
440+
}
441+
.subnav-done-btn.done {
442+
opacity: 1;
443+
color: var(--accent-light);
444+
border-color: var(--accent);
445+
background: rgba(35,134,54,0.15);
446+
}
387447
.sidebar-footer {
388448
padding: 12px 16px;
389449
border-top: 1px solid var(--border);

0 commit comments

Comments
 (0)