@mhamz.01/easyflow-whiteboard 2.123.0 → 2.125.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,3 +1,4 @@
1
+ import React from "react";
1
2
  import { FabricObject, Canvas } from "fabric";
2
3
  export interface Task {
3
4
  id: string;
@@ -40,6 +41,6 @@ interface CanvasOverlayLayerProps {
40
41
  fabricCanvas?: React.RefObject<Canvas | null>;
41
42
  canvasReady?: boolean;
42
43
  }
43
- export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, onDocumentsUpdate, canvasZoom, canvasViewport, selectionBox, selectedCanvasObjects, fabricCanvas, }: CanvasOverlayLayerProps): import("react/jsx-runtime").JSX.Element;
44
- export {};
44
+ declare const _default: React.NamedExoticComponent<CanvasOverlayLayerProps>;
45
+ export default _default;
45
46
  //# sourceMappingURL=custom-node-overlay-layer.d.ts.map
@@ -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;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":"AAEA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAI9C,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,wBA6pBG"}
@@ -1,19 +1,25 @@
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 React, { useState, useEffect, useRef } from "react";
4
4
  import TaskNode from "./custom-node";
5
5
  import DocumentNode from "./document-node";
6
6
  // ─── Component ────────────────────────────────────────────────────────────────
7
- export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, onDocumentsUpdate, canvasZoom = 1, canvasViewport = { x: 0, y: 0 }, selectionBox = null, selectedCanvasObjects = [], fabricCanvas, }) {
7
+ export default React.memo(function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, onDocumentsUpdate, canvasZoom = 1, canvasViewport = { x: 0, y: 0 }, selectionBox = null, selectedCanvasObjects = [], fabricCanvas, canvasReady: canvasReadyProp = false, }) {
8
8
  const [localTasks, setLocalTasks] = useState(tasks);
9
9
  const [localDocuments, setLocalDocuments] = useState(documents);
10
10
  const [selectedIds, setSelectedIds] = useState(new Set());
11
11
  const [dragging, setDragging] = useState(null);
12
- const [canvasReady, setCanvasReady] = useState(false);
13
12
  const nodeClipboardRef = useRef({
14
13
  tasks: [],
15
14
  documents: [],
16
15
  });
16
+ // ── Refs for always-current state (Issue 4) ────────────────────────────────
17
+ const localTasksRef = useRef(localTasks);
18
+ const localDocumentsRef = useRef(localDocuments);
19
+ const selectedIdsRef = useRef(selectedIds);
20
+ localTasksRef.current = localTasks;
21
+ localDocumentsRef.current = localDocuments;
22
+ selectedIdsRef.current = selectedIds;
17
23
  const dragStateRef = useRef({
18
24
  isDragging: false,
19
25
  itemIds: [],
@@ -23,29 +29,18 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
23
29
  offsetY: 0,
24
30
  });
25
31
  const rafIdRef = useRef(null);
32
+ const fabricMoveRafRef = useRef(null); // Issue 7
26
33
  const overlayRef = useRef(null);
27
- const selectedIdsRef = useRef(selectedIds);
28
- selectedIdsRef.current = selectedIds;
29
34
  // ── Sync props → local state ────────────────────────────────────────────────
30
- useEffect(() => { setLocalTasks(tasks); }, [tasks]);
31
- useEffect(() => { setLocalDocuments(documents); }, [documents]);
32
- // effect — polls until fabricCanvas.current is available:
33
35
  useEffect(() => {
34
- if (canvasReady)
35
- return;
36
- if (fabricCanvas?.current) {
37
- setCanvasReady(true);
38
- return;
39
- }
40
- // Poll every 50ms until canvas is ready (only needed on first load)
41
- const interval = setInterval(() => {
42
- if (fabricCanvas?.current) {
43
- setCanvasReady(true);
44
- clearInterval(interval);
45
- }
46
- }, 50);
47
- return () => clearInterval(interval);
48
- }, [fabricCanvas, canvasReady]);
36
+ setLocalTasks(tasks);
37
+ }, [tasks]);
38
+ useEffect(() => {
39
+ setLocalDocuments(documents);
40
+ }, [documents]);
41
+ // Issue 2: Remove internal canvasReady state and polling — use prop directly
42
+ // No longer needed: const [canvasReady, setCanvasReady] = useState(false);
43
+ // No longer needed: polling useEffect with setInterval
49
44
  // ── Event Forwarding (Fixes Zooming on Nodes) ───────────────────────────────
50
45
  const handleOverlayWheel = (e) => {
51
46
  if (e.ctrlKey || e.metaKey || e.shiftKey) {
@@ -61,7 +56,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
61
56
  x: nativeEvent.clientX - rect.left,
62
57
  y: nativeEvent.clientY - rect.top,
63
58
  };
64
- // We cast to 'any' here because we are manually triggering an internal
59
+ // We cast to 'any' here because we are manually triggering an internal
65
60
  // event bus, and Fabric's internal types for .fire() can be overly strict.
66
61
  canvas.fire("mouse:wheel", {
67
62
  e: nativeEvent,
@@ -72,6 +67,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
72
67
  e.stopPropagation();
73
68
  }
74
69
  };
70
+ // Issue 6: Remove canvasZoom from deps — not used in handler
75
71
  useEffect(() => {
76
72
  const overlayEl = overlayRef.current;
77
73
  const canvas = fabricCanvas?.current;
@@ -106,12 +102,14 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
106
102
  return () => {
107
103
  overlayEl.removeEventListener("wheel", handleGlobalWheel);
108
104
  };
109
- }, [fabricCanvas, canvasZoom, canvasReady]); // Re-bind when zoom changes to keep closure fresh
105
+ }, [fabricCanvas, canvasReadyProp]); // Issue 6: removed canvasZoom
110
106
  // ── Fabric → Overlay Sync (Fixes Dragging from Fabric area) ──────────────────
107
+ // Issue 5: Remove canvasZoom from deps — not used in handlers
111
108
  useEffect(() => {
112
109
  const canvas = fabricCanvas?.current;
113
110
  if (!canvas)
114
111
  return;
112
+ // Issue 7: Throttle handleObjectMoving with rAF gate
115
113
  const handleObjectMoving = (e) => {
116
114
  const target = e.transform?.target || e.target;
117
115
  if (!target)
@@ -122,10 +120,19 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
122
120
  target._prevTop = target.top;
123
121
  if (deltaX === 0 && deltaY === 0)
124
122
  return;
125
- // ── Read from ref — always fresh, never stale ──
126
123
  const sel = selectedIdsRef.current;
127
- setLocalTasks((prev) => prev.map((t) => sel.has(t.id) ? { ...t, x: t.x + deltaX, y: t.y + deltaY } : t));
128
- setLocalDocuments((prev) => prev.map((d) => sel.has(d.id) ? { ...d, x: d.x + deltaX, y: d.y + deltaY } : d));
124
+ // Skip if frame already queued
125
+ if (fabricMoveRafRef.current !== null)
126
+ return;
127
+ fabricMoveRafRef.current = requestAnimationFrame(() => {
128
+ fabricMoveRafRef.current = null;
129
+ setLocalTasks((prev) => prev.map((t) => sel.has(t.id)
130
+ ? { ...t, x: t.x + deltaX, y: t.y + deltaY }
131
+ : t));
132
+ setLocalDocuments((prev) => prev.map((d) => sel.has(d.id)
133
+ ? { ...d, x: d.x + deltaX, y: d.y + deltaY }
134
+ : d));
135
+ });
129
136
  };
130
137
  const handleMouseDown = (e) => {
131
138
  const target = e.target;
@@ -160,18 +167,17 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
160
167
  canvas.off("object:moving", handleObjectMoving);
161
168
  canvas.off("mouse:down", handleMouseDown);
162
169
  canvas.off("selection:cleared", handleSelectionCleared);
170
+ if (fabricMoveRafRef.current !== null) {
171
+ cancelAnimationFrame(fabricMoveRafRef.current);
172
+ }
163
173
  };
164
- // ── selectedIds REMOVED from deps read via selectedIdsRef instead ──────
165
- // Having selectedIds here caused the effect to re-register on every selection
166
- // change, creating a new closure each time. The second drag captured a stale
167
- // or empty selectedIds from the closure at re-registration time.
168
- }, [canvasZoom, fabricCanvas, canvasReady]);
174
+ }, [fabricCanvas, canvasReadyProp]); // Issue 5: removed canvasZoom
169
175
  // ── Helpers ─────────────────────────────────────────────────────────────────
