@windrun-huaiin/third-ui 13.1.2 → 13.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fuma/mdx/mermaid.js +50 -9
- package/dist/fuma/mdx/mermaid.mjs +50 -9
- package/package.json +1 -1
- package/src/fuma/mdx/mermaid.tsx +87 -24
package/dist/fuma/mdx/mermaid.js
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
var tslib_es6 = require('../../node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.js');
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
var server = require('@windrun-huaiin/base-ui/components/server');
|
|
7
|
+
var utils = require('@windrun-huaiin/lib/utils');
|
|
7
8
|
var nextThemes = require('next-themes');
|
|
8
9
|
var React = require('react');
|
|
10
|
+
var lib = require('@windrun-huaiin/base-ui/lib');
|
|
9
11
|
|
|
10
12
|
function sanitizeFilename(name) {
|
|
11
13
|
return name
|
|
@@ -24,6 +26,9 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
|
|
|
24
26
|
const isPanningRef = React.useRef(false);
|
|
25
27
|
const startPointRef = React.useRef({ x: 0, y: 0 });
|
|
26
28
|
const startTranslateRef = React.useRef({ x: 0, y: 0 });
|
|
29
|
+
const activePointersRef = React.useRef(new Map());
|
|
30
|
+
const pinchStartDistanceRef = React.useRef(0);
|
|
31
|
+
const pinchStartScaleRef = React.useRef(1);
|
|
27
32
|
React.useEffect(() => {
|
|
28
33
|
let isMounted = true;
|
|
29
34
|
void renderChart();
|
|
@@ -42,7 +47,7 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
|
|
|
42
47
|
const { svg } = yield mermaid.render(id.replaceAll(':', ''), chart.replaceAll('\\n', '\n'));
|
|
43
48
|
let svgWithWatermark = svg;
|
|
44
49
|
if (watermarkEnabled && watermarkText) {
|
|
45
|
-
svgWithWatermark = addWatermarkToSvg(svg, watermarkText);
|
|
50
|
+
svgWithWatermark = addWatermarkToSvg(svg, watermarkText, lib.themeSvgIconColor);
|
|
46
51
|
}
|
|
47
52
|
if (isMounted)
|
|
48
53
|
setSvg(svgWithWatermark);
|
|
@@ -83,12 +88,32 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
|
|
|
83
88
|
}
|
|
84
89
|
}, []);
|
|
85
90
|
const onPointerDown = React.useCallback((e) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
activePointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
92
|
+
if (activePointersRef.current.size === 2) {
|
|
93
|
+
const [first, second] = Array.from(activePointersRef.current.values());
|
|
94
|
+
pinchStartDistanceRef.current = Math.hypot(second.x - first.x, second.y - first.y);
|
|
95
|
+
pinchStartScaleRef.current = scale;
|
|
96
|
+
isPanningRef.current = false;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
isPanningRef.current = true;
|
|
100
|
+
startPointRef.current = { x: e.clientX, y: e.clientY };
|
|
101
|
+
startTranslateRef.current = Object.assign({}, translate);
|
|
102
|
+
}
|
|
89
103
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
90
|
-
}, [translate]);
|
|
104
|
+
}, [scale, translate]);
|
|
91
105
|
const onPointerMove = React.useCallback((e) => {
|
|
106
|
+
if (!activePointersRef.current.has(e.pointerId))
|
|
107
|
+
return;
|
|
108
|
+
activePointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
109
|
+
if (activePointersRef.current.size === 2) {
|
|
110
|
+
const [first, second] = Array.from(activePointersRef.current.values());
|
|
111
|
+
const distance = Math.hypot(second.x - first.x, second.y - first.y);
|
|
112
|
+
if (pinchStartDistanceRef.current > 0) {
|
|
113
|
+
setScale(clamp((distance / pinchStartDistanceRef.current) * pinchStartScaleRef.current, 0.25, 10));
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
92
117
|
if (!isPanningRef.current)
|
|
93
118
|
return;
|
|
94
119
|
const dx = e.clientX - startPointRef.current.x;
|
|
@@ -96,8 +121,24 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
|
|
|
96
121
|
setTranslate({ x: startTranslateRef.current.x + dx, y: startTranslateRef.current.y + dy });
|
|
97
122
|
}, []);
|
|
98
123
|
const onPointerUp = React.useCallback((e) => {
|
|
124
|
+
activePointersRef.current.delete(e.pointerId);
|
|
125
|
+
isPanningRef.current = false;
|
|
126
|
+
if (activePointersRef.current.size === 1) {
|
|
127
|
+
const remaining = Array.from(activePointersRef.current.values())[0];
|
|
128
|
+
startPointRef.current = remaining;
|
|
129
|
+
startTranslateRef.current = Object.assign({}, translate);
|
|
130
|
+
isPanningRef.current = true;
|
|
131
|
+
}
|
|
132
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
133
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
134
|
+
}
|
|
135
|
+
}, [translate]);
|
|
136
|
+
const onPointerCancel = React.useCallback((e) => {
|
|
137
|
+
activePointersRef.current.delete(e.pointerId);
|
|
99
138
|
isPanningRef.current = false;
|
|
100
|
-
e.currentTarget.
|
|
139
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
140
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
141
|
+
}
|
|
101
142
|
}, []);
|
|
102
143
|
const handleDownload = React.useCallback(() => {
|
|
103
144
|
if (!svg)
|
|
@@ -172,9 +213,9 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
|
|
|
172
213
|
window.scrollTo(0, scrollY);
|
|
173
214
|
};
|
|
174
215
|
}, [open]);
|
|
175
|
-
return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: enablePreview ? 'group relative cursor-zoom-in' : undefined, onClick: () => enablePreview && svg && setOpen(true), children: [jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: svg } }), enablePreview && svg && (jsxRuntime.jsx("div", { className: "pointer-events-none absolute right-2 top-2 hidden rounded bg-black/50 px-2 py-0.5 text-[12px] text-white group-hover:block", children: "Preview Chart" }))] }), title && (jsxRuntime.jsxs("div", { className: "mt-2 flex items-center justify-center text-center text-[13px] font-italic
|
|
216
|
+
return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: enablePreview ? 'group relative cursor-zoom-in' : undefined, onClick: () => enablePreview && svg && setOpen(true), children: [jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: svg } }), enablePreview && svg && (jsxRuntime.jsx("div", { className: "pointer-events-none absolute right-2 top-2 hidden rounded bg-black/50 px-2 py-0.5 text-[12px] text-white group-hover:block", children: "Preview Chart" }))] }), title && (jsxRuntime.jsxs("div", { className: utils.cn("mt-2 flex items-center justify-center text-center text-[13px] font-italic", lib.themeIconColor), children: [jsxRuntime.jsx(server.globalLucideIcons.Mmd, { className: 'mr-1 h-4 w-4' }), jsxRuntime.jsx("span", { children: title })] })), enablePreview && open && (jsxRuntime.jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": typeof title === 'string' ? title : 'Mermaid Preview', className: "fixed inset-0 z-9999 flex items-center justify-center", children: [jsxRuntime.jsx("div", { className: "absolute inset-0 bg-black/60", onClick: () => { setOpen(false); resetTransform(); }, onWheel: (e) => { e.preventDefault(); e.stopPropagation(); }, onTouchMove: (e) => { e.preventDefault(); e.stopPropagation(); } }), jsxRuntime.jsxs("div", { className: "relative z-1 max-w-[95vw] w-[95vw] h-[88vh] p-0 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-md shadow-2xl overflow-hidden", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-2 border-b border-neutral-200 dark:border-neutral-700", children: [jsxRuntime.jsxs("div", { className: utils.cn("min-w-0 flex items-center gap-2 text-sm", lib.themeIconColor), children: [jsxRuntime.jsx(server.globalLucideIcons.Mmd, { className: "h-4 w-4" }), jsxRuntime.jsx("span", { className: "truncate max-w-[50vw]", children: title !== null && title !== void 0 ? title : 'Mermaid Preview' })] }), jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [jsxRuntime.jsx("button", { "aria-label": "Zoom out", className: "hidden h-6 w-6 items-center justify-center rounded border border-neutral-300 text-[13px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:flex", onClick: () => zoomBy(-0.5), children: "\uFF0D" }), jsxRuntime.jsxs("span", { className: "mx-0.5 w-12 text-center text-[12px] select-none", children: [Math.round(scale * 100), "%"] }), jsxRuntime.jsx("button", { "aria-label": "Zoom in", className: "hidden h-6 w-6 items-center justify-center rounded border border-neutral-300 text-[13px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:flex", onClick: () => zoomBy(0.5), children: "\uFF0B" }), jsxRuntime.jsx("div", { className: "mx-1 hidden h-4 w-px bg-neutral-300 dark:bg-neutral-700 sm:block" }), jsxRuntime.jsx("button", { "aria-label": "Zoom 100%", className: "hidden h-6 min-w-8 items-center justify-center rounded border border-neutral-300 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:inline-flex", onClick: () => setScale(1), children: "X1" }), jsxRuntime.jsx("button", { "aria-label": "Zoom 200%", className: "ml-1 hidden h-6 min-w-8 items-center justify-center rounded border border-neutral-300 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:inline-flex", onClick: () => setScale(2), children: "X2" }), jsxRuntime.jsx("button", { "aria-label": "Zoom 300%", className: "ml-1 hidden h-6 min-w-8 items-center justify-center rounded border border-neutral-300 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:inline-flex", onClick: () => setScale(3), children: "X3" }), jsxRuntime.jsx("button", { "aria-label": "Zoom 1000%", className: "ml-1 hidden h-6 min-w-10 items-center justify-center rounded border border-neutral-300 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:inline-flex", onClick: () => setScale(10), children: "X10" }), jsxRuntime.jsx("button", { "aria-label": "Reset", className: utils.cn("ml-1 flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600", lib.themeIconColor), onClick: resetTransform, children: jsxRuntime.jsx(server.globalLucideIcons.RefreshCcw, { className: "h-3.5 w-3.5" }) }), jsxRuntime.jsx("button", { "aria-label": "Download SVG", className: utils.cn("ml-1 flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600", lib.themeIconColor), onClick: handleDownload, children: jsxRuntime.jsx(server.globalLucideIcons.Download, { className: "h-3.5 w-3.5" }) }), jsxRuntime.jsx("button", { "aria-label": "Close", className: utils.cn("ml-1 flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600", lib.themeIconColor), onClick: () => { setOpen(false); resetTransform(); }, children: jsxRuntime.jsx(server.globalLucideIcons.X, { className: "h-3.5 w-3.5" }) })] })] }), jsxRuntime.jsxs("div", { className: "relative h-[calc(88vh-40px)] w-full overflow-hidden bg-white dark:bg-neutral-900 overscroll-contain touch-none", onWheel: onWheel, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerCancel, children: [jsxRuntime.jsx("div", { className: "absolute left-1/2 top-1/2", style: { transform: `translate(-50%, -50%) translate(${translate.x}px, ${translate.y}px)` }, children: jsxRuntime.jsx("div", { style: { transform: `scale(${scale})`, transformOrigin: '50% 50%' }, dangerouslySetInnerHTML: { __html: svg } }) }), jsxRuntime.jsxs("div", { className: "absolute inset-x-3 bottom-3 rounded-md bg-white/92 px-3 py-2 shadow-sm backdrop-blur sm:hidden dark:bg-neutral-900/92", children: [jsxRuntime.jsxs("label", { className: "mb-1 flex items-center justify-between text-[11px] text-neutral-600 dark:text-neutral-300", children: [jsxRuntime.jsx("span", { children: "Zoom" }), jsxRuntime.jsxs("span", { children: [Math.round(scale * 100), "%"] })] }), jsxRuntime.jsx("input", { "aria-label": "Zoom slider", className: "block w-full", type: "range", min: "25", max: "1000", step: "5", value: Math.round(scale * 100), style: { accentColor: lib.themeSvgIconColor }, onChange: (e) => setScale(clamp(Number(e.target.value) / 100, 0.25, 10)) })] }), jsxRuntime.jsx("div", { className: "pointer-events-none absolute bottom-2 right-3 hidden rounded bg-black/40 px-2 py-1 text-xs text-white sm:block", children: "Drag to pan, click button to zoom-out or zoom-in" }), jsxRuntime.jsx("div", { className: "pointer-events-none absolute left-3 top-3 rounded bg-black/40 px-2 py-1 text-[11px] text-white sm:hidden", children: "Drag to pan, pinch to zoom-out or zoom-in" })] })] })] }))] }));
|
|
176
217
|
}
|
|
177
|
-
function addWatermarkToSvg(svg, watermark) {
|
|
218
|
+
function addWatermarkToSvg(svg, watermark, watermarkColor) {
|
|
178
219
|
const watermarkText = `
|
|
179
220
|
<text
|
|
180
221
|
x="100%"
|
|
@@ -182,7 +223,7 @@ function addWatermarkToSvg(svg, watermark) {
|
|
|
182
223
|
text-anchor="end"
|
|
183
224
|
font-size="12"
|
|
184
225
|
font-style="italic"
|
|
185
|
-
fill="
|
|
226
|
+
fill="${watermarkColor}"
|
|
186
227
|
opacity="0.40"
|
|
187
228
|
class="pointer-events-none"
|
|
188
229
|
dx="-8"
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { __awaiter } from '../../node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.mjs';
|
|
3
3
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
4
|
import { globalLucideIcons } from '@windrun-huaiin/base-ui/components/server';
|
|
5
|
+
import { cn } from '@windrun-huaiin/lib/utils';
|
|
5
6
|
import { useTheme } from 'next-themes';
|
|
6
7
|
import { useId, useState, useRef, useEffect, useCallback } from 'react';
|
|
8
|
+
import { themeIconColor, themeSvgIconColor } from '@windrun-huaiin/base-ui/lib';
|
|
7
9
|
|
|
8
10
|
function sanitizeFilename(name) {
|
|
9
11
|
return name
|
|
@@ -22,6 +24,9 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
|
|
|
22
24
|
const isPanningRef = useRef(false);
|
|
23
25
|
const startPointRef = useRef({ x: 0, y: 0 });
|
|
24
26
|
const startTranslateRef = useRef({ x: 0, y: 0 });
|
|
27
|
+
const activePointersRef = useRef(new Map());
|
|
28
|
+
const pinchStartDistanceRef = useRef(0);
|
|
29
|
+
const pinchStartScaleRef = useRef(1);
|
|
25
30
|
useEffect(() => {
|
|
26
31
|
let isMounted = true;
|
|
27
32
|
void renderChart();
|
|
@@ -40,7 +45,7 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
|
|
|
40
45
|
const { svg } = yield mermaid.render(id.replaceAll(':', ''), chart.replaceAll('\\n', '\n'));
|
|
41
46
|
let svgWithWatermark = svg;
|
|
42
47
|
if (watermarkEnabled && watermarkText) {
|
|
43
|
-
svgWithWatermark = addWatermarkToSvg(svg, watermarkText);
|
|
48
|
+
svgWithWatermark = addWatermarkToSvg(svg, watermarkText, themeSvgIconColor);
|
|
44
49
|
}
|
|
45
50
|
if (isMounted)
|
|
46
51
|
setSvg(svgWithWatermark);
|
|
@@ -81,12 +86,32 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
|
|
|
81
86
|
}
|
|
82
87
|
}, []);
|
|
83
88
|
const onPointerDown = useCallback((e) => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
activePointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
90
|
+
if (activePointersRef.current.size === 2) {
|
|
91
|
+
const [first, second] = Array.from(activePointersRef.current.values());
|
|
92
|
+
pinchStartDistanceRef.current = Math.hypot(second.x - first.x, second.y - first.y);
|
|
93
|
+
pinchStartScaleRef.current = scale;
|
|
94
|
+
isPanningRef.current = false;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
isPanningRef.current = true;
|
|
98
|
+
startPointRef.current = { x: e.clientX, y: e.clientY };
|
|
99
|
+
startTranslateRef.current = Object.assign({}, translate);
|
|
100
|
+
}
|
|
87
101
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
88
|
-
}, [translate]);
|
|
102
|
+
}, [scale, translate]);
|
|
89
103
|
const onPointerMove = useCallback((e) => {
|
|
104
|
+
if (!activePointersRef.current.has(e.pointerId))
|
|
105
|
+
return;
|
|
106
|
+
activePointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
107
|
+
if (activePointersRef.current.size === 2) {
|
|
108
|
+
const [first, second] = Array.from(activePointersRef.current.values());
|
|
109
|
+
const distance = Math.hypot(second.x - first.x, second.y - first.y);
|
|
110
|
+
if (pinchStartDistanceRef.current > 0) {
|
|
111
|
+
setScale(clamp((distance / pinchStartDistanceRef.current) * pinchStartScaleRef.current, 0.25, 10));
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
90
115
|
if (!isPanningRef.current)
|
|
91
116
|
return;
|
|
92
117
|
const dx = e.clientX - startPointRef.current.x;
|
|
@@ -94,8 +119,24 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
|
|
|
94
119
|
setTranslate({ x: startTranslateRef.current.x + dx, y: startTranslateRef.current.y + dy });
|
|
95
120
|
}, []);
|
|
96
121
|
const onPointerUp = useCallback((e) => {
|
|
122
|
+
activePointersRef.current.delete(e.pointerId);
|
|
123
|
+
isPanningRef.current = false;
|
|
124
|
+
if (activePointersRef.current.size === 1) {
|
|
125
|
+
const remaining = Array.from(activePointersRef.current.values())[0];
|
|
126
|
+
startPointRef.current = remaining;
|
|
127
|
+
startTranslateRef.current = Object.assign({}, translate);
|
|
128
|
+
isPanningRef.current = true;
|
|
129
|
+
}
|
|
130
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
131
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
132
|
+
}
|
|
133
|
+
}, [translate]);
|
|
134
|
+
const onPointerCancel = useCallback((e) => {
|
|
135
|
+
activePointersRef.current.delete(e.pointerId);
|
|
97
136
|
isPanningRef.current = false;
|
|
98
|
-
e.currentTarget.
|
|
137
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
138
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
139
|
+
}
|
|
99
140
|
}, []);
|
|
100
141
|
const handleDownload = useCallback(() => {
|
|
101
142
|
if (!svg)
|
|
@@ -170,9 +211,9 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
|
|
|
170
211
|
window.scrollTo(0, scrollY);
|
|
171
212
|
};
|
|
172
213
|
}, [open]);
|
|
173
|
-
return (jsxs("div", { children: [jsxs("div", { className: enablePreview ? 'group relative cursor-zoom-in' : undefined, onClick: () => enablePreview && svg && setOpen(true), children: [jsx("div", { dangerouslySetInnerHTML: { __html: svg } }), enablePreview && svg && (jsx("div", { className: "pointer-events-none absolute right-2 top-2 hidden rounded bg-black/50 px-2 py-0.5 text-[12px] text-white group-hover:block", children: "Preview Chart" }))] }), title && (jsxs("div", { className: "mt-2 flex items-center justify-center text-center text-[13px] font-italic
|
|
214
|
+
return (jsxs("div", { children: [jsxs("div", { className: enablePreview ? 'group relative cursor-zoom-in' : undefined, onClick: () => enablePreview && svg && setOpen(true), children: [jsx("div", { dangerouslySetInnerHTML: { __html: svg } }), enablePreview && svg && (jsx("div", { className: "pointer-events-none absolute right-2 top-2 hidden rounded bg-black/50 px-2 py-0.5 text-[12px] text-white group-hover:block", children: "Preview Chart" }))] }), title && (jsxs("div", { className: cn("mt-2 flex items-center justify-center text-center text-[13px] font-italic", themeIconColor), children: [jsx(globalLucideIcons.Mmd, { className: 'mr-1 h-4 w-4' }), jsx("span", { children: title })] })), enablePreview && open && (jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": typeof title === 'string' ? title : 'Mermaid Preview', className: "fixed inset-0 z-9999 flex items-center justify-center", children: [jsx("div", { className: "absolute inset-0 bg-black/60", onClick: () => { setOpen(false); resetTransform(); }, onWheel: (e) => { e.preventDefault(); e.stopPropagation(); }, onTouchMove: (e) => { e.preventDefault(); e.stopPropagation(); } }), jsxs("div", { className: "relative z-1 max-w-[95vw] w-[95vw] h-[88vh] p-0 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-md shadow-2xl overflow-hidden", children: [jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-2 border-b border-neutral-200 dark:border-neutral-700", children: [jsxs("div", { className: cn("min-w-0 flex items-center gap-2 text-sm", themeIconColor), children: [jsx(globalLucideIcons.Mmd, { className: "h-4 w-4" }), jsx("span", { className: "truncate max-w-[50vw]", children: title !== null && title !== void 0 ? title : 'Mermaid Preview' })] }), jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [jsx("button", { "aria-label": "Zoom out", className: "hidden h-6 w-6 items-center justify-center rounded border border-neutral-300 text-[13px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:flex", onClick: () => zoomBy(-0.5), children: "\uFF0D" }), jsxs("span", { className: "mx-0.5 w-12 text-center text-[12px] select-none", children: [Math.round(scale * 100), "%"] }), jsx("button", { "aria-label": "Zoom in", className: "hidden h-6 w-6 items-center justify-center rounded border border-neutral-300 text-[13px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:flex", onClick: () => zoomBy(0.5), children: "\uFF0B" }), jsx("div", { className: "mx-1 hidden h-4 w-px bg-neutral-300 dark:bg-neutral-700 sm:block" }), jsx("button", { "aria-label": "Zoom 100%", className: "hidden h-6 min-w-8 items-center justify-center rounded border border-neutral-300 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:inline-flex", onClick: () => setScale(1), children: "X1" }), jsx("button", { "aria-label": "Zoom 200%", className: "ml-1 hidden h-6 min-w-8 items-center justify-center rounded border border-neutral-300 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:inline-flex", onClick: () => setScale(2), children: "X2" }), jsx("button", { "aria-label": "Zoom 300%", className: "ml-1 hidden h-6 min-w-8 items-center justify-center rounded border border-neutral-300 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:inline-flex", onClick: () => setScale(3), children: "X3" }), jsx("button", { "aria-label": "Zoom 1000%", className: "ml-1 hidden h-6 min-w-10 items-center justify-center rounded border border-neutral-300 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:inline-flex", onClick: () => setScale(10), children: "X10" }), jsx("button", { "aria-label": "Reset", className: cn("ml-1 flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600", themeIconColor), onClick: resetTransform, children: jsx(globalLucideIcons.RefreshCcw, { className: "h-3.5 w-3.5" }) }), jsx("button", { "aria-label": "Download SVG", className: cn("ml-1 flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600", themeIconColor), onClick: handleDownload, children: jsx(globalLucideIcons.Download, { className: "h-3.5 w-3.5" }) }), jsx("button", { "aria-label": "Close", className: cn("ml-1 flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600", themeIconColor), onClick: () => { setOpen(false); resetTransform(); }, children: jsx(globalLucideIcons.X, { className: "h-3.5 w-3.5" }) })] })] }), jsxs("div", { className: "relative h-[calc(88vh-40px)] w-full overflow-hidden bg-white dark:bg-neutral-900 overscroll-contain touch-none", onWheel: onWheel, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerCancel, children: [jsx("div", { className: "absolute left-1/2 top-1/2", style: { transform: `translate(-50%, -50%) translate(${translate.x}px, ${translate.y}px)` }, children: jsx("div", { style: { transform: `scale(${scale})`, transformOrigin: '50% 50%' }, dangerouslySetInnerHTML: { __html: svg } }) }), jsxs("div", { className: "absolute inset-x-3 bottom-3 rounded-md bg-white/92 px-3 py-2 shadow-sm backdrop-blur sm:hidden dark:bg-neutral-900/92", children: [jsxs("label", { className: "mb-1 flex items-center justify-between text-[11px] text-neutral-600 dark:text-neutral-300", children: [jsx("span", { children: "Zoom" }), jsxs("span", { children: [Math.round(scale * 100), "%"] })] }), jsx("input", { "aria-label": "Zoom slider", className: "block w-full", type: "range", min: "25", max: "1000", step: "5", value: Math.round(scale * 100), style: { accentColor: themeSvgIconColor }, onChange: (e) => setScale(clamp(Number(e.target.value) / 100, 0.25, 10)) })] }), jsx("div", { className: "pointer-events-none absolute bottom-2 right-3 hidden rounded bg-black/40 px-2 py-1 text-xs text-white sm:block", children: "Drag to pan, click button to zoom-out or zoom-in" }), jsx("div", { className: "pointer-events-none absolute left-3 top-3 rounded bg-black/40 px-2 py-1 text-[11px] text-white sm:hidden", children: "Drag to pan, pinch to zoom-out or zoom-in" })] })] })] }))] }));
|
|
174
215
|
}
|
|
175
|
-
function addWatermarkToSvg(svg, watermark) {
|
|
216
|
+
function addWatermarkToSvg(svg, watermark, watermarkColor) {
|
|
176
217
|
const watermarkText = `
|
|
177
218
|
<text
|
|
178
219
|
x="100%"
|
|
@@ -180,7 +221,7 @@ function addWatermarkToSvg(svg, watermark) {
|
|
|
180
221
|
text-anchor="end"
|
|
181
222
|
font-size="12"
|
|
182
223
|
font-style="italic"
|
|
183
|
-
fill="
|
|
224
|
+
fill="${watermarkColor}"
|
|
184
225
|
opacity="0.40"
|
|
185
226
|
class="pointer-events-none"
|
|
186
227
|
dx="-8"
|
package/package.json
CHANGED
package/src/fuma/mdx/mermaid.tsx
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import { globalLucideIcons as icons } from '@windrun-huaiin/base-ui/components/server';
|
|
4
4
|
// Attention: do not use external dialog library, avoid react context conflict when building third-party applications
|
|
5
5
|
import type { MermaidConfig } from 'mermaid';
|
|
6
|
+
import { cn } from '@windrun-huaiin/lib/utils';
|
|
6
7
|
import { useTheme } from 'next-themes';
|
|
7
8
|
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
9
|
+
import { themeIconColor, themeSvgIconColor } from '@windrun-huaiin/base-ui/lib';
|
|
8
10
|
|
|
9
11
|
function sanitizeFilename(name: string) {
|
|
10
12
|
return name
|
|
@@ -35,6 +37,9 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
|
|
|
35
37
|
const isPanningRef = useRef(false);
|
|
36
38
|
const startPointRef = useRef({ x: 0, y: 0 });
|
|
37
39
|
const startTranslateRef = useRef({ x: 0, y: 0 });
|
|
40
|
+
const activePointersRef = useRef(new Map<number, { x: number; y: number }>());
|
|
41
|
+
const pinchStartDistanceRef = useRef(0);
|
|
42
|
+
const pinchStartScaleRef = useRef(1);
|
|
38
43
|
|
|
39
44
|
useEffect(() => {
|
|
40
45
|
let isMounted = true;
|
|
@@ -59,7 +64,7 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
|
|
|
59
64
|
);
|
|
60
65
|
let svgWithWatermark = svg;
|
|
61
66
|
if (watermarkEnabled && watermarkText) {
|
|
62
|
-
svgWithWatermark = addWatermarkToSvg(svg, watermarkText);
|
|
67
|
+
svgWithWatermark = addWatermarkToSvg(svg, watermarkText, themeSvgIconColor);
|
|
63
68
|
}
|
|
64
69
|
if (isMounted) setSvg(svgWithWatermark);
|
|
65
70
|
} catch (error) {
|
|
@@ -100,13 +105,33 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
|
|
|
100
105
|
}, []);
|
|
101
106
|
|
|
102
107
|
const onPointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
|
108
|
+
activePointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
109
|
+
if (activePointersRef.current.size === 2) {
|
|
110
|
+
const [first, second] = Array.from(activePointersRef.current.values());
|
|
111
|
+
pinchStartDistanceRef.current = Math.hypot(second.x - first.x, second.y - first.y);
|
|
112
|
+
pinchStartScaleRef.current = scale;
|
|
113
|
+
isPanningRef.current = false;
|
|
114
|
+
} else {
|
|
103
115
|
isPanningRef.current = true;
|
|
104
116
|
startPointRef.current = { x: e.clientX, y: e.clientY };
|
|
105
117
|
startTranslateRef.current = { ...translate };
|
|
118
|
+
}
|
|
106
119
|
(e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
|
|
107
|
-
}, [translate]);
|
|
120
|
+
}, [scale, translate]);
|
|
108
121
|
|
|
109
122
|
const onPointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
|
123
|
+
if (!activePointersRef.current.has(e.pointerId)) return;
|
|
124
|
+
activePointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
125
|
+
|
|
126
|
+
if (activePointersRef.current.size === 2) {
|
|
127
|
+
const [first, second] = Array.from(activePointersRef.current.values());
|
|
128
|
+
const distance = Math.hypot(second.x - first.x, second.y - first.y);
|
|
129
|
+
if (pinchStartDistanceRef.current > 0) {
|
|
130
|
+
setScale(clamp((distance / pinchStartDistanceRef.current) * pinchStartScaleRef.current, 0.25, 10));
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
110
135
|
if (!isPanningRef.current) return;
|
|
111
136
|
const dx = e.clientX - startPointRef.current.x;
|
|
112
137
|
const dy = e.clientY - startPointRef.current.y;
|
|
@@ -114,8 +139,25 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
|
|
|
114
139
|
}, []);
|
|
115
140
|
|
|
116
141
|
const onPointerUp = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
|
142
|
+
activePointersRef.current.delete(e.pointerId);
|
|
117
143
|
isPanningRef.current = false;
|
|
118
|
-
(
|
|
144
|
+
if (activePointersRef.current.size === 1) {
|
|
145
|
+
const remaining = Array.from(activePointersRef.current.values())[0];
|
|
146
|
+
startPointRef.current = remaining;
|
|
147
|
+
startTranslateRef.current = { ...translate };
|
|
148
|
+
isPanningRef.current = true;
|
|
149
|
+
}
|
|
150
|
+
if ((e.currentTarget as HTMLDivElement).hasPointerCapture(e.pointerId)) {
|
|
151
|
+
(e.currentTarget as HTMLDivElement).releasePointerCapture(e.pointerId);
|
|
152
|
+
}
|
|
153
|
+
}, [translate]);
|
|
154
|
+
|
|
155
|
+
const onPointerCancel = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
|
156
|
+
activePointersRef.current.delete(e.pointerId);
|
|
157
|
+
isPanningRef.current = false;
|
|
158
|
+
if ((e.currentTarget as HTMLDivElement).hasPointerCapture(e.pointerId)) {
|
|
159
|
+
(e.currentTarget as HTMLDivElement).releasePointerCapture(e.pointerId);
|
|
160
|
+
}
|
|
119
161
|
}, []);
|
|
120
162
|
|
|
121
163
|
const handleDownload = useCallback(() => {
|
|
@@ -203,7 +245,7 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
|
|
|
203
245
|
</div>
|
|
204
246
|
{title && (
|
|
205
247
|
<div
|
|
206
|
-
className="mt-2 flex items-center justify-center text-center text-[13px] font-italic
|
|
248
|
+
className={cn("mt-2 flex items-center justify-center text-center text-[13px] font-italic", themeIconColor)}
|
|
207
249
|
>
|
|
208
250
|
<icons.Mmd className='mr-1 h-4 w-4' />
|
|
209
251
|
<span>{title}</span>
|
|
@@ -226,74 +268,74 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
|
|
|
226
268
|
/>
|
|
227
269
|
<div className="relative z-1 max-w-[95vw] w-[95vw] h-[88vh] p-0 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-md shadow-2xl overflow-hidden">
|
|
228
270
|
{/* Top bar */}
|
|
229
|
-
<div className="flex items-center justify-between px-3 py-2 border-b border-neutral-200 dark:border-neutral-700">
|
|
230
|
-
<div className="flex items-center gap-2 text-sm
|
|
271
|
+
<div className="flex items-center justify-between gap-3 px-3 py-2 border-b border-neutral-200 dark:border-neutral-700">
|
|
272
|
+
<div className={cn("min-w-0 flex items-center gap-2 text-sm", themeIconColor)}>
|
|
231
273
|
<icons.Mmd className="h-4 w-4" />
|
|
232
274
|
<span className="truncate max-w-[50vw]">{title ?? 'Mermaid Preview'}</span>
|
|
233
275
|
</div>
|
|
234
|
-
<div className="flex items-center gap-0.5">
|
|
276
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
235
277
|
<button
|
|
236
278
|
aria-label="Zoom out"
|
|
237
|
-
className="
|
|
279
|
+
className="hidden h-6 w-6 items-center justify-center rounded border border-neutral-300 text-[13px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:flex"
|
|
238
280
|
onClick={() => zoomBy(-0.5)}
|
|
239
281
|
>
|
|
240
282
|
-
|
|
241
283
|
</button>
|
|
242
|
-
<span className="mx-0.5
|
|
284
|
+
<span className="mx-0.5 w-12 text-center text-[12px] select-none">{Math.round(scale * 100)}%</span>
|
|
243
285
|
<button
|
|
244
286
|
aria-label="Zoom in"
|
|
245
|
-
className="
|
|
287
|
+
className="hidden h-6 w-6 items-center justify-center rounded border border-neutral-300 text-[13px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:flex"
|
|
246
288
|
onClick={() => zoomBy(0.5)}
|
|
247
289
|
>
|
|
248
290
|
+
|
|
249
291
|
</button>
|
|
250
292
|
{/* quick zoom shortcuts */}
|
|
251
|
-
<div className="mx-1 h-4 w-px bg-neutral-300 dark:bg-neutral-700" />
|
|
293
|
+
<div className="mx-1 hidden h-4 w-px bg-neutral-300 dark:bg-neutral-700 sm:block" />
|
|
252
294
|
<button
|
|
253
295
|
aria-label="Zoom 100%"
|
|
254
|
-
className="
|
|
296
|
+
className="hidden h-6 min-w-8 items-center justify-center rounded border border-neutral-300 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:inline-flex"
|
|
255
297
|
onClick={() => setScale(1)}
|
|
256
298
|
>
|
|
257
299
|
X1
|
|
258
300
|
</button>
|
|
259
301
|
<button
|
|
260
302
|
aria-label="Zoom 200%"
|
|
261
|
-
className="ml-1
|
|
303
|
+
className="ml-1 hidden h-6 min-w-8 items-center justify-center rounded border border-neutral-300 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:inline-flex"
|
|
262
304
|
onClick={() => setScale(2)}
|
|
263
305
|
>
|
|
264
306
|
X2
|
|
265
307
|
</button>
|
|
266
308
|
<button
|
|
267
309
|
aria-label="Zoom 300%"
|
|
268
|
-
className="ml-1
|
|
310
|
+
className="ml-1 hidden h-6 min-w-8 items-center justify-center rounded border border-neutral-300 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:inline-flex"
|
|
269
311
|
onClick={() => setScale(3)}
|
|
270
312
|
>
|
|
271
313
|
X3
|
|
272
314
|
</button>
|
|
273
315
|
<button
|
|
274
316
|
aria-label="Zoom 1000%"
|
|
275
|
-
className="ml-1
|
|
317
|
+
className="ml-1 hidden h-6 min-w-10 items-center justify-center rounded border border-neutral-300 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:border-neutral-600 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400 sm:inline-flex"
|
|
276
318
|
onClick={() => setScale(10)}
|
|
277
319
|
>
|
|
278
320
|
X10
|
|
279
321
|
</button>
|
|
280
322
|
<button
|
|
281
323
|
aria-label="Reset"
|
|
282
|
-
className="ml-1 flex h-6 w-6 items-center justify-center rounded
|
|
324
|
+
className={cn("ml-1 flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600", themeIconColor)}
|
|
283
325
|
onClick={resetTransform}
|
|
284
326
|
>
|
|
285
327
|
<icons.RefreshCcw className="h-3.5 w-3.5" />
|
|
286
328
|
</button>
|
|
287
329
|
<button
|
|
288
330
|
aria-label="Download SVG"
|
|
289
|
-
className="ml-1 flex h-6 w-6 items-center justify-center rounded
|
|
331
|
+
className={cn("ml-1 flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600", themeIconColor)}
|
|
290
332
|
onClick={handleDownload}
|
|
291
333
|
>
|
|
292
334
|
<icons.Download className="h-3.5 w-3.5" />
|
|
293
335
|
</button>
|
|
294
336
|
<button
|
|
295
337
|
aria-label="Close"
|
|
296
|
-
className="ml-1 flex h-6 w-6 items-center justify-center rounded
|
|
338
|
+
className={cn("ml-1 flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600", themeIconColor)}
|
|
297
339
|
onClick={() => { setOpen(false); resetTransform(); }}
|
|
298
340
|
>
|
|
299
341
|
<icons.X className="h-3.5 w-3.5" />
|
|
@@ -303,11 +345,12 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
|
|
|
303
345
|
|
|
304
346
|
{/* Canvas */}
|
|
305
347
|
<div
|
|
306
|
-
className="relative h-[calc(88vh-40px)] w-full overflow-hidden bg-white dark:bg-neutral-900 touch-none
|
|
348
|
+
className="relative h-[calc(88vh-40px)] w-full overflow-hidden bg-white dark:bg-neutral-900 overscroll-contain touch-none"
|
|
307
349
|
onWheel={onWheel}
|
|
308
350
|
onPointerDown={onPointerDown}
|
|
309
351
|
onPointerMove={onPointerMove}
|
|
310
352
|
onPointerUp={onPointerUp}
|
|
353
|
+
onPointerCancel={onPointerCancel}
|
|
311
354
|
>
|
|
312
355
|
<div
|
|
313
356
|
className="absolute left-1/2 top-1/2"
|
|
@@ -318,9 +361,29 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
|
|
|
318
361
|
dangerouslySetInnerHTML={{ __html: svg }}
|
|
319
362
|
/>
|
|
320
363
|
</div>
|
|
364
|
+
<div className="absolute inset-x-3 bottom-3 rounded-md bg-white/92 px-3 py-2 shadow-sm backdrop-blur sm:hidden dark:bg-neutral-900/92">
|
|
365
|
+
<label className="mb-1 flex items-center justify-between text-[11px] text-neutral-600 dark:text-neutral-300">
|
|
366
|
+
<span>Zoom</span>
|
|
367
|
+
<span>{Math.round(scale * 100)}%</span>
|
|
368
|
+
</label>
|
|
369
|
+
<input
|
|
370
|
+
aria-label="Zoom slider"
|
|
371
|
+
className="block w-full"
|
|
372
|
+
type="range"
|
|
373
|
+
min="25"
|
|
374
|
+
max="1000"
|
|
375
|
+
step="5"
|
|
376
|
+
value={Math.round(scale * 100)}
|
|
377
|
+
style={{ accentColor: themeSvgIconColor }}
|
|
378
|
+
onChange={(e) => setScale(clamp(Number(e.target.value) / 100, 0.25, 10))}
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
321
381
|
{/* helper text */}
|
|
322
|
-
<div className="pointer-events-none absolute bottom-2 right-3 rounded bg-black/40 px-2 py-1 text-xs text-white">
|
|
323
|
-
Drag to pan,
|
|
382
|
+
<div className="pointer-events-none absolute bottom-2 right-3 hidden rounded bg-black/40 px-2 py-1 text-xs text-white sm:block">
|
|
383
|
+
Drag to pan, click button to zoom-out or zoom-in
|
|
384
|
+
</div>
|
|
385
|
+
<div className="pointer-events-none absolute left-3 top-3 rounded bg-black/40 px-2 py-1 text-[11px] text-white sm:hidden">
|
|
386
|
+
Drag to pan, pinch to zoom-out or zoom-in
|
|
324
387
|
</div>
|
|
325
388
|
</div>
|
|
326
389
|
</div>
|
|
@@ -330,7 +393,7 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
|
|
|
330
393
|
);
|
|
331
394
|
}
|
|
332
395
|
|
|
333
|
-
function addWatermarkToSvg(svg: string, watermark: string) {
|
|
396
|
+
function addWatermarkToSvg(svg: string, watermark: string, watermarkColor: string) {
|
|
334
397
|
const watermarkText = `
|
|
335
398
|
<text
|
|
336
399
|
x="100%"
|
|
@@ -338,7 +401,7 @@ function addWatermarkToSvg(svg: string, watermark: string) {
|
|
|
338
401
|
text-anchor="end"
|
|
339
402
|
font-size="12"
|
|
340
403
|
font-style="italic"
|
|
341
|
-
fill="
|
|
404
|
+
fill="${watermarkColor}"
|
|
342
405
|
opacity="0.40"
|
|
343
406
|
class="pointer-events-none"
|
|
344
407
|
dx="-8"
|
|
@@ -346,4 +409,4 @@ function addWatermarkToSvg(svg: string, watermark: string) {
|
|
|
346
409
|
>${watermark}</text>
|
|
347
410
|
`;
|
|
348
411
|
return svg.replace('</svg>', `${watermarkText}</svg>`);
|
|
349
|
-
}
|
|
412
|
+
}
|