mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-10 16:04:55 +00:00
site(learn): active section highlight + other-essays switcher on left rail
- Added scroll-spy to the chapter rail: as you scroll, the H2 closest to the upper third of the viewport becomes the active link (white background, sky text, sky ring). - Below the chapter rail, an 'Other essays' list shows every other learn article (6 on each page) with read-time, plus an 'All articles' link back to the library.
This commit is contained in:
@@ -53,11 +53,37 @@ const next = related.filter((r) => r.href !== path).slice(0, 2);
|
||||
|
||||
<!-- LEFT RAIL -->
|
||||
<aside class="hidden lg:block border-r border-[color:var(--border)] bg-[color:var(--surface-1)]/40">
|
||||
<div class="sticky top-0 max-h-screen overflow-y-auto py-8 px-6">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.22em] font-mono text-muted-foreground mb-4">On this page</div>
|
||||
<ol class="learn-chapter-rail space-y-px text-[13px]" data-chapter-rail>
|
||||
<li class="text-[11.5px] text-muted-foreground/60 italic">Loading…</li>
|
||||
</ol>
|
||||
<div class="sticky top-0 max-h-screen overflow-y-auto py-8 px-6 flex flex-col gap-7">
|
||||
<!-- On this page -->
|
||||
<div>
|
||||
<div class="text-[10.5px] uppercase tracking-[0.22em] font-mono text-muted-foreground mb-4">On this page</div>
|
||||
<ol class="learn-chapter-rail space-y-px text-[13px]" data-chapter-rail>
|
||||
<li class="text-[11.5px] text-muted-foreground/60 italic">Loading…</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- Other articles in the library -->
|
||||
<div class="pt-6 border-t border-[color:var(--border)]">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.22em] font-mono text-muted-foreground mb-4 flex items-center justify-between">
|
||||
<span>Other essays</span>
|
||||
<span class="font-mono text-[10.5px] text-muted-foreground/70">{related.length - 1}</span>
|
||||
</div>
|
||||
<ol class="space-y-px text-[13px]">
|
||||
{related.filter((r) => r.href !== path).map((r, i) => (
|
||||
<li>
|
||||
<a href={r.href} class="group flex items-baseline gap-3 py-1.5 px-2 -mx-2 rounded-md text-foreground/65 hover:bg-white hover:text-[#0369a1] transition-colors">
|
||||
<span class="font-mono text-[10.5px] text-muted-foreground/60 w-5 shrink-0">{String(i + 1).padStart(2, '0')}</span>
|
||||
<span class="leading-snug flex-1 truncate">{r.title}</span>
|
||||
<span class="font-mono text-[10px] text-muted-foreground/55 shrink-0">{r.read.replace(' read', '').replace(' min', 'm')}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<a href="/learn/" class="mt-4 inline-flex items-center gap-1.5 text-[11.5px] font-mono text-[#0369a1] hover:text-[#075985]">
|
||||
All articles
|
||||
<Icon name="arrowRight" size={10} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -113,26 +139,71 @@ const next = related.filter((r) => r.href !== path).slice(0, 2);
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
// Auto-build chapter rail from H2 elements in the prose body
|
||||
// Auto-build chapter rail from H2 elements + scroll-spy active state
|
||||
(function () {
|
||||
const rail = document.querySelector('[data-chapter-rail]');
|
||||
const main = document.querySelector('.learn-prose');
|
||||
if (!rail || !main) return;
|
||||
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
const h2s = main.querySelectorAll('h2');
|
||||
const h2s = Array.from(main.querySelectorAll('h2'));
|
||||
if (!h2s.length) return;
|
||||
|
||||
const baseClass = 'group flex items-baseline gap-3 py-1.5 px-2 -mx-2 rounded-md transition-colors';
|
||||
const inactiveClass = ' text-foreground/70 hover:bg-white hover:text-[#0369a1]';
|
||||
const activeClass = ' bg-white text-[#0369a1] font-semibold ring-1 ring-[color:var(--sky-2)]';
|
||||
|
||||
// Build rail
|
||||
rail.innerHTML = '';
|
||||
const links = [];
|
||||
h2s.forEach((h2, i) => {
|
||||
if (!h2.id) h2.id = slug(h2.textContent || `section-${i + 1}`);
|
||||
if (!h2.id) h2.id = slug(h2.textContent || 'section-' + (i + 1));
|
||||
const li = document.createElement('li');
|
||||
const a = document.createElement('a');
|
||||
a.href = `#${h2.id}`;
|
||||
a.className = 'group flex items-baseline gap-3 py-1.5 px-2 -mx-2 rounded-md text-foreground/70 hover:bg-white hover:text-[#0369a1] transition-colors';
|
||||
a.innerHTML = `<span class="font-mono text-[10.5px] text-muted-foreground/70 w-5 shrink-0">${String(i + 1).padStart(2, '0')}</span><span class="leading-snug">${h2.textContent}</span>`;
|
||||
a.href = '#' + h2.id;
|
||||
a.className = baseClass + inactiveClass;
|
||||
a.innerHTML = '<span class="font-mono text-[10.5px] text-muted-foreground/70 w-5 shrink-0 chapter-num">' + String(i + 1).padStart(2, '0') + '</span><span class="leading-snug">' + h2.textContent + '</span>';
|
||||
li.appendChild(a);
|
||||
rail.appendChild(li);
|
||||
links.push({ id: h2.id, el: a });
|
||||
});
|
||||
|
||||
// Scroll-spy: highlight closest H2 above viewport mid-line
|
||||
let currentId = null;
|
||||
function setActive(id) {
|
||||
if (id === currentId) return;
|
||||
currentId = id;
|
||||
links.forEach((l) => {
|
||||
if (l.id === id) {
|
||||
l.el.className = baseClass + activeClass;
|
||||
} else {
|
||||
l.el.className = baseClass + inactiveClass;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
const probe = window.innerHeight * 0.35;
|
||||
let active = h2s[0].id;
|
||||
for (const h2 of h2s) {
|
||||
const rect = h2.getBoundingClientRect();
|
||||
if (rect.top - probe <= 0) {
|
||||
active = h2.id;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
setActive(active);
|
||||
}
|
||||
|
||||
onScroll();
|
||||
let raf = 0;
|
||||
window.addEventListener('scroll', () => {
|
||||
if (raf) return;
|
||||
raf = requestAnimationFrame(() => {
|
||||
raf = 0;
|
||||
onScroll();
|
||||
});
|
||||
}, { passive: true });
|
||||
})();
|
||||
</script>
|
||||
</Layout>
|
||||
|
||||
Reference in New Issue
Block a user