@windrun-huaiin/third-ui 7.1.0 → 7.1.1

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.
@@ -9,6 +9,12 @@ var React = require('react');
9
9
 
10
10
  function _interopNamespaceDefaultOnly (e) { return Object.freeze({ __proto__: null, default: e }); }
11
11
 
12
+ function sanitizeFilename(name) {
13
+ return name
14
+ .replace(/[\/:*?"<>|]/g, '_')
15
+ .replace(/\s+/g, '_')
16
+ .slice(0, 120);
17
+ }
12
18
  function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview = true }) {
13
19
  const id = React.useId();
14
20
  const [svg, setSvg] = React.useState('');
@@ -95,6 +101,20 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
95
101
  isPanningRef.current = false;
96
102
  e.currentTarget.releasePointerCapture(e.pointerId);
97
103
  }, []);
104
+ const handleDownload = React.useCallback(() => {
105
+ if (!svg)
106
+ return;
107
+ const fileName = `${sanitizeFilename(title !== null && title !== void 0 ? title : 'mermaid')}.svg`;
108
+ const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
109
+ const url = URL.createObjectURL(blob);
110
+ const a = document.createElement('a');
111
+ a.href = url;
112
+ a.download = fileName;
113
+ document.body.appendChild(a);
114
+ a.click();
115
+ a.remove();
116
+ URL.revokeObjectURL(url);
117
+ }, [svg, title]);
98
118
  // prevent browser-level zoom (touchpad pinch/shortcut) from taking effect when the dialog is open
99
119
  React.useEffect(() => {
100
120
  if (!open)
@@ -154,7 +174,7 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
154
174
  window.scrollTo(0, scrollY);
155
175
  };
156
176
  }, [open]);
