@windrun-huaiin/third-ui 5.13.4 → 5.13.6
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/index.d.mts +5 -1
- package/dist/fuma/mdx/index.d.ts +5 -1
- package/dist/fuma/mdx/index.js +2717 -326
- package/dist/fuma/mdx/index.js.map +1 -1
- package/dist/fuma/mdx/index.mjs +2717 -326
- package/dist/fuma/mdx/index.mjs.map +1 -1
- package/dist/fuma/server.js +2471 -212
- package/dist/fuma/server.js.map +1 -1
- package/dist/fuma/server.mjs +2468 -209
- package/dist/fuma/server.mjs.map +1 -1
- package/dist/lib/server.d.mts +148 -0
- package/dist/lib/server.d.ts +148 -0
- package/package.json +2 -2
- package/src/fuma/mdx/mermaid.tsx +141 -3
package/src/fuma/mdx/mermaid.tsx
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogContent,
|
|
7
|
+
AlertDialogTitle,
|
|
8
|
+
} from '@base-ui/ui/alert-dialog';
|
|
4
9
|
import type { MermaidConfig } from 'mermaid';
|
|
5
10
|
import { useTheme } from 'next-themes';
|
|
6
|
-
import { useEffect, useId, useState } from 'react';
|
|
11
|
+
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
7
12
|
|
|
8
13
|
interface MermaidProps {
|
|
9
14
|
chart: string;
|
|
10
15
|
title?: string;
|
|
11
16
|
watermarkEnabled?: boolean;
|
|
12
17
|
watermarkText?: string;
|
|
18
|
+
/**
|
|
19
|
+
* enable preview dialog by clicking the chart, default is true
|
|
20
|
+
*/
|
|
21
|
+
enablePreview?: boolean;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
|
-
export function Mermaid({ chart, title, watermarkEnabled, watermarkText }: MermaidProps) {
|
|
24
|
+
export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview = true }: MermaidProps) {
|
|
16
25
|
const id = useId();
|
|
17
26
|
const [svg, setSvg] = useState('');
|
|
18
27
|
const { resolvedTheme } = useTheme();
|
|
28
|
+
const [open, setOpen] = useState(false);
|
|
29
|
+
// zoom & pan states for preview dialog
|
|
30
|
+
const [scale, setScale] = useState(1);
|
|
31
|
+
const [translate, setTranslate] = useState({ x: 0, y: 0 });
|
|
32
|
+
const isPanningRef = useRef(false);
|
|
33
|
+
const startPointRef = useRef({ x: 0, y: 0 });
|
|
34
|
+
const startTranslateRef = useRef({ x: 0, y: 0 });
|
|
19
35
|
|
|
20
36
|
useEffect(() => {
|
|
21
37
|
let isMounted = true;
|
|
@@ -52,10 +68,62 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText }: Merma
|
|
|
52
68
|
setSvg('');
|
|
53
69
|
};
|
|
54
70
|
}, [chart, id, resolvedTheme, watermarkEnabled, watermarkText]);
|
|
71
|
+
|
|
72
|
+
// helpers for preview zoom
|
|
73
|
+
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max);
|
|
74
|
+
const resetTransform = useCallback(() => {
|
|
75
|
+
setScale(1);
|
|
76
|
+
setTranslate({ x: 0, y: 0 });
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
const zoomBy = useCallback((delta: number) => {
|
|
80
|
+
// 基于中心缩放:保持缩放中心在画布中点,不引入位移
|
|
81
|
+
setScale((prev) => clamp(prev + delta, 0.25, 6));
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const onWheel = useCallback((e: React.WheelEvent<HTMLDivElement>) => {
|
|
85
|
+
// Cmd/Ctrl + 滚轮缩放(围绕中心点),否则上下平移
|
|
86
|
+
if (e.metaKey || e.ctrlKey) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
89
|
+
setScale((prev) => clamp(prev + delta, 0.25, 6));
|
|
90
|
+
} else {
|
|
91
|
+
setTranslate((prev) => ({ x: prev.x, y: prev.y - e.deltaY }));
|
|
92
|
+
}
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
const onPointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
|
96
|
+
isPanningRef.current = true;
|
|
97
|
+
startPointRef.current = { x: e.clientX, y: e.clientY };
|
|
98
|
+
startTranslateRef.current = { ...translate };
|
|
99
|
+
(e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
|
|
100
|
+
}, [translate]);
|
|
101
|
+
|
|
102
|
+
const onPointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
|
103
|
+
if (!isPanningRef.current) return;
|
|
104
|
+
const dx = e.clientX - startPointRef.current.x;
|
|
105
|
+
const dy = e.clientY - startPointRef.current.y;
|
|
106
|
+
setTranslate({ x: startTranslateRef.current.x + dx, y: startTranslateRef.current.y + dy });
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
const onPointerUp = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
|
110
|
+
isPanningRef.current = false;
|
|
111
|
+
(e.currentTarget as HTMLDivElement).releasePointerCapture(e.pointerId);
|
|
112
|
+
}, []);
|
|
55
113
|
|
|
56
114
|
return (
|
|
57
115
|
<div>
|
|
58
|
-
<div
|
|
116
|
+
<div
|
|
117
|
+
className={enablePreview ? 'group relative cursor-zoom-in' : undefined}
|
|
118
|
+
onClick={() => enablePreview && svg && setOpen(true)}
|
|
119
|
+
>
|
|
120
|
+
<div dangerouslySetInnerHTML={{ __html: svg }} />
|
|
121
|
+
{enablePreview && svg && (
|
|
122
|
+
<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">
|
|
123
|
+
Preview Chart
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
59
127
|
{title && (
|
|
60
128
|
<div
|
|
61
129
|
className="mt-2 flex items-center justify-center text-center text-[13px] font-italic text-[#AC62FD]"
|
|
@@ -64,6 +132,76 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText }: Merma
|
|
|
64
132
|
<span>{title}</span>
|
|
65
133
|
</div>
|
|
66
134
|
)}
|
|
135
|
+
|
|
136
|
+
{/* Preview Dialog */}
|
|
137
|
+
{enablePreview && (
|
|
138
|
+
<AlertDialog open={open} onOpenChange={(o) => { setOpen(o); if (!o) resetTransform(); }}>
|
|
139
|
+
<AlertDialogContent className="z-50 max-w-[95vw] w-[95vw] h-[88vh] p-0 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700">
|
|
140
|
+
<AlertDialogTitle className="sr-only">{title ?? 'Mermaid Preview'}</AlertDialogTitle>
|
|
141
|
+
{/* Top bar */}
|
|
142
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-neutral-200 dark:border-neutral-700">
|
|
143
|
+
<div className="flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-300">
|
|
144
|
+
<icons.Mmd className="h-4 w-4" />
|
|
145
|
+
<span className="truncate max-w-[50vw]">{title ?? 'Mermaid Preview'}</span>
|
|
146
|
+
</div>
|
|
147
|
+
<div className="flex items-center gap-0.5">
|
|
148
|
+
<button
|
|
149
|
+
aria-label="Zoom out"
|
|
150
|
+
className="flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px]"
|
|
151
|
+
onClick={() => zoomBy(-0.2)}
|
|
152
|
+
>
|
|
153
|
+
-
|
|
154
|
+
</button>
|
|
155
|
+
<span className="mx-0.5 text-[12px] w-12 text-center select-none">{Math.round(scale * 100)}%</span>
|
|
156
|
+
<button
|
|
157
|
+
aria-label="Zoom in"
|
|
158
|
+
className="flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px]"
|
|
159
|
+
onClick={() => zoomBy(0.2)}
|
|
160
|
+
>
|
|
161
|
+
+
|
|
162
|
+
</button>
|
|
163
|
+
<button
|
|
164
|
+
aria-label="Reset"
|
|
165
|
+
className="ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600"
|
|
166
|
+
onClick={resetTransform}
|
|
167
|
+
>
|
|
168
|
+
<icons.RefreshCcw className="h-3.5 w-3.5" />
|
|
169
|
+
</button>
|
|
170
|
+
<button
|
|
171
|
+
aria-label="Close"
|
|
172
|
+
className="ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600"
|
|
173
|
+
onClick={() => setOpen(false)}
|
|
174
|
+
>
|
|
175
|
+
<icons.X className="h-3.5 w-3.5" />
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{/* Canvas */}
|
|
181
|
+
<div
|
|
182
|
+
className="relative h-[calc(88vh-40px)] w-full overflow-hidden bg-white dark:bg-neutral-900"
|
|
183
|
+
onWheel={onWheel}
|
|
184
|
+
onPointerDown={onPointerDown}
|
|
185
|
+
onPointerMove={onPointerMove}
|
|
186
|
+
onPointerUp={onPointerUp}
|
|
187
|
+
>
|
|
188
|
+
<div
|
|
189
|
+
className="absolute left-1/2 top-1/2"
|
|
190
|
+
style={{ transform: `translate(-50%, -50%) translate(${translate.x}px, ${translate.y}px)` }}
|
|
191
|
+
>
|
|
192
|
+
<div
|
|
193
|
+
style={{ transform: `scale(${scale})`, transformOrigin: '50% 50%' }}
|
|
194
|
+
dangerouslySetInnerHTML={{ __html: svg }}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
{/* helper text */}
|
|
198
|
+
<div className="pointer-events-none absolute bottom-2 right-3 rounded bg-black/40 px-2 py-1 text-xs text-white">
|
|
199
|
+
Drag to pan, hold Cmd/Ctrl + scroll to zoom
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</AlertDialogContent>
|
|
203
|
+
</AlertDialog>
|
|
204
|
+
)}
|
|
67
205
|
</div>
|
|
68
206
|
);
|
|
69
207
|
}
|