From dd03d86642dfebb88cdd8b9a543ada8da17b2e42 Mon Sep 17 00:00:00 2001 From: Vectry Date: Mon, 9 Feb 2026 18:45:47 +0000 Subject: [PATCH] fix: remove code block language label overlap, add diagram zoom/pan/fullscreen - Remove 'bash' language label that overlapped with code text - Add zoom (scroll), pan (drag), and fullscreen toggle to Mermaid diagrams - Fullscreen mode with dark overlay, controls toolbar, and Esc to close - Zoom percentage indicator and reset button --- apps/web/src/components/code-block.tsx | 8 +- apps/web/src/components/mermaid-diagram.tsx | 191 ++++++++++++++++++-- 2 files changed, 175 insertions(+), 24 deletions(-) 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 + +
); }