Skip to content

feat(toc): add animated On this page table of contents#3

Open
edwintantawi wants to merge 9 commits into
chore/markdownfrom
chore/toc
Open

feat(toc): add animated On this page table of contents#3
edwintantawi wants to merge 9 commits into
chore/markdownfrom
chore/toc

Conversation

@edwintantawi

Copy link
Copy Markdown
Member

Summary

Renders the derived heading data (already built at content time) as an animated "On this page" table of contents on post and series-post pages — the fuma-nama.dev SVG-rail treatment, recolored to this project's monochrome theme.

  • SVG rail: a thread through the headings whose accented thumb follows the range of headings currently in view. Indent changes curve via cubic Béziers with control points offset toward the opposite item, so steps read as rounded corners, not diagonals; the rail spans the full height of the first and last entries.
  • Scroll-spy: tracks the visible heading range on rAF-coalesced scroll/resize. Geometry is measured after mount and re-measured on reflow, so server and first client render stay identical (no hydration mismatch — the SVG only appears client-side).
  • Behavior: filters to h2/h3, normalizes indent to the shallowest heading, smooth-scrolls with a replaceState hash on click, respects prefers-reduced-motion.
  • Responsive: fixed in the right gutter beside the centered article at ≥ xl; below that the gutter collapses to a floating "On this page" trigger opening the same list in a popover.

Module

New src/modules/toc/:

File Role
toc.utils.ts normalizeEntries (h2/h3 filter + level normalization), rail geometry, buildThreadPath
toc.hooks.ts useActiveHeadings — scroll-spy returning the visible heading range
components/toc-thumb.tsx faint full rail + clip-path thumb that transitions to the active span
components/table-of-contents.tsx TableOfContents core + PageTableOfContents responsive shell

Wired into posts/$slug.tsx and series/$slug/$postSlug.tsx; added a .toc-scroll thin-scrollbar utility to app.css.

Verification

  • vp check clean (format, lint, types).
  • vp build prerenders the series-post page; SSR renders the accessible <nav> + all heading links with correct normalized indentation, and the SVG rail is correctly absent server-side.
  • Bézier geometry verified by rasterizing the path (rounded steps, zero diagonal segments; rail spans first-top → last-bottom).

Live scroll animation was not exercised in an automated browser — worth an eyeball on /series/markdown/markdown-style-guide.

Render the derived heading data as a fixed 'On this page' rail beside post
and series-post articles, with an SVG thread whose accented thumb follows
the headings currently in view. The connector curves at indent changes via
cubic Beziers (control points offset toward the opposite item so steps read
as rounded corners, not diagonals) and spans the full height of the first
and last entries.

Scroll-spy tracks the visible heading range on rAF-coalesced scroll/resize;
geometry is measured after mount and re-measured on reflow, so server and
first client render stay identical. Filters to h2/h3, normalizes indent to
the shallowest heading, smooth-scrolls with a replaceState hash on click,
and respects prefers-reduced-motion. Below xl the fixed gutter collapses to
a floating trigger opening the same list in a popover.
Center each bend on the shared edge between items and trim the thumb
clip by the bend inset at both ends, so a lone active heading reads as
a straight line of uniform height and the bend into the next indent
only shows once the headings on both of its sides are active.
…files

Extract PageTableOfContents into its own component file, move rail
geometry measurement into a useRailGeometry hook, relocate thumbRange
next to the other rail math in toc.utils, and consolidate shared
interfaces in toc.types. No behavior change.
Track atStart/atEnd scroll boundaries alongside the visible heading ids,
and render a dot at each end of the rail. The thumb's clip-path now
stretches over a boundary's dot while that state holds, so the highlight
morphs continuously between dot and line as the reader crosses the
article's edges.
Add useScrollActiveIntoView, which scrolls the nearest scrollable ancestor
so the topmost active heading stays centered when a long list outgrows the
fixed desktop aside or the mobile popover. Scrolls only that ancestor, never
the window, and no-ops while the list fits or nothing is active.

Raise the aside to top-8 so the taller list has room to scroll within the
viewport.
Fade the top/bottom edges of the TOC scroll area with scroll-driven
animations, revealing each fade only when there is more to scroll that
way. Give anchor jumps smooth scrolling that honours reduced-motion.
The floating "On this page" menu now adapts to three tiers: the fixed
gutter on desktop, a right-side sheet on tablet (md-xl), and a full-width
bottom bar opening a bottom drawer on mobile (below md), replacing the
single popover used below xl.
Splits the growing hooks/utils/types files into focused modules
(toc.rail.ts, toc.scroll.ts, toc.entries.ts) and renames the page
component to ArticleToc for clarity, updating call sites accordingly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant