@bian-womp/spark-workbench 0.2.85 → 0.2.87

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/lib/esm/index.js CHANGED
@@ -5,7 +5,7 @@ import { Position, Handle, NodeResizer, useReactFlow, ReactFlowProvider, ReactFl
5
5
  import React, { useCallback, useState, useRef, useEffect, useMemo, createContext, useContext, useImperativeHandle } from 'react';
6
6
  import cx from 'classnames';
7
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
- import { XCircleIcon, WarningCircleIcon, CopyIcon, TrashIcon, XIcon, ArrowClockwiseIcon, ClockClockwiseIcon, StopIcon, PlayIcon, PlugsConnectedIcon, WifiHighIcon, WifiSlashIcon, PlayPauseIcon, LightningIcon, TreeStructureIcon, CornersOutIcon, DownloadIcon, UploadIcon, BugBeetleIcon, ListBulletsIcon } from '@phosphor-icons/react';
8
+ import { XCircleIcon, WarningCircleIcon, CopyIcon, TrashIcon, XIcon, ArrowClockwiseIcon, ClockClockwiseIcon, StopIcon, PlayIcon, PlugsConnectedIcon, WifiHighIcon, WifiSlashIcon, PlayPauseIcon, LightningIcon, TreeStructureIcon, CornersOutIcon, DownloadIcon, UploadIcon, ImageIcon, BugBeetleIcon, ListBulletsIcon } from '@phosphor-icons/react';
9
9
 
10
10
  class DefaultUIExtensionRegistry {
11
11
  constructor() {
@@ -2188,19 +2188,26 @@ function createHandleLayout(args) {
2188
2188
  };
2189
2189
  }
2190
2190
  function layoutNode(args, overrides) {
2191
- const { node, registry, showValues, overrides: sizeOverrides } = args;
2191
+ const { node, registry, showValues, overrides: sizeOverrides, missingInputs = [], missingOutputs = [], } = args;
2192
2192
  const { inputs, outputs } = computeEffectiveHandles(node, registry);
2193
2193
  const inputOrder = Object.keys(inputs).filter((k) => !isInputPrivate(inputs, k));
2194
2194
  const outputOrder = Object.keys(outputs);
2195
2195
  const estimateNodeSizeFn = overrides?.estimateNodeSize ?? estimateNodeSize;
2196
2196
  const createHandleBoundsFn = overrides?.createHandleBounds ?? createHandleBounds;
2197
2197
  const createHandleLayoutFn = overrides?.createHandleLayout ?? createHandleLayout;
2198
- const { width, height } = estimateNodeSizeFn({
2198
+ // Calculate base dimensions
2199
+ const baseRows = Math.max(inputOrder.length, outputOrder.length);
2200
+ const { width: baseWidth, height: baseHeight } = estimateNodeSizeFn({
2199
2201
  node,
2200
2202
  registry,
2201
2203
  showValues,
2202
2204
  overrides: sizeOverrides,
2203
2205
  });
2206
+ // Calculate final dimensions accounting for missing handles
2207
+ const finalRows = Math.max(inputOrder.length + missingInputs.length, outputOrder.length + missingOutputs.length);
2208
+ const width = baseWidth;
2209
+ const height = baseHeight + Math.max(0, finalRows - baseRows) * NODE_ROW_HEIGHT_PX;
2210
+ // Create bounds and layouts for regular handles
2204
2211
  const handles = [
2205
2212
  ...inputOrder.map((id, i) => createHandleBoundsFn({
2206
2213
  id,
@@ -2231,7 +2238,51 @@ function layoutNode(args, overrides) {
2231
2238
  rowIndex: i,
2232
2239
  })),
2233
2240
  ];
2234
- return { width, height, inputOrder, outputOrder, handles, handleLayout };
2241
+ // Create bounds and layouts for missing handles
2242
+ const missingHandleBounds = [
2243
+ ...missingInputs.map((id, i) => createHandleBoundsFn({
2244
+ id,
2245
+ type: "target",
2246
+ position: Position.Left,
2247
+ rowIndex: inputOrder.length + i,
2248
+ nodeWidth: width,
2249
+ })),
2250
+ ...missingOutputs.map((id, i) => createHandleBoundsFn({
2251
+ id,
2252
+ type: "source",
2253
+ position: Position.Right,
2254
+ rowIndex: outputOrder.length + i,
2255
+ nodeWidth: width,
2256
+ })),
2257
+ ];
2258
+ const missingHandleLayout = [
2259
+ ...missingInputs.map((id, i) => ({
2260
+ ...createHandleLayoutFn({
2261
+ id,
2262
+ type: "target",
2263
+ position: Position.Left,
2264
+ rowIndex: inputOrder.length + i,
2265
+ }),
2266
+ missing: true,
2267
+ })),
2268
+ ...missingOutputs.map((id, i) => ({
2269
+ ...createHandleLayoutFn({
2270
+ id,
2271
+ type: "source",
2272
+ position: Position.Right,
2273
+ rowIndex: outputOrder.length + i,
2274
+ }),
2275
+ missing: true,
2276
+ })),
2277
+ ];
2278
+ return {
2279
+ width,
2280
+ height,
2281
+ inputOrder,
2282
+ outputOrder,
2283
+ handles: [...handles, ...missingHandleBounds],
2284
+ handleLayout: [...handleLayout, ...missingHandleLayout],
2285
+ };
2235
2286
  }
2236
2287
 
2237
2288
  function useWorkbenchBridge(wb) {
@@ -2503,10 +2554,13 @@ function toReactFlow(def, positions, sizes, registry, opts) {
2503
2554
  const EDGE_STYLE_MISSING = { stroke: "#f59e0b", strokeWidth: 2 }; // amber-500
2504
2555
  const EDGE_STYLE_ERROR = { stroke: "#ef4444", strokeWidth: 2 };
2505
2556
  const EDGE_STYLE_RUNNING = { stroke: "#3b82f6" };
2506
- // Build a map of valid handles per node up-front
2557
+ // Build a map of valid handles per node up-front and cache handle data
2507
2558
  const validHandleMap = {};
2559
+ const nodeHandlesCache = {};
2508
2560
  for (const n of def.nodes) {
2509
- const { inputs, outputs } = computeEffectiveHandles(n, registry);
2561
+ const handles = computeEffectiveHandles(n, registry);
2562
+ nodeHandlesCache[n.nodeId] = handles;
2563
+ const { inputs, outputs } = handles;
2510
2564
  const inputOrder = Object.keys(inputs).filter((k) => !isInputPrivate(inputs, k));
2511
2565
  const outputOrder = Object.keys(outputs);
2512
2566
  validHandleMap[n.nodeId] = {
@@ -2535,87 +2589,36 @@ function toReactFlow(def, positions, sizes, registry, opts) {
2535
2589
  (missingOutputsByNode[srcId] || (missingOutputsByNode[srcId] = new Set())).add(srcHandle);
2536
2590
  }
2537
2591
  }
2538
- // This map is still used later for certain checks; align with valid handles
2539
- const nodeHandleMap = {};
2540
- Object.assign(nodeHandleMap, validHandleMap);
2541
2592
  // Get layout function overrides from UI registry
2542
2593
  const layoutNodeOverride = opts.ui?.getLayoutNode() ?? layoutNode;
2543
2594
  const createHandleBoundsFn = opts.ui?.getCreateHandleBounds() ?? createHandleBounds;
2544
2595
  const createHandleLayoutFn = opts.ui?.getCreateHandleLayout() ?? createHandleLayout;
2545
2596
  const estimateNodeSizeFn = opts.ui?.getEstimateNodeSize() ?? estimateNodeSize;
2546
- const computeLayout = (node, overrides) => {
2597
+ const computeLayout = (node, overrides, missingInputs, missingOutputs) => {
2547
2598
  return layoutNodeOverride
2548
2599
  ? layoutNodeOverride({
2549
2600
  node,
2550
2601
  registry,
2551
2602
  showValues: opts.showValues,
2552
2603
  overrides,
2604
+ missingInputs,
2605
+ missingOutputs,
2553
2606
  })
2554
2607
  : layoutNode({
2555
2608
  node,
2556
2609
  registry,
2557
2610
  showValues: opts.showValues,
2558
2611
  overrides,
2612
+ missingInputs,
2613
+ missingOutputs,
2559
2614
  }, {
2560
2615
  estimateNodeSize: estimateNodeSizeFn,
2561
2616
  createHandleBounds: createHandleBoundsFn,
2562
2617
  createHandleLayout: createHandleLayoutFn,
2563
2618
  });
2564
2619
  };
2565
- const calculateDimensionsWithMissingHandles = (geom, extraInputs, extraOutputs) => {
2566
- const baseLeftCount = geom.inputOrder.length;
2567
- const baseRightCount = geom.outputOrder.length;
2568
- const baseRows = Math.max(baseLeftCount, baseRightCount);
2569
- const newRows = Math.max(baseLeftCount + extraInputs.length, baseRightCount + extraOutputs.length);
2570
- return {
2571
- baseLeftCount,
2572
- baseRightCount,
2573
- baseRows,
2574
- newRows,
2575
- width: geom.width,
2576
- height: geom.height + Math.max(0, newRows - baseRows) * NODE_ROW_HEIGHT_PX,
2577
- };
2578
- };
2579
- const createMissingHandleLayouts = (extraInputs, extraOutputs, baseLeftCount, baseRightCount) => {
2580
- const left = extraInputs.map((id, i) => ({
2581
- ...createHandleLayoutFn({
2582
- id,
2583
- type: "target",
2584
- position: Position.Left,
2585
- rowIndex: baseLeftCount + i,
2586
- }),
2587
- missing: true,
2588
- }));
2589
- const right = extraOutputs.map((id, i) => ({
2590
- ...createHandleLayoutFn({
2591
- id,
2592
- type: "source",
2593
- position: Position.Right,
2594
- rowIndex: baseRightCount + i,
2595
- }),
2596
- missing: true,
2597
- }));
2598
- return [...left, ...right];
2599
- };
2600
- const createMissingHandleBounds = (extraInputs, extraOutputs, baseLeftCount, baseRightCount, nodeWidth) => {
2601
- const left = extraInputs.map((id, i) => createHandleBoundsFn({
2602
- id,
2603
- type: "target",
2604
- position: Position.Left,
2605
- rowIndex: baseLeftCount + i,
2606
- nodeWidth,
2607
- }));
2608
- const right = extraOutputs.map((id, i) => createHandleBoundsFn({
2609
- id,
2610
- type: "source",
2611
- position: Position.Right,
2612
- rowIndex: baseRightCount + i,
2613
- nodeWidth,
2614
- }));
2615
- return [...left, ...right];
2616
- };
2617
2620
  const nodes = def.nodes.map((n) => {
2618
- const { inputs: inputSource, outputs: outputSource } = computeEffectiveHandles(n, registry);
2621
+ const { inputs: inputSource, outputs: outputSource } = nodeHandlesCache[n.nodeId];
2619
2622
  const overrideSize = opts.getDefaultNodeSize?.(n.typeId);
2620
2623
  const customSize = sizes?.[n.nodeId];
2621
2624
  const sizeOverrides = customSize
@@ -2623,14 +2626,12 @@ function toReactFlow(def, positions, sizes, registry, opts) {
2623
2626
  : overrideSize;
2624
2627
  const extraInputs = Array.from(missingInputsByNode[n.nodeId] || []);
2625
2628
  const extraOutputs = Array.from(missingOutputsByNode[n.nodeId] || []);
2626
- const geom = computeLayout(n, sizeOverrides);
2627
- const finalDims = calculateDimensionsWithMissingHandles(geom, extraInputs, extraOutputs);
2628
- const renderWidth = customSize?.width ?? finalDims.width;
2629
- const renderHeight = customSize?.height ?? finalDims.height;
2630
- const initialGeom = customSize ? computeLayout(n, overrideSize) : geom;
2631
- const initialDims = customSize
2632
- ? calculateDimensionsWithMissingHandles(initialGeom, extraInputs, extraOutputs)
2633
- : finalDims;
2629
+ const geom = computeLayout(n, sizeOverrides, extraInputs, extraOutputs);
2630
+ const renderWidth = customSize?.width ?? geom.width;
2631
+ const renderHeight = customSize?.height ?? geom.height;
2632
+ const initialGeom = customSize
2633
+ ? computeLayout(n, overrideSize, extraInputs, extraOutputs)
2634
+ : geom;
2634
2635
  const inputHandles = geom.inputOrder.map((id) => ({
2635
2636
  id,
2636
2637
  typeId: getInputTypeId(inputSource, id),
@@ -2639,14 +2640,8 @@ function toReactFlow(def, positions, sizes, registry, opts) {
2639
2640
  id,
2640
2641
  typeId: formatDeclaredTypeSignature(outputSource[id]),
2641
2642
  }));
2642
- nodeHandleMap[n.nodeId] = {
2643
- inputs: new Set(inputHandles.map((h) => h.id)),
2644
- outputs: new Set(outputHandles.map((h) => h.id)),
2645
- };
2646
- const missingHandleLayouts = createMissingHandleLayouts(extraInputs, extraOutputs, finalDims.baseLeftCount, finalDims.baseRightCount);
2647
- const handleLayout = [...geom.handleLayout, ...missingHandleLayouts];
2648
- const missingHandleBounds = createMissingHandleBounds(extraInputs, extraOutputs, finalDims.baseLeftCount, finalDims.baseRightCount, renderWidth);
2649
- const handles = [...geom.handles, ...missingHandleBounds];
2643
+ const handleLayout = geom.handleLayout;
2644
+ const handles = geom.handles;
2650
2645
  return {
2651
2646
  id: n.nodeId,
2652
2647
  data: {
@@ -2662,8 +2657,8 @@ function toReactFlow(def, positions, sizes, registry, opts) {
2662
2657
  showValues: opts.showValues,
2663
2658
  renderWidth,
2664
2659
  renderHeight,
2665
- initialWidth: initialDims.width,
2666
- initialHeight: initialDims.height,
2660
+ initialWidth: initialGeom.width,
2661
+ initialHeight: initialGeom.height,
2667
2662
  inputValues: opts.inputs?.[n.nodeId],
2668
2663
  inputDefaults: opts.inputDefaults?.[n.nodeId],
2669
2664
  outputValues: opts.outputs?.[n.nodeId],
@@ -4451,7 +4446,7 @@ function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInv
4451
4446
  return (jsxs("div", { className: "flex items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", style: {
4452
4447
  maxHeight: NODE_HEADER_HEIGHT_PX,
4453
4448
  minHeight: NODE_HEADER_HEIGHT_PX,
4454
- }, children: [isEditing ? (jsx("input", { ref: inputRef, type: "text", value: editValue, onChange: (e) => setEditValue(e.target.value), onBlur: handleSave, onKeyDown: handleKeyDown, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), className: "flex-1 h-full text-sm bg-transparent border border-blue-500 rounded px-1 outline-none wb-nodrag", style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` } })) : (jsx("strong", { className: `flex-1 h-full text-sm select-none truncate ${typeId ? "cursor-text" : ""}`, style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` }, onDoubleClick: handleDoubleClick, title: typeId ? "Double-click to rename" : undefined, children: displayName })), jsxs("div", { className: "flex items-center gap-1", children: [jsx("button", { className: "w-4 h-4 border border-gray-400 rounded text-[10px] leading-3 flex items-center justify-center", title: "Invalidate and re-run", onClick: (e) => {
4449
+ }, children: [isEditing ? (jsx("input", { ref: inputRef, type: "text", value: editValue, onChange: (e) => setEditValue(e.target.value), onBlur: handleSave, onKeyDown: handleKeyDown, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), className: "flex-1 h-full text-sm bg-transparent border border-blue-500 rounded px-1 outline-none wb-nodrag", style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` } })) : (jsx("strong", { className: `react-flow__node-title flex-1 h-full text-sm select-none truncate ${typeId ? "cursor-text" : ""}`, style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` }, onDoubleClick: handleDoubleClick, title: typeId ? "Double-click to rename" : undefined, children: displayName })), jsxs("div", { className: "flex items-center gap-1", children: [jsx("button", { className: "w-4 h-4 border border-gray-400 rounded text-[10px] leading-3 flex items-center justify-center", title: "Invalidate and re-run", onClick: (e) => {
4455
4450
  e.stopPropagation();
4456
4451
  handleInvalidate();
4457
4452
  }, children: jsx(ArrowClockwiseIcon, { size: 10 }) }), right, validation.issues && validation.issues.length > 0 && (jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
@@ -4462,7 +4457,7 @@ function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInv
4462
4457
  }
4463
4458
 
4464
4459
  function NodeHandleItem({ kind, id, type, position, y, isConnectable, className, labelClassName, renderLabel, }) {
4465
- return (jsxs(Fragment, { children: [jsx(Handle, { id: id, type: type, position: position, isConnectable: isConnectable, className: className, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsx("div", { className: labelClassName + (kind === "input" ? " left-2" : " right-2"), style: {
4460
+ return (jsxs(Fragment, { children: [jsx(Handle, { id: id, type: type, position: position, isConnectable: isConnectable, className: className, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsx("div", { className: `${labelClassName} ${kind === "input" ? " left-2" : " right-2"}`, style: {
4466
4461
  top: (y ?? 0) - 8,
4467
4462
  ...(kind === "input"
4468
4463
  ? { right: "50%" }
@@ -4574,7 +4569,7 @@ function DefaultNodeContent({ data, isConnectable, }) {
4574
4569
  isDefault: false,
4575
4570
  };
4576
4571
  })();
4577
- return (jsxs("span", { className: `flex items-center gap-1 w-full ${valueText?.isDefault ? "text-gray-400" : ""}`, children: [kind === "output" ? (jsxs(Fragment, { children: [valueText !== undefined ? (jsx("span", { className: "opacity-60 truncate pl-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText.text })) : (jsx("span", { style: { flex: 1, minWidth: 0, maxWidth: "100%" } })), jsx("span", { className: "truncate shrink-0", style: valueText !== undefined ? { maxWidth: "40%" } : {}, children: prettyHandle(handleId) })] })) : (jsxs(Fragment, { children: [jsx("span", { className: "truncate shrink-0", style: valueText !== undefined ? { maxWidth: "40%" } : {}, children: prettyHandle(handleId) }), valueText !== undefined && (jsx("span", { className: `truncate pr-1 ${valueText.isDefault ? "text-gray-400" : "opacity-60"}`, style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText.text }))] })), hasAny && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "shrink-0", title: title }))] }));
4572
+ return (jsxs("span", { className: `flex items-center gap-1 w-full ${valueText?.isDefault ? "text-gray-400" : ""}`, children: [kind === "output" ? (jsxs(Fragment, { children: [valueText !== undefined ? (jsx("span", { className: "opacity-60 truncate pl-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText.text })) : (jsx("span", { style: { flex: 1, minWidth: 0, maxWidth: "100%" } })), jsx("span", { className: "truncate shrink-0", style: valueText !== undefined ? { maxWidth: "40%" } : {}, children: prettyHandle(handleId) })] })) : (jsxs(Fragment, { children: [jsx("span", { className: "truncate shrink-0", "data-handle-id": handleId, style: valueText !== undefined ? { maxWidth: "40%" } : {}, children: prettyHandle(handleId) }), valueText !== undefined && (jsx("span", { className: `truncate pr-1 ${valueText.isDefault ? "text-gray-400" : "opacity-60"}`, style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText.text }))] })), hasAny && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "shrink-0", title: title }))] }));
4578
4573
  } })] }));
4579
4574
  }
4580
4575
 
@@ -5488,6 +5483,601 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
5488
5483
  (SelectionContextMenuRenderer ? (jsx(SelectionContextMenuRenderer, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts })) : (jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts })))] }) }), toast && (jsx(KeyboardShortcutToast, { message: toast.message, onClose: hideToast }, toast.id))] }));
5489
5484
  });
5490
5485
 
5486
+ /**
5487
+ * Flow thumbnail capture utility
5488
+ * Captures React Flow canvas as SVG image
5489
+ */
5490
+ // ============================================================================
5491
+ // Utility Functions
5492
+ // ============================================================================
5493
+ /**
5494
+ * Parses CSS transform string to extract translate and scale values
5495
+ */
5496
+ function parseViewportTransform(transform) {
5497
+ let translateX = 0;
5498
+ let translateY = 0;
5499
+ let scale = 1;
5500
+ if (transform && transform !== "none") {
5501
+ // Try translate() scale() format first
5502
+ const translateMatch = transform.match(/translate\(([^,]+)px,\s*([^)]+)px\)/);
5503
+ const scaleMatch = transform.match(/scale\(([^)]+)\)/);
5504
+ if (translateMatch) {
5505
+ translateX = parseFloat(translateMatch[1]);
5506
+ translateY = parseFloat(translateMatch[2]);
5507
+ }
5508
+ if (scaleMatch) {
5509
+ scale = parseFloat(scaleMatch[1]);
5510
+ }
5511
+ // Fallback to matrix format
5512
+ if (!translateMatch) {
5513
+ const matrixMatch = transform.match(/matrix\(([^)]+)\)/);
5514
+ if (matrixMatch) {
5515
+ const values = matrixMatch[1]
5516
+ .split(",")
5517
+ .map((v) => parseFloat(v.trim()));
5518
+ if (values.length >= 6) {
5519
+ scale = Math.sqrt(values[0] * values[0] + values[1] * values[1]);
5520
+ translateX = values[4];
5521
+ translateY = values[5];
5522
+ }
5523
+ }
5524
+ }
5525
+ }
5526
+ return { translateX, translateY, scale };
5527
+ }
5528
+ /**
5529
+ * Calculates visible viewport bounds in flow coordinates
5530
+ */
5531
+ function calculateVisibleBounds(viewportRect, transform) {
5532
+ const { translateX, translateY, scale } = transform;
5533
+ // Guard against division by zero
5534
+ if (scale === 0) {
5535
+ console.warn("[flowThumbnail] Viewport scale is 0, using default bounds");
5536
+ return {
5537
+ minX: -translateX,
5538
+ minY: -translateY,
5539
+ maxX: viewportRect.width - translateX,
5540
+ maxY: viewportRect.height - translateY,
5541
+ };
5542
+ }
5543
+ // Screen to flow: (screenX - translateX) / scale
5544
+ return {
5545
+ minX: (0 - translateX) / scale,
5546
+ minY: (0 - translateY) / scale,
5547
+ maxX: (viewportRect.width - translateX) / scale,
5548
+ maxY: (viewportRect.height - translateY) / scale,
5549
+ };
5550
+ }
5551
+ /**
5552
+ * Parses border radius string (px or rem) to pixels
5553
+ */
5554
+ function parseBorderRadius(borderRadiusStr) {
5555
+ if (borderRadiusStr === "0px") {
5556
+ return 8; // default
5557
+ }
5558
+ const match = borderRadiusStr.match(/([\d.]+)(px|rem)/);
5559
+ if (match) {
5560
+ const value = parseFloat(match[1]);
5561
+ const unit = match[2];
5562
+ // Convert rem to px (assuming 16px base) or use px directly
5563
+ return unit === "rem" ? value * 16 : value;
5564
+ }
5565
+ // Try direct parseFloat as fallback
5566
+ const parsed = parseFloat(borderRadiusStr);
5567
+ return isNaN(parsed) ? 8 : parsed;
5568
+ }
5569
+ /**
5570
+ * Extracts stroke color from element, with fallback
5571
+ */
5572
+ function extractStrokeColor(element) {
5573
+ if (element instanceof SVGPathElement) {
5574
+ return (element.getAttribute("stroke") ||
5575
+ window.getComputedStyle(element).stroke ||
5576
+ "#b1b1b7");
5577
+ }
5578
+ const style = window.getComputedStyle(element);
5579
+ return (style.borderColor || style.borderTopColor || "#6b7280" // gray-500 default
5580
+ );
5581
+ }
5582
+ /**
5583
+ * Extracts stroke/border width from element, ensuring minimum value
5584
+ */
5585
+ function extractStrokeWidth(element, minWidth = 1) {
5586
+ if (element instanceof SVGPathElement) {
5587
+ const width = parseFloat(element.getAttribute("stroke-width") || "0") ||
5588
+ parseFloat(window.getComputedStyle(element).strokeWidth || "2");
5589
+ return width > 0 ? width : minWidth;
5590
+ }
5591
+ const style = window.getComputedStyle(element);
5592
+ const width = parseFloat(style.borderWidth || style.borderTopWidth || "0");
5593
+ return width > 0 ? width : minWidth;
5594
+ }
5595
+ /**
5596
+ * Checks if a rectangle intersects with visible bounds
5597
+ */
5598
+ function isRectVisible(x, y, width, height, bounds) {
5599
+ return (x + width >= bounds.minX &&
5600
+ x <= bounds.maxX &&
5601
+ y + height >= bounds.minY &&
5602
+ y <= bounds.maxY);
5603
+ }
5604
+ /**
5605
+ * Parses path data to get bounding box
5606
+ * Handles M (moveTo), L (lineTo), C (cubic Bezier), Q (quadratic Bezier), and H/V (horizontal/vertical) commands
5607
+ */
5608
+ function getPathBounds(pathData) {
5609
+ let minX = Infinity;
5610
+ let minY = Infinity;
5611
+ let maxX = -Infinity;
5612
+ let maxY = -Infinity;
5613
+ // Match coordinates from various path commands: M, L, C, Q, T, S, H, V
5614
+ // Pattern matches: command letter followed by coordinate pairs
5615
+ const coordPattern = /[MLCQTSHV](-?\d+\.?\d*),(-?\d+\.?\d*)/g;
5616
+ const coords = pathData.match(coordPattern);
5617
+ if (coords) {
5618
+ coords.forEach((coord) => {
5619
+ const match = coord.match(/(-?\d+\.?\d*),(-?\d+\.?\d*)/);
5620
+ if (match) {
5621
+ const x = parseFloat(match[1]);
5622
+ const y = parseFloat(match[2]);
5623
+ if (!isNaN(x) && !isNaN(y)) {
5624
+ minX = Math.min(minX, x);
5625
+ minY = Math.min(minY, y);
5626
+ maxX = Math.max(maxX, x);
5627
+ maxY = Math.max(maxY, y);
5628
+ }
5629
+ }
5630
+ });
5631
+ }
5632
+ return { minX, minY, maxX, maxY };
5633
+ }
5634
+ // ============================================================================
5635
+ // Edge Extraction
5636
+ // ============================================================================
5637
+ /**
5638
+ * Extracts visible edge paths from React Flow viewport
5639
+ */
5640
+ function extractEdgePaths(viewport, visibleBounds) {
5641
+ const edges = [];
5642
+ let minX = Infinity;
5643
+ let minY = Infinity;
5644
+ let maxX = -Infinity;
5645
+ let maxY = -Infinity;
5646
+ const edgePathElements = viewport.querySelectorAll(".react-flow__edge-path");
5647
+ edgePathElements.forEach((pathEl) => {
5648
+ const pathData = pathEl.getAttribute("d");
5649
+ if (!pathData)
5650
+ return;
5651
+ const pathBounds = getPathBounds(pathData);
5652
+ // Only include edge if it intersects with visible viewport
5653
+ if (pathBounds.maxX >= visibleBounds.minX &&
5654
+ pathBounds.minX <= visibleBounds.maxX &&
5655
+ pathBounds.maxY >= visibleBounds.minY &&
5656
+ pathBounds.minY <= visibleBounds.maxY) {
5657
+ edges.push({
5658
+ d: pathData,
5659
+ stroke: extractStrokeColor(pathEl),
5660
+ strokeWidth: extractStrokeWidth(pathEl, 2),
5661
+ });
5662
+ // Update bounding box
5663
+ minX = Math.min(minX, pathBounds.minX);
5664
+ minY = Math.min(minY, pathBounds.minY);
5665
+ maxX = Math.max(maxX, pathBounds.maxX);
5666
+ maxY = Math.max(maxY, pathBounds.maxY);
5667
+ }
5668
+ });
5669
+ return { edges, bounds: { minX, minY, maxX, maxY } };
5670
+ }
5671
+ // ============================================================================
5672
+ // Node Extraction
5673
+ // ============================================================================
5674
+ /**
5675
+ * Extracts node position from transform style
5676
+ */
5677
+ function extractNodePosition(nodeEl) {
5678
+ const transformStyle = nodeEl.style.transform || "";
5679
+ const translateMatch = transformStyle.match(/translate\(([^,]+)px,\s*([^)]+)px\)/);
5680
+ if (translateMatch) {
5681
+ return {
5682
+ x: parseFloat(translateMatch[1]),
5683
+ y: parseFloat(translateMatch[2]),
5684
+ };
5685
+ }
5686
+ return { x: 0, y: 0 };
5687
+ }
5688
+ /**
5689
+ * Extracts node dimensions from inline styles
5690
+ */
5691
+ function extractNodeDimensions(nodeEl) {
5692
+ const widthMatch = nodeEl.style.width?.match(/(\d+)px/);
5693
+ const heightMatch = nodeEl.style.height?.match(/(\d+)px/);
5694
+ return {
5695
+ width: widthMatch ? parseFloat(widthMatch[1]) : 150,
5696
+ height: heightMatch ? parseFloat(heightMatch[1]) : 40,
5697
+ };
5698
+ }
5699
+ /**
5700
+ * Extracts node styles (colors, border, radius) from computed styles
5701
+ */
5702
+ function extractNodeStyles(nodeContent) {
5703
+ const computedStyle = window.getComputedStyle(nodeContent);
5704
+ // Use gray background for nodes in thumbnail
5705
+ const fill = "#f3f4f6"; // gray-100 equivalent
5706
+ const stroke = extractStrokeColor(nodeContent);
5707
+ const strokeWidth = extractStrokeWidth(nodeContent, 1);
5708
+ const borderRadiusStr = computedStyle.borderRadius || "8px";
5709
+ const rx = parseBorderRadius(borderRadiusStr);
5710
+ const ry = rx; // Use same radius for both x and y
5711
+ return { fill, stroke, strokeWidth, rx, ry };
5712
+ }
5713
+ /**
5714
+ * Determines if a handle is a source (output) or target (input)
5715
+ */
5716
+ function isHandleSource(handleEl) {
5717
+ return (handleEl.classList.contains("react-flow__handle-right") ||
5718
+ handleEl.classList.contains("react-flow__handle-source"));
5719
+ }
5720
+ /**
5721
+ * Extracts handle position and calculates absolute coordinates
5722
+ */
5723
+ function extractHandlePosition(handleEl, nodeX, nodeY, nodeWidth, isSource) {
5724
+ const handleStyle = window.getComputedStyle(handleEl);
5725
+ const handleTop = parseFloat(handleStyle.top || "0");
5726
+ const handleLeft = handleStyle.left;
5727
+ const handleRight = handleStyle.right;
5728
+ const handleY = nodeY + handleTop;
5729
+ let handleX;
5730
+ if (isSource) {
5731
+ // Source handles are on the right edge
5732
+ if (handleRight !== "auto" && handleRight !== "") {
5733
+ const rightValue = parseFloat(handleRight) || 0;
5734
+ handleX = nodeX + nodeWidth + rightValue;
5735
+ }
5736
+ else {
5737
+ handleX = nodeX + nodeWidth;
5738
+ }
5739
+ }
5740
+ else {
5741
+ // Target handles are on the left edge
5742
+ if (handleLeft !== "auto" && handleLeft !== "") {
5743
+ const leftValue = parseFloat(handleLeft) || 0;
5744
+ handleX = nodeX + leftValue;
5745
+ }
5746
+ else {
5747
+ handleX = nodeX;
5748
+ }
5749
+ }
5750
+ return { x: handleX, y: handleY };
5751
+ }
5752
+ /**
5753
+ * Extracts handles from a node element
5754
+ */
5755
+ function extractNodeHandles(nodeEl, nodeX, nodeY, nodeWidth) {
5756
+ const handles = [];
5757
+ const handleElements = nodeEl.querySelectorAll(".react-flow__handle");
5758
+ handleElements.forEach((handleEl) => {
5759
+ const handleStyle = window.getComputedStyle(handleEl);
5760
+ const handleWidth = parseFloat(handleStyle.width || "12");
5761
+ const handleHeight = parseFloat(handleStyle.height || "12");
5762
+ const isSource = isHandleSource(handleEl);
5763
+ const position = extractHandlePosition(handleEl, nodeX, nodeY, nodeWidth, isSource);
5764
+ handles.push({
5765
+ x: position.x,
5766
+ y: position.y,
5767
+ width: handleWidth,
5768
+ height: handleHeight,
5769
+ fill: handleStyle.backgroundColor || "rgba(255, 255, 255, 0.5)",
5770
+ stroke: extractStrokeColor(handleEl),
5771
+ strokeWidth: extractStrokeWidth(handleEl, 1),
5772
+ type: isSource ? "source" : "target",
5773
+ });
5774
+ });
5775
+ return handles;
5776
+ }
5777
+ /**
5778
+ * Extracts node title text and position
5779
+ */
5780
+ function extractNodeTitle(nodeEl, nodeX, nodeY) {
5781
+ const titleElement = nodeEl.querySelector(".react-flow__node-title");
5782
+ if (!titleElement)
5783
+ return undefined;
5784
+ const titleText = titleElement.textContent || titleElement.innerText || "";
5785
+ if (!titleText.trim())
5786
+ return undefined;
5787
+ const titleStyle = window.getComputedStyle(titleElement);
5788
+ const titleRect = titleElement.getBoundingClientRect();
5789
+ const nodeRect = nodeEl.getBoundingClientRect();
5790
+ // Calculate title position relative to node (in flow coordinates)
5791
+ const titleRelativeX = titleRect.left - nodeRect.left;
5792
+ const titleRelativeY = titleRect.top - nodeRect.top;
5793
+ // Get font properties
5794
+ const fontSize = parseFloat(titleStyle.fontSize || "14");
5795
+ const fill = titleStyle.color || "#374151"; // gray-700 default
5796
+ const fontWeight = titleStyle.fontWeight || "600"; // bold default
5797
+ // Calculate text position (SVG text uses baseline)
5798
+ const lineHeight = parseFloat(titleStyle.lineHeight || String(fontSize * 1.2));
5799
+ const textBaselineOffset = lineHeight * 0.8; // Approximate baseline offset
5800
+ return {
5801
+ text: titleText.trim(),
5802
+ x: nodeX + titleRelativeX,
5803
+ y: nodeY + titleRelativeY + textBaselineOffset,
5804
+ fontSize,
5805
+ fill,
5806
+ fontWeight,
5807
+ };
5808
+ }
5809
+ /**
5810
+ * Extracts visible nodes from React Flow viewport
5811
+ */
5812
+ function extractNodes(viewport, visibleBounds) {
5813
+ const nodes = [];
5814
+ let minX = Infinity;
5815
+ let minY = Infinity;
5816
+ let maxX = -Infinity;
5817
+ let maxY = -Infinity;
5818
+ const nodeElements = viewport.querySelectorAll(".react-flow__node");
5819
+ nodeElements.forEach((nodeEl) => {
5820
+ const position = extractNodePosition(nodeEl);
5821
+ const dimensions = extractNodeDimensions(nodeEl);
5822
+ // Get the actual node content div (first child)
5823
+ const nodeContent = nodeEl.firstElementChild;
5824
+ if (!nodeContent)
5825
+ return;
5826
+ const styles = extractNodeStyles(nodeContent);
5827
+ const handles = extractNodeHandles(nodeEl, position.x, position.y, dimensions.width);
5828
+ const title = extractNodeTitle(nodeEl, position.x, position.y);
5829
+ // Only include node if it's within visible viewport
5830
+ if (isRectVisible(position.x, position.y, dimensions.width, dimensions.height, visibleBounds)) {
5831
+ nodes.push({
5832
+ x: position.x,
5833
+ y: position.y,
5834
+ width: dimensions.width,
5835
+ height: dimensions.height,
5836
+ ...styles,
5837
+ handles,
5838
+ title,
5839
+ });
5840
+ // Update bounding box
5841
+ minX = Math.min(minX, position.x);
5842
+ minY = Math.min(minY, position.y);
5843
+ maxX = Math.max(maxX, position.x + dimensions.width);
5844
+ maxY = Math.max(maxY, position.y + dimensions.height);
5845
+ // Update bounding box to include handles
5846
+ handles.forEach((handle) => {
5847
+ minX = Math.min(minX, handle.x);
5848
+ minY = Math.min(minY, handle.y);
5849
+ maxX = Math.max(maxX, handle.x + handle.width);
5850
+ maxY = Math.max(maxY, handle.y + handle.height);
5851
+ });
5852
+ }
5853
+ });
5854
+ return { nodes, bounds: { minX, minY, maxX, maxY } };
5855
+ }
5856
+ // ============================================================================
5857
+ // SVG Rendering
5858
+ // ============================================================================
5859
+ /**
5860
+ * Creates SVG element with dot pattern background (matching React Flow)
5861
+ */
5862
+ function createSVGElement(width, height) {
5863
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
5864
+ svg.setAttribute("width", String(width));
5865
+ svg.setAttribute("height", String(height));
5866
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
5867
+ // Create defs section for patterns
5868
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
5869
+ // Create dot pattern (matching React Flow's BackgroundVariant.Dots)
5870
+ // React Flow uses gap={12} and size={1} by default
5871
+ const pattern = document.createElementNS("http://www.w3.org/2000/svg", "pattern");
5872
+ pattern.setAttribute("id", "dot-pattern");
5873
+ pattern.setAttribute("x", "0");
5874
+ pattern.setAttribute("y", "0");
5875
+ pattern.setAttribute("width", "24"); // gap between dots (matching React Flow default)
5876
+ pattern.setAttribute("height", "24");
5877
+ pattern.setAttribute("patternUnits", "userSpaceOnUse");
5878
+ // Create a circle for the dot (centered in the pattern cell)
5879
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
5880
+ circle.setAttribute("cx", "12"); // Center of 24x24 pattern cell
5881
+ circle.setAttribute("cy", "12");
5882
+ circle.setAttribute("r", "1"); // dot radius = 1 (matching React Flow size={1})
5883
+ circle.setAttribute("fill", "#f1f1f1"); // gray color matching React Flow default
5884
+ pattern.appendChild(circle);
5885
+ defs.appendChild(pattern);
5886
+ svg.appendChild(defs);
5887
+ // Create background rectangle with white base
5888
+ const bgRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
5889
+ bgRect.setAttribute("width", String(width));
5890
+ bgRect.setAttribute("height", String(height));
5891
+ bgRect.setAttribute("fill", "#ffffff"); // Base background color
5892
+ svg.appendChild(bgRect);
5893
+ // Create pattern overlay rectangle
5894
+ const patternRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
5895
+ patternRect.setAttribute("width", String(width));
5896
+ patternRect.setAttribute("height", String(height));
5897
+ patternRect.setAttribute("fill", "url(#dot-pattern)");
5898
+ svg.appendChild(patternRect);
5899
+ // Create group with transform
5900
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
5901
+ svg.appendChild(group);
5902
+ return { svg, group };
5903
+ }
5904
+ /**
5905
+ * Renders a node rectangle to SVG group
5906
+ */
5907
+ function renderNodeRect(group, node) {
5908
+ const rectEl = document.createElementNS("http://www.w3.org/2000/svg", "rect");
5909
+ rectEl.setAttribute("x", String(node.x));
5910
+ rectEl.setAttribute("y", String(node.y));
5911
+ rectEl.setAttribute("width", String(node.width));
5912
+ rectEl.setAttribute("height", String(node.height));
5913
+ rectEl.setAttribute("rx", String(node.rx));
5914
+ rectEl.setAttribute("ry", String(node.ry));
5915
+ rectEl.setAttribute("fill", node.fill);
5916
+ rectEl.setAttribute("stroke", node.stroke);
5917
+ rectEl.setAttribute("stroke-width", String(node.strokeWidth));
5918
+ group.appendChild(rectEl);
5919
+ }
5920
+ /**
5921
+ * Renders a handle to SVG group
5922
+ */
5923
+ function renderHandle(group, handle) {
5924
+ const handleEl = document.createElementNS("http://www.w3.org/2000/svg", "rect");
5925
+ // Handles are centered on edges
5926
+ handleEl.setAttribute("x", String(handle.x - handle.width / 2));
5927
+ handleEl.setAttribute("y", String(handle.y - handle.height / 2));
5928
+ handleEl.setAttribute("width", String(handle.width));
5929
+ handleEl.setAttribute("height", String(handle.height));
5930
+ handleEl.setAttribute("rx", String(handle.width / 2)); // Make handles circular/rounded
5931
+ handleEl.setAttribute("ry", String(handle.height / 2));
5932
+ handleEl.setAttribute("fill", handle.fill);
5933
+ handleEl.setAttribute("stroke", handle.stroke);
5934
+ handleEl.setAttribute("stroke-width", String(handle.strokeWidth));
5935
+ group.appendChild(handleEl);
5936
+ }
5937
+ /**
5938
+ * Renders node title text to SVG group
5939
+ */
5940
+ function renderNodeTitle(group, title) {
5941
+ const textEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
5942
+ textEl.setAttribute("x", String(title.x));
5943
+ textEl.setAttribute("y", String(title.y));
5944
+ textEl.setAttribute("font-size", String(title.fontSize));
5945
+ textEl.setAttribute("fill", title.fill);
5946
+ textEl.setAttribute("font-weight", title.fontWeight);
5947
+ textEl.setAttribute("font-family", "system-ui, -apple-system, sans-serif");
5948
+ textEl.textContent = title.text;
5949
+ group.appendChild(textEl);
5950
+ }
5951
+ /**
5952
+ * Renders an edge path to SVG group
5953
+ */
5954
+ function renderEdgePath(group, edge) {
5955
+ const pathEl = document.createElementNS("http://www.w3.org/2000/svg", "path");
5956
+ pathEl.setAttribute("d", edge.d);
5957
+ pathEl.setAttribute("fill", "none");
5958
+ pathEl.setAttribute("stroke", edge.stroke);
5959
+ pathEl.setAttribute("stroke-width", String(edge.strokeWidth));
5960
+ group.appendChild(pathEl);
5961
+ }
5962
+ /**
5963
+ * Renders all nodes, handles, titles, and edges to SVG group
5964
+ */
5965
+ function renderContentToSVG(group, nodes, edges, transformX, transformY) {
5966
+ group.setAttribute("transform", `translate(${transformX}, ${transformY})`);
5967
+ // Render nodes
5968
+ nodes.forEach((node) => {
5969
+ renderNodeRect(group, node);
5970
+ node.handles.forEach((handle) => renderHandle(group, handle));
5971
+ if (node.title) {
5972
+ renderNodeTitle(group, node.title);
5973
+ }
5974
+ });
5975
+ // Render edges
5976
+ edges.forEach((edge) => renderEdgePath(group, edge));
5977
+ }
5978
+ // ============================================================================
5979
+ // Main Capture Function
5980
+ // ============================================================================
5981
+ /**
5982
+ * Captures a React Flow container element as an SVG image and returns data URL
5983
+ * @param containerElement - The React Flow container DOM element
5984
+ * @returns Promise resolving to SVG data URL string, or null if capture fails
5985
+ */
5986
+ async function captureCanvasThumbnail(containerElement) {
5987
+ if (!containerElement) {
5988
+ console.warn("[flowThumbnail] Container element is null");
5989
+ return null;
5990
+ }
5991
+ try {
5992
+ // Find the React Flow viewport element
5993
+ const reactFlowViewport = containerElement.querySelector(".react-flow__viewport");
5994
+ if (!reactFlowViewport) {
5995
+ console.warn("[flowThumbnail] React Flow viewport not found");
5996
+ return null;
5997
+ }
5998
+ // Parse viewport transform
5999
+ const viewportStyle = window.getComputedStyle(reactFlowViewport);
6000
+ const viewportTransform = viewportStyle.transform || viewportStyle.getPropertyValue("transform");
6001
+ const transform = parseViewportTransform(viewportTransform);
6002
+ // Calculate visible bounds
6003
+ const viewportRect = reactFlowViewport.getBoundingClientRect();
6004
+ const visibleBounds = calculateVisibleBounds(viewportRect, transform);
6005
+ // Extract edges and nodes
6006
+ const { edges, bounds: edgeBounds } = extractEdgePaths(reactFlowViewport, visibleBounds);
6007
+ const { nodes, bounds: nodeBounds } = extractNodes(reactFlowViewport, visibleBounds);
6008
+ // Calculate overall bounding box
6009
+ // Handle case where one or both bounds might be Infinity (no content)
6010
+ let minX = Infinity;
6011
+ let minY = Infinity;
6012
+ let maxX = -Infinity;
6013
+ let maxY = -Infinity;
6014
+ if (edgeBounds.minX !== Infinity) {
6015
+ minX = Math.min(minX, edgeBounds.minX);
6016
+ minY = Math.min(minY, edgeBounds.minY);
6017
+ maxX = Math.max(maxX, edgeBounds.maxX);
6018
+ maxY = Math.max(maxY, edgeBounds.maxY);
6019
+ }
6020
+ if (nodeBounds.minX !== Infinity) {
6021
+ minX = Math.min(minX, nodeBounds.minX);
6022
+ minY = Math.min(minY, nodeBounds.minY);
6023
+ maxX = Math.max(maxX, nodeBounds.maxX);
6024
+ maxY = Math.max(maxY, nodeBounds.maxY);
6025
+ }
6026
+ // If no visible content, use the visible viewport bounds
6027
+ if (minX === Infinity || (nodes.length === 0 && edges.length === 0)) {
6028
+ minX = visibleBounds.minX;
6029
+ minY = visibleBounds.minY;
6030
+ maxX = visibleBounds.maxX;
6031
+ maxY = visibleBounds.maxY;
6032
+ }
6033
+ // Use the visible viewport bounds exactly (what the user sees)
6034
+ const contentMinX = visibleBounds.minX;
6035
+ const contentMinY = visibleBounds.minY;
6036
+ const contentMaxX = visibleBounds.maxX;
6037
+ const contentMaxY = visibleBounds.maxY;
6038
+ const contentWidth = contentMaxX - contentMinX;
6039
+ const contentHeight = contentMaxY - contentMinY;
6040
+ // Create SVG
6041
+ const { svg, group } = createSVGElement(contentWidth, contentHeight);
6042
+ // Render content
6043
+ renderContentToSVG(group, nodes, edges, -contentMinX, -contentMinY);
6044
+ // Serialize SVG to string
6045
+ const serializer = new XMLSerializer();
6046
+ const svgString = serializer.serializeToString(svg);
6047
+ // Return SVG data URL
6048
+ const svgDataUrl = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgString)))}`;
6049
+ return svgDataUrl;
6050
+ }
6051
+ catch (error) {
6052
+ console.error("[flowThumbnail] Failed to capture thumbnail:", error);
6053
+ return null;
6054
+ }
6055
+ }
6056
+ /**
6057
+ * Captures a React Flow container element as an SVG image and downloads it
6058
+ * @param containerElement - The React Flow container DOM element
6059
+ * @returns Promise resolving to true if successful, false otherwise
6060
+ */
6061
+ async function downloadCanvasThumbnail(containerElement) {
6062
+ const svgDataUrl = await captureCanvasThumbnail(containerElement);
6063
+ if (!svgDataUrl) {
6064
+ return false;
6065
+ }
6066
+ // Create blob and download
6067
+ const base64Data = svgDataUrl.split(",")[1];
6068
+ const svgString = atob(base64Data);
6069
+ const blob = new Blob([svgString], { type: "image/svg+xml" });
6070
+ const url = URL.createObjectURL(blob);
6071
+ const link = document.createElement("a");
6072
+ link.href = url;
6073
+ link.download = `flow-thumbnail-${Date.now()}.svg`;
6074
+ document.body.appendChild(link);
6075
+ link.click();
6076
+ document.body.removeChild(link);
6077
+ URL.revokeObjectURL(url);
6078
+ return true;
6079
+ }
6080
+
5491
6081
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
5492
6082
  const { wb, runner, registry, selectedNodeId, runAutoLayout } = useWorkbenchContext();
5493
6083
  const [transportStatus, setTransportStatus] = useState({
@@ -5576,6 +6166,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5576
6166
  return defaultExamples;
5577
6167
  }, [overrides, defaultExamples]);
5578
6168
  const canvasRef = useRef(null);
6169
+ const canvasContainerRef = useRef(null);
5579
6170
  const uploadInputRef = useRef(null);
5580
6171
  const [registryReady, setRegistryReady] = useState(() => {
5581
6172
  // For local backends, registry is always ready
@@ -5937,7 +6528,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5937
6528
  // Normal change when not running
5938
6529
  onEngineChange?.(kind);
5939
6530
  }
5940
- }, children: [jsx("option", { value: "", children: "Select Engine\u2026" }), jsx("option", { value: "push", children: "Push" }), jsx("option", { value: "batched", children: "Batched" }), jsx("option", { value: "pull", children: "Pull" }), jsx("option", { value: "hybrid", children: "Hybrid" }), jsx("option", { value: "step", children: "Step" })] }), engineKind === "step" && (jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => runner.step(), disabled: !isGraphRunning, title: "Step", children: jsx(PlayPauseIcon, { size: 24 }) })), engineKind === "batched" && (jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => runner.flush(), disabled: !isGraphRunning, title: "Flush", children: jsx(LightningIcon, { size: 24 }) })), renderStartStopButton(), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: runAutoLayout, children: jsx(TreeStructureIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: jsx(CornersOutIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: download$1, children: jsx(DownloadIcon, { size: 24 }) }), jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: triggerUpload, children: jsx(UploadIcon, { size: 24 }) }), jsxs("label", { className: "flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx(BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxs("label", { className: "flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx(ListBulletsIcon, { size: 24, weight: showValues ? "fill" : undefined })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, contextPanel: overrides?.contextPanel })] })] }));
6531
+ }, children: [jsx("option", { value: "", children: "Select Engine\u2026" }), jsx("option", { value: "push", children: "Push" }), jsx("option", { value: "batched", children: "Batched" }), jsx("option", { value: "pull", children: "Pull" }), jsx("option", { value: "hybrid", children: "Hybrid" }), jsx("option", { value: "step", children: "Step" })] }), engineKind === "step" && (jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => runner.step(), disabled: !isGraphRunning, title: "Step", children: jsx(PlayPauseIcon, { size: 24 }) })), engineKind === "batched" && (jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => runner.flush(), disabled: !isGraphRunning, title: "Flush", children: jsx(LightningIcon, { size: 24 }) })), renderStartStopButton(), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: runAutoLayout, children: jsx(TreeStructureIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: jsx(CornersOutIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: download$1, children: jsx(DownloadIcon, { size: 24 }) }), jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: triggerUpload, children: jsx(UploadIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: async () => {
6532
+ await downloadCanvasThumbnail(canvasContainerRef.current);
6533
+ }, title: "Download Flow Thumbnail (SVG)", children: jsx(ImageIcon, { size: 24 }) }), jsxs("label", { className: "flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx(BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxs("label", { className: "flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx(ListBulletsIcon, { size: 24, weight: showValues ? "fill" : undefined })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", ref: canvasContainerRef, children: jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, contextPanel: overrides?.contextPanel })] })] }));
5941
6534
  }
5942
6535
  function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, backendOptions, overrides, onInit, onChange, }) {
5943
6536
  const [registry, setRegistry] = useState(createSimpleGraphRegistry());
@@ -6012,5 +6605,5 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
6012
6605
  return (jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, uiVersion: uiVersion, children: jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: onBackendKindChangeWithDispose, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, overrides: overrides, onInit: onInit, onChange: onChange }) }));
6013
6606
  }
6014
6607
 
6015
- export { AbstractWorkbench, CLIWorkbench, DefaultNode, DefaultNodeContent, DefaultNodeHeader, DefaultUIExtensionRegistry, InMemoryWorkbench, Inspector, LocalGraphRunner, NodeHandles, RemoteGraphRunner, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, computeEffectiveHandles, countVisibleHandles, createCopyHandler, createDefaultContextMenuHandlers, createHandleBounds, createHandleLayout, createNodeContextMenuHandlers, createNodeCopyHandler, createSelectionContextMenuHandlers, download, estimateNodeSize, excludeViewportFromUIState, formatDataUrlAsLabel, formatDeclaredTypeSignature, getBakeableOutputs, getHandleBoundsX, getHandleBoundsY, getHandleClassName, getHandleLayoutY, getNodeBorderClassNames, isValidViewport, layoutNode, mergeUIState, preformatValueForDisplay, prettyHandle, resolveOutputDisplay, summarizeDeep, toReactFlow, upload, useQueryParamBoolean, useQueryParamString, useThrottledValue, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
6608
+ export { AbstractWorkbench, CLIWorkbench, DefaultNode, DefaultNodeContent, DefaultNodeHeader, DefaultUIExtensionRegistry, InMemoryWorkbench, Inspector, LocalGraphRunner, NodeHandles, RemoteGraphRunner, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, captureCanvasThumbnail, computeEffectiveHandles, countVisibleHandles, createCopyHandler, createDefaultContextMenuHandlers, createHandleBounds, createHandleLayout, createNodeContextMenuHandlers, createNodeCopyHandler, createSelectionContextMenuHandlers, download, downloadCanvasThumbnail, estimateNodeSize, excludeViewportFromUIState, formatDataUrlAsLabel, formatDeclaredTypeSignature, getBakeableOutputs, getHandleBoundsX, getHandleBoundsY, getHandleClassName, getHandleLayoutY, getNodeBorderClassNames, isValidViewport, layoutNode, mergeUIState, preformatValueForDisplay, prettyHandle, resolveOutputDisplay, summarizeDeep, toReactFlow, upload, useQueryParamBoolean, useQueryParamString, useThrottledValue, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
6016
6609
  //# sourceMappingURL=index.js.map