From bb2482f3646f1eef5858ab72036da4e6f2abc126 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 04:10:32 +0000 Subject: [PATCH] Copy Mermaid diagrams as an image The diagram's copy button opens a menu like download's: Image (.png) puts the same PNG export on the clipboard, and Mermaid source copies the code. Each confirms with a toast. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y8zZi9uphvmprNJWq9UTWm --- .../markdown/render/diagram/Diagram.tsx | 55 +++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/src/features/markdown/render/diagram/Diagram.tsx b/src/features/markdown/render/diagram/Diagram.tsx index 4371dd2..ce3f6c1 100644 --- a/src/features/markdown/render/diagram/Diagram.tsx +++ b/src/features/markdown/render/diagram/Diagram.tsx @@ -1,11 +1,10 @@ import { Dialog as DialogPrimitive } from '@base-ui/react/dialog' -import { Download, FileCode2, FileImage, Maximize2, TriangleAlert, X } from 'lucide-react' +import { Copy, Download, FileCode2, FileImage, Maximize2, TriangleAlert, X } from 'lucide-react' import { use, useEffect, useState } from 'react' import { toast } from 'sonner' import { useTheme } from '@/features/theme/ThemeProvider' import { downloadFile } from '@/lib/download' -import { CopyButton } from '@/ui/copy-button' import { DropdownMenu, DropdownMenuContent, @@ -55,15 +54,38 @@ export function Diagram({ code, line }: { code: string; line?: number }) { const svg = state.svg + async function renderPng() { + const flat = await renderMermaid(code, resolved, look, { htmlLabels: false }) + return svgToPng(flat, diagramBackground(resolved)) + } + async function downloadPng() { try { - const flat = await renderMermaid(code, resolved, look, { htmlLabels: false }) - downloadFile('diagram.png', await svgToPng(flat, diagramBackground(resolved))) + downloadFile('diagram.png', await renderPng()) } catch { toast.error('This diagram could not be exported as PNG.') } } + async function copyPng() { + try { + // Hand the clipboard a promise so Safari still counts this as part of the click. + await navigator.clipboard.write([new ClipboardItem({ 'image/png': renderPng() })]) + toast.success('Copied diagram as image') + } catch { + toast.error('This diagram could not be copied as an image.') + } + } + + async function copySource() { + try { + await navigator.clipboard.writeText(code) + toast.success('Copied Mermaid source') + } catch { + toast.error('Clipboard access was blocked') + } + } + return (
@@ -111,7 +133,30 @@ export function Diagram({ code, line }: { code: string; line?: number }) { - + + + } + > + + + + void copyPng()}> + + Image (.png) + + void copySource()}> + + Mermaid source + + +