@bian-womp/spark-workbench 0.2.64 → 0.2.66

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.
Files changed (37) hide show
  1. package/lib/cjs/index.cjs +589 -205
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +54 -1
  4. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  5. package/lib/cjs/src/index.d.ts +1 -0
  6. package/lib/cjs/src/index.d.ts.map +1 -1
  7. package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +1 -1
  8. package/lib/cjs/src/misc/NodeContextMenu.d.ts.map +1 -1
  9. package/lib/cjs/src/misc/SelectionContextMenu.d.ts +3 -0
  10. package/lib/cjs/src/misc/SelectionContextMenu.d.ts.map +1 -0
  11. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  12. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts +18 -0
  13. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
  14. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts +39 -2
  15. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
  16. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +41 -0
  17. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  18. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  19. package/lib/esm/index.js +584 -206
  20. package/lib/esm/index.js.map +1 -1
  21. package/lib/esm/src/core/InMemoryWorkbench.d.ts +54 -1
  22. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  23. package/lib/esm/src/index.d.ts +1 -0
  24. package/lib/esm/src/index.d.ts.map +1 -1
  25. package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +1 -1
  26. package/lib/esm/src/misc/NodeContextMenu.d.ts.map +1 -1
  27. package/lib/esm/src/misc/SelectionContextMenu.d.ts +3 -0
  28. package/lib/esm/src/misc/SelectionContextMenu.d.ts.map +1 -0
  29. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  30. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts +18 -0
  31. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
  32. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts +39 -2
  33. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
  34. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +41 -0
  35. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  36. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  37. package/package.json +4 -4
package/lib/cjs/index.cjs CHANGED
@@ -307,6 +307,22 @@ class InMemoryWorkbench extends AbstractWorkbench {
307
307
  edges: [...this.selection.edges],
308
308
  };
309
309
  }
