@mhamz.01/easyflow-whiteboard 2.70.0 → 2.71.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,2CAulBzB"}
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,2CA6jBzB"}
@@ -1,6 +1,6 @@
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
6
  // ─── Component ────────────────────────────────────────────────────────────────
@@ -22,15 +22,20 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
22
22
  offsetX: 0,
23
23
  offsetY: 0,
24
24
  });
25
- const finalPositionsRef = useRef(null);
25
+ // 2. High-Frequency Refs (Bypasses React Render Cycle)
26
+ const taskRefs = useRef(new Map());
27
+ const docRefs = useRef(new Map());
28
+ const localTasksRef = useRef(tasks);
29
+ const localDocsRef = useRef(documents);
30
+ const selectedIdsRef = useRef(new Set());
26
31
  const rafIdRef = useRef(null);
27
32
  const overlayRef = useRef(null);
28
- const localTasksRef = useRef(localTasks);
29
- const localDocumentsRef = useRef(localDocuments);
30
- const selectedIdsRef = useRef(selectedIds);
33
+ // const selectedIdsRef = useRef<Set<string>>(selectedIds);
31
34
  selectedIdsRef.current = selectedIds;
35
+ // Sync Refs immediately
32
36
  localTasksRef.current = localTasks;
33
- localDocumentsRef.current = localDocuments;
37
+ localDocsRef.current = localDocuments;
38
+ selectedIdsRef.current = selectedIds;
34
39
  // ── Sync props → local state ────────────────────────────────────────────────
35
40
  useEffect(() => { setLocalTasks(tasks); }, [tasks]);
36
41
  useEffect(() => { setLocalDocuments(documents); }, [documents]);
@@ -113,6 +118,17 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
113
118
  };
114
119
  }, [fabricCanvas, canvasZoom, canvasReady]); // Re-bind when zoom changes to keep closure fresh
115
120
  // ── Fabric → Overlay Sync (Fixes Dragging from Fabric area) ──────────────────