157
- 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 text-[#AC62FD]", 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 px-3 py-2 border-b border-neutral-200 dark:border-neutral-700", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-300", 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 items-center gap-0.5", children: [jsxRuntime.jsx("button", { "aria-label": "Zoom out", className: "flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px]", onClick: () => zoomBy(-0.5), children: "\uFF0D" }), jsxRuntime.jsxs("span", { className: "mx-0.5 text-[12px] w-12 text-center select-none", children: [Math.round(scale * 100), "%"] }), jsxRuntime.jsx("button", { "aria-label": "Zoom in", className: "flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px]", onClick: () => zoomBy(0.5), children: "\uFF0B" }), jsxRuntime.jsx("div", { className: "mx-1 h-4 w-px bg-neutral-300 dark:bg-neutral-700" }), jsxRuntime.jsx("button", { "aria-label": "Zoom 100%", className: "inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(1), children: "X1" }), jsxRuntime.jsx("button", { "aria-label": "Zoom 200%", className: "ml-1 inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(2), children: "X2" }), jsxRuntime.jsx("button", { "aria-label": "Zoom 300%", className: "ml-1 inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(3), children: "X3" }), jsxRuntime.jsx("button", { "aria-label": "Zoom 1000%", className: "ml-1 inline-flex h-6 min-w-10 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(10), children: "X10" }), jsxRuntime.jsx("button", { "aria-label": "Reset", className: "ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600", onClick: resetTransform, children: jsxRuntime.jsx(server.globalLucideIcons.RefreshCcw, { className: "h-3.5 w-3.5" }) }), jsxRuntime.jsx("button", { "aria-label": "Close", className: "ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600", 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 touch-none overscroll-contain", onWheel: onWheel, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, 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.jsx("div", { className: "pointer-events-none absolute bottom-2 right-3 rounded bg-black/40 px-2 py-1 text-xs text-white", children: "Drag to pan, hold Cmd/Ctrl + scroll to zoom" })] })] })] }))] }));
177
+ 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 text-[#AC62FD]", 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 px-3 py-2 border-b border-neutral-200 dark:border-neutral-700", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-300", 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 items-center gap-0.5", children: [jsxRuntime.jsx("button", { "aria-label": "Zoom out", className: "flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px]", onClick: () => zoomBy(-0.5), children: "\uFF0D" }), jsxRuntime.jsxs("span", { className: "mx-0.5 text-[12px] w-12 text-center select-none", children: [Math.round(scale * 100), "%"] }), jsxRuntime.jsx("button", { "aria-label": "Zoom in", className: "flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px]", onClick: () => zoomBy(0.5), children: "\uFF0B" }), jsxRuntime.jsx("div", { className: "mx-1 h-4 w-px bg-neutral-300 dark:bg-neutral-700" }), jsxRuntime.jsx("button", { "aria-label": "Zoom 100%", className: "inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(1), children: "X1" }), jsxRuntime.jsx("button", { "aria-label": "Zoom 200%", className: "ml-1 inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(2), children: "X2" }), jsxRuntime.jsx("button", { "aria-label": "Zoom 300%", className: "ml-1 inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(3), children: "X3" }), jsxRuntime.jsx("button", { "aria-label": "Zoom 1000%", className: "ml-1 inline-flex h-6 min-w-10 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(10), children: "X10" }), jsxRuntime.jsx("button", { "aria-label": "Reset", className: "ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600", onClick: resetTransform, children: jsxRuntime.jsx(server.globalLucideIcons.RefreshCcw, { className: "h-3.5 w-3.5" }) }), jsxRuntime.jsx("button", { "aria-label": "Download SVG", className: "ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600", onClick: handleDownload, children: jsxRuntime.jsx(server.globalLucideIcons.Download, { className: "h-3.5 w-3.5" }) }), jsxRuntime.jsx("button", { "aria-label": "Close", className: "ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600", 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 touch-none overscroll-contain", onWheel: onWheel, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, 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.jsx("div", { className: "pointer-events-none absolute bottom-2 right-3 rounded bg-black/40 px-2 py-1 text-xs text-white", children: "Drag to pan, hold Cmd/Ctrl + scroll to zoom" })] })] })] }))] }));
158
178
  }
159
179
  function addWatermarkToSvg(svg, watermark) {
160
180
  const watermarkText = `
@@ -5,6 +5,12 @@ import { globalLucideIcons } from '@windrun-huaiin/base-ui/components/server';
5
5
  import { useTheme } from 'next-themes';
6
6
  import { useId, useState, useRef, useEffect, useCallback } from 'react';
7
7
 
8
+ function sanitizeFilename(name) {
9
+ return name
10
+ .replace(/[\/:*?"<>|]/g, '_')
11
+ .replace(/\s+/g, '_')
12
+ .slice(0, 120);
13
+ }
8
14
  function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview = true }) {
9
15
  const id = useId();
10
16
  const [svg, setSvg] = useState('');
@@ -91,6 +97,20 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
91
97
  isPanningRef.current = false;
92
98
  e.currentTarget.releasePointerCapture(e.pointerId);
93
99
  }, []);
100
+ const handleDownload = useCallback(() => {
101
+ if (!svg)
102
+ return;
103
+ const fileName = `${sanitizeFilename(title !== null && title !== void 0 ? title : 'mermaid')}.svg`;
104
+ const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
105
+ const url = URL.createObjectURL(blob);
106
+ const a = document.createElement('a');
107
+ a.href = url;
108
+ a.download = fileName;
109
+ document.body.appendChild(a);
110
+ a.click();
111
+ a.remove();
112
+ URL.revokeObjectURL(url);
113
+ }, [svg, title]);
94
114
  // prevent browser-level zoom (touchpad pinch/shortcut) from taking effect when the dialog is open
95
115
  useEffect(() => {
96
116
  if (!open)
@@ -150,7 +170,7 @@ function Mermaid({ chart, title, watermarkEnabled, watermarkText, enablePreview
150
170
  window.scrollTo(0, scrollY);
151
171
  };
152
172
  }, [open]);
153
- 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 text-[#AC62FD]", 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 px-3 py-2 border-b border-neutral-200 dark:border-neutral-700", children: [jsxs("div", { className: "flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-300", 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 items-center gap-0.5", children: [jsx("button", { "aria-label": "Zoom out", className: "flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px]", onClick: () => zoomBy(-0.5), children: "\uFF0D" }), jsxs("span", { className: "mx-0.5 text-[12px] w-12 text-center select-none", children: [Math.round(scale * 100), "%"] }), jsx("button", { "aria-label": "Zoom in", className: "flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px]", onClick: () => zoomBy(0.5), children: "\uFF0B" }), jsx("div", { className: "mx-1 h-4 w-px bg-neutral-300 dark:bg-neutral-700" }), jsx("button", { "aria-label": "Zoom 100%", className: "inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(1), children: "X1" }), jsx("button", { "aria-label": "Zoom 200%", className: "ml-1 inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(2), children: "X2" }), jsx("button", { "aria-label": "Zoom 300%", className: "ml-1 inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(3), children: "X3" }), jsx("button", { "aria-label": "Zoom 1000%", className: "ml-1 inline-flex h-6 min-w-10 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(10), children: "X10" }), jsx("button", { "aria-label": "Reset", className: "ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600", onClick: resetTransform, children: jsx(globalLucideIcons.RefreshCcw, { className: "h-3.5 w-3.5" }) }), jsx("button", { "aria-label": "Close", className: "ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600", 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 touch-none overscroll-contain", onWheel: onWheel, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, 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 } }) }), jsx("div", { className: "pointer-events-none absolute bottom-2 right-3 rounded bg-black/40 px-2 py-1 text-xs text-white", children: "Drag to pan, hold Cmd/Ctrl + scroll to zoom" })] })] })] }))] }));
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 text-[#AC62FD]", 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 px-3 py-2 border-b border-neutral-200 dark:border-neutral-700", children: [jsxs("div", { className: "flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-300", 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 items-center gap-0.5", children: [jsx("button", { "aria-label": "Zoom out", className: "flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px]", onClick: () => zoomBy(-0.5), children: "\uFF0D" }), jsxs("span", { className: "mx-0.5 text-[12px] w-12 text-center select-none", children: [Math.round(scale * 100), "%"] }), jsx("button", { "aria-label": "Zoom in", className: "flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px]", onClick: () => zoomBy(0.5), children: "\uFF0B" }), jsx("div", { className: "mx-1 h-4 w-px bg-neutral-300 dark:bg-neutral-700" }), jsx("button", { "aria-label": "Zoom 100%", className: "inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(1), children: "X1" }), jsx("button", { "aria-label": "Zoom 200%", className: "ml-1 inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(2), children: "X2" }), jsx("button", { "aria-label": "Zoom 300%", className: "ml-1 inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(3), children: "X3" }), jsx("button", { "aria-label": "Zoom 1000%", className: "ml-1 inline-flex h-6 min-w-10 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]", onClick: () => setScale(10), children: "X10" }), jsx("button", { "aria-label": "Reset", className: "ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600", onClick: resetTransform, children: jsx(globalLucideIcons.RefreshCcw, { className: "h-3.5 w-3.5" }) }), jsx("button", { "aria-label": "Download SVG", className: "ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600", onClick: handleDownload, children: jsx(globalLucideIcons.Download, { className: "h-3.5 w-3.5" }) }), jsx("button", { "aria-label": "Close", className: "ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600", 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 touch-none overscroll-contain", onWheel: onWheel, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, 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 } }) }), jsx("div", { className: "pointer-events-none absolute bottom-2 right-3 rounded bg-black/40 px-2 py-1 text-xs text-white", children: "Drag to pan, hold Cmd/Ctrl + scroll to zoom" })] })] })] }))] }));
154
174
  }
155
175
  function addWatermarkToSvg(svg, watermark) {
156
176
  const watermarkText = `
@@ -55,7 +55,7 @@ function XButton(props) {
55
55
  // Split button
56
56
  const { mainButton, menuItems, loadingText, menuWidth = 'w-40', className = '', mainButtonClassName = '', dropdownButtonClassName = '' } = props;
57
57
  const isMainDisabled = mainButton.disabled || isLoading;
58
- // loadingText 优先级:props.loadingText > mainButton.text > 'Loading...'
58
+ // loadingText prioty:props.loadingText > mainButton.text > 'Loading...'
59
59
  const actualLoadingText = loadingText || ((_b = mainButton.text) === null || _b === void 0 ? void 0 : _b.trim()) || 'Loading...';
60
60
  return (jsxRuntime.jsxs("div", { className: `relative flex bg-neutral-200 dark:bg-neutral-800 rounded-full ${className}`, children: [jsxRuntime.jsx("button", { onClick: () => handleButtonClick(mainButton.onClick), disabled: isMainDisabled, className: `flex-1 ${baseButtonClass} rounded-l-full ${isMainDisabled ? disabledClass : ''} ${mainButtonClassName}`, onMouseDown: e => { if (e.button === 2)
61
61
  e.preventDefault(); }, style: { borderTopRightRadius: 0, borderBottomRightRadius: 0 }, children: isLoading ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(server.globalLucideIcons.Loader2, { className: "w-5 h-5 mr-1 animate-spin" }), jsxRuntime.jsx("span", { children: actualLoadingText })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [mainButton.icon, jsxRuntime.jsx("span", { children: mainButton.text })] })) }), jsxRuntime.jsx("span", { className: `flex items-center justify-center w-10 py-2 cursor-pointer transition hover:bg-neutral-300 dark:hover:bg-neutral-700 rounded-r-full ${dropdownButtonClassName}`, onClick: e => { e.stopPropagation(); setMenuOpen(v => !v); }, tabIndex: 0, style: { borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }, children: jsxRuntime.jsx(server.globalLucideIcons.ChevronDown, { className: "w-6 h-6" }) }), menuOpen && (jsxRuntime.jsx("div", { ref: menuRef, className: `absolute right-0 top-full ${menuWidth} bg-white dark:bg-neutral-800 text-neutral-800 dark:text-white text-sm rounded-xl shadow-lg z-50 border border-neutral-200 dark:border-neutral-700 overflow-hidden animate-fade-in`, children: menuItems.map((item, index) => (jsxRuntime.jsxs("button", { onClick: () => {
@@ -53,7 +53,7 @@ function XButton(props) {
53
53
  // Split button
54
54
  const { mainButton, menuItems, loadingText, menuWidth = 'w-40', className = '', mainButtonClassName = '', dropdownButtonClassName = '' } = props;
55
55
  const isMainDisabled = mainButton.disabled || isLoading;
56
- // loadingText 优先级:props.loadingText > mainButton.text > 'Loading...'
56
+ // loadingText prioty:props.loadingText > mainButton.text > 'Loading...'
57
57
  const actualLoadingText = loadingText || ((_b = mainButton.text) === null || _b === void 0 ? void 0 : _b.trim()) || 'Loading...';
58
58
  return (jsxs("div", { className: `relative flex bg-neutral-200 dark:bg-neutral-800 rounded-full ${className}`, children: [jsx("button", { onClick: () => handleButtonClick(mainButton.onClick), disabled: isMainDisabled, className: `flex-1 ${baseButtonClass} rounded-l-full ${isMainDisabled ? disabledClass : ''} ${mainButtonClassName}`, onMouseDown: e => { if (e.button === 2)
59
59
  e.preventDefault(); }, style: { borderTopRightRadius: 0, borderBottomRightRadius: 0 }, children: isLoading ? (jsxs(Fragment, { children: [jsx(globalLucideIcons.Loader2, { className: "w-5 h-5 mr-1 animate-spin" }), jsx("span", { children: actualLoadingText })] })) : (jsxs(Fragment, { children: [mainButton.icon, jsx("span", { children: mainButton.text })] })) }), jsx("span", { className: `flex items-center justify-center w-10 py-2 cursor-pointer transition hover:bg-neutral-300 dark:hover:bg-neutral-700 rounded-r-full ${dropdownButtonClassName}`, onClick: e => { e.stopPropagation(); setMenuOpen(v => !v); }, tabIndex: 0, style: { borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }, children: jsx(globalLucideIcons.ChevronDown, { className: "w-6 h-6" }) }), menuOpen && (jsx("div", { ref: menuRef, className: `absolute right-0 top-full ${menuWidth} bg-white dark:bg-neutral-800 text-neutral-800 dark:text-white text-sm rounded-xl shadow-lg z-50 border border-neutral-200 dark:border-neutral-700 overflow-hidden animate-fade-in`, children: menuItems.map((item, index) => (jsxs("button", { onClick: () => {
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var coseBase$1 = require('../../../../../_virtual/cose-base.js');
3
+ var coseBase$1 = require('../../../../../_virtual/cose-base2.js');
4
4
  var layoutBase = require('../../../layout-base@1.0.2/node_modules/layout-base/layout-base.js');
5
5
 
6
6
  var coseBase = coseBase$1.__module.exports;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var coseBase$1 = require('../../../../../_virtual/cose-base2.js');
3
+ var coseBase$1 = require('../../../../../_virtual/cose-base.js');
4
4
  var layoutBase = require('../../../layout-base@2.0.1/node_modules/layout-base/layout-base.js');
5
5
 
6
6
  var coseBase = coseBase$1.__module.exports;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var layoutBase$1 = require('../../../../../_virtual/layout-base.js');
3
+ var layoutBase$1 = require('../../../../../_virtual/layout-base2.js');
4
4
 
5
5
  var layoutBase = layoutBase$1.__module.exports;
6
6
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var layoutBase$1 = require('../../../../../_virtual/layout-base2.js');
3
+ var layoutBase$1 = require('../../../../../_virtual/layout-base.js');
4
4
 
5
5
  var layoutBase = layoutBase$1.__module.exports;
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/third-ui",
3
- "version": "7.1.0",
3
+ "version": "7.1.1",
4
4
  "description": "Third-party integrated UI components for windrun-huaiin projects",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -64,8 +64,8 @@
64
64
  "mermaid": "^11.6.0",
65
65
  "react-medium-image-zoom": "^5.2.14",
66
66
  "zod": "^3.22.4",
67
- "@windrun-huaiin/lib": "^7.1.0",
68
- "@windrun-huaiin/base-ui": "^8.1.0"
67
+ "@windrun-huaiin/lib": "^7.1.1",
68
+ "@windrun-huaiin/base-ui": "^8.1.1"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "19.1.0",
@@ -6,6 +6,13 @@ import type { MermaidConfig } from 'mermaid';
6
6
  import { useTheme } from 'next-themes';
7
7
  import { useCallback, useEffect, useId, useRef, useState } from 'react';
8
8
 
9
+ function sanitizeFilename(name: string) {
10
+ return name
11
+ .replace(/[\/:*?"<>|]/g, '_')
12
+ .replace(/\s+/g, '_')
13
+ .slice(0, 120);
14
+ }
15
+
9
16
  interface MermaidProps {
10
17
  chart: string;
11
18
  title?: string;
@@ -111,6 +118,20 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
111
118
  (e.currentTarget as HTMLDivElement).releasePointerCapture(e.pointerId);
112
119
  }, []);
113
120
 
121
+ const handleDownload = useCallback(() => {
122
+ if (!svg) return;
123
+ const fileName = `${sanitizeFilename(title ?? 'mermaid')}.svg`;
124
+ const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
125
+ const url = URL.createObjectURL(blob);
126
+ const a = document.createElement('a');
127
+ a.href = url;
128
+ a.download = fileName;
129
+ document.body.appendChild(a);
130
+ a.click();
131
+ a.remove();
132
+ URL.revokeObjectURL(url);
133
+ }, [svg, title]);
134
+
114
135
  // prevent browser-level zoom (touchpad pinch/shortcut) from taking effect when the dialog is open
115
136
  useEffect(() => {
116
137
  if (!open) return;
@@ -213,7 +234,7 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
213
234
  <div className="flex items-center gap-0.5">
214
235
  <button
215
236
  aria-label="Zoom out"
216
- className="flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px]"
237
+ className="flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400"
217
238
  onClick={() => zoomBy(-0.5)}
218
239
  >
219
240
 
@@ -221,7 +242,7 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
221
242
  <span className="mx-0.5 text-[12px] w-12 text-center select-none">{Math.round(scale * 100)}%</span>
222
243
  <button
223
244
  aria-label="Zoom in"
224
- className="flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px]"
245
+ className="flex h-6 w-6 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 text-[13px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400"
225
246
  onClick={() => zoomBy(0.5)}
226
247
  >
227
248
 
@@ -230,42 +251,49 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, enableP
230
251
  <div className="mx-1 h-4 w-px bg-neutral-300 dark:bg-neutral-700" />
231
252
  <button
232
253
  aria-label="Zoom 100%"
233
- className="inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]"
254
+ className="inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400"
234
255
  onClick={() => setScale(1)}
235
256
  >
236
257
  X1
237
258
  </button>
238
259
  <button
239
260
  aria-label="Zoom 200%"
240
- className="ml-1 inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]"
261
+ className="ml-1 inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400"
241
262
  onClick={() => setScale(2)}
242
263
  >
243
264
  X2
244
265
  </button>
245
266
  <button
246
267
  aria-label="Zoom 300%"
247
- className="ml-1 inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]"
268
+ className="ml-1 inline-flex h-6 min-w-8 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400"
248
269
  onClick={() => setScale(3)}
249
270
  >
250
271
  X3
251
272
  </button>
252
273
  <button
253
274
  aria-label="Zoom 1000%"
254
- className="ml-1 inline-flex h-6 min-w-10 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px]"
275
+ className="ml-1 inline-flex h-6 min-w-10 items-center justify-center rounded border border-neutral-300 dark:border-neutral-600 px-1.5 text-[12px] transition-colors hover:bg-neutral-100 active:bg-neutral-200 hover:border-neutral-400 active:border-neutral-500 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 dark:hover:border-neutral-500 dark:active:border-neutral-400"
255
276
  onClick={() => setScale(10)}
256
277
  >
257
278
  X10
258
279
  </button>
259
280
  <button
260
281
  aria-label="Reset"
261
- className="ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600"
282
+ className="ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600 transition-colors hover:bg-purple-50 active:bg-purple-100 dark:hover:bg-purple-500/20 dark:active:bg-purple-500/30"
262
283
  onClick={resetTransform}
263
284
  >
264
285
  <icons.RefreshCcw className="h-3.5 w-3.5" />
265
286
  </button>
287
+ <button
288
+ aria-label="Download SVG"
289
+ className="ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600 transition-colors hover:bg-purple-50 active:bg-purple-100 dark:hover:bg-purple-500/20 dark:active:bg-purple-500/30"
290
+ onClick={handleDownload}
291
+ >
292
+ <icons.Download className="h-3.5 w-3.5" />
293
+ </button>
266
294
  <button
267
295
  aria-label="Close"
268
- className="ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600"
296
+ className="ml-1 flex h-6 w-6 items-center justify-center rounded text-purple-500 hover:text-purple-600 transition-colors hover:bg-purple-50 active:bg-purple-100 dark:hover:bg-purple-500/20 dark:active:bg-purple-500/30"
269
297
  onClick={() => { setOpen(false); resetTransform(); }}
270
298
  >
271
299
  <icons.X className="h-3.5 w-3.5" />
@@ -116,7 +116,7 @@ export function XButton(props: xButtonProps) {
116
116
  // Split button
117
117
  const { mainButton, menuItems, loadingText, menuWidth = 'w-40', className = '', mainButtonClassName = '', dropdownButtonClassName = '' } = props
118
118
  const isMainDisabled = mainButton.disabled || isLoading
119
- // loadingText 优先级:props.loadingText > mainButton.text > 'Loading...'
119
+ // loadingText prioty:props.loadingText > mainButton.text > 'Loading...'
120
120
  const actualLoadingText = loadingText || mainButton.text?.trim() || 'Loading...'
121
121
 
122
122
  return (