@mhamz.01/easyflow-whiteboard 2.119.0 → 2.121.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;AAS9C,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;IAC9C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;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,2CAglBzB"}
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;AAU9C,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;IAC9C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAkBD,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,2CAimBzB"}
@@ -1,8 +1,12 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useEffect, useRef } from "react";
3
+ import { useState, useEffect, useRef, useCallback } from "react";
4
4
  import TaskNode from "./custom-node";
5
5
  import DocumentNode from "./document-node";
6
+ import { memo } from "react";
7
+ // Wrap TaskNode and DocumentNode
8
+ const MemoTaskNode = memo(TaskNode);
9
+ const MemoDocumentNode = memo(DocumentNode);
6
10
  // ─── Component ────────────────────────────────────────────────────────────────
7
11
  export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, onDocumentsUpdate, canvasZoom = 1, canvasViewport = { x: 0, y: 0 }, selectionBox = null, selectedCanvasObjects = [], fabricCanvas, }) {
8
12
  const [localTasks, setLocalTasks] = useState(tasks);
@@ -14,6 +18,13 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
14
18
  tasks: [],
15
19
  documents: [],
16
20
  });
21
+ // In render, replace TaskNode/DocumentNode with memoized versions
22
+ {
23
+ localTasks.map((task) => renderItem(task.id, task.x, task.y, _jsx(MemoTaskNode, { ...task, isSelected: selectedIds.has(task.id), onSelect: handleSelect, onDragStart: handleDragStart, onStatusChange: handleStatusChange, zoom: 1 })));
24
+ }
25
+ {
26
+ localDocuments.map((doc) => renderItem(doc.id, doc.x, doc.y, _jsx(MemoDocumentNode, { ...doc, isSelected: selectedIds.has(doc.id), onSelect: handleSelect, onDragStart: handleDragStart })));
27
+ }
17
28
  const dragStateRef = useRef({
18
29
  isDragging: false,
19
30
  itemIds: [],
@@ -25,7 +36,16 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
25
36
  const rafIdRef = useRef(null);
26
37
  const overlayRef = useRef(null);
27
38
  const selectedIdsRef = useRef(selectedIds);
39
+ // Add these refs
40
+ const localTasksRef = useRef(localTasks);
41
+ const localDocsRef = useRef(localDocuments);
42
+ const onTasksUpdateRef = useRef(onTasksUpdate);
43
+ const onDocsUpdateRef = useRef(onDocumentsUpdate);
28
44
  selectedIdsRef.current = selectedIds;
45
+ localTasksRef.current = localTasks;
46
+ localDocsRef.current = localDocuments;
47
+ onTasksUpdateRef.current = onTasksUpdate;
48
+ onDocsUpdateRef.current = onDocumentsUpdate;
29
49
  // ── Sync props → local state ────────────────────────────────────────────────
30
50
  useEffect(() => { setLocalTasks(tasks); }, [tasks]);
31
51
  useEffect(() => { setLocalDocuments(documents); }, [documents]);
@@ -106,7 +126,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
106
126
  return () => {
107
127
  overlayEl.removeEventListener("wheel", handleGlobalWheel);
108
128
  };
109
- }, [fabricCanvas, canvasZoom, canvasReady]); // Re-bind when zoom changes to keep closure fresh
129
+ }, [fabricCanvas, canvasReady]); // Re-bind when zoom changes to keep closure fresh
110
130
  // ── Fabric → Overlay Sync (Fixes Dragging from Fabric area) ──────────────────
111
131
  useEffect(() => {
112
132
  const canvas = fabricCanvas?.current;
@@ -165,7 +185,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
165
185
  // Having selectedIds here caused the effect to re-register on every selection
166
186
  // change, creating a new closure each time. The second drag captured a stale
167
187
  // or empty selectedIds from the closure at re-registration time.
168
- }, [canvasZoom, fabricCanvas, canvasReady]);
188
+ }, [fabricCanvas, canvasReady]);
169
189
  // ── Helpers ─────────────────────────────────────────────────────────────────
170
190
  const getItemPosition = (id) => {
171
191
  const task = localTasks.find((t) => t.id === id);
@@ -219,72 +239,61 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
219
239
  return e.touches[0];
220
240
  return e;
221
241
  };
