@mhamz.01/easyflow-whiteboard 1.27.0 → 2.0.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.
@@ -1 +1 @@
1
- {"version":3,"file":"custom-node-overlay-layer.d.ts","sourceRoot":"","sources":["../../../src/components/node/custom-node-overlay-layer.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAM9C,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACxC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,UAAU,uBAAuB;IAC/B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACxC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,YAAY,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzE,qBAAqB,CAAC,EAAE,YAAY,EAAE,CAAC;IACvC,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;AAaD,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EACzC,KAAK,EACL,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,UAAc,EACd,cAA+B,EAC/B,YAAmB,EACnB,qBAA0B,EAC1B,YAAY,GACb,EAAE,uBAAuB,2CA4dzB"}
1
+ {"version":3,"file":"custom-node-overlay-layer.d.ts","sourceRoot":"","sources":["../../../src/components/node/custom-node-overlay-layer.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAM9C,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACxC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,UAAU,uBAAuB;IAC/B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACxC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,YAAY,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzE,qBAAqB,CAAC,EAAE,YAAY,EAAE,CAAC;IACvC,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;AAaD,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EACzC,KAAK,EACL,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,UAAc,EACd,cAA+B,EAC/B,YAAmB,EACnB,qBAA0B,EAC1B,YAAY,GACb,EAAE,uBAAuB,2CAudzB"}
@@ -128,38 +128,37 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
128
128
  return { x: doc.x, y: doc.y };
129
129
  return undefined;
130
130
  };
131
- const isItemInSelectionBox = (nodeX, nodeY, width, height, box) => {
132
- // EVERYTHING HERE IS NOW IN WORLD (SCENE) COORDINATES
133
- // No canvasZoom or canvasViewport needed for the check!
134
- const nodeX2 = nodeX + width;
135
- const nodeY2 = nodeY + height;
136
- return !(box.x2 < nodeX ||
137
- box.x1 > nodeX2 ||
138
- box.y2 < nodeY ||
139
- box.y1 > nodeY2);
131
+ const isItemInSelectionBox = (x, y, width, height, box) => {
132
+ const itemX1 = x * canvasZoom + canvasViewport.x;
133
+ const itemY1 = y * canvasZoom + canvasViewport.y;
134
+ const itemX2 = itemX1 + width * canvasZoom;
135
+ const itemY2 = itemY1 + height * canvasZoom;
136
+ const boxX1 = Math.min(box.x1, box.x2);
137
+ const boxY1 = Math.min(box.y1, box.y2);
138
+ const boxX2 = Math.max(box.x1, box.x2);
139
+ const boxY2 = Math.max(box.y1, box.y2);
140
+ return !(boxX2 < itemX1 || boxX1 > itemX2 || boxY2 < itemY1 || boxY1 > itemY2);
140
141
  };
141
142
  // ── Selection box detection ──────────────────────────────────────────────────
142
143
  useEffect(() => {
143
144
  if (!selectionBox)
144
- return;
145
+ return; // Don't clear on null — let keyboard/click handlers do that
145
146
  const newSelected = new Set();
146
- // Use the actual dimensions of your components
147
- // Better yet: Pass these as props if they are dynamic
148
- const NODE_WIDTH = 300;
149
- const TASK_HEIGHT = 140;
150
- const DOC_HEIGHT = 160;
151
- localTasks.forEach((t) => {
152
- if (isItemInSelectionBox(t.x, t.y, NODE_WIDTH, TASK_HEIGHT, selectionBox)) {
153
- newSelected.add(t.id);
154
- }
147
+ localTasks.forEach((task) => {
148
+ if (isItemInSelectionBox(task.x, task.y, 300, 140, selectionBox))
149
+ newSelected.add(task.id);
155
150
  });
156
- localDocuments.forEach((d) => {
157
- if (isItemInSelectionBox(d.x, d.y, NODE_WIDTH, DOC_HEIGHT, selectionBox)) {
158
- newSelected.add(d.id);
159
- }
151
+ localDocuments.forEach((doc) => {
152
+ if (isItemInSelectionBox(doc.x, doc.y, 300, 160, selectionBox))
153
+ newSelected.add(doc.id);
154
+ });
155
+ // Only update if something actually changed (avoids re-render churn)
156
+ setSelectedIds((prev) => {
157
+ const prevArr = Array.from(prev).sort().join(",");
158
+ const nextArr = Array.from(newSelected).sort().join(",");
159
+ return prevArr === nextArr ? prev : newSelected;
160
160
  });
161
- setSelectedIds(newSelected);
162
- }, [selectionBox]);
161
+ }, [selectionBox, localTasks, localDocuments]);
163
162
  // ── Drag start (HTML Node side) ──────────────────────────────────────────────
164
163
  // Helper to extract coordinates regardless of event type
165
164
  const getPointerEvent = (e) => {
@@ -329,25 +328,24 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
329
328
  }, [localTasks, localDocuments, selectedIds, onTasksUpdate, onDocumentsUpdate]);
330
329
  // ── Render helper ────────────────────────────────────────────────────────────
331
330
  const renderItem = (id, x, y, children) => {
332
- // We apply the viewport offset to the container,
333
- // so here we only care about the scaled local position.
334
331
  const screenX = x * canvasZoom;
335
332
  const screenY = y * canvasZoom;
333
+ // 1. Detect if the user is interacting with the canvas at all
334
+ // 'dragging' is your existing state.
335
+ // You might want to pass 'isZooming' or 'isPanning' from your main canvas component here.
336
336
  const isDragging = dragging?.itemIds.includes(id);
337
337
  return (_jsx("div", { className: "pointer-events-auto absolute", style: {
338
338
  left: 0,
339
339
  top: 0,
340
- // Using translate3d(x, y, 0) is non-negotiable for 2026 performance
340
+ // 2. Use translate3d for GPU performance
341
341
  transform: `translate3d(${screenX}px, ${screenY}px, 0) scale(${canvasZoom})`,
342
342
  transformOrigin: "top left",
343
- // CRITICAL: Disable all transitions.
344
- // Even a 100ms transition will cause the node to "float" away during zoom.
343
+ // 3. THE FIX: Remove transition entirely during any viewport change
344
+ // Any 'ease' during zoom causes the "shaking" behavior.
345
345
  transition: "none",
346
+ // 4. Optimization
346
347
  willChange: "transform",
347
348
  zIndex: isDragging ? 1000 : 1,
348
- // Added: Prevent browser from trying to optimize text rendering during move
349
- // which causes the "shaking" text bug.
350
- backfaceVisibility: "hidden",
351
349
  }, children: children }, id));
352
350
  };
353
351
  return (_jsx("div", { ref: overlayRef, className: "absolute inset-0 pointer-events-none", style: { zIndex: 50 }, onWheel: handleOverlayWheel, onClick: (e) => {
@@ -11,16 +11,16 @@
11
11
  // FabricImage,
12
12
  // classRegistry
13
13
  // } from "fabric";
14
- // import { useWhiteboardStore } from "@/app/store/whiteboard-store";
14
+ // import { useWhiteboardStore } from "../../store/whiteboard-store";
15
15
  // import WhiteboardToolbar from "../toolbar/whiteboard-toolbar";
16
16
  // import ToolOptionsPanel from "../toolbar/tooloptions-panel";
17
17
  // import CanvasOverlayLayer from "../node/custom-node-overlay-layer";
18
18
  // import type { Document } from "../node/custom-node-overlay-layer";
19
19
  // import ZoomControls from "../zoomcontrol/zoom-control";
20
- // import { initializeFabricCanvas, updateDrawingObject, addText, calculateDashArray,addWelcomeContent } from "@/lib/fabric-utils";
21
- // import { Frame } from "@/lib/fabric-frame";
22
- // import { Arrow } from "@/lib/fabric-arrow";
23
- // import { BidirectionalArrow } from "@/lib/fabric-bidirectional-arrow";
20
+ // import { initializeFabricCanvas, updateDrawingObject, addText, calculateDashArray,addWelcomeContent } from "../..//lib/fabric-utils";
21
+ // import { Frame } from "../../lib/fabric-frame";
22
+ // import { Arrow } from "../../lib/fabric-arrow";
23
+ // import { BidirectionalArrow } from "../../lib/fabric-bidirectional-arrow";
24
24
  // import * as fabric from "fabric";
25
25
  // interface Task {
26
26
  // id: string;
@@ -896,6 +896,7 @@
896
896
  // const handleAddTaskFromDropdown = (taskTemplate: { id: string; title: string; status: "todo" | "in-progress" | "done"; assignee?: string; project?: string; priority?: "low" | "medium" | "high"; dueDate?: string; }) => { const canvas = fabricCanvasRef.current; if (!canvas) return; const vpt = canvas.viewportTransform; if (!vpt) return; const cx = (canvas.getWidth() / 2 - vpt[4]) / canvasZoom; const cy = (canvas.getHeight() / 2 - vpt[5]) / canvasZoom; setTasks((prev) => [...prev, { ...taskTemplate, id: `${taskTemplate.id}-${Date.now()}`, x: cx - 150, y: cy - 60 }]); };
897
897
  // const handleAddDocumentFromDropdown = (docTemplate: Omit<Document, "x" | "y">) => { const canvas = fabricCanvasRef.current; if (!canvas) return; const vpt = canvas.viewportTransform; if (!vpt) return; const cx = (canvas.getWidth() / 2 - vpt[4]) / canvasZoom; const cy = (canvas.getHeight() / 2 - vpt[5]) / canvasZoom; setDocuments((prev) => [...prev, { ...docTemplate, id: `${docTemplate.id}-${Date.now()}`, x: cx - 150, y: cy - 80 }]); };
898
898
  // return (
899
+ // <div className="easyflow-whiteboard">
899
900
  // <div ref={containerRef} className="relative w-full h-screen overflow-hidden bg-[#0b0b0b]">
900
901
  // <div className="absolute inset-0 pointer-events-none" style={{ backgroundImage: `radial-gradient(circle, rgba(255,255,255,0.2) 1.2px, transparent 1.2px)`, backgroundSize: "40px 40px", zIndex: 0 }} />
901
902
  // <canvas ref={canvasRef} className="absolute inset-0" style={{ zIndex: 1 }} />
@@ -907,5 +908,6 @@
907
908
  // <div className="pointer-events-auto"><ZoomControls zoom={canvasZoom} onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onResetZoom={handleResetZoom} /></div>
908
909
  // </div>
909
910
  // </div>
911
+ // </div>
910
912
  // );
911
913
  // }
@@ -1 +1 @@
1
- {"version":3,"file":"useSelection.d.ts","sourceRoot":"","sources":["../../src/hooks/useSelection.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAQ,YAAY,EAAe,MAAM,QAAQ,CAAC;AAOjE,UAAU,iBAAiB;IACzB,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,eAAe,EAAE,CAAC,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1F,wBAAwB,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IAC5D,YAAY,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;CAC/C;AAED,eAAO,MAAM,YAAY,GAAI,oHAQ1B,iBAAiB,SA2JnB,CAAC"}
1
+ {"version":3,"file":"useSelection.d.ts","sourceRoot":"","sources":["../../src/hooks/useSelection.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAQ,YAAY,EAAe,MAAM,QAAQ,CAAC;AAMjE,UAAU,iBAAiB;IACzB,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,eAAe,EAAE,CAAC,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1F,wBAAwB,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IAC5D,YAAY,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;CAC/C;AAED,eAAO,MAAM,YAAY,GAAI,oHAQ1B,iBAAiB,SAgLnB,CAAC"}
@@ -1,5 +1,5 @@
1
1
  // hooks/useSelection.ts
2
- import { useEffect } from "react";
2
+ import { useEffect, useRef } from "react";
3
3
  import { Rect, FabricImage } from "fabric";
4
4
  import { Frame } from "../lib/fabric-frame";
5
5
  import { Arrow } from "../lib/fabric-arrow";
@@ -8,6 +8,15 @@ import { useWhiteboardStore } from "../store/whiteboard-store";
8
8
  export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewport, setSelectionBox, setSelectedCanvasObjects, isDrawingRef, }) => {
9
9
  const setSelectedObjectType = useWhiteboardStore((state) => state.setSelectedObjectType);
10
10
  const setActiveTool = useWhiteboardStore((state) => state.setActiveTool);
11
+ // ── KEY FIX 1: Store zoom/viewport in refs so the effect never re-registers ──
12
+ // This prevents listener teardown/re-attach during pan/zoom gestures
13
+ const zoomRef = useRef(canvasZoom);
14
+ const viewportRef = useRef(canvasViewport);
15
+ const activeToolRef = useRef(activeTool);
16
+ // Keep refs in sync with latest props on every render
17
+ useEffect(() => { zoomRef.current = canvasZoom; }, [canvasZoom]);
18
+ useEffect(() => { viewportRef.current = canvasViewport; }, [canvasViewport]);
19
+ useEffect(() => { activeToolRef.current = activeTool; }, [activeTool]);
11
20
  useEffect(() => {
12
21
  const canvas = fabricCanvas.current;
13
22
  if (!canvas)
@@ -17,9 +26,11 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
17
26
  let selRect = null;
18
27
  let rafId = null;
19
28
  const onDown = (e) => {
20
- if (activeTool !== "select" || e.target)
29
+ // ── KEY FIX 2: Use ref instead of closure-captured activeTool ──
30
+ if (activeToolRef.current !== "select" || e.target)
21
31
  return;
22
32
  isSelecting = true;
33
+ // getScenePoint returns world coordinates (zoom + pan already factored in by Fabric)
23
34
  const p = canvas.getScenePoint(e.e);
24
35
  selStart = { x: p.x, y: p.y };
25
36
  selRect = new Rect({
@@ -27,17 +38,18 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
27
38
  top: p.y,
28
39
  width: 0,
29
40
  height: 0,
30
- fill: "transparent",
31
- stroke: "transparent",
32
- strokeWidth: 0,
41
+ fill: "rgba(2, 154, 255, 0.08)",
42
+ stroke: "#029AFF",
43
+ strokeWidth: 1 / zoomRef.current, // stays 1px visually at any zoom
33
44
  selectable: false,
34
45
  evented: false,
35
- visible: false,
46
+ // ── KEY FIX 3: Make it visible so user sees the selection rect ──
47
+ // was `visible: false` before — invisible but still blocking
48
+ excludeFromExport: true,
36
49
  });
37
50
  canvas.add(selRect);
38
51
  canvas.renderAll();
39
52
  };
40
- // hooks/useSelection.ts - UPDATED
41
53
  const onMove = (e) => {
42
54
  if (!isSelecting || !selRect)
43
55
  return;
@@ -47,34 +59,46 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
47
59
  const p = canvas.getScenePoint(e.e);
48
60
  const w = p.x - selStart.x;
49
61
  const h = p.y - selStart.y;
50
- const x1 = Math.min(selStart.x, p.x);
51
- const y1 = Math.min(selStart.y, p.y);
52
- const x2 = Math.max(selStart.x, p.x);
53
- const y2 = Math.max(selStart.y, p.y);
54
62
  selRect.set({
55
- left: x1,
56
- top: y1,
57
- width: x2 - x1,
58
- height: y2 - y1,
63
+ left: w < 0 ? p.x : selStart.x,
64
+ top: h < 0 ? p.y : selStart.y,
65
+ width: Math.abs(w),
66
+ height: Math.abs(h),
67
+ strokeWidth: 1 / zoomRef.current, // keep stroke sharp at any zoom
59
68
  });
60
69
  selRect.setCoords();
61
70
  canvas.renderAll();
62
- // PASS WORLD COORDINATES ONLY
63
- setSelectionBox({ x1, y1, x2, y2 });
71
+ // ── KEY FIX 4: Read from refs — always fresh, no stale closure ──
72
+ const zoom = zoomRef.current;
73
+ const vp = viewportRef.current;
74
+ // World → Screen conversion:
75
+ // screenX = worldX * zoom + panX (matches exactly how nodes are rendered)
76
+ setSelectionBox({
77
+ x1: Math.min(selStart.x, p.x) * zoom + vp.x,
78
+ y1: Math.min(selStart.y, p.y) * zoom + vp.y,
79
+ x2: Math.max(selStart.x, p.x) * zoom + vp.x,
80
+ y2: Math.max(selStart.y, p.y) * zoom + vp.y,
81
+ });
64
82
  });
65
83
  };
66
84
  const onUp = () => {
67
- if (!isSelecting || !selRect)
85
+ if (!isSelecting)
68
86
  return;
69
87
  if (rafId !== null) {
70
88
  cancelAnimationFrame(rafId);
71
89
  rafId = null;
72
90
  }
73
- canvas.remove(selRect);
74
- canvas.renderAll();
91
+ if (selRect) {
92
+ canvas.remove(selRect);
93
+ canvas.renderAll();
94
+ selRect = null;
95
+ }
75
96
  isSelecting = false;
76
- selRect = null;
77
- setTimeout(() => setSelectionBox(null), 100);
97
+ // ── KEY FIX 5: Delay clear long enough for overlay useEffect to fire ──
98
+ // onDeselected (selection:cleared) was calling setSelectionBox(null) immediately,
99
+ // racing with the overlay's useEffect. 150ms ensures the React render cycle
100
+ // processes the final box position before it's cleared.
101
+ setTimeout(() => setSelectionBox(null), 150);
78
102
  };
79
103
  const onSelected = () => {
80
104
  const sel = canvas.getActiveObject();
@@ -105,9 +129,11 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
105
129
  };
106
130
  const onDeselected = () => {
107
131
  setSelectedObjectType(null);
108
- setSelectionBox(null);
132
+ // ── KEY FIX 6: Do NOT clear selectionBox here ──
133
+ // This was racing with onUp's setTimeout and clearing before overlay processed it.
134
+ // onUp owns the selectionBox lifecycle. Only clear canvas objects here.
109
135
  setSelectedCanvasObjects([]);
110
- if (!isDrawingRef.current && activeTool !== "select" && activeTool !== "pan") {
136
+ if (!isDrawingRef.current && activeToolRef.current !== "select" && activeToolRef.current !== "pan") {
111
137
  setActiveTool("select");
112
138
  }
113
139
  };
@@ -129,15 +155,7 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
129
155
  if (selRect)
130
156
  canvas.remove(selRect);
131
157
  };
132
- }, [
133
- activeTool,
134
- canvasZoom,
135
- canvasViewport,
136
- fabricCanvas,
137
- setSelectionBox,
138
- setSelectedCanvasObjects,
139
- setSelectedObjectType,
140
- setActiveTool,
141
- isDrawingRef
142
- ]);
158
+ // ── KEY FIX 7: Remove canvasZoom/canvasViewport/activeTool from deps ──
159
+ // They are now read via refs — effect only registers once per canvas mount.
160
+ }, [fabricCanvas, setSelectionBox, setSelectedCanvasObjects, setSelectedObjectType, setActiveTool, isDrawingRef]);
143
161
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "1.27.0",
3
+ "version": "2.0.0",
4
4
  "description": "A feature-rich whiteboard component built with Fabric.js and React",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -43,7 +43,7 @@
43
43
  "devDependencies": {
44
44
  "@tailwindcss/cli": "^4.2.1",
45
45
  "@types/fabric": "^5.3.11",
46
- "@types/node": "^20.0.0",
46
+ "@types/node": "^20.19.37",
47
47
  "@types/react": "^19.0.0",
48
48
  "@types/react-dom": "^19.0.0",
49
49
  "typescript": "^5.0.0"