Files
Naiyuan Qing a964c5229d feat(editor): pan/zoom the image attachment preview (MUL-5316) (#5971)
* feat(editor): pan/zoom the image attachment preview (MUL-5316)

Image preview could only fit-to-window, so a screenshot of text opened
unreadably small with no way in. It now runs on the same pan/zoom canvas
the Mermaid viewer uses: fit on open, then wheel (cursor-anchored), drag,
pinch, double-click fit<->100%, +/-/0 and arrow keys, plus a toolbar with
zoom out / % / zoom in / fit / actual size / reset.

The canvas is generalised out of Mermaid rather than duplicated:
diagram-transform -> zoom-transform, use-diagram-canvas -> use-zoom-canvas,
the canvas CSS out of mermaid.css into zoom-canvas.css, and a shared
ZoomCanvas + ZoomControls that MermaidViewer now composes too (dropping its
hand-rolled toolbar and canvas markup). The five zoom labels move from
editor.mermaid.* to a shared editor.canvas.* in all four locales.

Two fixes the generalisation forced, both of which also improve Mermaid:

- MIN_SCALE floored computeFitScale at 25%, so content more than 4x the
  canvas could not be fitted at all. A 1600x8000 full-page screenshot needs
  ~10% and would have opened cropped — worse than the fit-only behaviour it
  replaces. The lower bound is now min(0.25, fitScale), threaded through
  clampTransform / zoomToAt / canZoomOut.
- The canvas measured its viewport with getBoundingClientRect while its
  container was mid scale-in animation, fitting against a viewport a few
  percent too small; ResizeObserver reports the untransformed layout box so
  it never fired to correct it. Measures offsetWidth/offsetHeight now.

Image-specific handling: natural size is read from the ref as well as
onLoad (a cached image is already complete before onLoad attaches), and an
image with no intrinsic size — an SVG with only a viewBox — keeps the old
letterboxed render with the zoom controls hidden instead of a blank canvas.
The canvas is focused on open so the keyboard controls work without a click
first, native image drag is disabled so it can't hijack the pan, and the
backdrop only closes on a click that actually lands on it.

Verified: pnpm typecheck, pnpm lint, pnpm test (3003 views tests, 50 in
attachment-preview-modal). The flex chain and the long-screenshot fit were
also checked in headless Chromium, which jsdom cannot measure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(ui): keep kbd keycaps readable inside tooltips

Upstream shadcn inverts its tooltip surface, so Kbd forced near-white text
inside tooltip-content. Our TooltipContent keeps the popover surface, which
made keycaps render white-on-white. Drop the inverted-surface overrides so
keycaps keep their regular muted colors, which read correctly on popover in
both themes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(editor): drop the redundant reset zoom control, add shortcut tooltips

The reset toolbar button was a literal alias of fit (reset: fit) — it could
never do anything fit doesn't, so remove it along with the reset API, the
isFitted state that only served its disabled look, and the reset_view copy
in all four locales.

Replace the native title attribute on the remaining zoom controls with the
shared Base UI Tooltip + ShortcutKeycaps pattern (same as the editor bubble
menu), showing the matching keyboard hints: zoom out (-), zoom in (+), fit
to view (0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Walt <walt@multica.ai>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-27 13:27:58 +08:00
..