222
- const handleDragStart = (itemId, e) => {
223
- // 1. Safety check for the Fabric instance
242
+ const handleDragStart = useCallback((itemId, e) => {
224
243
  const canvas = fabricCanvas?.current;
225
244
  if (!canvas)
226
245
  return;
227
- // 2. Normalize the event (Touch vs Mouse)
228
246
  if (e.cancelable)
229
247
  e.preventDefault();
230
248
  const pointer = getPointerEvent(e);
231
- // 3. Determine which items are being dragged
232
- // selection update DOES NOT trigger before drag snapshot
233
- let itemsToDrag;
234
- if (selectedIds.has(itemId)) {
235
- itemsToDrag = Array.from(selectedIds);
236
- }
237
- else {
238
- itemsToDrag = [itemId];
239
- }
240
- // 4. Capture current World Transform (Zoom & Pan)
241
- // We read directly from the canvas to ensure zero-frame lag
249
+ // read from ref always fresh
250
+ const currentSelectedIds = selectedIdsRef.current;
251
+ let itemsToDrag = currentSelectedIds.has(itemId)
252
+ ? Array.from(currentSelectedIds)
253
+ : [itemId];
242
254
  const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
243
255
  const liveZoom = vpt[0];
244
256
  const liveVpX = vpt[4];
245
257
  const liveVpY = vpt[5];
246
- // 5. Convert the Click Position from Screen Pixels to World Units
247
258
  const clickWorldX = (pointer.clientX - liveVpX) / liveZoom;
248
259
  const clickWorldY = (pointer.clientY - liveVpY) / liveZoom;
249
- // 6. Get the clicked item's current World Position
250
- const clickedPos = getItemPosition(itemId);
260
+ // read positions from refs
261
+ const getPos = (id) => {
262
+ const t = localTasksRef.current.find((t) => t.id === id);
263
+ if (t)
264
+ return { x: t.x, y: t.y };
265
+ const d = localDocsRef.current.find((d) => d.id === id);
266
+ if (d)
267
+ return { x: d.x, y: d.y };
268
+ };
269
+ const clickedPos = getPos(itemId);
251
270
  if (!clickedPos)
252
271
  return;
253
- // 7. Calculate the Offset in WORLD UNITS
254
- // This is the distance from the mouse to the node's top-left in the infinite grid.
255
- // This value remains constant even if you zoom during the drag.
256
- const worldOffsetX = clickWorldX - clickedPos.x;
257
- const worldOffsetY = clickWorldY - clickedPos.y;
258
- // 8. Snapshot starting positions for all selected HTML nodes
259
272
  const startPositions = new Map();
260
273
  itemsToDrag.forEach((id) => {
261
- const pos = getItemPosition(id);
274
+ const pos = getPos(id);
262
275
  if (pos)
263
276
  startPositions.set(id, pos);
264
277
  });
265
- // 9. Snapshot starting positions for all selected Fabric objects
266
278
  const canvasObjectsStartPos = new Map();
267
279
  selectedCanvasObjects.forEach((obj) => {
268
280
  canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
269
281
  });
270
- // 10. Commit to the ref for the requestAnimationFrame loop
271
282
  dragStateRef.current = {
272
283
  isDragging: true,
273
284
  itemIds: itemsToDrag,
274
285
  startPositions,
275
286
  canvasObjectsStartPos,
276
- offsetX: clickWorldX, // Now stored as World Units
277
- offsetY: clickWorldY, // Now stored as World Units
287
+ offsetX: clickWorldX,
288
+ offsetY: clickWorldY,
278
289
  };
279
- if (!selectedIds.has(itemId) && dragStateRef.current.itemIds.length === 0) {
290
+ if (!currentSelectedIds.has(itemId))
280
291
  setSelectedIds(new Set([itemId]));
281
- }
282
- // 11. Trigger UI states
283
292
  setDragging({ itemIds: itemsToDrag });
284
293
  document.body.style.cursor = "grabbing";
285
294
  document.body.style.userSelect = "none";
286
295
  document.body.style.touchAction = "none";
287
- };
296
+ }, []); // now truly stable
288
297
  // ── Drag move (HTML Node side) ───────────────────────────────────────────────
