@mhamz.01/easyflow-whiteboard 2.85.0 → 2.88.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,545 +1,470 @@
1
- "use strict";
2
- // "use client";
3
- // import { useState, useEffect, useRef } from "react";
4
- // import { FabricObject, Canvas } from "fabric";
5
- // import TaskNode from "./custom-node";
6
- // import DocumentNode from "./document-node";
7
- // // ─── Interfaces ───────────────────────────────────────────────────────────────
8
- // // THIS LAYER HAS ISSUE RELATED TO JUMP OF HTML NODES DURING DRAG/ZOOM
9
- // // THIS NEED TO BE FIXED BEFORE ANY OTHER FEATURES ARE ADDED TO THIS COMPONENT
10
- // export interface Task {
11
- // id: string;
12
- // title: string;
13
- // status: "todo" | "in-progress" | "done";
14
- // x: number;
15
- // y: number;
16
- // assignee?: string;
17
- // project?: string;
18
- // priority?: "low" | "medium" | "high";
19
- // dueDate?: string;
20
- // }
21
- // export interface Document {
22
- // id: string;
23
- // title: string;
24
- // project: string;
25
- // breadcrumb?: string[];
26
- // preview: string;
27
- // updatedAt?: string;
28
- // x: number;
29
- // y: number;
30
- // }
31
- // interface CanvasOverlayLayerProps {
32
- // tasks: Task[];
33
- // documents: Document[];
34
- // onTasksUpdate?: (tasks: Task[]) => void;
35
- // onDocumentsUpdate?: (documents: Document[]) => void;
36
- // canvasZoom?: number;
37
- // canvasViewport?: { x: number; y: number };
38
- // selectionBox?: { x1: number; y1: number; x2: number; y2: number } | null;
39
- // selectedCanvasObjects?: FabricObject[];
40
- // fabricCanvas?: React.RefObject<Canvas | null>;
41
- // canvasReady?: boolean;
42
- // }
43
- // interface DragState {
44
- // isDragging: boolean;
45
- // itemIds: string[];
46
- // startPositions: Map<string, { x: number; y: number }>;
47
- // canvasObjectsStartPos: Map<FabricObject, { left: number; top: number }>;
48
- // offsetX: number;
49
- // offsetY: number;
50
- // }
51
- // // ─── Component ────────────────────────────────────────────────────────────────
52
- // export default function CanvasOverlayLayer({
53
- // tasks,
54
- // documents,
55
- // onTasksUpdate,
56
- // onDocumentsUpdate,
57
- // canvasZoom = 1,
58
- // canvasViewport = { x: 0, y: 0 },
59
- // selectionBox = null,
60
- // selectedCanvasObjects = [],
61
- // fabricCanvas,
62
- // }: CanvasOverlayLayerProps) {
63
- // const [localTasks, setLocalTasks] = useState<Task[]>(tasks);
64
- // const [localDocuments, setLocalDocuments] = useState<Document[]>(documents);
65
- // const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
66
- // const [dragging, setDragging] = useState<{ itemIds: string[] } | null>(null);
67
- // const [canvasReady, setCanvasReady] = useState(false);
68
- // const nodeClipboardRef = useRef<{ tasks: Task[]; documents: Document[] }>({
69
- // tasks: [],
70
- // documents: [],
71
- // });
72
- // const dragStateRef = useRef<DragState>({
73
- // isDragging: false,
74
- // itemIds: [],
75
- // startPositions: new Map(),
76
- // canvasObjectsStartPos: new Map(),
77
- // offsetX: 0,
78
- // offsetY: 0,
79
- // });
80
- // const rafIdRef = useRef<number | null>(null);
81
- // const overlayRef = useRef<HTMLDivElement>(null);
82
- // const selectedIdsRef = useRef<Set<string>>(selectedIds);
83
- // selectedIdsRef.current = selectedIds;
84
- // // ── Sync props → local state ────────────────────────────────────────────────
85
- // useEffect(() => { setLocalTasks(tasks); }, [tasks]);
86
- // useEffect(() => { setLocalDocuments(documents); }, [documents]);
87
- // // effect polls until fabricCanvas.current is available:
88
- // useEffect(() => {
89
- // if (canvasReady) return;
90
- // if (fabricCanvas?.current) {
91
- // setCanvasReady(true);
92
- // return;
93
- // }
94
- // // Poll every 50ms until canvas is ready (only needed on first load)
95
- // const interval = setInterval(() => {
96
- // if (fabricCanvas?.current) {
97
- // setCanvasReady(true);
98
- // clearInterval(interval);
99
- // }
100
- // }, 50);
101
- // return () => clearInterval(interval);
102
- // }, [fabricCanvas, canvasReady]);
103
- // // ── Event Forwarding (Fixes Zooming on Nodes) ───────────────────────────────
104
- // const handleOverlayWheel = (e: React.WheelEvent) => {
105
- // if (e.ctrlKey || e.metaKey || e.shiftKey) {
106
- // const canvas = fabricCanvas?.current;
107
- // if (!canvas) return;
108
- // const nativeEvent = e.nativeEvent;
109
- // // getScenePoint handles the transformation from screen to canvas space
110
- // const scenePoint = canvas.getScenePoint(nativeEvent);
111
- // // Viewport point is simply the mouse position relative to the canvas element
112
- // const rect = canvas.getElement().getBoundingClientRect();
113
- // const viewportPoint = {
114
- // x: nativeEvent.clientX - rect.left,
115
- // y: nativeEvent.clientY - rect.top,
116
- // };
117
- // // We cast to 'any' here because we are manually triggering an internal
118
- // // event bus, and Fabric's internal types for .fire() can be overly strict.
119
- // canvas.fire("mouse:wheel", {
120
- // e: nativeEvent,
121
- // scenePoint,
122
- // viewportPoint,
123
- // } as any);
124
- // e.preventDefault();
125
- // e.stopPropagation();
126
- // }
127
- // };
128
- // useEffect(() => {
129
- // const overlayEl = overlayRef.current;
130
- // const canvas = fabricCanvas?.current;
131
- // if (!overlayEl || !canvas) return;
132
- // const handleGlobalWheel = (e: WheelEvent) => {
133
- // // Check if the user is hovering over an element that has pointer-events: auto
134
- // // (meaning they are hovering over a Task or Document)
135
- // const target = e.target as HTMLElement;
136
- // const isOverNode = target !== overlayEl;
137
- // if ((e.ctrlKey || e.metaKey) && isOverNode) {
138
- // // 1. Prevent Browser Zoom immediately
139
- // e.preventDefault();
140
- // e.stopPropagation();
141
- // // 2. Calculate coordinates for Fabric
142
- // const scenePoint = canvas.getScenePoint(e);
143
- // const rect = canvas.getElement().getBoundingClientRect();
144
- // const viewportPoint = {
145
- // x: e.clientX - rect.left,
146
- // y: e.clientY - rect.top,
147
- // };
148
- // // 3. Manually fire the event into Fabric
149
- // canvas.fire("mouse:wheel", {
150
- // e: e,
151
- // scenePoint,
152
- // viewportPoint,
153
- // } as any);
154
- // }
155
- // };
156
- // // CRITICAL: { passive: false } allows us to cancel the browser's zoom
157
- // overlayEl.addEventListener("wheel", handleGlobalWheel, { passive: false });
158
- // return () => {
159
- // overlayEl.removeEventListener("wheel", handleGlobalWheel);
160
- // };
161
- // }, [fabricCanvas, canvasZoom ,canvasReady]); // Re-bind when zoom changes to keep closure fresh
162
- // // ── Fabric Overlay Sync (Fixes Dragging from Fabric area) ──────────────────
163
- // useEffect(() => {
164
- // const canvas = fabricCanvas?.current;
165
- // if (!canvas) return;
166
- // const handleObjectMoving = (e: any) => {
167
- // const target = e.transform?.target || e.target;
168
- // if (!target) return;
169
- // const deltaX = target.left - (target._prevLeft ?? target.left);
170
- // const deltaY = target.top - (target._prevTop ?? target.top);
171
- // target._prevLeft = target.left;
172
- // target._prevTop = target.top;
173
- // if (deltaX === 0 && deltaY === 0) return;
174
- // // ── Read from ref — always fresh, never stale ──
175
- // const sel = selectedIdsRef.current;
176
- // setLocalTasks((prev) =>
177
- // prev.map((t) => sel.has(t.id) ? { ...t, x: t.x + deltaX, y: t.y + deltaY } : t)
178
- // );
179
- // setLocalDocuments((prev) =>
180
- // prev.map((d) => sel.has(d.id) ? { ...d, x: d.x + deltaX, y: d.y + deltaY } : d)
181
- // );
182
- // };
183
- // const handleMouseDown = (e: any) => {
184
- // const target = e.target;
185
- // if (target) {
186
- // target._prevLeft = target.left;
187
- // target._prevTop = target.top;
188
- // }
189
- // if (!target) {
190
- // setSelectedIds(new Set());
191
- // return;
192
- // }
193
- // // At zoom=1 with identity VPT, getActiveObject() can return null before
194
- // // Fabric updates _activeObject. Use e.transform as the primary check —
195
- // // it is populated by Fabric's hit-test regardless of zoom level.
196
- // const transformTarget = e.transform?.target;
197
- // const activeObject = canvas.getActiveObject();
198
- // const activeObjects = canvas.getActiveObjects();
199
- // const isPartOfActiveSelection =
200
- // transformTarget === target || // most reliable — direct from event
201
- // activeObject === target || // selection box group
202
- // activeObjects.includes(target); // individual object in multi-select
203
- // if (!isPartOfActiveSelection) {
204
- // setSelectedIds(new Set());
205
- // }
206
- // };
207
- // const handleSelectionCleared = () => {
208
- // setSelectedIds(new Set());
209
- // };
210
- // canvas.on("object:moving", handleObjectMoving);
211
- // canvas.on("mouse:down", handleMouseDown);
212
- // canvas.on("selection:cleared", handleSelectionCleared);
213
- // return () => {
214
- // canvas.off("object:moving", handleObjectMoving);
215
- // canvas.off("mouse:down", handleMouseDown);
216
- // canvas.off("selection:cleared", handleSelectionCleared);
217
- // };
218
- // // ── selectedIds REMOVED from deps — read via selectedIdsRef instead ──────
219
- // // Having selectedIds here caused the effect to re-register on every selection
220
- // // change, creating a new closure each time. The second drag captured a stale
221
- // // or empty selectedIds from the closure at re-registration time.
222
- // }, [canvasZoom, fabricCanvas ,canvasReady]);
223
- // // ── Helpers ─────────────────────────────────────────────────────────────────
224
- // const getItemPosition = (id: string): { x: number; y: number } | undefined => {
225
- // const task = localTasks.find((t) => t.id === id);
226
- // if (task) return { x: task.x, y: task.y };
227
- // const doc = localDocuments.find((d) => d.id === id);
228
- // if (doc) return { x: doc.x, y: doc.y };
229
- // return undefined;
230
- // };
231
- // const isItemInSelectionBox = (
232
- // x: number,
233
- // y: number,
234
- // width: number,
235
- // height: number,
236
- // box: { x1: number; y1: number; x2: number; y2: number }
237
- // ) => {
238
- // const itemX1 = x * canvasZoom + canvasViewport.x;
239
- // const itemY1 = y * canvasZoom + canvasViewport.y;
240
- // const itemX2 = itemX1 + width * canvasZoom;
241
- // const itemY2 = itemY1 + height * canvasZoom;
242
- // const boxX1 = Math.min(box.x1, box.x2);
243
- // const boxY1 = Math.min(box.y1, box.y2);
244
- // const boxX2 = Math.max(box.x1, box.x2);
245
- // const boxY2 = Math.max(box.y1, box.y2);
246
- // return !(boxX2 < itemX1 || boxX1 > itemX2 || boxY2 < itemY1 || boxY1 > itemY2);
247
- // };
248
- // // ── Selection box detection ──────────────────────────────────────────────────
249
- // useEffect(() => {
250
- // if (!selectionBox) return;
251
- // // ── O(n) single pass no sort, no join, no extra allocations ──
252
- // const newSelected = new Set<string>();
253
- // for (const task of localTasks) {
254
- // if (isItemInSelectionBox(task.x, task.y, 300, 140, selectionBox))
255
- // newSelected.add(task.id);
256
- // }
257
- // for (const doc of localDocuments) {
258
- // if (isItemInSelectionBox(doc.x, doc.y, 320, 160, selectionBox))
259
- // newSelected.add(doc.id);
260
- // }
261
- // // ── O(n) equality check: size first (fast path), then membership ──
262
- // setSelectedIds((prev) => {
263
- // if (prev.size !== newSelected.size) return newSelected;
264
- // for (const id of newSelected) {
265
- // if (!prev.has(id)) return newSelected; // found a difference, swap
266
- // }
267
- // return prev; // identical return same reference, no re-render
268
- // });
269
- // }, [selectionBox, localTasks, localDocuments, canvasZoom, canvasViewport]);
270
- // // ── Drag start (HTML Node side) ──────────────────────────────────────────────
271
- // // Helper to extract coordinates regardless of event type
272
- // const getPointerEvent = (e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent) => {
273
- // if ('touches' in e && e.touches.length > 0) return e.touches[0];
274
- // return e as { clientX: number; clientY: number };
275
- // };
276
- // const handleDragStart = (itemId: string, e: React.MouseEvent | React.TouchEvent) => {
277
- // // 1. Safety check for the Fabric instance
278
- // const canvas = fabricCanvas?.current;
279
- // if (!canvas) return;
280
- // // 2. Normalize the event (Touch vs Mouse)
281
- // if (e.cancelable) e.preventDefault();
282
- // const pointer = getPointerEvent(e);
283
- // // 3. Determine which items are being dragged
284
- // // selection update DOES NOT trigger before drag snapshot
285
- // let itemsToDrag: string[];
286
- // if (selectedIds.has(itemId)) {
287
- // itemsToDrag = Array.from(selectedIds);
288
- // } else {
289
- // itemsToDrag = [itemId];
290
- // }
291
- // // 4. Capture current World Transform (Zoom & Pan)
292
- // // We read directly from the canvas to ensure zero-frame lag
293
- // const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
294
- // const liveZoom = vpt[0];
295
- // const liveVpX = vpt[4];
296
- // const liveVpY = vpt[5];
297
- // // 5. Convert the Click Position from Screen Pixels to World Units
298
- // const clickWorldX = (pointer.clientX - liveVpX) / liveZoom;
299
- // const clickWorldY = (pointer.clientY - liveVpY) / liveZoom;
300
- // // 6. Get the clicked item's current World Position
301
- // const clickedPos = getItemPosition(itemId);
302
- // if (!clickedPos) return;
303
- // // 7. Calculate the Offset in WORLD UNITS
304
- // // This is the distance from the mouse to the node's top-left in the infinite grid.
305
- // // This value remains constant even if you zoom during the drag.
306
- // const worldOffsetX = clickWorldX - clickedPos.x;
307
- // const worldOffsetY = clickWorldY - clickedPos.y;
308
- // // 8. Snapshot starting positions for all selected HTML nodes
309
- // const startPositions = new Map<string, { x: number; y: number }>();
310
- // itemsToDrag.forEach((id) => {
311
- // const pos = getItemPosition(id);
312
- // if (pos) startPositions.set(id, pos);
313
- // });
314
- // // 9. Snapshot starting positions for all selected Fabric objects
315
- // const canvasObjectsStartPos = new Map<FabricObject, { left: number; top: number }>();
316
- // selectedCanvasObjects.forEach((obj) => {
317
- // canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
318
- // });
319
- // // 10. Commit to the ref for the requestAnimationFrame loop
320
- // dragStateRef.current = {
321
- // isDragging: true,
322
- // itemIds: itemsToDrag,
323
- // startPositions,
324
- // canvasObjectsStartPos,
325
- // offsetX: clickWorldX, // Now stored as World Units
326
- // offsetY: clickWorldY, // Now stored as World Units
327
- // };
328
- // if (!selectedIds.has(itemId) && dragStateRef.current.itemIds.length === 0) {
329
- // setSelectedIds(new Set([itemId]));
330
- // }
331
- // // 11. Trigger UI states
332
- // setDragging({ itemIds: itemsToDrag });
333
- // document.body.style.cursor = "grabbing";
334
- // document.body.style.userSelect = "none";
335
- // document.body.style.touchAction = "none";
336
- // };
337
- // // ── Drag move (HTML Node side) ───────────────────────────────────────────────
338
- // useEffect(() => {
339
- // if (!dragging) return;
340
- // // Inside the useEffect that watches [dragging, localTasks, localDocuments, fabricCanvas]
341
- // const handleMove = (e: MouseEvent | TouchEvent) => {
342
- // if (!dragStateRef.current.isDragging) return;
343
- // if (e.cancelable) e.preventDefault();
344
- // const pointer = getPointerEvent(e);
345
- // // 1. Throttle updates using requestAnimationFrame for 120Hz/144Hz screen support
346
- // if (rafIdRef.current !== null) cancelAnimationFrame(rafIdRef.current);
347
- // rafIdRef.current = requestAnimationFrame(() => {
348
- // const { itemIds, startPositions, canvasObjectsStartPos, offsetX, offsetY } = dragStateRef.current;
349
- // const canvas = fabricCanvas?.current;
350
- // if (!canvas) return;
351
- // // 2. Read the "Source of Truth" transform from the canvas
352
- // const vpt = canvas.viewportTransform!;
353
- // const liveZoom = vpt[0]; // Scale
354
- // const liveVpX = vpt[4]; // Pan X
355
- // const liveVpY = vpt[5]; // Pan Y
356
- // // 3. Convert current Mouse Screen Position → World Position
357
- // const currentWorldX = (pointer.clientX - liveVpX) / liveZoom;
358
- // const currentWorldY = (pointer.clientY - liveVpY) / liveZoom;
359
- // // 4. Calculate where the "Anchor" node should be in World Units
360
- // // (Current Mouse World - Initial World Offset from Start)
361
- // const deltaX = currentWorldX - offsetX;
362
- // const deltaY = currentWorldY - offsetY;
363
- // if (itemIds.length > 0) {
364
- // console.groupCollapsed(`Dragging Node: ${itemIds[0]}`);
365
- // console.log("Screen Mouse:", { x: pointer.clientX, y: pointer.clientY });
366
- // console.log("World Mouse:", { x: currentWorldX.toFixed(2), y: currentWorldY.toFixed(2) });
367
- // console.log("Canvas Zoom:", liveZoom.toFixed(2));
368
- // console.log("New Node Pos:", { x: deltaX.toFixed(2), y: deltaY.toFixed(2) });
369
- // console.groupEnd();
370
- // }
371
- // // 5. Calculate the Movement Delta in World Units
372
- // // We compare where the first item started vs where it is now.
373
- // const firstId = itemIds[0];
374
- // const firstStart = startPositions.get(firstId);
375
- // if (!firstStart) return;
376
- // // The real problem of task jumps
377
- // // const deltaX = deltaX - firstStart.x;
378
- // // 6. Update HTML Nodes (Batching these into one state update)
379
- // setLocalTasks((prev) =>
380
- // prev.map((t) => itemIds.includes(t.id) ? {
381
- // ...t,
382
- // x: (startPositions.get(t.id)?.x ?? t.x) + deltaX,
383
- // y: (startPositions.get(t.id)?.y ?? t.y) + deltaY,
384
- // } : t)
385
- // );
386
- // setLocalDocuments((prev) =>
387
- // prev.map((d) => itemIds.includes(d.id) ? {
388
- // ...d,
389
- // x: (startPositions.get(d.id)?.x ?? d.x) + deltaX,
390
- // y: (startPositions.get(d.id)?.y ?? d.y) + deltaY,
391
- // } : d)
392
- // );
393
- // // 7. Sync Fabric Objects (Imperative update for performance)
394
- // canvasObjectsStartPos.forEach((startPos, obj) => {
395
- // obj.set({
396
- // left: startPos.left + deltaX,
397
- // top: startPos.top + deltaY,
398
- // });
399
- // obj.setCoords(); // Required for selection/intersection accuracy
400
- // });
401
- // // 8. Single render call for all Fabric changes
402
- // canvas.requestRenderAll();
403
- // });
404
- // };
405
- // const handleEnd = () => {
406
- // if (rafIdRef.current !== null) cancelAnimationFrame(rafIdRef.current);
407
- // dragStateRef.current.isDragging = false;
408
- // setDragging(null);
409
- // document.body.style.cursor = "";
410
- // document.body.style.userSelect = "";
411
- // document.body.style.touchAction = "";
412
- // onTasksUpdate?.(localTasks);
413
- // onDocumentsUpdate?.(localDocuments);
414
- // };
415
- // window.addEventListener("mousemove", handleMove, { passive: false });
416
- // window.addEventListener("mouseup", handleEnd);
417
- // window.addEventListener("touchmove", handleMove, { passive: false });
418
- // window.addEventListener("touchend", handleEnd);
419
- // window.addEventListener("touchcancel", handleEnd);
420
- // return () => {
421
- // window.removeEventListener("mousemove", handleMove);
422
- // window.removeEventListener("mouseup", handleEnd);
423
- // window.removeEventListener("touchmove", handleMove);
424
- // window.removeEventListener("touchend", handleEnd);
425
- // window.removeEventListener("touchcancel", handleEnd);
426
- // };
427
- // }, [dragging, localTasks, localDocuments, fabricCanvas]);
428
- // // ── Selection, Status, Keyboard Logic ────────────────────────────────────────
429
- // const handleSelect = (id: string, e?: React.MouseEvent) => {
430
- // if (e?.shiftKey || e?.ctrlKey || e?.metaKey) {
431
- // setSelectedIds((prev) => {
432
- // const next = new Set(prev);
433
- // next.has(id) ? next.delete(id) : next.add(id);
434
- // return next;
435
- // });
436
- // } else {
437
- // setSelectedIds(new Set([id]));
438
- // }
439
- // };
440
- // const handleStatusChange = (taskId: string, newStatus: "todo" | "in-progress" | "done") => {
441
- // const updated = localTasks.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t));
442
- // setLocalTasks(updated);
443
- // onTasksUpdate?.(updated);
444
- // };
445
- // useEffect(() => {
446
- // const handleKeyDown = (e: KeyboardEvent) => {
447
- // // Don't trigger if typing in input
448
- // if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
449
- // // Select All
450
- // if ((e.ctrlKey || e.metaKey) && e.key === "a") {
451
- // e.preventDefault();
452
- // setSelectedIds(new Set([...localTasks.map((t) => t.id), ...localDocuments.map((d) => d.id)]));
453
- // }
454
- // // Clear selection
455
- // if (e.key === "Escape") {
456
- // setSelectedIds(new Set());
457
- // }
458
- // // ← ADD THIS: Delete selected nodes
459
- // if ((e.key === "Delete" || e.key === "Backspace") && selectedIds.size > 0) {
460
- // e.preventDefault();
461
- // const updatedTasks = localTasks.filter((t) => !selectedIds.has(t.id));
462
- // const updatedDocs = localDocuments.filter((d) => !selectedIds.has(d.id));
463
- // setLocalTasks(updatedTasks);
464
- // setLocalDocuments(updatedDocs);
465
- // setSelectedIds(new Set());
466
- // onTasksUpdate?.(updatedTasks);
467
- // onDocumentsUpdate?.(updatedDocs);
468
- // }
469
- // };
470
- // window.addEventListener("keydown", handleKeyDown);
471
- // return () => window.removeEventListener("keydown", handleKeyDown);
472
- // }, [localTasks, localDocuments, selectedIds, onTasksUpdate, onDocumentsUpdate]);
473
- // // ── Render helper ────────────────────────────────────────────────────────────
474
- // const renderItem = (id: string, x: number, y: number, children: React.ReactNode) => {
475
- // const screenX = x * canvasZoom;
476
- // const screenY = y * canvasZoom;
477
- // // 1. Detect if the user is interacting with the canvas at all
478
- // // 'dragging' is your existing state.
479
- // // You might want to pass 'isZooming' or 'isPanning' from your main canvas component here.
480
- // const isDragging = dragging?.itemIds.includes(id);
481
- // return (
482
- // <div
483
- // key={id}
484
- // className="pointer-events-auto absolute"
485
- // style={{
486
- // left: 0,
487
- // top: 0,
488
- // // 2. Use translate3d for GPU performance
489
- // transform: `translate3d(${screenX}px, ${screenY}px, 0) scale(${canvasZoom})`,
490
- // transformOrigin: "top left",
491
- // // 3. THE FIX: Remove transition entirely during any viewport change
492
- // // Any 'ease' during zoom causes the "shaking" behavior.
493
- // transition: "none",
494
- // // 4. Optimization
495
- // willChange: "transform",
496
- // zIndex: isDragging ? 1000 : 1,
497
- // }}
498
- // >
499
- // {children}
500
- // </div>
501
- // );
502
- // };
503
- // return (
504
- // <div
505
- // ref={overlayRef}
506
- // className="absolute inset-0 pointer-events-none"
507
- // style={{ zIndex: 50 }}
508
- // onWheel={handleOverlayWheel}
509
- // onClick={(e) => {
510
- // if (e.target === e.currentTarget) setSelectedIds(new Set());
511
- // }}
512
- // >
513
- // <div
514
- // className="absolute top-0 left-0 pointer-events-none"
515
- // style={{
516
- // transform: `translate(${canvasViewport.x}px, ${canvasViewport.y}px)`,
517
- // transformOrigin: "top left",
518
- // }}
519
- // >
520
- // {localTasks.map((task) =>
521
- // renderItem(task.id, task.x, task.y,
522
- // <TaskNode
523
- // {...task}
524
- // isSelected={selectedIds.has(task.id)}
525
- // onSelect={handleSelect}
526
- // onDragStart={handleDragStart}
527
- // onStatusChange={handleStatusChange}
528
- // zoom={1}
529
- // />
530
- // )
531
- // )}
532
- // {localDocuments.map((doc) =>
533
- // renderItem(doc.id, doc.x, doc.y,
534
- // <DocumentNode
535
- // {...doc}
536
- // isSelected={selectedIds.has(doc.id)}
537
- // onSelect={handleSelect}
538
- // onDragStart={handleDragStart}
539
- // />
540
- // )
541
- // )}
542
- // </div>
543
- // </div>
544
- // );
545
- // }
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect, useRef } from "react";
4
+ import TaskNode from "./custom-node";
5
+ import DocumentNode from "./document-node";
6
+ // ─── Component ────────────────────────────────────────────────────────────────
7
+ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, onDocumentsUpdate, canvasZoom = 1, canvasViewport = { x: 0, y: 0 }, selectionBox = null, selectedCanvasObjects = [], fabricCanvas, }) {
8
+ const [liveVpt, setLiveVpt] = useState({ x: canvasViewport.x, y: canvasViewport.y, zoom: canvasZoom });
9
+ const [localTasks, setLocalTasks] = useState(tasks);
10
+ const [localDocuments, setLocalDocuments] = useState(documents);
11
+ const [selectedIds, setSelectedIds] = useState(new Set());
12
+ const [dragging, setDragging] = useState(null);
13
+ const [canvasReady, setCanvasReady] = useState(false);
14
+ const dragStateRef = useRef({
15
+ isDragging: false,
16
+ itemIds: [],
17
+ startPositions: new Map(),
18
+ canvasObjectsStartPos: new Map(),
19
+ offsetX: 0,
20
+ offsetY: 0,
21
+ });
22
+ const rafIdRef = useRef(null);
23
+ const overlayRef = useRef(null);
24
+ const selectedIdsRef = useRef(selectedIds);
25
+ selectedIdsRef.current = selectedIds;
26
+ // ── Sync props → local state ────────────────────────────────────────────────
27
+ useEffect(() => { setLocalTasks(tasks); }, [tasks]);
28
+ useEffect(() => { setLocalDocuments(documents); }, [documents]);
29
+ // effect — polls until fabricCanvas.current is available:
30
+ useEffect(() => {
31
+ if (canvasReady)
32
+ return;
33
+ if (fabricCanvas?.current) {
34
+ setCanvasReady(true);
35
+ return;
36
+ }
37
+ // Poll every 50ms until canvas is ready (only needed on first load)
38
+ const interval = setInterval(() => {
39
+ if (fabricCanvas?.current) {
40
+ setCanvasReady(true);
41
+ clearInterval(interval);
42
+ }
43
+ }, 50);
44
+ return () => clearInterval(interval);
45
+ }, [fabricCanvas, canvasReady]);
46
+ // ── Event Forwarding (Fixes Zooming on Nodes) ───────────────────────────────
47
+ const handleOverlayWheel = (e) => {
48
+ if (e.ctrlKey || e.metaKey || e.shiftKey) {
49
+ const canvas = fabricCanvas?.current;
50
+ if (!canvas)
51
+ return;
52
+ const nativeEvent = e.nativeEvent;
53
+ // getScenePoint handles the transformation from screen to canvas space
54
+ const scenePoint = canvas.getScenePoint(nativeEvent);
55
+ // Viewport point is simply the mouse position relative to the canvas element
56
+ const rect = canvas.getElement().getBoundingClientRect();
57
+ const viewportPoint = {
58
+ x: nativeEvent.clientX - rect.left,
59
+ y: nativeEvent.clientY - rect.top,
60
+ };
61
+ // We cast to 'any' here because we are manually triggering an internal
62
+ // event bus, and Fabric's internal types for .fire() can be overly strict.
63
+ canvas.fire("mouse:wheel", {
64
+ e: nativeEvent,
65
+ scenePoint,
66
+ viewportPoint,
67
+ });
68
+ e.preventDefault();
69
+ e.stopPropagation();
70
+ }
71
+ };
72
+ useEffect(() => {
73
+ const overlayEl = overlayRef.current;
74
+ const canvas = fabricCanvas?.current;
75
+ if (!overlayEl || !canvas)
76
+ return;
77
+ const handleGlobalWheel = (e) => {
78
+ // Check if the user is hovering over an element that has pointer-events: auto
79
+ // (meaning they are hovering over a Task or Document)
80
+ const target = e.target;
81
+ const isOverNode = target !== overlayEl;
82
+ if ((e.ctrlKey || e.metaKey) && isOverNode) {
83
+ // 1. Prevent Browser Zoom immediately
84
+ e.preventDefault();
85
+ e.stopPropagation();
86
+ // 2. Calculate coordinates for Fabric
87
+ const scenePoint = canvas.getScenePoint(e);
88
+ const rect = canvas.getElement().getBoundingClientRect();
89
+ const viewportPoint = {
90
+ x: e.clientX - rect.left,
91
+ y: e.clientY - rect.top,
92
+ };
93
+ // 3. Manually fire the event into Fabric
94
+ canvas.fire("mouse:wheel", {
95
+ e: e,
96
+ scenePoint,
97
+ viewportPoint,
98
+ });
99
+ }
100
+ };
101
+ // CRITICAL: { passive: false } allows us to cancel the browser's zoom
102
+ overlayEl.addEventListener("wheel", handleGlobalWheel, { passive: false });
103
+ return () => {
104
+ overlayEl.removeEventListener("wheel", handleGlobalWheel);
105
+ };
106
+ }, [fabricCanvas, canvasZoom, canvasReady]); // Re-bind when zoom changes to keep closure fresh
107
+ // ── Fabric → Overlay Sync (Fixes Dragging from Fabric area) ──────────────────
108
+ useEffect(() => {
109
+ const canvas = fabricCanvas?.current;
110
+ if (!canvas)
111
+ return;
112
+ const handleObjectMoving = (e) => {
113
+ const target = e.transform?.target || e.target;
114
+ if (!target)
115
+ return;
116
+ const deltaX = target.left - (target._prevLeft ?? target.left);
117
+ const deltaY = target.top - (target._prevTop ?? target.top);
118
+ target._prevLeft = target.left;
119
+ target._prevTop = target.top;
120
+ if (deltaX === 0 && deltaY === 0)
121
+ return;
122
+ // ── Read from ref — always fresh, never stale ──
123
+ const sel = selectedIdsRef.current;
124
+ setLocalTasks((prev) => prev.map((t) => sel.has(t.id) ? { ...t, x: t.x + deltaX, y: t.y + deltaY } : t));
125
+ setLocalDocuments((prev) => prev.map((d) => sel.has(d.id) ? { ...d, x: d.x + deltaX, y: d.y + deltaY } : d));
126
+ };
127
+ const handleMouseDown = (e) => {
128
+ const target = e.target;
129
+ if (target) {
130
+ target._prevLeft = target.left;
131
+ target._prevTop = target.top;
132
+ }
133
+ if (!target) {
134
+ setSelectedIds(new Set());
135
+ return;
136
+ }
137
+ // At zoom=1 with identity VPT, getActiveObject() can return null before
138
+ // Fabric updates _activeObject. Use e.transform as the primary check —
139
+ // it is populated by Fabric's hit-test regardless of zoom level.
140
+ const transformTarget = e.transform?.target;
141
+ const activeObject = canvas.getActiveObject();
142
+ const activeObjects = canvas.getActiveObjects();
143
+ const isPartOfActiveSelection = transformTarget === target || // most reliable — direct from event
144
+ activeObject === target || // selection box group
145
+ activeObjects.includes(target); // individual object in multi-select
146
+ if (!isPartOfActiveSelection) {
147
+ setSelectedIds(new Set());
148
+ }
149
+ };
150
+ const handleSelectionCleared = () => {
151
+ setSelectedIds(new Set());
152
+ };
153
+ canvas.on("object:moving", handleObjectMoving);
154
+ canvas.on("mouse:down", handleMouseDown);
155
+ canvas.on("selection:cleared", handleSelectionCleared);
156
+ return () => {
157
+ canvas.off("object:moving", handleObjectMoving);
158
+ canvas.off("mouse:down", handleMouseDown);
159
+ canvas.off("selection:cleared", handleSelectionCleared);
160
+ };
161
+ // ── selectedIds REMOVED from deps read via selectedIdsRef instead ──────
162
+ // Having selectedIds here caused the effect to re-register on every selection
163
+ // change, creating a new closure each time. The second drag captured a stale
164
+ // or empty selectedIds from the closure at re-registration time.
165
+ }, [canvasZoom, fabricCanvas, canvasReady]);
166
+ // 2. Sync directly with Fabric's render heartbeat
167
+ useEffect(() => {
168
+ const canvas = fabricCanvas?.current;
169
+ if (!canvas)
170
+ return;
171
+ const sync = () => {
172
+ const vpt = canvas.viewportTransform;
173
+ if (vpt) {
174
+ setLiveVpt({ x: vpt[4], y: vpt[5], zoom: vpt[0] });
175
+ }
176
+ };
177
+ canvas.on("after:render", sync);
178
+ return () => { canvas.off("after:render", sync); };
179
+ }, [fabricCanvas, canvasReady]);
180
+ // ── Helpers ─────────────────────────────────────────────────────────────────
181
+ const getItemPosition = (id) => {
182
+ const task = localTasks.find((t) => t.id === id);
183
+ if (task)
184
+ return { x: task.x, y: task.y };
185
+ const doc = localDocuments.find((d) => d.id === id);
186
+ if (doc)
187
+ return { x: doc.x, y: doc.y };
188
+ return undefined;
189
+ };
190
+ const isItemInSelectionBox = (x, y, width, height, box) => {
191
+ const itemX1 = x * canvasZoom + canvasViewport.x;
192
+ const itemY1 = y * canvasZoom + canvasViewport.y;
193
+ const itemX2 = itemX1 + width * canvasZoom;
194
+ const itemY2 = itemY1 + height * canvasZoom;
195
+ const boxX1 = Math.min(box.x1, box.x2);
196
+ const boxY1 = Math.min(box.y1, box.y2);
197
+ const boxX2 = Math.max(box.x1, box.x2);
198
+ const boxY2 = Math.max(box.y1, box.y2);
199
+ return !(boxX2 < itemX1 || boxX1 > itemX2 || boxY2 < itemY1 || boxY1 > itemY2);
200
+ };
201
+ // ── Selection box detection ──────────────────────────────────────────────────
202
+ useEffect(() => {
203
+ if (!selectionBox)
204
+ return;
205
+ // ── O(n) single pass — no sort, no join, no extra allocations ──
206
+ const newSelected = new Set();
207
+ for (const task of localTasks) {
208
+ if (isItemInSelectionBox(task.x, task.y, 300, 140, selectionBox))
209
+ newSelected.add(task.id);
210
+ }
211
+ for (const doc of localDocuments) {
212
+ if (isItemInSelectionBox(doc.x, doc.y, 320, 160, selectionBox))
213
+ newSelected.add(doc.id);
214
+ }
215
+ // ── O(n) equality check: size first (fast path), then membership ──
216
+ setSelectedIds((prev) => {
217
+ if (prev.size !== newSelected.size)
218
+ return newSelected;
219
+ for (const id of newSelected) {
220
+ if (!prev.has(id))
221
+ return newSelected; // found a difference, swap
222
+ }
223
+ return prev; // identical return same reference, no re-render
224
+ });
225
+ }, [selectionBox, localTasks, localDocuments, canvasZoom, canvasViewport]);
226
+ // ── Drag start (HTML Node side) ──────────────────────────────────────────────
227
+ // Helper to extract coordinates regardless of event type
228
+ const getPointerEvent = (e) => {
229
+ if ('touches' in e && e.touches.length > 0)
230
+ return e.touches[0];
231
+ return e;
232
+ };
233
+ const handleDragStart = (itemId, e) => {
234
+ // 1. Safety check for the Fabric instance
235
+ const canvas = fabricCanvas?.current;
236
+ if (!canvas)
237
+ return;
238
+ // 2. Normalize the event (Touch vs Mouse)
239
+ if (e.cancelable)
240
+ e.preventDefault();
241
+ const pointer = getPointerEvent(e);
242
+ // 3. Determine which items are being dragged
243
+ // selection update DOES NOT trigger before drag snapshot
244
+ let itemsToDrag;
245
+ if (selectedIds.has(itemId)) {
246
+ itemsToDrag = Array.from(selectedIds);
247
+ }
248
+ else {
249
+ itemsToDrag = [itemId];
250
+ }
251
+ // 4. Capture current World Transform (Zoom & Pan)
252
+ // We read directly from the canvas to ensure zero-frame lag
253
+ const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
254
+ const liveZoom = vpt[0];
255
+ const liveVpX = vpt[4];
256
+ const liveVpY = vpt[5];
257
+ // 5. Convert the Click Position from Screen Pixels to World Units
258
+ const clickWorldX = (pointer.clientX - liveVpX) / liveZoom;
259
+ const clickWorldY = (pointer.clientY - liveVpY) / liveZoom;
260
+ // 6. Get the clicked item's current World Position
261
+ const clickedPos = getItemPosition(itemId);
262
+ if (!clickedPos)
263
+ return;
264
+ // 7. Calculate the Offset in WORLD UNITS
265
+ // This is the distance from the mouse to the node's top-left in the infinite grid.
266
+ // This value remains constant even if you zoom during the drag.
267
+ const worldOffsetX = clickWorldX - clickedPos.x;
268
+ const worldOffsetY = clickWorldY - clickedPos.y;
269
+ // 8. Snapshot starting positions for all selected HTML nodes
270
+ const startPositions = new Map();
271
+ itemsToDrag.forEach((id) => {
272
+ const pos = getItemPosition(id);
273
+ if (pos)
274
+ startPositions.set(id, pos);
275
+ });
276
+ // 9. Snapshot starting positions for all selected Fabric objects
277
+ const canvasObjectsStartPos = new Map();
278
+ selectedCanvasObjects.forEach((obj) => {
279
+ canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
280
+ });
281
+ // 10. Commit to the ref for the requestAnimationFrame loop
282
+ dragStateRef.current = {
283
+ isDragging: true,
284
+ itemIds: itemsToDrag,
285
+ startPositions,
286
+ canvasObjectsStartPos,
287
+ offsetX: clickWorldX, // Now stored as World Units
288
+ offsetY: clickWorldY, // Now stored as World Units
289
+ };
290
+ if (!selectedIds.has(itemId) && dragStateRef.current.itemIds.length === 0) {
291
+ setSelectedIds(new Set([itemId]));
292
+ }
293
+ // 11. Trigger UI states
294
+ setDragging({ itemIds: itemsToDrag });
295
+ document.body.style.cursor = "grabbing";
296
+ document.body.style.userSelect = "none";
297
+ document.body.style.touchAction = "none";
298
+ };
299
+ // ── Drag move (HTML Node side) ───────────────────────────────────────────────
300
+ useEffect(() => {
301
+ if (!dragging)
302
+ return;
303
+ // Inside the useEffect that watches [dragging, localTasks, localDocuments, fabricCanvas]
304
+ const handleMove = (e) => {
305
+ if (!dragStateRef.current.isDragging)
306
+ return;
307
+ if (e.cancelable)
308
+ e.preventDefault();
309
+ const pointer = getPointerEvent(e);
310
+ // 1. Throttle updates using requestAnimationFrame for 120Hz/144Hz screen support
311
+ if (rafIdRef.current !== null)
312
+ cancelAnimationFrame(rafIdRef.current);
313
+ rafIdRef.current = requestAnimationFrame(() => {
314
+ const { itemIds, startPositions, canvasObjectsStartPos, offsetX, offsetY } = dragStateRef.current;
315
+ const canvas = fabricCanvas?.current;
316
+ if (!canvas)
317
+ return;
318
+ // 2. Read the "Source of Truth" transform from the canvas
319
+ const vpt = canvas.viewportTransform;
320
+ const liveZoom = vpt[0]; // Scale
321
+ const liveVpX = vpt[4]; // Pan X
322
+ const liveVpY = vpt[5]; // Pan Y
323
+ // 3. Convert current Mouse Screen Position → World Position
324
+ const currentWorldX = (pointer.clientX - liveVpX) / liveZoom;
325
+ const currentWorldY = (pointer.clientY - liveVpY) / liveZoom;
326
+ // 4. Calculate where the "Anchor" node should be in World Units
327
+ // (Current Mouse World - Initial World Offset from Start)
328
+ const deltaX = currentWorldX - offsetX;
329
+ const deltaY = currentWorldY - offsetY;
330
+ if (itemIds.length > 0) {
331
+ console.groupCollapsed(`Dragging Node: ${itemIds[0]}`);
332
+ console.log("Screen Mouse:", { x: pointer.clientX, y: pointer.clientY });
333
+ console.log("World Mouse:", { x: currentWorldX.toFixed(2), y: currentWorldY.toFixed(2) });
334
+ console.log("Canvas Zoom:", liveZoom.toFixed(2));
335
+ console.log("New Node Pos:", { x: deltaX.toFixed(2), y: deltaY.toFixed(2) });
336
+ console.groupEnd();
337
+ }
338
+ // 5. Calculate the Movement Delta in World Units
339
+ // We compare where the first item started vs where it is now.
340
+ const firstId = itemIds[0];
341
+ const firstStart = startPositions.get(firstId);
342
+ if (!firstStart)
343
+ return;
344
+ // The real problem of task jumps
345
+ // const deltaX = deltaX - firstStart.x;
346
+ // 6. Update HTML Nodes (Batching these into one state update)
347
+ setLocalTasks((prev) => prev.map((t) => itemIds.includes(t.id) ? {
348
+ ...t,
349
+ x: (startPositions.get(t.id)?.x ?? t.x) + deltaX,
350
+ y: (startPositions.get(t.id)?.y ?? t.y) + deltaY,
351
+ } : t));
352
+ setLocalDocuments((prev) => prev.map((d) => itemIds.includes(d.id) ? {
353
+ ...d,
354
+ x: (startPositions.get(d.id)?.x ?? d.x) + deltaX,
355
+ y: (startPositions.get(d.id)?.y ?? d.y) + deltaY,
356
+ } : d));
357
+ // 7. Sync Fabric Objects (Imperative update for performance)
358
+ canvasObjectsStartPos.forEach((startPos, obj) => {
359
+ obj.set({
360
+ left: startPos.left + deltaX,
361
+ top: startPos.top + deltaY,
362
+ });
363
+ obj.setCoords(); // Required for selection/intersection accuracy
364
+ });
365
+ // 8. Single render call for all Fabric changes
366
+ canvas.requestRenderAll();
367
+ });
368
+ };
369
+ const handleEnd = () => {
370
+ if (rafIdRef.current !== null)
371
+ cancelAnimationFrame(rafIdRef.current);
372
+ dragStateRef.current.isDragging = false;
373
+ setDragging(null);
374
+ document.body.style.cursor = "";
375
+ document.body.style.userSelect = "";
376
+ document.body.style.touchAction = "";
377
+ onTasksUpdate?.(localTasks);
378
+ onDocumentsUpdate?.(localDocuments);
379
+ };
380
+ window.addEventListener("mousemove", handleMove, { passive: false });
381
+ window.addEventListener("mouseup", handleEnd);
382
+ window.addEventListener("touchmove", handleMove, { passive: false });
383
+ window.addEventListener("touchend", handleEnd);
384
+ window.addEventListener("touchcancel", handleEnd);
385
+ return () => {
386
+ window.removeEventListener("mousemove", handleMove);
387
+ window.removeEventListener("mouseup", handleEnd);
388
+ window.removeEventListener("touchmove", handleMove);
389
+ window.removeEventListener("touchend", handleEnd);
390
+ window.removeEventListener("touchcancel", handleEnd);
391
+ };
392
+ }, [dragging, localTasks, localDocuments, fabricCanvas]);
393
+ // ── Selection, Status, Keyboard Logic ────────────────────────────────────────
394
+ const handleSelect = (id, e) => {
395
+ if (e?.shiftKey || e?.ctrlKey || e?.metaKey) {
396
+ setSelectedIds((prev) => {
397
+ const next = new Set(prev);
398
+ next.has(id) ? next.delete(id) : next.add(id);
399
+ return next;
400
+ });
401
+ }
402
+ else {
403
+ setSelectedIds(new Set([id]));
404
+ }
405
+ };
406
+ const handleStatusChange = (taskId, newStatus) => {
407
+ const updated = localTasks.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t));
408
+ setLocalTasks(updated);
409
+ onTasksUpdate?.(updated);
410
+ };
411
+ useEffect(() => {
412
+ const handleKeyDown = (e) => {
413
+ // Don't trigger if typing in input
414
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
415
+ return;
416
+ // Select All
417
+ if ((e.ctrlKey || e.metaKey) && e.key === "a") {
418
+ e.preventDefault();
419
+ setSelectedIds(new Set([...localTasks.map((t) => t.id), ...localDocuments.map((d) => d.id)]));
420
+ }
421
+ // Clear selection
422
+ if (e.key === "Escape") {
423
+ setSelectedIds(new Set());
424
+ }
425
+ // ← ADD THIS: Delete selected nodes
426
+ if ((e.key === "Delete" || e.key === "Backspace") && selectedIds.size > 0) {
427
+ e.preventDefault();
428
+ const updatedTasks = localTasks.filter((t) => !selectedIds.has(t.id));
429
+ const updatedDocs = localDocuments.filter((d) => !selectedIds.has(d.id));
430
+ setLocalTasks(updatedTasks);
431
+ setLocalDocuments(updatedDocs);
432
+ setSelectedIds(new Set());
433
+ onTasksUpdate?.(updatedTasks);
434
+ onDocumentsUpdate?.(updatedDocs);
435
+ }
436
+ };
437
+ window.addEventListener("keydown", handleKeyDown);
438
+ return () => window.removeEventListener("keydown", handleKeyDown);
439
+ }, [localTasks, localDocuments, selectedIds, onTasksUpdate, onDocumentsUpdate]);
440
+ // ── Render helper ────────────────────────────────────────────────────────────
441
+ const renderItem = (id, x, y, children) => {
442
+ const screenX = x * liveVpt.zoom;
443
+ const screenY = y * liveVpt.zoom;
444
+ const isDragging = dragging?.itemIds.includes(id);
445
+ // 1. Detect if the user is interacting with the canvas at all
446
+ // 'dragging' is your existing state.
447
+ // You might want to pass 'isZooming' or 'isPanning' from your main canvas component here.
448
+ return (_jsx("div", { className: "pointer-events-auto absolute", style: {
449
+ left: 0,
450
+ top: 0,
451
+ // 2. Use translate3d for GPU performance
452
+ transform: `translate3d(${screenX}px, ${screenY}px, 0) scale(${liveVpt.zoom})`,
453
+ transformOrigin: "top left",
454
+ // 3. THE FIX: Remove transition entirely during any viewport change
455
+ // Any 'ease' during zoom causes the "shaking" behavior.
456
+ transition: "none",
457
+ // 4. Optimization
458
+ willChange: "transform",
459
+ zIndex: isDragging ? 1000 : 1,
460
+ }, children: children }, id));
461
+ };
462
+ return (_jsx("div", { ref: overlayRef, className: "absolute inset-0 pointer-events-none", style: { zIndex: 50 }, onWheel: handleOverlayWheel, onClick: (e) => {
463
+ if (e.target === e.currentTarget)
464
+ setSelectedIds(new Set());
465
+ }, children: _jsxs("div", { className: "absolute top-0 left-0 pointer-events-none", style: {
466
+ transform: `translate3d(${liveVpt.x}px, ${liveVpt.y}px, 0)`,
467
+ transformOrigin: "top left",
468
+ willChange: "transform",
469
+ }, 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 })))] }) }));
470
+ }