← the Atlas

Markdown preview: footnotes open in a click popover

Features·seedling·implemented·

Click a footnote marker in the Code-tab Markdown preview and its definition opens in a dismissible popover anchored to the marker — no more scrolling to the bottom of the document and back. Reuses the wikilink-disambiguation seam; no new package, nothing to install.

Shipped in #1514. The document preview already renders GFM footnotes (marked-footnote) — the reference is a superscript [n] that links to a definitions list at the very bottom. Reading one means scrolling down, reading, and scrolling back to where you were. This change keeps that bottom list but adds a click popover: tap the marker, read the footnote in place, dismiss. It threads the same click seam the wikilink and relative-link previews already use, so it added no dependency and no new package. Verdict: ~half a day, low risk — the decisions below took the simplest branch at every fork (dismiss-on-scroll, click/tap only, useAnchoredPopover untouched).

What the reader sees

A footnote marker becomes a button. Click (or tap) the small [1] — the marker carries a pointer cursor and a subtle hover highlight so it reads as clickable — and its definition opens in a card anchored just under the marker, where your eye already is. Click anywhere else, scroll the document, or click the marker again, and it closes. The gesture is identical on a trackpad and on a phone — there is no hover step, so nothing is stranded on touch.

today — scroll down, then back
…the daemon promotes the surface only once it settles[1], which is why a cold start looks idle for a beat.
⌄   scroll past the whole document   ⌄
Footnotes
1. The settle gate waits for the first PTY frame.
this plan — click → popover
…the daemon promotes the surface only once it settles[1], which is why a cold start looks idle for a beat.
1The settle gate waits for the first PTY frame — see the daemon note.
click outside · scroll to dismisssee all ↓

The details that matter to a reader:

Footnote popups reuse the wikilink seam — no new package exists / reused this plan ① reader clicks a [n] footnote marker @kolu/solid-markdown the leaf — gains exactly one callback bindInteractions · onClick delegated — links & scroll already dispatch here querySelector('#md-footnote-n') → <li> the scroll branch's own lookup, reused onFootnote(anchor, defNode) NEW the package's only added surface callback client renders the overlay — as it does for wikilinks footnote popover panel cloned <li> · ↩ back-refs stripped inner links flow through host resolvers useAnchoredPopover + <Portal> the wikilink menu reuses this exact hook BrowseFileDispatcher already wires onNavigateWikilink Nothing new to install. The dependency arrow (client → @kolu/solid-markdown) is preserved — the overlay stays client-side, exactly as wikilink disambiguation does.
A footnote click threads the same three seams alerts and wikilinks already use. The package gains one callback; the client renders the overlay with the hook the wikilink-disambiguation menu already uses.

This is a leaf, not a new @kolu/* package. Apply the electricity test: a footnote popover hides only bounded logic — pair a marker to its definition <li> by id, then place a panel beside it. It hides no hard volatility — no transport, no reconnect, no persistence, no GPU-context loss — so it fails all three extraction tests. Nothing to receptacle; nothing to install.

The boundary it must respect is the dependency arrow. @kolu/solid-markdown depends only on solid-js, marked*, dompurify, shiki, yaml, and two tiny leaf utils — it has zero knowledge of the client app, of Corvu, or of Portal, and the arrow points outward (client → package). The package already hands hard, host-specific decisions back across that arrow rather than owning them: onNavigateRelative and onNavigateWikilink exist precisely because resolving a link is host volatility. Footnote popovers split along the same seam:

The verdict is contingent on the click model (§ the Architecture⇄Implementation loop). Because we open on click, the existing useAnchoredPopover fits with no new dependency — it is built for click-to-open, outside-click/Escape-dismiss panels. Had we chosen a hover hovercard, we would have needed hover-intent timing plus floating-ui-style autoUpdate scroll-tracking that useAnchoredPopover does not have, which would have pulled a positioning dependency (likely @corvu/tooltip) into play and pressured this boundary. The “leaf, no new package, no new dep” verdict is the click decision; a different trigger model would have produced a different shape.

Implementation — one PR, threading the existing seams

One PR, built test-first — the change is small because it is almost entirely wiring into seams that already exist. It threads the same render → sanitize → interact → host-overlay path that alerts (data-md-alert) and wikilinks (data-md-wikilink) already proved, so there is nothing to stage across releases: the package gains one callback and the client renders one panel, in the same diff.

Package side — pin the contract, then one marker and one callback

DOMPurify is brutal on footnote markup. [email protected] (the version ^1.2.4 actually resolves to) emits a forward ref as <sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> — note there is no class on it (the plan’s earlier “class="footnote-ref"” was wrong); the distinctive marker is the bare data-footnote-ref attribute, and the back-ref carries data-footnote-backref. The sanitizer strips data-footnote-* and aria-describedby and namespaces ids/#-hrefs with an md- prefix, so what survives is <sup><a id="md-footnote-ref-1" href="#md-footnote-1" data-md-footnote>1</a></sup>.

The whole feature stands on that markup, and the version floats — so pin the contract the way this package already does: a node render test. @kolu/solid-markdown’s vitest.config.ts deliberately keeps a Node-only environment (“the sanitize layer is covered by the browser e2e suite, not here”), and data-md-wikilink / data-md-alert survival is e2e-covered, not unit — adding happy-dom (absent from the repo) just to unit-test sanitization would contradict that boundary. So the red-first test lives in render.test.ts: feed footnote markdown through renderMarkdownToRawHtml and assert rewriteFootnotes flags exactly the forward refs with data-md-footnote (the back-ref untagged, every re-cite tagged) — a marked-footnote bump that changed the marker then fails loudly. Marker→popover survival rides the e2e. With that pinned, three small changes mint the marker and route the click — the package’s only new surface:

Client side — render the popover (reuse useAnchoredPopover)

Sharp edges, and how each is handled

Each was a real trap; none is left open.