@bian-womp/spark-workbench 0.2.90 → 0.2.91

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/cjs/index.cjs CHANGED
@@ -3151,11 +3151,12 @@ function extractEdgePaths(viewport, visibleBounds) {
3151
3151
  if (!pathData)
3152
3152
  return;
3153
3153
  const pathBounds = getPathBounds(pathData);
3154
- // Only include edge if it intersects with visible viewport
3155
- if (pathBounds.maxX >= visibleBounds.minX &&
3156
- pathBounds.minX <= visibleBounds.maxX &&
3157
- pathBounds.maxY >= visibleBounds.minY &&
3158
- pathBounds.minY <= visibleBounds.maxY) {
3154
+ // Only include edge if it intersects with visible viewport (or if ignoring viewport)
3155
+ if (!visibleBounds ||
3156
+ (pathBounds.maxX >= visibleBounds.minX &&
3157
+ pathBounds.minX <= visibleBounds.maxX &&
3158
+ pathBounds.maxY >= visibleBounds.minY &&
3159
+ pathBounds.minY <= visibleBounds.maxY)) {
3159
3160
  edges.push({
3160
3161
  d: pathData,
3161
3162
  stroke: extractStrokeColor(pathEl),
@@ -3201,10 +3202,10 @@ function extractNodeDimensions(nodeEl) {
3201
3202
  /**
3202
3203
  * Extracts node styles (colors, border, radius) from computed styles
3203
3204
  */
3204
- function extractNodeStyles(nodeContent) {
3205
+ function extractNodeStyles(nodeContent, nodeBackgroundColor) {
3205
3206
  const computedStyle = window.getComputedStyle(nodeContent);
3206
- // Use gray background for nodes in thumbnail
3207
- const fill = "#f3f4f6"; // gray-100 equivalent
3207
+ // Use provided background color or default gray background for nodes in thumbnail
3208
+ const fill = nodeBackgroundColor || "#f3f4f6"; // gray-100 equivalent
3208
3209
  const stroke = extractStrokeColor(nodeContent);
3209
3210
  const strokeWidth = extractStrokeWidth(nodeContent, 1);
3210
3211
  const borderRadiusStr = computedStyle.borderRadius || "8px";
@@ -3311,7 +3312,8 @@ function extractNodeTitle(nodeEl, nodeX, nodeY) {
3311
3312
  /**
3312
3313
  * Extracts visible nodes from React Flow viewport
3313
3314
  */
3314
- function extractNodes(viewport, visibleBounds) {
3315
+ function extractNodes(viewport, visibleBounds, options = {}) {
3316
+ const { exportHandles = true, exportNodeTitle = true, nodeBackgroundColor, } = options;
3315
3317
  const nodes = [];
3316
3318
  let minX = Infinity;
3317
3319
  let minY = Infinity;
@@ -3325,11 +3327,16 @@ function extractNodes(viewport, visibleBounds) {
3325
3327
  const nodeContent = nodeEl.firstElementChild;
3326
3328
  if (!nodeContent)
3327
3329
  return;
3328
- const styles = extractNodeStyles(nodeContent);
3329
- const handles = extractNodeHandles(nodeEl, position.x, position.y, dimensions.width);
3330
- const title = extractNodeTitle(nodeEl, position.x, position.y);
3331
- // Only include node if it's within visible viewport
3332
- if (isRectVisible(position.x, position.y, dimensions.width, dimensions.height, visibleBounds)) {
3330
+ const styles = extractNodeStyles(nodeContent, nodeBackgroundColor);
3331
+ const handles = exportHandles
3332
+ ? extractNodeHandles(nodeEl, position.x, position.y, dimensions.width)
3333
+ : [];
3334
+ const title = exportNodeTitle
3335
+ ? extractNodeTitle(nodeEl, position.x, position.y)
3336
+ : undefined;
3337
+ // Only include node if it's within visible viewport (or if ignoring viewport)
3338
+ if (!visibleBounds ||
3339
+ isRectVisible(position.x, position.y, dimensions.width, dimensions.height, visibleBounds)) {
3333
3340
  nodes.push({
3334
3341
  x: position.x,
3335
3342
  y: position.y,
@@ -3361,43 +3368,47 @@ function extractNodes(viewport, visibleBounds) {
3361
3368
  /**
3362
3369
  * Creates SVG element with dot pattern background (matching React Flow)
3363
3370
  */
3364
- function createSVGElement(width, height) {
3371
+ function createSVGElement(width, height, options = {}) {
3372
+ const { exportBackgroundPattern = true, backgroundPatternColor = "#f1f1f1" } = options;
3365
3373
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3366
3374
  svg.setAttribute("width", String(width));
3367
3375
  svg.setAttribute("height", String(height));
3368
3376
  svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
3369
- // Create defs section for patterns
3370
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
3371
- // Create dot pattern (matching React Flow's BackgroundVariant.Dots)
3372
- // React Flow uses gap={12} and size={1} by default
3373
- const pattern = document.createElementNS("http://www.w3.org/2000/svg", "pattern");
3374
- pattern.setAttribute("id", "dot-pattern");
3375
- pattern.setAttribute("x", "0");
3376
- pattern.setAttribute("y", "0");
3377
- pattern.setAttribute("width", "24"); // gap between dots (matching React Flow default)
3378
- pattern.setAttribute("height", "24");
3379
- pattern.setAttribute("patternUnits", "userSpaceOnUse");
3380
- // Create a circle for the dot (centered in the pattern cell)
3381
- const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
3382
- circle.setAttribute("cx", "12"); // Center of 24x24 pattern cell
3383
- circle.setAttribute("cy", "12");
3384
- circle.setAttribute("r", "1"); // dot radius = 1 (matching React Flow size={1})
3385
- circle.setAttribute("fill", "#f1f1f1"); // gray color matching React Flow default
3386
- pattern.appendChild(circle);
3387
- defs.appendChild(pattern);
3388
- svg.appendChild(defs);
3389
3377
  // Create background rectangle with white base
3390
3378
  const bgRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
3391
3379
  bgRect.setAttribute("width", String(width));
3392
3380
  bgRect.setAttribute("height", String(height));
3393
3381
  bgRect.setAttribute("fill", "#ffffff"); // Base background color
3394
3382
  svg.appendChild(bgRect);
3395
- // Create pattern overlay rectangle
3396
- const patternRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
3397
- patternRect.setAttribute("width", String(width));
3398
- patternRect.setAttribute("height", String(height));
3399
- patternRect.setAttribute("fill", "url(#dot-pattern)");
3400
- svg.appendChild(patternRect);
3383
+ // Create dot pattern if enabled
3384
+ if (exportBackgroundPattern) {
3385
+ // Create defs section for patterns
3386
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
3387
+ // Create dot pattern (matching React Flow's BackgroundVariant.Dots)
3388
+ // React Flow uses gap={12} and size={1} by default
3389
+ const pattern = document.createElementNS("http://www.w3.org/2000/svg", "pattern");
3390
+ pattern.setAttribute("id", "dot-pattern");
3391
+ pattern.setAttribute("x", "0");
3392
+ pattern.setAttribute("y", "0");
3393
+ pattern.setAttribute("width", "24"); // gap between dots (matching React Flow default)
3394
+ pattern.setAttribute("height", "24");
3395
+ pattern.setAttribute("patternUnits", "userSpaceOnUse");
3396
+ // Create a circle for the dot (centered in the pattern cell)
3397
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
3398
+ circle.setAttribute("cx", "12"); // Center of 24x24 pattern cell
3399
+ circle.setAttribute("cy", "12");
3400
+ circle.setAttribute("r", "1"); // dot radius = 1 (matching React Flow size={1})
3401
+ circle.setAttribute("fill", backgroundPatternColor);
3402
+ pattern.appendChild(circle);
3403
+ defs.appendChild(pattern);
3404
+ svg.appendChild(defs);
3405
+ // Create pattern overlay rectangle
3406
+ const patternRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
3407
+ patternRect.setAttribute("width", String(width));
3408
+ patternRect.setAttribute("height", String(height));
3409
+ patternRect.setAttribute("fill", "url(#dot-pattern)");
3410
+ svg.appendChild(patternRect);
3411
+ }
3401
3412
  // Create group with transform
3402
3413
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
3403
3414
  svg.appendChild(group);
@@ -3464,13 +3475,16 @@ function renderEdgePath(group, edge) {
3464
3475
  /**
3465
3476
  * Renders all nodes, handles, titles, and edges to SVG group
3466
3477
  */
3467
- function renderContentToSVG(group, nodes, edges, transformX, transformY) {
3478
+ function renderContentToSVG(group, nodes, edges, transformX, transformY, options = {}) {
3479
+ const { exportHandles = true, exportNodeTitle = true } = options;
3468
3480
  group.setAttribute("transform", `translate(${transformX}, ${transformY})`);
3469
3481
  // Render nodes
3470
3482
  nodes.forEach((node) => {
3471
3483
  renderNodeRect(group, node);
3472
- node.handles.forEach((handle) => renderHandle(group, handle));
3473
- if (node.title) {
3484
+ if (exportHandles) {
3485
+ node.handles.forEach((handle) => renderHandle(group, handle));
3486
+ }
3487
+ if (exportNodeTitle && node.title) {
3474
3488
  renderNodeTitle(group, node.title);
3475
3489
  }
3476
3490
  });
@@ -3483,9 +3497,11 @@ function renderContentToSVG(group, nodes, edges, transformX, transformY) {
3483
3497
  /**
3484
3498
  * Captures a React Flow container element as an SVG image and returns data URL
3485
3499
  * @param containerElement - The React Flow container DOM element
3500
+ * @param options - Options for thumbnail capture
3486
3501
  * @returns Promise resolving to SVG data URL string, or null if capture fails
3487
3502
  */
3488
- async function captureCanvasThumbnail(containerElement) {
3503
+ async function captureCanvasThumbnail(containerElement, options = {}) {
3504
+ const { ignoreViewport = true, padding = 40, exportHandles = true, exportNodeTitle = true, exportBackgroundPattern = true, nodeBackgroundColor, backgroundPatternColor, } = options;
3489
3505
  if (!containerElement) {
3490
3506
  console.warn("[flowThumbnail] Container element is null");
3491
3507
  return null;
@@ -3501,12 +3517,18 @@ async function captureCanvasThumbnail(containerElement) {
3501
3517
  const viewportStyle = window.getComputedStyle(reactFlowViewport);
3502
3518
  const viewportTransform = viewportStyle.transform || viewportStyle.getPropertyValue("transform");
3503
3519
  const transform = parseViewportTransform(viewportTransform);
3504
- // Calculate visible bounds
3520
+ // Calculate visible bounds (null if ignoring viewport)
3505
3521
  const viewportRect = reactFlowViewport.getBoundingClientRect();
3506
- const visibleBounds = calculateVisibleBounds(viewportRect, transform);
3507
- // Extract edges and nodes
3522
+ const visibleBounds = ignoreViewport
3523
+ ? null
3524
+ : calculateVisibleBounds(viewportRect, transform);
3525
+ // Extract edges and nodes (pass null bounds if ignoring viewport to get all)
3508
3526
  const { edges, bounds: edgeBounds } = extractEdgePaths(reactFlowViewport, visibleBounds);
3509
- const { nodes, bounds: nodeBounds } = extractNodes(reactFlowViewport, visibleBounds);
3527
+ const { nodes, bounds: nodeBounds } = extractNodes(reactFlowViewport, visibleBounds, {
3528
+ exportHandles,
3529
+ exportNodeTitle,
3530
+ nodeBackgroundColor,
3531
+ });
3510
3532
  // Calculate overall bounding box
3511
3533
  // Handle case where one or both bounds might be Infinity (no content)
3512
3534
  let minX = Infinity;
@@ -3525,24 +3547,49 @@ async function captureCanvasThumbnail(containerElement) {
3525
3547
  maxX = Math.max(maxX, nodeBounds.maxX);
3526
3548
  maxY = Math.max(maxY, nodeBounds.maxY);
3527
3549
  }
3528
- // If no visible content, use the visible viewport bounds
3550
+ // If no content found, return null
3529
3551
  if (minX === Infinity || (nodes.length === 0 && edges.length === 0)) {
3552
+ if (ignoreViewport) {
3553
+ console.warn("[flowThumbnail] No content found in graph");
3554
+ return null;
3555
+ }
3556
+ // Fallback to visible viewport bounds if not ignoring viewport
3530
3557
  minX = visibleBounds.minX;
3531
3558
  minY = visibleBounds.minY;
3532
3559
  maxX = visibleBounds.maxX;
3533
3560
  maxY = visibleBounds.maxY;
3534
3561
  }
3535
- // Use the visible viewport bounds exactly (what the user sees)
3536
- const contentMinX = visibleBounds.minX;
3537
- const contentMinY = visibleBounds.minY;
3538
- const contentMaxX = visibleBounds.maxX;
3539
- const contentMaxY = visibleBounds.maxY;
3562
+ // Calculate content bounds with padding if ignoring viewport
3563
+ let contentMinX;
3564
+ let contentMinY;
3565
+ let contentMaxX;
3566
+ let contentMaxY;
3567
+ if (ignoreViewport) {
3568
+ // Add padding when exporting whole graph
3569
+ contentMinX = minX - padding;
3570
+ contentMinY = minY - padding;
3571
+ contentMaxX = maxX + padding;
3572
+ contentMaxY = maxY + padding;
3573
+ }
3574
+ else {
3575
+ // Use the visible viewport bounds exactly (what the user sees)
3576
+ contentMinX = visibleBounds.minX;
3577
+ contentMinY = visibleBounds.minY;
3578
+ contentMaxX = visibleBounds.maxX;
3579
+ contentMaxY = visibleBounds.maxY;
3580
+ }
3540
3581
  const contentWidth = contentMaxX - contentMinX;
3541
3582
  const contentHeight = contentMaxY - contentMinY;
3542
- // Create SVG
3543
- const { svg, group } = createSVGElement(contentWidth, contentHeight);
3544
- // Render content
3545
- renderContentToSVG(group, nodes, edges, -contentMinX, -contentMinY);
3583
+ // Create SVG with options
3584
+ const { svg, group } = createSVGElement(contentWidth, contentHeight, {
3585
+ exportBackgroundPattern,
3586
+ backgroundPatternColor,
3587
+ });
3588
+ // Render content with options
3589
+ renderContentToSVG(group, nodes, edges, -contentMinX, -contentMinY, {
3590
+ exportHandles,
3591
+ exportNodeTitle,
3592
+ });
3546
3593
  // Serialize SVG to string
3547
3594
  const serializer = new XMLSerializer();
3548
3595
  const svgString = serializer.serializeToString(svg);
@@ -3558,10 +3605,11 @@ async function captureCanvasThumbnail(containerElement) {
3558
3605
  /**
3559
3606
  * Captures a React Flow container element as an SVG image and downloads it
3560
3607
  * @param containerElement - The React Flow container DOM element
3608
+ * @param options - Options for thumbnail capture
3561
3609
  * @returns Promise resolving to true if successful, false otherwise
3562
3610
  */
3563
- async function downloadCanvasThumbnail(containerElement) {
3564
- const svgDataUrl = await captureCanvasThumbnail(containerElement);
3611
+ async function downloadCanvasThumbnail(containerElement, options = {}) {
3612
+ const svgDataUrl = await captureCanvasThumbnail(containerElement, options);
3565
3613
  if (!svgDataUrl) {
3566
3614
  return false;
3567
3615
  }
@@ -4520,24 +4568,24 @@ function IssueBadge({ level, title, size = 12, className, }) {
4520
4568
  return (jsxRuntime.jsx("button", { type: "button", className: `inline-flex items-center justify-center shrink-0 ${colorClass} ${className ?? ""}`, title: title, style: { width: size, height: size }, children: level === "error" ? (jsxRuntime.jsx(react$1.XCircleIcon, { size: size, weight: "fill" })) : (jsxRuntime.jsx(react$1.WarningCircleIcon, { size: size, weight: "fill" })) }));
4521
4569
  }
4522
4570
 
4523
- function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInvalidate, }) {
4571
+ function DefaultNodeHeader({ id, typeId, validation, right, showId, onInvalidate, }) {
4524
4572
  const ctx = useWorkbenchContext();
4525
4573
  const [isEditing, setIsEditing] = React.useState(false);
4526
4574
  const [editValue, setEditValue] = React.useState("");
4527
4575
  const inputRef = React.useRef(null);
4528
4576
  // Use getNodeDisplayName if typeId is provided, otherwise use title prop
4529
- const displayName = typeId ? ctx.getNodeDisplayName(id) : title ?? id;
4530
- const effectiveTypeId = typeId ?? title ?? id;
4577
+ const displayName = typeId ? ctx.getNodeDisplayName(id) : id;
4578
+ const effectiveTypeId = typeId ?? id;
4531
4579
  // Get the default display name (without custom name) for comparison
4532
4580
  const getDefaultDisplayName = React.useCallback(() => {
4533
4581
  if (!typeId)
4534
- return title ?? id;
4582
+ return id;
4535
4583
  const node = ctx.wb.def.nodes.find((n) => n.nodeId === id);
4536
4584
  if (!node)
4537
4585
  return id;
4538
4586
  const desc = ctx.registry.nodes.get(node.typeId);
4539
4587
  return desc?.displayName || node.typeId;
4540
- }, [ctx, id, typeId, title]);
4588
+ }, [ctx, id, typeId]);
4541
4589
  const handleInvalidate = React.useCallback(() => {
4542
4590
  try {
4543
4591
  if (onInvalidate)