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
This commit is contained in:
@@ -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 (
|
||||
<div className="relative group my-4">
|
||||
{language && (
|
||||
<div className="absolute top-0 left-0 px-3 py-1 text-[10px] font-medium uppercase tracking-wider text-zinc-500 bg-black/30 rounded-tl-lg rounded-br-lg border-b border-r border-white/5">
|
||||
{language}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="absolute top-2 right-2 p-1.5 rounded-md bg-white/5 border border-white/10 text-zinc-500 hover:text-white hover:bg-white/10 transition-all opacity-0 group-hover:opacity-100 z-10"
|
||||
@@ -47,7 +41,7 @@ export function CodeBlock({ children, className, inline }: CodeBlockProps) {
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
<pre className="overflow-x-auto rounded-lg bg-black/50 border border-white/10 p-4 pt-8 text-sm leading-relaxed font-mono scrollbar-thin">
|
||||
<pre className="overflow-x-auto rounded-lg bg-black/50 border border-white/10 p-4 text-sm leading-relaxed font-mono scrollbar-thin">
|
||||
<code className={`text-zinc-300 ${className || ""}`}>{codeString}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [svgHtml, setSvgHtml] = useState<string>("");
|
||||
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 (
|
||||
<div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20">
|
||||
@@ -66,17 +133,107 @@ export function MermaidDiagram({ chart }: MermaidDiagramProps) {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="mermaid-diagram overflow-x-auto"
|
||||
style={{ minHeight: "100px" }}
|
||||
const controls = (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setZoom((z) => Math.min(z + 0.25, 5))}
|
||||
className="p-1.5 rounded-md text-zinc-500 hover:text-white hover:bg-white/10 transition-colors"
|
||||
title="Zoom in"
|
||||
>
|
||||
<ZoomIn className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="text-xs text-zinc-500 w-12 text-center tabular-nums">
|
||||
{Math.round(zoom * 100)}%
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setZoom((z) => Math.max(z - 0.25, 0.3))}
|
||||
className="p-1.5 rounded-md text-zinc-500 hover:text-white hover:bg-white/10 transition-colors"
|
||||
title="Zoom out"
|
||||
>
|
||||
<ZoomOut className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="w-px h-4 bg-white/10 mx-1" />
|
||||
<button
|
||||
onClick={resetView}
|
||||
className="p-1.5 rounded-md text-zinc-500 hover:text-white hover:bg-white/10 transition-colors"
|
||||
title="Reset view"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
className="p-1.5 rounded-md text-zinc-500 hover:text-white hover:bg-white/10 transition-colors"
|
||||
title={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
|
||||
>
|
||||
{isFullscreen ? (
|
||||
<Minimize2 className="w-4 h-4" />
|
||||
) : (
|
||||
<Maximize2 className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const diagramView = (fullHeight?: boolean) => (
|
||||
<div
|
||||
className={`overflow-hidden ${isPanning ? "cursor-grabbing" : "cursor-grab"}`}
|
||||
onWheel={handleWheel}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
style={fullHeight ? { height: "100%" } : { minHeight: "100px" }}
|
||||
>
|
||||
{!isReady && (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="w-6 h-6 border-2 border-blue-500/30 border-t-blue-500 rounded-full animate-spin" />
|
||||
<div
|
||||
className="mermaid-diagram flex items-center justify-center"
|
||||
style={{
|
||||
transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
|
||||
transformOrigin: "center center",
|
||||
transition: isPanning ? "none" : "transform 0.15s ease-out",
|
||||
minHeight: fullHeight ? "100%" : "100px",
|
||||
}}
|
||||
dangerouslySetInnerHTML={svgHtml ? { __html: svgHtml } : undefined}
|
||||
>
|
||||
{!svgHtml && !isReady && (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="w-6 h-6 border-2 border-blue-500/30 border-t-blue-500 rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isFullscreen) {
|
||||
return (
|
||||
<>
|
||||
<div style={{ minHeight: "100px" }} />
|
||||
<div className="fixed inset-0 z-50 bg-[#0a0a0f]/95 backdrop-blur-sm flex flex-col">
|
||||
<div className="flex items-center justify-between px-6 py-3 border-b border-white/10">
|
||||
<span className="text-sm text-zinc-400">Architecture Diagram</span>
|
||||
{controls}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{diagramView(true)}
|
||||
</div>
|
||||
<div className="px-6 py-2 border-t border-white/10 text-center">
|
||||
<span className="text-xs text-zinc-600">
|
||||
Scroll to zoom · Drag to pan · Esc to close
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-end mb-2">{controls}</div>
|
||||
{diagramView()}
|
||||
<div className="mt-2 text-center">
|
||||
<span className="text-xs text-zinc-600">
|
||||
Scroll to zoom · Drag to pan
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user