@bian-womp/spark-workbench 0.2.85 → 0.2.86

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() {
@@ -4451,7 +4451,7 @@ function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInv
4451
4451
  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
4452
  maxHeight: NODE_HEADER_HEIGHT_PX,
4453
4453
  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) => {
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: `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
4455
  e.stopPropagation();
4456
4456
  handleInvalidate();
4457
4457
  }, children: jsx(ArrowClockwiseIcon, { size: 10 }) }), right, validation.issues && validation.issues.length > 0 && (jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
@@ -4462,7 +4462,7 @@ function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInv
4462
4462
  }
4463
4463
 
4464
4464
  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: {
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: {
4466
4466
  top: (y ?? 0) - 8,
4467
4467
  ...(kind === "input"
4468
4468
  ? { right: "50%" }
@@ -4574,7 +4574,7 @@ function DefaultNodeContent({ data, isConnectable, }) {
4574
4574
  isDefault: false,
4575
4575
  };
4576
4576
  })();
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 }))] }));
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", "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
4578
  } })] }));
4579
4579
  }
4580
4580
 
@@ -5488,6 +5488,601 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
5488
5488
  (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
5489
  });
5490
5490
 
5491
+ /**
5492
+ * Flow thumbnail capture utility
5493
+ * Captures React Flow canvas as SVG image
5494
+ */
5495
+ // ============================================================================
5496
+ // Utility Functions
5497
+ // ============================================================================
5498
+ /**
5499
+ * Parses CSS transform string to extract translate and scale values
5500
+ */
5501
+ function parseViewportTransform(transform) {
5502
+ let translateX = 0;
5503
+ let translateY = 0;
5504
+ let scale = 1;
5505
+ if (transform && transform !== "none") {
5506
+ // Try translate() scale() format first
5507
+ const translateMatch = transform.match(/translate\(([^,]+)px,\s*([^)]+)px\)/);
5508
+ const scaleMatch = transform.match(/scale\(([^)]+)\)/);
5509
+ if (translateMatch) {
5510
+ translateX = parseFloat(translateMatch[1]);
5511
+ translateY = parseFloat(translateMatch[2]);
5512
+ }
5513
+ if (scaleMatch) {
5514
+ scale = parseFloat(scaleMatch[1]);
5515
+ }
5516
+ // Fallback to matrix format
5517
+ if (!translateMatch) {
5518
+ const matrixMatch = transform.match(/matrix\(([^)]+)\)/);
5519
+ if (matrixMatch) {
5520
+ const values = matrixMatch[1]
5521
+ .split(",")
5522
+ .map((v) => parseFloat(v.trim()));
5523
+ if (values.length >= 6) {
5524
+ scale = Math.sqrt(values[0] * values[0] + values[1] * values[1]);
5525
+ translateX = values[4];
5526
+ translateY = values[5];
5527
+ }
5528
+ }
5529
+ }
5530
+ }
5531
+ return { translateX, translateY, scale };
5532
+ }
5533
+ /**
5534
+ * Calculates visible viewport bounds in flow coordinates
5535
+ */
5536
+ function calculateVisibleBounds(viewportRect, transform) {
5537
+ const { translateX, translateY, scale } = transform;
5538
+ // Guard against division by zero
5539
+ if (scale === 0) {
5540
+ console.warn("[flowThumbnail] Viewport scale is 0, using default bounds");
5541
+ return {
5542
+ minX: -translateX,
5543
+ minY: -translateY,
5544
+ maxX: viewportRect.width - translateX,
5545
+ maxY: viewportRect.height - translateY,
5546
+ };
5547
+ }
5548
+ // Screen to flow: (screenX - translateX) / scale
5549
+ return {
5550
+ minX: (0 - translateX) / scale,
5551
+ minY: (0 - translateY) / scale,
5552
+ maxX: (viewportRect.width - translateX) / scale,
5553
+ maxY: (viewportRect.height - translateY) / scale,
5554
+ };
5555
+ }
5556
+ /**
5557
+ * Parses border radius string (px or rem) to pixels
5558
+ */
5559
+ function parseBorderRadius(borderRadiusStr) {
5560
+ if (borderRadiusStr === "0px") {
5561
+ return 8; // default
5562
+ }
5563
+ const match = borderRadiusStr.match(/([\d.]+)(px|rem)/);
5564
+ if (match) {
5565
+ const value = parseFloat(match[1]);
5566
+ const unit = match[2];
5567
+ // Convert rem to px (assuming 16px base) or use px directly
5568
+ return unit === "rem" ? value * 16 : value;
5569
+ }
5570
+ // Try direct parseFloat as fallback
5571
+ const parsed = parseFloat(borderRadiusStr);
5572
+ return isNaN(parsed) ? 8 : parsed;
5573
+ }
5574
+ /**
5575
+ * Extracts stroke color from element, with fallback
5576
+ */
5577
+ function extractStrokeColor(element) {
5578
+ if (element instanceof SVGPathElement) {
5579
+ return (element.getAttribute("stroke") ||
5580
+ window.getComputedStyle(element).stroke ||
5581
+ "#b1b1b7");
5582
+ }
5583
+ const style = window.getComputedStyle(element);
5584
+ return (style.borderColor || style.borderTopColor || "#6b7280" // gray-500 default
5585
+ );
5586
+ }
5587
+ /**
5588
+ * Extracts stroke/border width from element, ensuring minimum value
5589
+ */
5590
+ function extractStrokeWidth(element, minWidth = 1) {
5591
+ if (element instanceof SVGPathElement) {
5592
+ const width = parseFloat(element.getAttribute("stroke-width") || "0") ||
5593
+ parseFloat(window.getComputedStyle(element).strokeWidth || "2");
5594
+ return width > 0 ? width : minWidth;
5595
+ }
5596
+ const style = window.getComputedStyle(element);
5597
+ const width = parseFloat(style.borderWidth || style.borderTopWidth || "0");
5598
+ return width > 0 ? width : minWidth;
5599
+ }
5600
+ /**
5601
+ * Checks if a rectangle intersects with visible bounds
5602
+ */
5603
+ function isRectVisible(x, y, width, height, bounds) {
5604
+ return (x + width >= bounds.minX &&
5605
+ x <= bounds.maxX &&
5606
+ y + height >= bounds.minY &&
5607
+ y <= bounds.maxY);
5608
+ }
5609
+ /**
5610
+ * Parses path data to get bounding box
5611
+ * Handles M (moveTo), L (lineTo), C (cubic Bezier), Q (quadratic Bezier), and H/V (horizontal/vertical) commands
5612
+ */
5613
+ function getPathBounds(pathData) {
5614
+ let minX = Infinity;
5615
+ let minY = Infinity;
5616
+ let maxX = -Infinity;
5617
+ let maxY = -Infinity;
5618
+ // Match coordinates from various path commands: M, L, C, Q, T, S, H, V
5619
+ // Pattern matches: command letter followed by coordinate pairs
5620
+ const coordPattern = /[MLCQTSHV](-?\d+\.?\d*),(-?\d+\.?\d*)/g;
5621
+ const coords = pathData.match(coordPattern);
5622
+ if (coords) {
5623
+ coords.forEach((coord) => {
5624
+ const match = coord.match(/(-?\d+\.?\d*),(-?\d+\.?\d*)/);
5625
+ if (match) {
5626
+ const x = parseFloat(match[1]);
5627
+ const y = parseFloat(match[2]);
5628
+ if (!isNaN(x) && !isNaN(y)) {
5629
+ minX = Math.min(minX, x);
5630
+ minY = Math.min(minY, y);
5631
+ maxX = Math.max(maxX, x);
5632
+ maxY = Math.max(maxY, y);
5633
+ }
5634
+ }
5635
+ });
5636
+ }
5637
+ return { minX, minY, maxX, maxY };
5638
+ }
5639
+ // ============================================================================
5640
+ // Edge Extraction
5641
+ // ============================================================================
5642
+ /**
5643
+ * Extracts visible edge paths from React Flow viewport
5644
+ */
5645
+ function extractEdgePaths(viewport, visibleBounds) {
5646
+ const edges = [];
5647
+ let minX = Infinity;
5648
+ let minY = Infinity;
5649
+ let maxX = -Infinity;
5650
+ let maxY = -Infinity;
5651
+ const edgePathElements = viewport.querySelectorAll(".react-flow__edge-path");
5652
+ edgePathElements.forEach((pathEl) => {
5653
+ const pathData = pathEl.getAttribute("d");
5654
+ if (!pathData)
5655
+ return;
5656
+ const pathBounds = getPathBounds(pathData);
5657
+ // Only include edge if it intersects with visible viewport
5658
+ if (pathBounds.maxX >= visibleBounds.minX &&
5659
+ pathBounds.minX <= visibleBounds.maxX &&
5660
+ pathBounds.maxY >= visibleBounds.minY &&
5661
+ pathBounds.minY <= visibleBounds.maxY) {
5662
+ edges.push({
5663
+ d: pathData,
5664
+ stroke: extractStrokeColor(pathEl),
5665
+ strokeWidth: extractStrokeWidth(pathEl, 2),
5666
+ });
5667
+ // Update bounding box
5668
+ minX = Math.min(minX, pathBounds.minX);
5669
+ minY = Math.min(minY, pathBounds.minY);
5670
+ maxX = Math.max(maxX, pathBounds.maxX);
5671
+ maxY = Math.max(maxY, pathBounds.maxY);
5672
+ }
5673
+ });
5674
+ return { edges, bounds: { minX, minY, maxX, maxY } };
5675
+ }
5676
+ // ============================================================================
5677
+ // Node Extraction
5678
+ // ============================================================================
5679
+ /**
5680
+ * Extracts node position from transform style
5681
+ */
5682
+ function extractNodePosition(nodeEl) {
5683
+ const transformStyle = nodeEl.style.transform || "";
5684
+ const translateMatch = transformStyle.match(/translate\(([^,]+)px,\s*([^)]+)px\)/);
5685
+ if (translateMatch) {
5686
+ return {
5687
+ x: parseFloat(translateMatch[1]),
5688
+ y: parseFloat(translateMatch[2]),
5689
+ };
5690
+ }
5691
+ return { x: 0, y: 0 };
5692
+ }
5693
+ /**
5694
+ * Extracts node dimensions from inline styles
5695
+ */
5696
+ function extractNodeDimensions(nodeEl) {
5697
+ const widthMatch = nodeEl.style.width?.match(/(\d+)px/);
5698
+ const heightMatch = nodeEl.style.height?.match(/(\d+)px/);
5699
+ return {
5700
+ width: widthMatch ? parseFloat(widthMatch[1]) : 150,
5701
+ height: heightMatch ? parseFloat(heightMatch[1]) : 40,
5702
+ };
5703
+ }
5704
+ /**
5705
+ * Extracts node styles (colors, border, radius) from computed styles
5706
+ */
5707
+ function extractNodeStyles(nodeContent) {
5708
+ const computedStyle = window.getComputedStyle(nodeContent);
5709
+ // Use gray background for nodes in thumbnail
5710
+ const fill = "#f3f4f6"; // gray-100 equivalent
5711
+ const stroke = extractStrokeColor(nodeContent);
5712
+ const strokeWidth = extractStrokeWidth(nodeContent, 1);
5713
+ const borderRadiusStr = computedStyle.borderRadius || "8px";
5714
+ const rx = parseBorderRadius(borderRadiusStr);
5715
+ const ry = rx; // Use same radius for both x and y
5716
+ return { fill, stroke, strokeWidth, rx, ry };
5717
+ }
5718
+ /**
5719
+ * Determines if a handle is a source (output) or target (input)
5720
+ */
5721
+ function isHandleSource(handleEl) {
5722
+ return (handleEl.classList.contains("react-flow__handle-right") ||
5723
+ handleEl.classList.contains("react-flow__handle-source"));
5724
+ }
5725
+ /**
5726
+ * Extracts handle position and calculates absolute coordinates
5727
+ */
5728
+ function extractHandlePosition(handleEl, nodeX, nodeY, nodeWidth, isSource) {
5729
+ const handleStyle = window.getComputedStyle(handleEl);
5730
+ const handleTop = parseFloat(handleStyle.top || "0");
5731
+ const handleLeft = handleStyle.left;
5732
+ const handleRight = handleStyle.right;
5733
+ const handleY = nodeY + handleTop;
5734
+ let handleX;
5735
+ if (isSource) {
5736
+ // Source handles are on the right edge
5737
+ if (handleRight !== "auto" && handleRight !== "") {
5738
+ const rightValue = parseFloat(handleRight) || 0;
5739
+ handleX = nodeX + nodeWidth + rightValue;
5740
+ }
5741
+ else {
5742
+ handleX = nodeX + nodeWidth;
5743
+ }
5744
+ }
5745
+ else {
5746
+ // Target handles are on the left edge
5747
+ if (handleLeft !== "auto" && handleLeft !== "") {
5748
+ const leftValue = parseFloat(handleLeft) || 0;
5749
+ handleX = nodeX + leftValue;
5750
+ }
5751
+ else {
5752
+ handleX = nodeX;
5753
+ }
5754
+ }
5755
+ return { x: handleX, y: handleY };
5756
+ }
5757
+ /**
5758
+ * Extracts handles from a node element
5759
+ */
5760
+ function extractNodeHandles(nodeEl, nodeX, nodeY, nodeWidth) {
5761
+ const handles = [];
5762
+ const handleElements = nodeEl.querySelectorAll(".react-flow__handle");
5763
+ handleElements.forEach((handleEl) => {
5764
+ const handleStyle = window.getComputedStyle(handleEl);
5765
+ const handleWidth = parseFloat(handleStyle.width || "12");
5766
+ const handleHeight = parseFloat(handleStyle.height || "12");
5767
+ const isSource = isHandleSource(handleEl);
5768
+ const position = extractHandlePosition(handleEl, nodeX, nodeY, nodeWidth, isSource);
5769
+ handles.push({
5770
+ x: position.x,
5771
+ y: position.y,
5772
+ width: handleWidth,
5773
+ height: handleHeight,
5774
+ fill: handleStyle.backgroundColor || "rgba(255, 255, 255, 0.5)",
5775
+ stroke: extractStrokeColor(handleEl),
5776
+ strokeWidth: extractStrokeWidth(handleEl, 1),
5777
+ type: isSource ? "source" : "target",
5778
+ });
5779
+ });
5780
+ return handles;
5781
+ }
5782
+ /**
5783
+ * Extracts node title text and position
5784
+ */
5785
+ function extractNodeTitle(nodeEl, nodeX, nodeY) {
5786
+ const titleElement = nodeEl.querySelector(".react-flow__node-title");
5787
+ if (!titleElement)
5788
+ return undefined;
5789
+ const titleText = titleElement.textContent || titleElement.innerText || "";
5790
+ if (!titleText.trim())
5791
+ return undefined;
5792
+ const titleStyle = window.getComputedStyle(titleElement);
5793
+ const titleRect = titleElement.getBoundingClientRect();
5794
+ const nodeRect = nodeEl.getBoundingClientRect();
5795
+ // Calculate title position relative to node (in flow coordinates)
5796
+ const titleRelativeX = titleRect.left - nodeRect.left;
5797
+ const titleRelativeY = titleRect.top - nodeRect.top;
5798
+ // Get font properties
5799
+ const fontSize = parseFloat(titleStyle.fontSize || "14");
5800
+ const fill = titleStyle.color || "#374151"; // gray-700 default
5801
+ const fontWeight = titleStyle.fontWeight || "600"; // bold default
5802
+ // Calculate text position (SVG text uses baseline)
5803
+ const lineHeight = parseFloat(titleStyle.lineHeight || String(fontSize * 1.2));
5804
+ const textBaselineOffset = lineHeight * 0.8; // Approximate baseline offset
5805
+ return {
5806
+ text: titleText.trim(),
5807
+ x: nodeX + titleRelativeX,
5808
+ y: nodeY + titleRelativeY + textBaselineOffset,
5809
+ fontSize,
5810
+ fill,
5811
+ fontWeight,
5812
+ };
5813
+ }
5814
+ /**
5815
+ * Extracts visible nodes from React Flow viewport
5816
+ */
5817
+ function extractNodes(viewport, visibleBounds) {
5818
+ const nodes = [];
5819
+ let minX = Infinity;
5820
+ let minY = Infinity;
5821
+ let maxX = -Infinity;
5822
+ let maxY = -Infinity;
5823
+ const nodeElements = viewport.querySelectorAll(".react-flow__node");
5824
+ nodeElements.forEach((nodeEl) => {
5825
+ const position = extractNodePosition(nodeEl);
5826
+ const dimensions = extractNodeDimensions(nodeEl);
5827
+ // Get the actual node content div (first child)
5828
+ const nodeContent = nodeEl.firstElementChild;
5829
+ if (!nodeContent)
5830
+ return;
5831
+ const styles = extractNodeStyles(nodeContent);
5832
+ const handles = extractNodeHandles(nodeEl, position.x, position.y, dimensions.width);
5833
+ const title = extractNodeTitle(nodeEl, position.x, position.y);
5834
+ // Only include node if it's within visible viewport
5835
+ if (isRectVisible(position.x, position.y, dimensions.width, dimensions.height, visibleBounds)) {
5836
+ nodes.push({
5837
+ x: position.x,
5838
+ y: position.y,
5839
+ width: dimensions.width,
5840
+ height: dimensions.height,
5841
+ ...styles,
5842
+ handles,
5843
+ title,
5844
+ });
5845
+ // Update bounding box
5846
+ minX = Math.min(minX, position.x);
5847
+ minY = Math.min(minY, position.y);
5848
+ maxX = Math.max(maxX, position.x + dimensions.width);
5849
+ maxY = Math.max(maxY, position.y + dimensions.height);
5850
+ // Update bounding box to include handles
5851
+ handles.forEach((handle) => {
5852
+ minX = Math.min(minX, handle.x);
5853
+ minY = Math.min(minY, handle.y);
5854
+ maxX = Math.max(maxX, handle.x + handle.width);
5855
+ maxY = Math.max(maxY, handle.y + handle.height);
5856
+ });
5857
+ }
5858
+ });
5859
+ return { nodes, bounds: { minX, minY, maxX, maxY } };
5860
+ }
5861
+ // ============================================================================
5862
+ // SVG Rendering
5863
+ // ============================================================================
5864
+ /**
5865
+ * Creates SVG element with dot pattern background (matching React Flow)
5866
+ */
5867
+ function createSVGElement(width, height) {
5868
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
5869
+ svg.setAttribute("width", String(width));
5870
+ svg.setAttribute("height", String(height));
5871
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
5872
+ // Create defs section for patterns
5873
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
5874
+ // Create dot pattern (matching React Flow's BackgroundVariant.Dots)
5875
+ // React Flow uses gap={12} and size={1} by default
5876
+ const pattern = document.createElementNS("http://www.w3.org/2000/svg", "pattern");
5877
+ pattern.setAttribute("id", "dot-pattern");
5878
+ pattern.setAttribute("x", "0");
5879
+ pattern.setAttribute("y", "0");
5880
+ pattern.setAttribute("width", "24"); // gap between dots (matching React Flow default)
5881
+ pattern.setAttribute("height", "24");
5882
+ pattern.setAttribute("patternUnits", "userSpaceOnUse");
5883
+ // Create a circle for the dot (centered in the pattern cell)
5884
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
5885
+ circle.setAttribute("cx", "12"); // Center of 24x24 pattern cell
5886
+ circle.setAttribute("cy", "12");
5887
+ circle.setAttribute("r", "1"); // dot radius = 1 (matching React Flow size={1})
5888
+ circle.setAttribute("fill", "#f1f1f1"); // gray color matching React Flow default
5889
+ pattern.appendChild(circle);
5890
+ defs.appendChild(pattern);
5891
+ svg.appendChild(defs);
5892
+ // Create background rectangle with white base
5893
+ const bgRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
5894
+ bgRect.setAttribute("width", String(width));
5895
+ bgRect.setAttribute("height", String(height));
5896
+ bgRect.setAttribute("fill", "#ffffff"); // Base background color
5897
+ svg.appendChild(bgRect);
5898
+ // Create pattern overlay rectangle
5899
+ const patternRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
5900
+ patternRect.setAttribute("width", String(width));
5901
+ patternRect.setAttribute("height", String(height));
5902
+ patternRect.setAttribute("fill", "url(#dot-pattern)");
5903
+ svg.appendChild(patternRect);
5904
+ // Create group with transform
5905
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
5906
+ svg.appendChild(group);
5907
+ return { svg, group };
5908
+ }
5909
+ /**
5910
+ * Renders a node rectangle to SVG group
5911
+ */
5912
+ function renderNodeRect(group, node) {
5913
+ const rectEl = document.createElementNS("http://www.w3.org/2000/svg", "rect");
5914
+ rectEl.setAttribute("x", String(node.x));
5915
+ rectEl.setAttribute("y", String(node.y));
5916
+ rectEl.setAttribute("width", String(node.width));
5917
+ rectEl.setAttribute("height", String(node.height));
5918
+ rectEl.setAttribute("rx", String(node.rx));
5919
+ rectEl.setAttribute("ry", String(node.ry));
5920
+ rectEl.setAttribute("fill", node.fill);
5921
+ rectEl.setAttribute("stroke", node.stroke);
5922
+ rectEl.setAttribute("stroke-width", String(node.strokeWidth));
5923
+ group.appendChild(rectEl);
5924
+ }
5925
+ /**
5926
+ * Renders a handle to SVG group
5927
+ */
5928
+ function renderHandle(group, handle) {
5929
+ const handleEl = document.createElementNS("http://www.w3.org/2000/svg", "rect");
5930
+ // Handles are centered on edges
5931
+ handleEl.setAttribute("x", String(handle.x - handle.width / 2));
5932
+ handleEl.setAttribute("y", String(handle.y - handle.height / 2));
5933
+ handleEl.setAttribute("width", String(handle.width));
5934
+ handleEl.setAttribute("height", String(handle.height));
5935
+ handleEl.setAttribute("rx", String(handle.width / 2)); // Make handles circular/rounded
5936
+ handleEl.setAttribute("ry", String(handle.height / 2));
5937
+ handleEl.setAttribute("fill", handle.fill);
5938
+ handleEl.setAttribute("stroke", handle.stroke);
5939
+ handleEl.setAttribute("stroke-width", String(handle.strokeWidth));
5940
+ group.appendChild(handleEl);
5941
+ }
5942
+ /**
5943
+ * Renders node title text to SVG group
5944
+ */
5945
+ function renderNodeTitle(group, title) {
5946
+ const textEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
5947
+ textEl.setAttribute("x", String(title.x));
5948
+ textEl.setAttribute("y", String(title.y));
5949
+ textEl.setAttribute("font-size", String(title.fontSize));
5950
+ textEl.setAttribute("fill", title.fill);
5951
+ textEl.setAttribute("font-weight", title.fontWeight);
5952
+ textEl.setAttribute("font-family", "system-ui, -apple-system, sans-serif");
5953
+ textEl.textContent = title.text;
5954
+ group.appendChild(textEl);
5955
+ }
5956
+ /**
5957
+ * Renders an edge path to SVG group
5958
+ */
5959
+ function renderEdgePath(group, edge) {
5960
+ const pathEl = document.createElementNS("http://www.w3.org/2000/svg", "path");
5961
+ pathEl.setAttribute("d", edge.d);
5962
+ pathEl.setAttribute("fill", "none");
5963
+ pathEl.setAttribute("stroke", edge.stroke);
5964
+ pathEl.setAttribute("stroke-width", String(edge.strokeWidth));
5965
+ group.appendChild(pathEl);
5966
+ }
5967
+ /**
5968
+ * Renders all nodes, handles, titles, and edges to SVG group
5969
+ */
5970
+ function renderContentToSVG(group, nodes, edges, transformX, transformY) {
5971
+ group.setAttribute("transform", `translate(${transformX}, ${transformY})`);
5972
+ // Render nodes
5973
+ nodes.forEach((node) => {
5974
+ renderNodeRect(group, node);
5975
+ node.handles.forEach((handle) => renderHandle(group, handle));
5976
+ if (node.title) {
5977
+ renderNodeTitle(group, node.title);
5978
+ }
5979
+ });
5980
+ // Render edges
5981
+ edges.forEach((edge) => renderEdgePath(group, edge));
5982
+ }
5983
+ // ============================================================================
5984
+ // Main Capture Function
5985
+ // ============================================================================
5986
+ /**
5987
+ * Captures a React Flow container element as an SVG image and returns data URL
5988
+ * @param containerElement - The React Flow container DOM element
5989
+ * @returns Promise resolving to SVG data URL string, or null if capture fails
5990
+ */
5991
+ async function captureCanvasThumbnail(containerElement) {
5992
+ if (!containerElement) {
5993
+ console.warn("[flowThumbnail] Container element is null");
5994
+ return null;
5995
+ }
5996
+ try {
5997
+ // Find the React Flow viewport element
5998
+ const reactFlowViewport = containerElement.querySelector(".react-flow__viewport");
5999
+ if (!reactFlowViewport) {
6000
+ console.warn("[flowThumbnail] React Flow viewport not found");
6001
+ return null;
6002
+ }
6003
+ // Parse viewport transform
6004
+ const viewportStyle = window.getComputedStyle(reactFlowViewport);
6005
+ const viewportTransform = viewportStyle.transform || viewportStyle.getPropertyValue("transform");
6006
+ const transform = parseViewportTransform(viewportTransform);
6007
+ // Calculate visible bounds
6008
+ const viewportRect = reactFlowViewport.getBoundingClientRect();
6009
+ const visibleBounds = calculateVisibleBounds(viewportRect, transform);
6010
+ // Extract edges and nodes
6011
+ const { edges, bounds: edgeBounds } = extractEdgePaths(reactFlowViewport, visibleBounds);
6012
+ const { nodes, bounds: nodeBounds } = extractNodes(reactFlowViewport, visibleBounds);
6013
+ // Calculate overall bounding box
6014
+ // Handle case where one or both bounds might be Infinity (no content)
6015
+ let minX = Infinity;
6016
+ let minY = Infinity;
6017
+ let maxX = -Infinity;
6018
+ let maxY = -Infinity;
6019
+ if (edgeBounds.minX !== Infinity) {
6020
+ minX = Math.min(minX, edgeBounds.minX);
6021
+ minY = Math.min(minY, edgeBounds.minY);
6022
+ maxX = Math.max(maxX, edgeBounds.maxX);
6023
+ maxY = Math.max(maxY, edgeBounds.maxY);
6024
+ }
6025
+ if (nodeBounds.minX !== Infinity) {
6026
+ minX = Math.min(minX, nodeBounds.minX);
6027
+ minY = Math.min(minY, nodeBounds.minY);
6028
+ maxX = Math.max(maxX, nodeBounds.maxX);
6029
+ maxY = Math.max(maxY, nodeBounds.maxY);
6030
+ }
6031
+ // If no visible content, use the visible viewport bounds
6032
+ if (minX === Infinity || (nodes.length === 0 && edges.length === 0)) {
6033
+ minX = visibleBounds.minX;
6034
+ minY = visibleBounds.minY;
6035
+ maxX = visibleBounds.maxX;
6036
+ maxY = visibleBounds.maxY;
6037
+ }
6038
+ // Use the visible viewport bounds exactly (what the user sees)
6039
+ const contentMinX = visibleBounds.minX;
6040
+ const contentMinY = visibleBounds.minY;
6041
+ const contentMaxX = visibleBounds.maxX;
6042
+ const contentMaxY = visibleBounds.maxY;
6043
+ const contentWidth = contentMaxX - contentMinX;
6044
+ const contentHeight = contentMaxY - contentMinY;
6045
+ // Create SVG
6046
+ const { svg, group } = createSVGElement(contentWidth, contentHeight);
6047
+ // Render content
6048
+ renderContentToSVG(group, nodes, edges, -contentMinX, -contentMinY);
6049
+ // Serialize SVG to string
6050
+ const serializer = new XMLSerializer();
6051
+ const svgString = serializer.serializeToString(svg);
6052
+ // Return SVG data URL
6053
+ const svgDataUrl = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgString)))}`;
6054
+ return svgDataUrl;
6055
+ }
6056
+ catch (error) {
6057
+ console.error("[flowThumbnail] Failed to capture thumbnail:", error);
6058
+ return null;
6059
+ }
6060
+ }
6061
+ /**
6062
+ * Captures a React Flow container element as an SVG image and downloads it
6063
+ * @param containerElement - The React Flow container DOM element
6064
+ * @returns Promise resolving to true if successful, false otherwise
6065
+ */
6066
+ async function downloadCanvasThumbnail(containerElement) {
6067
+ const svgDataUrl = await captureCanvasThumbnail(containerElement);
6068
+ if (!svgDataUrl) {
6069
+ return false;
6070
+ }
6071
+ // Create blob and download
6072
+ const base64Data = svgDataUrl.split(",")[1];
6073
+ const svgString = atob(base64Data);
6074
+ const blob = new Blob([svgString], { type: "image/svg+xml" });
6075
+ const url = URL.createObjectURL(blob);
6076
+ const link = document.createElement("a");
6077
+ link.href = url;
6078
+ link.download = `flow-thumbnail-${Date.now()}.svg`;
6079
+ document.body.appendChild(link);
6080
+ link.click();
6081
+ document.body.removeChild(link);
6082
+ URL.revokeObjectURL(url);
6083
+ return true;
6084
+ }
6085
+
5491
6086
  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
6087
  const { wb, runner, registry, selectedNodeId, runAutoLayout } = useWorkbenchContext();
5493
6088
  const [transportStatus, setTransportStatus] = useState({
@@ -5576,6 +6171,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5576
6171
  return defaultExamples;
5577
6172
  }, [overrides, defaultExamples]);
5578
6173
  const canvasRef = useRef(null);
6174
+ const canvasContainerRef = useRef(null);
5579
6175
  const uploadInputRef = useRef(null);
5580
6176
  const [registryReady, setRegistryReady] = useState(() => {
5581
6177
  // For local backends, registry is always ready
@@ -5937,7 +6533,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5937
6533
  // Normal change when not running
5938
6534
  onEngineChange?.(kind);
5939
6535
  }
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 })] })] }));
6536
+ }, 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 () => {
6537
+ await downloadCanvasThumbnail(canvasContainerRef.current);
6538
+ }, 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
6539
  }
5942
6540
  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
6541
  const [registry, setRegistry] = useState(createSimpleGraphRegistry());
@@ -6012,5 +6610,5 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
6012
6610
  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
6611
  }
6014
6612
 
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 };
6613
+ 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
6614
  //# sourceMappingURL=index.js.map