Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion src/browser/features/Messages/Mermaid.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanup, render, waitFor } from "@testing-library/react";
import { cleanup, fireEvent, render, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { GlobalWindow } from "happy-dom";
import { StreamingContext } from "./StreamingContext";
Expand Down Expand Up @@ -179,6 +179,38 @@ describe("Mermaid layout stability", () => {
});
});

test("zoom controls scale rendered diagrams", async () => {
const view = renderMermaid();

await waitFor(() => {
expect(view.container.querySelector(".mermaid-container svg")).not.toBeNull();
});

const container = view.container.querySelector<HTMLElement>(".mermaid-container");
expect(container).not.toBeNull();
expect(container?.style.getPropertyValue("--diagram-zoom")).toBe("1");

const buttons = Array.from(view.container.querySelectorAll("button"));
const zoomOutButton = buttons.find((button) => button.textContent === "−");
const zoomInButton = buttons.find((button) => button.textContent === "+");
if (!zoomOutButton || !zoomInButton) {
throw new Error("Expected Mermaid zoom controls to render");
}

fireEvent.click(zoomInButton);

await waitFor(() => {
expect(container?.style.getPropertyValue("--diagram-zoom")).toBe("1.1");
});
expect(window.localStorage.getItem("mermaid-diagram-zoom")).toBe("1.1");

fireEvent.click(zoomOutButton);

await waitFor(() => {
expect(container?.style.getPropertyValue("--diagram-zoom")).toBe("1");
});
});

test("renders sanitized SVG inside the stable container", async () => {
mermaidRender.mockImplementation(() =>
Promise.resolve({
Expand Down
59 changes: 32 additions & 27 deletions src/browser/features/Messages/Mermaid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { TooltipIfPresent } from "@/browser/components/Tooltip/Tooltip";
import { usePersistedState } from "@/browser/hooks/usePersistedState";

const MIN_HEIGHT = 300;
const MAX_HEIGHT = 1200;
const DEFAULT_ZOOM = 1;
const MIN_ZOOM = 0.5;
const MAX_ZOOM = 3;
const ZOOM_STEP = 0.1;

// Initialize mermaid
mermaid.initialize({
Expand Down Expand Up @@ -188,6 +191,14 @@ export function sanitizeMermaidSvg(svg: string): string | null {
return svgRoot.outerHTML;
}

function normalizeDiagramZoom(value: number): number {
if (!Number.isFinite(value)) {
return DEFAULT_ZOOM;
}

return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Number(value.toFixed(2))));
}

// Common button styles
const getButtonStyle = (disabled = false): CSSProperties => ({
background: disabled ? "rgba(255, 255, 255, 0.05)" : "rgba(255, 255, 255, 0.1)",
Expand Down Expand Up @@ -286,24 +297,26 @@ export const Mermaid: React.FC<{ chart: string }> = ({ chart }) => {
return () => clearTimeout(timer);
}, [chart]);

const [diagramMaxHeight, setDiagramMaxHeight] = usePersistedState(
"mermaid-diagram-max-height",
MIN_HEIGHT,
const [storedDiagramZoom, setStoredDiagramZoom] = usePersistedState(
"mermaid-diagram-zoom",
DEFAULT_ZOOM,
{ listener: true }
);

const atMinHeight = diagramMaxHeight <= MIN_HEIGHT;
const atMaxHeight = diagramMaxHeight >= MAX_HEIGHT;

const handleIncreaseHeight = () => {
if (!atMaxHeight) {
setDiagramMaxHeight((prev) => Math.min(MAX_HEIGHT, Math.round(prev * 1.1)));
// The +/- controls are shown as zoom affordances, so scale the rendered SVG
// itself; changing only max-height is a no-op for diagrams already below the cap.
const diagramZoom = normalizeDiagramZoom(storedDiagramZoom);
const atMinZoom = diagramZoom <= MIN_ZOOM;
const atMaxZoom = diagramZoom >= MAX_ZOOM;

const handleZoomIn = () => {
if (!atMaxZoom) {
setStoredDiagramZoom((prev) => normalizeDiagramZoom(normalizeDiagramZoom(prev) + ZOOM_STEP));
}
};

const handleDecreaseHeight = () => {
if (!atMinHeight) {
setDiagramMaxHeight((prev) => Math.max(MIN_HEIGHT, Math.round(prev * 0.9)));
const handleZoomOut = () => {
if (!atMinZoom) {
setStoredDiagramZoom((prev) => normalizeDiagramZoom(normalizeDiagramZoom(prev) - ZOOM_STEP));
}
};

Expand Down Expand Up @@ -395,21 +408,13 @@ export const Mermaid: React.FC<{ chart: string }> = ({ chart }) => {
gap: "4px",
}}
>
<TooltipIfPresent tooltip="Decrease diagram height" side="bottom">
<button
onClick={handleDecreaseHeight}
disabled={atMinHeight}
style={getButtonStyle(atMinHeight)}
>
<TooltipIfPresent tooltip="Zoom out diagram" side="bottom">
<button onClick={handleZoomOut} disabled={atMinZoom} style={getButtonStyle(atMinZoom)}>
−
</button>
</TooltipIfPresent>
<TooltipIfPresent tooltip="Increase diagram height" side="bottom">
<button
onClick={handleIncreaseHeight}
disabled={atMaxHeight}
style={getButtonStyle(atMaxHeight)}
>
<TooltipIfPresent tooltip="Zoom in diagram" side="bottom">
<button onClick={handleZoomIn} disabled={atMaxZoom} style={getButtonStyle(atMaxZoom)}>
+
</button>
</TooltipIfPresent>
Expand All @@ -424,7 +429,7 @@ export const Mermaid: React.FC<{ chart: string }> = ({ chart }) => {
style={{
maxWidth: "70%",
margin: "0 auto",
["--diagram-max-height" as string]: `${diagramMaxHeight}px`,
["--diagram-zoom" as string]: `${diagramZoom}`,
minHeight: `${MIN_HEIGHT}px`,
...(showPendingPlaceholder
? {
Expand Down
4 changes: 3 additions & 1 deletion src/browser/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1993,7 +1993,7 @@ code {
display: flex;
justify-content: center;
align-items: center;
overflow-x: auto;
overflow: auto;
max-width: 100%;
}

Expand All @@ -2002,6 +2002,8 @@ code {
max-width: none;
max-height: var(--diagram-max-height, 300px);
height: auto;
/* Electron is Chromium-only; zoom affects layout/scrollbars, unlike transform. */
zoom: var(--diagram-zoom, 1);
}

/* Mermaid in modal - allow larger sizing */
Expand Down
4 changes: 3 additions & 1 deletion vscode/src/webview/webview.css
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ body {
display: flex;
justify-content: center;
align-items: center;
overflow-x: auto;
overflow: auto;
max-width: 100%;
}

Expand All @@ -724,6 +724,8 @@ body {
max-width: none;
max-height: var(--diagram-max-height, 300px);
height: auto;
/* VS Code webviews are Chromium-backed; zoom affects layout/scrollbars, unlike transform. */
zoom: var(--diagram-zoom, 1);
}

/* Mermaid in modal - allow larger sizing */
Expand Down
Loading