Skip to content

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

Merged
edwintantawi merged 10 commits into
chore/markdownfrom
chore/toc
Jul 26, 2026
Merged

feat(toc): add animated On this page table of contents#3
edwintantawi merged 10 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/:

FileRole
toc.utils.tsnormalizeEntries (h2/h3 filter + level normalization), rail geometry, buildThreadPath
toc.hooks.tsuseActiveHeadings — scroll-spy returning the visible heading range
components/toc-thumb.tsxfaint full rail + clip-path thumb that transitions to the active span
components/table-of-contents.tsxTableOfContents 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.
- Use `import * as React from 'react'` in markdown and field components
- Move 'use client' from the markdown Tabs wrapper to the base-ui tabs
component where the client boundary actually lives
- Exclude lucide-react from client dep optimization to silence the
rsc:use-client inconsistently-optimized warning
- Rename rollupOptions to rolldownOptions for Rolldown-based Vite+
@edwintantawi
edwintantawi merged commit 110dd2b into chore/markdownJul 26, 2026
@edwintantawi
edwintantawi deleted the chore/toc branch July 26, 2026 16:20
edwintantawi added a commit that referenced this pull request Jul 26, 2026
* perf(fonts): preload Geist fonts via dedicated stylesheet
Split font imports out of app.css into their own stylesheet and preload
them from the root route, avoiding render-blocking font loads.
* feat(security): enable CSRF protection for server functions
Add createCsrfMiddleware to the start instance so server function
requests are validated, independent of any specific route.
* feat(blog): add MDX-powered post module with content collections
Introduce a content-collections pipeline that parses MDX posts under
src/content/posts, compiles them via @mdx-js/rollup, and streams the
rendered output through React Server Components so the compiled MDX
never ships to the client bundle.
- content-collections.ts defines the `posts` collection, validated
against post.schema.ts and enriched with git-derived last-modified
timestamps (modules/markdown/utils.ts).
- modules/post exposes server functions to list and load posts, used
by the new /posts and /posts/$slug routes.
- vite.config.ts wires up the MDX plugin, content-collections vite
plugin, and RSC support (needed to stream MDX without eval on
Workers), and enables typography via @tailwindcss/typography.
- Generalize the i18n pathname/prerender helpers
(lib/i18n/utils.ts) to accept an arbitrary route list instead of a
fixed config object, so the new /posts routes can be prerendered
per locale alongside /.
* feat(series): add MDX-powered series module with grouped multi-part posts
Introduce a series content type on top of the existing MDX pipeline:
- Add `series` and `seriesPost` content collections with frontmatter
validation, numeric order prefixes, and integrity checks that reject
duplicate orders and posts orphaned by a missing `_index.mdx`.
- Add the series module (server functions, schema, types) exposing list,
detail, and post loaders backed by RSC-streamed MDX.
- Add /series, /series/$slug, and /series/$slug/$postSlug routes plus a
home-page nav link and the /series prerender path.
- Move markdown-style-guide.mdx into a markdown series and add sample
frontend-development series content.
* chore(deps): add Shiki and remark-directive tooling for MDX
Pulls in the packages behind the new MDX pipeline: shiki and
rehype-pretty-code with @shikijs/transformers for compile-time syntax
highlighting, remark-directive with mdast-util-directive/mdx-jsx for the
:::tabs syntax, plus unist-util-visit, vfile and @types/mdast for the
custom remark plugin.
* feat(markdown): add MDX component overrides for links, code, and tabs
Replaces the empty component map with app-aware overrides handed to the
MDX runtime: Anchor routes internal links through TanStack Router and
opens external ones in an isolated tab; CodeBlock wraps fenced code with
a copy button that reads the block's rendered end state; and Tabs renders
the group emitted by the :::tabs directive.
* feat(markdown): highlight code and render :::tabs in the MDX pipeline
Extends the Vite MDX plugin with rehype-pretty-code (dual GitHub themes
emitted as CSS variables, toggled by .dark) and the Shiki notation
transformers for diff/focus/error/warning lines and indent guides. Adds a
remark-tabs plugin that rewrites :::tabs / ::tab[Label] container
directives into <Tabs> elements, and styles all of it in a new
markdown.css imported from app.css.
Renames plugin.ts to vite-plugin.ts to sit alongside the new plugins/
directory of remark transforms.
* docs(series): expand the Markdown style guide with code and tabs
Grows the reference post into a full tour: paragraphs and line breaks,
link variants, footnotes, and horizontal rules, plus a large code section
covering language tags, titles, line/word highlights, line numbers,
diffs, focus, errors/warnings, indent guides, and inline highlighting —
alongside :::tabs and escape examples.
* chore(deps): add hast/mdast and image-size tooling for content images
Support the build-time image pipeline and thumbnail captions: image-size
for intrinsic dimensions, rehype-mdx-import-media to fingerprint local
srcs, and hast/mdast utilities to parse inline-Markdown captions. Pin
@types/hast to 3.0.5 in the workspace catalog to avoid a duplicate-copy
type mismatch between mdast-util-to-hast and hast-util-raw.
* feat(thumbnail): add frontmatter thumbnails with rendered captions
Add a thumbnail module (schema, server-side caption parser, and figure
component) and wire it into posts and series. Frontmatter gains an
optional thumbnail whose document-relative src resolves to a
fingerprinted asset at build time (resolveAsset) and whose inline-Markdown
caption is rendered to HTML on the server, so no Markdown parser reaches
the client. Series posts inherit the series thumbnail when they don't set
their own, and detail pages render it above the header.
* feat(markdown): render local images with intrinsic dimensions and captions
Add a rehype-image build pass that stamps intrinsic width/height on
colocated images to eliminate layout shift, and promotes a standalone
titled image into a figure with a Markdown-rendered caption. Swap the MDX
img tag for an app-aware Image component with lazy loading, run
rehype-mdx-import-media (after rehype-image) to fingerprint local srcs,
and add the matching prose image/figure styles. Also moves the
font-ligature reset from code spans to the global root.
* docs(series): expand the Markdown style guide and add thumbnails
Rework the style guide into a complete reference covering frontmatter,
GFM prose, code tooling, tabs, and the sharp edges around each, with
example fences shown beside their rendered output. Add a series cover
thumbnail with an image-credit caption and the supporting assets.
* chore(content): remove demo frontend-development series and journey post
Drop the placeholder demo content now that the Markdown series is the
canonical formatting reference for the site.
* refactor(markdown): rename module files to markdown.* convention
Rename utils.ts to markdown.utils.ts and vite-plugin.ts to
markdown.vite.ts to match the module-prefixed filename convention, and
update the imports in content-collections.ts and vite.config.ts.
* docs(markdown): document the MarkdownRender component
Add a JSDoc block to MarkdownRender explaining that it renders compiled
MDXContent with the app's mdxComponents overrides applied.
* style(markdown): add thin code-block scrollbar and refine copy button
* style(markdown): truncate long code-block filenames with ellipsis
The title header was a flex container, where text-overflow has no effect.
Switch it to a block box (line-height centers the single line) so a long
filename ellipsis-truncates on narrow screens instead of overflowing.
* feat(markdown): colorize matching brackets in code blocks
Add the @shikijs/colorized-brackets transformer to the rehype-pretty-code
pipeline so nested brackets render in level-cycling colors. Pinned to
4.3.1 to match the existing shiki/@shikijs/transformers version.
Works with the dual-theme setup without CSS changes: under defaultColor:
false the transformer overrides each bracket span's --shiki-light/
--shiki-dark vars, which markdown.css already reads.
* feat(markdown): add heading permalinks and responsive table scroll
Stamp a stable id on every heading (rehype-slug) and append a
hover-revealed "#" permalink (rehype-autolink-headings) for deep linking,
with scroll-margin-top so fragment jumps clear the top edge. Route hash
links through a plain same-page anchor in the Anchor override so
permalinks scroll in place instead of opening a new tab.
Wrap GFM tables in a horizontal scroll container floored by a min-width,
so wide tables scroll instead of crushing columns on narrow viewports,
reusing the code block's thin-scrollbar styling.
* style(markdown): offset footnote jump targets
Give footnote definitions and back-reference targets the same
scroll-margin-top as headings, so following a footnote reference or its
back-arrow lands with a gap above rather than flush against the top edge.
* feat(markdown): add GitHub-style alert callouts
Rewrite `> [!NOTE]`-style blockquotes into styled Callout components via a
new remarkAlert plugin. Supports the five GitHub alert types (NOTE, TIP,
IMPORTANT, WARNING, CAUTION), each with an accent left border and icon,
and documents them in the Markdown style guide.
* feat(markdown): derive table of contents from headings
Add extractTableOfContents, which reuses the render pipeline's rehype-slug
to build {depth, title, id} entries from a document's Markdown headings, so
#id links land on the same slugs the page renders. Wire the resulting toc
through the post, series, and series-post collections and expose it on their
document types and server functions.
* Merge pull request #3 from devsantara/chore/toc
feat(toc): add animated On this page table of contents
Sign up for freeto 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

@edwintantawi