diff --git a/apps/web/src/components/code-block.tsx b/apps/web/src/components/code-block.tsx
index 98ef55a..28c7d22 100644
--- a/apps/web/src/components/code-block.tsx
+++ b/apps/web/src/components/code-block.tsx
@@ -20,7 +20,6 @@ export function CodeBlock({ children, className, inline }: CodeBlockProps) {
);
}
- const language = className?.replace("language-", "") || "";
const codeString = String(children).replace(/\n$/, "");
const handleCopy = async () => {
@@ -31,11 +30,6 @@ export function CodeBlock({ children, className, inline }: CodeBlockProps) {
return (
- {language && (
-
- {language}
-
- )}
)}
-
+
{codeString}
diff --git a/apps/web/src/components/mermaid-diagram.tsx b/apps/web/src/components/mermaid-diagram.tsx
index 2ada914..55fdc58 100644
--- a/apps/web/src/components/mermaid-diagram.tsx
+++ b/apps/web/src/components/mermaid-diagram.tsx
@@ -1,7 +1,8 @@
"use client";
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useRef, useState, useCallback } from "react";
import mermaid from "mermaid";
+import { Maximize2, Minimize2, ZoomIn, ZoomOut, RotateCcw } from "lucide-react";
interface MermaidDiagramProps {
chart: string;
@@ -11,6 +12,12 @@ export function MermaidDiagram({ chart }: MermaidDiagramProps) {
const containerRef = useRef(null);
const [error, setError] = useState(null);
const [isReady, setIsReady] = useState(false);
+ const [svgHtml, setSvgHtml] = useState("");
+ const [isFullscreen, setIsFullscreen] = useState(false);
+ const [zoom, setZoom] = useState(1);
+ const [pan, setPan] = useState({ x: 0, y: 0 });
+ const [isPanning, setIsPanning] = useState(false);
+ const panStart = useRef({ x: 0, y: 0 });
useEffect(() => {
mermaid.initialize({
@@ -38,25 +45,85 @@ export function MermaidDiagram({ chart }: MermaidDiagramProps) {
}, []);
useEffect(() => {
- if (!isReady || !containerRef.current || !chart) return;
+ if (!isReady || !chart) return;
const renderChart = async () => {
try {
const id = `mermaid-${Math.random().toString(36).slice(2, 9)}`;
const { svg } = await mermaid.render(id, chart);
-
- if (containerRef.current) {
- containerRef.current.innerHTML = svg;
- setError(null);
- }
+ setSvgHtml(svg);
+ setError(null);
} catch (err) {
- setError(err instanceof Error ? err.message : "Failed to render diagram");
+ setError(
+ err instanceof Error ? err.message : "Failed to render diagram"
+ );
}
};
renderChart();
}, [chart, isReady]);
+ const handleWheel = useCallback((e: React.WheelEvent) => {
+ e.preventDefault();
+ const delta = e.deltaY > 0 ? -0.1 : 0.1;
+ setZoom((prev) => Math.min(Math.max(0.3, prev + delta), 5));
+ }, []);
+
+ const handleMouseDown = useCallback(
+ (e: React.MouseEvent) => {
+ if (e.button !== 0) return;
+ setIsPanning(true);
+ panStart.current = { x: e.clientX - pan.x, y: e.clientY - pan.y };
+ },
+ [pan]
+ );
+
+ const handleMouseMove = useCallback(
+ (e: React.MouseEvent) => {
+ if (!isPanning) return;
+ setPan({
+ x: e.clientX - panStart.current.x,
+ y: e.clientY - panStart.current.y,
+ });
+ },
+ [isPanning]
+ );
+
+ const handleMouseUp = useCallback(() => {
+ setIsPanning(false);
+ }, []);
+
+ const resetView = useCallback(() => {
+ setZoom(1);
+ setPan({ x: 0, y: 0 });
+ }, []);
+
+ const toggleFullscreen = useCallback(() => {
+ setIsFullscreen((prev) => !prev);
+ setZoom(1);
+ setPan({ x: 0, y: 0 });
+ }, []);
+
+ useEffect(() => {
+ if (!isFullscreen) return;
+ const handleKey = (e: KeyboardEvent) => {
+ if (e.key === "Escape") setIsFullscreen(false);
+ };
+ window.addEventListener("keydown", handleKey);
+ return () => window.removeEventListener("keydown", handleKey);
+ }, [isFullscreen]);
+
+ useEffect(() => {
+ if (isFullscreen) {
+ document.body.style.overflow = "hidden";
+ } else {
+ document.body.style.overflow = "";
+ }
+ return () => {
+ document.body.style.overflow = "";
+ };
+ }, [isFullscreen]);
+
if (error) {
return (
@@ -66,17 +133,107 @@ export function MermaidDiagram({ chart }: MermaidDiagramProps) {
);
}
- return (
-
+
+
+ {Math.round(zoom * 100)}%
+
+
+
+
+
+
+ );
+
+ const diagramView = (fullHeight?: boolean) => (
+
- {!isReady && (
-
-
+
+ {!svgHtml && !isReady && (
+
+ )}
+
+
+ );
+
+ if (isFullscreen) {
+ return (
+ <>
+
+
+
+ Architecture Diagram
+ {controls}
+
+
+ {diagramView(true)}
+
+
+
+ Scroll to zoom · Drag to pan · Esc to close
+
+
- )}
+ >
+ );
+ }
+
+ return (
+
+
{controls}
+ {diagramView()}
+
+
+ Scroll to zoom · Drag to pan
+
+
);
}