@vllnt/ui 0.1.0
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/LICENSE +21 -0
- package/README.md +312 -0
- package/dist/chunk-XRV5RSYH.js +568 -0
- package/dist/flow-diagram-N3EHM6VB.js +1 -0
- package/dist/index.d.ts +1835 -0
- package/dist/index.js +8551 -0
- package/dist/tailwind-preset.d.ts +5 -0
- package/dist/tailwind-preset.js +87 -0
- package/package.json +143 -0
- package/styles.css +92 -0
- package/themes/default.css +44 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import '@xyflow/react/dist/style.css';
|
|
2
|
+
import { memo, useEffect, useCallback, Component, useState } from 'react';
|
|
3
|
+
import { ReactFlow, Background, BackgroundVariant, ReactFlowProvider, useReactFlow, getNodesBounds, getViewportForBounds } from '@xyflow/react';
|
|
4
|
+
import { clsx } from 'clsx';
|
|
5
|
+
import { twMerge } from 'tailwind-merge';
|
|
6
|
+
import { useTheme } from 'next-themes';
|
|
7
|
+
import { Plus, Minus, Move, Maximize2, X, Copy, Check, Loader2, AlertTriangle, RefreshCw } from 'lucide-react';
|
|
8
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
9
|
+
import { createPortal } from 'react-dom';
|
|
10
|
+
import { toPng } from 'html-to-image';
|
|
11
|
+
|
|
12
|
+
// src/components/flow-diagram/flow-diagram.tsx
|
|
13
|
+
function cn(...inputs) {
|
|
14
|
+
return twMerge(clsx(inputs));
|
|
15
|
+
}
|
|
16
|
+
var BUTTON_CLASS = "flex h-8 w-8 items-center justify-center rounded hover:bg-muted transition-colors disabled:opacity-50 disabled:cursor-not-allowed";
|
|
17
|
+
function ControlButton({
|
|
18
|
+
disabled,
|
|
19
|
+
icon,
|
|
20
|
+
label,
|
|
21
|
+
onClick,
|
|
22
|
+
title
|
|
23
|
+
}) {
|
|
24
|
+
return /* @__PURE__ */ jsx(
|
|
25
|
+
"button",
|
|
26
|
+
{
|
|
27
|
+
"aria-label": label,
|
|
28
|
+
className: BUTTON_CLASS,
|
|
29
|
+
disabled,
|
|
30
|
+
onClick,
|
|
31
|
+
title,
|
|
32
|
+
type: "button",
|
|
33
|
+
children: icon
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
function getCopyIcon(status) {
|
|
38
|
+
switch (status) {
|
|
39
|
+
case "copying":
|
|
40
|
+
return /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" });
|
|
41
|
+
case "success":
|
|
42
|
+
return /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 text-green-500" });
|
|
43
|
+
case "error":
|
|
44
|
+
return /* @__PURE__ */ jsx(X, { className: "h-4 w-4 text-destructive" });
|
|
45
|
+
case "idle":
|
|
46
|
+
case void 0:
|
|
47
|
+
return /* @__PURE__ */ jsx(Copy, { className: "h-4 w-4" });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function getCopyTitle(status) {
|
|
51
|
+
switch (status) {
|
|
52
|
+
case "copying":
|
|
53
|
+
return "Copying...";
|
|
54
|
+
case "success":
|
|
55
|
+
return "Copied!";
|
|
56
|
+
case "error":
|
|
57
|
+
return "Copy failed";
|
|
58
|
+
case "idle":
|
|
59
|
+
case void 0:
|
|
60
|
+
return "Copy as image";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
var FlowControls = memo(function FlowControls2({
|
|
64
|
+
className,
|
|
65
|
+
copyStatus,
|
|
66
|
+
onCopy,
|
|
67
|
+
onFitView,
|
|
68
|
+
onFullscreen,
|
|
69
|
+
onZoomIn,
|
|
70
|
+
onZoomOut,
|
|
71
|
+
showCopy = false,
|
|
72
|
+
showFullscreen = false
|
|
73
|
+
}) {
|
|
74
|
+
return /* @__PURE__ */ jsxs(
|
|
75
|
+
"div",
|
|
76
|
+
{
|
|
77
|
+
className: cn(
|
|
78
|
+
"absolute bottom-4 left-4 z-10 flex flex-col gap-1 rounded-md border border-border bg-background/90 p-1 backdrop-blur-sm",
|
|
79
|
+
className
|
|
80
|
+
),
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsx(
|
|
83
|
+
ControlButton,
|
|
84
|
+
{
|
|
85
|
+
icon: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
86
|
+
label: "Zoom in",
|
|
87
|
+
onClick: onZoomIn,
|
|
88
|
+
title: "Zoom in"
|
|
89
|
+
}
|
|
90
|
+
),
|
|
91
|
+
/* @__PURE__ */ jsx(
|
|
92
|
+
ControlButton,
|
|
93
|
+
{
|
|
94
|
+
icon: /* @__PURE__ */ jsx(Minus, { className: "h-4 w-4" }),
|
|
95
|
+
label: "Zoom out",
|
|
96
|
+
onClick: onZoomOut,
|
|
97
|
+
title: "Zoom out"
|
|
98
|
+
}
|
|
99
|
+
),
|
|
100
|
+
/* @__PURE__ */ jsx("div", { className: "h-px bg-border" }),
|
|
101
|
+
/* @__PURE__ */ jsx(
|
|
102
|
+
ControlButton,
|
|
103
|
+
{
|
|
104
|
+
icon: /* @__PURE__ */ jsx(Move, { className: "h-4 w-4" }),
|
|
105
|
+
label: "Fit view",
|
|
106
|
+
onClick: onFitView,
|
|
107
|
+
title: "Fit view"
|
|
108
|
+
}
|
|
109
|
+
),
|
|
110
|
+
showCopy && onCopy ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
111
|
+
/* @__PURE__ */ jsx("div", { className: "h-px bg-border" }),
|
|
112
|
+
/* @__PURE__ */ jsx(
|
|
113
|
+
ControlButton,
|
|
114
|
+
{
|
|
115
|
+
disabled: copyStatus === "copying",
|
|
116
|
+
icon: getCopyIcon(copyStatus),
|
|
117
|
+
label: getCopyTitle(copyStatus),
|
|
118
|
+
onClick: onCopy,
|
|
119
|
+
title: getCopyTitle(copyStatus)
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
] }) : null,
|
|
123
|
+
showFullscreen && onFullscreen ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
124
|
+
/* @__PURE__ */ jsx("div", { className: "h-px bg-border" }),
|
|
125
|
+
/* @__PURE__ */ jsx(
|
|
126
|
+
ControlButton,
|
|
127
|
+
{
|
|
128
|
+
icon: /* @__PURE__ */ jsx(Maximize2, { className: "h-4 w-4" }),
|
|
129
|
+
label: "Fullscreen",
|
|
130
|
+
onClick: onFullscreen,
|
|
131
|
+
title: "Toggle fullscreen"
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
] }) : null
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
var REACT_FLOW_CLASS = [
|
|
140
|
+
// Node container styling — no !important on bg/color so inline styles take precedence
|
|
141
|
+
"[&_.react-flow__node]:rounded-md",
|
|
142
|
+
"[&_.react-flow__node]:border",
|
|
143
|
+
"[&_.react-flow__node]:border-border",
|
|
144
|
+
"[&_.react-flow__node]:bg-card",
|
|
145
|
+
"[&_.react-flow__node]:px-4",
|
|
146
|
+
"[&_.react-flow__node]:py-2",
|
|
147
|
+
"[&_.react-flow__node]:shadow-sm",
|
|
148
|
+
// Node text styling — defaults, overridable by inline style.color
|
|
149
|
+
"[&_.react-flow__node]:text-sm",
|
|
150
|
+
"[&_.react-flow__node]:text-card-foreground",
|
|
151
|
+
"[&_.react-flow__node]:font-medium",
|
|
152
|
+
// Edge styling
|
|
153
|
+
"[&_.react-flow__edge-path]:!stroke-muted-foreground",
|
|
154
|
+
"[&_.react-flow__edge-path]:!stroke-2",
|
|
155
|
+
// Arrow marker styling
|
|
156
|
+
"[&_.react-flow__arrowhead_polyline]:!fill-muted-foreground",
|
|
157
|
+
"[&_.react-flow__arrowhead_polyline]:!stroke-muted-foreground"
|
|
158
|
+
].join(" ");
|
|
159
|
+
var FlowCanvas = memo(function FlowCanvas2({
|
|
160
|
+
allowCopy,
|
|
161
|
+
allowFullscreen,
|
|
162
|
+
className,
|
|
163
|
+
copyStatus,
|
|
164
|
+
edges,
|
|
165
|
+
fitView,
|
|
166
|
+
fitViewOptions,
|
|
167
|
+
height,
|
|
168
|
+
isFullscreen,
|
|
169
|
+
nodes,
|
|
170
|
+
onCopy,
|
|
171
|
+
onFitView,
|
|
172
|
+
onFullscreen,
|
|
173
|
+
onNodeClick,
|
|
174
|
+
onZoomIn,
|
|
175
|
+
onZoomOut,
|
|
176
|
+
showControls,
|
|
177
|
+
title
|
|
178
|
+
}) {
|
|
179
|
+
const { resolvedTheme } = useTheme();
|
|
180
|
+
const colorMode = resolvedTheme === "dark" ? "dark" : "light";
|
|
181
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("relative w-full", className), children: [
|
|
182
|
+
title ? /* @__PURE__ */ jsx("div", { className: "mb-2 text-sm font-medium text-foreground", children: title }) : null,
|
|
183
|
+
/* @__PURE__ */ jsx(
|
|
184
|
+
"div",
|
|
185
|
+
{
|
|
186
|
+
className: "relative w-full rounded-lg border border-border bg-background overflow-hidden",
|
|
187
|
+
style: { height: isFullscreen ? "100%" : height },
|
|
188
|
+
children: /* @__PURE__ */ jsxs(
|
|
189
|
+
ReactFlow,
|
|
190
|
+
{
|
|
191
|
+
className: REACT_FLOW_CLASS,
|
|
192
|
+
colorMode,
|
|
193
|
+
edges,
|
|
194
|
+
elementsSelectable: false,
|
|
195
|
+
fitView,
|
|
196
|
+
fitViewOptions: fitViewOptions ?? { padding: 0.2 },
|
|
197
|
+
nodes,
|
|
198
|
+
nodesConnectable: false,
|
|
199
|
+
nodesDraggable: false,
|
|
200
|
+
onNodeClick,
|
|
201
|
+
panOnScroll: true,
|
|
202
|
+
proOptions: { hideAttribution: true },
|
|
203
|
+
zoomOnScroll: true,
|
|
204
|
+
children: [
|
|
205
|
+
/* @__PURE__ */ jsx(
|
|
206
|
+
Background,
|
|
207
|
+
{
|
|
208
|
+
className: "!bg-background [&>pattern>circle]:fill-muted-foreground/20",
|
|
209
|
+
gap: 16,
|
|
210
|
+
size: 1,
|
|
211
|
+
variant: BackgroundVariant.Dots
|
|
212
|
+
}
|
|
213
|
+
),
|
|
214
|
+
showControls ? /* @__PURE__ */ jsx(
|
|
215
|
+
FlowControls,
|
|
216
|
+
{
|
|
217
|
+
copyStatus,
|
|
218
|
+
onCopy,
|
|
219
|
+
onFitView,
|
|
220
|
+
onFullscreen,
|
|
221
|
+
onZoomIn,
|
|
222
|
+
onZoomOut,
|
|
223
|
+
showCopy: allowCopy,
|
|
224
|
+
showFullscreen: allowFullscreen
|
|
225
|
+
}
|
|
226
|
+
) : null
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
] });
|
|
233
|
+
});
|
|
234
|
+
var FlowErrorBoundary = class extends Component {
|
|
235
|
+
constructor(props) {
|
|
236
|
+
super(props);
|
|
237
|
+
this.handleRetry = () => {
|
|
238
|
+
this.setState({ error: null, hasError: false });
|
|
239
|
+
};
|
|
240
|
+
this.state = { error: null, hasError: false };
|
|
241
|
+
}
|
|
242
|
+
static getDerivedStateFromError(error) {
|
|
243
|
+
return { error, hasError: true };
|
|
244
|
+
}
|
|
245
|
+
componentDidCatch(error, errorInfo) {
|
|
246
|
+
console.error("[FlowDiagram] Error caught by boundary:", error, errorInfo);
|
|
247
|
+
this.props.onError?.(error, errorInfo);
|
|
248
|
+
}
|
|
249
|
+
render() {
|
|
250
|
+
const { children, className, fallback, height = 400 } = this.props;
|
|
251
|
+
const { error, hasError } = this.state;
|
|
252
|
+
if (hasError) {
|
|
253
|
+
if (fallback) {
|
|
254
|
+
return fallback;
|
|
255
|
+
}
|
|
256
|
+
return /* @__PURE__ */ jsxs(
|
|
257
|
+
"div",
|
|
258
|
+
{
|
|
259
|
+
className: cn(
|
|
260
|
+
"flex flex-col items-center justify-center gap-4 rounded-lg border border-border bg-muted/50 p-8 text-center",
|
|
261
|
+
className
|
|
262
|
+
),
|
|
263
|
+
style: { height },
|
|
264
|
+
children: [
|
|
265
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10", children: /* @__PURE__ */ jsx(AlertTriangle, { className: "h-6 w-6 text-destructive" }) }),
|
|
266
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
267
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-medium text-foreground", children: "Failed to render diagram" }),
|
|
268
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: error?.message ?? "An unexpected error occurred while rendering the flow diagram." })
|
|
269
|
+
] }),
|
|
270
|
+
/* @__PURE__ */ jsxs(
|
|
271
|
+
"button",
|
|
272
|
+
{
|
|
273
|
+
className: "inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90",
|
|
274
|
+
onClick: this.handleRetry,
|
|
275
|
+
type: "button",
|
|
276
|
+
children: [
|
|
277
|
+
/* @__PURE__ */ jsx(RefreshCw, { className: "h-4 w-4" }),
|
|
278
|
+
"Try again"
|
|
279
|
+
]
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return children;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var FlowFullscreen = memo(function FlowFullscreen2({
|
|
290
|
+
children,
|
|
291
|
+
isOpen,
|
|
292
|
+
onClose
|
|
293
|
+
}) {
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (!isOpen) return;
|
|
296
|
+
const handleKeyDown = (e) => {
|
|
297
|
+
if (e.key === "Escape") {
|
|
298
|
+
onClose();
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
302
|
+
return () => {
|
|
303
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
304
|
+
};
|
|
305
|
+
}, [isOpen, onClose]);
|
|
306
|
+
if (!isOpen) return null;
|
|
307
|
+
if (typeof document === "undefined") return null;
|
|
308
|
+
return createPortal(
|
|
309
|
+
/* @__PURE__ */ jsxs(
|
|
310
|
+
"div",
|
|
311
|
+
{
|
|
312
|
+
"aria-label": "Flow diagram fullscreen view",
|
|
313
|
+
"aria-modal": "true",
|
|
314
|
+
className: cn(
|
|
315
|
+
"fixed inset-0 z-[9999] flex flex-col bg-background",
|
|
316
|
+
"animate-in fade-in duration-200"
|
|
317
|
+
),
|
|
318
|
+
role: "dialog",
|
|
319
|
+
children: [
|
|
320
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-12 items-center justify-end border-b border-border px-4", children: /* @__PURE__ */ jsx(
|
|
321
|
+
"button",
|
|
322
|
+
{
|
|
323
|
+
"aria-label": "Close fullscreen",
|
|
324
|
+
className: "flex h-8 w-8 items-center justify-center rounded hover:bg-muted transition-colors",
|
|
325
|
+
onClick: onClose,
|
|
326
|
+
title: "Close fullscreen (Esc)",
|
|
327
|
+
type: "button",
|
|
328
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-5 w-5" })
|
|
329
|
+
}
|
|
330
|
+
) }),
|
|
331
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-hidden", children })
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
),
|
|
335
|
+
document.body
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
var MIN_ZOOM = 0.1;
|
|
339
|
+
var MAX_ZOOM = 2;
|
|
340
|
+
var ZOOM_STEP = 0.2;
|
|
341
|
+
function useZoomControls(reactFlow) {
|
|
342
|
+
const [zoom, setZoom] = useState(1);
|
|
343
|
+
const zoomIn = useCallback(() => {
|
|
344
|
+
const newZoom = Math.min(zoom + ZOOM_STEP, MAX_ZOOM);
|
|
345
|
+
setZoom(newZoom);
|
|
346
|
+
void reactFlow.zoomTo(newZoom, { duration: 200 });
|
|
347
|
+
}, [zoom, reactFlow]);
|
|
348
|
+
const zoomOut = useCallback(() => {
|
|
349
|
+
const newZoom = Math.max(zoom - ZOOM_STEP, MIN_ZOOM);
|
|
350
|
+
setZoom(newZoom);
|
|
351
|
+
void reactFlow.zoomTo(newZoom, { duration: 200 });
|
|
352
|
+
}, [zoom, reactFlow]);
|
|
353
|
+
const fitView = useCallback(() => {
|
|
354
|
+
void reactFlow.fitView({ duration: 200, padding: 0.2 });
|
|
355
|
+
}, [reactFlow]);
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
const viewport = reactFlow.getViewport();
|
|
358
|
+
setZoom(viewport.zoom);
|
|
359
|
+
}, [reactFlow]);
|
|
360
|
+
return { fitView, zoom, zoomIn, zoomOut };
|
|
361
|
+
}
|
|
362
|
+
function useFullscreenState(allowFullscreen) {
|
|
363
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
364
|
+
const toggleFullscreen = useCallback(() => {
|
|
365
|
+
if (!allowFullscreen) return;
|
|
366
|
+
setIsFullscreen((previous) => !previous);
|
|
367
|
+
}, [allowFullscreen]);
|
|
368
|
+
const closeFullscreen = useCallback(() => {
|
|
369
|
+
setIsFullscreen(false);
|
|
370
|
+
}, []);
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
if (!isFullscreen) return;
|
|
373
|
+
const handleKeyDown = (e) => {
|
|
374
|
+
if (e.key === "Escape") {
|
|
375
|
+
closeFullscreen();
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
379
|
+
return () => {
|
|
380
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
381
|
+
};
|
|
382
|
+
}, [isFullscreen, closeFullscreen]);
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
document.body.style.overflow = isFullscreen ? "hidden" : "";
|
|
385
|
+
return () => {
|
|
386
|
+
document.body.style.overflow = "";
|
|
387
|
+
};
|
|
388
|
+
}, [isFullscreen]);
|
|
389
|
+
return { closeFullscreen, isFullscreen, toggleFullscreen };
|
|
390
|
+
}
|
|
391
|
+
var IMAGE_WIDTH = 1024;
|
|
392
|
+
var IMAGE_HEIGHT = 768;
|
|
393
|
+
var COPY_SUCCESS_DURATION = 2e3;
|
|
394
|
+
async function captureFlowImage(reactFlow) {
|
|
395
|
+
const nodes = reactFlow.getNodes();
|
|
396
|
+
if (nodes.length === 0) {
|
|
397
|
+
throw new Error("Cannot copy: no nodes in diagram");
|
|
398
|
+
}
|
|
399
|
+
const nodesBounds = getNodesBounds(nodes);
|
|
400
|
+
const viewport = getViewportForBounds(
|
|
401
|
+
nodesBounds,
|
|
402
|
+
IMAGE_WIDTH,
|
|
403
|
+
IMAGE_HEIGHT,
|
|
404
|
+
0.5,
|
|
405
|
+
2,
|
|
406
|
+
0.2
|
|
407
|
+
);
|
|
408
|
+
const flowElement = document.querySelector(".react-flow__viewport");
|
|
409
|
+
if (!flowElement || !(flowElement instanceof HTMLElement)) {
|
|
410
|
+
throw new Error("Cannot copy: flow viewport element not found");
|
|
411
|
+
}
|
|
412
|
+
const dataUrl = await toPng(flowElement, {
|
|
413
|
+
backgroundColor: "white",
|
|
414
|
+
height: IMAGE_HEIGHT,
|
|
415
|
+
style: {
|
|
416
|
+
height: String(IMAGE_HEIGHT),
|
|
417
|
+
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
|
|
418
|
+
width: String(IMAGE_WIDTH)
|
|
419
|
+
},
|
|
420
|
+
width: IMAGE_WIDTH
|
|
421
|
+
});
|
|
422
|
+
const response = await fetch(dataUrl);
|
|
423
|
+
if (!response.ok) {
|
|
424
|
+
throw new Error("Failed to fetch image data");
|
|
425
|
+
}
|
|
426
|
+
return response.blob();
|
|
427
|
+
}
|
|
428
|
+
function useCopyToClipboard(reactFlow) {
|
|
429
|
+
const [copyStatus, setCopyStatus] = useState("idle");
|
|
430
|
+
const copyToClipboard = useCallback(async () => {
|
|
431
|
+
setCopyStatus("copying");
|
|
432
|
+
try {
|
|
433
|
+
const blob = await captureFlowImage(reactFlow);
|
|
434
|
+
const clipboardItem = new ClipboardItem({ ["image/png"]: blob });
|
|
435
|
+
await navigator.clipboard.write([clipboardItem]);
|
|
436
|
+
setCopyStatus("success");
|
|
437
|
+
setTimeout(() => {
|
|
438
|
+
setCopyStatus("idle");
|
|
439
|
+
}, COPY_SUCCESS_DURATION);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
console.error("[FlowDiagram] Copy failed:", error);
|
|
442
|
+
setCopyStatus("error");
|
|
443
|
+
setTimeout(() => {
|
|
444
|
+
setCopyStatus("idle");
|
|
445
|
+
}, COPY_SUCCESS_DURATION);
|
|
446
|
+
}
|
|
447
|
+
}, [reactFlow]);
|
|
448
|
+
return { copyStatus, copyToClipboard };
|
|
449
|
+
}
|
|
450
|
+
function useFlowDiagram(options = {}) {
|
|
451
|
+
const { allowFullscreen = true } = options;
|
|
452
|
+
const reactFlow = useReactFlow();
|
|
453
|
+
const { fitView, zoom, zoomIn, zoomOut } = useZoomControls(reactFlow);
|
|
454
|
+
const { closeFullscreen, isFullscreen, toggleFullscreen } = useFullscreenState(allowFullscreen);
|
|
455
|
+
const { copyStatus, copyToClipboard } = useCopyToClipboard(reactFlow);
|
|
456
|
+
return {
|
|
457
|
+
closeFullscreen,
|
|
458
|
+
copyStatus,
|
|
459
|
+
copyToClipboard,
|
|
460
|
+
fitView,
|
|
461
|
+
isFullscreen,
|
|
462
|
+
toggleFullscreen,
|
|
463
|
+
zoom,
|
|
464
|
+
zoomIn,
|
|
465
|
+
zoomOut
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function validateFlowData(nodes, edges) {
|
|
469
|
+
if (nodes.length === 0 && edges.length > 0) {
|
|
470
|
+
console.warn(
|
|
471
|
+
"[FlowDiagram] Edges provided without nodes - edges will not render"
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
475
|
+
const invalidEdges = edges.filter(
|
|
476
|
+
(e) => !nodeIds.has(e.source) || !nodeIds.has(e.target)
|
|
477
|
+
);
|
|
478
|
+
if (invalidEdges.length > 0) {
|
|
479
|
+
console.warn(
|
|
480
|
+
`[FlowDiagram] ${invalidEdges.length} edge(s) reference non-existent nodes:`,
|
|
481
|
+
invalidEdges.map((e) => `${e.id}: ${e.source} -> ${e.target}`)
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
const nodesWithoutPosition = nodes.filter((n) => n.position === void 0);
|
|
485
|
+
if (nodesWithoutPosition.length > 0) {
|
|
486
|
+
console.warn(
|
|
487
|
+
`[FlowDiagram] ${nodesWithoutPosition.length} node(s) missing position:`,
|
|
488
|
+
nodesWithoutPosition.map((n) => n.id)
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
var FlowDiagramInner = memo(function FlowDiagramInner2({
|
|
493
|
+
allowCopy = false,
|
|
494
|
+
allowFullscreen = true,
|
|
495
|
+
className,
|
|
496
|
+
edges,
|
|
497
|
+
fitView = true,
|
|
498
|
+
fitViewOptions,
|
|
499
|
+
height = 400,
|
|
500
|
+
nodes,
|
|
501
|
+
onNodeClick,
|
|
502
|
+
showControls = true,
|
|
503
|
+
title
|
|
504
|
+
}) {
|
|
505
|
+
useEffect(() => {
|
|
506
|
+
validateFlowData(nodes, edges);
|
|
507
|
+
}, [nodes, edges]);
|
|
508
|
+
const {
|
|
509
|
+
closeFullscreen,
|
|
510
|
+
copyStatus,
|
|
511
|
+
copyToClipboard,
|
|
512
|
+
fitView: handleFitView,
|
|
513
|
+
isFullscreen,
|
|
514
|
+
toggleFullscreen,
|
|
515
|
+
zoomIn,
|
|
516
|
+
zoomOut
|
|
517
|
+
} = useFlowDiagram({ allowFullscreen });
|
|
518
|
+
const handleNodeClick = useCallback(
|
|
519
|
+
(_event, node) => {
|
|
520
|
+
onNodeClick?.(node);
|
|
521
|
+
},
|
|
522
|
+
[onNodeClick]
|
|
523
|
+
);
|
|
524
|
+
const handleCopy = useCallback(() => {
|
|
525
|
+
void copyToClipboard();
|
|
526
|
+
}, [copyToClipboard]);
|
|
527
|
+
const canvasProps = {
|
|
528
|
+
allowCopy,
|
|
529
|
+
allowFullscreen,
|
|
530
|
+
className,
|
|
531
|
+
copyStatus,
|
|
532
|
+
edges,
|
|
533
|
+
fitView,
|
|
534
|
+
fitViewOptions,
|
|
535
|
+
height,
|
|
536
|
+
isFullscreen,
|
|
537
|
+
nodes,
|
|
538
|
+
onCopy: allowCopy ? handleCopy : void 0,
|
|
539
|
+
onFitView: handleFitView,
|
|
540
|
+
onFullscreen: allowFullscreen ? toggleFullscreen : void 0,
|
|
541
|
+
onNodeClick: onNodeClick ? handleNodeClick : void 0,
|
|
542
|
+
onZoomIn: zoomIn,
|
|
543
|
+
onZoomOut: zoomOut,
|
|
544
|
+
showControls,
|
|
545
|
+
title
|
|
546
|
+
};
|
|
547
|
+
if (isFullscreen) {
|
|
548
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
549
|
+
/* @__PURE__ */ jsx(
|
|
550
|
+
"div",
|
|
551
|
+
{
|
|
552
|
+
className: cn(
|
|
553
|
+
"rounded-lg border border-border bg-muted/50",
|
|
554
|
+
className
|
|
555
|
+
),
|
|
556
|
+
style: { height }
|
|
557
|
+
}
|
|
558
|
+
),
|
|
559
|
+
/* @__PURE__ */ jsx(FlowFullscreen, { isOpen: isFullscreen, onClose: closeFullscreen, children: /* @__PURE__ */ jsx(FlowCanvas, { ...canvasProps }) })
|
|
560
|
+
] });
|
|
561
|
+
}
|
|
562
|
+
return /* @__PURE__ */ jsx(FlowCanvas, { ...canvasProps });
|
|
563
|
+
});
|
|
564
|
+
var FlowDiagram = memo(function FlowDiagram2(props) {
|
|
565
|
+
return /* @__PURE__ */ jsx(FlowErrorBoundary, { height: props.height, children: /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(FlowDiagramInner, { ...props }) }) });
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
export { FlowControls, FlowDiagram, FlowErrorBoundary, FlowFullscreen, cn, useFlowDiagram };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FlowControls, FlowDiagram, FlowErrorBoundary, FlowFullscreen, useFlowDiagram } from './chunk-XRV5RSYH.js';
|