Add a Table Of Contents for lessons - #132
Merged
Merged
Conversation
The HTML prototype reuses the application's stylesheet files verbatim; copying the current base.css and the two theme files brings the prototype's visual language back in line with the app in one move. Refs #131
Every prototype page header gains the chevron-book logo mark next to the wordmark, and authenticated pages move Log out out of the nav into the account group (divider, signed-in username, Log out button), with a visually hidden prefix for screen readers, mirroring the shipped header structure. The wireframes carry the same header anatomy: the brand cluster on all four frames, the account group on the authenticated one, with the register wireframe export joining the set. Refs #131
The prototype's lesson article now wraps its body in the lesson-content block and shows the shipped content styles working on real markup: a note alert and a tip alert with a custom title (the exact div/p structure the alerts extension emits), and a code block carrying highlight.js token spans so the token-mapped colors render. All styling comes from the refreshed stylesheets; the markup mirrors what the application serves. Refs #131
The prototype's lesson page adopts the lesson-layout grid with the sticky Contents panel: a details/nav rail beside the content on wide screens, a pinned collapsed bar on small ones, entries with level indents, an aria-current mark on the active section, and anchors that jump to real headings. Two wireframes join the set: the desktop lesson frame with the pinned TOC rail and a mobile lesson frame with the collapsed sticky bar. Refs #131
Lesson pages have no table of contents. The plan records the hybrid architecture: the renderer mints heading anchors and TOC entries after sanitization (author ids keep dying, the allowlist is unchanged), the template renders a sticky details/nav panel that works without JavaScript, and lesson-toc.js adds wide-screen auto-open plus an IntersectionObserver scroll-spy. Includes the CSS design appendix agreed after the wireframe review. Refs #131
ADR-0018 chooses server-minted heading anchors and TOC entries (a post-sanitization pass, so author ids keep dying and the ADR-0013 allowlist stays untouched) combined with a client-side scroll-spy (IntersectionObserver writing aria-current). Server-only, client-only, and the heading-anchor extension through the sanitizer are the rejected alternatives; the sticky always-viewable behavior is pure CSS, so the no-JavaScript floor holds. Refs #131
The renderer now returns a RenderedMarkdown record: the sanitized HTML plus TocEntry records for every rendered h2..h4. Anchor ids are stamped in the post-sanitization pass, sharing one jsoup parse with the alert class narrowing: the sanitizer strips any author-supplied id first, so raw HTML can never clobber page anchors like the #main skip-link target, and the allowlist itself stays id-free (ADR-0018). Slugs fold accents (NFD), lowercase, hyphenate, and deduplicate with numeric suffixes; a heading whose text reduces to nothing falls back to a generic slug. The content-addressed cache key is unchanged, the cached value widens to the record. Callers and existing tests updated mechanically; new tests cover the id minting, accent folding, deduplication, and the skip-link spoof case. Refs #131
The lesson page becomes a two-column grid on wide screens: content plus a 16rem rail where the TOC pins below the viewport top and scrolls internally when longer than the screen. On small screens the closed details summary is a slim bar pinned to the viewport top, so the TOC stays one tap away at any scroll position. Everything here is HTML and CSS: the no-JavaScript floor of ADR-0018, pinned by the journey test asserting the nav and its anchors in the server HTML, and the absence of a TOC on lessons with fewer than two headings. The TOC list precedes the content in the DOM so assistive tech meets the navigation aid first; the grid places it visually right. Indent levels ride a data attribute because BEM's double underscore collides with Thymeleaf's preprocessing syntax when composed dynamically. Refs #131
lesson-toc.js adds the two things the server cannot know: it opens the TOC panel on wide viewports (the markup ships closed, which is the right mobile floor) and tracks the reading position with an IntersectionObserver, marking the current section's TOC link with aria-current. The CSS keys the highlight off the attribute alone, the same pattern as the header nav, so visual and assistive state cannot drift apart. Anchor jumps get scroll-margin-top so headings never land glued to the viewport edge or under the pinned mobile bar, and smooth scrolling applies only under prefers-reduced-motion: no-preference, consistent with the site's motion policy. Without JavaScript the TOC remains sticky and expandable; this commit is pure enhancement. Fixes #131
The first layout never actually pinned: a sticky element can only travel inside its parent box, and both breakpoints gave it a parent exactly as tall as itself (a start-aligned grid item on desktop, an auto-sized grid row on mobile), so there was no runway and the TOC scrolled away with the page. Each breakpoint now pins the element whose parent spans the lesson. On small screens the aside itself pins at the viewport top: the layout is normal flow there, so the aside's containing block includes the whole article. From 46rem the grid rail returns, the aside keeps the default stretch to full row height, and the panel inside it pins below the viewport top. Verified pinned at top, middle, and bottom scroll positions on both presets. Refs #131
Field-tested on an iPhone SE viewport: tapping a TOC link left the expanded panel pinned over the top of the viewport, hiding the very section the reader jumped to; the mobile scroll-margin only budgets for the collapsed bar. On narrow viewports the click handler now collapses the panel and then drives the jump itself (pushState for the URL hash, then scrollIntoView from the settled layout). The ordering matters: the collapse shifts layout and the browser's native anchor scroll loses that race, which reproduced as a jump that never arrived. The desktop rail keeps the default behavior since it never overlaps content, and without JavaScript the panel still closes by tapping the bar. Refs #131
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #132 +/- ##
============================================
+ Coverage 81.71% 82.54% +0.83%
- Complexity 202 211 +9
============================================
Files 38 38
Lines 864 888 +24
Branches 53 55 +2
============================================
+ Hits 706 733 +27
Misses 124 124
+ Partials 34 31 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Long lessons had no way to see their structure or jump to a section,
and headings had no anchors to deep-link to.
This PR adds a sticky table of contents for lesson pages.
See ADR-0018.
Fixes #131
It is hybrid (both server and client side):
h2..h4(post-sanitization, so author-supplied ids still die andthe allowlist is unchanged) and returns the TOC with the HTML.
details/navcontents panel, pinned as asidebar on wide screens and as a slim collapsed bar on mobile;
works without JavaScript, shown from 2 headings up.
lesson-toc.jsauto-opens the panel on wide viewports,marks the current section with
aria-currentwhile scrolling(IntersectionObserver), and on mobile collapses the panel when a
section is picked, then drives the jump from the settled layout.
the same header, lesson content, and TOC elements.
Verification