170
176
  const getItemPosition = (id) => {
171
- const task = localTasks.find((t) => t.id === id);
177
+ const task = localTasksRef.current.find((t) => t.id === id);
172
178
  if (task)
173
179
  return { x: task.x, y: task.y };
174
- const doc = localDocuments.find((d) => d.id === id);
180
+ const doc = localDocumentsRef.current.find((d) => d.id === id);
175
181
  if (doc)
176
182
  return { x: doc.x, y: doc.y };
177
183
  return undefined;
@@ -193,11 +199,11 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
193
199
  return;
194
200
  // ── O(n) single pass — no sort, no join, no extra allocations ──
195
201
  const newSelected = new Set();
196
- for (const task of localTasks) {
202
+ for (const task of localTasksRef.current) {
197
203
  if (isItemInSelectionBox(task.x, task.y, 300, 140, selectionBox))
198
204
  newSelected.add(task.id);
199
205
  }
200
- for (const doc of localDocuments) {
206
+ for (const doc of localDocumentsRef.current) {
201
207
  if (isItemInSelectionBox(doc.x, doc.y, 320, 160, selectionBox))
202
208
  newSelected.add(doc.id);
203
209
  }
@@ -215,7 +221,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
215
221
  // ── Drag start (HTML Node side) ──────────────────────────────────────────────
216
222
  // Helper to extract coordinates regardless of event type
217
223
  const getPointerEvent = (e) => {
218
- if ('touches' in e && e.touches.length > 0)
224
+ if ("touches" in e && e.touches.length > 0)
219
225
  return e.touches[0];
220
226
  return e;
221
227
  };
@@ -231,8 +237,8 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
231
237
  // 3. Determine which items are being dragged
232
238
  // selection update DOES NOT trigger before drag snapshot
233
239
  let itemsToDrag;
234
- if (selectedIds.has(itemId)) {
235
- itemsToDrag = Array.from(selectedIds);
240
+ if (selectedIdsRef.current.has(itemId)) {
241
+ itemsToDrag = Array.from(selectedIdsRef.current);
236
242
  }
237
243
  else {
238
244
  itemsToDrag = [itemId];
@@ -265,7 +271,10 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
265
271
  // 9. Snapshot starting positions for all selected Fabric objects
266
272
  const canvasObjectsStartPos = new Map();
267
273
  selectedCanvasObjects.forEach((obj) => {
268
- canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
274
+ canvasObjectsStartPos.set(obj, {
275
+ left: obj.left || 0,
276
+ top: obj.top || 0,
277
+ });
269
278
  });
270
279
  // 10. Commit to the ref for the requestAnimationFrame loop
271
280
  dragStateRef.current = {
@@ -276,7 +285,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
276
285
  offsetX: clickWorldX, // Now stored as World Units
277
286
  offsetY: clickWorldY, // Now stored as World Units
278
287
  };
279
- if (!selectedIds.has(itemId) && dragStateRef.current.itemIds.length === 0) {
288
+ if (!selectedIdsRef.current.has(itemId) && dragStateRef.current.itemIds.length === 0) {
280
289
  setSelectedIds(new Set([itemId]));
281
290
  }
282
291
  // 11. Trigger UI states
@@ -286,10 +295,10 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
286
295
  document.body.style.touchAction = "none";
287
296
  };
288
297
  // ── Drag move (HTML Node side) ───────────────────────────────────────────────
298
+ // Issue 3: Remove localTasks/localDocuments from deps — use refs instead
289
299
  useEffect(() => {
290
300
  if (!dragging)
291
301
  return;
292
- // Inside the useEffect that watches [dragging, localTasks, localDocuments, fabricCanvas]
293
302
  const handleMove = (e) => {
294
303
  if (!dragStateRef.current.isDragging)
295
304
  return;
@@ -300,7 +309,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
300
309
  if (rafIdRef.current !== null)
301
310
  cancelAnimationFrame(rafIdRef.current);
302
311
  rafIdRef.current = requestAnimationFrame(() => {
303
- const { itemIds, startPositions, canvasObjectsStartPos, offsetX, offsetY } = dragStateRef.current;
312
+ const { itemIds, startPositions, canvasObjectsStartPos, offsetX, offsetY, } = dragStateRef.current;
304
313
  const canvas = fabricCanvas?.current;
305
314
  if (!canvas)
306
315
  return;
@@ -316,33 +325,27 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
316
325
  // (Current Mouse World - Initial World Offset from Start)
317
326
  const deltaX = currentWorldX - offsetX;
318
327
  const deltaY = currentWorldY - offsetY;
319
- // if (itemIds.length > 0) {
320
- // console.groupCollapsed(`Dragging Node: ${itemIds[0]}`);
321
- // console.log("Screen Mouse:", { x: pointer.clientX, y: pointer.clientY });
322
- // console.log("World Mouse:", { x: currentWorldX.toFixed(2), y: currentWorldY.toFixed(2) });
323
- // console.log("Canvas Zoom:", liveZoom.toFixed(2));
324
- // console.log("New Node Pos:", { x: deltaX.toFixed(2), y: deltaY.toFixed(2) });
325
- // console.groupEnd();
326
- // }
327
328
  // 5. Calculate the Movement Delta in World Units
328
329
  // We compare where the first item started vs where it is now.
329
330
  const firstId = itemIds[0];
330
331
  const firstStart = startPositions.get(firstId);
331
332
  if (!firstStart)
332
333
  return;
333
- // The real problem of task jumps
334
- // const deltaX = deltaX - firstStart.x;
335
334
  // 6. Update HTML Nodes (Batching these into one state update)
336
- setLocalTasks((prev) => prev.map((t) => itemIds.includes(t.id) ? {
337
- ...t,
338
- x: (startPositions.get(t.id)?.x ?? t.x) + deltaX,
339
- y: (startPositions.get(t.id)?.y ?? t.y) + deltaY,
340
- } : t));
341
- setLocalDocuments((prev) => prev.map((d) => itemIds.includes(d.id) ? {
342
- ...d,
343
- x: (startPositions.get(d.id)?.x ?? d.x) + deltaX,
344
- y: (startPositions.get(d.id)?.y ?? d.y) + deltaY,
345
- } : d));
335
+ setLocalTasks((prev) => prev.map((t) => itemIds.includes(t.id)
336
+ ? {
337
+ ...t,
338
+ x: (startPositions.get(t.id)?.x ?? t.x) + deltaX,
339
+ y: (startPositions.get(t.id)?.y ?? t.y) + deltaY,
340
+ }
341
+ : t));
342
+ setLocalDocuments((prev) => prev.map((d) => itemIds.includes(d.id)
343
+ ? {
344
+ ...d,
345
+ x: (startPositions.get(d.id)?.x ?? d.x) + deltaX,
346
+ y: (startPositions.get(d.id)?.y ?? d.y) + deltaY,
347
+ }
348
+ : d));
346
349
  // 7. Sync Fabric Objects (Imperative update for performance)
347
350
  canvasObjectsStartPos.forEach((startPos, obj) => {
348
351
  obj.set({
@@ -355,6 +358,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
355
358
  canvas.requestRenderAll();
356
359
  });
357
360
  };
361
+ // Issue 4: Use refs for always-current values
358
362
  const handleEnd = () => {
359
363
  if (rafIdRef.current !== null)
360
364
  cancelAnimationFrame(rafIdRef.current);
@@ -363,8 +367,8 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
363
367
  document.body.style.cursor = "";
364
368
  document.body.style.userSelect = "";
365
369
  document.body.style.touchAction = "";
366
- onTasksUpdate?.(localTasks);
367
- onDocumentsUpdate?.(localDocuments);
370
+ onTasksUpdate?.(localTasksRef.current);
371
+ onDocumentsUpdate?.(localDocumentsRef.current);
368
372
  };
369
373
  window.addEventListener("mousemove", handleMove, { passive: false });
370
374
  window.addEventListener("mouseup", handleEnd);
@@ -378,7 +382,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
378
382
  window.removeEventListener("touchend", handleEnd);
379
383
  window.removeEventListener("touchcancel", handleEnd);
380
384
  };
381
- }, [dragging, localTasks, localDocuments, fabricCanvas]);
385
+ }, [dragging, fabricCanvas]); // Issue 3: removed localTasks/localDocuments
382
386
  // ── Selection, Status, Keyboard Logic ────────────────────────────────────────
383
387
  const handleSelect = (id, e) => {
384
388
  if (e?.shiftKey || e?.ctrlKey || e?.metaKey) {
@@ -393,29 +397,36 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
393
397
  }
394
398
  };
395
399
  const handleStatusChange = (taskId, newStatus) => {
396
- const updated = localTasks.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t));
400
+ const updated = localTasksRef.current.map((t) => t.id === taskId ? { ...t, status: newStatus } : t);
397
401
  setLocalTasks(updated);
398
402
  onTasksUpdate?.(updated);
399
403
  };
404
+ // Issue 9: Use refs and only depend on stable callbacks
400
405
  useEffect(() => {
401
406
  const handleKeyDown = (e) => {
402
407
  // Don't trigger if typing in input
403
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
408
+ if (e.target instanceof HTMLInputElement ||
409
+ e.target instanceof HTMLTextAreaElement)
404
410
  return;
405
411
  // Select All
406
412
  if ((e.ctrlKey || e.metaKey) && e.key === "a") {
407
413
  e.preventDefault();
408
- setSelectedIds(new Set([...localTasks.map((t) => t.id), ...localDocuments.map((d) => d.id)]));
414
+ setSelectedIds(new Set([
415
+ ...localTasksRef.current.map((t) => t.id),
416
+ ...localDocumentsRef.current.map((d) => d.id),
417
+ ]));
409
418
  }
410
419
  // Clear selection
411
420
  if (e.key === "Escape") {
412
421
  setSelectedIds(new Set());
413
422
  }
414
- // ← ADD THIS: Delete selected nodes
415
- if ((e.key === "Delete" || e.key === "Backspace") && selectedIds.size > 0) {
423
+ // Delete selected nodes
424
+ if ((e.key === "Delete" || e.key === "Backspace") &&
425
+ selectedIdsRef.current.size > 0) {
416
426
  e.preventDefault();
417
- const updatedTasks = localTasks.filter((t) => !selectedIds.has(t.id));
418
- const updatedDocs = localDocuments.filter((d) => !selectedIds.has(d.id));
427
+ const sel = selectedIdsRef.current;
428
+ const updatedTasks = localTasksRef.current.filter((t) => !sel.has(t.id));
429
+ const updatedDocs = localDocumentsRef.current.filter((d) => !sel.has(d.id));
419
430
  setLocalTasks(updatedTasks);
420
431
  setLocalDocuments(updatedDocs);
421
432
  setSelectedIds(new Set());
@@ -425,14 +436,12 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
425
436
  };
426
437
  window.addEventListener("keydown", handleKeyDown);
427
438
  return () => window.removeEventListener("keydown", handleKeyDown);
428
- }, [localTasks, localDocuments, selectedIds, onTasksUpdate, onDocumentsUpdate]);
439
+ }, [onTasksUpdate, onDocumentsUpdate]); // Issue 9: only stable callbacks
429
440
  // ── Render helper ────────────────────────────────────────────────────────────
430
441
  const renderItem = (id, x, y, children) => {
431
442
  const screenX = x * canvasZoom;
432
443
  const screenY = y * canvasZoom;
433
444
  // 1. Detect if the user is interacting with the canvas at all
434
- // 'dragging' is your existing state.
435
- // You might want to pass 'isZooming' or 'isPanning' from your main canvas component here.
436
445
  const isDragging = dragging?.itemIds.includes(id);
437
446
  return (_jsx("div", { className: "pointer-events-auto absolute", style: {
438
447
  left: 0,
@@ -455,4 +464,19 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
455
464
  transform: `translate(${canvasViewport.x}px, ${canvasViewport.y}px)`,
456
465
  transformOrigin: "top left",
457
466
  }, 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 })))] }) }));