289
298
  useEffect(() => {
290
299
  if (!dragging)
@@ -363,8 +372,8 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
363
372
  document.body.style.cursor = "";
364
373
  document.body.style.userSelect = "";
365
374
  document.body.style.touchAction = "";
366
- onTasksUpdate?.(localTasks);
367
- onDocumentsUpdate?.(localDocuments);
375
+ onTasksUpdateRef.current?.(localTasksRef.current); // ref instead of closure
376
+ onDocsUpdateRef.current?.(localDocsRef.current);
368
377
  };
369
378
  window.addEventListener("mousemove", handleMove, { passive: false });
370
379
  window.addEventListener("mouseup", handleEnd);
@@ -378,9 +387,9 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
378
387
  window.removeEventListener("touchend", handleEnd);
379
388
  window.removeEventListener("touchcancel", handleEnd);
380
389
  };
381
- }, [dragging, localTasks, localDocuments, fabricCanvas]);
390
+ }, [dragging, fabricCanvas]);
382
391
  // ── Selection, Status, Keyboard Logic ────────────────────────────────────────
383
- const handleSelect = (id, e) => {
392
+ const handleSelect = useCallback((id, e) => {
384
393
  if (e?.shiftKey || e?.ctrlKey || e?.metaKey) {
385
394
  setSelectedIds((prev) => {
386
395
  const next = new Set(prev);
@@ -391,41 +400,38 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
391
400
  else {
392
401
  setSelectedIds(new Set([id]));
393
402
  }
394
- };
395
- const handleStatusChange = (taskId, newStatus) => {
396
- const updated = localTasks.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t));
397
- setLocalTasks(updated);
398
- onTasksUpdate?.(updated);
399
- };
403
+ }, []);
404
+ const handleStatusChange = useCallback((taskId, newStatus) => {
405
+ setLocalTasks((prev) => {
406
+ const updated = prev.map((t) => t.id === taskId ? { ...t, status: newStatus } : t);
407
+ onTasksUpdateRef.current?.(updated);
408
+ return updated;
409
+ });
410
+ }, []);
400
411
  useEffect(() => {
401
412
  const handleKeyDown = (e) => {
402
- // Don't trigger if typing in input
403
413
  if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
404
414
  return;
405
- // Select All
406
415
  if ((e.ctrlKey || e.metaKey) && e.key === "a") {
407
416
  e.preventDefault();
408
- setSelectedIds(new Set([...localTasks.map((t) => t.id), ...localDocuments.map((d) => d.id)]));
417
+ setSelectedIds(new Set([...localTasksRef.current.map((t) => t.id), ...localDocsRef.current.map((d) => d.id)]));
409
418
  }
410
- // Clear selection
411
- if (e.key === "Escape") {
419
+ if (e.key === "Escape")
412
420
  setSelectedIds(new Set());
413
- }
414
- // ← ADD THIS: Delete selected nodes
415
- if ((e.key === "Delete" || e.key === "Backspace") && selectedIds.size > 0) {
421
+ if ((e.key === "Delete" || e.key === "Backspace") && selectedIdsRef.current.size > 0) {
416
422
  e.preventDefault();
417
- const updatedTasks = localTasks.filter((t) => !selectedIds.has(t.id));
418
- const updatedDocs = localDocuments.filter((d) => !selectedIds.has(d.id));
423
+ const updatedTasks = localTasksRef.current.filter((t) => !selectedIdsRef.current.has(t.id));
424
+ const updatedDocs = localDocsRef.current.filter((d) => !selectedIdsRef.current.has(d.id));
419
425
  setLocalTasks(updatedTasks);
420
426
  setLocalDocuments(updatedDocs);
421
427
  setSelectedIds(new Set());
422
- onTasksUpdate?.(updatedTasks);
423
- onDocumentsUpdate?.(updatedDocs);
428
+ onTasksUpdateRef.current?.(updatedTasks);
429
+ onDocsUpdateRef.current?.(updatedDocs);
424
430
  }
425
431
  };
426
432
  window.addEventListener("keydown", handleKeyDown);
427
433
  return () => window.removeEventListener("keydown", handleKeyDown);
428
- }, [localTasks, localDocuments, selectedIds, onTasksUpdate, onDocumentsUpdate]);
434
+ }, []); // stable everything via refs
429
435
  // ── Render helper ────────────────────────────────────────────────────────────