310
+ /**
311
+ * Delete all selected nodes and edges.
312
+ */
313
+ deleteSelection() {
314
+ const selection = this.getSelection();
315
+ // Delete all selected nodes (this will also remove connected edges)
316
+ for (const nodeId of selection.nodes) {
317
+ this.removeNode(nodeId);
318
+ }
319
+ // Delete remaining selected edges (edges not connected to deleted nodes)
320
+ for (const edgeId of selection.edges) {
321
+ this.disconnect(edgeId);
322
+ }
323
+ // Clear selection
324
+ this.setSelection({ nodes: [], edges: [] });
325
+ }
310
326
  setViewport(viewport, opts) {
311
327
  this.viewport = { ...viewport };
312
328
  this.emit("graphUiChanged", {
@@ -367,6 +383,142 @@ class InMemoryWorkbench extends AbstractWorkbench {
367
383
  for (const h of Array.from(set))
368
384
  h(payload);
369
385
  }
386
+ /**
387
+ * Copy selected nodes and their internal edges.
388
+ * Returns data in a format suitable for pasting.
389
+ * Positions are normalized relative to the selection bounds center.
390
+ * Uses the same logic as duplicate: copies inputs without bindings and supports copyOutputsFrom.
391
+ */
392
+ copySelection(runner, getNodeSize) {
393
+ const selection = this.getSelection();
394
+ if (selection.nodes.length === 0)
395
+ return null;
396
+ const def = this.export();
397
+ const positions = this.getPositions();
398
+ const selectedNodeSet = new Set(selection.nodes);
399
+ // Collect nodes to copy
400
+ const nodesToCopy = def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
401
+ if (nodesToCopy.length === 0)
402
+ return null;
403
+ // Calculate bounds
404
+ let minX = Infinity;
405
+ let minY = Infinity;
406
+ let maxX = -Infinity;
407
+ let maxY = -Infinity;
408
+ nodesToCopy.forEach((node) => {
409
+ const pos = positions[node.nodeId] || { x: 0, y: 0 };
410
+ const size = getNodeSize?.(node.nodeId, node.typeId) || {
411
+ width: 200,
412
+ height: 100,
413
+ };
414
+ minX = Math.min(minX, pos.x);
415
+ minY = Math.min(minY, pos.y);
416
+ maxX = Math.max(maxX, pos.x + size.width);
417
+ maxY = Math.max(maxY, pos.y + size.height);
418
+ });
419
+ const bounds = { minX, minY, maxX, maxY };
420
+ const centerX = (bounds.minX + bounds.maxX) / 2;
421
+ const centerY = (bounds.minY + bounds.maxY) / 2;
422
+ // Get inputs for each node
423
+ // Include values from inbound edges if those edges are selected
424
+ const allInputs = runner.getInputs(def);
425
+ const selectedEdgeSet = new Set(selection.edges);
426
+ const copiedNodes = nodesToCopy.map((node) => {
427
+ const pos = positions[node.nodeId] || { x: 0, y: 0 };
428
+ // Get all inbound edges for this node
429
+ const inboundEdges = def.edges.filter((e) => e.target.nodeId === node.nodeId);
430
+ // Build set of handles that have inbound edges
431
+ // But only exclude handles whose edges are NOT selected
432
+ const inboundHandlesToExclude = new Set(inboundEdges
433
+ .filter((e) => !selectedEdgeSet.has(e.id)) // Only exclude if edge is not selected
434
+ .map((e) => e.target.handle));
435
+ const allNodeInputs = allInputs[node.nodeId] || {};
436
+ // Include inputs that either:
437
+ // 1. Don't have inbound edges (literal values)
438
+ // 2. Have inbound edges that ARE selected (preserve the value from the edge)
439
+ const inputsToCopy = Object.fromEntries(Object.entries(allNodeInputs).filter(([handle]) => !inboundHandlesToExclude.has(handle)));
440
+ return {
441
+ typeId: node.typeId,
442
+ params: node.params,
443
+ resolvedHandles: node.resolvedHandles,
444
+ position: {
445
+ x: pos.x - centerX,
446
+ y: pos.y - centerY,
447
+ },
448
+ inputs: inputsToCopy,
449
+ originalNodeId: node.nodeId,
450
+ };
451
+ });
452
+ // Collect edges between copied nodes
453
+ const copiedEdges = def.edges
454
+ .filter((edge) => {
455
+ return (selectedNodeSet.has(edge.source.nodeId) &&
456
+ selectedNodeSet.has(edge.target.nodeId));
457
+ })
458
+ .map((edge) => ({
459
+ sourceNodeId: edge.source.nodeId,
460
+ sourceHandle: edge.source.handle,
461
+ targetNodeId: edge.target.nodeId,
462
+ targetHandle: edge.target.handle,
463
+ typeId: edge.typeId,
464
+ }));
465
+ return {
466
+ nodes: copiedNodes,
467
+ edges: copiedEdges,
468
+ bounds,
469
+ };
470
+ }
471
+ /**
472
+ * Paste copied graph data at the specified center position.
473
+ * Returns the mapping from original node IDs to new node IDs.
474
+ * Uses copyOutputsFrom to copy outputs from original nodes (like duplicate does).
475
+ */
476
+ pasteCopiedData(data, center) {
477
+ const nodeIdMap = new Map();
478
+ const edgeIds = [];
479
+ // Add nodes
480
+ for (const nodeData of data.nodes) {
481
+ const newNodeId = this.addNode({
482
+ typeId: nodeData.typeId,
483
+ params: nodeData.params,
484
+ resolvedHandles: nodeData.resolvedHandles,
485
+ position: {
486
+ x: nodeData.position.x + center.x,
487
+ y: nodeData.position.y + center.y,
488
+ },
489
+ }, {
490
+ inputs: nodeData.inputs,
491
+ copyOutputsFrom: nodeData.originalNodeId,
492
+ dry: true,
493
+ });
494
+ nodeIdMap.set(nodeData.originalNodeId, newNodeId);
495
+ }
496
+ // Add edges
497
+ for (const edgeData of data.edges) {
498
+ const newSourceNodeId = nodeIdMap.get(edgeData.sourceNodeId);
499
+ const newTargetNodeId = nodeIdMap.get(edgeData.targetNodeId);
500
+ if (newSourceNodeId && newTargetNodeId) {
501
+ const edgeId = this.connect({
502
+ source: {
503
+ nodeId: newSourceNodeId,
504
+ handle: edgeData.sourceHandle,
505
+ },
506
+ target: {
507
+ nodeId: newTargetNodeId,
508
+ handle: edgeData.targetHandle,
509
+ },
510
+ typeId: edgeData.typeId,
511
+ }, { dry: true });
512
+ edgeIds.push(edgeId);
513
+ }
514
+ }
515
+ // Select the newly pasted nodes
516
+ this.setSelection({
517
+ nodes: Array.from(nodeIdMap.values()),
518
+ edges: edgeIds,
519
+ });
520
+ return { nodeIdMap, edgeIds };
521
+ }
370
522
  }
371
523
 
372
524
  class CLIWorkbench {
@@ -2913,6 +3065,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2913
3065
  updateEdgeType,
2914
3066
  triggerExternal,
2915
3067
  uiVersion,
3068
+ overrides,
2916
3069
  }), [
2917
3070
  wb,
2918
3071
  runner,
@@ -2952,10 +3105,272 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2952
3105
  wb,
2953
3106
  runner,
2954
3107
  uiVersion,
3108
+ overrides,
2955
3109
  ]);
2956
3110
  return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
2957
3111
  }
2958
3112
 
