Skip to content

fix(components): make the Mermaid full-screen viewer closable on touch - #388

Merged
lodystage[bot] merged 1 commit into
mainfrom
feat/mermaid
Sep 4, 2026
Merged

fix(components): make the Mermaid full-screen viewer closable on touch#388
lodystage[bot] merged 1 commit into
mainfrom
feat/mermaid

Conversation

@lodystage

@lodystage lodystage Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Related issue

Problem / pressure

A Mermaid diagram opened full screen on a phone could not be closed.

Streamdown's bundled Mermaid overlay puts its only exit — a 32px button — at a
raw top-4 right-4. Measured against a simulated iPhone inset that is
{top: 16, right: 16, w: 32, h: 32} against --safe-area-top: 59px: the whole
button sits inside the status-bar strip, where the system takes the touch. The
overlay's own content layer is size-full with stopPropagation, so
elementFromPoint anywhere on the "backdrop" returns that layer and a tap never
reaches the close handler. Escape is the third exit and touch devices have none.
Opened, the overlay was a dead end.

It was also unreadable before it was inescapable: the overlay renders the
diagram scaled to fit, which turns the sequence diagrams agents emit into a grey
texture on a 390px screen. That is what makes a user reach for the corner in the
first place.

Two adjacent gaps came out of the same reproduction: the block's own
copy/download controls are revealed only on :hover/:focus-within, with no
(hover: none) rule — the code block has one — so on touch they are invisible
for good; and the full-screen portal mounts on document.body at z-50, below
--z-dialog (80) and every other layer on the app's scale.