458
- }
467
+ }, (prev, next) => {
468
+ // Custom comparator — skip re-render if props are equal
469
+ // Return true to skip re-render, false to re-render
470
+ return (prev.tasks === next.tasks &&
471
+ prev.documents === next.documents &&
472
+ prev.canvasZoom === next.canvasZoom &&
473
+ prev.canvasViewport?.x === next.canvasViewport?.x &&
474
+ prev.canvasViewport?.y === next.canvasViewport?.y &&
475
+ prev.selectionBox === next.selectionBox &&
476
+ prev.selectedCanvasObjects === next.selectedCanvasObjects &&
477
+ prev.onTasksUpdate === next.onTasksUpdate &&
478
+ prev.onDocumentsUpdate === next.onDocumentsUpdate &&
479
+ prev.canvasReady === next.canvasReady
480
+ // fabricCanvas ref intentionally omitted — it's stable and doesn't need comparison
481
+ );
482
+ });
@@ -1,8 +1,8 @@
1
- import { RefObject } from "react";
1
+ import React, { RefObject } from "react";
2
2
  import fabric from "fabric";
3
3
  interface ToolOptionsPanelProps {
4
4
  fabricCanvas: RefObject<fabric.Canvas | null>;
5
5
  }
6
- export default function ToolOptionsPanel({ fabricCanvas, }: ToolOptionsPanelProps): import("react/jsx-runtime").JSX.Element;
7
- export {};
6
+ declare const _default: React.NamedExoticComponent<ToolOptionsPanelProps>;
7
+ export default _default;
8
8
  //# sourceMappingURL=tooloptions-panel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tooloptions-panel.d.ts","sourceRoot":"","sources":["../../../src/components/toolbar/tooloptions-panel.tsx"],"names":[],"mappings":"AAGA,OAAc,EAAE,SAAS,EAAgC,MAAM,OAAO,CAAC;AAUvE,OAAO,MAA2C,MAAM,QAAQ,CAAC;AAIjE,UAAU,qBAAqB;IAC7B,YAAY,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,YAAY,GACb,EAAE,qBAAqB,2CA0MvB"}