430
436
  const renderItem = (id, x, y, children) => {
431
437
  const screenX = x * canvasZoom;
@@ -454,5 +460,5 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
454
460
  }, children: _jsxs("div", { className: "absolute top-0 left-0 pointer-events-none", style: {
455
461
  transform: `translate(${canvasViewport.x}px, ${canvasViewport.y}px)`,
456
462
  transformOrigin: "top left",
457
- }, children: [localTasks.map((task) => renderItem(task.id, task.x, task.y, _jsx(TaskNode, { ...task, isSelected: selectedIds.has(task.id), onSelect: handleSelect, onDragStart: handleDragStart, onStatusChange: handleStatusChange, zoom: 1 }))), localDocuments.map((doc) => renderItem(doc.id, doc.x, doc.y, _jsx(DocumentNode, { ...doc, isSelected: selectedIds.has(doc.id), onSelect: handleSelect, onDragStart: handleDragStart })))] }) }));
463
+ }, children: [localTasks.map((task) => renderItem(task.id, task.x, task.y, _jsx(MemoTaskNode, { ...task, isSelected: selectedIds.has(task.id), onSelect: handleSelect, onDragStart: handleDragStart, onStatusChange: handleStatusChange, zoom: 1 }))), localDocuments.map((doc) => renderItem(doc.id, doc.x, doc.y, _jsx(MemoDocumentNode, { ...doc, isSelected: selectedIds.has(doc.id), onSelect: handleSelect, onDragStart: handleDragStart })))] }) }));
458
464
  }
@@ -1 +1 @@
1
- {"version":3,"file":"whiteboard-test.d.ts","sourceRoot":"","sources":["../../../src/components/whiteboard/whiteboard-test.tsx"],"names":[],"mappings":"AAQC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAkB3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGtD,YAAY,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACvE,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAIxE,UAAU,IAAI;IACZ,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,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID,UAAU,qBAAqB;IAC7B,WAAW,CAAC,EAAE;QACZ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;QACf,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;KACxB,CAAC;IACF,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAClC,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,GAAG,EAAE,CAAC;QACb,SAAS,EAAE,GAAG,EAAE,CAAC;KAClB,KAAK,IAAI,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAID,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,WAAW,EACX,MAAM,EACN,cAAc,EACd,cAAmB,EACnB,kBAAuB,EACvB,aAAqB,GACtB,EAAE,qBAAqB,2CA6PvB"}
1
+ {"version":3,"file":"whiteboard-test.d.ts","sourceRoot":"","sources":["../../../src/components/whiteboard/whiteboard-test.tsx"],"names":[],"mappings":"AAQC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAkB3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGtD,YAAY,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACvE,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAIxE,UAAU,IAAI;IACZ,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,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID,UAAU,qBAAqB;IAC7B,WAAW,CAAC,EAAE;QACZ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;QACf,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;KACxB,CAAC;IACF,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAClC,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,GAAG,EAAE,CAAC;QACb,SAAS,EAAE,GAAG,EAAE,CAAC;KAClB,KAAK,IAAI,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAID,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,WAAW,EACX,MAAM,EACN,cAAc,EACd,cAAmB,EACnB,kBAAuB,EACvB,aAAqB,GACtB,EAAE,qBAAqB,2CA4PvB"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useRef, useState } from "react";
3
+ import { useCallback, useMemo, useRef, useState } from "react";
4
4
  import { classRegistry } from "fabric";
5
5
  import { useWhiteboardStore } from "../../store/whiteboard-store";
6
6
  import WhiteboardToolbar from "../toolbar/whiteboard-toolbar";
@@ -161,12 +161,8 @@ export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs,
161
161
  drawingHandlers,
162
162
  eraserHandlers,
163
163
  });
