fix(web): simplify Mermaid presentation
This commit is contained in:
1 parent
3743cf4adb
commit
6b4dc287df
8 files changed
+154
-186
No files matched your search
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { NodeViewContent, NodeViewWrapper, type NodeViewProps } from "@tiptap/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Check, CircleAlert, Code2, Copy, Maximize2, Workflow } from "lucide-react";
|
||||
import { Check, CircleAlert, Code2, Copy, Maximize2 } from "lucide-react";
|
||||
import { MERMAID_THEME_PALETTES, useMermaidTheme } from "./ThemeProvider";
|
||||
import { MermaidViewer } from "./MermaidViewer";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { copyTextToClipboard } from "@/lib/clipboard";
|
||||
import { renderMermaidWithFallback } from "@/lib/mermaid-renderer";
|
||||
import { getOfficialMermaidThemeVariables } from "@/lib/mermaid-theme";
|
||||
@@ -34,7 +35,7 @@ const loadBeautifulMermaid = () => {
|
||||
|
||||
export const MermaidCodeBlock = ({ editor, node }: NodeViewProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { mermaidRenderer, mermaidTheme, setMermaidRenderer } = useMermaidTheme();
|
||||
const { mermaidTheme } = useMermaidTheme();
|
||||
const language = typeof node.attrs.language === "string" ? node.attrs.language.toLowerCase() : "plaintext";
|
||||
const source = node.textContent.trim();
|
||||
const isMermaid = language === "mermaid";
|
||||
@@ -90,7 +91,6 @@ export const MermaidCodeBlock = ({ editor, node }: NodeViewProps) => {
|
||||
return renderedSvg;
|
||||
});
|
||||
const renderPromise = renderMermaidWithFallback({
|
||||
renderer: mermaidRenderer,
|
||||
renderBeautiful,
|
||||
renderOfficial,
|
||||
});
|
||||
@@ -114,7 +114,7 @@ export const MermaidCodeBlock = ({ editor, node }: NodeViewProps) => {
|
||||
cancelled = true;
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, [isMermaid, mermaidRenderer, mermaidTheme, source]);
|
||||
}, [isMermaid, mermaidTheme, source]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper
|
||||
@@ -124,99 +124,106 @@ export const MermaidCodeBlock = ({ editor, node }: NodeViewProps) => {
|
||||
data-language={language}
|
||||
>
|
||||
{isMermaid ? (
|
||||
<div className="edgeever-mermaid-toolbar" contentEditable={false}>
|
||||
<button
|
||||
type="button"
|
||||
className="edgeever-mermaid-tool-button"
|
||||
aria-label={t(mermaidRenderer === "mermaid"
|
||||
? "editorToolbar.mermaidUseOrthogonalLayout"
|
||||
: "editorToolbar.mermaidUseStandardLayout")}
|
||||
title={t(mermaidRenderer === "mermaid"
|
||||
? "editorToolbar.mermaidUseOrthogonalLayout"
|
||||
: "editorToolbar.mermaidUseStandardLayout")}
|
||||
aria-pressed={mermaidRenderer === "beautiful"}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setMermaidRenderer(mermaidRenderer === "mermaid" ? "beautiful" : "mermaid");
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Workflow aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="edgeever-mermaid-tool-button"
|
||||
data-state={copyState}
|
||||
aria-label={t(copyState === "copied" ? "editorToolbar.codeCopied" : copyState === "error" ? "editorToolbar.codeCopyFailed" : "editorToolbar.copyCode")}
|
||||
title={t(copyState === "copied" ? "editorToolbar.codeCopied" : copyState === "error" ? "editorToolbar.codeCopyFailed" : "editorToolbar.copyCode")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void handleCopy();
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
{copyState === "copied"
|
||||
? <Check aria-hidden="true" />
|
||||
: copyState === "error"
|
||||
? <CircleAlert aria-hidden="true" />
|
||||
: <Copy aria-hidden="true" />}
|
||||
</button>
|
||||
{editor.isEditable && (
|
||||
<button
|
||||
type="button"
|
||||
className="edgeever-mermaid-tool-button"
|
||||
aria-label={t(sourceVisible ? "editorToolbar.mermaidHideSource" : "editorToolbar.mermaidShowSource")}
|
||||
title={t(sourceVisible ? "editorToolbar.mermaidHideSource" : "editorToolbar.mermaidShowSource")}
|
||||
aria-pressed={sourceVisible}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setSourceVisible((visible) => !visible);
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Code2 aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
{svg && (
|
||||
<button
|
||||
type="button"
|
||||
className="edgeever-mermaid-tool-button"
|
||||
aria-label={t("editorToolbar.mermaidOpenViewer")}
|
||||
title={t("editorToolbar.mermaidOpenViewer")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setViewerOpen(true);
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Maximize2 aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<TooltipProvider delayDuration={0} skipDelayDuration={0}>
|
||||
<div className="edgeever-mermaid-toolbar" contentEditable={false}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="edgeever-mermaid-tool-button"
|
||||
data-state={copyState}
|
||||
aria-label={t(copyState === "copied" ? "editorToolbar.codeCopied" : copyState === "error" ? "editorToolbar.codeCopyFailed" : "editorToolbar.copyCode")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void handleCopy();
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
{copyState === "copied"
|
||||
? <Check aria-hidden="true" />
|
||||
: copyState === "error"
|
||||
? <CircleAlert aria-hidden="true" />
|
||||
: <Copy aria-hidden="true" />}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{t(copyState === "copied" ? "editorToolbar.codeCopied" : copyState === "error" ? "editorToolbar.codeCopyFailed" : "editorToolbar.copyCode")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{editor.isEditable && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="edgeever-mermaid-tool-button"
|
||||
aria-label={t(sourceVisible ? "editorToolbar.mermaidHideSource" : "editorToolbar.mermaidShowSource")}
|
||||
aria-pressed={sourceVisible}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setSourceVisible((visible) => !visible);
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Code2 aria-hidden="true" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{t(sourceVisible ? "editorToolbar.mermaidHideSource" : "editorToolbar.mermaidShowSource")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{svg && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="edgeever-mermaid-tool-button"
|
||||
aria-label={t("editorToolbar.mermaidOpenViewer")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setViewerOpen(true);
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Maximize2 aria-hidden="true" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{t("editorToolbar.mermaidOpenViewer")}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="edgeever-code-copy-button"
|
||||
contentEditable={false}
|
||||
aria-label={t(copyState === "copied" ? "editorToolbar.codeCopied" : copyState === "error" ? "editorToolbar.codeCopyFailed" : "editorToolbar.copyCode")}
|
||||
title={t(copyState === "copied" ? "editorToolbar.codeCopied" : copyState === "error" ? "editorToolbar.codeCopyFailed" : "editorToolbar.copyCode")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void handleCopy();
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
{copyState === "copied"
|
||||
? t("editorToolbar.codeCopied")
|
||||
: copyState === "error"
|
||||
? t("editorToolbar.codeCopyFailed")
|
||||
: t("editorToolbar.copyCode")}
|
||||
</button>
|
||||
<TooltipProvider delayDuration={0} skipDelayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="edgeever-code-copy-button"
|
||||
contentEditable={false}
|
||||
aria-label={t(copyState === "copied" ? "editorToolbar.codeCopied" : copyState === "error" ? "editorToolbar.codeCopyFailed" : "editorToolbar.copyCode")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void handleCopy();
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
{copyState === "copied"
|
||||
? t("editorToolbar.codeCopied")
|
||||
: copyState === "error"
|
||||
? t("editorToolbar.codeCopyFailed")
|
||||
: t("editorToolbar.copyCode")}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{t(copyState === "copied" ? "editorToolbar.codeCopied" : copyState === "error" ? "editorToolbar.codeCopyFailed" : "editorToolbar.copyCode")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{isMermaid && (
|
||||
<div
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
normalizeMermaidSvgForViewer,
|
||||
resolveMermaidViewerBackground,
|
||||
} from "@/lib/mermaid-svg";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
interface MermaidViewerProps {
|
||||
closeLabel: string;
|
||||
@@ -60,17 +61,22 @@ export const MermaidViewer = ({
|
||||
if (!open || !objectUrl) return null;
|
||||
|
||||
const resetButton = (
|
||||
<button
|
||||
key="reset-mermaid-zoom"
|
||||
type="button"
|
||||
className="edgeever-mermaid-viewer-reset"
|
||||
disabled={zoom <= 1}
|
||||
aria-label={resetZoomLabel}
|
||||
title={resetZoomLabel}
|
||||
onClick={() => zoomRef.current?.changeZoom(1)}
|
||||
>
|
||||
<RotateCcw aria-hidden="true" />
|
||||
</button>
|
||||
<TooltipProvider key="reset-mermaid-zoom" delayDuration={0} skipDelayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="edgeever-mermaid-viewer-reset"
|
||||
disabled={zoom <= 1}
|
||||
aria-label={resetZoomLabel}
|
||||
onClick={() => zoomRef.current?.changeZoom(1)}
|
||||
>
|
||||
<RotateCcw aria-hidden="true" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{resetZoomLabel}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -49,9 +49,6 @@ export const MERMAID_THEME_PALETTES: Record<MermaidThemeName, MermaidThemePalett
|
||||
"one-dark": { bg: "#282c34", fg: "#abb2bf", line: "#4b5263", accent: "#c678dd", muted: "#5c6370" },
|
||||
};
|
||||
|
||||
export const MERMAID_RENDERERS = ["mermaid", "beautiful"] as const;
|
||||
export type MermaidRenderer = (typeof MERMAID_RENDERERS)[number];
|
||||
|
||||
export const EDITOR_THEME_NAMES = [
|
||||
"default",
|
||||
"minimal-emerald",
|
||||
@@ -117,8 +114,6 @@ interface AppearanceThemeContextValue {
|
||||
interface MermaidThemeContextValue {
|
||||
mermaidTheme: MermaidThemeName;
|
||||
setMermaidTheme: (theme: MermaidThemeName) => void;
|
||||
mermaidRenderer: MermaidRenderer;
|
||||
setMermaidRenderer: (renderer: MermaidRenderer) => void;
|
||||
}
|
||||
|
||||
interface EditorThemeContextValue {
|
||||
@@ -136,7 +131,6 @@ interface ThemeProviderProps {
|
||||
|
||||
const THEME_STORAGE_KEY = "edgeever.theme";
|
||||
const MERMAID_THEME_STORAGE_KEY = "edgeever.mermaid-theme";
|
||||
const MERMAID_RENDERER_STORAGE_KEY = "edgeever.mermaid-renderer";
|
||||
const EDITOR_THEME_STORAGE_KEY = "edgeever.editor-theme";
|
||||
const CUSTOM_EDITOR_THEME_STORAGE_KEY = "edgeever.custom-editor-theme";
|
||||
const CUSTOM_EDITOR_THEMES_STORAGE_KEY = "edgeever.custom-editor-themes";
|
||||
@@ -173,11 +167,6 @@ export const getStoredMermaidTheme = (): MermaidThemeName => {
|
||||
return MERMAID_THEME_NAMES.includes(stored as MermaidThemeName) ? stored as MermaidThemeName : "zinc-light";
|
||||
};
|
||||
|
||||
export const getStoredMermaidRenderer = (): MermaidRenderer => {
|
||||
const stored = readLocalStorageItem(MERMAID_RENDERER_STORAGE_KEY);
|
||||
return MERMAID_RENDERERS.includes(stored as MermaidRenderer) ? stored as MermaidRenderer : "beautiful";
|
||||
};
|
||||
|
||||
export const getStoredEditorTheme = (): string => {
|
||||
return readLocalStorageItem(EDITOR_THEME_STORAGE_KEY) || "default";
|
||||
};
|
||||
@@ -267,7 +256,6 @@ export const ThemeProvider = ({ children }: ThemeProviderProps) => {
|
||||
const [preference, setPreferenceState] = useState<ThemePreference>(getStoredThemePreference);
|
||||
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => resolveTheme(preference));
|
||||
const [mermaidTheme, setMermaidThemeState] = useState<MermaidThemeName>(getStoredMermaidTheme);
|
||||
const [mermaidRenderer, setMermaidRendererState] = useState<MermaidRenderer>(getStoredMermaidRenderer);
|
||||
const [editorTheme, setEditorThemeState] = useState<string>(getStoredEditorTheme);
|
||||
const [customEditorThemes, setCustomEditorThemesState] = useState<CustomEditorTheme[]>(getStoredCustomEditorThemes);
|
||||
|
||||
@@ -323,11 +311,6 @@ export const ThemeProvider = ({ children }: ThemeProviderProps) => {
|
||||
window.localStorage.setItem(MERMAID_THEME_STORAGE_KEY, nextTheme);
|
||||
}, []);
|
||||
|
||||
const setMermaidRenderer = useCallback((nextRenderer: MermaidRenderer) => {
|
||||
setMermaidRendererState(nextRenderer);
|
||||
window.localStorage.setItem(MERMAID_RENDERER_STORAGE_KEY, nextRenderer);
|
||||
}, []);
|
||||
|
||||
const setEditorTheme = useCallback((nextTheme: string) => {
|
||||
setEditorThemeState(nextTheme);
|
||||
window.localStorage.setItem(EDITOR_THEME_STORAGE_KEY, nextTheme);
|
||||
@@ -346,10 +329,8 @@ export const ThemeProvider = ({ children }: ThemeProviderProps) => {
|
||||
() => ({
|
||||
mermaidTheme,
|
||||
setMermaidTheme,
|
||||
mermaidRenderer,
|
||||
setMermaidRenderer,
|
||||
}),
|
||||
[mermaidRenderer, mermaidTheme, setMermaidRenderer, setMermaidTheme]
|
||||
[mermaidTheme, setMermaidTheme]
|
||||
);
|
||||
|
||||
const editorValue = useMemo(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const readSource = (relativePath) => readFileSync(new URL(relativePath, import.meta.url), "utf8");
|
||||
|
||||
describe("Mermaid tooltips", () => {
|
||||
test("uses shared tooltips instead of native title attributes", () => {
|
||||
const codeBlock = readSource("./MermaidCodeBlock.tsx");
|
||||
const viewer = readSource("./MermaidViewer.tsx");
|
||||
|
||||
expect(codeBlock).toContain("TooltipProvider");
|
||||
expect(codeBlock).toContain("TooltipTrigger");
|
||||
expect(codeBlock).toContain("TooltipContent");
|
||||
expect(viewer).toContain("TooltipContent");
|
||||
expect(codeBlock).not.toMatch(/\btitle=/);
|
||||
expect(viewer).not.toMatch(/\btitle=/);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { renderMermaidSVG } from "beautiful-mermaid";
|
||||
import mermaid from "mermaid";
|
||||
import { getStoredMermaidRenderer } from "../components/ThemeProvider";
|
||||
import { renderMermaidWithFallback } from "./mermaid-renderer";
|
||||
|
||||
const GANTT_SOURCE = `gantt
|
||||
@@ -9,49 +8,19 @@ const GANTT_SOURCE = `gantt
|
||||
section Web
|
||||
Compatibility fallback :fallback, 2026-07-30, 1d`;
|
||||
|
||||
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
|
||||
|
||||
const restoreWindow = () => {
|
||||
if (originalWindow) {
|
||||
Object.defineProperty(globalThis, "window", originalWindow);
|
||||
} else {
|
||||
delete globalThis.window;
|
||||
}
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
restoreWindow();
|
||||
});
|
||||
|
||||
describe("Mermaid renderer fallback", () => {
|
||||
test("defaults to the compact renderer when no browser preference exists", () => {
|
||||
delete globalThis.window;
|
||||
expect(getStoredMermaidRenderer()).toBe("beautiful");
|
||||
});
|
||||
|
||||
test("defaults to the compact renderer when window exists without localStorage", () => {
|
||||
globalThis.window = {
|
||||
location: { hostname: "notes.example.com" },
|
||||
};
|
||||
expect(getStoredMermaidRenderer()).toBe("beautiful");
|
||||
});
|
||||
|
||||
test("defaults to the compact renderer when localStorage is empty or blocked", () => {
|
||||
globalThis.window = {
|
||||
localStorage: {
|
||||
getItem: () => null,
|
||||
test("always uses the compact renderer when it supports the diagram", async () => {
|
||||
let officialRenderCount = 0;
|
||||
const svg = await renderMermaidWithFallback({
|
||||
renderBeautiful: () => '<svg data-renderer="beautiful"></svg>',
|
||||
renderOfficial: () => {
|
||||
officialRenderCount += 1;
|
||||
return '<svg data-renderer="mermaid"></svg>';
|
||||
},
|
||||
};
|
||||
expect(getStoredMermaidRenderer()).toBe("beautiful");
|
||||
});
|
||||
|
||||
globalThis.window = {
|
||||
localStorage: {
|
||||
getItem: () => {
|
||||
throw new Error("blocked");
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getStoredMermaidRenderer()).toBe("beautiful");
|
||||
expect(officialRenderCount).toBe(0);
|
||||
expect(svg).toContain('data-renderer="beautiful"');
|
||||
});
|
||||
|
||||
test("uses the official renderer for a valid Gantt diagram unsupported by beautiful-mermaid", async () => {
|
||||
@@ -59,7 +28,6 @@ describe("Mermaid renderer fallback", () => {
|
||||
|
||||
let officialRenderCount = 0;
|
||||
const svg = await renderMermaidWithFallback({
|
||||
renderer: "beautiful",
|
||||
renderBeautiful: () => renderMermaidSVG(GANTT_SOURCE),
|
||||
renderOfficial: () => {
|
||||
officialRenderCount += 1;
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
export type MermaidRendererName = "mermaid" | "beautiful";
|
||||
|
||||
interface MermaidRenderFallbackOptions {
|
||||
renderer: MermaidRendererName;
|
||||
renderBeautiful: () => string | Promise<string>;
|
||||
renderOfficial: () => string | Promise<string>;
|
||||
}
|
||||
|
||||
export const renderMermaidWithFallback = async ({
|
||||
renderer,
|
||||
renderBeautiful,
|
||||
renderOfficial,
|
||||
}: MermaidRenderFallbackOptions) => {
|
||||
if (renderer === "beautiful") {
|
||||
try {
|
||||
return await renderBeautiful();
|
||||
} catch {
|
||||
return renderOfficial();
|
||||
}
|
||||
try {
|
||||
return await renderBeautiful();
|
||||
} catch {
|
||||
return renderOfficial();
|
||||
}
|
||||
|
||||
return renderOfficial();
|
||||
};
|
||||
@@ -1104,8 +1104,6 @@ export const enUS = {
|
||||
mermaidSource: "Mermaid source",
|
||||
mermaidShowSource: "Show Mermaid source",
|
||||
mermaidHideSource: "Hide Mermaid source",
|
||||
mermaidUseStandardLayout: "Switch to standard layout",
|
||||
mermaidUseOrthogonalLayout: "Switch to orthogonal layout",
|
||||
mermaidOpenViewer: "Open Mermaid diagram viewer",
|
||||
mermaidViewer: "Mermaid diagram viewer",
|
||||
mermaidZoomIn: "Zoom in diagram",
|
||||
|
||||
@@ -1104,8 +1104,6 @@ export const zhCN = {
|
||||
mermaidSource: "Mermaid 源码",
|
||||
mermaidShowSource: "显示 Mermaid 源码",
|
||||
mermaidHideSource: "隐藏 Mermaid 源码",
|
||||
mermaidUseStandardLayout: "切换到标准布局",
|
||||
mermaidUseOrthogonalLayout: "切换到正交布局",
|
||||
mermaidOpenViewer: "放大查看 Mermaid 图表",
|
||||
mermaidViewer: "Mermaid 图表查看器",
|
||||
mermaidZoomIn: "放大图表",
|
||||
|
||||
Reference in new issue
Block a user