Skip to content

feat(mobile): draw SVG files and pinch-zoom every image preview, Android included - #36608

Merged
nwparker merged 5 commits into
mainfrom
nwparker/mobile-svg-viewer
Oct 31, 2026
Merged

nwparker merged 5 commits into
mainfrom
nwparker/mobile-svg-viewer

Conversation

@nwparker

@nwparker nwparker commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​224 $\color{#cf222e}{\Huge{\mathbf{−}}}$​2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​222
Prod 36 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​600 $\color{#cf222e}{\Huge{\mathbf{−}}}$​143 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​457

Stacked on #36576 (desktop SVG + image viewer). Merge that one first, or collapse the two.

ELI5

On the phone, .svg files now show the drawing instead of a wall of XML, with a Preview | Source toggle to get the XML back. Every image preview on the phone can now be pinched, dragged and double-tapped to zoom — including on Android, where image zoom never worked.

What Changed

SVG on mobile (mobile half of #23629)

  • Before: mobile-artifact-kind.ts sent .svg down the text path on purpose, so the file preview and mirrored file tabs showed XML source.
  • After: .svg is a new vector artifact kind. It is still read as text (files.read), and the same reply feeds a drawing and the existing syntax-highlighted Source view (MobileSvgPreview). The drawing is sized from the SVG's own width/height/viewBox using the same readSvgIntrinsicSize desktop uses, so "fit" and "100%" agree across apps. Fit enlarges vectors to fill the screen.
  • Markdown previews on the phone now draw ![](diagram.svg) too, instead of rejecting it as an unsupported image.
  • A truncated SVG keeps the source view. A writable terminal artifact gets the same Preview/Source toggle: Preview draws the unsaved draft, as desktop does, and Source is the existing editor. A save failure shows in both modes.
  • The SVG is drawn as an <img> data URI, so its scripts never run and its external references are never fetched. The shipped shell CSP already allows img-src data:.

Pan/zoom on mobile (all images)

  • Before: <ScrollView maximumZoomScale> — iOS-only props, so Android had no zoom at all, and the box was sized from the screen rather than the file.
  • After: MobilePanZoomImage uses pointer events with touch-action: none on the web page: two-finger pinch zooms at the fingers, one finger drags, double-tap toggles fit ↔ actual size (or 2.5× when actual size is about the same as fit). Panning stops at the image's edges, and rotating the phone while zoomed keeps the image against them. The content under the fingers stays under them while a pinch moves.
  • While zoomed, the viewer claims Back: Android's Back returns to fit before it leaves the screen.
  • The math is shared with desktop: pan-zoom-view.ts moved from src/renderer/.../editor/ to src/shared/, unchanged except that its three constants moved with it.
  • MobileHtmlPreview's Preview/Source tabs were extracted to MobilePreviewSourceTabs and reused.

Compatibility

  • No host change: hosts already return .svg as UTF-8 text from files.read (readLocalMobileFile), so older desktops and SSH hosts work.
  • These screens run in the phone's web shell, so no native rebuild is needed (no new dependency; native fingerprint unchanged).

Linked Issue

Fixes #23629 (with #36576 for the desktop half)

Visual Proof

iOS 26 simulator (iPhone, real WKWebView in the Orca dev client, paired to a host built from this branch). The pinch gestures are real two-finger touches.

wide.svg (viewBox only) fitted, then pinched (the vector stays sharp and the page chrome does not zoom):

ios-wide-fit ios-wide-pinched

Zoomed and dragged right: the image pans to its left edge and stops there.

ios-zoom-pan-right

A 1600 × 900 PNG pinched, then the phone rotated while zoomed: the image still fills the screen.

ios-png-zoomed ios-png-rotated

Headless Chromium with touch (the render test), as an Android WebView stand-in:

Mobile web bundle under the shipped shell CSP, 390 × 844 with touch, rendering a viewBox-only SVG (1200 × 240). Fitted to the phone's width (the browser alone would draw it at 300 px):

mobile fit

After a two-finger pinch:

mobile pinched

Before: the same file showed as XML source (the behavior described in #23629).

Testing

  • New render test mobile-web-app-svg-preview-render.test.mjs builds the real mobile web bundle, serves it with the shipped CSP, and in headless Chromium with touch: checks the drawing loads at 5:1 and fills the width, pinches with two CDP touch points (width more than doubles), double-taps back to fit, and flips to Source. It produced the screenshots above.

  • mobile-markdown-image-source.test.ts: an SVG image in markdown is read with files.read and drawn as an SVG data URI.

  • mobile-artifact-kind.test.ts updated for the vector kind.

  • Mobile typecheck and the full mobile suite pass (12,000 tests). The only two failures, native-document-bundle.test.ts, are in terminal code this PR doesn't touch; CI decides. Desktop tc:web and the moved pan-zoom-view tests pass.

  • New strings are translated into all 10 mobile locales.

  • iOS 26 simulator, real app. I built the dev client for this branch and served the web bundle from a headless host built from this branch, then paired the two. On .svg and .png I checked fit, real two-finger pinch, one-finger pan with edge stops, double-tap back to fit, the Source tab, rotation while zoomed, and an SVG image inside Markdown.

  • Cross-reviewed twice with Codex (GPT 6.1 Sol, xhigh). Fixed from that review: pinch drift when the fingers move, blank space after rotating while panned, missing Preview on writable SVG artifacts, and a save error hidden in Preview. Regression tests cover the pinch and rotation math.

  • Known, not from this PR: on iOS 26, a long rightward drag anywhere in a web-shell page triggers the system's full-screen back swipe. That pops the host screen, even on a Markdown file with no zoom. A zoomed image therefore pans right only until iOS takes the gesture. Fixing it needs a shell change to the nested stack's swipe handling, which would also change Back for every page with in-page history, so it is left for a separate PR.

  • Not tested: a physical phone and an Android emulator (none installed). Headless Chromium covers Chromium's touch pipeline.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

@nwparker
nwparker force-pushed the nwparker/mobile-svg-viewer branch from 253a192 to 1b0fd60 Compare October 31, 2026 06:27
@nwparker
nwparker force-pushed the nwparker/svg-viewer branch from 6850f56 to 2b49018 Compare October 31, 2026 08:10
@nwparker
nwparker force-pushed the nwparker/mobile-svg-viewer branch 4 times, most recently from af9ef0b to 8781d6b Compare October 31, 2026 08:55
@nwparker
nwparker force-pushed the nwparker/mobile-svg-viewer branch from 8781d6b to fa81c08 Compare October 31, 2026 09:02
@nwparker
nwparker force-pushed the nwparker/svg-viewer branch 2 times, most recently from 21e0395 to db1c4f7 Compare October 31, 2026 23:10
@nwparker
nwparker force-pushed the nwparker/mobile-svg-viewer branch from fa81c08 to f063e17 Compare October 31, 2026 23:10
@nwparker
nwparker force-pushed the nwparker/mobile-svg-viewer branch from f063e17 to 3a0d75a Compare October 31, 2026 23:27
@nwparker
nwparker changed the base branch from nwparker/svg-viewer to main October 31, 2026 23:27
@coderabbitai

coderabbitai Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a80edd22-4927-4f11-934d-a58b119469a0
Commits

Reviewing files that changed from the base of the PR and between 91c9200 and 3a0d75a.

Files selected for processing: 41
  • config/scripts/mobile-web-app/preview/mobile-web-app-svg-preview-render.test.mjs
  • mobile/src/components/MobileHtmlPreview.tsx
  • mobile/src/components/MobilePanZoomImage.tsx
  • mobile/src/components/MobilePreviewSourceTabs.tsx
  • mobile/src/components/MobileSvgPreview.tsx
  • mobile/src/components/use-mobile-pan-zoom.ts
  • mobile/src/files/MobileFilePreviewBody.test.tsx
  • mobile/src/files/MobileFilePreviewBody.tsx
  • mobile/src/files/MobileFilePreviewScreen.tsx
  • mobile/src/files/mobile-file-preview-response.ts
  • mobile/src/files/mobile-file-preview-styles.ts
  • mobile/src/files/mobile-file-tab-doc.ts
  • mobile/src/files/mobile-markdown-image-source.test.ts
  • mobile/src/files/mobile-markdown-image-source.ts
  • mobile/src/i18n/locales/de.json
  • mobile/src/i18n/locales/en.json
  • mobile/src/i18n/locales/es.json
  • mobile/src/i18n/locales/fr.json
  • mobile/src/i18n/locales/ja.json
  • mobile/src/i18n/locales/ko.json
  • mobile/src/i18n/locales/pt.json
  • mobile/src/i18n/locales/ru.json
  • mobile/src/i18n/locales/tr.json
  • mobile/src/i18n/locales/zh-Hant.json
  • mobile/src/i18n/locales/zh.json
  • mobile/src/session/MobileSessionFileReader.tsx
  • mobile/src/session/mobile-artifact-kind.test.ts
  • mobile/src/session/mobile-artifact-kind.ts
  • mobile/src/session/mobile-session-document-read-settlement.ts
  • mobile/src/session/mobile-session-reader-styles.ts
  • mobile/src/session/mobile-session-route-types.ts
  • src/renderer/src/components/editor/DiagramViewerModal.tsx
  • src/renderer/src/components/editor/ImageViewer.tsx
  • src/renderer/src/components/editor/PanZoomImageDialog.tsx
  • src/renderer/src/components/editor/PanZoomImageSurface.tsx
  • src/renderer/src/components/editor/image-png-export.ts
  • src/renderer/src/components/editor/image-viewer-zoom.ts
  • src/renderer/src/components/editor/use-pan-zoom-viewport.ts
  • src/shared/pan-zoom-view.test.ts
  • src/shared/pan-zoom-view.ts
  • src/shared/svg-document.ts
Files with no reviewable changes: 2
  • mobile/src/session/mobile-session-reader-styles.ts
  • src/renderer/src/components/editor/image-viewer-zoom.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

Walkthrough

The mobile app now classifies SVG files as vector documents and renders non-truncated SVGs in a preview with source access. A shared pan/zoom hook supports dragging, pinch zoom, and double-tap zoom, and image previews now use it. File previews can use draft SVG markup and show save errors. The change also updates SVG translations, adds preview tests, and relocates shared pan/zoom constants and imports.

Severity of issue fixed: Medium

Merge Risk: Minimal · up to 3a0d7

No actionable issue was established for the mobile SVG preview or image gestures. The change is mergeable after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly summarizes the main changes: mobile SVG rendering and pinch-zoom support for image previews, including Android.
Description check Passed The description is mostly complete. It includes an ELI5 summary, detailed changes, linked issue, visual proof, testing results, platform coverage, known limitations, and completed testing checkboxes. …
Linked Issues check Passed For #23629, the PR implements the stated mobile scope. mobile-artifact-kind.ts classifies SVG files as vector. MobileSvgPreview provides Preview and Source modes and renders a data URI with intr…
Out of Scope Changes check Passed The changed files support #23629 mobile objectives. The mobile viewer, SVG data-URI helper, shared pan/zoom code, localization entries, refactoring, and tests all support the requested SVG preview or …
Full details: Description check

Explanation

The description is mostly complete. It includes an ELI5 summary, detailed changes, linked issue, visual proof, testing results, platform coverage, known limitations, and completed testing checkboxes. It does not include explicit sections for Why, AI Disclosure, Review, Agent skill upstream boundary, Notes, or the remaining checklist items, but the core required information is present.

Full details: Linked Issues check

Explanation

For #23629, the PR implements the stated mobile scope. mobile-artifact-kind.ts classifies SVG files as vector. MobileSvgPreview provides Preview and Source modes and renders a data URI with intrinsic SVG sizing. MobileFilePreviewBody preserves truncated-source behavior and shows draft content and save errors for editable SVG files. Markdown SVG loading uses files.read. MobilePanZoomImage and useMobilePanZoom provide pointer-based pan, pinch zoom, double-tap zoom, edge containment, rotation handling, and Back-to-fit behavior. Shared pan/zoom code supports the fit and 100% geometry requirement. The added render and unit tests cover the key behavior. The desktop half is explicitly outside this mobile PR scope.

Full details: Out of Scope Changes check

Explanation

The changed files support #23629 mobile objectives. The mobile viewer, SVG data-URI helper, shared pan/zoom code, localization entries, refactoring, and tests all support the requested SVG preview or image navigation behavior. No unrelated product change is demonstrated.

Full details: Docstring Coverage

Explanation

Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 14 functions across 27 files. (11 skipped: 11 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@nwparker nwparker closed this Oct 31, 2026
@nwparker nwparker reopened this Oct 31, 2026
@nwparker
nwparker merged commit 6fb044b into main Oct 31, 2026
58 checks passed
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.

[Feature]: Pan and zoom SVG files as vector documents — desktop viewer and Orca mobile

1 participant