Conversation
Footnote refs now open a popover with the cited body instead of scrolling the reader away. Behavior is identical on desktop and mobile per the spec — on small screens the popover slides up as a bottom sheet; on wide screens it anchors above or below the ref. The bottom <aside> list stays rendered as the no-JS / print fallback. The lookup seam is a deliberate data-attribute contract (data-footnote-ref / data-footnote-id / data-footnote-list / data-footnote-embed), not Pandoc's .footnote-ref / .footnote-list class names — those are stable-by-stasis, not by encapsulation, so any Pandoc class-name churn would silently break the popup. The closest([data-footnote-embed]) walk routes refs inside an embedded note to that embed's own <aside>, and top-level refs to the non-embedded aside; both can legitimately contain the same numeric id since Pandoc numbers footnotes per document. The popup uses the native HTML Popover API — auto-dismiss, Esc to close, and accessibility come for free. No dependency added. If the browser lacks Popover support, the click handler is never installed and anchor navigation resumes. e2e coverage (cucumber+playwright) exercises all three paths on the fixture notebook: parent body, inside a callout, inside an embedded note. Manual chrome-mcp check lands on docs/guide/markdown/embed.md, which now carries one footnote per context.
A callout renders its body via pandoc, which emits its own <Note:List> for footnotes cited inside. So the "first top-level aside" rule picked the callout's aside for a parent-level ref and served the wrong body — caught by a chrome-mcp walk over docs/guide/markdown/embed.html, confirmed by inspecting the three <aside data-footnote-list> elements on that page. Generalize the attribute from data-footnote-embed to data-footnote-scope and apply it to both embed sections and callout divs. findTarget() now looks up the ref's closest scope (callout, embed, or none), then finds the aside owned by that exact scope (nearest-scope-ancestor === self). Nested scopes no longer leak across. e2e: callout scenario switched from "index 2 in parent body" to "index 1 inside a callout" to match the callout's own per-scope numbering. New "inside a callout" step targets [data-callout] refs; the "inside an embedded note" step is tightened to section[data-footnote-scope] so it doesn't match a callout.
Hickey/Lowy Analysis
Hickey rationaleFive things live cleanly in The duplicated mobile breakpoint (CSS The one no-op — Lowy rationaleThe data-attribute seam is the right volatility boundary: the markup layer emits semantic intent; the presentation layer reads it. Replacing click-to-open with hover-to-open or a sidebar panel touches only The one real coupling leak Lowy flagged (beyond the Hickey-caught backref strip) was |
|
| Step | Status | Duration | Verification |
|---|---|---|---|
| sync | ✓ | 1s | git fetch ok; forge=github; noGit=false |
| research | ✓ | 0s | Mapped footnote rendering (pandoc.tpl:70-84), heist-extra splice API (Footnotes.hs:42-61), embed/callout templates, e2e structure. Identified embed-scope collision and refined attribute contract. |
| branch | ✓ | 2s | Created branch footnote-popups off origin/master. |
| implement | ✓ | 3m 2s | Added data-footnote-* attrs, new components/footnote-popup.tpl with scope-aware lookup + Popover-API open + mobile bottom-sheet. e2e fixtures + 3 scenarios + 3 steps. [^parent] footnote to docs/guide/markdown/embed.md for chrome-mcp check. |
| check | ✓ | 6s | cabal build all: Up to date. |
| docs | ✓ | 17s | Documented popup behavior in docs/guide/markdown.md. |
| fmt | ✓ | 11s | pre-commit clean: cabal-fmt, fourmolu, hlint, nixpkgs-fmt. |
| commit | ✓ | 20s | Pushed feature branch with primary feature commit. |
| hickey+lowy | ✓ | 5m 26s | Hickey: 6 findings (5 Fix, 1 No-op). Lowy: 5 seams (2 Fix, 3 No-op). 6 follow-up commits. |
| police | ✓ | 4m 34s | Rules: 2 fixes. Fact-check: 1 fix. Elegance: 5 refactors. 8 commits. |
| test | ✓ | 7m 49s | Chrome-mcp walked 4 refs on /embed.html: parent/callout/embed scopes all routed correctly. Mobile bottom-sheet verified at 400×700; desktop popover at 1280×720. Found + fixed a callout-scope bug. e2e scenarios added. |
| create-pr | ✓ | 1m 43s | Draft PR #642 + hickey/lowy analysis comment. |
| ci | ✓ | 1m 6s | Vira CI passed against HEAD e9d435b. Built 5 packages incl. link-check (250 internal links). |
| Total | 29m 27s |
Slowest step: test (7m 49s).
Optimization suggestions
testdominated at 7m 49s because chrome-mcp surfaced a real bug (callouts also needingdata-footnote-scope), which pushed an extra edit-commit-reverify cycle into this step. That's the test step doing its job — no change recommended. The alternative (catching it only in e2e duringci) would have been longer since the fix would have landed duringciretry-mode.hickey+lowyat 5m 26s ran both sub-agents in parallel; individual agent wall-clock was ~3m each. If both had to re-run on a future touch,--review-model=haikuwould trim this at the cost of shallower critique.policeat 4m 34s is close-to-floor for a change of this size; most of the cost is the three passes (rules + fact-check + elegance). No slack to cut without dropping a pass.create-prat 1m 43s includes a backtick-escape fix round-trip (gh pr edit). Worth remembering: single-quoted heredoc is load-bearing, but writing backticks inside the heredoc must stay bare — escaping them as ``` produces literal backslashes.
Workflow completed at 2026-04-21 21:43 UTC.
…lash Footnote refs no longer render as anchors, so there is no #fn1 / #fnref1 navigation for the browser (or the live-server) to scroll to. The <sup> itself is the click target, with role="button" / tabindex=0 / aria-label and a keyboard handler (Enter, Space) to match. The bottom <aside> is still emitted so the popup has a place to clone content from, but carries the HTML `hidden` attribute and aria-hidden, so it never renders on screen — the popup is the only footnote UI. Dead CSS removed: the :target flash keyframe in styles.tpl targeted `ol.footnote-list li:target` and `sup.footnote-ref:target a`, both of which are now unreachable (no fragment navigation, no inner anchor). Popover recreation is now defensive against Emanote's live-server DOM patches: `ensurePopover` checks `isConnected` and re-attaches if a patch detached the previously cached element. `findTarget` does the same check for the cached top-level aside.
| > [!warning] | ||
| > The embed wiki-link syntax must appear on a paragraph of its own, with no other text added next to it.[^blk] Recursive embeds are supported. | ||
|
|
||
| [^parent]: A page-level footnote, rendered into the bottom list and also shown in the click-anywhere popup. The popup is a progressive enhancement — the bottom list stays usable when JS is off or on print. |
There was a problem hiding this comment.
Remove this outdated footnote.
There was a problem hiding this comment.
Removed in 2a7c923 — the copy described a progressive-enhancement fallback that no longer exists (the popup is the only on-screen UI now).
| popup clone. Bound to data-footnote-ref / data-footnote-id / | ||
| data-footnote-list / data-footnote-scope. --> | ||
|
|
||
| <style data-category="footnote-popup"> |
There was a problem hiding this comment.
Fact check if we stricktly need all these styles in this file and that they cannot be done in tailwind itself.
There was a problem hiding this comment.
Audited in 2a7c923. Popover chrome, body styling, active-state, focus-visible, and the mobile bottom-sheet shape all moved to inline Tailwind utilities (applied via JS className for the popover/body, inline on the <sup> for state variants). The .emanote-footnote-popup--mobile class-toggle is gone — max-sm:* variants do it viewport-conditionally. The only CSS that survives is one rule: #emanote-footnote-popover::backdrop { background: transparent; }. Tailwind has no variant for the Popover ::backdrop pseudo-element, and expressing it any other way would be strictly worse than one line of CSS.
…sync
Addresses PR review:
- Popover chrome and body styling move from the <style> block to
inline Tailwind utilities applied via JS `className` (`POPOVER_CLASS`
/ `BODY_CLASS`). Only the Popover `::backdrop` pseudo-element
remains in `<style>` — Tailwind has no variant for it.
- Active-state and focus-visible states on the <sup> ref collapse
into inline variant classes (`[&.emanote-footnote-active]:*`,
`focus-visible:*`), matching the rest of components/pandoc.tpl.
- Mobile bottom-sheet layout drops its `.emanote-footnote-popup--mobile`
class toggle: `max-sm:*` variants on the popover + body are
viewport-conditional by construction, and `positionPopover` still
clears inline top/left on mobile so the utility-driven layout isn't
overridden by stale desktop positioning.
Separately, footnotes now render in print mode: the <aside> emits
`class="hidden print:block …"` with a decimal-list header, so
printed output carries the cited bodies even though the popup is
the only on-screen UI. e2e suite gains a scenario that asserts no
<aside> is visible on screen and at least one becomes visible under
`page.emulateMedia({ media: 'print' })`, plus a content check.
Outdated `[^parent]` footnote on docs/guide/markdown/embed.md
(reviewer: "Remove this outdated footnote") dropped; its copy
described the now-removed progressive-enhancement fallback.
Footnote refs open a popup — that's the only on-screen UI. No
#fn1anchor, no visible bottom list, no scroll-away. The<sup>is the click target (role=button, Enter/Space activates). Desktop gets a floating card; mobile gets a bottom-sheet. For print, the hidden<aside>flips toprint:blockwith a decimal list so paper copies still carry the cited bodies.The popup binds to a
data-footnote-*contract (ref, id, list, scope), not Pandoc class names. Scope routing handles callouts and embedded notes as their own footnote boundaries — each one can legitimately carry afn1.Native HTML Popover API; no dep. Popover chrome styled entirely via inline Tailwind classes applied in JS; the only CSS rule that survives is
::backdrop { background: transparent }(Tailwind has no variant for that pseudo-element).Verification
docs/guide/markdown/embed.html— all refs (parent / callout / embed) route to correct bodies,scrollYstays 0, no hash.page.emulateMedia({ media: "print" })→ at least one aside visible, body content present).2a7c9238.Try it locally