Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions apps/web/src/app/components/LegalPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Link from "next/link";
import type { ReactNode } from "react";
import { PublicHeader } from "@/app/components/PublicHeader";
import { TableOfContents } from "@/app/components/TableOfContents";
import { formatDate, localePath, type Locale } from "@/i18n/config";
import { getDictionary } from "@/i18n/messages";

Expand Down Expand Up @@ -47,13 +48,7 @@ export function LegalPage({ locale, document, effectiveDate }: LegalPageProps) {
<section className="site-wrap grid gap-10 pb-20 lg:grid-cols-[220px_1fr]">
<aside className="lg:sticky lg:top-24 lg:self-start">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--ink-3)]">{t.onThisPage}</p>
<nav className="site-toc mt-3">
{document.sections.map((section) => (
<a key={section.id} href={`#${section.id}`}>
{section.title}
</a>
))}
</nav>
<TableOfContents className="mt-3" items={document.sections.map((section) => ({ id: section.id, label: section.title }))} />
<p className="mt-8 text-xs font-semibold uppercase tracking-[0.14em] text-[var(--ink-3)]">{t.related}</p>
<nav className="site-toc mt-3">
{legalLinks.map((link) => (
Expand All @@ -66,7 +61,7 @@ export function LegalPage({ locale, document, effectiveDate }: LegalPageProps) {

<article className="site-card site-prose max-w-3xl p-6 md:p-10">
{document.sections.map((section) => (
<section key={section.id} id={section.id}>
<section key={section.id} id={section.id} className="scroll-mt-24">
<h2>{section.title}</h2>
{section.content}
</section>
Expand Down
52 changes: 52 additions & 0 deletions apps/web/src/app/components/TableOfContents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import { useEffect, useState } from "react";

export type TocItem = { id: string; label: string };

// A section becomes current once its top crosses this line, just under the sticky header.
const activationLine = 120;

export function TableOfContents({ items, className = "" }: { items: TocItem[]; className?: string }) {
const [activeId, setActiveId] = useState(items[0]?.id);

useEffect(() => {
const sections = items.flatMap((item) => document.getElementById(item.id) ?? []);
if (!sections.length) return;

let frame = 0;
const update = () => {
frame = 0;
const atBottom = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2;
// Short closing sections can never reach the line, so at the bottom take the last one on screen.
const line = atBottom ? window.innerHeight : activationLine;
let current = sections[0];
for (const section of sections) {
if (section.getBoundingClientRect().top <= line) current = section;
}
setActiveId(current.id);
};
const schedule = () => {
if (!frame) frame = requestAnimationFrame(update);
};

update();
window.addEventListener("scroll", schedule, { passive: true });
window.addEventListener("resize", schedule);
return () => {
cancelAnimationFrame(frame);
window.removeEventListener("scroll", schedule);
window.removeEventListener("resize", schedule);
};
}, [items]);

return (
<nav className={`site-toc ${className}`}>
{items.map((item) => (
<a key={item.id} href={`#${item.id}`} aria-current={item.id === activeId ? "location" : undefined}>
{item.label}
</a>
))}
</nav>
);
}
18 changes: 9 additions & 9 deletions apps/web/src/app/components/docs/ContentArticle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ArrowLeft, ArrowRight, Play } from "lucide-react";
import { getLessonCourse } from "@/lib/lesson-syllabi";
import { CourseReferences } from "./CourseReferences";
import { PublicHeader } from "@/app/components/PublicHeader";
import { TableOfContents } from "@/app/components/TableOfContents";
import { DocSectionView } from "./DocSectionView";
import { RichText } from "./DiagramBlocks";
import { formatDate, localePath, localeTags, localeUrl, type Locale } from "@/i18n/config";
Expand Down Expand Up @@ -218,15 +219,14 @@ export function ContentArticle({ locale, doc }: { locale: Locale; doc: Doc }) {
<aside className="max-xl:hidden">
<div className="sticky top-20">
<p className="px-2 text-xs font-semibold text-[var(--text3)]">{t.onThisPage}</p>
<nav className="site-toc mt-2">
{doc.sections.map((section, index) => (
<a key={section.heading} href={`#${sectionId(index)}`}>
{section.heading}
</a>
))}
{doc.keyTerms?.length ? <a href="#key-terms">{t.keyTerms}</a> : null}
{doc.quiz?.length ? <a href="#check-yourself">{t.checkYourself}</a> : null}
</nav>
<TableOfContents
className="mt-2"
items={[
...doc.sections.map((section, index) => ({ id: sectionId(index), label: section.heading })),
...(doc.keyTerms?.length ? [{ id: "key-terms", label: t.keyTerms }] : []),
...(doc.quiz?.length ? [{ id: "check-yourself", label: t.checkYourself }] : []),
]}
/>
</div>
</aside>
</div>
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,11 @@ code {
color: var(--text);
}

.site-toc a[aria-current="location"] {
background: var(--selected);
color: var(--text);
}

.site :where(a, button, summary):focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
Expand Down
Loading