Skip to content

fix(toc): stop the floating outline from sitting on the text it covers - #545

Merged
PathGao merged 1 commit into
masterfrom
fix/toc-overlay-176
Aug 8, 2026
Merged

fix(toc): stop the floating outline from sitting on the text it covers#545
PathGao merged 1 commit into
masterfrom
fix/toc-overlay-176

Conversation

@PathGao

@PathGao PathGao commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

What this is

Closes #176, reported by @StrollStars: with the outline unpinned, showing it covers part of the text instead of sitting beside it. Their question was whether the unpinned outline was meant to float and retract on hover — "otherwise the unpinned mode has no point."

The mode does have a point, but their reading of the symptom was right, and the design it depends on has not been holding for most windows.

Two changes:

  • A. The test that decides whether the outline is covering anything was asking about the wrong pane.
  • B. An outline that is covering the text now gets out of the way when you use it, or when you reach past it.

Mechanism

A — the overlap test measured a pane the outline was not on.

The outline is positioned against the layout container, so it lands on whichever pane owns that edge, and the editor is always the first child — it takes the left edge whenever it is on screen at all. isOverhanging nevertheless always measured the viewer pane against the preview's centred content width:

                 what is under the outline        what was measured
reading          viewer alone → preview both      preview          ✓
split            editor | viewer                  preview          ✗  left side is the editor
editing only     viewer is flex: 0                preview          ✗  and viewerWidth is 0

In editing-only mode viewerWidth > 0 is false, so the whole expression fell through to "no overlap" while the panel sat on top of the code — with neither the shadow nor the border that exist to say exactly that. Asking "is the preview underneath me at all" has to come first: the preview centres its content and leaves a gutter to fall into, the editor fills its pane edge to edge and has none to lend.

B — the gutter the floating mode depends on is usually not there.

Unpinned does not mean auto-hiding; it means "do not reflow the text". The panel is supposed to land in the margin beside the centred preview. With the shipped defaults that margin is wide enough only from 1360px of viewer width:

tocWidth 240 > (viewerWidth − previewMaxWidth) / 2
240 > (W − 880) / 2   ⟺   W < 1360 overlaps

That is wider than most windows, and split view halves the pane, so it would need a window around 2720px. For most readers the floating outline has been covering their text since it shipped, with no way to dismiss it except the toggle it came from. Hence #176.

It now collapses when you pick an entry — the panel exists to be called up, used once and dismissed — and when you press past it onto what it was covering. Both are gated on the predicate A repairs, so a pinned outline stays (it is a sidebar) and one genuinely sitting in the margin keeps its old behaviour.

The highlight had to stop belonging to the outline.

A jump marks the landing heading and holds the mark until the reader moves. The listeners that clear it belonged to Toc, and a later instance starts with its own activeTargetEl and cannot see what an earlier one marked — so hiding the outline right after a jump stranded the mark on the heading permanently. That was already reachable by hand (jump, then hide the outline); B would have made it routine. Clearing is now handed to listeners that outlive the component, deferred past the jump's own smooth scroll for the same reason clickLock exists.

Scope

Deliberately left alone:

  • The gutter arithmetic itself. Widening the default preview or narrowing the default outline would hide 关于取消固定目录 #176 for some window sizes and not others. The honest fix is dismissal, not a number that happens to work on the reporter's monitor.
  • Auto-collapse on keystrokes. Only pointerdown dismisses. Clicking into the editor is the normal way to start typing and already closes it; adding keydown would close the outline while someone is using find-as-you-type in another pane.
  • Hover to reveal / retract, which is what the reporter guessed the mode was. It is a different feature, and it would make the outline appear when the pointer merely crosses the margin.
  • Noticed and not touched: the toggle button brightens on hover (opacity: .6 → 1) while the panel it opens does not react to the pointer at all. That inconsistency is very likely what led the reporter to expect a hover-driven panel.

Tests

New scripts/tocOverlay.test.ts covers the extracted predicate over all six placements, the 1360px boundary either side, the full-width case and the 50px floor, plus two source assertions that both auto-collapse paths stay gated on the same predicate.

Reverting each half with the tests kept:

  • drop the isTocOverPreview early return → red, covering the editor always counts, however wide the window is
  • drop the collapse-on-jump line → red, the outline collapses itself only when it is in the way

scripts/previewWidth.test.ts asserted the gutter arithmetic by matching the inline expression in MarkdownViewer.svelte; it now follows it into tocOverlay.ts and additionally pins that the viewer still feeds it the same preference.

Verification

npm audit      found 0 vulnerabilities
npm run check  673 files, 0 errors, 0 warnings
npm test       920 passing, 0 failing
cargo test     145 passing, 0 failing

Also built and ran a debug bundle on macOS (aarch64) and exercised it by hand in a window narrow enough to overhang: collapse-on-jump, collapse-on-click-outside, the pinned outline staying put, and the landing highlight surviving the jump's own scroll and then clearing on the next reader action.