1
+ {"version":3,"file":"tooloptions-panel.d.ts","sourceRoot":"","sources":["../../../src/components/toolbar/tooloptions-panel.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,SAAS,EAA6C,MAAM,OAAO,CAAC;AAUpF,OAAO,MAA4C,MAAM,QAAQ,CAAC;AAIlE,UAAU,qBAAqB;IAC7B,YAAY,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;;AAWD,wBA2PG"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useEffect, useMemo, useState } from "react";
3
+ import React, { useEffect, useMemo, useState, useCallback } from "react";
4
4
  import { cn } from "../../lib/utils";
5
5
  import { useWhiteboardStore } from "../../store/whiteboard-store";
6
6
  import PenOptions from "./options/pen-option";
@@ -12,13 +12,21 @@ import ArrowOptions from "./options/arrow-options";
12
12
  import { Rect, Circle, IText } from "fabric";
13
13
  import { Settings2, ChevronDown } from "lucide-react";
14
14
  import LayerControls from "./layers-control";
15
- export default function ToolOptionsPanel({ fabricCanvas, }) {
15
+ // ── Memoized child components ─────────────────────────────────────────────────
16
+ const MemoizedPenOptions = React.memo(PenOptions);
17
+ const MemoizedShapeOptions = React.memo(ShapeOptions);
18
+ const MemoizedTextOptions = React.memo(TextOptions);
19
+ const MemoizedImageOptions = React.memo(ImageOptions);
20
+ const MemoizedLineOptions = React.memo(LineOptions);
21
+ const MemoizedArrowOptions = React.memo(ArrowOptions);
22
+ const MemoizedLayerControls = React.memo(LayerControls);
23
+ export default React.memo(function ToolOptionsPanel({ fabricCanvas, }) {
16
24
  const activeTool = useWhiteboardStore((state) => state.activeTool);
17
25
  const selectedObjectType = useWhiteboardStore((state) => state.selectedObjectType);
18
26
  const toolOptions = useWhiteboardStore((state) => state.toolOptions);
19
27
  const [isMobileExpanded, setIsMobileExpanded] = useState(false);
20
28
  const displayTool = selectedObjectType || activeTool;
21
- // Real-time fabric updates
29
+ // ── Real-time fabric updates ──────────────────────────────────────────────────
22
30
  useEffect(() => {
23
31
  const canvas = fabricCanvas.current;
24
32
  if (!canvas)
@@ -57,21 +65,43 @@ export default function ToolOptionsPanel({ fabricCanvas, }) {
57
65
  }
58
66
  canvas.renderAll();
59
67
  }, [toolOptions, selectedObjectType, fabricCanvas]);
60
- const toolsWithoutOptions = ["select", "pan", "undo", "redo", "eraser"];
68
+ // ── Memoized constants ────────────────────────────────────────────────────────
69
+ const toolsWithoutOptions = useMemo(() => ["select", "pan", "undo", "redo", "eraser"], []);
61
70
  const isOpen = useMemo(() => {
62
71
  const target = selectedObjectType || activeTool;
63
72
  return target && !toolsWithoutOptions.includes(target);
64
- }, [activeTool, selectedObjectType]);
73
+ }, [activeTool, selectedObjectType, toolsWithoutOptions]);
74
+ // ── Auto-collapse mobile panel when tool changes ───────────────────────────────
65
75
  useEffect(() => {
66
76
  if (!isOpen) {
67
77
  setIsMobileExpanded(false);
68
78
  }
69
79
  }, [isOpen]);
70
- return (_jsxs(_Fragment, { children: [_jsxs("aside", { className: cn("hidden md:flex", "absolute left-4 top-1/2 -translate-y-1/2 z-40", "flex-col", "w-64 max-h-[85vh]", "bg-[#1A1A1E]/90 backdrop-blur-xl", "border border-white/10", "shadow-[0_20px_50px_rgba(0,0,0,0.5)]", "rounded-[24px]", "transition-all duration-500 cubic-bezier(0.16, 1, 0.3, 1)", isOpen
71
- ? "translate-x-0 opacity-100 visible scale-100"
72
- : "-translate-x-12 opacity-0 invisible scale-95 pointer-events-none"), children: [_jsx("div", { className: "absolute -left-[1px] top-1/2 -translate-y-1/2 w-[3px] h-12 bg-[#029AFF] rounded-r-full shadow-[0_0_15px_#029AFF]" }), _jsxs("div", { className: "flex items-center justify-between px-5 py-4 border-b border-white/5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Settings2, { className: "w-4 h-4 text-[#029AFF]" }), _jsx("h3", { className: "text-[11px] font-bold uppercase tracking-[0.1em] text-neutral-400", children: "Properties" })] }), _jsx("div", { className: "text-[10px] px-2 py-0.5 rounded-full bg-white/5 text-white/40 border border-white/5", children: displayTool })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-5 custom-scrollbar scroll-smooth", children: _jsxs("div", { className: "space-y-6 text-neutral-200", children: [displayTool === "pen" && _jsx(PenOptions, {}), (displayTool === "rectangle" || displayTool === "circle" || displayTool === "frame") && (_jsx(ShapeOptions, { shapeType: displayTool })), displayTool === "text" && _jsx(TextOptions, {}), displayTool === "image" && _jsx(ImageOptions, {}), displayTool === "line" && _jsx(LineOptions, {}), displayTool === "arrow" && _jsx(ArrowOptions, { fabricCanvas: fabricCanvas }), selectedObjectType && (_jsx(LayerControls, { fabricCanvas: fabricCanvas }))] }) }), _jsx("div", { className: "p-3 flex justify-center", children: _jsx("div", { className: "w-8 h-1 rounded-full bg-white/10" }) })] }), isOpen && (_jsxs("div", { className: cn("md:hidden", "fixed left-3 top-4 z-40", // Positioned Top-Left to stay clear of Top-Right zoom
73
- "w-[calc(100%-80px)] max-w-[180px]", // Leave room on the right to tap canvas
74
- "bg-[#1A1A1E]/95 backdrop-blur-xl", "border border-white/10", "rounded-2xl shadow-2xl", "transition-all duration-300 ease-in-out", isMobileExpanded ? "max-h-[70vh]" : "h-[48px]"), children: [_jsxs("div", { className: "flex items-center justify-between px-4 h-[48px] cursor-pointer", onClick: () => setIsMobileExpanded(!isMobileExpanded), children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Settings2, { className: "w-4 h-4 text-[#029AFF]" }), _jsxs("span", { className: "text-[10px] font-bold uppercase tracking-wider text-neutral-300", children: [displayTool, " Settings"] })] }), _jsx("div", { className: "flex items-center gap-2", children: _jsx("div", { className: cn("transition-transform duration-300", isMobileExpanded ? "rotate-180" : "rotate-0"), children: _jsx(ChevronDown, { className: "w-4 h-4 text-white/50" }) }) })] }), _jsx("div", { className: cn("overflow-hidden transition-all duration-300", isMobileExpanded ? "opacity-100 h-auto" : "opacity-0 h-0"), children: _jsxs("div", { className: "px-4 pb-5 pt-2 space-y-4 overflow-y-auto max-h-[60vh] custom-scrollbar", children: [_jsx("div", { className: "h-[1px] w-full bg-white/5 mb-4" }), displayTool === "pen" && _jsx(PenOptions, {}), (displayTool === "rectangle" || displayTool === "circle" || displayTool === "frame") && (_jsx(ShapeOptions, { shapeType: displayTool })), displayTool === "text" && _jsx(TextOptions, {}), displayTool === "image" && _jsx(ImageOptions, {}), displayTool === "line" && _jsx(LineOptions, {}), displayTool === "arrow" && _jsx(ArrowOptions, { fabricCanvas: fabricCanvas })] }) })] })), _jsx("style", { children: `
80
+ // ── Memoized handlers ─────────────────────────────────────────────────────────
81
+ const handleMobileToggle = useCallback(() => {
82
+ setIsMobileExpanded((prev) => !prev);
83
+ }, []);
84
+ // ── Memoized desktop sidebar content ──────────────────────────────────────────
85
+ const desktopContent = useMemo(() => {
86
+ return (_jsxs("div", { className: "space-y-6 text-neutral-200", children: [displayTool === "pen" && _jsx(MemoizedPenOptions, {}), (displayTool === "rectangle" || displayTool === "circle" || displayTool === "frame") && (_jsx(MemoizedShapeOptions, { shapeType: displayTool })), displayTool === "text" && _jsx(MemoizedTextOptions, {}), displayTool === "image" && _jsx(MemoizedImageOptions, {}), displayTool === "line" && _jsx(MemoizedLineOptions, {}), displayTool === "arrow" && _jsx(MemoizedArrowOptions, { fabricCanvas: fabricCanvas }), selectedObjectType && _jsx(MemoizedLayerControls, { fabricCanvas: fabricCanvas })] }));
87
+ }, [displayTool, selectedObjectType, fabricCanvas]);
88
+ // ── Memoized mobile content ───────────────────────────────────────────────────
89
+ const mobileContent = useMemo(() => {
90
+ return (_jsxs("div", { className: "px-4 pb-5 pt-2 space-y-4 overflow-y-auto max-h-[60vh] custom-scrollbar", children: [_jsx("div", { className: "h-[1px] w-full bg-white/5 mb-4" }), displayTool === "pen" && _jsx(MemoizedPenOptions, {}), (displayTool === "rectangle" || displayTool === "circle" || displayTool === "frame") && (_jsx(MemoizedShapeOptions, { shapeType: displayTool })), displayTool === "text" && _jsx(MemoizedTextOptions, {}), displayTool === "image" && _jsx(MemoizedImageOptions, {}), displayTool === "line" && _jsx(MemoizedLineOptions, {}), displayTool === "arrow" && _jsx(MemoizedArrowOptions, { fabricCanvas: fabricCanvas })] }));
91
+ }, [displayTool, fabricCanvas]);
92
+ // ── Memoized desktop header ───────────────────────────────────────────────────
93
+ const desktopHeader = useMemo(() => (_jsxs("div", { className: "flex items-center justify-between px-5 py-4 border-b border-white/5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Settings2, { className: "w-4 h-4 text-[#029AFF]" }), _jsx("h3", { className: "text-[11px] font-bold uppercase tracking-[0.1em] text-neutral-400", children: "Properties" })] }), _jsx("div", { className: "text-[10px] px-2 py-0.5 rounded-full bg-white/5 text-white/40 border border-white/5", children: displayTool })] })), [displayTool]);
94
+ // ── Memoized mobile header ────────────────────────────────────────────────────
95
+ const mobileHeader = useMemo(() => (_jsxs("div", { className: "flex items-center justify-between px-4 h-[48px] cursor-pointer", onClick: handleMobileToggle, children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Settings2, { className: "w-4 h-4 text-[#029AFF]" }), _jsxs("span", { className: "text-[10px] font-bold uppercase tracking-wider text-neutral-300", children: [displayTool, " Settings"] })] }), _jsx("div", { className: "flex items-center gap-2", children: _jsx("div", { className: cn("transition-transform duration-300", isMobileExpanded ? "rotate-180" : "rotate-0"), children: _jsx(ChevronDown, { className: "w-4 h-4 text-white/50" }) }) })] })), [displayTool, isMobileExpanded, handleMobileToggle]);
96
+ // ── Memoized desktop sidebar classes ──────────────────────────────────────────
97
+ const desktopSidebarClasses = useMemo(() => cn("hidden md:flex", "absolute left-4 top-1/2 -translate-y-1/2 z-40", "flex-col", "w-64 max-h-[85vh]", "bg-[#1A1A1E]/90 backdrop-blur-xl", "border border-white/10", "shadow-[0_20px_50px_rgba(0,0,0,0.5)]", "rounded-[24px]", "transition-all duration-500 cubic-bezier(0.16, 1, 0.3, 1)", isOpen
98
+ ? "translate-x-0 opacity-100 visible scale-100"
99
+ : "-translate-x-12 opacity-0 invisible scale-95 pointer-events-none"), [isOpen]);
100
+ // ── Memoized mobile panel classes ─────────────────────────────────────────────
101
+ const mobilePanelClasses = useMemo(() => cn("md:hidden", "fixed left-3 top-4 z-40", "w-[calc(100%-80px)] max-w-[180px]", "bg-[#1A1A1E]/95 backdrop-blur-xl", "border border-white/10", "rounded-2xl shadow-2xl", "transition-all duration-300 ease-in-out", isMobileExpanded ? "max-h-[70vh]" : "h-[48px]"), [isMobileExpanded]);
102
+ // ── Memoized mobile content container classes ─────────────────────────────────
103
+ const mobileContentClasses = useMemo(() => cn("overflow-hidden transition-all duration-300", isMobileExpanded ? "opacity-100 h-auto" : "opacity-0 h-0"), [isMobileExpanded]);
104
+ return (_jsxs(_Fragment, { children: [_jsxs("aside", { className: desktopSidebarClasses, children: [_jsx("div", { className: "absolute -left-[1px] top-1/2 -translate-y-1/2 w-[3px] h-12 bg-[#029AFF] rounded-r-full shadow-[0_0_15px_#029AFF]" }), desktopHeader, _jsx("div", { className: "flex-1 overflow-y-auto p-5 custom-scrollbar scroll-smooth", children: desktopContent }), _jsx("div", { className: "p-3 flex justify-center", children: _jsx("div", { className: "w-8 h-1 rounded-full bg-white/10" }) })] }), isOpen && (_jsxs("div", { className: mobilePanelClasses, children: [mobileHeader, _jsx("div", { className: mobileContentClasses, children: mobileContent })] })), _jsx("style", { children: `
75
105
  .custom-scrollbar::-webkit-scrollbar {
76
106
  width: 4px;
77
107
  }
@@ -86,4 +116,8 @@ export default function ToolOptionsPanel({ fabricCanvas, }) {
86
116
  background: #029AFF;
87
117
  }
88
118
  ` })] }));
89
- }
119
+ }, (prev, next) => {
120
+ // Custom comparator for React.memo
121
+ // Return true to skip re-render, false to re-render
122
+ return prev.fabricCanvas === next.fabricCanvas;
123
+ });
@@ -1,4 +1,4 @@
1
- import { MutableRefObject } from "react";
1
+ import React, { MutableRefObject } from "react";
2
2
  import { Canvas } from "fabric";
3
3
  interface TaskTemplate {
4
4
  id: string;
@@ -26,6 +26,6 @@ interface WhiteboardToolbarProps {
26
26
  availableDocuments?: DocumentTemplate[];
27
27
  isLoadingData?: boolean;
28
28
  }
29
- export default function WhiteboardToolbar({ fabricCanvas, isRestoringRef, onAddTask, onAddDocument, availableTasks, availableDocuments, isLoadingData }: WhiteboardToolbarProps): import("react/jsx-runtime").JSX.Element;
30
- export {};
29
+ declare const _default: React.NamedExoticComponent<WhiteboardToolbarProps>;
30
+ export default _default;
31
31
  //# sourceMappingURL=whiteboard-toolbar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"whiteboard-toolbar.d.ts","sourceRoot":"","sources":["../../../src/components/toolbar/whiteboard-toolbar.tsx"],"names":[],"mappings":"AAEA,OAAa,EAAqB,gBAAgB,EAAY,MAAM,OAAO,CAAC;AAK5E,OAAO,EAAE,MAAM,EAAQ,MAAM,QAAQ,CAAC;AAOtC,UAAU,YAAY;IACpB,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC7F;AACD,UAAU,gBAAgB;IACxB,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAC3C,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5D;AAkBD,UAAU,sBAAsB;IAC9B,YAAY,EAAE,gBAAgB,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9C,cAAc,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC1C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAGhD,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAChC,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;CAEzB;AAED,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,aAAa,EAAE,cAAmB,EACrH,kBAAuB,EAAE,aAAmB,EAAE,EAAE,sBAAsB,2CAkMvE"}
1
+ {"version":3,"file":"whiteboard-toolbar.d.ts","sourceRoot":"","sources":["../../../src/components/toolbar/whiteboard-toolbar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAqB,gBAAgB,EAAkC,MAAM,OAAO,CAAC;AAKnG,OAAO,EAAE,MAAM,EAAQ,MAAM,QAAQ,CAAC;AAQtC,UAAU,YAAY;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACxC,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,UAAU,gBAAgB;IACxB,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;CACpB;AAkBD,UAAU,sBAAsB;IAC9B,YAAY,EAAE,gBAAgB,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9C,cAAc,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC1C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAChD,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAChC,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;;AAUD,wBA2SG"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useEffect, useRef, useState } from "react";
3
+ import React, { useEffect, useRef, useState, useCallback, useMemo } from "react";
4
4
  import { MousePointer2, Pencil, Square, Circle, Minus, MoveRight, Type, Eraser, Hand, Frame, Undo2, Redo2, Image as ImageIcon, } from "lucide-react";
5
5
  import { util } from "fabric";
6
6
  import { useWhiteboardStore } from "../../store/whiteboard-store";
@@ -24,7 +24,13 @@ const TOOLS = [
24
24
  { id: "undo", icon: Undo2, label: "Undo", shortcut: "⌘Z", category: "history" },
25
25
  { id: "redo", icon: Redo2, label: "Redo", shortcut: "⌘⇧Z", category: "history" },
26
26
  ];
27
- export default function WhiteboardToolbar({ fabricCanvas, isRestoringRef, onAddTask, onAddDocument, availableTasks = [], availableDocuments = [], isLoadingData = false }) {
27
+ // ── Memoized ToolButton wrapper ───────────────────────────────────────────────
28
+ const MemoizedToolButton = React.memo(ToolButton);
29
+ const MemoizedToolbarSeparator = React.memo(ToolbarSeparator);
30
+ const MemoizedTaskDropdown = React.memo(TaskDropdown);
31
+ const MemoizedDocumentDropdown = React.memo(DocumentDropdown);
32
+ // ── Main Component ────────────────────────────────────────────────────────────
33
+ export default React.memo(function WhiteboardToolbar({ fabricCanvas, isRestoringRef, onAddTask, onAddDocument, availableTasks = [], availableDocuments = [], isLoadingData = false, }) {
28
34
  const activeTool = useWhiteboardStore((s) => s.activeTool);
29
35
  const setActiveTool = useWhiteboardStore((s) => s.setActiveTool);
30
36
  const canUndo = useWhiteboardStore((s) => s.canUndo);
@@ -32,53 +38,33 @@ export default function WhiteboardToolbar({ fabricCanvas, isRestoringRef, onAddT
32
38
  const undo = useWhiteboardStore((s) => s.undo);
33
39
  const redo = useWhiteboardStore((s) => s.redo);
34
40
  const scrollRef = useRef(null);
35
- const [scrollPosition, setScrollPosition] = useState(0);
36
41
  const fileInputRef = useRef(null);
37
- const handleScroll = () => {
42
+ const [scrollPosition, setScrollPosition] = useState(0);
43
+ // ── Memoized callbacks ────────────────────────────────────────────────────────
44
+ const handleScroll = useCallback(() => {
38
45
  if (scrollRef.current) {
39
46
  const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current;
40
47
  const position = scrollLeft / (scrollWidth - clientWidth);
41
48
  setScrollPosition(position);
42
49
  }
43
- };
44
- // fade out and remove the welcome hero when user selects a tool for the first time
45
- useEffect(() => {
46
- const canvas = fabricCanvas.current;
47
- // If the user selects something other than the default 'select' tool
48
- if (!canvas || activeTool === "select")
49
- return;
50
- const hero = canvas.getObjects().find((obj) => obj.id === "welcome-hero");
51
- if (hero) {
52
- hero.animate({
53
- opacity: 0,
54
- top: (hero.top || 0) - 20 // Subtle "lift" animation on exit
55
- }, {
56
- duration: 500,
57
- easing: util.ease.easeInQuad,
58
- onChange: () => canvas.requestRenderAll(),
59
- onComplete: () => {
60
- canvas.remove(hero);
61
- canvas.requestRenderAll();
62
- },
63
- });
64
- }
65
- }, [activeTool]);
66
- // ── Restore helper ───────────────────────────────────────────────────────────
67
- // Sets isRestoringRef=true BEFORE loadFromJSON so that the object:added events
68
- // fired during restore do NOT push new entries onto the history stack.
69
- // Without this, undo corrupts the stack and redo breaks permanently.
70
- const restoreCanvas = (canvas, json) => {
71
- isRestoringRef.current = true; // ← block saveState during restore
50
+ }, []);
51
+ // ── Restore helper (memoized) ─────────────────────────────────────────────────
52
+ const restoreCanvas = useCallback((canvas, json) => {
53
+ isRestoringRef.current = true;
72
54
  canvas.loadFromJSON(JSON.parse(json)).then(() => {
73
55
  canvas.isDrawingMode = false;
74
56
  canvas.selection = true;
75
- canvas.forEachObject((obj) => { obj.selectable = true; obj.evented = true; });
57
+ canvas.forEachObject((obj) => {
58
+ obj.selectable = true;
59
+ obj.evented = true;
60
+ });
76
61
  canvas.renderAll();
77
62
  setActiveTool("select");
78
- isRestoringRef.current = false; // ← re-enable saveState after restore done
63
+ isRestoringRef.current = false;
79
64
  });
80
- };
81
- const handleToolClick = (toolId) => {
65
+ }, [setActiveTool, isRestoringRef]);
66
+ // ── Handle tool click (memoized) ──────────────────────────────────────────────
67
+ const handleToolClick = useCallback((toolId) => {
82
68
  const canvas = fabricCanvas.current;
83
69
  if (!canvas)
84
70
  return;
@@ -101,8 +87,9 @@ export default function WhiteboardToolbar({ fabricCanvas, isRestoringRef, onAddT
101
87
  return;
102
88
  }
103
89
  setActiveTool(toolId);
104
- };
105
- const handleImageUpload = (e) => {
90
+ }, [fabricCanvas, undo, redo, restoreCanvas, setActiveTool]);
91
+ // ── Handle image upload (memoized) ────────────────────────────────────────────
92
+ const handleImageUpload = useCallback((e) => {
106
93
  const file = e.target.files?.[0];
107
94
  if (!file || !file.type.startsWith("image/"))
108
95
  return;
@@ -110,19 +97,56 @@ export default function WhiteboardToolbar({ fabricCanvas, isRestoringRef, onAddT
110
97
  if (!canvas)
111
98
  return;
112
99
  const reader = new FileReader();
113
- reader.onload = (ev) => { const url = ev.target?.result; if (url)
114
- addImageToCanvas(canvas, url); };
100
+ reader.onload = (ev) => {
101
+ const url = ev.target?.result;
102
+ if (url)
103
+ addImageToCanvas(canvas, url);
104
+ };
115
105
  reader.readAsDataURL(file);
116
106
  e.target.value = "";
117
- };
118
- // Keyboard shortcuts
107
+ }, [fabricCanvas]);
108
+ // ── Hero animation effect ─────────────────────────────────────────────────────
109
+ useEffect(() => {
110
+ const canvas = fabricCanvas.current;
111
+ if (!canvas || activeTool === "select")
112
+ return;
113
+ const hero = canvas.getObjects().find((obj) => obj.id === "welcome-hero");
114
+ if (hero) {
115
+ hero.animate({
116
+ opacity: 0,
117
+ top: (hero.top || 0) - 20,
118
+ }, {
119
+ duration: 500,
120
+ easing: util.ease.easeInQuad,
121
+ onChange: () => canvas.requestRenderAll(),
122
+ onComplete: () => {
123
+ canvas.remove(hero);
124
+ canvas.requestRenderAll();
125
+ },
126
+ });
127
+ }
128
+ }, [activeTool, fabricCanvas]);
129
+ // ── Keyboard shortcuts effect ─────────────────────────────────────────────────
119
130
  useEffect(() => {
120
131
  const onKey = (e) => {
121
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
132
+ if (e.target instanceof HTMLInputElement ||
133
+ e.target instanceof HTMLTextAreaElement)
122
134
  return;
123
135
  const key = e.key.toLowerCase();
124
136
  const ctrl = e.ctrlKey || e.metaKey;
125
- const shortcuts = { v: "select", h: "pan", p: "pen", r: "rectangle", c: "circle", f: "frame", l: "line", a: "arrow", t: "text", i: "image", e: "eraser" };
137
+ const shortcuts = {
138
+ v: "select",
139
+ h: "pan",
140
+ p: "pen",
141
+ r: "rectangle",
142
+ c: "circle",
143
+ f: "frame",
144
+ l: "line",
145
+ a: "arrow",
146
+ t: "text",
147
+ i: "image",
148
+ e: "eraser",
149
+ };
126
150
  if (shortcuts[key] && !ctrl) {
127
151
  e.preventDefault();
128
152
  handleToolClick(shortcuts[key]);
@@ -148,8 +172,14 @@ export default function WhiteboardToolbar({ fabricCanvas, isRestoringRef, onAddT
148
172
  };
149
173
  window.addEventListener("keydown", onKey);
150
174
  return () => window.removeEventListener("keydown", onKey);
151
- }, [canUndo, canRedo, activeTool]);
152
- return (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", onChange: handleImageUpload, className: "hidden" }), _jsxs("div", { className: "fixed bottom-4 md:bottom-6 left-0 right-0 z-50 flex flex-col items-center gap-2 px-4 pointer-events-none", children: [_jsxs("div", { className: "relative w-full md:w-auto max-w-full pointer-events-auto", children: [_jsx("div", { className: "absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#0b0b0b] to-transparent pointer-events-none z-10 md:hidden rounded-l-2xl" }), _jsx("div", { className: "absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-[#0b0b0b] to-transparent pointer-events-none z-10 md:hidden rounded-r-2xl" }), _jsxs("div", { ref: scrollRef, onScroll: handleScroll, className: "flex items-center gap-1 px-2 py-2 bg-black/95 backdrop-blur-md border border-[#A1A1A1] rounded-2xl shadow-2xl overflow-x-auto md:overflow-x-visible snap-x snap-mandatory md:snap-none scrollbar-hide scroll-smooth", children: [TOOLS.map((tool, index) => (_jsxs("div", { className: "flex items-center snap-center", children: [_jsx(ToolButton, { icon: tool.icon, label: tool.label, isActive: activeTool === tool.id, onClick: () => handleToolClick(tool.id), disabled: (tool.id === "undo" && !canUndo) || (tool.id === "redo" && !canRedo), shortcut: tool.shortcut }), (index === 1 || index === 9) && (_jsx("div", { className: "hidden md:block mx-1", children: _jsx(ToolbarSeparator, {}) }))] }, tool.id))), (onAddTask || onAddDocument) && (_jsxs("div", { className: "flex items-center gap-1 snap-center pr-2", children: [_jsx("div", { className: "mx-1 flex-shrink-0", children: _jsx(ToolbarSeparator, {}) }), onAddTask && _jsx(TaskDropdown, { onAddTask: onAddTask, availableTasks: availableTasks }), onAddDocument && _jsx(DocumentDropdown, { onAddDocument: onAddDocument, availableDocuments: availableDocuments })] }))] })] }), _jsxs("div", { className: "flex gap-1.5 md:hidden", children: [_jsx("div", { className: `w-1.5 h-1.5 rounded-full transition-all duration-300 ${scrollPosition < 0.5 ? "bg-white w-6" : "bg-white/30"}` }), _jsx("div", { className: `w-1.5 h-1.5 rounded-full transition-all duration-300 ${scrollPosition >= 0.5 ? "bg-white w-6" : "bg-white/30"}` })] })] }), _jsx("style", { children: `
175
+ }, [fabricCanvas, activeTool, handleToolClick]);
176
+ // ── Memoized tool buttons ─────────────────────────────────────────────────────
177
+ const toolButtons = useMemo(() => TOOLS.map((tool, index) => (_jsxs("div", { className: "flex items-center snap-center", children: [_jsx(MemoizedToolButton, { icon: tool.icon, label: tool.label, isActive: activeTool === tool.id, onClick: () => handleToolClick(tool.id), disabled: (tool.id === "undo" && !canUndo) || (tool.id === "redo" && !canRedo), shortcut: tool.shortcut }), (index === 1 || index === 9) && (_jsx("div", { className: "hidden md:block mx-1", children: _jsx(MemoizedToolbarSeparator, {}) }))] }, tool.id))), [activeTool, canUndo, canRedo, handleToolClick]);
178
+ // ── Memoized dropdown section ─────────────────────────────────────────────────
179
+ const dropdownSection = useMemo(() => (onAddTask || onAddDocument) && (_jsxs("div", { className: "flex items-center gap-1 snap-center pr-2", children: [_jsx("div", { className: "mx-1 flex-shrink-0", children: _jsx(MemoizedToolbarSeparator, {}) }), onAddTask && (_jsx(MemoizedTaskDropdown, { onAddTask: onAddTask, availableTasks: availableTasks })), onAddDocument && (_jsx(MemoizedDocumentDropdown, { onAddDocument: onAddDocument, availableDocuments: availableDocuments }))] })), [onAddTask, onAddDocument, availableTasks, availableDocuments]);
180
+ // ── Scroll indicator visibility ───────────────────────────────────────────────
181
+ const scrollIndicators = useMemo(() => (_jsxs("div", { className: "flex gap-1.5 md:hidden", children: [_jsx("div", { className: `w-1.5 h-1.5 rounded-full transition-all duration-300 ${scrollPosition < 0.5 ? "bg-white w-6" : "bg-white/30"}` }), _jsx("div", { className: `w-1.5 h-1.5 rounded-full transition-all duration-300 ${scrollPosition >= 0.5 ? "bg-white w-6" : "bg-white/30"}` })] })), [scrollPosition]);
182
+ return (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", onChange: handleImageUpload, className: "hidden" }), _jsxs("div", { className: "fixed bottom-4 md:bottom-6 left-0 right-0 z-50 flex flex-col items-center gap-2 px-4 pointer-events-none", children: [_jsxs("div", { className: "relative w-full md:w-auto max-w-full pointer-events-auto", children: [_jsx("div", { className: "absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#0b0b0b] to-transparent pointer-events-none z-10 md:hidden rounded-l-2xl" }), _jsx("div", { className: "absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-[#0b0b0b] to-transparent pointer-events-none z-10 md:hidden rounded-r-2xl" }), _jsxs("div", { ref: scrollRef, onScroll: handleScroll, className: "flex items-center gap-1 px-2 py-2 bg-black/95 backdrop-blur-md border border-[#A1A1A1] rounded-2xl shadow-2xl overflow-x-auto md:overflow-x-visible snap-x snap-mandatory md:snap-none scrollbar-hide scroll-smooth", children: [toolButtons, dropdownSection] })] }), scrollIndicators] }), _jsx("style", { children: `
153
183
  .scrollbar-hide::-webkit-scrollbar { display: none; }
154
184
  .scrollbar-hide {
155
185
  -ms-overflow-style: none;
@@ -157,4 +187,14 @@ export default function WhiteboardToolbar({ fabricCanvas, isRestoringRef, onAddT
157
187
  -webkit-overflow-scrolling: touch;
158
188
  }
159
189
  ` })] }));
160
- }
190
+ }, (prev, next) => {
191
+ // Custom comparator for React.memo
192
+ // Return true to skip re-render, false to re-render
193
+ return (prev.fabricCanvas === next.fabricCanvas &&
194
+ prev.isRestoringRef === next.isRestoringRef &&
195
+ prev.onAddTask === next.onAddTask &&
196
+ prev.onAddDocument === next.onAddDocument &&
197
+ prev.availableTasks === next.availableTasks &&
198
+ prev.availableDocuments === next.availableDocuments &&
199
+ prev.isLoadingData === next.isLoadingData);
200
+ });
@@ -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,2CA4PvB"}
1
+ {"version":3,"file":"whiteboard-test.d.ts","sourceRoot":"","sources":["../../../src/components/whiteboard/whiteboard-test.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAkB5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGxD,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;AAED,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;IAChC,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,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;AA2HD,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,WAAW,EACX,MAAM,EACN,cAAc,EACd,cAAmB,EACnB,kBAAuB,EACvB,aAAqB,GACtB,EAAE,qBAAqB,2CAiRvB"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useCallback, useMemo, useRef, useState } from "react";
3
+ import React, { useCallback, 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";
@@ -23,6 +23,54 @@ import { usePersistence } from "../../hooks/usePersistance";
23
23
  import { ToolbarSkeleton } from "../toolbar/toolbar-skeleton/toolbar-skeleton";
24
24
  import { useCopyPaste } from "../../hooks/useCopyPaste";
25
25
  classRegistry.setClass(Frame, "frame");
26
+ // ── Memoized Toolbar wrapper (Issue 10) ───────────────────────────────────────
27
+ const MemoizedToolbarWrapper = React.memo(function ToolbarWrapper({ canvasReady, fabricCanvasRef, isRestoringRef, onAddTask, onAddDocument, availableDocuments, availableTasks, isLoadingData, }) {
28
+ return canvasReady ? (_jsx(WhiteboardToolbar, { fabricCanvas: fabricCanvasRef, isRestoringRef: isRestoringRef, onAddTask: onAddTask, onAddDocument: onAddDocument, availableDocuments: availableDocuments, availableTasks: availableTasks, isLoadingData: isLoadingData })) : (_jsx(ToolbarSkeleton, {}));
29
+ });
30
+ // ── Memoized Overlay wrapper (Issue 11) ───────────────────────────────────────
31
+ // This wrapper only updates when tasks/documents/canvasReady change
32
+ // Not on every pan/zoom frame
33
+ const MemoizedOverlayWrapper = React.memo(function OverlayWrapper({ tasks, documents, canvasReady, fabricCanvasRef, onTasksUpdate, onDocumentsUpdate, }) {
34
+ // Issue 11: Read zoom/viewport directly from fabricCanvas instead of prop drilling
35
+ // This prevents re-renders on every pan/zoom frame
36
+ const [canvasZoom, setCanvasZoom] = useState(1);
37
+ const [canvasViewport, setCanvasViewport] = useState({ x: 0, y: 0 });
38
+ const [selectionBox, setSelectionBox] = useState(null);
39
+ const [selectedCanvasObjects, setSelectedCanvasObjects] = useState([]);
40
+ // Effect to sync zoom/viewport from fabric canvas
41
+ // This keeps the overlay in sync without prop drilling
42
+ React.useEffect(() => {
43
+ const canvas = fabricCanvasRef.current;
44
+ if (!canvas)
45
+ return;
46
+ const updateViewport = () => {
47
+ const vpt = canvas.viewportTransform;
48
+ if (!vpt)
49
+ return;
50
+ // Extract zoom (scale) from viewport transform
51
+ const zoom = vpt[0];
52
+ const vpX = vpt[4];
53
+ const vpY = vpt[5];
54
+ setCanvasZoom(zoom);
55
+ setCanvasViewport({ x: vpX, y: vpY });
56
+ };
57
+ // Update on viewport change
58
+ canvas.on("viewport:changed", updateViewport);
59
+ updateViewport(); // Initial sync
60
+ return () => {
61
+ canvas.off("viewport:changed", updateViewport);
62
+ };
63
+ }, [fabricCanvasRef, canvasReady]);
64
+ return (_jsx(CanvasOverlayLayer, { tasks: tasks, documents: documents, onTasksUpdate: onTasksUpdate, onDocumentsUpdate: onDocumentsUpdate, canvasZoom: canvasZoom, canvasViewport: canvasViewport, selectionBox: selectionBox, selectedCanvasObjects: selectedCanvasObjects, fabricCanvas: fabricCanvasRef, canvasReady: canvasReady }));
65
+ }, (prev, next) => {
66
+ // Custom comparator — only re-render if these props change
67
+ return (prev.tasks === next.tasks &&
68
+ prev.documents === next.documents &&
69
+ prev.canvasReady === next.canvasReady &&
70
+ prev.fabricCanvasRef === next.fabricCanvasRef &&
71
+ prev.onTasksUpdate === next.onTasksUpdate &&
72
+ prev.onDocumentsUpdate === next.onDocumentsUpdate);
73
+ });
26
74
  export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs, availableTasks = [], availableDocuments = [], isLoadingData = false, }) {
27
75
  // Refs
28
76
  const canvasRef = useRef(null);
@@ -48,14 +96,10 @@ export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs,
48
96
  const [tasks, setTasks] = useState([]);
49
97
  const [canvasReady, setCanvasReady] = useState(false);
50
98
  const [documents, setDocuments] = useState([]);
51
- const [canvasZoom, setCanvasZoom] = useState(1);
52
- const [canvasViewport, setCanvasViewport] = useState({ x: 0, y: 0 });
53
- const [selectionBox, setSelectionBox] = useState(null);
54
- const [selectedCanvasObjects, setSelectedCanvasObjects] = useState([]);
55
99
  const MIN_ZOOM = 0.1;
56
100
  const MAX_ZOOM = 5;
57
101
  const ZOOM_STEP = 0.1;
58
- // CHANGE 2: Pass initialData to useCanvasInit (replaces localStorage.getItem)
102
+ // Initialize canvas
59
103
  useCanvasInit({
60
104
  canvasRef,
61
105
  fabricCanvasRef,
@@ -68,7 +112,7 @@ export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs,
68
112
  initialData,
69
113
  onReady: () => setCanvasReady(true),
70
114
  });
71
- // CHANGE 3: Pass onSave and saveDebounceMs to usePersistence (replaces localStorage.setItem)
115
+ // Persistence
72
116
  usePersistence({
73
117
  fabricCanvas: fabricCanvasRef,
74
118
  tasks,
@@ -121,26 +165,26 @@ export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs,
121
165
  fabricCanvas: fabricCanvasRef,
122
166
  MIN_ZOOM,
123
167
  MAX_ZOOM,
124
- canvasZoom,
125
- canvasViewport,
126
- setCanvasZoom,
127
- setCanvasViewport,
168
+ canvasZoom: 1, // Not used for state — just reference
169
+ canvasViewport: { x: 0, y: 0 },
170
+ setCanvasZoom: () => { }, // Not used anymore
171
+ setCanvasViewport: () => { }, // Not used anymore
128
172
  });
129
173
  // Pan
130
174
  usePan({
131
175
  fabricCanvas: fabricCanvasRef,
132
176
  activeTool,
133
177
  handleZoom,
134
- setCanvasViewport,
178
+ setCanvasViewport: () => { }, // Not used anymore
135
179
  });
136
180
  // Selection
137
181
  useSelection({
138
182
  fabricCanvas: fabricCanvasRef,
139
183
  activeTool,
140
- canvasZoom,
141
- canvasViewport,
142
- setSelectionBox,
143
- setSelectedCanvasObjects,
184
+ canvasZoom: 1, // Read from canvas instead
185
+ canvasViewport: { x: 0, y: 0 },
186
+ setSelectionBox: () => { }, // Not used anymore
187
+ setSelectedCanvasObjects: () => { }, // Not used anymore
144
188
  isDrawingRef,
145
189
  });
146
190
  // Text style updates
@@ -161,7 +205,7 @@ export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs,
161
205
  drawingHandlers,
162
206
  eraserHandlers,
163
207
  });
164
- // 1. Memoize static handlers
208
+ // ── Memoized static handlers ──────────────────────────────────────────────────
165
209
  const handleAddTaskFromDropdown = useCallback((taskTemplate) => {
166
210
  const canvas = fabricCanvasRef.current;
167
211
  if (!canvas)
@@ -172,7 +216,15 @@ export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs,
172
216
  const liveZoom = vpt[0];
173
217
  const cx = (canvas.getWidth() / 2 - vpt[4]) / liveZoom;
174
218
  const cy = (canvas.getHeight() / 2 - vpt[5]) / liveZoom;
175
- setTasks((prev) => [...prev, { ...taskTemplate, id: `${taskTemplate.id}-${Date.now()}`, x: cx - 150, y: cy - 60 }]);
219
+ setTasks((prev) => [
220
+ ...prev,
221
+ {
222
+ ...taskTemplate,
223
+ id: `${taskTemplate.id}-${Date.now()}`,
224
+ x: cx - 150,
225
+ y: cy - 60,
226
+ },
227
+ ]);
176
228
  }, []);
177
229
  const handleAddDocumentFromDropdown = useCallback((docTemplate) => {
178
230
  const canvas = fabricCanvasRef.current;
@@ -184,18 +236,43 @@ export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs,
184
236
  const liveZoom = vpt[0];
185
237
  const cx = (canvas.getWidth() / 2 - vpt[4]) / liveZoom;
186
238
  const cy = (canvas.getHeight() / 2 - vpt[5]) / liveZoom;
187
- setDocuments((prev) => [...prev, { ...docTemplate, id: `${docTemplate.id}-${Date.now()}`, x: cx - 160, y: cy - 80 }]);
239
+ setDocuments((prev) => [
240
+ ...prev,
241
+ {
242
+ ...docTemplate,
243
+ id: `${docTemplate.id}-${Date.now()}`,
244
+ x: cx - 160,
245
+ y: cy - 80,
246
+ },
247
+ ]);
188
248
  }, []);
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]);
249
+ // ── Zoom handlers (read from canvas directly) ────────────────────────────────
250
+ const handleZoomIn = useCallback(() => {
251
+ const canvas = fabricCanvasRef.current;
252
+ if (!canvas)
253
+ return;
254
+ const vpt = canvas.viewportTransform;
255
+ if (!vpt)
256
+ return;
257
+ const currentZoom = vpt[0];
258
+ handleZoom(currentZoom + ZOOM_STEP);
259
+ }, [handleZoom]);
260
+ const handleZoomOut = useCallback(() => {
261
+ const canvas = fabricCanvasRef.current;
262
+ if (!canvas)
263
+ return;
264
+ const vpt = canvas.viewportTransform;
265
+ if (!vpt)
266
+ return;
267
+ const currentZoom = vpt[0];
268
+ handleZoom(currentZoom - ZOOM_STEP);
269
+ }, [handleZoom]);
270
+ const handleResetZoom = useCallback(() => {
271
+ handleZoom(1);
272
+ }, [handleZoom]);
196
273
  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: {
197
274
  backgroundImage: `radial-gradient(circle, rgba(255,255,255,0.2) 1.2px, transparent 1.2px)`,
198
275
  backgroundSize: "40px 40px",
199
276
  zIndex: 0,
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 }) })] })] }) }));
277
+ } }), _jsx("canvas", { ref: canvasRef, className: "absolute inset-0", style: { zIndex: 1 } }), _jsx(MemoizedOverlayWrapper, { tasks: tasks, documents: documents, canvasReady: canvasReady, fabricCanvasRef: fabricCanvasRef, onTasksUpdate: setTasks, onDocumentsUpdate: setDocuments }), _jsxs("div", { className: "absolute inset-0 pointer-events-none", style: { zIndex: 100 }, children: [_jsx("div", { className: "pointer-events-auto", children: _jsx(MemoizedToolbarWrapper, { canvasReady: canvasReady, fabricCanvasRef: fabricCanvasRef, isRestoringRef: isRestoringRef, onAddTask: handleAddTaskFromDropdown, onAddDocument: handleAddDocumentFromDropdown, availableDocuments: availableDocuments, availableTasks: availableTasks, isLoadingData: isLoadingData }) }), _jsx("div", { className: "pointer-events-auto", children: _jsx(ToolOptionsPanel, { fabricCanvas: fabricCanvasRef }) }), _jsx("div", { className: "pointer-events-auto", children: _jsx(ZoomControls, { zoom: 1, onZoomIn: handleZoomIn, onZoomOut: handleZoomOut, onResetZoom: handleResetZoom }) })] })] }) }));
201
278
  }
package/dist/styles.css CHANGED
@@ -333,6 +333,24 @@
333
333
  .z-\[100\] {
334
334
  z-index: 100;
335
335
  }
336
+ .container {
337
+ width: 100%;
338
+ @media (width >= 40rem) {
339
+ max-width: 40rem;
340
+ }
341
+ @media (width >= 48rem) {
342
+ max-width: 48rem;
343
+ }
344
+ @media (width >= 64rem) {
345
+ max-width: 64rem;
346
+ }
347
+ @media (width >= 80rem) {
348
+ max-width: 80rem;
349
+ }
350
+ @media (width >= 96rem) {
351
+ max-width: 96rem;
352
+ }
353
+ }
336
354
  .-mx-1 {
337
355
  margin-inline: calc(var(--spacing) * -1);
338
356
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.123.0",
3
+ "version": "2.125.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",