164
- // Zoom controls
165
- const handleZoomIn = () => handleZoom(canvasZoom + ZOOM_STEP);
166
- const handleZoomOut = () => handleZoom(canvasZoom - ZOOM_STEP);
167
- const handleResetZoom = () => handleZoom(1);
168
- // Dropdown handlers
169
- const handleAddTaskFromDropdown = (taskTemplate) => {
164
+ // 1. Memoize static handlers
165
+ const handleAddTaskFromDropdown = useCallback((taskTemplate) => {
170
166
  const canvas = fabricCanvasRef.current;
171
167
  if (!canvas)
172
168
  return;
@@ -176,12 +172,9 @@ export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs,
176
172
  const liveZoom = vpt[0];
177
173
  const cx = (canvas.getWidth() / 2 - vpt[4]) / liveZoom;
178
174
  const cy = (canvas.getHeight() / 2 - vpt[5]) / liveZoom;
179
- setTasks((prev) => [
180
- ...prev,
181
- { ...taskTemplate, id: `${taskTemplate.id}-${Date.now()}`, x: cx - 150, y: cy - 60 },
182
- ]);
183
- };
184
- const handleAddDocumentFromDropdown = (docTemplate) => {
175
+ setTasks((prev) => [...prev, { ...taskTemplate, id: `${taskTemplate.id}-${Date.now()}`, x: cx - 150, y: cy - 60 }]);
176
+ }, []);
177
+ const handleAddDocumentFromDropdown = useCallback((docTemplate) => {
185
178
  const canvas = fabricCanvasRef.current;
186
179
  if (!canvas)
187
180
  return;
@@ -191,16 +184,18 @@ export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs,
191
184
  const liveZoom = vpt[0];
192
185
  const cx = (canvas.getWidth() / 2 - vpt[4]) / liveZoom;
193
186
  const cy = (canvas.getHeight() / 2 - vpt[5]) / liveZoom;
194
- setDocuments((prev) => [
195
- ...prev,
196
- { ...docTemplate, id: `${docTemplate.id}-${Date.now()}`, x: cx - 160, y: cy - 80 },
197
- ]);
198
- };
199
- return (_jsx("div", { className: "easyflow-whiteboard w-screen h-screen", children: _jsxs("div", { ref: containerRef, className: "relative w-full h-full overflow-hidden bg-[#0b0b0b]", style: { touchAction: "none", overscrollBehavior: "none" }, children: [_jsx("div", { className: "absolute inset-0 pointer-events-none", style: {
187
+ setDocuments((prev) => [...prev, { ...docTemplate, id: `${docTemplate.id}-${Date.now()}`, x: cx - 160, y: cy - 80 }]);
188
+ }, []);
189
+ const handleZoomIn = useCallback(() => handleZoom(canvasZoom + ZOOM_STEP), [canvasZoom, handleZoom]);
190
+ const handleZoomOut = useCallback(() => handleZoom(canvasZoom - ZOOM_STEP), [canvasZoom, handleZoom]);
191
+ const handleResetZoom = useCallback(() => handleZoom(1), [handleZoom]);
192
+ const MemoOverlay = useMemo(() => (_jsx(CanvasOverlayLayer, { tasks: tasks, documents: documents, onTasksUpdate: setTasks, onDocumentsUpdate: setDocuments, canvasZoom: canvasZoom, canvasViewport: canvasViewport, selectionBox: selectionBox, selectedCanvasObjects: selectedCanvasObjects, fabricCanvas: fabricCanvasRef, canvasReady: canvasReady })), [tasks, documents, canvasZoom, canvasViewport, selectionBox, selectedCanvasObjects, canvasReady]);
193
+ const MemoToolbar = useMemo(() => (canvasReady
194
+ ? _jsx(WhiteboardToolbar, { fabricCanvas: fabricCanvasRef, isRestoringRef: isRestoringRef, onAddTask: handleAddTaskFromDropdown, onAddDocument: handleAddDocumentFromDropdown, availableDocuments: availableDocuments, availableTasks: availableTasks, isLoadingData: isLoadingData })
195
+ : _jsx(ToolbarSkeleton, {})), [canvasReady, availableDocuments, availableTasks, isLoadingData, handleAddTaskFromDropdown, handleAddDocumentFromDropdown]);
196
+ return (_jsx("div", { className: "easyflow-whiteboard w-full h-full", children: _jsxs("div", { ref: containerRef, className: "relative w-full h-full overflow-hidden bg-[#0b0b0b]", style: { touchAction: "none", overscrollBehavior: "none" }, children: [_jsx("div", { className: "absolute inset-0 pointer-events-none", style: {
200
197
  backgroundImage: `radial-gradient(circle, rgba(255,255,255,0.2) 1.2px, transparent 1.2px)`,
201
198
  backgroundSize: "40px 40px",
202
199
  zIndex: 0,
203
- } }), _jsx("canvas", { ref: canvasRef, className: "absolute inset-0", style: { zIndex: 1 } }), _jsx(CanvasOverlayLayer, { tasks: tasks, documents: documents, onTasksUpdate: setTasks, onDocumentsUpdate: setDocuments, canvasZoom: canvasZoom, canvasViewport: canvasViewport, selectionBox: selectionBox, selectedCanvasObjects: selectedCanvasObjects, fabricCanvas: fabricCanvasRef, canvasReady: canvasReady }), _jsxs("div", { className: "absolute inset-0 pointer-events-none", style: { zIndex: 100 }, children: [_jsx("div", { className: "pointer-events-auto", children: canvasReady ?
204
- _jsx(WhiteboardToolbar, { fabricCanvas: fabricCanvasRef, isRestoringRef: isRestoringRef, onAddTask: handleAddTaskFromDropdown, onAddDocument: handleAddDocumentFromDropdown, availableDocuments: availableDocuments, availableTasks: availableTasks, isLoadingData: isLoadingData })
205
- : _jsx(ToolbarSkeleton, {}) }), _jsx("div", { className: "pointer-events-auto", children: _jsx(ToolOptionsPanel, { fabricCanvas: fabricCanvasRef }) }), _jsx("div", { className: "pointer-events-auto", children: _jsx(ZoomControls, { zoom: canvasZoom, onZoomIn: handleZoomIn, onZoomOut: handleZoomOut, onResetZoom: handleResetZoom }) })] })] }) }));
200
+ } }), _jsx("canvas", { ref: canvasRef, className: "absolute inset-0", style: { zIndex: 1 } }), MemoOverlay, _jsxs("div", { className: "absolute inset-0 pointer-events-none", style: { zIndex: 100 }, children: [_jsx("div", { className: "pointer-events-auto", children: MemoToolbar }), _jsx("div", { className: "pointer-events-auto", children: _jsx(ToolOptionsPanel, { fabricCanvas: fabricCanvasRef }) }), _jsx("div", { className: "pointer-events-auto", children: _jsx(ZoomControls, { zoom: canvasZoom, onZoomIn: handleZoomIn, onZoomOut: handleZoomOut, onResetZoom: handleResetZoom }) })] })] }) }));
206
201
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useEraser.d.ts","sourceRoot":"","sources":["../../src/hooks/useEraser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,UAAU,cAAc;IACtB,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,GAAG,CAAC;IACjB,cAAc,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,gBAAgB,EAAE,SAAS,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/C,eAAe,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACpC,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC7C,mBAAmB,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,kBAAkB,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,eAAO,MAAM,SAAS,GAAI,oKAWvB,cAAc;;;;gCAsBoB;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE;;CA0H5D,CAAC"}
1
+ {"version":3,"file":"useEraser.d.ts","sourceRoot":"","sources":["../../src/hooks/useEraser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,UAAU,cAAc;IACtB,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,GAAG,CAAC;IACjB,cAAc,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,gBAAgB,EAAE,SAAS,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/C,eAAe,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACpC,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC7C,mBAAmB,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,kBAAkB,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,eAAO,MAAM,SAAS,GAAI,oKAWvB,cAAc;;;;gCAsBoB;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE;;CA0I5D,CAAC"}
@@ -24,7 +24,7 @@ export const useEraser = ({ fabricCanvas, activeTool, toolOptions, eraserTraceRe
24
24
  const canvas = fabricCanvas.current;
25
25
  if (!canvas || activeTool !== "eraser")
26
26
  return;
27
- // Update or create trace circle
27
+ // Create or update trace circle
28
28
  if (!eraserTraceRef.current) {
29
29
  eraserTraceRef.current = new Circle({
30
30
  radius: toolOptions.eraser.size / 1.5,
@@ -45,54 +45,66 @@ export const useEraser = ({ fabricCanvas, activeTool, toolOptions, eraserTraceRe
45
45
  eraserTraceRef.current.set({ left: pointer.x, top: pointer.y });
46
46
  eraserTraceRef.current.setCoords();
47
47
  canvas.bringObjectToFront(eraserTraceRef.current);
48
+ // Smooth continuous trail
48
49
  if (eraserActiveRef.current) {
49
50
  suppressHistoryRef.current = true;
50
51
  if (eraserPathPointsRef.current.length === 0) {
51
52
  eraserPathPointsRef.current = [`M ${pointer.x} ${pointer.y}`];
52
53
  const path = new fabric.Path(eraserPathPointsRef.current.join(" "), {
53
- stroke: "rgba(53, 53, 53, 0.7)",
54
+ stroke: "rgba(220, 220, 220, 0.7)",
54
55
  strokeWidth: toolOptions.eraser.size,
55
56
  fill: null,
56
57
  selectable: false,
57
58
  evented: false,
58
59
  strokeLineCap: "round",
59
60
  strokeLineJoin: "round",
61
+ opacity: 1,
60
62
  excludeFromExport: true,
61
63
  });
62
64
  canvas.add(path);
63
65
  eraserPathRef.current = path;
64
66
  }
65
67
  else {
66
- // ── FIX 1: Mutate path in place — no remove/add, no events fired ──
67
68
  eraserPathPointsRef.current.push(`L ${pointer.x} ${pointer.y}`);
68
69
  const pathString = eraserPathPointsRef.current.join(" ");
69
70
  if (eraserPathRef.current) {
70
- const parsed = new fabric.Path(pathString);
71
- eraserPathRef.current.set({ path: parsed.path });
72
- eraserPathRef.current.setCoords();
71
+ canvas.remove(eraserPathRef.current);
73
72
  }
73
+ const path = new fabric.Path(pathString, {
74
+ stroke: "rgba(53, 53, 53, 0.7)",
75
+ strokeWidth: toolOptions.eraser.size,
76
+ fill: null,
77
+ selectable: false,
78
+ evented: false,
79
+ strokeLineCap: "round",
80
+ strokeLineJoin: "round",
81
+ opacity: 1,
82
+ excludeFromExport: true,
83
+ });
84
+ canvas.add(path);
85
+ eraserPathRef.current = path;
74
86
  }
75
87
  canvas.bringObjectToFront(eraserTraceRef.current);
76
88
  }
77
- // ── FIX 2: Intersection check only while actively erasing ──
78
- if (eraserActiveRef.current) {
79
- const previousTargets = new Set(eraserTargetsRef.current);
80
- eraserTargetsRef.current.clear();
81
- canvas.forEachObject((obj) => {
82
- if (obj === eraserTraceRef.current || obj === eraserPathRef.current)
83
- return;
84
- if (obj.intersectsWithObject(eraserTraceRef.current)) {
85
- eraserTargetsRef.current.add(obj);
86
- if (obj.opacity !== 0.3)
87
- obj.set({ opacity: 0.3 });
88
- }
89
- else if (previousTargets.has(obj)) {
90
- obj.set({ opacity: 1 });
89
+ // Find objects under eraser
90
+ const previousTargets = new Set(eraserTargetsRef.current);
91
+ eraserTargetsRef.current.clear();
92
+ canvas.forEachObject((obj) => {
93
+ if (obj === eraserTraceRef.current)
94
+ return;
95
+ if (obj === eraserPathRef.current)
96
+ return;
97
+ if (obj.intersectsWithObject(eraserTraceRef.current)) {
98
+ eraserTargetsRef.current.add(obj);
99
+ if (eraserActiveRef.current && obj.opacity !== 0.3) {
100
+ obj.set({ opacity: 0.3 });
91
101
  }
92
- });
93
- }
94
- // ── FIX 3: requestRenderAll instead of renderAll ──
95
- canvas.requestRenderAll();
102
+ }
103
+ else if (previousTargets.has(obj)) {
104
+ obj.set({ opacity: 1 });
105
+ }
106
+ });
107
+ canvas.renderAll();
96
108
  };
97
109
  const handleEraserUp = () => {
98
110
  const canvas = fabricCanvas.current;
package/dist/styles.css CHANGED
@@ -570,9 +570,6 @@
570
570
  .w-px {
571
571
  width: 1px;
572
572
  }
573
- .w-screen {
574
- width: 100vw;
575
- }
576
573
  .max-w-\[120px\] {
577
574
  max-width: 120px;
578
575
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.119.0",
3
+ "version": "2.121.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",