@particle-academy/react-fancy 2.8.1 → 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.d.cts CHANGED
@@ -2231,7 +2231,12 @@ interface CanvasContextValue {
2231
2231
  nodeRects: Map<string, NodeRect>;
2232
2232
  registryVersion: number;
2233
2233
  containerRef: React.RefObject<HTMLDivElement | null>;
2234
+ /** Grid spacing in canvas-space pixels (unaffected by zoom). */
2235
+ gridSize: number;
2236
+ /** When true, dragged nodes snap their top-left corner to the grid. */
2237
+ snapToGrid: boolean;
2234
2238
  }
2239
+ type GridStyle = "dots" | "lines" | "none";
2235
2240
  interface CanvasProps {
2236
2241
  children: ReactNode;
2237
2242
  viewport?: ViewportState;
@@ -2241,8 +2246,17 @@ interface CanvasProps {
2241
2246
  maxZoom?: number;
2242
2247
  pannable?: boolean;
2243
2248
  zoomable?: boolean;
2249
+ /** Grid spacing in canvas-space pixels. Defaults to 20. */
2244
2250
  gridSize?: number;
2251
+ /** Show/hide the canvas grid. Defaults to false. */
2245
2252
  showGrid?: boolean;
2253
+ /** Grid pattern when shown — dots (default), lines, or none. Setting this
2254
+ * to "none" hides the grid even when `showGrid` is true. */
2255
+ gridStyle?: GridStyle;
2256
+ /** Grid color (any CSS color). Defaults to a faint zinc. */
2257
+ gridColor?: string;
2258
+ /** Snap dragged nodes to the grid. Defaults to false. */
2259
+ snapToGrid?: boolean;
2246
2260
  /** Automatically fit all nodes into view on initial mount */
2247
2261
  fitOnMount?: boolean;
2248
2262
  className?: string;
@@ -2308,7 +2322,7 @@ declare namespace CanvasControls {
2308
2322
  var displayName: string;
2309
2323
  }
2310
2324
 
2311
- declare function CanvasRoot({ children, viewport: controlledViewport, defaultViewport, onViewportChange, minZoom, maxZoom, pannable, zoomable, showGrid, fitOnMount, className, style, }: CanvasProps): react_jsx_runtime.JSX.Element;
2325
+ declare function CanvasRoot({ children, viewport: controlledViewport, defaultViewport, onViewportChange, minZoom, maxZoom, pannable, zoomable, showGrid, gridStyle, gridSize, gridColor, snapToGrid, fitOnMount, className, style, }: CanvasProps): react_jsx_runtime.JSX.Element;
2312
2326
  declare const Canvas: typeof CanvasRoot & {
2313
2327
  Node: typeof CanvasNode;
2314
2328
  Edge: typeof CanvasEdge;
@@ -2474,6 +2488,15 @@ interface TreeNavProps {
2474
2488
  draggable?: boolean;
2475
2489
  /** Callback when a node is moved via drag and drop */
2476
2490
  onNodeMove?: (sourceId: string, targetId: string, position: DropPosition) => void;
2491
+ /** Accept drops from outside the tree — OS files, items from other
2492
+ * components, etc. When true, drag-over is allowed for any data source
2493
+ * and `onExternalDrop` fires on drop. (default: false) */
2494
+ acceptExternalDrops?: boolean;
2495
+ /** Called when an external drag (file or cross-component) is dropped on
2496
+ * a node. The raw event lets you read `event.dataTransfer.files` for OS
2497
+ * file drops or `event.dataTransfer.getData(type)` for custom MIME
2498
+ * payloads from other components. */
2499
+ onExternalDrop?: (event: React.DragEvent, target: TreeNodeData, position: DropPosition) => void;
2477
2500
  /** Controlled expanded node IDs */
2478
2501
  expandedIds?: string[];
2479
2502
  /** Default expanded node IDs (uncontrolled) */
@@ -2501,6 +2524,8 @@ interface TreeNavContextValue {
2501
2524
  dragState: DragState;
2502
2525
  setDragState: (state: DragState) => void;
2503
2526
  onNodeMove?: (sourceId: string, targetId: string, position: DropPosition) => void;
2527
+ acceptExternalDrops: boolean;
2528
+ onExternalDrop?: (event: React.DragEvent, target: TreeNodeData, position: DropPosition) => void;
2504
2529
  nodes: TreeNodeData[];
2505
2530
  expandNode: (id: string) => void;
2506
2531
  }
@@ -2515,7 +2540,7 @@ declare namespace TreeNode {
2515
2540
  var displayName: string;
2516
2541
  }
2517
2542
 
2518
- declare function TreeNavRoot({ nodes, selectedId, onSelect, onNodeContextMenu, draggable, onNodeMove, expandedIds: controlledExpanded, defaultExpandedIds, onExpandedChange, defaultExpandAll, indentSize, showIcons, className, }: TreeNavProps): react_jsx_runtime.JSX.Element;
2543
+ declare function TreeNavRoot({ nodes, selectedId, onSelect, onNodeContextMenu, draggable, onNodeMove, acceptExternalDrops, onExternalDrop, expandedIds: controlledExpanded, defaultExpandedIds, onExpandedChange, defaultExpandAll, indentSize, showIcons, className, }: TreeNavProps): react_jsx_runtime.JSX.Element;
2519
2544
  declare namespace TreeNavRoot {
2520
2545
  var displayName: string;
2521
2546
  }
package/dist/index.d.ts CHANGED
@@ -2231,7 +2231,12 @@ interface CanvasContextValue {
2231
2231
  nodeRects: Map<string, NodeRect>;
2232
2232
  registryVersion: number;
2233
2233
  containerRef: React.RefObject<HTMLDivElement | null>;
2234
+ /** Grid spacing in canvas-space pixels (unaffected by zoom). */
2235
+ gridSize: number;
2236
+ /** When true, dragged nodes snap their top-left corner to the grid. */
2237
+ snapToGrid: boolean;
2234
2238
  }
2239
+ type GridStyle = "dots" | "lines" | "none";
2235
2240
  interface CanvasProps {
2236
2241
  children: ReactNode;
2237
2242
  viewport?: ViewportState;
@@ -2241,8 +2246,17 @@ interface CanvasProps {
2241
2246
  maxZoom?: number;
2242
2247
  pannable?: boolean;
2243
2248
  zoomable?: boolean;
2249
+ /** Grid spacing in canvas-space pixels. Defaults to 20. */
2244
2250
  gridSize?: number;
2251
+ /** Show/hide the canvas grid. Defaults to false. */
2245
2252
  showGrid?: boolean;
2253
+ /** Grid pattern when shown — dots (default), lines, or none. Setting this
2254
+ * to "none" hides the grid even when `showGrid` is true. */
2255
+ gridStyle?: GridStyle;
2256
+ /** Grid color (any CSS color). Defaults to a faint zinc. */
2257
+ gridColor?: string;
2258
+ /** Snap dragged nodes to the grid. Defaults to false. */
2259
+ snapToGrid?: boolean;
2246
2260
  /** Automatically fit all nodes into view on initial mount */
2247
2261
  fitOnMount?: boolean;
2248
2262
  className?: string;
@@ -2308,7 +2322,7 @@ declare namespace CanvasControls {
2308
2322
  var displayName: string;
2309
2323
  }
2310
2324
 
2311
- declare function CanvasRoot({ children, viewport: controlledViewport, defaultViewport, onViewportChange, minZoom, maxZoom, pannable, zoomable, showGrid, fitOnMount, className, style, }: CanvasProps): react_jsx_runtime.JSX.Element;
2325
+ declare function CanvasRoot({ children, viewport: controlledViewport, defaultViewport, onViewportChange, minZoom, maxZoom, pannable, zoomable, showGrid, gridStyle, gridSize, gridColor, snapToGrid, fitOnMount, className, style, }: CanvasProps): react_jsx_runtime.JSX.Element;
2312
2326
  declare const Canvas: typeof CanvasRoot & {
2313
2327
  Node: typeof CanvasNode;
2314
2328
  Edge: typeof CanvasEdge;
@@ -2474,6 +2488,15 @@ interface TreeNavProps {
2474
2488
  draggable?: boolean;
2475
2489
  /** Callback when a node is moved via drag and drop */
2476
2490
  onNodeMove?: (sourceId: string, targetId: string, position: DropPosition) => void;
2491
+ /** Accept drops from outside the tree — OS files, items from other
2492
+ * components, etc. When true, drag-over is allowed for any data source
2493
+ * and `onExternalDrop` fires on drop. (default: false) */
2494
+ acceptExternalDrops?: boolean;
2495
+ /** Called when an external drag (file or cross-component) is dropped on
2496
+ * a node. The raw event lets you read `event.dataTransfer.files` for OS
2497
+ * file drops or `event.dataTransfer.getData(type)` for custom MIME
2498
+ * payloads from other components. */
2499
+ onExternalDrop?: (event: React.DragEvent, target: TreeNodeData, position: DropPosition) => void;
2477
2500
  /** Controlled expanded node IDs */
2478
2501
  expandedIds?: string[];
2479
2502
  /** Default expanded node IDs (uncontrolled) */
@@ -2501,6 +2524,8 @@ interface TreeNavContextValue {
2501
2524
  dragState: DragState;
2502
2525
  setDragState: (state: DragState) => void;
2503
2526
  onNodeMove?: (sourceId: string, targetId: string, position: DropPosition) => void;
2527
+ acceptExternalDrops: boolean;
2528
+ onExternalDrop?: (event: React.DragEvent, target: TreeNodeData, position: DropPosition) => void;
2504
2529
  nodes: TreeNodeData[];
2505
2530
  expandNode: (id: string) => void;
2506
2531
  }
@@ -2515,7 +2540,7 @@ declare namespace TreeNode {
2515
2540
  var displayName: string;
2516
2541
  }
2517
2542
 
2518
- declare function TreeNavRoot({ nodes, selectedId, onSelect, onNodeContextMenu, draggable, onNodeMove, expandedIds: controlledExpanded, defaultExpandedIds, onExpandedChange, defaultExpandAll, indentSize, showIcons, className, }: TreeNavProps): react_jsx_runtime.JSX.Element;
2543
+ declare function TreeNavRoot({ nodes, selectedId, onSelect, onNodeContextMenu, draggable, onNodeMove, acceptExternalDrops, onExternalDrop, expandedIds: controlledExpanded, defaultExpandedIds, onExpandedChange, defaultExpandAll, indentSize, showIcons, className, }: TreeNavProps): react_jsx_runtime.JSX.Element;
2519
2544
  declare namespace TreeNavRoot {
2520
2545
  var displayName: string;
2521
2546
  }
package/dist/index.js CHANGED
@@ -11461,7 +11461,7 @@ function useCanvas() {
11461
11461
  return ctx;
11462
11462
  }
11463
11463
  function CanvasNode({ children, id, x, y, draggable, onPositionChange, className, style }) {
11464
- const { registerNode, unregisterNode, viewport } = useCanvas();
11464
+ const { registerNode, unregisterNode, viewport, gridSize, snapToGrid } = useCanvas();
11465
11465
  const nodeRef = useRef(null);
11466
11466
  const isDragging = useRef(false);
11467
11467
  const dragStart = useRef({ mouseX: 0, mouseY: 0, nodeX: 0, nodeY: 0 });
@@ -11494,9 +11494,15 @@ function CanvasNode({ children, id, x, y, draggable, onPositionChange, className
11494
11494
  if (!isDragging.current) return;
11495
11495
  const dx = (e.clientX - dragStart.current.mouseX) / viewport.zoom;
11496
11496
  const dy = (e.clientY - dragStart.current.mouseY) / viewport.zoom;
11497
- onPositionChange?.(dragStart.current.nodeX + dx, dragStart.current.nodeY + dy);
11497
+ let nx = dragStart.current.nodeX + dx;
11498
+ let ny = dragStart.current.nodeY + dy;
11499
+ if (snapToGrid && gridSize > 0) {
11500
+ nx = Math.round(nx / gridSize) * gridSize;
11501
+ ny = Math.round(ny / gridSize) * gridSize;
11502
+ }
11503
+ onPositionChange?.(nx, ny);
11498
11504
  },
11499
- [viewport.zoom, onPositionChange]
11505
+ [viewport.zoom, onPositionChange, snapToGrid, gridSize]
11500
11506
  );
11501
11507
  const handlePointerUp = useCallback(() => {
11502
11508
  isDragging.current = false;
@@ -11744,6 +11750,10 @@ function CanvasRoot({
11744
11750
  pannable = true,
11745
11751
  zoomable = true,
11746
11752
  showGrid = false,
11753
+ gridStyle = "dots",
11754
+ gridSize = 20,
11755
+ gridColor = "rgb(161 161 170 / 0.3)",
11756
+ snapToGrid = false,
11747
11757
  fitOnMount = false,
11748
11758
  className,
11749
11759
  style
@@ -11761,8 +11771,8 @@ function CanvasRoot({
11761
11771
  containerRef
11762
11772
  });
11763
11773
  const ctx = useMemo(
11764
- () => ({ viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion, containerRef }),
11765
- [viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion]
11774
+ () => ({ viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion, containerRef, gridSize, snapToGrid }),
11775
+ [viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion, gridSize, snapToGrid]
11766
11776
  );
11767
11777
  const hasFitted = useRef(false);
11768
11778
  useEffect(() => {
@@ -11818,9 +11828,13 @@ function CanvasRoot({
11818
11828
  {
11819
11829
  "data-canvas-bg": "",
11820
11830
  className: "absolute inset-0",
11821
- style: showGrid ? {
11822
- backgroundImage: `radial-gradient(circle, rgb(161 161 170 / 0.3) 1px, transparent 1px)`,
11823
- backgroundSize: `${20 * viewport.zoom}px ${20 * viewport.zoom}px`,
11831
+ style: showGrid && gridStyle !== "none" ? gridStyle === "lines" ? {
11832
+ backgroundImage: `linear-gradient(to right, ${gridColor} 1px, transparent 1px), linear-gradient(to bottom, ${gridColor} 1px, transparent 1px)`,
11833
+ backgroundSize: `${gridSize * viewport.zoom}px ${gridSize * viewport.zoom}px`,
11834
+ backgroundPosition: `${viewport.panX}px ${viewport.panY}px`
11835
+ } : {
11836
+ backgroundImage: `radial-gradient(circle, ${gridColor} 1px, transparent 1px)`,
11837
+ backgroundSize: `${gridSize * viewport.zoom}px ${gridSize * viewport.zoom}px`,
11824
11838
  backgroundPosition: `${viewport.panX}px ${viewport.panY}px`
11825
11839
  } : void 0
11826
11840
  }
@@ -12537,6 +12551,8 @@ function TreeNode({ node, depth }) {
12537
12551
  dragState,
12538
12552
  setDragState,
12539
12553
  onNodeMove,
12554
+ acceptExternalDrops,
12555
+ onExternalDrop,
12540
12556
  nodes,
12541
12557
  expandNode
12542
12558
  } = useTreeNav();
@@ -12579,14 +12595,18 @@ function TreeNode({ node, depth }) {
12579
12595
  clearAutoExpand();
12580
12596
  setDragState({ draggedNodeId: null, dropTargetId: null, dropPosition: null });
12581
12597
  }, [clearAutoExpand, setDragState]);
12598
+ const isExternalDrag = !dragState.draggedNodeId;
12582
12599
  const handleDragOver = useCallback((e) => {
12583
- if (!dragState.draggedNodeId) return;
12584
- const sourceId = dragState.draggedNodeId;
12585
- if (sourceId === node.id) return;
12586
- if (isDescendantOf(nodes, sourceId, node.id)) return;
12600
+ if (isExternalDrag) {
12601
+ if (!acceptExternalDrops) return;
12602
+ } else {
12603
+ const sourceId = dragState.draggedNodeId;
12604
+ if (sourceId === node.id) return;
12605
+ if (isDescendantOf(nodes, sourceId, node.id)) return;
12606
+ }
12587
12607
  e.preventDefault();
12588
12608
  e.stopPropagation();
12589
- e.dataTransfer.dropEffect = "move";
12609
+ e.dataTransfer.dropEffect = isExternalDrag ? "copy" : "move";
12590
12610
  const position = computeDropPosition(e, !!isFolder);
12591
12611
  if (isFolder && !isExpanded && position === "inside") {
12592
12612
  if (!autoExpandTimer.current) {
@@ -12599,9 +12619,9 @@ function TreeNode({ node, depth }) {
12599
12619
  clearAutoExpand();
12600
12620
  }
12601
12621
  if (dragState.dropTargetId !== node.id || dragState.dropPosition !== position) {
12602
- setDragState({ draggedNodeId: sourceId, dropTargetId: node.id, dropPosition: position });
12622
+ setDragState({ draggedNodeId: dragState.draggedNodeId, dropTargetId: node.id, dropPosition: position });
12603
12623
  }
12604
- }, [dragState, node.id, isFolder, isExpanded, nodes, setDragState, expandNode, clearAutoExpand]);
12624
+ }, [dragState, isExternalDrag, acceptExternalDrops, node.id, isFolder, isExpanded, nodes, setDragState, expandNode, clearAutoExpand]);
12605
12625
  const handleDragLeave = useCallback((e) => {
12606
12626
  if (!e.currentTarget.contains(e.relatedTarget)) {
12607
12627
  clearAutoExpand();
@@ -12615,21 +12635,25 @@ function TreeNode({ node, depth }) {
12615
12635
  e.stopPropagation();
12616
12636
  clearAutoExpand();
12617
12637
  const sourceId = dragState.draggedNodeId;
12618
- const position = dragState.dropPosition;
12619
- if (!sourceId || !position) return;
12620
- if (sourceId === node.id) return;
12621
- if (isDescendantOf(nodes, sourceId, node.id)) return;
12622
- onNodeMove?.(sourceId, node.id, position);
12638
+ const position = dragState.dropPosition ?? computeDropPosition(e, !!isFolder);
12639
+ if (sourceId) {
12640
+ if (sourceId === node.id) return;
12641
+ if (isDescendantOf(nodes, sourceId, node.id)) return;
12642
+ onNodeMove?.(sourceId, node.id, position);
12643
+ } else if (acceptExternalDrops) {
12644
+ onExternalDrop?.(e, node, position);
12645
+ }
12623
12646
  setDragState({ draggedNodeId: null, dropTargetId: null, dropPosition: null });
12624
- }, [dragState, node.id, nodes, onNodeMove, setDragState, clearAutoExpand]);
12647
+ }, [dragState, node, isFolder, nodes, onNodeMove, acceptExternalDrops, onExternalDrop, setDragState, clearAutoExpand]);
12625
12648
  const canDrag = draggable && !node.disabled;
12649
+ const dropEnabled = draggable || acceptExternalDrops;
12626
12650
  return /* @__PURE__ */ jsxs(
12627
12651
  "div",
12628
12652
  {
12629
12653
  "data-react-fancy-tree-node": "",
12630
- onDragOver: draggable ? handleDragOver : void 0,
12631
- onDragLeave: draggable ? handleDragLeave : void 0,
12632
- onDrop: draggable ? handleDrop : void 0,
12654
+ onDragOver: dropEnabled ? handleDragOver : void 0,
12655
+ onDragLeave: dropEnabled ? handleDragLeave : void 0,
12656
+ onDrop: dropEnabled ? handleDrop : void 0,
12633
12657
  children: [
12634
12658
  isDropTarget && dropPosition === "before" && /* @__PURE__ */ jsx(
12635
12659
  "div",
@@ -12702,6 +12726,8 @@ function TreeNavRoot({
12702
12726
  onNodeContextMenu,
12703
12727
  draggable = false,
12704
12728
  onNodeMove,
12729
+ acceptExternalDrops = false,
12730
+ onExternalDrop,
12705
12731
  expandedIds: controlledExpanded,
12706
12732
  defaultExpandedIds,
12707
12733
  onExpandedChange,
@@ -12755,6 +12781,8 @@ function TreeNavRoot({
12755
12781
  dragState,
12756
12782
  setDragState,
12757
12783
  onNodeMove,
12784
+ acceptExternalDrops,
12785
+ onExternalDrop,
12758
12786
  nodes,
12759
12787
  expandNode
12760
12788
  }),
@@ -12769,16 +12797,19 @@ function TreeNavRoot({
12769
12797
  draggable,
12770
12798
  dragState,
12771
12799
  onNodeMove,
12800
+ acceptExternalDrops,
12801
+ onExternalDrop,
12772
12802
  nodes,
12773
12803
  expandNode
12774
12804
  ]
12775
12805
  );
12806
+ const dropEnabled = draggable || acceptExternalDrops;
12776
12807
  return /* @__PURE__ */ jsx(TreeNavContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
12777
12808
  "nav",
12778
12809
  {
12779
12810
  "data-react-fancy-tree-nav": "",
12780
12811
  className: cn("flex flex-col gap-0.5 py-1 text-sm", className),
12781
- onDragEnd: draggable ? handleDragEnd : void 0,
12812
+ onDragEnd: dropEnabled ? handleDragEnd : void 0,
12782
12813
  children: nodes.map((node) => /* @__PURE__ */ jsx(TreeNode, { node, depth: 0 }, node.id))
12783
12814
  }
12784
12815
  ) });