What I did not verify: nothing was built or run on Windows or Linux. The changed code is layout and DOM-event logic with no platform branches, so the risk is low, but "low" is a judgement and not a test run. The stranded-highlight handover is timing-dependent and was checked by hand on one machine at one scroll speed, not automated — it is the part of this PR most worth a second pair of eyes.

**A. The overlap test was asking about the wrong pane.** The outline is
positioned against the layout container, so it lands on whichever pane
holds that edge — and the editor is always the first child, so it takes
the left edge whenever it is on screen. But `isOverhanging` always
measured the VIEWER pane against the preview's centred content width.
That is only the right question in reading mode:

  reading        viewer alone         → preview on both sides    ✓ measured
  split          editor | viewer      → editor on the left       ✗ measured the preview
  editing only   viewer is `flex: 0`  → editor on both sides     ✗ viewerWidth is 0

In editing-only mode `viewerWidth > 0` is false, so the expression fell
through to "no overlap" while the panel sat on top of the code with
neither the shadow nor the border that exist to say so. The test now
answers "is the preview underneath me at all" first, and only reaches for
arithmetic when it is. Moved to `utils/tocOverlay.ts` with the six cases
under test.

**B. An outline that covers the text now gets out of the way.** This is
what #176 is actually about. Unpinned does not mean auto-hiding, it means
"do not reflow the text" — the panel is supposed to fall into the gutter
beside the centred preview. With the defaults that gutter is only wide
enough at 1360px of viewer width (240 > (W - 880) / 2), which is wider
than most windows and twice as wide as split view can offer. So for most
readers the floating mode has covered their text since the day it
shipped, and there was no way to dismiss it except the toggle it came
from. It now collapses when you pick an entry, and when you reach past it
to touch what it was covering.

Both paths are gated on the same predicate A repairs: pinned it is a
sidebar, and one genuinely sitting in the margin harms nothing and keeps
its old behaviour.

**The highlight had to stop being owned by the outline.** A jump marks
the heading in the preview and leaves the mark until the reader moves —
but the listeners that clear it belonged to the outline, and a new
instance starts with its own `activeTargetEl` and cannot see what an
earlier one marked. Hiding the outline right after a jump therefore
stranded the mark forever. That was already reachable by hand; B makes it
routine. The clearing is handed to listeners that outlive the component,
deferred past the jump's own smooth scroll for the reason `clickLock`
exists.

Refs #176
@PathGao
PathGao merged commit f29928a into master Aug 8, 2026
4 checks passed
@PathGao
PathGao deleted the fix/toc-overlay-176 branch August 8, 2026 08:44
PathGao added a commit that referenced this pull request Aug 8, 2026
…ople download (#558)

**The syntax reference.** Nothing in 2.7.3 changed what renders, so the
compatibility table is untouched. What changed is behaviour, and five of
those are demonstrable on the page itself, so they belong in §17 and §15:
the right-click Edit jump (#539), split-view sync by source line (#541),
sticky scroll (#555), the unpinned outline getting out of the way (#545),
and CJK word-wise navigation with the IME space no longer boxed (#546).

The outline line is written from what f29928a actually does — it collapses
when you pick an entry or reach past it — rather than from the shorter
"steps aside" the commit subject suggests.

Checked and left alone: Copy Reference still follows the document's own
spelling (`preferredReferenceStyle`), heading completion and task
checkboxes were already described correctly. Encoding, line endings, the
clipboard and the load-race fixes are not syntax and are not on this page.

**Making it findable.** It was one link in the nav and one clause in a
Features bullet. It now has its own section between Download and
Installation from source, with the raw URL first so the primary action is
downloading the file rather than reading it on GitHub — the document is
built to be opened in Markpad, and reading it here is the fallback. The
section also suggests handing the file to an AI: it is a complete list of
what renders, so an assistant can reformat an existing document or write a
new one that uses the whole range.

"Markdown support" is gone as a heading. In a README that word means the
help desk; `## What Markpad renders` says what the section is, and does
not collide with `## Features`, which is where "what Markpad can do"
already lives.

The release body gets the same section, with the links pinned to the tag.

**The two platform notices.** The Windows SmartScreen note has been pasted
into the release body by hand since it was written — the workflow never
generated it. Both notices are generated now, so the macOS one cannot go
missing the release someone forgets, and the Windows one must no longer be
added by hand or it will appear twice.

The macOS notice covers what #209 costs a new user: an unnotarized `.dmg`
is refused on first launch with only Cancel and Move to Trash, so
Control-click → Open comes first as the one-step way past it and the
System Settings route second. It also names the consequence people report
as a separate bug — unsigned means file access is granted per prompt, so
the dialogs repeat, most visibly with auto-save on and in image-heavy
documents — and links to the self-signing workaround in #209, noting it
has to be redone after every update.

Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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