@bian-womp/spark-workbench 0.2.55 → 0.2.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cjs/index.cjs CHANGED
@@ -32,13 +32,6 @@ class DefaultUIExtensionRegistry {
32
32
  }
33
33
  return result;
34
34
  }
35
- registerIconProvider(provider) {
36
- this.iconProvider = provider;
37
- return this;
38
- }
39
- getIconProvider() {
40
- return this.iconProvider;
41
- }
42
35
  // React Flow renderers
43
36
  registerConnectionLineRenderer(renderer) {
44
37
  this.connectionLineRenderer = renderer;
@@ -82,6 +75,35 @@ class DefaultUIExtensionRegistry {
82
75
  getNodeContextMenuRenderer() {
83
76
  return this.nodeContextMenuRenderer;
84
77
  }
78
+ // Layout function overrides
79
+ registerEstimateNodeSize(override) {
80
+ this.estimateNodeSizeOverride = override;
81
+ return this;
82
+ }
83
+ getEstimateNodeSize() {
84
+ return this.estimateNodeSizeOverride;
85
+ }
86
+ registerCreateHandleBounds(override) {
87
+ this.createHandleBoundsOverride = override;
88
+ return this;
89
+ }
90
+ getCreateHandleBounds() {
91
+ return this.createHandleBoundsOverride;
92
+ }
93
+ registerCreateHandleLayout(override) {
94
+ this.createHandleLayoutOverride = override;
95
+ return this;
96
+ }
97
+ getCreateHandleLayout() {
98
+ return this.createHandleLayoutOverride;
99
+ }
100
+ registerLayoutNode(override) {
101
+ this.layoutNodeOverride = override;
102
+ return this;
103
+ }
104
+ getLayoutNode() {
105
+ return this.layoutNodeOverride;
106
+ }
85
107
  }
86
108
 
87
109
  class AbstractWorkbench {
@@ -1414,7 +1436,8 @@ function summarizeDeep(value) {
1414
1436
 
1415
1437
  // Shared UI constants for node layout to keep mapping and rendering in sync
1416
1438
  const NODE_HEADER_HEIGHT_PX = 24;
1417
- const NODE_ROW_HEIGHT_PX = 22;
1439
+ const NODE_ROW_HEIGHT_PX = 18;
1440
+ const HANDLE_SIZE_PX = 12;
1418
1441
 
1419
1442
  function computeEffectiveHandles(node, registry) {
1420
1443
  const desc = registry.nodes.get(node.typeId);
@@ -1441,53 +1464,105 @@ function estimateNodeSize(args) {
1441
1464
  const baseWidth = showValues ? 320 : 240;
1442
1465
  const width = overrides?.width ?? baseWidth;
1443
1466
  const height = overrides?.height ?? NODE_HEADER_HEIGHT_PX + rows * NODE_ROW_HEIGHT_PX;
1444
- return { width, height, inputsCount, outputsCount, rowCount: rows };
1467
+ return { width, height };
1445
1468
  }
1446
- function layoutNode(args) {
1447
- const { node, registry, showValues, overrides } = args;
1469
+ /**
1470
+ * Calculate the Y position for handle layout (center of row).
1471
+ * Used for positioning handles in React Flow.
1472
+ */
1473
+ function getHandleLayoutY(rowIndex) {
1474
+ return (NODE_HEADER_HEIGHT_PX +
1475
+ rowIndex * NODE_ROW_HEIGHT_PX +
1476
+ NODE_ROW_HEIGHT_PX / 2);
1477
+ }
1478
+ /**
1479
+ * Calculate the Y position for handle bounds (top + centering offset).
1480
+ * Used for hit-testing and edge routing.
1481
+ */
1482
+ function getHandleBoundsY(rowIndex) {
1483
+ return (NODE_HEADER_HEIGHT_PX +
1484
+ rowIndex * NODE_ROW_HEIGHT_PX +
1485
+ (NODE_ROW_HEIGHT_PX - HANDLE_SIZE_PX) / 2 +
1486
+ 1);
1487
+ }
1488
+ /**
1489
+ * Calculate the X position for handle bounds based on position and node width.
1490
+ */
1491
+ function getHandleBoundsX(position, nodeWidth) {
1492
+ if (position === react.Position.Left) {
1493
+ return -HANDLE_SIZE_PX / 2 + 1;
1494
+ }
1495
+ else {
1496
+ return nodeWidth - HANDLE_SIZE_PX / 2 - 1;
1497
+ }
1498
+ }
1499
+ /**
1500
+ * Create handle bounds object for hit-testing/edge routing.
1501
+ */
1502
+ function createHandleBounds(args) {
1503
+ return {
1504
+ id: args.id,
1505
+ type: args.type,
1506
+ position: args.position,
1507
+ x: getHandleBoundsX(args.position, args.nodeWidth),
1508
+ y: getHandleBoundsY(args.rowIndex),
1509
+ width: HANDLE_SIZE_PX,
1510
+ height: HANDLE_SIZE_PX,
1511
+ };
1512
+ }
1513
+ /**
1514
+ * Create handle layout object for React Flow rendering.
1515
+ */
1516
+ function createHandleLayout(args) {
1517
+ return {
1518
+ id: args.id,
1519
+ type: args.type,
1520
+ position: args.position,
1521
+ y: getHandleLayoutY(args.rowIndex),
1522
+ };
1523
+ }
1524
+ function layoutNode(args, overrides) {
1525
+ const { node, registry, showValues, overrides: sizeOverrides } = args;
1448
1526
  const { inputs, outputs } = computeEffectiveHandles(node, registry);
1449
1527
  const inputOrder = Object.keys(inputs).filter((k) => !sparkGraph.isInputPrivate(inputs, k));
1450
1528
  const outputOrder = Object.keys(outputs);
1451
- const { width, height } = estimateNodeSize({
1529
+ const estimateNodeSizeFn = overrides?.estimateNodeSize ?? estimateNodeSize;
1530
+ const createHandleBoundsFn = overrides?.createHandleBounds ?? createHandleBounds;
1531
+ const createHandleLayoutFn = overrides?.createHandleLayout ?? createHandleLayout;
1532
+ const { width, height } = estimateNodeSizeFn({
1452
1533
  node,
1453
1534
  registry,
1454
1535
  showValues,
1455
- overrides,
1536
+ overrides: sizeOverrides,
1456
1537
  });
1457
- const HEADER = NODE_HEADER_HEIGHT_PX;
1458
- const ROW = NODE_ROW_HEIGHT_PX;
1459
1538
  const handles = [
1460
- ...inputOrder.map((id, i) => ({
1539
+ ...inputOrder.map((id, i) => createHandleBoundsFn({
1461
1540
  id,
1462
1541
  type: "target",
1463
1542
  position: react.Position.Left,
1464
- x: 0,
1465
- y: HEADER + i * ROW,
1466
- width: 1,
1467
- height: ROW + 2,
1543
+ rowIndex: i,
1544
+ nodeWidth: width,
1468
1545
  })),
1469
- ...outputOrder.map((id, i) => ({
1546
+ ...outputOrder.map((id, i) => createHandleBoundsFn({
1470
1547
  id,
1471
1548
  type: "source",
1472
1549
  position: react.Position.Right,
1473
- x: width - 1,
1474
- y: HEADER + i * ROW,
1475
- width: 1,
1476
- height: ROW + 2,
1550
+ rowIndex: i,
1551
+ nodeWidth: width,
1477
1552
  })),
1478
1553
  ];
1479
1554
  const handleLayout = [
1480
- ...inputOrder.map((id, i) => ({
1555
+ ...inputOrder.map((id, i) => createHandleLayoutFn({
1481
1556
  id,
1482
1557
  type: "target",
1483
1558
  position: react.Position.Left,
1484
- y: HEADER + i * ROW + ROW / 2,
1559
+ rowIndex: i,
1485
1560
  })),
1486
- ...outputOrder.map((id, i) => ({
1561
+ ...outputOrder.map((id, i) => createHandleLayoutFn({
1487
1562
  id,
1488
1563
  type: "source",
1489
1564
  position: react.Position.Right,
1490
- y: HEADER + i * ROW + ROW / 2,
1565
+ rowIndex: i,
1491
1566
  })),
1492
1567
  ];
1493
1568
  return { width, height, inputOrder, outputOrder, handles, handleLayout };
@@ -1776,15 +1851,32 @@ function toReactFlow(def, positions, registry, opts) {
1776
1851
  // This map is still used later for certain checks; align with valid handles
1777
1852
  const nodeHandleMap = {};
1778
1853
  Object.assign(nodeHandleMap, validHandleMap);
1854
+ // Get layout function overrides from UI registry
1855
+ const layoutNodeOverride = opts.ui?.getLayoutNode() ?? layoutNode;
1856
+ const createHandleBoundsFn = opts.ui?.getCreateHandleBounds() ?? createHandleBounds;
1857
+ const createHandleLayoutFn = opts.ui?.getCreateHandleLayout() ?? createHandleLayout;
1858
+ const estimateNodeSizeFn = opts.ui?.getEstimateNodeSize() ?? estimateNodeSize;
1779
1859
  const nodes = def.nodes.map((n) => {
1780
1860
  const { inputs: inputSource, outputs: outputSource } = computeEffectiveHandles(n, registry);
1781
1861
  const overrideSize = opts.getDefaultNodeSize?.(n.typeId);
1782
- const geom = layoutNode({
1783
- node: n,
1784
- registry,
1785
- showValues: opts.showValues,
1786
- overrides: overrideSize,
1787
- });
1862
+ // If layoutNode is overridden, use it directly; otherwise use default with internal overrides
1863
+ const geom = layoutNodeOverride
1864
+ ? layoutNodeOverride({
1865
+ node: n,
1866
+ registry,
1867
+ showValues: opts.showValues,
1868
+ overrides: overrideSize,
1869
+ })
1870
+ : layoutNode({
1871
+ node: n,
1872
+ registry,
1873
+ showValues: opts.showValues,
1874
+ overrides: overrideSize,
1875
+ }, {
1876
+ estimateNodeSize: estimateNodeSizeFn,
1877
+ createHandleBounds: createHandleBoundsFn,
1878
+ createHandleLayout: createHandleLayoutFn,
1879
+ });
1788
1880
  const inputHandles = geom.inputOrder.map((id) => ({
1789
1881
  id,
1790
1882
  typeId: sparkGraph.getInputTypeId(inputSource, id),
@@ -1802,20 +1894,22 @@ function toReactFlow(def, positions, registry, opts) {
1802
1894
  const baseRightCount = geom.outputOrder.length;
1803
1895
  const extraInputs = Array.from(missingInputsByNode[n.nodeId] || []);
1804
1896
  const extraOutputs = Array.from(missingOutputsByNode[n.nodeId] || []);
1805
- const HEADER = NODE_HEADER_HEIGHT_PX;
1806
- const ROW = NODE_ROW_HEIGHT_PX;
1807
1897
  const extraHandleLayoutLeft = extraInputs.map((id, i) => ({
1808
- id,
1809
- type: "target",
1810
- position: react.Position.Left,
1811
- y: HEADER + (baseLeftCount + i) * ROW + ROW / 2,
1898
+ ...createHandleLayoutFn({
1899
+ id,
1900
+ type: "target",
1901
+ position: react.Position.Left,
1902
+ rowIndex: baseLeftCount + i,
1903
+ }),
1812
1904
  missing: true,
1813
1905
  }));
1814
1906
  const extraHandleLayoutRight = extraOutputs.map((id, i) => ({
1815
- id,
1816
- type: "source",
1817
- position: react.Position.Right,
1818
- y: HEADER + (baseRightCount + i) * ROW + ROW / 2,
1907
+ ...createHandleLayoutFn({
1908
+ id,
1909
+ type: "source",
1910
+ position: react.Position.Right,
1911
+ rowIndex: baseRightCount + i,
1912
+ }),
1819
1913
  missing: true,
1820
1914
  }));
1821
1915
  const handleLayout = [
@@ -1824,23 +1918,19 @@ function toReactFlow(def, positions, registry, opts) {
1824
1918
  ...extraHandleLayoutRight,
1825
1919
  ];
1826
1920
  // Precompute handle bounds (including missing) so edges can render immediately
1827
- const missingBoundsLeft = extraInputs.map((id, i) => ({
1921
+ const missingBoundsLeft = extraInputs.map((id, i) => createHandleBoundsFn({
1828
1922
  id,
1829
1923
  type: "target",
1830
1924
  position: react.Position.Left,
1831
- x: 0,
1832
- y: HEADER + (baseLeftCount + i) * ROW,
1833
- width: 1,
1834
- height: ROW + 2,
1925
+ rowIndex: baseLeftCount + i,
1926
+ nodeWidth: geom.width,
1835
1927
  }));
1836
- const missingBoundsRight = extraOutputs.map((id, i) => ({
1928
+ const missingBoundsRight = extraOutputs.map((id, i) => createHandleBoundsFn({
1837
1929
  id,
1838
1930
  type: "source",
1839
1931
  position: react.Position.Right,
1840
- x: geom.width - 1,
1841
- y: HEADER + (baseRightCount + i) * ROW,
1842
- width: 1,
1843
- height: ROW + 2,
1932
+ rowIndex: baseRightCount + i,
1933
+ nodeWidth: geom.width,
1844
1934
  }));
1845
1935
  const handles = [
1846
1936
  ...geom.handles,
@@ -1851,7 +1941,7 @@ function toReactFlow(def, positions, registry, opts) {
1851
1941
  const baseRows = Math.max(baseLeftCount, baseRightCount);
1852
1942
  const newRows = Math.max(baseLeftCount + extraInputs.length, baseRightCount + extraOutputs.length);
1853
1943
  const initialWidth = geom.width;
1854
- const initialHeight = geom.height + Math.max(0, newRows - baseRows) * ROW;
1944
+ const initialHeight = geom.height + Math.max(0, newRows - baseRows) * NODE_ROW_HEIGHT_PX;
1855
1945
  return {
1856
1946
  id: n.nodeId,
1857
1947
  data: {
@@ -1987,7 +2077,7 @@ function getHandleClassName(args) {
1987
2077
  else {
1988
2078
  borderColor = "!border-gray-500 dark:!border-gray-400";
1989
2079
  }
1990
- return cx("!w-3 !h-3 !bg-white !dark:bg-stone-900", borderColor, kind === "output" && "!rounded-none");
2080
+ return cx("!w-3 !h-3 !bg-white/50 !dark:bg-stone-900", borderColor, kind === "output" && "!rounded-none");
1991
2081
  }
1992
2082
 
1993
2083
  function generateTimestamp() {
@@ -3059,7 +3149,7 @@ function NodeHandleItem({ kind, id, type, position, y, isConnectable, className,
3059
3149
  textOverflow: "ellipsis",
3060
3150
  }, children: renderLabel({ kind, id }) }))] }));
3061
3151
  }
3062
- function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray-600", outputClassName = "!w-2 !h-2 !bg-gray-600", getClassName, renderLabel, labelClassName = "absolute text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", }) {
3152
+ function NodeHandles({ data, isConnectable, getClassName, renderLabel, labelClassName = "absolute text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", }) {
3063
3153
  const layout = data.handleLayout ?? [];
3064
3154
  const byId = React.useMemo(() => {
3065
3155
  const m = new Map();
@@ -3090,28 +3180,26 @@ function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray
3090
3180
  const placed = byId.get(`target:${h.id}`) ?? byId.get(h.id);
3091
3181
  const position = placed?.position ?? react.Position.Left;
3092
3182
  const y = placed?.y;
3093
- const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ??
3094
- inputClassName;
3183
+ const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ?? "";
3095
3184
  return (jsxRuntime.jsx(NodeHandleItem, { kind: "input", id: h.id, type: "target", position: position, y: y, isConnectable: isConnectable, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, h.id));
3096
3185
  }), missingInputs.map((h) => {
3097
3186
  const key = `missing-input:${h.id}`;
3098
3187
  const position = h.position ?? react.Position.Left;
3099
3188
  const y = h.y;
3100
3189
  const cls = "!w-3 !h-3 !bg-amber-400 !border-amber-500";
3101
- return (jsxRuntime.jsx(NodeHandleItem, { kind: "input", id: h.id, type: "target", position: position, y: y, isConnectable: false, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, key));
3190
+ return (jsxRuntime.jsx(NodeHandleItem, { kind: "input", id: h.id, type: "target", position: position, y: y, isConnectable: false, className: `${cls} wb-nodrag wb-nowheel`, labelClassName: labelClassName, renderLabel: renderLabel }, key));
3102
3191
  }), (data.outputHandles ?? []).map((h) => {
3103
3192
  const placed = byId.get(`source:${h.id}`) ?? byId.get(h.id);
3104
3193
  const position = placed?.position ?? react.Position.Right;
3105
3194
  const y = placed?.y;
3106
- const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ??
3107
- outputClassName;
3108
- return (jsxRuntime.jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: isConnectable, className: `${cls} wb-nodrag wb-nowheel`, labelClassName: labelClassName, renderLabel: renderLabel }, h.id));
3195
+ const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ?? "";
3196
+ return (jsxRuntime.jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: isConnectable, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, h.id));
3109
3197
  }), missingOutputs.map((h) => {
3110
3198
  const key = `missing-output:${h.id}`;
3111
3199
  const position = h.position ?? react.Position.Right;
3112
3200
  const y = h.y;
3113
3201
  const cls = "!w-3 !h-3 !bg-amber-400 !border-amber-500 !rounded-none wb-nodrag wb-nowheel";
3114
- return (jsxRuntime.jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: false, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, key));
3202
+ return (jsxRuntime.jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: false, className: `${cls} wb-nodrag wb-nowheel`, labelClassName: labelClassName, renderLabel: renderLabel }, key));
3115
3203
  })] }));
3116
3204
  }
3117
3205
 
@@ -3140,7 +3228,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
3140
3228
  status,
3141
3229
  validation,
3142
3230
  });
3143
- return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900", containerBorder), style: {
3231
+ return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/50 !dark:bg-stone-900", containerBorder), style: {
3144
3232
  position: "relative",
3145
3233
  minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
3146
3234
  minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
@@ -3519,6 +3607,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3519
3607
  const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, } = useWorkbenchContext();
3520
3608
  const nodeValidation = validationByNode;
3521
3609
  const edgeValidation = validationByEdge.errors;
3610
+ const [registryVersion, setRegistryVersion] = React.useState(0);
3522
3611
  // Keep stable references for nodes/edges to avoid unnecessary updates
3523
3612
  const prevNodesRef = React.useRef([]);
3524
3613
  const prevEdgesRef = React.useRef([]);
@@ -3644,6 +3733,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3644
3733
  selectedNodeIds: new Set(sel.nodes),
3645
3734
  selectedEdgeIds: new Set(sel.edges),
3646
3735
  getDefaultNodeSize,
3736
+ ui,
3647
3737
  });
3648
3738
  // Retain references for unchanged items
3649
3739
  const stableNodes = retainStabilityById(prevNodesRef.current, out.nodes, isSameNode);
@@ -3789,7 +3879,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3789
3879
  const onCloseNodeMenu = React.useCallback(() => {
3790
3880
  setNodeMenuOpen(false);
3791
3881
  }, []);
3792
- const nodeIds = React.useMemo(() => Array.from(registry.nodes.keys()), [registry]);
3882
+ React.useEffect(() => {
3883
+ const off = runner.on("registry", () => {
3884
+ setRegistryVersion((v) => v + 1);
3885
+ });
3886
+ return () => off();
3887
+ }, [runner]);
3888
+ const nodeIds = React.useMemo(() => Array.from(registry.nodes.keys()), [registry, registryVersion]);
3793
3889
  const defaultContextMenuHandlers = React.useMemo(() => ({
3794
3890
  onAddNode: addNodeAt,
3795
3891
  onClose: onCloseMenu,
@@ -4409,11 +4505,16 @@ exports.WorkbenchProvider = WorkbenchProvider;
4409
4505
  exports.WorkbenchStudio = WorkbenchStudio;
4410
4506
  exports.computeEffectiveHandles = computeEffectiveHandles;
4411
4507
  exports.countVisibleHandles = countVisibleHandles;
4508
+ exports.createHandleBounds = createHandleBounds;
4509
+ exports.createHandleLayout = createHandleLayout;
4412
4510
  exports.download = download;
4413
4511
  exports.estimateNodeSize = estimateNodeSize;
4414
4512
  exports.formatDataUrlAsLabel = formatDataUrlAsLabel;
4415
4513
  exports.formatDeclaredTypeSignature = formatDeclaredTypeSignature;
4514
+ exports.getHandleBoundsX = getHandleBoundsX;
4515
+ exports.getHandleBoundsY = getHandleBoundsY;
4416
4516
  exports.getHandleClassName = getHandleClassName;
4517
+ exports.getHandleLayoutY = getHandleLayoutY;
4417
4518
  exports.getNodeBorderClassNames = getNodeBorderClassNames;
4418
4519
  exports.layoutNode = layoutNode;
4419
4520
  exports.preformatValueForDisplay = preformatValueForDisplay;