3113
+ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose, getDefaultNodeSize, onCopyResult) {
3114
+ const doBake = async (handleId) => {
3115
+ try {
3116
+ const typeId = outputTypesMap?.[nodeId]?.[handleId];
3117
+ const raw = outputsMap?.[nodeId]?.[handleId];
3118
+ if (!typeId || raw === undefined)
3119
+ return;
3120
+ const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
3121
+ const coerceIfNeeded = async (fromType, toType, value) => {
3122
+ if (!toType || toType === fromType || !runner?.coerce)
3123
+ return value;
3124
+ try {
3125
+ return await runner.coerce(fromType, toType, value);
3126
+ }
3127
+ catch {
3128
+ return value;
3129
+ }
3130
+ };
3131
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3132
+ const isArray = typeId.endsWith("[]");
3133
+ const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
3134
+ const tArr = isArray ? registry.types.get(typeId) : undefined;
3135
+ const tElem = registry.types.get(baseTypeId);
3136
+ const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
3137
+ const arrTarget = isArray ? tArr?.bakeTarget : undefined;
3138
+ const elemTarget = isArray ? tElem?.bakeTarget : undefined;
3139
+ if (singleTarget) {
3140
+ const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3141
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3142
+ const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3143
+ wb.addNode({
3144
+ typeId: singleTarget.nodeTypeId,
3145
+ position: { x: pos.x + 180, y: pos.y },
3146
+ }, { inputs: { [singleTarget.inputHandle]: coerced } });
3147
+ return;
3148
+ }
3149
+ if (isArray && arrTarget) {
3150
+ const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3151
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3152
+ const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3153
+ wb.addNode({
3154
+ typeId: arrTarget.nodeTypeId,
3155
+ position: { x: pos.x + 180, y: pos.y },
3156
+ }, { inputs: { [arrTarget.inputHandle]: coerced } });
3157
+ return;
3158
+ }
3159
+ if (isArray && elemTarget) {
3160
+ const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3161
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3162
+ const src = unwrap(raw);
3163
+ const items = Array.isArray(src) ? src : [src];
3164
+ const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
3165
+ const COLS = 4;
3166
+ const DX = 180;
3167
+ const DY = 160;
3168
+ for (let idx = 0; idx < coercedItems.length; idx++) {
3169
+ const col = idx % COLS;
3170
+ const row = Math.floor(idx / COLS);
3171
+ wb.addNode({
3172
+ typeId: elemTarget.nodeTypeId,
3173
+ position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3174
+ }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3175
+ }
3176
+ return;
3177
+ }
3178
+ }
3179
+ catch { }
3180
+ };
3181
+ return {
3182
+ onDelete: () => {
3183
+ wb.removeNode(nodeId);
3184
+ onClose();
3185
+ },
3186
+ onDuplicate: async () => {
3187
+ const def = wb.export();
3188
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3189
+ if (!n)
3190
+ return onClose();
3191
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3192
+ const inboundHandles = new Set(def.edges
3193
+ .filter((e) => e.target.nodeId === nodeId)
3194
+ .map((e) => e.target.handle));
3195
+ const allInputs = runner.getInputs(def)[nodeId] || {};
3196
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3197
+ const newNodeId = wb.addNode({
3198
+ typeId: n.typeId,
3199
+ params: n.params,
3200
+ position: { x: pos.x + 24, y: pos.y + 24 },
3201
+ resolvedHandles: n.resolvedHandles,
3202
+ }, {
3203
+ inputs: inputsWithoutBindings,
3204
+ copyOutputsFrom: nodeId,
3205
+ dry: true,
3206
+ });
3207
+ // Select the newly duplicated node
3208
+ wb.setSelection({
3209
+ nodes: [newNodeId],
3210
+ edges: [],
3211
+ });
3212
+ onClose();
3213
+ },
3214
+ onDuplicateWithEdges: async () => {
3215
+ const def = wb.export();
3216
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3217
+ if (!n)
3218
+ return onClose();
3219
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3220
+ // Get inputs without bindings (literal values only)
3221
+ const inputs = runner.getInputs(def)[nodeId] || {};
3222
+ // Add the duplicated node
3223
+ const newNodeId = wb.addNode({
3224
+ typeId: n.typeId,
3225
+ params: n.params,
3226
+ position: { x: pos.x + 24, y: pos.y + 24 },
3227
+ resolvedHandles: n.resolvedHandles,
3228
+ }, {
3229
+ inputs,
3230
+ copyOutputsFrom: nodeId,
3231
+ dry: true,
3232
+ });
3233
+ // Find all incoming edges (edges where target is the original node)
3234
+ const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3235
+ // Duplicate each incoming edge to point to the new node
3236
+ for (const edge of incomingEdges) {
3237
+ wb.connect({
3238
+ source: edge.source, // Keep the same source
3239
+ target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3240
+ typeId: edge.typeId,
3241
+ }, { dry: true });
3242
+ }
3243
+ // Select the newly duplicated node and edges
3244
+ wb.setSelection({
3245
+ nodes: [newNodeId],
3246
+ edges: [],
3247
+ });
3248
+ onClose();
3249
+ },
3250
+ onRunPull: async () => {
3251
+ try {
3252
+ await runner.computeNode(nodeId);
3253
+ }
3254
+ catch { }
3255
+ onClose();
3256
+ },
3257
+ onBake: async (handleId) => {
3258
+ await doBake(handleId);
3259
+ onClose();
3260
+ },
3261
+ onCopy: () => {
3262
+ const copyHandler = createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyResult);
3263
+ copyHandler();
3264
+ onClose();
3265
+ },
3266
+ onCopyId: async () => {
3267
+ try {
3268
+ await navigator.clipboard.writeText(nodeId);
3269
+ }
3270
+ catch { }
3271
+ onClose();
3272
+ },
3273
+ onClose,
3274
+ };
3275
+ }
3276
+ /**
3277
+ * Creates a copy handler that copies the current selection and optionally stores the result.
3278
+ */
3279
+ function createCopyHandler(wb, runner, getDefaultNodeSize, onCopyResult) {
3280
+ return () => {
3281
+ const getNodeSize = getDefaultNodeSize
3282
+ ? (nodeId, typeId) => getDefaultNodeSize(typeId)
3283
+ : undefined;
3284
+ const data = wb.copySelection(runner, getNodeSize);
3285
+ if (onCopyResult) {
3286
+ onCopyResult(data);
3287
+ }
3288
+ };
3289
+ }
3290
+ /**
3291
+ * Creates a copy handler for a single node (temporarily selects it, copies, then restores selection).
3292
+ */
3293
+ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyResult) {
3294
+ return () => {
3295
+ // Select the node first, then copy
3296
+ const currentSelection = wb.getSelection();
3297
+ wb.setSelection({ nodes: [nodeId], edges: [] });
3298
+ const getNodeSize = getDefaultNodeSize
3299
+ ? (nodeId, typeId) => getDefaultNodeSize(typeId)
3300
+ : undefined;
3301
+ const data = wb.copySelection(runner, getNodeSize);
3302
+ // Restore original selection
3303
+ wb.setSelection(currentSelection);
3304
+ if (onCopyResult) {
3305
+ onCopyResult(data);
3306
+ }
3307
+ };
3308
+ }
3309
+ /**
3310
+ * Creates base selection context menu handlers.
3311
+ */
3312
+ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
3313
+ return {
3314
+ onCopy: runner
3315
+ ? createCopyHandler(wb, runner, getDefaultNodeSize, onCopyResult)
3316
+ : () => {
3317
+ // No-op if runner not available
3318
+ onClose();
3319
+ },
3320
+ onDelete: () => {
3321
+ wb.deleteSelection();
3322
+ onClose();
3323
+ },
3324
+ onClose,
3325
+ };
3326
+ }
3327
+ /**
3328
+ * Creates base default context menu handlers.
3329
+ */
3330
+ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste) {
3331
+ return {
3332
+ onAddNode,
3333
+ onPaste,
3334
+ onClose,
3335
+ };
3336
+ }
3337
+ function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3338
+ try {
3339
+ const def = wb.export();
3340
+ const node = def.nodes.find((n) => n.nodeId === nodeId);
3341
+ if (!node)
3342
+ return [];
3343
+ const desc = registry.nodes.get(node.typeId);
3344
+ const handles = Object.keys(desc?.outputs || {});
3345
+ const out = [];
3346
+ for (const h of handles) {
3347
+ const tId = outputTypesMap?.[nodeId]?.[h];
3348
+ if (!tId)
3349
+ continue;
3350
+ if (tId.endsWith("[]")) {
3351
+ const base = tId.slice(0, -2);
3352
+ const tArr = registry.types.get(tId);
3353
+ const tElem = registry.types.get(base);
3354
+ const arrT = tArr?.bakeTarget;
3355
+ const elemT = tElem?.bakeTarget;
3356
+ if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3357
+ (elemT && registry.nodes.has(elemT.nodeTypeId)))
3358
+ out.push(h);
3359
+ }
3360
+ else {
3361
+ const t = registry.types.get(tId);
3362
+ const bt = t?.bakeTarget;
3363
+ if (bt && registry.nodes.has(bt.nodeTypeId))
3364
+ out.push(h);
3365
+ }
3366
+ }
3367
+ return out;
3368
+ }
3369
+ catch {
3370
+ return [];
3371
+ }
3372
+ }
3373
+
2959
3374
  function IssueBadge({ level, title, size = 12, className, }) {
2960
3375
  const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
2961
3376
  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" })) }));
@@ -3168,7 +3583,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3168
3583
  }