Summary

  • mermaid-diagram-viewer.tsx is a new full-screen viewer. Its control bar is
    padded by the --safe-area-* variables rather than a fixed viewport offset,
    its controls are 44px (the shared size="icon" is 36), it always offers three
    exits (close button, a click anywhere off the diagram, Escape), and it stacks
    at --z-image-viewer so a diagram opened from a message inside a dialog lands
    above that dialog. Escape is handled in the capture phase with
    stopImmediatePropagation, so it dismisses the viewer alone instead of also
    closing the dialog underneath.
  • computeInitialDiagramZoom opens a diagram that does not fit at NATURAL size
    and pans it, and scales up only one that already fits (cap 3x). Explicit
    zoom-out / reset / zoom-in controls replace the fit-to-screen behavior.
  • markdown-renderer.tsx sets controls.mermaid.fullscreen: false and opens
    the new viewer from a delegated click/Enter/Space on the diagram, passing a
    cloneNode(true) of the already-rendered SVG — a live node, not markup
    re-parsed through dangerouslySetInnerHTML, and the copy in the conversation
    keeps its own size.
  • Streamdown owns that markup and the diagram appears only after a lazy import
    resolves, so role/tabindex/aria-label are applied by a MutationObserver
    that is installed only for text containing a ```mermaid fence.
  • tailwind/index.css gives mermaid-block-actions the (hover: none) reveal
    the code block already had, swaps the block's cursor: default for zoom-in,
    and adds a :focus-visible ring for the keyboard affordance.
  • MermaidPhoneViewer is a phone-shaped Storybook fixture that sets the
    --safe-area-* variables (a headless browser reports zero insets, so without
    it nothing can show a control parked under the status bar). The ai-gui
    AGENTS.md records the invariant.

Before / after

Before After
Close button {top: 16, w: 32, h: 32} — entirely inside a 59px status-bar inset {top: 59, right: 4, w: 44, h: 44} — clears the inset, meets the 44px touch floor
Tap on the backdrop: elementFromPoint returns the size-full stopPropagation layer; overlay stays open Click off the diagram closes; diagram itself does not
One exit, unreachable on touch Three exits: button, click-off, Escape
Diagram scaled to fit a 390px screen — labels illegible Opens at 100% and pans; +/− and reset available
Portal at z-50, under --z-dialog (80) --z-image-viewer (95)
Mermaid copy/download invisible on touch (no (hover: none) rule) Visible, matching the code block

Test plan

  • New tests/markdown-mermaid-fullscreen.test.tsx (8 cases): the bundled
    control is gone while copy/download remain; the diagram carries
    role/tabindex/aria-label; opening leaves the message copy in place; the
    control bar reserves the safe area and the close button is 44px at
    --z-image-viewer; all three exits close and a click on the diagram does not;
    Enter opens from the keyboard; plus the zoom-selection function directly.
    Verified the suite fails when fullscreen is restored to true.
  • Deterministic timing: fake timers drive Streamdown's 300ms visibility debounce
    and idle callback through advanceTimersByTimeAsync, so no wall-clock waits.
  • pnpm test in packages/components: 420 files / 3022 tests pass. (An earlier
    run had one unrelated markdown-streaming-reparse 5s timeout under load; it
    runs in 939ms in isolation and the clean full run includes it.)
  • pnpm --filter @lody/components typecheck clean; pnpm check:quick passes
    (lint 0 errors, i18n keys complete in en and zh_CN, code-collab imports,
    platform boundaries, public boundary); Prettier clean on every touched file.
  • Browser verification against the Storybook fixture at 390x844 with simulated
    insets and at 1280x800: measured the numbers in the Before/after table from
    the live DOM; confirmed all three exits with dispatched events; confirmed the
    (hover: none) rule ships in the compiled stylesheet; confirmed the viewer
    SVG scales 874 -> 1093px at 125% while the message copy stays 874px.
  • Not run: pnpm check in full. Its test:ci leg covers apps/cli and
    apps/electron, which this change does not touch; the component package's own
    suite and every static guard were run instead.

Context handoff

Instructions for reviewing agents

  • Review focus: mermaid-diagram-viewer.tsx (safe-area padding, the three
    exits, capture-phase Escape) and the delegated open path in
    markdown-renderer.tsx — particularly that the cloned SVG is a node rather
    than re-parsed markup, and that the MutationObserver is installed only for
    text with a mermaid fence, since a conversation mounts one renderer per block.
  • Decisions to challenge: opening a non-fitting diagram at natural size
    instead of fit-to-screen; making the diagram itself the click target rather
    than injecting a button into Streamdown's toolbar; and not pushing a history
    entry for back-gesture dismissal, which keeps this consistent with
    ZoomableImageViewer but leaves Android back unhandled.
  • Plausible failures / evidence gaps: safe-area evidence is a simulated
    inset in a headless Chromium, not a physical device, and the mobile entry
    point lives outside this repository; pinch-to-zoom is not implemented, so
    mobile zoom is buttons plus native pan; duplicated SVG ids now exist while
    the viewer is open (both copies resolve url(#...) to identical defs, but
    that is reasoning, not a rendered check on every diagram type).

Authoring context

  • User goal / directives: a Mermaid diagram opened full screen on a phone
    could not be closed by the X in the corner; analyze, reproduce, fix, verify.
  • Constraints / non-goals: no change to how diagrams are rendered
    (beautiful-mermaid, themes, the fallback path) or to the inline block's
    layout; Streamdown stays on the same version; the mobile app's own sources are
    outside this repository, so the fix lands in the shared component.
  • Risk-bearing decisions: none touching data, auth, or protocol. The change
    is presentation plus one dependency control flag; the cloned SVG is inserted
    as a node, so no new HTML-injection surface.
  • Destructive or irreversible behavior: none. No stored state, migration, or
    network path. The viewer restores document.body.style.overflow to its prior
    value on unmount.
  • Deliberately not done or tested: pinch-to-zoom and back-gesture dismissal
    (both explained above); Streamdown's inline panZoom stays off, since
    enabling it would hand chat scroll gestures to the diagram; no physical-device
    run.
  • Unknowns / confidence: high for the geometry and the three exits, which
    are measured and asserted; moderate for real iOS, where the inset comes from
    env() rather than the fixture's override and standalone-PWA insets differ
    from the browser's.

🤖 Generated with Claude Code

Streamdown's bundled Mermaid overlay put its only exit — a 32px button — at a
raw `top-4 right-4`, which on a phone lands entirely inside the status-bar
inset, where the system takes the touch. Its content layer then covered the
whole backdrop and stopped propagation, so a backdrop tap never reached the
close handler either, and touch surfaces have no Escape. The overlay could be
opened and not left. It also rendered the diagram scaled to fit, which turns an
agent's sequence diagram into an unreadable grey texture.

Turn that overlay off and mount `MermaidDiagramViewer` instead: controls padded
by the `--safe-area-*` variables at 44px, three exits (close button, a click off
the diagram, Escape), and `--z-image-viewer` so a diagram opened from a message
inside a dialog lands above it. A diagram that does not fit opens at natural
size and pans, with explicit zoom controls.

Streamdown owns the diagram markup, so the click target and its role/tabindex
are applied by observer, and the block's own copy/download controls now reveal
themselves on `(hover: none)` — the same rule the code block already carried,
without which they were invisible on touch for good.

Model: claude-opus-5

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e14f3d22b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}}
onClick={handleSurfaceClick}
>
<div className="flex min-h-full min-w-full items-center justify-center p-4">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Align oversized diagrams to a scrollable edge

When a diagram is wider than the viewer—as intended for phone-sized sequence diagrams—justify-center centers the non-shrinking SVG inside a viewport-width flex container, placing its leading portion at a negative horizontal offset. The scroll surface starts at offset zero and cannot scroll into that negative overflow, so roughly half of the diagram remains permanently clipped; use safe centering or start alignment whenever the content overflows. This breaks the documented natural-size-and-pan behavior for the primary mobile scenario.

AGENTS.md reference: packages/components/src/components/ai-gui/AGENTS.md:L120-L122

Useful? React with 👍 / 👎.

@lodystage
lodystage Bot merged commit f10f9c4 into main Sep 4, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant