@bian-womp/spark-workbench 0.2.65 → 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 +453 -215
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +4 -0
  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 +448 -216
  20. package/lib/esm/index.js.map +1 -1
  21. package/lib/esm/src/core/InMemoryWorkbench.d.ts +4 -0
  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/esm/index.js CHANGED
@@ -305,6 +305,22 @@ class InMemoryWorkbench extends AbstractWorkbench {
305
305
  edges: [...this.selection.edges],
306
306
  };
307
307
  }
308
+ /**
309
+ * Delete all selected nodes and edges.
310
+ */
311
+ deleteSelection() {
312
+ const selection = this.getSelection();
313
+ // Delete all selected nodes (this will also remove connected edges)
314
+ for (const nodeId of selection.nodes) {
315
+ this.removeNode(nodeId);
316
+ }
317
+ // Delete remaining selected edges (edges not connected to deleted nodes)
318
+ for (const edgeId of selection.edges) {
319
+ this.disconnect(edgeId);
320
+ }
321
+ // Clear selection
322
+ this.setSelection({ nodes: [], edges: [] });
323
+ }
308
324
  setViewport(viewport, opts) {
309
325
  this.viewport = { ...viewport };
310
326
  this.emit("graphUiChanged", {
@@ -3047,6 +3063,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3047
3063
  updateEdgeType,
3048
3064
  triggerExternal,
3049
3065
  uiVersion,
3066
+ overrides,
3050
3067
  }), [
3051
3068
  wb,
3052
3069
  runner,
@@ -3086,10 +3103,272 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3086
3103
  wb,
3087
3104
  runner,
3088
3105
  uiVersion,
3106
+ overrides,
3089
3107
  ]);
3090
3108
  return (jsx(WorkbenchContext.Provider, { value: value, children: children }));
3091
3109
  }
3092
3110
 
3111
+ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose, getDefaultNodeSize, onCopyResult) {
3112
+ const doBake = async (handleId) => {
3113
+ try {
3114
+ const typeId = outputTypesMap?.[nodeId]?.[handleId];
3115
+ const raw = outputsMap?.[nodeId]?.[handleId];
3116
+ if (!typeId || raw === undefined)
3117
+ return;
3118
+ const unwrap = (v) => isTypedOutput(v) ? getTypedOutputValue(v) : v;
3119
+ const coerceIfNeeded = async (fromType, toType, value) => {
3120
+ if (!toType || toType === fromType || !runner?.coerce)
3121
+ return value;
3122
+ try {
3123
+ return await runner.coerce(fromType, toType, value);
3124
+ }
3125
+ catch {
3126
+ return value;
3127
+ }
3128
+ };
3129
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3130
+ const isArray = typeId.endsWith("[]");
3131
+ const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
3132
+ const tArr = isArray ? registry.types.get(typeId) : undefined;
3133
+ const tElem = registry.types.get(baseTypeId);
3134
+ const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
3135
+ const arrTarget = isArray ? tArr?.bakeTarget : undefined;
3136
+ const elemTarget = isArray ? tElem?.bakeTarget : undefined;
3137
+ if (singleTarget) {
3138
+ const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3139
+ const inType = getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3140
+ const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3141
+ wb.addNode({
3142
+ typeId: singleTarget.nodeTypeId,
3143
+ position: { x: pos.x + 180, y: pos.y },
3144
+ }, { inputs: { [singleTarget.inputHandle]: coerced } });
3145
+ return;
3146
+ }
3147
+ if (isArray && arrTarget) {
3148
+ const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3149
+ const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3150
+ const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3151
+ wb.addNode({
3152
+ typeId: arrTarget.nodeTypeId,
3153
+ position: { x: pos.x + 180, y: pos.y },
3154
+ }, { inputs: { [arrTarget.inputHandle]: coerced } });
3155
+ return;
3156
+ }
3157
+ if (isArray && elemTarget) {
3158
+ const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3159
+ const inType = getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3160
+ const src = unwrap(raw);
3161
+ const items = Array.isArray(src) ? src : [src];
3162
+ const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
3163
+ const COLS = 4;
3164
+ const DX = 180;
3165
+ const DY = 160;
3166
+ for (let idx = 0; idx < coercedItems.length; idx++) {
3167
+ const col = idx % COLS;
3168
+ const row = Math.floor(idx / COLS);
3169
+ wb.addNode({
3170
+ typeId: elemTarget.nodeTypeId,
3171
+ position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3172
+ }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3173
+ }
3174
+ return;
3175
+ }
3176
+ }
3177
+ catch { }
3178
+ };
3179
+ return {
3180
+ onDelete: () => {
3181
+ wb.removeNode(nodeId);
3182
+ onClose();
3183
+ },
3184
+ onDuplicate: async () => {
3185
+ const def = wb.export();
3186
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3187
+ if (!n)
3188
+ return onClose();
3189
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3190
+ const inboundHandles = new Set(def.edges
3191
+ .filter((e) => e.target.nodeId === nodeId)
3192
+ .map((e) => e.target.handle));
3193
+ const allInputs = runner.getInputs(def)[nodeId] || {};
3194
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3195
+ const newNodeId = wb.addNode({
3196
+ typeId: n.typeId,
3197
+ params: n.params,
3198
+ position: { x: pos.x + 24, y: pos.y + 24 },
3199
+ resolvedHandles: n.resolvedHandles,
3200
+ }, {
3201
+ inputs: inputsWithoutBindings,
3202
+ copyOutputsFrom: nodeId,
3203
+ dry: true,
3204
+ });
3205
+ // Select the newly duplicated node
3206
+ wb.setSelection({
3207
+ nodes: [newNodeId],
3208
+ edges: [],
3209
+ });
3210
+ onClose();
3211
+ },
3212
+ onDuplicateWithEdges: async () => {
3213
+ const def = wb.export();
3214
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3215
+ if (!n)
3216
+ return onClose();
3217
+ const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3218
+ // Get inputs without bindings (literal values only)
3219
+ const inputs = runner.getInputs(def)[nodeId] || {};
3220
+ // Add the duplicated node
3221
+ const newNodeId = wb.addNode({
3222
+ typeId: n.typeId,
3223
+ params: n.params,
3224
+ position: { x: pos.x + 24, y: pos.y + 24 },
3225
+ resolvedHandles: n.resolvedHandles,
3226
+ }, {
3227
+ inputs,
3228
+ copyOutputsFrom: nodeId,
3229
+ dry: true,
3230
+ });
3231
+ // Find all incoming edges (edges where target is the original node)
3232
+ const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3233
+ // Duplicate each incoming edge to point to the new node
3234
+ for (const edge of incomingEdges) {
3235
+ wb.connect({
3236
+ source: edge.source, // Keep the same source
3237
+ target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3238
+ typeId: edge.typeId,
3239
+ }, { dry: true });
3240
+ }
3241
+ // Select the newly duplicated node and edges
3242
+ wb.setSelection({
3243
+ nodes: [newNodeId],
3244
+ edges: [],
3245
+ });
3246
+ onClose();
3247
+ },
3248
+ onRunPull: async () => {
3249
+ try {
3250
+ await runner.computeNode(nodeId);
3251
+ }
3252
+ catch { }
3253
+ onClose();
3254
+ },
3255
+ onBake: async (handleId) => {
3256
+ await doBake(handleId);
3257
+ onClose();
3258
+ },
3259
+ onCopy: () => {
3260
+ const copyHandler = createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyResult);
3261
+ copyHandler();
3262
+ onClose();
3263
+ },
3264
+ onCopyId: async () => {
3265
+ try {
3266
+ await navigator.clipboard.writeText(nodeId);
3267
+ }
3268
+ catch { }
3269
+ onClose();
3270
+ },
3271
+ onClose,
3272
+ };
3273
+ }
3274
+ /**
3275
+ * Creates a copy handler that copies the current selection and optionally stores the result.
3276
+ */
3277
+ function createCopyHandler(wb, runner, getDefaultNodeSize, onCopyResult) {
3278
+ return () => {
3279
+ const getNodeSize = getDefaultNodeSize
3280
+ ? (nodeId, typeId) => getDefaultNodeSize(typeId)
3281
+ : undefined;
3282
+ const data = wb.copySelection(runner, getNodeSize);
3283
+ if (onCopyResult) {
3284
+ onCopyResult(data);
3285
+ }
3286
+ };
3287
+ }
3288
+ /**
3289
+ * Creates a copy handler for a single node (temporarily selects it, copies, then restores selection).
3290
+ */
3291
+ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyResult) {
3292
+ return () => {
3293
+ // Select the node first, then copy
3294
+ const currentSelection = wb.getSelection();
3295
+ wb.setSelection({ nodes: [nodeId], edges: [] });
3296
+ const getNodeSize = getDefaultNodeSize
3297
+ ? (nodeId, typeId) => getDefaultNodeSize(typeId)
3298
+ : undefined;
3299
+ const data = wb.copySelection(runner, getNodeSize);
3300
+ // Restore original selection
3301
+ wb.setSelection(currentSelection);
3302
+ if (onCopyResult) {
3303
+ onCopyResult(data);
3304
+ }
3305
+ };
3306
+ }
3307
+ /**
3308
+ * Creates base selection context menu handlers.
3309
+ */
3310
+ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
3311
+ return {
3312
+ onCopy: runner
3313
+ ? createCopyHandler(wb, runner, getDefaultNodeSize, onCopyResult)
3314
+ : () => {
3315
+ // No-op if runner not available
3316
+ onClose();
3317
+ },
3318
+ onDelete: () => {
3319
+ wb.deleteSelection();
3320
+ onClose();
3321
+ },
3322
+ onClose,
3323
+ };
3324
+ }
3325
+ /**
3326
+ * Creates base default context menu handlers.
3327
+ */
3328
+ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste) {
3329
+ return {
3330
+ onAddNode,
3331
+ onPaste,
3332
+ onClose,
3333
+ };
3334
+ }
3335
+ function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3336
+ try {
3337
+ const def = wb.export();
3338
+ const node = def.nodes.find((n) => n.nodeId === nodeId);
3339
+ if (!node)
3340
+ return [];
3341
+ const desc = registry.nodes.get(node.typeId);
3342
+ const handles = Object.keys(desc?.outputs || {});
3343
+ const out = [];
3344
+ for (const h of handles) {
3345
+ const tId = outputTypesMap?.[nodeId]?.[h];
3346
+ if (!tId)
3347
+ continue;
3348
+ if (tId.endsWith("[]")) {
3349
+ const base = tId.slice(0, -2);
3350
+ const tArr = registry.types.get(tId);
3351
+ const tElem = registry.types.get(base);
3352
+ const arrT = tArr?.bakeTarget;
3353
+ const elemT = tElem?.bakeTarget;
3354
+ if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3355
+ (elemT && registry.nodes.has(elemT.nodeTypeId)))
3356
+ out.push(h);
3357
+ }
3358
+ else {
3359
+ const t = registry.types.get(tId);
3360
+ const bt = t?.bakeTarget;
3361
+ if (bt && registry.nodes.has(bt.nodeTypeId))
3362
+ out.push(h);
3363
+ }
3364
+ }
3365
+ return out;
3366
+ }
3367
+ catch {
3368
+ return [];
3369
+ }
3370
+ }
3371
+
3093
3372
  function IssueBadge({ level, title, size = 12, className, }) {
3094
3373
  const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
3095
3374
  return (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" ? (jsx(XCircleIcon, { size: size, weight: "fill" })) : (jsx(WarningCircleIcon, { size: size, weight: "fill" })) }));
@@ -3302,7 +3581,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3302
3581
  }
3303
3582
  catch { }
3304
3583
  };
3305
- return (jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (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: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsx("div", { className: "break-words", children: err.message }), jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (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: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsx("div", { className: "break-words", children: err.message })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (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: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Registry Error" }), jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxs("div", { children: [jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (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) => {
3584
+ return (jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto select-none`, children: [jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (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: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsx("div", { className: "break-words", children: err.message }), jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (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: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsx("div", { className: "break-words", children: err.message })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (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: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Registry Error" }), jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxs("div", { children: [jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (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) => {
3306
3585
  e.stopPropagation();
3307
3586
  deleteEdgeById(m.data?.edgeId);
3308
3587
  }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxs("div", { children: [jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), renderEdgeStatus(), jsx("div", { className: "mt-1", children: jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
@@ -3370,7 +3649,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3370
3649
  const title = inIssues
3371
3650
  .map((v) => `${v.code}: ${v.message}`)
3372
3651
  .join("; ");
3373
- return (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-32 flex flex-col", children: [jsx("span", { children: prettyHandle(h) }), jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxs("div", { className: "flex items-center gap-1 flex-1", children: [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
3652
+ return (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-32 flex flex-col", children: [jsx("span", { children: prettyHandle(h) }), jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxs("div", { className: "flex items-center gap-1 flex-1", children: [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
3374
3653
  ? String(current)
3375
3654
  : "", onChange: (e) => {
3376
3655
  const val = e.target.value;
@@ -3384,7 +3663,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3384
3663
  ? `Default: ${placeholder}`
3385
3664
  : "(select)" }), registry.enums
3386
3665
  .get(typeId)
3387
- ?.options.map((opt) => (jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (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: jsx(XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsx("div", { className: "flex items-center gap-1 flex-1", children: jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxs("div", { className: "flex items-center gap-1 flex-1", children: [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
3666
+ ?.options.map((opt) => (jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (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: jsx(XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsx("div", { className: "flex items-center gap-1 flex-1", children: jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxs("div", { className: "flex items-center gap-1 flex-1", children: [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
3388
3667
  ? `Default: ${placeholder}`
3389
3668
  : undefined, value: displayValue, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
3390
3669
  if (e.key === "Enter")
@@ -3637,6 +3916,13 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3637
3916
  handlers.onAddNode(typeId, { position: p });
3638
3917
  handlers.onClose();
3639
3918
  };
3919
+ const handlePaste = () => {
3920
+ if (!handlers.onPaste)
3921
+ return;
3922
+ const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
3923
+ handlers.onPaste(p);
3924
+ handlers.onClose();
3925
+ };
3640
3926
  const renderTree = (tree, path = []) => {
3641
3927
  const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
3642
3928
  return (jsx("div", { children: entries.map(([key, child]) => {
@@ -3651,10 +3937,10 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3651
3937
  return (jsxs("div", { children: [jsx("div", { className: "px-2 py-1 text-[11px] uppercase tracking-wide text-gray-400", children: label }), child.__self && (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 })), jsx("div", { className: "pl-2 border-l border-gray-200 ml-2", children: renderTree(child, [...path, key]) })] }, idKey));
3652
3938
  }) }));
3653
3939
  };
3654
- return (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) => {
3940
+ return (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) => {
3655
3941
  e.preventDefault();
3656
3942
  e.stopPropagation();
3657
- }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsx("div", { className: "px-2 pb-1", children: 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() }) }), jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
3943
+ }, children: [handlers.onPaste && (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 && jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsx("div", { className: "px-2 pb-1", children: 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() }) }), jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
3658
3944
  }
3659
3945
 
3660
3946
  function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, }) {
@@ -3692,209 +3978,55 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
3692
3978
  const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
3693
3979
  (MENU_MIN_WIDTH + PADDING));
3694
3980
  const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
3695
- return (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) => {
3981
+ return (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) => {
3696
3982
  e.preventDefault();
3697
3983
  e.stopPropagation();
3698
- }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), bakeableOutputs.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h))), jsx("div", { className: "h-px bg-gray-200 my-1" })] })), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" })] }));
3984
+ }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopy, children: "Copy" }), 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 && (jsxs(Fragment, { children: [jsx("div", { className: "h-px bg-gray-200 my-1" }), jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h)))] }))] }));
3699
3985
  }
3700
3986
 
3701
- function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose) {
3702
- const doBake = async (handleId) => {
3703
- try {
3704
- const typeId = outputTypesMap?.[nodeId]?.[handleId];
3705
- const raw = outputsMap?.[nodeId]?.[handleId];
3706
- if (!typeId || raw === undefined)
3707
- return;
3708
- const unwrap = (v) => isTypedOutput(v) ? getTypedOutputValue(v) : v;
3709
- const coerceIfNeeded = async (fromType, toType, value) => {
3710
- if (!toType || toType === fromType || !runner?.coerce)
3711
- return value;
3712
- try {
3713
- return await runner.coerce(fromType, toType, value);
3714
- }
3715
- catch {
3716
- return value;
3717
- }
3718
- };
3719
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3720
- const isArray = typeId.endsWith("[]");
3721
- const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
3722
- const tArr = isArray ? registry.types.get(typeId) : undefined;
3723
- const tElem = registry.types.get(baseTypeId);
3724
- const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
3725
- const arrTarget = isArray ? tArr?.bakeTarget : undefined;
3726
- const elemTarget = isArray ? tElem?.bakeTarget : undefined;
3727
- if (singleTarget) {
3728
- const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3729
- const inType = getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3730
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3731
- wb.addNode({
3732
- typeId: singleTarget.nodeTypeId,
3733
- position: { x: pos.x + 180, y: pos.y },
3734
- }, { inputs: { [singleTarget.inputHandle]: coerced } });
3735
- return;
3736
- }
3737
- if (isArray && arrTarget) {
3738
- const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3739
- const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3740
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3741
- wb.addNode({
3742
- typeId: arrTarget.nodeTypeId,
3743
- position: { x: pos.x + 180, y: pos.y },
3744
- }, { inputs: { [arrTarget.inputHandle]: coerced } });
3745
- return;
3746
- }
3747
- if (isArray && elemTarget) {
3748
- const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3749
- const inType = getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3750
- const src = unwrap(raw);
3751
- const items = Array.isArray(src) ? src : [src];
3752
- const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
3753
- const COLS = 4;
3754
- const DX = 180;
3755
- const DY = 160;
3756
- for (let idx = 0; idx < coercedItems.length; idx++) {
3757
- const col = idx % COLS;
3758
- const row = Math.floor(idx / COLS);
3759
- wb.addNode({
3760
- typeId: elemTarget.nodeTypeId,
3761
- position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3762
- }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3763
- }
3987
+ function SelectionContextMenu({ open, clientPos, handlers, }) {
3988
+ const ref = useRef(null);
3989
+ // Close on outside click and on ESC
3990
+ useEffect(() => {
3991
+ if (!open)
3992
+ return;
3993
+ const onDown = (e) => {
3994
+ if (!ref.current)
3764
3995
  return;
3765
- }
3766
- }
3767
- catch { }
3768
- };
3769
- return {
3770
- onDelete: () => {
3771
- wb.removeNode(nodeId);
3772
- onClose();
3773
- },
3774
- onDuplicate: async () => {
3775
- const def = wb.export();
3776
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3777
- if (!n)
3778
- return onClose();
3779
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3780
- const inboundHandles = new Set(def.edges
3781
- .filter((e) => e.target.nodeId === nodeId)
3782
- .map((e) => e.target.handle));
3783
- const allInputs = runner.getInputs(def)[nodeId] || {};
3784
- const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3785
- const newNodeId = wb.addNode({
3786
- typeId: n.typeId,
3787
- params: n.params,
3788
- position: { x: pos.x + 24, y: pos.y + 24 },
3789
- resolvedHandles: n.resolvedHandles,
3790
- }, {
3791
- inputs: inputsWithoutBindings,
3792
- copyOutputsFrom: nodeId,
3793
- dry: true,
3794
- });
3795
- // Select the newly duplicated node
3796
- wb.setSelection({
3797
- nodes: [newNodeId],
3798
- edges: [],
3799
- });
3800
- onClose();
3801
- },
3802
- onDuplicateWithEdges: async () => {
3803
- const def = wb.export();
3804
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3805
- if (!n)
3806
- return onClose();
3807
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3808
- // Get inputs without bindings (literal values only)
3809
- const inputs = runner.getInputs(def)[nodeId] || {};
3810
- // Add the duplicated node
3811
- const newNodeId = wb.addNode({
3812
- typeId: n.typeId,
3813
- params: n.params,
3814
- position: { x: pos.x + 24, y: pos.y + 24 },
3815
- resolvedHandles: n.resolvedHandles,
3816
- }, {
3817
- inputs,
3818
- copyOutputsFrom: nodeId,
3819
- dry: true,
3820
- });
3821
- // Find all incoming edges (edges where target is the original node)
3822
- const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3823
- // Duplicate each incoming edge to point to the new node
3824
- for (const edge of incomingEdges) {
3825
- wb.connect({
3826
- source: edge.source, // Keep the same source
3827
- target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3828
- typeId: edge.typeId,
3829
- }, { dry: true });
3830
- }
3831
- // Select the newly duplicated node and edges
3832
- wb.setSelection({
3833
- nodes: [newNodeId],
3834
- edges: [],
3835
- });
3836
- onClose();
3837
- },
3838
- onRunPull: async () => {
3839
- try {
3840
- await runner.computeNode(nodeId);
3841
- }
3842
- catch { }
3843
- onClose();
3844
- },
3845
- onBake: async (handleId) => {
3846
- await doBake(handleId);
3847
- onClose();
3848
- },
3849
- onCopyId: async () => {
3850
- try {
3851
- await navigator.clipboard.writeText(nodeId);
3852
- }
3853
- catch { }
3854
- onClose();
3855
- },
3856
- onClose,
3857
- };
3858
- }
3859
- function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3860
- try {
3861
- const def = wb.export();
3862
- const node = def.nodes.find((n) => n.nodeId === nodeId);
3863
- if (!node)
3864
- return [];
3865
- const desc = registry.nodes.get(node.typeId);
3866
- const handles = Object.keys(desc?.outputs || {});
3867
- const out = [];
3868
- for (const h of handles) {
3869
- const tId = outputTypesMap?.[nodeId]?.[h];
3870
- if (!tId)
3871
- continue;
3872
- if (tId.endsWith("[]")) {
3873
- const base = tId.slice(0, -2);
3874
- const tArr = registry.types.get(tId);
3875
- const tElem = registry.types.get(base);
3876
- const arrT = tArr?.bakeTarget;
3877
- const elemT = tElem?.bakeTarget;
3878
- if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3879
- (elemT && registry.nodes.has(elemT.nodeTypeId)))
3880
- out.push(h);
3881
- }
3882
- else {
3883
- const t = registry.types.get(tId);
3884
- const bt = t?.bakeTarget;
3885
- if (bt && registry.nodes.has(bt.nodeTypeId))
3886
- out.push(h);
3887
- }
3888
- }
3889
- return out;
3890
- }
3891
- catch {
3892
- return [];
3893
- }
3996
+ if (!ref.current.contains(e.target))
3997
+ handlers.onClose();
3998
+ };
3999
+ const onKey = (e) => {
4000
+ if (e.key === "Escape")
4001
+ handlers.onClose();
4002
+ };
4003
+ window.addEventListener("mousedown", onDown, true);
4004
+ window.addEventListener("keydown", onKey);
4005
+ return () => {
4006
+ window.removeEventListener("mousedown", onDown, true);
4007
+ window.removeEventListener("keydown", onKey);
4008
+ };
4009
+ }, [open, handlers]);
4010
+ useEffect(() => {
4011
+ if (open)
4012
+ ref.current?.focus();
4013
+ }, [open]);
4014
+ if (!open || !clientPos)
4015
+ return null;
4016
+ // Clamp menu position to viewport
4017
+ const MENU_MIN_WIDTH = 180;
4018
+ const PADDING = 16;
4019
+ const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
4020
+ (MENU_MIN_WIDTH + PADDING));
4021
+ const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 100);
4022
+ return (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) => {
4023
+ e.preventDefault();
4024
+ e.stopPropagation();
4025
+ }, children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopy, children: "Copy" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" })] }));
3894
4026
  }
3895
4027
 
3896
4028
  const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
3897
- const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, } = useWorkbenchContext();
4029
+ const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, overrides, } = useWorkbenchContext();
3898
4030
  const nodeValidation = validationByNode;
3899
4031
  const edgeValidation = validationByEdge.errors;
3900
4032
  const [registryVersion, setRegistryVersion] = useState(0);
@@ -4133,23 +4265,100 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4133
4265
  const [nodeMenuOpen, setNodeMenuOpen] = useState(false);
4134
4266
  const [nodeMenuPos, setNodeMenuPos] = useState(null);
4135
4267
  const [nodeAtMenu, setNodeAtMenu] = useState(null);
4268
+ const [selectionMenuPos, setSelectionMenuPos] = useState(null);
4269
+ const [selectionMenuOpen, setSelectionMenuOpen] = useState(false);
4270
+ // Compute the rectangular screen-space bounds of the current selection
4271
+ const getSelectionScreenBounds = () => {
4272
+ if (typeof document === "undefined")
4273
+ return null;
4274
+ const selection = wb.getSelection();
4275
+ if (!selection.nodes.length)
4276
+ return null;
4277
+ let bounds = null;
4278
+ for (const nodeId of selection.nodes) {
4279
+ const el = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`);
4280
+ if (!el)
4281
+ continue;
4282
+ const rect = el.getBoundingClientRect();
4283
+ if (!bounds) {
4284
+ bounds = {
4285
+ left: rect.left,
4286
+ top: rect.top,
4287
+ right: rect.right,
4288
+ bottom: rect.bottom,
4289
+ };
4290
+ }
4291
+ else {
4292
+ bounds.left = Math.min(bounds.left, rect.left);
4293
+ bounds.top = Math.min(bounds.top, rect.top);
4294
+ bounds.right = Math.max(bounds.right, rect.right);
4295
+ bounds.bottom = Math.max(bounds.bottom, rect.bottom);
4296
+ }
4297
+ }
4298
+ return bounds;
4299
+ };
4136
4300
  const onContextMenu = (e) => {
4137
4301
  e.preventDefault();
4138
- // Determine if right-clicked over a node by hit-testing selection
4302
+ // First, check if the cursor is inside the rectangular bounds of the current selection
4303
+ const selectionBounds = getSelectionScreenBounds();
4304
+ if (selectionBounds) {
4305
+ const { left, top, right, bottom } = selectionBounds;
4306
+ if (e.clientX >= left &&
4307
+ e.clientX <= right &&
4308
+ e.clientY >= top &&
4309
+ e.clientY <= bottom) {
4310
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4311
+ setSelectionMenuOpen(true);
4312
+ setMenuOpen(false);
4313
+ setNodeMenuOpen(false);
4314
+ return;
4315
+ }
4316
+ }
4317
+ // Determine if right-clicked over a node by hit-testing
4139
4318
  const target = e.target?.closest(".react-flow__node");
4140
4319
  if (target) {
4141
4320
  // Resolve node id from data-id attribute React Flow sets
4142
4321
  const nodeId = target.getAttribute("data-id");
4143
- setNodeAtMenu(nodeId);
4144
- setNodeMenuPos({ x: e.clientX, y: e.clientY });
4145
- setNodeMenuOpen(true);
4146
- setMenuOpen(false);
4322
+ const selection = wb.getSelection();
4323
+ const isSelected = nodeId && selection.nodes.includes(nodeId);
4324
+ if (isSelected) {
4325
+ // Right-clicked on a selected node - show selection menu
4326
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4327
+ setSelectionMenuOpen(true);
4328
+ setMenuOpen(false);
4329
+ setNodeMenuOpen(false);
4330
+ return;
4331
+ }
4332
+ else {
4333
+ // Right-clicked on a non-selected node - show node menu
4334
+ setNodeAtMenu(nodeId);
4335
+ setNodeMenuPos({ x: e.clientX, y: e.clientY });
4336
+ setNodeMenuOpen(true);
4337
+ setMenuOpen(false);
4338
+ setSelectionMenuOpen(false);
4339
+ return;
4340
+ }
4147
4341
  }
4148
- else {
4149
- setMenuPos({ x: e.clientX, y: e.clientY });
4150
- setMenuOpen(true);
4151
- setNodeMenuOpen(false);
4342
+ // Check if right-clicked on a selected edge
4343
+ const edgeTarget = e.target?.closest(".react-flow__edge");
4344
+ if (edgeTarget) {
4345
+ const edgeId = edgeTarget.getAttribute("data-id");
4346
+ const selection = wb.getSelection();
4347
+ const isSelected = edgeId && selection.edges.includes(edgeId);
4348
+ if (isSelected) {
4349
+ // Right-clicked on a selected edge - show selection menu
4350
+ setSelectionMenuPos({ x: e.clientX, y: e.clientY });
4351
+ setSelectionMenuOpen(true);
4352
+ setMenuOpen(false);
4353
+ setNodeMenuOpen(false);
4354
+ return;
4355
+ }
4152
4356
  }
4357
+ // Right-clicked on empty space - show default menu
4358
+ setMenuPos({ x: e.clientX, y: e.clientY });
4359
+ setMenuOpen(true);
4360
+ setNodeMenuOpen(false);
4361
+ setSelectionMenuOpen(false);
4153
4362
  };
4154
4363
  const addNodeAt = useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
4155
4364
  const onCloseMenu = useCallback(() => {
@@ -4158,6 +4367,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4158
4367
  const onCloseNodeMenu = useCallback(() => {
4159
4368
  setNodeMenuOpen(false);
4160
4369
  }, []);
4370
+ const onCloseSelectionMenu = useCallback(() => {
4371
+ setSelectionMenuOpen(false);
4372
+ }, []);
4161
4373
  useEffect(() => {
4162
4374
  const off = runner.on("registry", () => {
4163
4375
  setRegistryVersion((v) => v + 1);
@@ -4165,14 +4377,32 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4165
4377
  return () => off();
4166
4378
  }, [runner]);
4167
4379
  const nodeIds = useMemo(() => Array.from(registry.nodes.keys()), [registry, registryVersion]);
4168
- const defaultContextMenuHandlers = useMemo(() => ({
4169
- onAddNode: addNodeAt,
4170
- onClose: onCloseMenu,
4171
- }), [addNodeAt, onCloseMenu]);
4380
+ const defaultContextMenuHandlers = useMemo(() => {
4381
+ const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu);
4382
+ if (overrides?.getDefaultContextMenuHandlers) {
4383
+ return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
4384
+ }
4385
+ return baseHandlers;
4386
+ }, [addNodeAt, onCloseMenu, overrides, wb]);
4387
+ const selectionContextMenuHandlers = useMemo(() => {
4388
+ const baseHandlers = createSelectionContextMenuHandlers(wb, onCloseSelectionMenu, overrides?.getDefaultNodeSize, undefined, // onCopyResult - will be provided by overrides
4389
+ runner);
4390
+ if (overrides?.getSelectionContextMenuHandlers) {
4391
+ const selection = wb.getSelection();
4392
+ return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, {
4393
+ getDefaultNodeSize: overrides.getDefaultNodeSize,
4394
+ });
4395
+ }
4396
+ return baseHandlers;
4397
+ }, [wb, runner, overrides, onCloseSelectionMenu]);
4172
4398
  const nodeContextMenuHandlers = useMemo(() => {
4173
4399
  if (!nodeAtMenu)
4174
4400
  return null;
4175
- return createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu);
4401
+ const baseHandlers = createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu, overrides?.getDefaultNodeSize);
4402
+ if (overrides?.getNodeContextMenuHandlers) {
4403
+ return overrides.getNodeContextMenuHandlers(wb, nodeAtMenu, baseHandlers);
4404
+ }
4405
+ return baseHandlers;
4176
4406
  }, [
4177
4407
  nodeAtMenu,
4178
4408
  wb,
@@ -4181,6 +4411,8 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4181
4411
  outputsMap,
4182
4412
  outputTypesMap,
4183
4413
  onCloseNodeMenu,
4414
+ overrides?.getDefaultNodeSize,
4415
+ overrides?.getNodeContextMenuHandlers,
4184
4416
  ]);
4185
4417
  const canRunPull = useMemo(() => engineKind()?.toString() === "pull", [engineKind]);
4186
4418
  const bakeableOutputs = useMemo(() => {
@@ -4236,7 +4468,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4236
4468
  }
4237
4469
  }, 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 ? (jsx(BackgroundRenderer, {})) : (jsx(Background, { id: "workbench-canvas-background", variant: BackgroundVariant.Dots, gap: 12, size: 1 })), MinimapRenderer ? jsx(MinimapRenderer, {}) : jsx(MiniMap, {}), ControlsRenderer ? jsx(ControlsRenderer, {}) : jsx(Controls, {}), DefaultContextMenuRenderer ? (jsx(DefaultContextMenuRenderer, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })) : (jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })), !!nodeAtMenu &&
4238
4470
  nodeContextMenuHandlers &&
4239
- (NodeContextMenuRenderer ? (jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })) : (jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })))] }) }) }));
4471
+ (NodeContextMenuRenderer ? (jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })) : (jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs }))), selectionMenuOpen && selectionMenuPos && (jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers }))] }) }) }));
4240
4472
  });
4241
4473
 
4242
4474
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
@@ -4773,5 +5005,5 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
4773
5005
  return (jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, uiVersion: uiVersion, children: jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: onBackendKindChangeWithDispose, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, overrides: overrides, onInit: onInit, onChange: onChange }) }));
4774
5006
  }
4775
5007
 
4776
- export { AbstractWorkbench, CLIWorkbench, DefaultNode, DefaultNodeContent, DefaultNodeHeader, DefaultUIExtensionRegistry, InMemoryWorkbench, Inspector, LocalGraphRunner, NodeHandles, RemoteGraphRunner, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, computeEffectiveHandles, countVisibleHandles, createHandleBounds, createHandleLayout, download, estimateNodeSize, formatDataUrlAsLabel, formatDeclaredTypeSignature, getHandleBoundsX, getHandleBoundsY, getHandleClassName, getHandleLayoutY, getNodeBorderClassNames, layoutNode, preformatValueForDisplay, prettyHandle, resolveOutputDisplay, summarizeDeep, toReactFlow, upload, useQueryParamBoolean, useQueryParamString, useThrottledValue, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
5008
+ export { AbstractWorkbench, CLIWorkbench, DefaultNode, DefaultNodeContent, DefaultNodeHeader, DefaultUIExtensionRegistry, InMemoryWorkbench, Inspector, LocalGraphRunner, NodeHandles, RemoteGraphRunner, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, computeEffectiveHandles, countVisibleHandles, createCopyHandler, createDefaultContextMenuHandlers, createHandleBounds, createHandleLayout, createNodeContextMenuHandlers, createNodeCopyHandler, createSelectionContextMenuHandlers, download, estimateNodeSize, formatDataUrlAsLabel, formatDeclaredTypeSignature, getBakeableOutputs, getHandleBoundsX, getHandleBoundsY, getHandleClassName, getHandleLayoutY, getNodeBorderClassNames, layoutNode, preformatValueForDisplay, prettyHandle, resolveOutputDisplay, summarizeDeep, toReactFlow, upload, useQueryParamBoolean, useQueryParamString, useThrottledValue, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
4777
5009
  //# sourceMappingURL=index.js.map