3169
3584
  catch { }
3170
3585
  };
3171
- return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), jsxRuntime.jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
3586
+ return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto select-none`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), jsxRuntime.jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
3172
3587
  e.stopPropagation();
3173
3588
  deleteEdgeById(m.data?.edgeId);
3174
3589
  }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), renderEdgeStatus(), jsxRuntime.jsx("div", { className: "mt-1", children: jsxRuntime.jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
@@ -3236,7 +3651,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3236
3651
  const title = inIssues
3237
3652
  .map((v) => `${v.code}: ${v.message}`)
3238
3653
  .join("; ");
3239
- return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1", value: current !== undefined && current !== null
3654
+ return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1 select-text", value: current !== undefined && current !== null
3240
3655
  ? String(current)
3241
3656
  : "", onChange: (e) => {
3242
3657
  const val = e.target.value;
@@ -3250,7 +3665,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3250
3665
  ? `Default: ${placeholder}`
3251
3666
  : "(select)" }), registry.enums
3252
3667
  .get(typeId)
3253
- ?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-1", children: jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1", placeholder: placeholder
3668
+ ?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-1", children: jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1 select-text", placeholder: placeholder
3254
3669
  ? `Default: ${placeholder}`
3255
3670
  : undefined, value: displayValue, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
3256
3671
  if (e.key === "Enter")
@@ -3503,6 +3918,13 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3503
3918
  handlers.onAddNode(typeId, { position: p });
3504
3919
  handlers.onClose();
3505
3920
  };
3921
+ const handlePaste = () => {
3922
+ if (!handlers.onPaste)
3923
+ return;
3924
+ const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
3925
+ handlers.onPaste(p);
3926
+ handlers.onClose();
3927
+ };
3506
3928
  const renderTree = (tree, path = []) => {
3507
3929
  const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
3508
3930
  return (jsxRuntime.jsx("div", { children: entries.map(([key, child]) => {
@@ -3517,10 +3939,10 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3517
3939
  return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "px-2 py-1 text-[11px] uppercase tracking-wide text-gray-400", children: label }), child.__self && (jsxRuntime.jsx("button", { onClick: () => handleClick(child.__self), className: "block w-full text-left px-3 py-1 hover:bg-gray-100 cursor-pointer", title: child.__self, children: label })), jsxRuntime.jsx("div", { className: "pl-2 border-l border-gray-200 ml-2", children: renderTree(child, [...path, key]) })] }, idKey));
3518
3940
  }) }));
3519
3941
  };
3520
- return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-none shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3942
+ return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3521
3943
  e.preventDefault();
3522
3944
  e.stopPropagation();
3523
- }, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxRuntime.jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsxRuntime.jsx("div", { className: "px-2 pb-1", children: jsxRuntime.jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 px-2 py-1 text-sm outline-none focus:border-gray-400", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsxRuntime.jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
3945
+ }, children: [handlers.onPaste && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed", onClick: handlePaste, children: "Paste" })), handlers.onPaste && jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxRuntime.jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsxRuntime.jsx("div", { className: "px-2 pb-1", children: jsxRuntime.jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 px-2 py-1 text-sm outline-none focus:border-gray-400 select-text", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsxRuntime.jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
3524
3946
  }
3525
3947
 
3526
3948
  function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, }) {
@@ -3558,199 +3980,55 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
3558
3980
  const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
3559
3981
  (MENU_MIN_WIDTH + PADDING));
3560
3982
  const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
3561
- return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3983
+ return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3562
3984
  e.preventDefault();
3563
3985
  e.stopPropagation();
3564
- }, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), canRunPull && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), bakeableOutputs.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h))), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" })] })), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" })] }));
3986
+ }, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), canRunPull && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopy, children: "Copy" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" }), bakeableOutputs.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h)))] }))] }));
3565
3987
  }
3566
3988
 
3567
- function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose) {
3568
- const doBake = async (handleId) => {
3569
- try {
3570
- const typeId = outputTypesMap?.[nodeId]?.[handleId];
3571
- const raw = outputsMap?.[nodeId]?.[handleId];
3572
- if (!typeId || raw === undefined)
3573
- return;
3574
- const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
3575
- const coerceIfNeeded = async (fromType, toType, value) => {
3576
- if (!toType || toType === fromType || !runner?.coerce)
3577
- return value;
3578
- try {
3579
- return await runner.coerce(fromType, toType, value);
3580
- }
3581
- catch {
3582
- return value;
3583
- }
3584
- };
3585
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3586
- const isArray = typeId.endsWith("[]");
3587
- const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
3588
- const tArr = isArray ? registry.types.get(typeId) : undefined;
3589
- const tElem = registry.types.get(baseTypeId);
3590
- const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
3591
- const arrTarget = isArray ? tArr?.bakeTarget : undefined;
3592
- const elemTarget = isArray ? tElem?.bakeTarget : undefined;
3593
- if (singleTarget) {
3594
- const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3595
- const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3596
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3597
- wb.addNode({
3598
- typeId: singleTarget.nodeTypeId,
3599
- position: { x: pos.x + 180, y: pos.y },
3600
- }, { inputs: { [singleTarget.inputHandle]: coerced } });
3601
- return;
3602
- }
3603
- if (isArray && arrTarget) {
3604
- const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3605
- const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3606
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3607
- wb.addNode({
3608
- typeId: arrTarget.nodeTypeId,
3609
- position: { x: pos.x + 180, y: pos.y },
3610
- }, { inputs: { [arrTarget.inputHandle]: coerced } });
3611
- return;
3612
- }
3613
- if (isArray && elemTarget) {
3614
- const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3615
- const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3616
- const src = unwrap(raw);
3617
- const items = Array.isArray(src) ? src : [src];
3618
- const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
3619
- const COLS = 4;
3620
- const DX = 180;
3621
- const DY = 160;
3622
- for (let idx = 0; idx < coercedItems.length; idx++) {
3623
- const col = idx % COLS;
3624
- const row = Math.floor(idx / COLS);
3625
- wb.addNode({
3626
- typeId: elemTarget.nodeTypeId,
3627
- position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3628
- }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3629
- }
3989
+ function SelectionContextMenu({ open, clientPos, handlers, }) {
3990
+ const ref = React.useRef(null);
3991
+ // Close on outside click and on ESC
3992
+ React.useEffect(() => {
3993
+ if (!open)
3994
+ return;
3995
+ const onDown = (e) => {
3996
+ if (!ref.current)
3630
3997
  return;
3631
- }
3632
- }
3633
- catch { }
3634
- };
3635
- return {
3636
- onDelete: () => {
3637
- wb.removeNode(nodeId);
3638
- onClose();
3639
- },
3640
- onDuplicate: async () => {
3641
- const def = wb.export();
3642
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3643
- if (!n)
3644
- return onClose();
3645
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3646
- const inboundHandles = new Set(def.edges
3647
- .filter((e) => e.target.nodeId === nodeId)
3648
- .map((e) => e.target.handle));
3649
- const allInputs = runner.getInputs(def)[nodeId] || {};
3650
- const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3651
- wb.addNode({
3652
- typeId: n.typeId,
3653
- params: n.params,
3654
- position: { x: pos.x + 24, y: pos.y + 24 },
3655
- resolvedHandles: n.resolvedHandles,
3656
- }, {
3657
- inputs: inputsWithoutBindings,
3658
- copyOutputsFrom: nodeId,
3659
- dry: true,
3660
- });
3661
- onClose();
3662
- },
3663
- onDuplicateWithEdges: async () => {
3664
- const def = wb.export();
3665
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3666
- if (!n)
3667
- return onClose();
3668
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3669
- // Get inputs without bindings (literal values only)
3670
- const inputs = runner.getInputs(def)[nodeId] || {};
3671
- // Add the duplicated node
3672
- const newNodeId = wb.addNode({
3673
- typeId: n.typeId,
3674
- params: n.params,
3675
- position: { x: pos.x + 24, y: pos.y + 24 },
3676
- resolvedHandles: n.resolvedHandles,
3677
- }, {
3678
- inputs,
3679
- copyOutputsFrom: nodeId,
3680
- dry: true,
3681
- });
3682
- // Find all incoming edges (edges where target is the original node)
3683
- const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3684
- // Duplicate each incoming edge to point to the new node
3685
- for (const edge of incomingEdges) {
3686
- wb.connect({
3687
- source: edge.source, // Keep the same source
3688
- target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3689
- typeId: edge.typeId,
3690
- }, { dry: true });
3691
- }
3692
- onClose();
3693
- },
3694
- onRunPull: async () => {
3695
- try {
3696
- await runner.computeNode(nodeId);
3697
- }
3698
- catch { }
3699
- onClose();
3700
- },
3701
- onBake: async (handleId) => {
3702
- await doBake(handleId);
3703
- onClose();
3704
- },
3705
- onCopyId: async () => {
3706
- try {
3707
- await navigator.clipboard.writeText(nodeId);
3708
- }
3709
- catch { }
3710
- onClose();
3711
- },
3712
- onClose,
3713
- };
3714
- }
3715
- function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3716
- try {
3717
- const def = wb.export();
3718
- const node = def.nodes.find((n) => n.nodeId === nodeId);
3719
- if (!node)
3720
- return [];
3721
- const desc = registry.nodes.get(node.typeId);
3722
- const handles = Object.keys(desc?.outputs || {});
3723
- const out = [];
3724
- for (const h of handles) {
3725
- const tId = outputTypesMap?.[nodeId]?.[h];
3726
- if (!tId)
3727
- continue;
3728
- if (tId.endsWith("[]")) {
3729
- const base = tId.slice(0, -2);
3730
- const tArr = registry.types.get(tId);
3731
- const tElem = registry.types.get(base);
3732
- const arrT = tArr?.bakeTarget;
3733
- const elemT = tElem?.bakeTarget;
3734
- if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3735
- (elemT && registry.nodes.has(elemT.nodeTypeId)))
3736
- out.push(h);
3737
- }
3738
- else {
3739
- const t = registry.types.get(tId);
3740
- const bt = t?.bakeTarget;
3741
- if (bt && registry.nodes.has(bt.nodeTypeId))
3742
- out.push(h);
3743
- }
3744
- }
3745
- return out;
3746
- }
3747
- catch {
3748
- return [];
3749
- }
3998
+ if (!ref.current.contains(e.target))
3999
+ handlers.onClose();
4000
+ };
4001
+ const onKey = (e) => {
4002
+ if (e.key === "Escape")
4003
+ handlers.onClose();
4004
+ };
4005
+ window.addEventListener("mousedown", onDown, true);
4006
+ window.addEventListener("keydown", onKey);
4007
+ return () => {
4008
+ window.removeEventListener("mousedown", onDown, true);
4009
+ window.removeEventListener("keydown", onKey);
4010
+ };
4011
+ }, [open, handlers]);
4012
+ React.useEffect(() => {
4013
+ if (open)
4014
+ ref.current?.focus();
4015
+ }, [open]);
4016
+ if (!open || !clientPos)
4017
+ return null;
4018
+ // Clamp menu position to viewport
4019
+ const MENU_MIN_WIDTH = 180;
4020
+ const PADDING = 16;
4021
+ const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
4022
+ (MENU_MIN_WIDTH + PADDING));
4023
+ const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 100);
4024
+ return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
4025
+ e.preventDefault();
4026
+ e.stopPropagation();
4027
+ }, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopy, children: "Copy" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" })] }));
3750
4028
  }
3751
4029
 
3752
4030
  const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
3753
- const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, } = useWorkbenchContext();
4031
+ const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, overrides, } = useWorkbenchContext();
3754
4032
  const nodeValidation = validationByNode;
3755
4033
  const edgeValidation = validationByEdge.errors;
3756
4034
  const [registryVersion, setRegistryVersion] = React.useState(0);
@@ -3989,23 +4267,100 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3989
4267
  const [nodeMenuOpen, setNodeMenuOpen] = React.useState(false);
3990
4268
  const [nodeMenuPos, setNodeMenuPos] = React.useState(null);
3991
4269
  const [nodeAtMenu, setNodeAtMenu] = React.useState(null);
4270
+ const [selectionMenuPos, setSelectionMenuPos] = React.useState(null);
4271
+ const [selectionMenuOpen, setSelectionMenuOpen] = React.useState(false);
4272
+ // Compute the rectangular screen-space bounds of the current selection
4273
+ const getSelectionScreenBounds = () => {
4274
+ if (typeof document === "undefined")
4275
+ return null;
4276
+ const selection = wb.getSelection();
4277
+ if (!selection.nodes.length)
4278
+ return null;
4279
+ let bounds = null;
4280
+ for (const nodeId of selection.nodes) {
4281
+ const el = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`);
4282
+ if (!el)
4283
+ continue;
4284
+ const rect = el.getBoundingClientRect();
4285
+ if (!bounds) {
4286
+ bounds = {
4287
+ left: rect.left,
4288
+ top: rect.top,
4289
+ right: rect.right,
4290
+ bottom: rect.bottom,
4291
+ };
4292
+ }
4293
+ else {
4294
+ bounds.left = Math.min(bounds.left, rect.left);
4295
+ bounds.top = Math.min(bounds.top, rect.top);
4296
+ bounds.right = Math.max(bounds.right, rect.right);
4297
+ bounds.bottom = Math.max(bounds.bottom, rect.bottom);
4298
+ }
4299
+ }
4300
+ return bounds;
4301
+ };
3992
4302
  const onContextMenu = (e) => {
3993
4303
  e.preventDefault();
3994
- // Determine if right-clicked over a node by hit-testing selection
4304
+ // First, check if the cursor is inside the rectangular bounds of the current selection
4305
+ const selectionBounds = getSelectionScreenBounds();
4306
+ if (selectionBounds) {
4307
+ const { left, top, right, bottom } = selectionBounds;
4308
+ if (e.clientX >= left &&
4309
+ e.clientX <= right &&
4310
+ e.clientY >= top &&
4311
+ e.clientY <= bottom) {
4312
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4313
+ setSelectionMenuOpen(true);
4314
+ setMenuOpen(false);
4315
+ setNodeMenuOpen(false);
4316
+ return;
4317
+ }
4318
+ }
4319
+ // Determine if right-clicked over a node by hit-testing
3995
4320
  const target = e.target?.closest(".react-flow__node");
3996
4321
  if (target) {
3997
4322
  // Resolve node id from data-id attribute React Flow sets
3998
4323
  const nodeId = target.getAttribute("data-id");
3999
- setNodeAtMenu(nodeId);
4000
- setNodeMenuPos({ x: e.clientX, y: e.clientY });
4001
- setNodeMenuOpen(true);
4002
- setMenuOpen(false);
4324
+ const selection = wb.getSelection();
4325
+ const isSelected = nodeId && selection.nodes.includes(nodeId);
4326
+ if (isSelected) {
4327
+ // Right-clicked on a selected node - show selection menu
4328
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4329
+ setSelectionMenuOpen(true);
4330
+ setMenuOpen(false);
4331
+ setNodeMenuOpen(false);
4332
+ return;
4333
+ }
4334
+ else {
4335
+ // Right-clicked on a non-selected node - show node menu
4336
+ setNodeAtMenu(nodeId);
4337
+ setNodeMenuPos({ x: e.clientX, y: e.clientY });
4338
+ setNodeMenuOpen(true);
4339
+ setMenuOpen(false);
4340
+ setSelectionMenuOpen(false);
4341
+ return;
4342
+ }
4003
4343
  }
4004
- else {
4005
- setMenuPos({ x: e.clientX, y: e.clientY });
4006
- setMenuOpen(true);
4007
- setNodeMenuOpen(false);
4344
+ // Check if right-clicked on a selected edge
4345
+ const edgeTarget = e.target?.closest(".react-flow__edge");
4346
+ if (edgeTarget) {
4347
+ const edgeId = edgeTarget.getAttribute("data-id");
4348
+ const selection = wb.getSelection();
4349
+ const isSelected = edgeId && selection.edges.includes(edgeId);
4350
+ if (isSelected) {
4351
+ // Right-clicked on a selected edge - show selection menu
4352
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4353
+ setSelectionMenuOpen(true);
4354
+ setMenuOpen(false);
4355
+ setNodeMenuOpen(false);
4356
+ return;
4357
+ }
4008
4358
  }
4359
+ // Right-clicked on empty space - show default menu
4360
+ setMenuPos({ x: e.clientX, y: e.clientY });
4361
+ setMenuOpen(true);
4362
+ setNodeMenuOpen(false);
4363
+ setSelectionMenuOpen(false);
4009
4364
  };
4010
4365
  const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
4011
4366
  const onCloseMenu = React.useCallback(() => {
@@ -4014,6 +4369,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4014
4369
  const onCloseNodeMenu = React.useCallback(() => {
4015
4370
  setNodeMenuOpen(false);
4016
4371
  }, []);
4372
+ const onCloseSelectionMenu = React.useCallback(() => {
4373
+ setSelectionMenuOpen(false);
4374
+ }, []);
4017
4375
  React.useEffect(() => {
4018
4376
  const off = runner.on("registry", () => {
4019
4377
  setRegistryVersion((v) => v + 1);
@@ -4021,14 +4379,32 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4021
4379
  return () => off();
4022
4380
  }, [runner]);
4023
4381
  const nodeIds = React.useMemo(() => Array.from(registry.nodes.keys()), [registry, registryVersion]);
4024
- const defaultContextMenuHandlers = React.useMemo(() => ({
4025
- onAddNode: addNodeAt,
4026
- onClose: onCloseMenu,
4027
- }), [addNodeAt, onCloseMenu]);
4382
+ const defaultContextMenuHandlers = React.useMemo(() => {
4383
+ const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu);
4384
+ if (overrides?.getDefaultContextMenuHandlers) {
4385
+ return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
4386
+ }
4387
+ return baseHandlers;
4388
+ }, [addNodeAt, onCloseMenu, overrides, wb]);
4389
+ const selectionContextMenuHandlers = React.useMemo(() => {
4390
+ const baseHandlers = createSelectionContextMenuHandlers(wb, onCloseSelectionMenu, overrides?.getDefaultNodeSize, undefined, // onCopyResult - will be provided by overrides
4391
+ runner);
4392
+ if (overrides?.getSelectionContextMenuHandlers) {
4393
+ const selection = wb.getSelection();
4394
+ return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, {
4395
+ getDefaultNodeSize: overrides.getDefaultNodeSize,
4396
+ });
4397
+ }
4398
+ return baseHandlers;
4399
+ }, [wb, runner, overrides, onCloseSelectionMenu]);
4028
4400
  const nodeContextMenuHandlers = React.useMemo(() => {
4029
4401
  if (!nodeAtMenu)
4030
4402
  return null;
4031
- return createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu);
4403
+ const baseHandlers = createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu, overrides?.getDefaultNodeSize);
4404
+ if (overrides?.getNodeContextMenuHandlers) {
4405
+ return overrides.getNodeContextMenuHandlers(wb, nodeAtMenu, baseHandlers);
4406
+ }
4407
+ return baseHandlers;
4032
4408
  }, [
4033
4409
  nodeAtMenu,
4034
4410
  wb,
@@ -4037,6 +4413,8 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4037
4413
  outputsMap,
4038
4414
  outputTypesMap,
4039
4415
  onCloseNodeMenu,
4416
+ overrides?.getDefaultNodeSize,
4417
+ overrides?.getNodeContextMenuHandlers,
4040
4418
  ]);
4041
4419
  const canRunPull = React.useMemo(() => engineKind()?.toString() === "pull", [engineKind]);
4042
4420
  const bakeableOutputs = React.useMemo(() => {
@@ -4092,7 +4470,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4092
4470
  }
4093
4471
  }, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onMoveEnd: onMoveEnd, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", fitView: true, children: [BackgroundRenderer ? (jsxRuntime.jsx(BackgroundRenderer, {})) : (jsxRuntime.jsx(react.Background, { id: "workbench-canvas-background", variant: react.BackgroundVariant.Dots, gap: 12, size: 1 })), MinimapRenderer ? jsxRuntime.jsx(MinimapRenderer, {}) : jsxRuntime.jsx(react.MiniMap, {}), ControlsRenderer ? jsxRuntime.jsx(ControlsRenderer, {}) : jsxRuntime.jsx(react.Controls, {}), DefaultContextMenuRenderer ? (jsxRuntime.jsx(DefaultContextMenuRenderer, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })) : (jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })), !!nodeAtMenu &&
4094
4472
  nodeContextMenuHandlers &&
4095
- (NodeContextMenuRenderer ? (jsxRuntime.jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })) : (jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })))] }) }) }));
4473
+ (NodeContextMenuRenderer ? (jsxRuntime.jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })) : (jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs }))), selectionMenuOpen && selectionMenuPos && (jsxRuntime.jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers }))] }) }) }));
4096
4474
  });
