@bian-womp/spark-workbench 0.2.90 → 0.2.92
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 +117 -68
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/misc/DefaultNodeHeader.d.ts +1 -2
- package/lib/cjs/src/misc/DefaultNodeHeader.d.ts.map +1 -1
- package/lib/cjs/src/misc/thumbnail-utils.d.ts +38 -2
- package/lib/cjs/src/misc/thumbnail-utils.d.ts.map +1 -1
- package/lib/esm/index.js +117 -68
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/misc/DefaultNodeHeader.d.ts +1 -2
- package/lib/esm/src/misc/DefaultNodeHeader.d.ts.map +1 -1
- package/lib/esm/src/misc/thumbnail-utils.d.ts +38 -2
- package/lib/esm/src/misc/thumbnail-utils.d.ts.map +1 -1
- package/package.json +4 -4
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 (
|
|
3156
|
-
pathBounds.
|
|
3157
|
-
|
|
3158
|
-
|
|
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 =
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
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", backgroundColor = "#ffffff", } = 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
|
|
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
|
-
// Create background rectangle with white base
|
|
3377
|
+
// Create background rectangle with configurable background color
|
|
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
|
-
bgRect.setAttribute("fill",
|
|
3381
|
+
bgRect.setAttribute("fill", backgroundColor);
|
|
3394
3382
|
svg.appendChild(bgRect);
|
|
3395
|
-
// Create pattern
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
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
|
-
|
|
3473
|
-
|
|
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, backgroundColor, } = 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 =
|
|
3507
|
-
|
|
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,50 @@ 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
|
|
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
|
-
//
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
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
|
-
|
|
3545
|
-
|
|
3583
|
+
// Create SVG with options
|
|
3584
|
+
const { svg, group } = createSVGElement(contentWidth, contentHeight, {
|
|
3585
|
+
exportBackgroundPattern,
|
|
3586
|
+
backgroundPatternColor,
|
|
3587
|
+
backgroundColor,
|
|
3588
|
+
});
|
|
3589
|
+
// Render content with options
|
|
3590
|
+
renderContentToSVG(group, nodes, edges, -contentMinX, -contentMinY, {
|
|
3591
|
+
exportHandles,
|
|
3592
|
+
exportNodeTitle,
|
|
3593
|
+
});
|
|
3546
3594
|
// Serialize SVG to string
|
|
3547
3595
|
const serializer = new XMLSerializer();
|
|
3548
3596
|
const svgString = serializer.serializeToString(svg);
|
|
@@ -3558,10 +3606,11 @@ async function captureCanvasThumbnail(containerElement) {
|
|
|
3558
3606
|
/**
|
|
3559
3607
|
* Captures a React Flow container element as an SVG image and downloads it
|
|
3560
3608
|
* @param containerElement - The React Flow container DOM element
|
|
3609
|
+
* @param options - Options for thumbnail capture
|
|
3561
3610
|
* @returns Promise resolving to true if successful, false otherwise
|
|
3562
3611
|
*/
|
|
3563
|
-
async function downloadCanvasThumbnail(containerElement) {
|
|
3564
|
-
const svgDataUrl = await captureCanvasThumbnail(containerElement);
|
|
3612
|
+
async function downloadCanvasThumbnail(containerElement, options = {}) {
|
|
3613
|
+
const svgDataUrl = await captureCanvasThumbnail(containerElement, options);
|
|
3565
3614
|
if (!svgDataUrl) {
|
|
3566
3615
|
return false;
|
|
3567
3616
|
}
|
|
@@ -4520,24 +4569,24 @@ function IssueBadge({ level, title, size = 12, className, }) {
|
|
|
4520
4569
|
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
4570
|
}
|
|
4522
4571
|
|
|
4523
|
-
function DefaultNodeHeader({ id, typeId,
|
|
4572
|
+
function DefaultNodeHeader({ id, typeId, validation, right, showId, onInvalidate, }) {
|
|
4524
4573
|
const ctx = useWorkbenchContext();
|
|
4525
4574
|
const [isEditing, setIsEditing] = React.useState(false);
|
|
4526
4575
|
const [editValue, setEditValue] = React.useState("");
|
|
4527
4576
|
const inputRef = React.useRef(null);
|
|
4528
4577
|
// Use getNodeDisplayName if typeId is provided, otherwise use title prop
|
|
4529
|
-
const displayName = typeId ? ctx.getNodeDisplayName(id) :
|
|
4530
|
-
const effectiveTypeId = typeId ??
|
|
4578
|
+
const displayName = typeId ? ctx.getNodeDisplayName(id) : id;
|
|
4579
|
+
const effectiveTypeId = typeId ?? id;
|
|
4531
4580
|
// Get the default display name (without custom name) for comparison
|
|
4532
4581
|
const getDefaultDisplayName = React.useCallback(() => {
|
|
4533
4582
|
if (!typeId)
|
|
4534
|
-
return
|
|
4583
|
+
return id;
|
|
4535
4584
|
const node = ctx.wb.def.nodes.find((n) => n.nodeId === id);
|
|
4536
4585
|
if (!node)
|
|
4537
4586
|
return id;
|
|
4538
4587
|
const desc = ctx.registry.nodes.get(node.typeId);
|
|
4539
4588
|
return desc?.displayName || node.typeId;
|
|
4540
|
-
}, [ctx, id, typeId
|
|
4589
|
+
}, [ctx, id, typeId]);
|
|
4541
4590
|
const handleInvalidate = React.useCallback(() => {
|
|
4542
4591
|
try {
|
|
4543
4592
|
if (onInvalidate)
|