Skip to content

feat(mermaid): zoom the viewer at the pointer and pan by dragging - #1392

Closed
escapeWu wants to merge 1 commit into
agegr:mainfrom
escapeWu:feat/mermaid-viewer-pan-zoom
Closed

escapeWu wants to merge 1 commit into
agegr:mainfrom
escapeWu:feat/mermaid-viewer-pan-zoom

Conversation

@escapeWu

@escapeWu escapeWu commented Oct 22, 2026 •

Copy link
Copy Markdown

What

The full-screen Mermaid viewer (click a rendered diagram) now zooms and pans like a map:

  • The wheel zooms around the cursor, so the part being read stays under it.
  • Left-drag pans, and keeps panning when the pointer leaves the viewport (pointer capture).
  • The toolbar buttons zoom around the center, and the reset button fits the diagram to the width.
  • Clicking empty space still closes the viewer. Ending a pan over empty space does not.

Related: #267, which asked for zoom and pan of a diagram.

Problem on main

The viewer steps the diagram's width in percent inside a scrolling box (overflow: auto). The wheel scrolls the box instead of zooming, a left press does nothing, and a zoom step cannot be anchored to the part of the diagram being read.

To see it on main: ask for a larger flowchart, click Preview, click the rendered diagram, then try the wheel and a drag.

How it works

  • Mermaid emits an svg sized to its container (width="100%", a max-width style), so there is nothing to scale and no natural size to pan over. The viewer pins the svg to the pixels of its own viewBox and moves the wrapper with transform: translate(x, y) scale(zoom) and transform-origin: 0 0. 100% is the diagram's own pixel size.
  • Anchored zoom: the next offset is pointer - (pointer - offset) * (next / current). The wheel listener is attached by hand with { passive: false }, because React's wheel handler is passive and could not stop the page behind the modal from scrolling.
  • Click-to-close moved into pointerup. Pointer capture makes every click land on the viewport, so a click handler would close the viewer after any pan that ended over empty space. Now the press must start on the viewport itself and move less than 3 px.
  • showModal() and the fitting share one useLayoutEffect. A closed <dialog> has no layout box, so measuring before it opens reads zeros, and opening in a useEffect painted one unfitted frame.
  • CSS: the viewport is overflow: hidden with touch-action: none and grab/grabbing cursors, and the canvas is absolutely positioned. No new colors and no new strings (it reuses the zoom, fit and close labels). docs/agents/sessions.md has the note.

Behavior changes worth a look

  • 100% is now the diagram's own size. On main the diagram opens stretched to the viewport width, so a small diagram is enlarged. Now fit never enlarges past 100%.
  • Zoom range is 10%–800%, and the buttons step by 1.25x.
  • Touch: one finger pans. There is no pinch zoom (the buttons zoom).

Verification

  • node_modules/.bin/tsc --noEmit — clean. npm run lint — clean.
  • npm test — 2870 tests, 2867 pass, 0 fail, 1 skipped. Two tests in lib/session-membership.integration.test.mjs timed out at 15 s in a run where two suites were going at once. That file passes on its own (2/2, 6 s) and this change does not touch session listing.

Not run: a browser pass on this branch, and next build (per AGENTS.md). The change is in pointer and layout behavior that the unit tests do not reach.

The full-screen Mermaid viewer only stepped the diagram's width in percent
inside a scrolling box: the wheel scrolled instead of zooming, a left press
did nothing, and a zoom step could not be anchored to the part of the
diagram being read. Lay the diagram out at its viewBox pixels and move the
wrapper with translate+scale: the wheel zooms around the cursor, a left-drag
pans with pointer capture, and the toolbar's reset fits the diagram to the
width without enlarging it past its own size.

Background-click-to-close now lives in pointerup (the press started on the
viewport itself and moved under 3 px), because pointer capture makes every
click land on the viewport and a pan ending on empty space would otherwise
dismiss the diagram. Opening the modal moved into the layout effect that
measures it: a closed <dialog> has no layout box, so fitting beforehand
read zeros and a useEffect open painted one unfitted frame.
@escapeWu
escapeWu force-pushed the feat/mermaid-viewer-pan-zoom branch from 208c1c3 to b8c12ed Compare October 22, 2026 07:45
@escapeWu escapeWu closed this Oct 26, 2026
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