Add sticky chapter outline for the reading demo

This commit is contained in:
pat
2026-09-01 03:44:04 +00:00
parent 5f24a406cf
commit d7c6a239c1
@@ -0,0 +1,99 @@
import { useEffect, useMemo, useState } from "react";
function OutlineBody({ groups, complete, count, total, active, showTitle }) {
return (
<>
<p className="outline-progress">
{count}/{total} Try-its complete
</p>
{showTitle ? <p className="outline-heading">In this chapter</p> : null}
{groups.map((g) => (
<div key={g.label} className="outline-group">
<p className="outline-group-label spec">{g.label}</p>
<ul>
{g.items.map((item) => {
const done = item.recipeId ? !!complete[item.recipeId] : false;
const isActive = active === item.id;
return (
<li key={item.id}>
<a
href={`#${item.id}`}
className={isActive ? "is-active" : undefined}
aria-current={isActive ? "location" : undefined}
>
<span>{item.label}</span>
{done ? (
<span className="outline-done" aria-label="Try-it complete">
✓
</span>
) : null}
</a>
</li>
);
})}
</ul>
</div>
))}
</>
);
}
export default function ChapterOutline({
groups,
complete = {},
count = 0,
total = 0,
variant = "desktop",
}) {
const ids = useMemo(
() => groups.flatMap((g) => g.items.map((item) => item.id)),
[groups]
);
const [active, setActive] = useState(ids[0] || "");
useEffect(() => {
const els = ids.map((id) => document.getElementById(id)).filter(Boolean);
if (!els.length || typeof IntersectionObserver === "undefined") return;
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((e) => e.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
if (visible[0]?.target?.id) setActive(visible[0].target.id);
},
{ rootMargin: "-88px 0px -55% 0px", threshold: [0, 0.1, 0.25, 0.5, 1] }
);
els.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, [ids]);
if (variant === "mobile") {
return (
<details className="chapter-outline-mobile">
<summary>In this chapter</summary>
<OutlineBody
groups={groups}
complete={complete}
count={count}
total={total}
active={active}
showTitle={false}
/>
</details>
);
}
return (
<nav className="chapter-outline" aria-label="In this chapter">
<OutlineBody
groups={groups}
complete={complete}
count={count}
total={total}
active={active}
showTitle
/>
</nav>
);
}