mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-13 11:30:58 +02:00
* 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>
74 lines
2.4 KiB
CSS
74 lines
2.4 KiB
CSS
/*
|
|
* Shared pan/zoom canvas — the Mermaid viewer and the image attachment
|
|
* preview both render through `ZoomCanvas` (editor/zoom-canvas.tsx).
|
|
*
|
|
* Not scoped to `.rich-text-editor`: both viewers are portaled to
|
|
* document.body and never render inside the editor subtree. The background is
|
|
* deliberately left to the caller so each surface can pick its own backdrop.
|
|
*/
|
|
|
|
.zoom-canvas {
|
|
position: relative;
|
|
width: 100%;
|
|
/* Flex owns the height: the header is shrink-0 and the canvas takes the rest.
|
|
`min-height: 0` is what allows it to shrink below content size instead of
|
|
pushing the toolbar out of the dialog. */
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
cursor: grab;
|
|
/* Claim every touch gesture: browser pan/pinch would otherwise scroll or
|
|
zoom the page underneath instead of moving the content. */
|
|
touch-action: none;
|
|
/* A pan that leaves the canvas would otherwise start a native text selection
|
|
and highlight the toolbar text plus the whole content box. Done in CSS
|
|
rather than by preventDefault-ing pointerdown, which would also suppress
|
|
the default focus and silently kill the keyboard controls. */
|
|
user-select: none;
|
|
outline: none;
|
|
}
|
|
|
|
.zoom-canvas:focus-visible {
|
|
outline: 2px solid var(--ring);
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
.zoom-canvas.is-panning {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.zoom-canvas-content {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
transform-origin: 0 0;
|
|
will-change: transform;
|
|
}
|
|
|
|
/* Only discrete controls (toolbar buttons, +/-/0, arrow keys) ease between
|
|
states. Drag and wheel/pinch set `is-animated` off so the content tracks the
|
|
input exactly — easing direct manipulation reads as lag, not polish. */
|
|
.zoom-canvas-content.is-animated {
|
|
transition: transform 150ms ease-out;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.zoom-canvas-content.is-animated {
|
|
transition: none;
|
|
}
|
|
}
|
|
|
|
/* Unmeasured fallback: content whose natural size can't be read (an SVG with
|
|
only a viewBox has no intrinsic size in Chromium) has nothing to build a
|
|
transform against. Keep the same element chain as the measured branch so
|
|
React swaps classes instead of remounting — a remount would drop a decoded
|
|
image and flash — and just letterbox the content in place. */
|
|
.zoom-canvas-fit {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 1rem;
|
|
}
|