4097
4475
 
4098
4476
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
@@ -4646,12 +5024,18 @@ exports.WorkbenchProvider = WorkbenchProvider;
4646
5024
  exports.WorkbenchStudio = WorkbenchStudio;
4647
5025
  exports.computeEffectiveHandles = computeEffectiveHandles;
4648
5026
  exports.countVisibleHandles = countVisibleHandles;
5027
+ exports.createCopyHandler = createCopyHandler;
5028
+ exports.createDefaultContextMenuHandlers = createDefaultContextMenuHandlers;
4649
5029
  exports.createHandleBounds = createHandleBounds;
4650
5030
  exports.createHandleLayout = createHandleLayout;
5031
+ exports.createNodeContextMenuHandlers = createNodeContextMenuHandlers;
5032
+ exports.createNodeCopyHandler = createNodeCopyHandler;
5033
+ exports.createSelectionContextMenuHandlers = createSelectionContextMenuHandlers;
4651
5034
  exports.download = download;
4652
5035
  exports.estimateNodeSize = estimateNodeSize;
4653
5036
  exports.formatDataUrlAsLabel = formatDataUrlAsLabel;
4654
5037
  exports.formatDeclaredTypeSignature = formatDeclaredTypeSignature;
5038
+ exports.getBakeableOutputs = getBakeableOutputs;
4655
5039
  exports.getHandleBoundsX = getHandleBoundsX;
4656
5040
  exports.getHandleBoundsY = getHandleBoundsY;
4657
5041
  exports.getHandleClassName = getHandleClassName;