65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
// Security: SVG content rendered here is server-sanitized by DOMPurify (DIAG-05).
|
|
// Pattern mirrors MarkdownBody.tsx mermaid rendering using dangerouslySetInnerHTML.
|
|
import type { DiagramBundle } from "@/types/content-bundles";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
interface DiagramPreviewProps {
|
|
bundle: DiagramBundle | null;
|
|
className?: string;
|
|
}
|
|
|
|
function base64ToBlob(base64: string, mimeType: string): Blob {
|
|
const byteString = atob(base64);
|
|
const ab = new ArrayBuffer(byteString.length);
|
|
const ia = new Uint8Array(ab);
|
|
for (let i = 0; i < byteString.length; i++) {
|
|
ia[i] = byteString.charCodeAt(i);
|
|
}
|
|
return new Blob([ab], { type: mimeType });
|
|
}
|
|
|
|
function triggerDownload(blob: Blob, filename: string) {
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
export function DiagramPreview({ bundle, className }: DiagramPreviewProps) {
|
|
if (!bundle) return null;
|
|
|
|
const svgHtml = atob(bundle.svgBase64);
|
|
|
|
function handleDownloadSvg() {
|
|
if (!bundle) return;
|
|
const blob = base64ToBlob(bundle.svgBase64, "image/svg+xml");
|
|
triggerDownload(blob, "diagram.svg");
|
|
}
|
|
|
|
function handleDownloadPng() {
|
|
if (!bundle) return;
|
|
const blob = base64ToBlob(bundle.pngBase64, "image/png");
|
|
triggerDownload(blob, "diagram.png");
|
|
}
|
|
|
|
return (
|
|
<div className={className}>
|
|
{/* SVG is pre-sanitized by DOMPurify on the server (diagram-renderer.ts, DIAG-05).
|
|
This mirrors MarkdownBody.tsx mermaid SVG rendering. */}
|
|
{/* eslint-disable-next-line react/no-danger */}
|
|
<div className="paperclip-mermaid overflow-x-auto" dangerouslySetInnerHTML={{ __html: svgHtml }} />
|
|
<div className="flex gap-2 mt-3">
|
|
<Button type="button" variant="ghost" size="sm" onClick={handleDownloadSvg}>
|
|
Download SVG
|
|
</Button>
|
|
<Button type="button" variant="ghost" size="sm" onClick={handleDownloadPng}>
|
|
Download PNG
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|