121
+ const updateNodeStyles = useCallback((id, x, y, zoom, vp) => {
122
+ const el = taskRefs.current.get(id) || docRefs.current.get(id);
123
+ if (!el)
124
+ return;
125
+ // Calculate final screen position
126
+ const screenX = x * zoom + vp.x;
127
+ const screenY = y * zoom + vp.y;
128
+ // Update via CSS Variables or Direct Transform
129
+ // This is 10x faster than a React State update
130
+ el.style.transform = `translate3d(${screenX}px, ${screenY}px, 0) scale(${zoom})`;
131
+ }, []);
116
132
  useEffect(() => {
117
133
  const canvas = fabricCanvas?.current;
118
134
  if (!canvas)
@@ -236,8 +252,8 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
236
252
  // 3. Determine which items are being dragged
237
253
  // selection update DOES NOT trigger before drag snapshot
238
254
  let itemsToDrag;
239
- if (selectedIdsRef.current.has(itemId)) {
240
- itemsToDrag = Array.from(selectedIdsRef.current);
255
+ if (selectedIds.has(itemId)) {
256
+ itemsToDrag = Array.from(selectedIds);
241
257
  }
242
258
  else {
243
259
  itemsToDrag = [itemId];
@@ -281,7 +297,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
281
297
  offsetX: worldOffsetX, // Now stored as World Units
282
298
  offsetY: worldOffsetY, // Now stored as World Units
283
299
  };
284
- if (!selectedIdsRef.current.has(itemId) && dragStateRef.current.itemIds.length === 0) {
300
+ if (!selectedIds.has(itemId) && dragStateRef.current.itemIds.length === 0) {
285
301
  setSelectedIds(new Set([itemId]));
286
302
  }
287
303
  // 11. Trigger UI states
@@ -295,88 +311,48 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
295
311
  if (!dragging)
296
312
  return;
297
313
  // Inside the useEffect that watches [dragging, localTasks, localDocuments, fabricCanvas]
298
- const handleMove = (e) => {
314
+ const handleMove = useCallback((e) => {
299
315
  if (!dragStateRef.current.isDragging)
300
316
  return;
301
- if (e.cancelable)
302
- e.preventDefault();
303
317
  const pointer = getPointerEvent(e);
304
- // 1. Throttle updates using requestAnimationFrame for 120Hz/144Hz screen support
305
- if (rafIdRef.current !== null)
306
- cancelAnimationFrame(rafIdRef.current);
307
- rafIdRef.current = requestAnimationFrame(() => {
308
- const { itemIds, startPositions, canvasObjectsStartPos, offsetX, offsetY } = dragStateRef.current;
318
+ requestAnimationFrame(() => {
319
+ const { itemIds, startPositions, offsetX, offsetY } = dragStateRef.current;
309
320
  const canvas = fabricCanvas?.current;
310
321
  if (!canvas)
311
322
  return;
312
- // 2. Read the "Source of Truth" transform from the canvas
313
323
  const vpt = canvas.viewportTransform;
314
- const liveZoom = vpt[0]; // Scale
315
- const liveVpX = vpt[4]; // Pan X
316
- const liveVpY = vpt[5]; // Pan Y
317
- // 3. Convert current Mouse Screen Position → World Position
324
+ const liveZoom = vpt[0];
325
+ const liveVpX = vpt[4];
326
+ const liveVpY = vpt[5];
318
327
  const currentWorldX = (pointer.clientX - liveVpX) / liveZoom;
319
328
  const currentWorldY = (pointer.clientY - liveVpY) / liveZoom;
320
- // 4. Calculate where the "Anchor" node should be in World Units
321
- // (Current Mouse World - Initial World Offset from Start)
322
- const newWorldX = currentWorldX - offsetX;
323
- const newWorldY = currentWorldY - offsetY;
324
- // 5. Calculate the Movement Delta in World Units
325
- // We compare where the first item started vs where it is now.
326
- const firstId = itemIds[0];
327
- const firstStart = startPositions.get(firstId);
328
- if (!firstStart)
329
- return;
330
- const deltaX = newWorldX - firstStart.x;
331
- const deltaY = newWorldY - firstStart.y;
332
- // 6. Update HTML Nodes (Batching these into one state update)
333
- setLocalTasks((prev) => {
334
- const next = prev.map((t) => itemIds.includes(t.id)
335
- ? { ...t, x: (startPositions.get(t.id)?.x ?? t.x) + deltaX,
336
- y: (startPositions.get(t.id)?.y ?? t.y) + deltaY }
337
- : t);
338
- localTasksRef.current = next; // ← write-through: ref always has latest
339
- return next;
340
- });
341
- setLocalDocuments((prev) => {
342
- const next = prev.map((d) => itemIds.includes(d.id)
343
- ? { ...d, x: (startPositions.get(d.id)?.x ?? d.x) + deltaX,
344
- y: (startPositions.get(d.id)?.y ?? d.y) + deltaY }
345
- : d);
346
- localDocumentsRef.current = next; // ← write-through
347
- return next;
329
+ const deltaX = (currentWorldX - offsetX) - (startPositions.get(itemIds[0])?.x ?? 0);
330
+ const deltaY = (currentWorldY - offsetY) - (startPositions.get(itemIds[0])?.y ?? 0);
331
+ // DOM UPDATE (Instant)
332
+ itemIds.forEach(id => {
333
+ const start = startPositions.get(id);
334
+ if (start) {
335
+ updateNodeStyles(id, start.x + deltaX, start.y + deltaY, liveZoom, { x: liveVpX, y: liveVpY });
336
+ }
348
337
  });
349
- // 7. Sync Fabric Objects (Imperative update for performance)
350
- canvasObjectsStartPos.forEach((startPos, obj) => {
351
- obj.set({
352
- left: startPos.left + deltaX,
353
- top: startPos.top + deltaY,
354
- });
355
- obj.setCoords(); // Required for selection/intersection accuracy
338
+ // FABRIC UPDATE (Batched)
339
+ dragStateRef.current.canvasObjectsStartPos.forEach((pos, obj) => {
340
+ obj.set({ left: pos.left + deltaX, top: pos.top + deltaY });
341
+ obj.setCoords();
356
342
  });
357
- finalPositionsRef.current = {
358
- tasks: localTasksRef.current, // will be updated by React after setState flushes
359
- documents: localDocumentsRef.current,
360
- };
361
- // 8. Single render call for all Fabric changes
362
343
  canvas.requestRenderAll();
363
344
  });
364
- };
345
+ }, [fabricCanvas, updateNodeStyles]);
365
346
  const handleEnd = () => {
366
- if (rafIdRef.current !== null) {
347
+ if (rafIdRef.current !== null)
367
348
  cancelAnimationFrame(rafIdRef.current);
368
- rafIdRef.current = null;
369
- }
370
349
  dragStateRef.current.isDragging = false;
371
350
  setDragging(null);
372
351
  document.body.style.cursor = "";
373
352
  document.body.style.userSelect = "";
374
353
  document.body.style.touchAction = "";
375
- // ✅ FIX 1+3: Read from live refs — never from the stale closure.
376
- // localTasksRef is kept in sync on every render, so this is always
377
- // the position after the last committed setState, not the t=0 snapshot.
378
- onTasksUpdate?.(localTasksRef.current);
379
- onDocumentsUpdate?.(localDocumentsRef.current);
354
+ onTasksUpdate?.(localTasks);
355
+ onDocumentsUpdate?.(localDocuments);
380
356
  };
381
357
  window.addEventListener("mousemove", handleMove, { passive: false });
382
358
  window.addEventListener("mouseup", handleEnd);
@@ -438,27 +414,33 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
438
414
  window.addEventListener("keydown", handleKeyDown);
439
415
  return () => window.removeEventListener("keydown", handleKeyDown);
440
416
  }, [localTasks, localDocuments, selectedIds, onTasksUpdate, onDocumentsUpdate]);
441
- const getAbsoluteTransform = (x, y) => {
442
- // We calculate the screen position in one go, matching Fabric's internal VPT logic
443
- const screenX = x * canvasZoom + canvasViewport.x;
444
- const screenY = y * canvasZoom + canvasViewport.y;
445
- return `translate3d(${screenX}px, ${screenY}px, 0) scale(${canvasZoom})`;
446
- };
447
417
  // ── Render helper ────────────────────────────────────────────────────────────
448
418
  const renderItem = (id, x, y, children) => {
419
+ const screenX = x * canvasZoom;
420
+ const screenY = y * canvasZoom;
421
+ // 1. Detect if the user is interacting with the canvas at all
422
+ // 'dragging' is your existing state.
423
+ // You might want to pass 'isZooming' or 'isPanning' from your main canvas component here.
449
424
  const isDragging = dragging?.itemIds.includes(id);
450
425
  return (_jsx("div", { className: "pointer-events-auto absolute", style: {
451
426
  left: 0,
452
427
  top: 0,
453
- // FIX: Combined Transform prevents the "Pan vs Zoom" lag
454
- transform: getAbsoluteTransform(x, y),
428
+ // 2. Use translate3d for GPU performance
429
+ transform: `translate3d(${screenX}px, ${screenY}px, 0) scale(${canvasZoom})`,
455
430
  transformOrigin: "top left",
456
- transition: "none", // Critical: prevents animation-induced jitter
431
+ // 3. THE FIX: Remove transition entirely during any viewport change
432
+ // Any 'ease' during zoom causes the "shaking" behavior.
433
+ transition: "none",
434
+ // 4. Optimization
457
435
  willChange: "transform",
458
436
  zIndex: isDragging ? 1000 : 1,
459
437
  }, children: children }, id));
460
438
  };
461
- return (_jsxs("div", { ref: overlayRef, className: "absolute inset-0 pointer-events-none overflow-hidden", style: { zIndex: 50 }, 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 })))] })
462
- // </div>
463
- );
439
+ return (_jsx("div", { ref: overlayRef, className: "absolute inset-0 pointer-events-none", style: { zIndex: 50 }, onWheel: handleOverlayWheel, onClick: (e) => {
440
+ if (e.target === e.currentTarget)
441
+ setSelectedIds(new Set());
442
+ }, children: _jsxs("div", { className: "absolute top-0 left-0 pointer-events-none", style: {
443
+ transform: `translate(${canvasViewport.x}px, ${canvasViewport.y}px)`,
444
+ transformOrigin: "top left",
445
+ }, 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 })))] }) }));
464
446
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.70.0",
3
+ "version": "2.71.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",