@particle-academy/react-fancy 2.8.0 → 2.9.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
@@ -11462,12 +11462,30 @@ function countCardChildren(children) {
11462
11462
  n += 1;
11463
11463
  return;
11464
11464
  }
11465
- if (child.type === react.Fragment) {
11466
- n += countCardChildren(child.props.children);
11467
- }
11465
+ const inner = child.props.children;
11466
+ if (inner !== void 0) n += countCardChildren(inner);
11468
11467
  });
11469
11468
  return n;
11470
11469
  }
11470
+ function findCardIndex(children, cardId) {
11471
+ let idx = -1;
11472
+ let i = 0;
11473
+ function walk2(nodes) {
11474
+ react.Children.forEach(nodes, (child) => {
11475
+ if (idx !== -1) return;
11476
+ if (!react.isValidElement(child)) return;
11477
+ if (child.type === KanbanCard) {
11478
+ if (child.props.id === cardId) idx = i;
11479
+ i += 1;
11480
+ return;
11481
+ }
11482
+ const inner = child.props.children;
11483
+ if (inner !== void 0) walk2(inner);
11484
+ });
11485
+ }
11486
+ walk2(children);
11487
+ return idx;
11488
+ }
11471
11489
  function KanbanColumn({
11472
11490
  children,
11473
11491
  id,
@@ -11480,26 +11498,46 @@ function KanbanColumn({
11480
11498
  const { onCardMove, draggedCard, dragSource, registerColumn } = useKanban();
11481
11499
  const [dragOver, setDragOver] = react.useState(false);
11482
11500
  const [dropIndex, setDropIndex] = react.useState(null);
11501
+ const [dropY, setDropY] = react.useState(null);
11483
11502
  const cardsRef = react.useRef(null);
11484
11503
  react.useEffect(() => registerColumn(id), [id, registerColumn]);
11485
- const updateDropIndex = react.useCallback((clientY) => {
11504
+ const updateDrop = react.useCallback((clientY) => {
11486
11505
  const container = cardsRef.current;
11487
11506
  if (!container) {
11488
11507
  setDropIndex(null);
11508
+ setDropY(null);
11489
11509
  return;
11490
11510
  }
11491
- const cards = container.querySelectorAll(
11492
- ":scope > [data-react-fancy-kanban-card]"
11511
+ const all = Array.from(
11512
+ container.querySelectorAll(
11513
+ "[data-react-fancy-kanban-card]"
11514
+ )
11515
+ );
11516
+ const cards = all.filter(
11517
+ (el) => el.closest("[data-react-fancy-kanban-column]") === cardsRef.current?.closest("[data-react-fancy-kanban-column]")
11493
11518
  );
11519
+ const containerRect = container.getBoundingClientRect();
11520
+ if (cards.length === 0) {
11521
+ setDropIndex(0);
11522
+ setDropY(0);
11523
+ return;
11524
+ }
11494
11525
  let idx = cards.length;
11526
+ let yRel = 0;
11495
11527
  for (let i = 0; i < cards.length; i++) {
11496
11528
  const rect = cards[i].getBoundingClientRect();
11497
11529
  if (clientY < rect.top + rect.height / 2) {
11498
11530
  idx = i;
11531
+ yRel = rect.top - containerRect.top;
11499
11532
  break;
11500
11533
  }
11501
11534
  }
11535
+ if (idx === cards.length) {
11536
+ const last = cards[cards.length - 1].getBoundingClientRect();
11537
+ yRel = last.bottom - containerRect.top;
11538
+ }
11502
11539
  setDropIndex(idx);
11540
+ setDropY(yRel);
11503
11541
  }, []);
11504
11542
  const handleDragOver = react.useCallback(
11505
11543
  (e) => {
@@ -11507,14 +11545,15 @@ function KanbanColumn({
11507
11545
  e.preventDefault();
11508
11546
  e.stopPropagation();
11509
11547
  setDragOver(true);
11510
- updateDropIndex(e.clientY);
11548
+ updateDrop(e.clientY);
11511
11549
  },
11512
- [draggedCard, updateDropIndex]
11550
+ [draggedCard, updateDrop]
11513
11551
  );
11514
11552
  const handleDragLeave = react.useCallback((e) => {
11515
11553
  if (e.currentTarget.contains(e.relatedTarget)) return;
11516
11554
  setDragOver(false);
11517
11555
  setDropIndex(null);
11556
+ setDropY(null);
11518
11557
  }, []);
11519
11558
  const handleDrop = react.useCallback(
11520
11559
  (e) => {
@@ -11522,16 +11561,15 @@ function KanbanColumn({
11522
11561
  e.preventDefault();
11523
11562
  e.stopPropagation();
11524
11563
  const target = dropIndex ?? 0;
11525
- if (dragSource && draggedCard) {
11564
+ if (dragSource) {
11526
11565
  let finalIdx = target;
11527
11566
  if (dragSource === id) {
11528
11567
  const srcIdx = findCardIndex(children, draggedCard);
11529
- if (srcIdx !== -1 && target > srcIdx) {
11530
- finalIdx = target - 1;
11531
- }
11568
+ if (srcIdx !== -1 && target > srcIdx) finalIdx = target - 1;
11532
11569
  if (srcIdx === finalIdx) {
11533
11570
  setDragOver(false);
11534
11571
  setDropIndex(null);
11572
+ setDropY(null);
11535
11573
  return;
11536
11574
  }
11537
11575
  }
@@ -11539,27 +11577,13 @@ function KanbanColumn({
11539
11577
  }
11540
11578
  setDragOver(false);
11541
11579
  setDropIndex(null);
11580
+ setDropY(null);
11542
11581
  },
11543
11582
  [draggedCard, dragSource, dropIndex, id, onCardMove, children]
11544
11583
  );
11545
11584
  const cardCount = countCardChildren(children);
11546
- if (hideWhenEmpty && cardCount === 0 && !draggedCard) {
11547
- return null;
11548
- }
11549
- let cardSeen = 0;
11550
- const showIndicator = draggedCard !== null && dropIndex !== null && dragOver;
11551
- const renderedChildren = react.Children.toArray(children).map((child, i) => {
11552
- const isCard = react.isValidElement(child) && child.type === KanbanCard;
11553
- const indicator = showIndicator && isCard && cardSeen === dropIndex ? /* @__PURE__ */ jsxRuntime.jsx(DropIndicator, {}, `drop-${i}`) : null;
11554
- if (isCard) cardSeen += 1;
11555
- return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
11556
- indicator,
11557
- child
11558
- ] }, i);
11559
- });
11560
- if (showIndicator && dropIndex === cardCount) {
11561
- renderedChildren.push(/* @__PURE__ */ jsxRuntime.jsx(DropIndicator, {}, "drop-end"));
11562
- }
11585
+ if (hideWhenEmpty && cardCount === 0 && !draggedCard) return null;
11586
+ const showIndicator = draggedCard !== null && dropIndex !== null && dropY !== null && dragOver;
11563
11587
  const overWip = wipLimit !== void 0 && cardCount > wipLimit;
11564
11588
  return /* @__PURE__ */ jsxRuntime.jsx(KanbanColumnContext.Provider, { value: id, children: /* @__PURE__ */ jsxRuntime.jsxs(
11565
11589
  "div",
@@ -11594,36 +11618,22 @@ function KanbanColumn({
11594
11618
  }
11595
11619
  )
11596
11620
  ] }),
11597
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: cardsRef, className: "flex flex-1 flex-col gap-2", children: renderedChildren })
11621
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: cardsRef, className: "relative flex flex-1 flex-col gap-2", children: [
11622
+ children,
11623
+ showIndicator && /* @__PURE__ */ jsxRuntime.jsx(
11624
+ "div",
11625
+ {
11626
+ "data-react-fancy-kanban-drop-indicator": "",
11627
+ style: { top: dropY },
11628
+ className: "pointer-events-none absolute left-0 right-0 h-0.5 -translate-y-1/2 rounded-full bg-blue-500/80 shadow-[0_0_0_3px_rgba(59,130,246,0.15)]"
11629
+ }
11630
+ )
11631
+ ] })
11598
11632
  ]
11599
11633
  }
11600
11634
  ) });
11601
11635
  }
11602
11636
  KanbanColumn.displayName = "KanbanColumn";
11603
- function DropIndicator() {
11604
- return /* @__PURE__ */ jsxRuntime.jsx(
11605
- "div",
11606
- {
11607
- "data-react-fancy-kanban-drop-indicator": "",
11608
- className: "h-0.5 -my-1 rounded-full bg-blue-500/80"
11609
- }
11610
- );
11611
- }
11612
- function findCardIndex(children, cardId) {
11613
- let idx = -1;
11614
- let i = 0;
11615
- react.Children.forEach(children, (child) => {
11616
- if (idx !== -1) return;
11617
- if (!react.isValidElement(child)) return;
11618
- if (child.type === KanbanCard) {
11619
- if (child.props.id === cardId) {
11620
- idx = i;
11621
- }
11622
- i += 1;
11623
- }
11624
- });
11625
- return idx;
11626
- }
11627
11637
  function KanbanColumnHandle({
11628
11638
  children,
11629
11639
  className
@@ -11761,7 +11771,7 @@ function useCanvas() {
11761
11771
  return ctx;
11762
11772
  }
11763
11773
  function CanvasNode({ children, id, x, y, draggable, onPositionChange, className, style }) {
11764
- const { registerNode, unregisterNode, viewport } = useCanvas();
11774
+ const { registerNode, unregisterNode, viewport, gridSize, snapToGrid } = useCanvas();
11765
11775
  const nodeRef = react.useRef(null);
11766
11776
  const isDragging = react.useRef(false);
11767
11777
  const dragStart = react.useRef({ mouseX: 0, mouseY: 0, nodeX: 0, nodeY: 0 });
@@ -11794,9 +11804,15 @@ function CanvasNode({ children, id, x, y, draggable, onPositionChange, className
11794
11804
  if (!isDragging.current) return;
11795
11805
  const dx = (e.clientX - dragStart.current.mouseX) / viewport.zoom;
11796
11806
  const dy = (e.clientY - dragStart.current.mouseY) / viewport.zoom;
11797
- 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);
11798
11814
  },
11799
- [viewport.zoom, onPositionChange]
11815
+ [viewport.zoom, onPositionChange, snapToGrid, gridSize]
11800
11816
  );
11801
11817
  const handlePointerUp = react.useCallback(() => {
11802
11818
  isDragging.current = false;
@@ -12044,6 +12060,10 @@ function CanvasRoot({
12044
12060
  pannable = true,
12045
12061
  zoomable = true,
12046
12062
  showGrid = false,
12063
+ gridStyle = "dots",
12064
+ gridSize = 20,
12065
+ gridColor = "rgb(161 161 170 / 0.3)",
12066
+ snapToGrid = false,
12047
12067
  fitOnMount = false,
12048
12068
  className,
12049
12069
  style
@@ -12061,8 +12081,8 @@ function CanvasRoot({
12061
12081
  containerRef
12062
12082
  });
12063
12083
  const ctx = react.useMemo(
12064
- () => ({ viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion, containerRef }),
12065
- [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]
12066
12086
  );
12067
12087
  const hasFitted = react.useRef(false);
12068
12088
  react.useEffect(() => {
@@ -12118,9 +12138,13 @@ function CanvasRoot({
12118
12138
  {
12119
12139
  "data-canvas-bg": "",
12120
12140
  className: "absolute inset-0",
12121
- style: showGrid ? {
12122
- backgroundImage: `radial-gradient(circle, rgb(161 161 170 / 0.3) 1px, transparent 1px)`,
12123
- 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`,
12124
12148
  backgroundPosition: `${viewport.panX}px ${viewport.panY}px`
12125
12149
  } : void 0
12126
12150
  }
@@ -12837,6 +12861,8 @@ function TreeNode({ node, depth }) {
12837
12861
  dragState,
12838
12862
  setDragState,
12839
12863
  onNodeMove,
12864
+ acceptExternalDrops,
12865
+ onExternalDrop,
12840
12866
  nodes,
12841
12867
  expandNode
12842
12868
  } = useTreeNav();
@@ -12879,14 +12905,18 @@ function TreeNode({ node, depth }) {
12879
12905
  clearAutoExpand();
12880
12906
  setDragState({ draggedNodeId: null, dropTargetId: null, dropPosition: null });
12881
12907
  }, [clearAutoExpand, setDragState]);
12908
+ const isExternalDrag = !dragState.draggedNodeId;
12882
12909
  const handleDragOver = react.useCallback((e) => {
12883
- if (!dragState.draggedNodeId) return;
12884
- const sourceId = dragState.draggedNodeId;
12885
- if (sourceId === node.id) return;
12886
- if (isDescendantOf(nodes, sourceId, node.id)) return;
12910
+ if (isExternalDrag) {
12911
+ if (!acceptExternalDrops) return;
12912
+ } else {
12913
+ const sourceId = dragState.draggedNodeId;
12914
+ if (sourceId === node.id) return;
12915
+ if (isDescendantOf(nodes, sourceId, node.id)) return;
12916
+ }
12887
12917
  e.preventDefault();
12888
12918
  e.stopPropagation();
12889
- e.dataTransfer.dropEffect = "move";
12919
+ e.dataTransfer.dropEffect = isExternalDrag ? "copy" : "move";
12890
12920
  const position = computeDropPosition(e, !!isFolder);
12891
12921
  if (isFolder && !isExpanded && position === "inside") {
12892
12922
  if (!autoExpandTimer.current) {
@@ -12899,9 +12929,9 @@ function TreeNode({ node, depth }) {
12899
12929
  clearAutoExpand();
12900
12930
  }
12901
12931
  if (dragState.dropTargetId !== node.id || dragState.dropPosition !== position) {
12902
- setDragState({ draggedNodeId: sourceId, dropTargetId: node.id, dropPosition: position });
12932
+ setDragState({ draggedNodeId: dragState.draggedNodeId, dropTargetId: node.id, dropPosition: position });
12903
12933
  }
12904
- }, [dragState, node.id, isFolder, isExpanded, nodes, setDragState, expandNode, clearAutoExpand]);
12934
+ }, [dragState, isExternalDrag, acceptExternalDrops, node.id, isFolder, isExpanded, nodes, setDragState, expandNode, clearAutoExpand]);
12905
12935
  const handleDragLeave = react.useCallback((e) => {
12906
12936
  if (!e.currentTarget.contains(e.relatedTarget)) {
12907
12937
  clearAutoExpand();
@@ -12915,21 +12945,25 @@ function TreeNode({ node, depth }) {
12915
12945
  e.stopPropagation();
12916
12946
  clearAutoExpand();
12917
12947
  const sourceId = dragState.draggedNodeId;
12918
- const position = dragState.dropPosition;
12919
- if (!sourceId || !position) return;
12920
- if (sourceId === node.id) return;
12921
- if (isDescendantOf(nodes, sourceId, node.id)) return;
12922
- onNodeMove?.(sourceId, node.id, position);
12948
+ const position = dragState.dropPosition ?? computeDropPosition(e, !!isFolder);
12949
+ if (sourceId) {
12950
+ if (sourceId === node.id) return;
12951
+ if (isDescendantOf(nodes, sourceId, node.id)) return;
12952
+ onNodeMove?.(sourceId, node.id, position);
12953
+ } else if (acceptExternalDrops) {
12954
+ onExternalDrop?.(e, node, position);
12955
+ }
12923
12956
  setDragState({ draggedNodeId: null, dropTargetId: null, dropPosition: null });
12924
- }, [dragState, node.id, nodes, onNodeMove, setDragState, clearAutoExpand]);
12957
+ }, [dragState, node, isFolder, nodes, onNodeMove, acceptExternalDrops, onExternalDrop, setDragState, clearAutoExpand]);
12925
12958
  const canDrag = draggable && !node.disabled;
12959
+ const dropEnabled = draggable || acceptExternalDrops;
12926
12960
  return /* @__PURE__ */ jsxRuntime.jsxs(
12927
12961
  "div",
12928
12962
  {
12929
12963
  "data-react-fancy-tree-node": "",
12930
- onDragOver: draggable ? handleDragOver : void 0,
12931
- onDragLeave: draggable ? handleDragLeave : void 0,
12932
- onDrop: draggable ? handleDrop : void 0,
12964
+ onDragOver: dropEnabled ? handleDragOver : void 0,
12965
+ onDragLeave: dropEnabled ? handleDragLeave : void 0,
12966
+ onDrop: dropEnabled ? handleDrop : void 0,
12933
12967
  children: [
12934
12968
  isDropTarget && dropPosition === "before" && /* @__PURE__ */ jsxRuntime.jsx(
12935
12969
  "div",
@@ -13002,6 +13036,8 @@ function TreeNavRoot({
13002
13036
  onNodeContextMenu,
13003
13037
  draggable = false,
13004
13038
  onNodeMove,
13039
+ acceptExternalDrops = false,
13040
+ onExternalDrop,
13005
13041
  expandedIds: controlledExpanded,
13006
13042
  defaultExpandedIds,
13007
13043
  onExpandedChange,
@@ -13055,6 +13091,8 @@ function TreeNavRoot({
13055
13091
  dragState,
13056
13092
  setDragState,
13057
13093
  onNodeMove,
13094
+ acceptExternalDrops,
13095
+ onExternalDrop,
13058
13096
  nodes,
13059
13097
  expandNode
13060
13098
  }),
@@ -13069,16 +13107,19 @@ function TreeNavRoot({
13069
13107
  draggable,
13070
13108
  dragState,
13071
13109
  onNodeMove,
13110
+ acceptExternalDrops,
13111
+ onExternalDrop,
13072
13112
  nodes,
13073
13113
  expandNode
13074
13114
  ]
13075
13115
  );
13116
+ const dropEnabled = draggable || acceptExternalDrops;
13076
13117
  return /* @__PURE__ */ jsxRuntime.jsx(TreeNavContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
13077
13118
  "nav",
13078
13119
  {
13079
13120
  "data-react-fancy-tree-nav": "",
13080
13121
  className: cn("flex flex-col gap-0.5 py-1 text-sm", className),
13081
- onDragEnd: draggable ? handleDragEnd : void 0,
13122
+ onDragEnd: dropEnabled ? handleDragEnd : void 0,
13082
13123
  children: nodes.map((node) => /* @__PURE__ */ jsxRuntime.jsx(TreeNode, { node, depth: 0 }, node.id))
13083
13124
  }
13084
13125
  ) });