@particle-academy/react-fancy 2.8.1 → 2.10.0

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/dist/index.cjs CHANGED
@@ -11771,7 +11771,7 @@ function useCanvas() {
11771
11771
  return ctx;
11772
11772
  }
11773
11773
  function CanvasNode({ children, id, x, y, draggable, onPositionChange, className, style }) {
11774
- const { registerNode, unregisterNode, viewport } = useCanvas();
11774
+ const { registerNode, unregisterNode, viewport, gridSize, snapToGrid } = useCanvas();
11775
11775
  const nodeRef = react.useRef(null);
11776
11776
  const isDragging = react.useRef(false);
11777
11777
  const dragStart = react.useRef({ mouseX: 0, mouseY: 0, nodeX: 0, nodeY: 0 });
@@ -11804,9 +11804,15 @@ function CanvasNode({ children, id, x, y, draggable, onPositionChange, className
11804
11804
  if (!isDragging.current) return;
11805
11805
  const dx = (e.clientX - dragStart.current.mouseX) / viewport.zoom;
11806
11806
  const dy = (e.clientY - dragStart.current.mouseY) / viewport.zoom;
11807
- onPositionChange?.(dragStart.current.nodeX + dx, dragStart.current.nodeY + dy);
11807
+ let nx = dragStart.current.nodeX + dx;
11808
+ let ny = dragStart.current.nodeY + dy;
11809
+ if (snapToGrid && gridSize > 0) {
11810
+ nx = Math.round(nx / gridSize) * gridSize;
11811
+ ny = Math.round(ny / gridSize) * gridSize;
11812
+ }
11813
+ onPositionChange?.(nx, ny);
11808
11814
  },
11809
- [viewport.zoom, onPositionChange]
11815
+ [viewport.zoom, onPositionChange, snapToGrid, gridSize]
11810
11816
  );
11811
11817
  const handlePointerUp = react.useCallback(() => {
11812
11818
  isDragging.current = false;
@@ -12054,6 +12060,10 @@ function CanvasRoot({
12054
12060
  pannable = true,
12055
12061
  zoomable = true,
12056
12062
  showGrid = false,
12063
+ gridStyle = "dots",
12064
+ gridSize = 20,
12065
+ gridColor = "rgb(161 161 170 / 0.3)",
12066
+ snapToGrid = false,
12057
12067
  fitOnMount = false,
12058
12068
  className,
12059
12069
  style
@@ -12071,8 +12081,8 @@ function CanvasRoot({
12071
12081
  containerRef
12072
12082
  });
12073
12083
  const ctx = react.useMemo(
12074
- () => ({ viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion, containerRef }),
12075
- [viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion]
12084
+ () => ({ viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion, containerRef, gridSize, snapToGrid }),
12085
+ [viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion, gridSize, snapToGrid]
12076
12086
  );
12077
12087
  const hasFitted = react.useRef(false);
12078
12088
  react.useEffect(() => {
@@ -12128,9 +12138,13 @@ function CanvasRoot({
12128
12138
  {
12129
12139
  "data-canvas-bg": "",
12130
12140
  className: "absolute inset-0",
12131
- style: showGrid ? {
12132
- backgroundImage: `radial-gradient(circle, rgb(161 161 170 / 0.3) 1px, transparent 1px)`,
12133
- backgroundSize: `${20 * viewport.zoom}px ${20 * viewport.zoom}px`,
12141
+ style: showGrid && gridStyle !== "none" ? gridStyle === "lines" ? {
12142
+ backgroundImage: `linear-gradient(to right, ${gridColor} 1px, transparent 1px), linear-gradient(to bottom, ${gridColor} 1px, transparent 1px)`,
12143
+ backgroundSize: `${gridSize * viewport.zoom}px ${gridSize * viewport.zoom}px`,
12144
+ backgroundPosition: `${viewport.panX}px ${viewport.panY}px`
12145
+ } : {
12146
+ backgroundImage: `radial-gradient(circle, ${gridColor} 1px, transparent 1px)`,
12147
+ backgroundSize: `${gridSize * viewport.zoom}px ${gridSize * viewport.zoom}px`,
12134
12148
  backgroundPosition: `${viewport.panX}px ${viewport.panY}px`
12135
12149
  } : void 0
12136
12150
  }
@@ -12270,65 +12284,615 @@ function DiagramEntity({
12270
12284
  ) });
12271
12285
  }
12272
12286
  DiagramEntity.displayName = "DiagramEntity";
12273
- var HEADER_HEIGHT = 36;
12274
- var FIELD_HEIGHT = 29;
12275
- var SYMBOL_SIZE = 12;
12276
- function oneSymbol(pt, direction) {
12277
- const s = SYMBOL_SIZE * 0.6;
12287
+
12288
+ // src/components/Diagram/diagram.markers.ts
12289
+ function defaultMarkersForType(type) {
12290
+ switch (type) {
12291
+ case "one-to-one":
12292
+ return { fromMarker: "one", toMarker: "one", lineStyle: "solid" };
12293
+ case "one-to-many":
12294
+ return { fromMarker: "one", toMarker: "many", lineStyle: "solid" };
12295
+ case "many-to-one":
12296
+ return { fromMarker: "many", toMarker: "one", lineStyle: "solid" };
12297
+ case "many-to-many":
12298
+ return { fromMarker: "many", toMarker: "many", lineStyle: "solid" };
12299
+ case "association":
12300
+ return { fromMarker: "none", toMarker: "arrow", lineStyle: "solid" };
12301
+ case "aggregation":
12302
+ return { fromMarker: "diamond-open", toMarker: "none", lineStyle: "solid" };
12303
+ case "composition":
12304
+ return { fromMarker: "diamond", toMarker: "none", lineStyle: "solid" };
12305
+ case "inheritance":
12306
+ return { fromMarker: "none", toMarker: "triangle-open", lineStyle: "solid" };
12307
+ case "implementation":
12308
+ return { fromMarker: "none", toMarker: "triangle-open", lineStyle: "dashed" };
12309
+ case "dependency":
12310
+ return { fromMarker: "none", toMarker: "arrow", lineStyle: "dashed" };
12311
+ default:
12312
+ return { fromMarker: "none", toMarker: "none", lineStyle: "solid" };
12313
+ }
12314
+ }
12315
+ var SIZE = 12;
12316
+ function renderMarker(marker, pt, direction) {
12317
+ if (typeof marker === "string" && marker.startsWith("emoji:")) {
12318
+ return { paths: [], text: marker.slice(6) };
12319
+ }
12320
+ switch (marker) {
12321
+ case "none":
12322
+ return null;
12323
+ case "arrow":
12324
+ return { paths: [{ d: arrowPath(pt, direction), fill: "stroke" }] };
12325
+ case "arrow-open":
12326
+ return { paths: [{ d: arrowPath(pt, direction), fill: "none" }] };
12327
+ case "circle":
12328
+ return { paths: [{ d: circlePath(pt), fill: "stroke" }] };
12329
+ case "circle-open":
12330
+ return { paths: [{ d: circlePath(pt), fill: "background" }] };
12331
+ case "square":
12332
+ return { paths: [{ d: squarePath(pt, direction), fill: "stroke" }] };
12333
+ case "square-open":
12334
+ return { paths: [{ d: squarePath(pt, direction), fill: "background" }] };
12335
+ case "diamond":
12336
+ return { paths: [{ d: diamondPath(pt, direction), fill: "stroke" }] };
12337
+ case "diamond-open":
12338
+ return { paths: [{ d: diamondPath(pt, direction), fill: "background" }] };
12339
+ case "triangle":
12340
+ return { paths: [{ d: trianglePath(pt, direction), fill: "stroke" }] };
12341
+ case "triangle-open":
12342
+ return { paths: [{ d: trianglePath(pt, direction), fill: "background" }] };
12343
+ case "cross":
12344
+ return { paths: [{ d: crossPath(pt, direction), fill: "none" }] };
12345
+ case "one":
12346
+ return { paths: [{ d: oneSymbol(pt, direction), fill: "none" }] };
12347
+ case "many":
12348
+ return { paths: [{ d: crowFootSymbol(pt, direction), fill: "none" }] };
12349
+ case "optional-one":
12350
+ return {
12351
+ paths: [
12352
+ { d: circleOuter(pt, direction), fill: "background" },
12353
+ { d: oneSymbolOffset(pt, direction), fill: "none" }
12354
+ ]
12355
+ };
12356
+ case "optional-many":
12357
+ return {
12358
+ paths: [
12359
+ { d: circleOuter(pt, direction), fill: "background" },
12360
+ { d: crowFootSymbolOffset(pt, direction), fill: "none" }
12361
+ ]
12362
+ };
12363
+ default:
12364
+ if (typeof marker === "string" && marker !== "") {
12365
+ return { paths: [], text: marker };
12366
+ }
12367
+ return null;
12368
+ }
12369
+ }
12370
+ function markerInset(marker) {
12371
+ if (marker === "none" || marker === void 0) return 0;
12372
+ if (typeof marker === "string" && (marker.startsWith("emoji:") || !KNOWN_MARKERS.has(marker))) {
12373
+ return SIZE;
12374
+ }
12375
+ switch (marker) {
12376
+ case "circle":
12377
+ case "circle-open":
12378
+ return SIZE * 0.6;
12379
+ case "one":
12380
+ return 0;
12381
+ // bar sits AT endpoint
12382
+ case "many":
12383
+ return SIZE;
12384
+ case "optional-one":
12385
+ return SIZE * 1.2;
12386
+ case "optional-many":
12387
+ return SIZE * 1.8;
12388
+ default:
12389
+ return SIZE;
12390
+ }
12391
+ }
12392
+ var KNOWN_MARKERS = /* @__PURE__ */ new Set([
12393
+ "none",
12394
+ "arrow",
12395
+ "arrow-open",
12396
+ "circle",
12397
+ "circle-open",
12398
+ "square",
12399
+ "square-open",
12400
+ "diamond",
12401
+ "diamond-open",
12402
+ "triangle",
12403
+ "triangle-open",
12404
+ "one",
12405
+ "many",
12406
+ "optional-one",
12407
+ "optional-many",
12408
+ "cross"
12409
+ ]);
12410
+ function dirVec(direction) {
12278
12411
  switch (direction) {
12279
12412
  case "left":
12413
+ return [-1, 0];
12280
12414
  case "right":
12281
- return `M${pt.x},${pt.y - s} L${pt.x},${pt.y + s}`;
12415
+ return [1, 0];
12282
12416
  case "up":
12417
+ return [0, -1];
12283
12418
  case "down":
12284
- return `M${pt.x - s},${pt.y} L${pt.x + s},${pt.y}`;
12419
+ return [0, 1];
12285
12420
  }
12286
12421
  }
12287
- function crowFootSymbol(pt, direction) {
12288
- const s = SYMBOL_SIZE;
12289
- const spread = s * 0.8;
12290
- let tip;
12422
+ function perpVec(direction) {
12291
12423
  switch (direction) {
12292
- case "right":
12293
- tip = { x: pt.x - s, y: pt.y };
12294
- return [
12295
- `M${pt.x},${pt.y - spread} L${tip.x},${tip.y}`,
12296
- `M${pt.x},${pt.y} L${tip.x},${tip.y}`,
12297
- `M${pt.x},${pt.y + spread} L${tip.x},${tip.y}`,
12298
- // bar at entity edge
12299
- `M${pt.x},${pt.y - spread} L${pt.x},${pt.y + spread}`
12300
- ].join(" ");
12301
12424
  case "left":
12302
- tip = { x: pt.x + s, y: pt.y };
12303
- return [
12304
- `M${pt.x},${pt.y - spread} L${tip.x},${tip.y}`,
12305
- `M${pt.x},${pt.y} L${tip.x},${tip.y}`,
12306
- `M${pt.x},${pt.y + spread} L${tip.x},${tip.y}`,
12307
- `M${pt.x},${pt.y - spread} L${pt.x},${pt.y + spread}`
12308
- ].join(" ");
12309
- case "down":
12310
- tip = { x: pt.x, y: pt.y - s };
12311
- return [
12312
- `M${pt.x - spread},${pt.y} L${tip.x},${tip.y}`,
12313
- `M${pt.x},${pt.y} L${tip.x},${tip.y}`,
12314
- `M${pt.x + spread},${pt.y} L${tip.x},${tip.y}`,
12315
- `M${pt.x - spread},${pt.y} L${pt.x + spread},${pt.y}`
12316
- ].join(" ");
12425
+ case "right":
12426
+ return [0, 1];
12317
12427
  case "up":
12318
- tip = { x: pt.x, y: pt.y + s };
12319
- return [
12320
- `M${pt.x - spread},${pt.y} L${tip.x},${tip.y}`,
12321
- `M${pt.x},${pt.y} L${tip.x},${tip.y}`,
12322
- `M${pt.x + spread},${pt.y} L${tip.x},${tip.y}`,
12323
- `M${pt.x - spread},${pt.y} L${pt.x + spread},${pt.y}`
12324
- ].join(" ");
12325
- }
12326
- }
12327
- function getSymbolPath(type, end, pt, direction) {
12328
- const side = end === "start" ? type.split("-to-")[0] : type.split("-to-")[1];
12329
- if (side === "one") return oneSymbol(pt, direction);
12330
- if (side === "many") return crowFootSymbol(pt, direction);
12331
- return null;
12428
+ case "down":
12429
+ return [1, 0];
12430
+ }
12431
+ }
12432
+ function arrowPath(pt, direction, _filled) {
12433
+ const [dx, dy] = dirVec(direction);
12434
+ const [px, py] = perpVec(direction);
12435
+ const tipX = pt.x + dx * SIZE;
12436
+ const tipY = pt.y + dy * SIZE;
12437
+ const baseAX = pt.x + px * (SIZE * 0.55);
12438
+ const baseAY = pt.y + py * (SIZE * 0.55);
12439
+ const baseBX = pt.x - px * (SIZE * 0.55);
12440
+ const baseBY = pt.y - py * (SIZE * 0.55);
12441
+ return `M${baseAX},${baseAY} L${tipX},${tipY} L${baseBX},${baseBY} Z`;
12442
+ }
12443
+ function circlePath(pt, _filled) {
12444
+ const r = SIZE * 0.45;
12445
+ return `M${pt.x - r},${pt.y} a${r},${r} 0 1 0 ${r * 2},0 a${r},${r} 0 1 0 ${-r * 2},0 Z`;
12446
+ }
12447
+ function squarePath(pt, direction, _filled) {
12448
+ const [dx, dy] = dirVec(direction);
12449
+ const [px, py] = perpVec(direction);
12450
+ const half = SIZE * 0.5;
12451
+ const cx = pt.x + dx * half;
12452
+ const cy = pt.y + dy * half;
12453
+ const tlX = cx - px * half - dx * half;
12454
+ const tlY = cy - py * half - dy * half;
12455
+ const trX = cx + px * half - dx * half;
12456
+ const trY = cy + py * half - dy * half;
12457
+ const brX = cx + px * half + dx * half;
12458
+ const brY = cy + py * half + dy * half;
12459
+ const blX = cx - px * half + dx * half;
12460
+ const blY = cy - py * half + dy * half;
12461
+ return `M${tlX},${tlY} L${trX},${trY} L${brX},${brY} L${blX},${blY} Z`;
12462
+ }
12463
+ function diamondPath(pt, direction, _filled) {
12464
+ const [dx, dy] = dirVec(direction);
12465
+ const [px, py] = perpVec(direction);
12466
+ const len = SIZE * 1.2;
12467
+ const cx = pt.x + dx * (len / 2);
12468
+ const cy = pt.y + dy * (len / 2);
12469
+ const aX = pt.x;
12470
+ const aY = pt.y;
12471
+ const bX = cx + px * (SIZE * 0.45);
12472
+ const bY = cy + py * (SIZE * 0.45);
12473
+ const tX = pt.x + dx * len;
12474
+ const tY = pt.y + dy * len;
12475
+ const dX = cx - px * (SIZE * 0.45);
12476
+ const dY = cy - py * (SIZE * 0.45);
12477
+ return `M${aX},${aY} L${bX},${bY} L${tX},${tY} L${dX},${dY} Z`;
12478
+ }
12479
+ function trianglePath(pt, direction, _filled) {
12480
+ const [dx, dy] = dirVec(direction);
12481
+ const [px, py] = perpVec(direction);
12482
+ const len = SIZE * 1.1;
12483
+ const baseCX = pt.x + dx * len;
12484
+ const baseCY = pt.y + dy * len;
12485
+ const baseAX = baseCX + px * (SIZE * 0.6);
12486
+ const baseAY = baseCY + py * (SIZE * 0.6);
12487
+ const baseBX = baseCX - px * (SIZE * 0.6);
12488
+ const baseBY = baseCY - py * (SIZE * 0.6);
12489
+ return `M${pt.x},${pt.y} L${baseAX},${baseAY} L${baseBX},${baseBY} Z`;
12490
+ }
12491
+ function crossPath(pt, direction) {
12492
+ const [dx, dy] = dirVec(direction);
12493
+ const [px, py] = perpVec(direction);
12494
+ const s = SIZE * 0.5;
12495
+ const cx = pt.x + dx * (SIZE * 0.5);
12496
+ const cy = pt.y + dy * (SIZE * 0.5);
12497
+ return [
12498
+ `M${cx - s * (px + dx)},${cy - s * (py + dy)} L${cx + s * (px + dx)},${cy + s * (py + dy)}`,
12499
+ `M${cx - s * (px - dx)},${cy - s * (py - dy)} L${cx + s * (px - dx)},${cy + s * (py - dy)}`
12500
+ ].join(" ");
12501
+ }
12502
+ function oneSymbol(pt, direction) {
12503
+ const [, py] = perpVec(direction);
12504
+ const [px] = perpVec(direction);
12505
+ const half = SIZE * 0.6;
12506
+ return `M${pt.x - px * half},${pt.y - py * half} L${pt.x + px * half},${pt.y + py * half}`;
12507
+ }
12508
+ function crowFootSymbol(pt, direction) {
12509
+ const [dx, dy] = dirVec(direction);
12510
+ const [px, py] = perpVec(direction);
12511
+ const tipX = pt.x + dx * SIZE;
12512
+ const tipY = pt.y + dy * SIZE;
12513
+ const spread = SIZE * 0.8;
12514
+ const aX = pt.x + px * spread, aY = pt.y + py * spread;
12515
+ const cX = pt.x - px * spread, cY = pt.y - py * spread;
12516
+ return [
12517
+ `M${aX},${aY} L${tipX},${tipY}`,
12518
+ `M${pt.x},${pt.y} L${tipX},${tipY}`,
12519
+ `M${cX},${cY} L${tipX},${tipY}`,
12520
+ `M${aX},${aY} L${cX},${cY}`
12521
+ ].join(" ");
12522
+ }
12523
+ function circleOuter(pt, direction) {
12524
+ const [dx, dy] = dirVec(direction);
12525
+ const r = SIZE * 0.4;
12526
+ const cx = pt.x + dx * (SIZE * 0.4 + r);
12527
+ const cy = pt.y + dy * (SIZE * 0.4 + r);
12528
+ return `M${cx - r},${cy} a${r},${r} 0 1 0 ${r * 2},0 a${r},${r} 0 1 0 ${-r * 2},0 Z`;
12529
+ }
12530
+ function oneSymbolOffset(pt, direction) {
12531
+ const [px, py] = perpVec(direction);
12532
+ const half = SIZE * 0.6;
12533
+ return `M${pt.x - px * half},${pt.y - py * half} L${pt.x + px * half},${pt.y + py * half}`;
12534
+ }
12535
+ function crowFootSymbolOffset(pt, direction) {
12536
+ const [dx, dy] = dirVec(direction);
12537
+ const [px, py] = perpVec(direction);
12538
+ const inset = SIZE * 0.8;
12539
+ const startX = pt.x + dx * inset;
12540
+ const startY = pt.y + dy * inset;
12541
+ const tipX = pt.x + dx * (inset + SIZE);
12542
+ const tipY = pt.y + dy * (inset + SIZE);
12543
+ const spread = SIZE * 0.8;
12544
+ const aX = startX + px * spread, aY = startY + py * spread;
12545
+ const cX = startX - px * spread, cY = startY - py * spread;
12546
+ return [
12547
+ `M${aX},${aY} L${tipX},${tipY}`,
12548
+ `M${startX},${startY} L${tipX},${tipY}`,
12549
+ `M${cX},${cY} L${tipX},${tipY}`
12550
+ ].join(" ");
12551
+ }
12552
+
12553
+ // src/components/Diagram/diagram.routing.ts
12554
+ var STUB = 24;
12555
+ var DODGE_PADDING = 16;
12556
+ var MAX_DODGE_ITERATIONS = 6;
12557
+ function pickAnchors(from, to, fromY, toY) {
12558
+ const fcx = from.x + from.width / 2;
12559
+ const fcy = from.y + from.height / 2;
12560
+ const tcx = to.x + to.width / 2;
12561
+ const tcy = to.y + to.height / 2;
12562
+ const dx = tcx - fcx;
12563
+ const dy = tcy - fcy;
12564
+ let fromSide, toSide;
12565
+ if (Math.abs(dx) >= Math.abs(dy)) {
12566
+ fromSide = dx >= 0 ? "right" : "left";
12567
+ toSide = dx >= 0 ? "left" : "right";
12568
+ } else {
12569
+ fromSide = dy >= 0 ? "bottom" : "top";
12570
+ toSide = dy >= 0 ? "top" : "bottom";
12571
+ }
12572
+ return {
12573
+ from: anchorOnSide(from, fromSide, fromY),
12574
+ to: anchorOnSide(to, toSide, toY)
12575
+ };
12576
+ }
12577
+ function anchorOnSide(box, side, fieldY) {
12578
+ switch (side) {
12579
+ case "right":
12580
+ return { side, x: box.x + box.width, y: box.y + (fieldY ?? box.height / 2) };
12581
+ case "left":
12582
+ return { side, x: box.x, y: box.y + (fieldY ?? box.height / 2) };
12583
+ case "top":
12584
+ return { side, x: box.x + box.width / 2, y: box.y };
12585
+ case "bottom":
12586
+ return { side, x: box.x + box.width / 2, y: box.y + box.height };
12587
+ }
12588
+ }
12589
+ function stubOut(a, distance2 = STUB) {
12590
+ switch (a.side) {
12591
+ case "right":
12592
+ return { x: a.x + distance2, y: a.y };
12593
+ case "left":
12594
+ return { x: a.x - distance2, y: a.y };
12595
+ case "top":
12596
+ return { x: a.x, y: a.y - distance2 };
12597
+ case "bottom":
12598
+ return { x: a.x, y: a.y + distance2 };
12599
+ }
12600
+ }
12601
+ function manhattanPath(from, to, obstacles = []) {
12602
+ const f = { x: from.x, y: from.y };
12603
+ const t = { x: to.x, y: to.y };
12604
+ const fs = stubOut(from);
12605
+ const ts = stubOut(to);
12606
+ const fHoriz = from.side === "left" || from.side === "right";
12607
+ const tHoriz = to.side === "left" || to.side === "right";
12608
+ if (fHoriz && tHoriz) {
12609
+ const midX = pickClearMidX((fs.x + ts.x) / 2, fs.y, ts.y, obstacles);
12610
+ return uniqPath([
12611
+ f,
12612
+ fs,
12613
+ { x: midX, y: fs.y },
12614
+ { x: midX, y: ts.y },
12615
+ ts,
12616
+ t
12617
+ ]);
12618
+ }
12619
+ if (!fHoriz && !tHoriz) {
12620
+ const midY = pickClearMidY((fs.y + ts.y) / 2, fs.x, ts.x, obstacles);
12621
+ return uniqPath([
12622
+ f,
12623
+ fs,
12624
+ { x: fs.x, y: midY },
12625
+ { x: ts.x, y: midY },
12626
+ ts,
12627
+ t
12628
+ ]);
12629
+ }
12630
+ if (fHoriz) {
12631
+ return uniqPath([
12632
+ f,
12633
+ fs,
12634
+ { x: ts.x, y: fs.y },
12635
+ ts,
12636
+ t
12637
+ ]);
12638
+ }
12639
+ return uniqPath([
12640
+ f,
12641
+ fs,
12642
+ { x: fs.x, y: ts.y },
12643
+ ts,
12644
+ t
12645
+ ]);
12646
+ }
12647
+ function pickClearMidX(idealX, y1, y2, obstacles) {
12648
+ if (obstacles.length === 0) return idealX;
12649
+ const yMin = Math.min(y1, y2);
12650
+ const yMax = Math.max(y1, y2);
12651
+ let midX = idealX;
12652
+ for (let i = 0; i < 4; i++) {
12653
+ let shifted = false;
12654
+ for (const ob of obstacles) {
12655
+ if (ob.y + ob.height + DODGE_PADDING < yMin) continue;
12656
+ if (ob.y - DODGE_PADDING > yMax) continue;
12657
+ const left = ob.x - DODGE_PADDING;
12658
+ const right = ob.x + ob.width + DODGE_PADDING;
12659
+ if (midX > left && midX < right) {
12660
+ midX = Math.abs(midX - left) <= Math.abs(midX - right) ? left - 1 : right + 1;
12661
+ shifted = true;
12662
+ }
12663
+ }
12664
+ if (!shifted) break;
12665
+ }
12666
+ return midX;
12667
+ }
12668
+ function pickClearMidY(idealY, x1, x2, obstacles) {
12669
+ if (obstacles.length === 0) return idealY;
12670
+ const xMin = Math.min(x1, x2);
12671
+ const xMax = Math.max(x1, x2);
12672
+ let midY = idealY;
12673
+ for (let i = 0; i < 4; i++) {
12674
+ let shifted = false;
12675
+ for (const ob of obstacles) {
12676
+ if (ob.x + ob.width + DODGE_PADDING < xMin) continue;
12677
+ if (ob.x - DODGE_PADDING > xMax) continue;
12678
+ const top = ob.y - DODGE_PADDING;
12679
+ const bot = ob.y + ob.height + DODGE_PADDING;
12680
+ if (midY > top && midY < bot) {
12681
+ midY = Math.abs(midY - top) <= Math.abs(midY - bot) ? top - 1 : bot + 1;
12682
+ shifted = true;
12683
+ }
12684
+ }
12685
+ if (!shifted) break;
12686
+ }
12687
+ return midY;
12688
+ }
12689
+ function uniqPath(points) {
12690
+ const out = [];
12691
+ for (const p of points) {
12692
+ const last = out[out.length - 1];
12693
+ if (!last || Math.abs(last.x - p.x) > 0.5 || Math.abs(last.y - p.y) > 0.5) {
12694
+ out.push(p);
12695
+ }
12696
+ }
12697
+ return out;
12698
+ }
12699
+ function dodgeObstacles(path, obstacles, padding = DODGE_PADDING) {
12700
+ if (obstacles.length === 0 || path.length < 2) return path;
12701
+ let working = path.slice();
12702
+ for (let iter = 0; iter < MAX_DODGE_ITERATIONS; iter++) {
12703
+ let dodged = false;
12704
+ const result = [working[0]];
12705
+ for (let i = 1; i < working.length; i++) {
12706
+ const a = result[result.length - 1];
12707
+ const b = working[i];
12708
+ const detour = detourAround(a, b, obstacles, padding);
12709
+ if (detour.length === 0) {
12710
+ result.push(b);
12711
+ } else {
12712
+ for (const p of detour) result.push(p);
12713
+ result.push(b);
12714
+ dodged = true;
12715
+ }
12716
+ }
12717
+ working = uniqPath(result);
12718
+ if (!dodged) break;
12719
+ }
12720
+ return working;
12721
+ }
12722
+ function detourAround(a, b, obstacles, padding) {
12723
+ const isHorizontal = Math.abs(a.y - b.y) < 0.5;
12724
+ const isVertical = Math.abs(a.x - b.x) < 0.5;
12725
+ if (!isHorizontal && !isVertical) return [];
12726
+ let best = null;
12727
+ for (const r of obstacles) {
12728
+ const expanded = expandRect(r, padding);
12729
+ if (!segmentCrossesRect(a, b, expanded)) continue;
12730
+ let entry, exit;
12731
+ if (isHorizontal) {
12732
+ entry = b.x > a.x ? expanded.x : expanded.x + expanded.width;
12733
+ exit = b.x > a.x ? expanded.x + expanded.width : expanded.x;
12734
+ } else {
12735
+ entry = b.y > a.y ? expanded.y : expanded.y + expanded.height;
12736
+ exit = b.y > a.y ? expanded.y + expanded.height : expanded.y;
12737
+ }
12738
+ const dist = isHorizontal ? Math.abs(entry - a.x) : Math.abs(entry - a.y);
12739
+ if (!best || dist < (isHorizontal ? Math.abs(best.entry - a.x) : Math.abs(best.entry - a.y))) {
12740
+ best = { rect: expanded, entry, exit };
12741
+ }
12742
+ }
12743
+ if (!best) return [];
12744
+ if (isHorizontal) {
12745
+ const aboveY = best.rect.y - 1;
12746
+ const belowY = best.rect.y + best.rect.height + 1;
12747
+ const detourY = Math.abs(a.y - aboveY) <= Math.abs(a.y - belowY) ? aboveY : belowY;
12748
+ return [
12749
+ { x: best.entry, y: a.y },
12750
+ { x: best.entry, y: detourY },
12751
+ { x: best.exit, y: detourY },
12752
+ { x: best.exit, y: a.y }
12753
+ ];
12754
+ } else {
12755
+ const leftX = best.rect.x - 1;
12756
+ const rightX = best.rect.x + best.rect.width + 1;
12757
+ const detourX = Math.abs(a.x - leftX) <= Math.abs(a.x - rightX) ? leftX : rightX;
12758
+ return [
12759
+ { x: a.x, y: best.entry },
12760
+ { x: detourX, y: best.entry },
12761
+ { x: detourX, y: best.exit },
12762
+ { x: a.x, y: best.exit }
12763
+ ];
12764
+ }
12765
+ }
12766
+ function expandRect(r, padding) {
12767
+ return {
12768
+ x: r.x - padding,
12769
+ y: r.y - padding,
12770
+ width: r.width + padding * 2,
12771
+ height: r.height + padding * 2
12772
+ };
12773
+ }
12774
+ function segmentCrossesRect(a, b, rect) {
12775
+ const xMin = Math.min(a.x, b.x);
12776
+ const xMax = Math.max(a.x, b.x);
12777
+ const yMin = Math.min(a.y, b.y);
12778
+ const yMax = Math.max(a.y, b.y);
12779
+ if (xMax < rect.x) return false;
12780
+ if (xMin > rect.x + rect.width) return false;
12781
+ if (yMax < rect.y) return false;
12782
+ if (yMin > rect.y + rect.height) return false;
12783
+ if (a.x >= rect.x + 1 && a.x <= rect.x + rect.width - 1 && a.y >= rect.y + 1 && a.y <= rect.y + rect.height - 1) return false;
12784
+ if (b.x >= rect.x + 1 && b.x <= rect.x + rect.width - 1 && b.y >= rect.y + 1 && b.y <= rect.y + rect.height - 1) return false;
12785
+ return true;
12786
+ }
12787
+ function pathFromPoints(points, cornerRadius = 8) {
12788
+ if (points.length === 0) return "";
12789
+ if (points.length === 1) return `M${points[0].x},${points[0].y}`;
12790
+ if (points.length === 2 || cornerRadius <= 0) {
12791
+ return points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x},${p.y}`).join(" ");
12792
+ }
12793
+ let d = `M${points[0].x},${points[0].y}`;
12794
+ for (let i = 1; i < points.length - 1; i++) {
12795
+ const prev = points[i - 1];
12796
+ const curr = points[i];
12797
+ const next = points[i + 1];
12798
+ const r = Math.min(
12799
+ cornerRadius,
12800
+ distance(prev, curr) / 2,
12801
+ distance(curr, next) / 2
12802
+ );
12803
+ if (r < 1) {
12804
+ d += ` L${curr.x},${curr.y}`;
12805
+ continue;
12806
+ }
12807
+ const beforeX = curr.x + Math.sign(prev.x - curr.x) * r;
12808
+ const beforeY = curr.y + Math.sign(prev.y - curr.y) * r;
12809
+ const afterX = curr.x + Math.sign(next.x - curr.x) * r;
12810
+ const afterY = curr.y + Math.sign(next.y - curr.y) * r;
12811
+ d += ` L${beforeX},${beforeY} Q${curr.x},${curr.y} ${afterX},${afterY}`;
12812
+ }
12813
+ const last = points[points.length - 1];
12814
+ d += ` L${last.x},${last.y}`;
12815
+ return d;
12816
+ }
12817
+ function distance(a, b) {
12818
+ return Math.hypot(b.x - a.x, b.y - a.y);
12819
+ }
12820
+ function bezierPath2(from, to) {
12821
+ const fs = stubOut(from, Math.max(40, distance(from, to) * 0.3));
12822
+ const ts = stubOut(to, Math.max(40, distance(from, to) * 0.3));
12823
+ return `M${from.x},${from.y} C${fs.x},${fs.y} ${ts.x},${ts.y} ${to.x},${to.y}`;
12824
+ }
12825
+ function midPoint(points) {
12826
+ if (points.length === 0) return { x: 0, y: 0 };
12827
+ if (points.length === 1) return points[0];
12828
+ let total = 0;
12829
+ const segLens = [];
12830
+ for (let i = 1; i < points.length; i++) {
12831
+ const len = distance(points[i - 1], points[i]);
12832
+ segLens.push(len);
12833
+ total += len;
12834
+ }
12835
+ let target = total / 2;
12836
+ for (let i = 0; i < segLens.length; i++) {
12837
+ if (target <= segLens[i]) {
12838
+ const t = segLens[i] === 0 ? 0 : target / segLens[i];
12839
+ const a = points[i];
12840
+ const b = points[i + 1];
12841
+ return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
12842
+ }
12843
+ target -= segLens[i];
12844
+ }
12845
+ return points[points.length - 1];
12846
+ }
12847
+ function insetEndpoints(points, inset) {
12848
+ if (points.length < 2) return points;
12849
+ const result = points.slice();
12850
+ if (inset.from > 0) {
12851
+ const a = result[0];
12852
+ const b = result[1];
12853
+ const len = distance(a, b);
12854
+ if (len > inset.from) {
12855
+ const t = inset.from / len;
12856
+ result[0] = { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
12857
+ }
12858
+ }
12859
+ if (inset.to > 0) {
12860
+ const lastIdx = result.length - 1;
12861
+ const a = result[lastIdx];
12862
+ const b = result[lastIdx - 1];
12863
+ const len = distance(a, b);
12864
+ if (len > inset.to) {
12865
+ const t = inset.to / len;
12866
+ result[lastIdx] = { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
12867
+ }
12868
+ }
12869
+ return result;
12870
+ }
12871
+ var HEADER_HEIGHT = 36;
12872
+ var FIELD_HEIGHT = 29;
12873
+ var DEFAULT_COLOR2 = "#71717a";
12874
+ function markerDirection(side) {
12875
+ switch (side) {
12876
+ case "left":
12877
+ return "right";
12878
+ // entity body is to the right of a left-side anchor
12879
+ case "right":
12880
+ return "left";
12881
+ case "top":
12882
+ return "down";
12883
+ case "bottom":
12884
+ return "up";
12885
+ }
12886
+ }
12887
+ function strokeDashArray(style) {
12888
+ switch (style) {
12889
+ case "dashed":
12890
+ return "8 4";
12891
+ case "dotted":
12892
+ return "2 4";
12893
+ default:
12894
+ return void 0;
12895
+ }
12332
12896
  }
12333
12897
  function DiagramRelation({
12334
12898
  from,
@@ -12336,6 +12900,12 @@ function DiagramRelation({
12336
12900
  fromField: fromFieldProp,
12337
12901
  toField: toFieldProp,
12338
12902
  type,
12903
+ fromMarker: fromMarkerProp,
12904
+ toMarker: toMarkerProp,
12905
+ lineStyle: lineStyleProp,
12906
+ routing = "manhattan",
12907
+ color = DEFAULT_COLOR2,
12908
+ strokeWidth = 2,
12339
12909
  label
12340
12910
  }) {
12341
12911
  const { nodeRects, registryVersion } = useCanvas();
@@ -12344,68 +12914,160 @@ function DiagramRelation({
12344
12914
  const fromRect = nodeRects.get(from);
12345
12915
  const toRect = nodeRects.get(to);
12346
12916
  if (!fromRect || !toRect) return null;
12917
+ const defaults = defaultMarkersForType(type);
12918
+ const fromMarker = fromMarkerProp ?? defaults.fromMarker;
12919
+ const toMarker = toMarkerProp ?? defaults.toMarker;
12920
+ const lineStyle = lineStyleProp ?? defaults.lineStyle;
12347
12921
  const fromEntity = schema.entities.find((e) => (e.id ?? e.name) === from);
12348
12922
  const toEntity = schema.entities.find((e) => (e.id ?? e.name) === to);
12349
- let fromFieldIdx = -1;
12350
- let toFieldIdx = -1;
12351
- if (fromFieldProp && fromEntity?.fields) {
12352
- fromFieldIdx = fromEntity.fields.findIndex((f) => f.name === fromFieldProp);
12353
- } else if (fromEntity?.fields) {
12354
- fromFieldIdx = fromEntity.fields.findIndex((f) => f.primary);
12355
- }
12356
- if (toFieldProp && toEntity?.fields) {
12357
- toFieldIdx = toEntity.fields.findIndex((f) => f.name === toFieldProp);
12358
- } else if (toEntity?.fields) {
12359
- const fromName = (fromEntity?.name ?? from).toLowerCase();
12360
- toFieldIdx = toEntity.fields.findIndex(
12361
- (f) => f.foreign && (f.name === `${fromName}_id` || f.name === `${fromName}Id`)
12362
- );
12363
- if (toFieldIdx === -1) {
12364
- toFieldIdx = toEntity.fields.findIndex((f) => f.foreign);
12365
- }
12366
- }
12367
- const fromFieldY = fromFieldIdx >= 0 ? HEADER_HEIGHT + fromFieldIdx * FIELD_HEIGHT + FIELD_HEIGHT / 2 : fromRect.height / 2;
12368
- const toFieldY = toFieldIdx >= 0 ? HEADER_HEIGHT + toFieldIdx * FIELD_HEIGHT + FIELD_HEIGHT / 2 : toRect.height / 2;
12369
- const fromCx = fromRect.x + fromRect.width / 2;
12370
- const toCx = toRect.x + toRect.width / 2;
12371
- let fromPt, toPt;
12372
- let fromDir;
12373
- let toDir;
12374
- if (fromCx <= toCx) {
12375
- fromPt = { x: fromRect.x + fromRect.width, y: fromRect.y + fromFieldY };
12376
- toPt = { x: toRect.x, y: toRect.y + toFieldY };
12377
- fromDir = "right";
12378
- toDir = "left";
12923
+ const fromFieldY = resolveFieldY(fromRect, fromEntity?.fields, fromFieldProp, true);
12924
+ const toFieldY = resolveFieldY(toRect, toEntity?.fields, toFieldProp, false, fromEntity?.name ?? from);
12925
+ const anchors = pickAnchors(fromRect, toRect, fromFieldY, toFieldY);
12926
+ if (anchors.from.side === "top" || anchors.from.side === "bottom") {
12927
+ anchors.from.x = fromRect.x + fromRect.width / 2;
12928
+ }
12929
+ if (anchors.to.side === "top" || anchors.to.side === "bottom") {
12930
+ anchors.to.x = toRect.x + toRect.width / 2;
12931
+ }
12932
+ let points;
12933
+ if (routing === "straight") {
12934
+ points = [
12935
+ { x: anchors.from.x, y: anchors.from.y },
12936
+ { x: anchors.to.x, y: anchors.to.y }
12937
+ ];
12938
+ } else if (routing === "bezier") {
12939
+ points = [];
12379
12940
  } else {
12380
- fromPt = { x: fromRect.x, y: fromRect.y + fromFieldY };
12381
- toPt = { x: toRect.x + toRect.width, y: toRect.y + toFieldY };
12382
- fromDir = "left";
12383
- toDir = "right";
12384
- }
12385
- const offsetFrom = { ...fromPt };
12386
- const offsetTo = { ...toPt };
12387
- if (fromDir === "right") offsetFrom.x += SYMBOL_SIZE;
12388
- else offsetFrom.x -= SYMBOL_SIZE;
12389
- if (toDir === "left") offsetTo.x -= SYMBOL_SIZE;
12390
- else offsetTo.x += SYMBOL_SIZE;
12391
- const adx = Math.abs(offsetTo.x - offsetFrom.x);
12392
- const ady = Math.abs(offsetTo.y - offsetFrom.y);
12393
- const off = Math.max(adx * 0.4, ady * 0.25, 40);
12394
- const cp1x = offsetFrom.x + (fromDir === "right" ? off : -off);
12395
- const cp2x = offsetTo.x + (toDir === "left" ? -off : off);
12396
- const linePath = `M${offsetFrom.x},${offsetFrom.y} C${cp1x},${offsetFrom.y} ${cp2x},${offsetTo.y} ${offsetTo.x},${offsetTo.y}`;
12397
- const startSymbol = getSymbolPath(type, "start", fromPt, fromDir);
12398
- const endSymbol = getSymbolPath(type, "end", toPt, toDir);
12399
- return { linePath, startSymbol, endSymbol, midX: (offsetFrom.x + offsetTo.x) / 2, midY: (offsetFrom.y + offsetTo.y) / 2 };
12400
- }, [from, to, fromFieldProp, toFieldProp, type, schema, nodeRects, registryVersion]);
12941
+ const obstacles = [];
12942
+ nodeRects.forEach((rect, id) => {
12943
+ if (id === from || id === to) return;
12944
+ obstacles.push(rect);
12945
+ });
12946
+ const initial = manhattanPath(anchors.from, anchors.to, obstacles);
12947
+ points = dodgeObstacles(initial, obstacles);
12948
+ }
12949
+ const insetAmount = { from: markerInset(fromMarker), to: markerInset(toMarker) };
12950
+ if (routing !== "bezier") {
12951
+ points = insetEndpoints(points, insetAmount);
12952
+ }
12953
+ const linePath = routing === "bezier" ? bezierPath2(anchors.from, anchors.to) : pathFromPoints(points);
12954
+ const fromMarkerRenderable = renderMarker(
12955
+ fromMarker,
12956
+ { x: anchors.from.x, y: anchors.from.y },
12957
+ markerDirection(anchors.from.side)
12958
+ );
12959
+ const toMarkerRenderable = renderMarker(
12960
+ toMarker,
12961
+ { x: anchors.to.x, y: anchors.to.y },
12962
+ markerDirection(anchors.to.side)
12963
+ );
12964
+ const mid = routing === "bezier" ? { x: (anchors.from.x + anchors.to.x) / 2, y: (anchors.from.y + anchors.to.y) / 2 } : midPoint(points);
12965
+ return {
12966
+ linePath,
12967
+ fromMarker: fromMarkerRenderable,
12968
+ toMarker: toMarkerRenderable,
12969
+ fromAnchor: anchors.from,
12970
+ toAnchor: anchors.to,
12971
+ mid,
12972
+ lineStyle
12973
+ };
12974
+ }, [
12975
+ from,
12976
+ to,
12977
+ fromFieldProp,
12978
+ toFieldProp,
12979
+ type,
12980
+ fromMarkerProp,
12981
+ toMarkerProp,
12982
+ lineStyleProp,
12983
+ routing,
12984
+ schema,
12985
+ nodeRects,
12986
+ registryVersion
12987
+ ]);
12401
12988
  if (!result) return null;
12989
+ const dashArray = strokeDashArray(result.lineStyle);
12402
12990
  return /* @__PURE__ */ jsxRuntime.jsxs("g", { "data-react-fancy-diagram-relation": "", children: [
12403
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: result.linePath, fill: "none", stroke: "#71717a", strokeWidth: 2 }),
12404
- result.startSymbol && /* @__PURE__ */ jsxRuntime.jsx("path", { d: result.startSymbol, fill: "none", stroke: "#71717a", strokeWidth: 2 }),
12405
- result.endSymbol && /* @__PURE__ */ jsxRuntime.jsx("path", { d: result.endSymbol, fill: "none", stroke: "#71717a", strokeWidth: 2 }),
12406
- label && /* @__PURE__ */ jsxRuntime.jsx("foreignObject", { x: result.midX - 40, y: result.midY - 12, width: 80, height: 24, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center text-xs text-zinc-500", children: label }) })
12991
+ /* @__PURE__ */ jsxRuntime.jsx(
12992
+ "path",
12993
+ {
12994
+ d: result.linePath,
12995
+ fill: "none",
12996
+ stroke: color,
12997
+ strokeWidth,
12998
+ strokeDasharray: dashArray,
12999
+ strokeLinecap: "round",
13000
+ strokeLinejoin: "round"
13001
+ }
13002
+ ),
13003
+ result.fromMarker?.paths.map((shape, i) => /* @__PURE__ */ jsxRuntime.jsx(
13004
+ "path",
13005
+ {
13006
+ d: shape.d,
13007
+ fill: shape.fill === "stroke" ? color : shape.fill === "background" ? "#ffffff" : "none",
13008
+ stroke: color,
13009
+ strokeWidth,
13010
+ strokeLinecap: "round",
13011
+ strokeLinejoin: "round"
13012
+ },
13013
+ `fm-${i}`
13014
+ )),
13015
+ result.fromMarker?.text && /* @__PURE__ */ jsxRuntime.jsx(
13016
+ "text",
13017
+ {
13018
+ x: result.fromAnchor.x,
13019
+ y: result.fromAnchor.y,
13020
+ fontSize: 16,
13021
+ textAnchor: "middle",
13022
+ dominantBaseline: "middle",
13023
+ style: { userSelect: "none" },
13024
+ children: result.fromMarker.text
13025
+ }
13026
+ ),
13027
+ result.toMarker?.text && /* @__PURE__ */ jsxRuntime.jsx(
13028
+ "text",
13029
+ {
13030
+ x: result.toAnchor.x,
13031
+ y: result.toAnchor.y,
13032
+ fontSize: 16,
13033
+ textAnchor: "middle",
13034
+ dominantBaseline: "middle",
13035
+ style: { userSelect: "none" },
13036
+ children: result.toMarker.text
13037
+ }
13038
+ ),
13039
+ result.toMarker?.paths.map((shape, i) => /* @__PURE__ */ jsxRuntime.jsx(
13040
+ "path",
13041
+ {
13042
+ d: shape.d,
13043
+ fill: shape.fill === "stroke" ? color : shape.fill === "background" ? "#ffffff" : "none",
13044
+ stroke: color,
13045
+ strokeWidth,
13046
+ strokeLinecap: "round",
13047
+ strokeLinejoin: "round"
13048
+ },
13049
+ `tm-${i}`
13050
+ )),
13051
+ label && /* @__PURE__ */ jsxRuntime.jsx("foreignObject", { x: result.mid.x - 50, y: result.mid.y - 12, width: 100, height: 24, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center text-xs text-zinc-500", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded bg-white/90 px-1.5 py-0.5 dark:bg-zinc-900/90", children: label }) }) })
12407
13052
  ] });
12408
13053
  }
13054
+ function resolveFieldY(rect, fields, fieldProp, isFrom, fromName) {
13055
+ if (!fields || fields.length === 0) return void 0;
13056
+ let idx = -1;
13057
+ if (fieldProp) {
13058
+ idx = fields.findIndex((f) => f.name === fieldProp);
13059
+ } else if (isFrom) {
13060
+ idx = fields.findIndex((f) => f.primary);
13061
+ } else {
13062
+ const lower = (fromName ?? "").toLowerCase();
13063
+ idx = fields.findIndex(
13064
+ (f) => f.foreign && (f.name === `${lower}_id` || f.name === `${lower}Id`)
13065
+ );
13066
+ if (idx === -1) idx = fields.findIndex((f) => f.foreign);
13067
+ }
13068
+ if (idx < 0) return void 0;
13069
+ return HEADER_HEIGHT + idx * FIELD_HEIGHT + FIELD_HEIGHT / 2;
13070
+ }
12409
13071
  DiagramRelation._isCanvasEdge = true;
12410
13072
  DiagramRelation.displayName = "DiagramRelation";
12411
13073
  var FORMAT_LABELS = {
@@ -12847,6 +13509,8 @@ function TreeNode({ node, depth }) {
12847
13509
  dragState,
12848
13510
  setDragState,
12849
13511
  onNodeMove,
13512
+ acceptExternalDrops,
13513
+ onExternalDrop,
12850
13514
  nodes,
12851
13515
  expandNode
12852
13516
  } = useTreeNav();
@@ -12889,14 +13553,18 @@ function TreeNode({ node, depth }) {
12889
13553
  clearAutoExpand();
12890
13554
  setDragState({ draggedNodeId: null, dropTargetId: null, dropPosition: null });
12891
13555
  }, [clearAutoExpand, setDragState]);
13556
+ const isExternalDrag = !dragState.draggedNodeId;
12892
13557
  const handleDragOver = react.useCallback((e) => {
12893
- if (!dragState.draggedNodeId) return;
12894
- const sourceId = dragState.draggedNodeId;
12895
- if (sourceId === node.id) return;
12896
- if (isDescendantOf(nodes, sourceId, node.id)) return;
13558
+ if (isExternalDrag) {
13559
+ if (!acceptExternalDrops) return;
13560
+ } else {
13561
+ const sourceId = dragState.draggedNodeId;
13562
+ if (sourceId === node.id) return;
13563
+ if (isDescendantOf(nodes, sourceId, node.id)) return;
13564
+ }
12897
13565
  e.preventDefault();
12898
13566
  e.stopPropagation();
12899
- e.dataTransfer.dropEffect = "move";
13567
+ e.dataTransfer.dropEffect = isExternalDrag ? "copy" : "move";
12900
13568
  const position = computeDropPosition(e, !!isFolder);
12901
13569
  if (isFolder && !isExpanded && position === "inside") {
12902
13570
  if (!autoExpandTimer.current) {
@@ -12909,9 +13577,9 @@ function TreeNode({ node, depth }) {
12909
13577
  clearAutoExpand();
12910
13578
  }
12911
13579
  if (dragState.dropTargetId !== node.id || dragState.dropPosition !== position) {
12912
- setDragState({ draggedNodeId: sourceId, dropTargetId: node.id, dropPosition: position });
13580
+ setDragState({ draggedNodeId: dragState.draggedNodeId, dropTargetId: node.id, dropPosition: position });
12913
13581
  }
12914
- }, [dragState, node.id, isFolder, isExpanded, nodes, setDragState, expandNode, clearAutoExpand]);
13582
+ }, [dragState, isExternalDrag, acceptExternalDrops, node.id, isFolder, isExpanded, nodes, setDragState, expandNode, clearAutoExpand]);
12915
13583
  const handleDragLeave = react.useCallback((e) => {
12916
13584
  if (!e.currentTarget.contains(e.relatedTarget)) {
12917
13585
  clearAutoExpand();
@@ -12925,21 +13593,25 @@ function TreeNode({ node, depth }) {
12925
13593
  e.stopPropagation();
12926
13594
  clearAutoExpand();
12927
13595
  const sourceId = dragState.draggedNodeId;
12928
- const position = dragState.dropPosition;
12929
- if (!sourceId || !position) return;
12930
- if (sourceId === node.id) return;
12931
- if (isDescendantOf(nodes, sourceId, node.id)) return;
12932
- onNodeMove?.(sourceId, node.id, position);
13596
+ const position = dragState.dropPosition ?? computeDropPosition(e, !!isFolder);
13597
+ if (sourceId) {
13598
+ if (sourceId === node.id) return;
13599
+ if (isDescendantOf(nodes, sourceId, node.id)) return;
13600
+ onNodeMove?.(sourceId, node.id, position);
13601
+ } else if (acceptExternalDrops) {
13602
+ onExternalDrop?.(e, node, position);
13603
+ }
12933
13604
  setDragState({ draggedNodeId: null, dropTargetId: null, dropPosition: null });
12934
- }, [dragState, node.id, nodes, onNodeMove, setDragState, clearAutoExpand]);
13605
+ }, [dragState, node, isFolder, nodes, onNodeMove, acceptExternalDrops, onExternalDrop, setDragState, clearAutoExpand]);
12935
13606
  const canDrag = draggable && !node.disabled;
13607
+ const dropEnabled = draggable || acceptExternalDrops;
12936
13608
  return /* @__PURE__ */ jsxRuntime.jsxs(
12937
13609
  "div",
12938
13610
  {
12939
13611
  "data-react-fancy-tree-node": "",
12940
- onDragOver: draggable ? handleDragOver : void 0,
12941
- onDragLeave: draggable ? handleDragLeave : void 0,
12942
- onDrop: draggable ? handleDrop : void 0,
13612
+ onDragOver: dropEnabled ? handleDragOver : void 0,
13613
+ onDragLeave: dropEnabled ? handleDragLeave : void 0,
13614
+ onDrop: dropEnabled ? handleDrop : void 0,
12943
13615
  children: [
12944
13616
  isDropTarget && dropPosition === "before" && /* @__PURE__ */ jsxRuntime.jsx(
12945
13617
  "div",
@@ -13012,6 +13684,8 @@ function TreeNavRoot({
13012
13684
  onNodeContextMenu,
13013
13685
  draggable = false,
13014
13686
  onNodeMove,
13687
+ acceptExternalDrops = false,
13688
+ onExternalDrop,
13015
13689
  expandedIds: controlledExpanded,
13016
13690
  defaultExpandedIds,
13017
13691
  onExpandedChange,
@@ -13065,6 +13739,8 @@ function TreeNavRoot({
13065
13739
  dragState,
13066
13740
  setDragState,
13067
13741
  onNodeMove,
13742
+ acceptExternalDrops,
13743
+ onExternalDrop,
13068
13744
  nodes,
13069
13745
  expandNode
13070
13746
  }),
@@ -13079,16 +13755,19 @@ function TreeNavRoot({
13079
13755
  draggable,
13080
13756
  dragState,
13081
13757
  onNodeMove,
13758
+ acceptExternalDrops,
13759
+ onExternalDrop,
13082
13760
  nodes,
13083
13761
  expandNode
13084
13762
  ]
13085
13763
  );
13764
+ const dropEnabled = draggable || acceptExternalDrops;
13086
13765
  return /* @__PURE__ */ jsxRuntime.jsx(TreeNavContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
13087
13766
  "nav",
13088
13767
  {
13089
13768
  "data-react-fancy-tree-nav": "",
13090
13769
  className: cn("flex flex-col gap-0.5 py-1 text-sm", className),
13091
- onDragEnd: draggable ? handleDragEnd : void 0,
13770
+ onDragEnd: dropEnabled ? handleDragEnd : void 0,
13092
13771
  children: nodes.map((node) => /* @__PURE__ */ jsxRuntime.jsx(TreeNode, { node, depth: 0 }, node.id))
13093
13772
  }
13094
13773
  ) });