@mhamz.01/easyflow-whiteboard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/node/custom-node-overlay-layer.d.ts +44 -0
- package/dist/components/node/custom-node-overlay-layer.d.ts.map +1 -0
- package/dist/components/node/custom-node-overlay-layer.js +353 -0
- package/dist/components/node/custom-node.d.ts +17 -0
- package/dist/components/node/custom-node.d.ts.map +1 -0
- package/dist/components/node/custom-node.js +63 -0
- package/dist/components/node/document-node.d.ts +14 -0
- package/dist/components/node/document-node.d.ts.map +1 -0
- package/dist/components/node/document-node.js +58 -0
- package/dist/components/toolbar/document-dropdown.d.ts +14 -0
- package/dist/components/toolbar/document-dropdown.d.ts.map +1 -0
- package/dist/components/toolbar/document-dropdown.js +66 -0
- package/dist/components/toolbar/options/arrow-options.d.ts +8 -0
- package/dist/components/toolbar/options/arrow-options.d.ts.map +1 -0
- package/dist/components/toolbar/options/arrow-options.js +109 -0
- package/dist/components/toolbar/options/erase-option.d.ts +2 -0
- package/dist/components/toolbar/options/erase-option.d.ts.map +1 -0
- package/dist/components/toolbar/options/erase-option.js +20 -0
- package/dist/components/toolbar/options/image-options.d.ts +2 -0
- package/dist/components/toolbar/options/image-options.d.ts.map +1 -0
- package/dist/components/toolbar/options/image-options.js +10 -0
- package/dist/components/toolbar/options/line-options.d.ts +2 -0
- package/dist/components/toolbar/options/line-options.d.ts.map +1 -0
- package/dist/components/toolbar/options/line-options.js +46 -0
- package/dist/components/toolbar/options/pen-option.d.ts +2 -0
- package/dist/components/toolbar/options/pen-option.d.ts.map +1 -0
- package/dist/components/toolbar/options/pen-option.js +53 -0
- package/dist/components/toolbar/options/shape-option.d.ts +6 -0
- package/dist/components/toolbar/options/shape-option.d.ts.map +1 -0
- package/dist/components/toolbar/options/shape-option.js +58 -0
- package/dist/components/toolbar/options/text-option.d.ts +2 -0
- package/dist/components/toolbar/options/text-option.d.ts.map +1 -0
- package/dist/components/toolbar/options/text-option.js +73 -0
- package/dist/components/toolbar/task-dropdown.d.ts +15 -0
- package/dist/components/toolbar/task-dropdown.d.ts.map +1 -0
- package/dist/components/toolbar/task-dropdown.js +85 -0
- package/dist/components/toolbar/toolbar-button.d.ts +12 -0
- package/dist/components/toolbar/toolbar-button.d.ts.map +1 -0
- package/dist/components/toolbar/toolbar-button.js +8 -0
- package/dist/components/toolbar/toolbar-seperator.d.ts +6 -0
- package/dist/components/toolbar/toolbar-seperator.d.ts.map +1 -0
- package/dist/components/toolbar/toolbar-seperator.js +5 -0
- package/dist/components/toolbar/tooloptions-panel.d.ts +8 -0
- package/dist/components/toolbar/tooloptions-panel.d.ts.map +1 -0
- package/dist/components/toolbar/tooloptions-panel.js +88 -0
- package/dist/components/toolbar/whiteboard-toolbar.d.ts +28 -0
- package/dist/components/toolbar/whiteboard-toolbar.d.ts.map +1 -0
- package/dist/components/toolbar/whiteboard-toolbar.js +160 -0
- package/dist/components/ui/dropdown-menu.d.ts +26 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +51 -0
- package/dist/components/ui/label.d.ts +5 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +8 -0
- package/dist/components/ui/slider.d.ts +5 -0
- package/dist/components/ui/slider.d.ts.map +1 -0
- package/dist/components/ui/slider.js +14 -0
- package/dist/components/whiteboard/whiteboard-test.d.ts +2 -0
- package/dist/components/whiteboard/whiteboard-test.d.ts.map +1 -0
- package/dist/components/whiteboard/whiteboard-test.js +207 -0
- package/dist/components/whiteboard/whiteboard.d.ts +1 -0
- package/dist/components/whiteboard/whiteboard.d.ts.map +1 -0
- package/dist/components/whiteboard/whiteboard.js +911 -0
- package/dist/components/zoomcontrol/zoom-control.d.ts +9 -0
- package/dist/components/zoomcontrol/zoom-control.d.ts.map +1 -0
- package/dist/components/zoomcontrol/zoom-control.js +7 -0
- package/dist/hooks/useCanvasInit.d.ts +15 -0
- package/dist/hooks/useCanvasInit.d.ts.map +1 -0
- package/dist/hooks/useCanvasInit.js +89 -0
- package/dist/hooks/useDrawing.d.ts +23 -0
- package/dist/hooks/useDrawing.d.ts.map +1 -0
- package/dist/hooks/useDrawing.js +142 -0
- package/dist/hooks/useEraser.d.ts +27 -0
- package/dist/hooks/useEraser.d.ts.map +1 -0
- package/dist/hooks/useEraser.js +143 -0
- package/dist/hooks/useLiveUpdate.d.ts +9 -0
- package/dist/hooks/useLiveUpdate.d.ts.map +1 -0
- package/dist/hooks/useLiveUpdate.js +63 -0
- package/dist/hooks/useMouseHandlers.d.ts +25 -0
- package/dist/hooks/useMouseHandlers.d.ts.map +1 -0
- package/dist/hooks/useMouseHandlers.js +44 -0
- package/dist/hooks/usePan.d.ts +17 -0
- package/dist/hooks/usePan.d.ts.map +1 -0
- package/dist/hooks/usePan.js +80 -0
- package/dist/hooks/usePersistance.d.ts +13 -0
- package/dist/hooks/usePersistance.d.ts.map +1 -0
- package/dist/hooks/usePersistance.js +79 -0
- package/dist/hooks/useSelection.d.ts +21 -0
- package/dist/hooks/useSelection.d.ts.map +1 -0
- package/dist/hooks/useSelection.js +142 -0
- package/dist/hooks/useTextStyle.d.ts +9 -0
- package/dist/hooks/useTextStyle.d.ts.map +1 -0
- package/dist/hooks/useTextStyle.js +32 -0
- package/dist/hooks/useToolManager.d.ts +15 -0
- package/dist/hooks/useToolManager.d.ts.map +1 -0
- package/dist/hooks/useToolManager.js +115 -0
- package/dist/hooks/useZoom.d.ts +25 -0
- package/dist/hooks/useZoom.d.ts.map +1 -0
- package/dist/hooks/useZoom.js +133 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/lib/eraser-brush.d.ts +1 -0
- package/dist/lib/eraser-brush.d.ts.map +1 -0
- package/dist/lib/eraser-brush.js +21 -0
- package/dist/lib/fabric-arrow.d.ts +16 -0
- package/dist/lib/fabric-arrow.d.ts.map +1 -0
- package/dist/lib/fabric-arrow.js +50 -0
- package/dist/lib/fabric-bidirectional-arrow.d.ts +20 -0
- package/dist/lib/fabric-bidirectional-arrow.d.ts.map +1 -0
- package/dist/lib/fabric-bidirectional-arrow.js +65 -0
- package/dist/lib/fabric-frame.d.ts +7 -0
- package/dist/lib/fabric-frame.d.ts.map +1 -0
- package/dist/lib/fabric-frame.js +25 -0
- package/dist/lib/fabric-utils.d.ts +30 -0
- package/dist/lib/fabric-utils.d.ts.map +1 -0
- package/dist/lib/fabric-utils.js +273 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +5 -0
- package/dist/store/whiteboard-store.d.ts +99 -0
- package/dist/store/whiteboard-store.d.ts.map +1 -0
- package/dist/store/whiteboard-store.js +137 -0
- package/dist/types/canvas-node.d.ts +24 -0
- package/dist/types/canvas-node.d.ts.map +1 -0
- package/dist/types/canvas-node.js +1 -0
- package/package.json +34 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { FabricObject, Canvas } from "fabric";
|
|
2
|
+
export interface Task {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
status: "todo" | "in-progress" | "done";
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
assignee?: string;
|
|
9
|
+
project?: string;
|
|
10
|
+
priority?: "low" | "medium" | "high";
|
|
11
|
+
dueDate?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Document {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
project: string;
|
|
17
|
+
breadcrumb?: string[];
|
|
18
|
+
preview: string;
|
|
19
|
+
updatedAt?: string;
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
}
|
|
23
|
+
interface CanvasOverlayLayerProps {
|
|
24
|
+
tasks: Task[];
|
|
25
|
+
documents: Document[];
|
|
26
|
+
onTasksUpdate?: (tasks: Task[]) => void;
|
|
27
|
+
onDocumentsUpdate?: (documents: Document[]) => void;
|
|
28
|
+
canvasZoom?: number;
|
|
29
|
+
canvasViewport?: {
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
};
|
|
33
|
+
selectionBox?: {
|
|
34
|
+
x1: number;
|
|
35
|
+
y1: number;
|
|
36
|
+
x2: number;
|
|
37
|
+
y2: number;
|
|
38
|
+
} | null;
|
|
39
|
+
selectedCanvasObjects?: FabricObject[];
|
|
40
|
+
fabricCanvas?: React.RefObject<Canvas | null>;
|
|
41
|
+
}
|
|
42
|
+
export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, onDocumentsUpdate, canvasZoom, canvasViewport, selectionBox, selectedCanvasObjects, fabricCanvas, }: CanvasOverlayLayerProps): import("react/jsx-runtime").JSX.Element;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=custom-node-overlay-layer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom-node-overlay-layer.d.ts","sourceRoot":"","sources":["../../../src/components/node/custom-node-overlay-layer.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAM9C,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACxC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,UAAU,uBAAuB;IAC/B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACxC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,YAAY,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzE,qBAAqB,CAAC,EAAE,YAAY,EAAE,CAAC;IACvC,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;AAaD,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EACzC,KAAK,EACL,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,UAAc,EACd,cAA+B,EAC/B,YAAmB,EACnB,qBAA0B,EAC1B,YAAY,GACb,EAAE,uBAAuB,2CA+czB"}
|
|
@@ -0,0 +1,353 @@
|
|
|
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 [localTasks, setLocalTasks] = useState(tasks);
|
|
9
|
+
const [localDocuments, setLocalDocuments] = useState(documents);
|
|
10
|
+
const [selectedIds, setSelectedIds] = useState(new Set());
|
|
11
|
+
const [dragging, setDragging] = useState(null);
|
|
12
|
+
const dragStateRef = useRef({
|
|
13
|
+
isDragging: false,
|
|
14
|
+
itemIds: [],
|
|
15
|
+
startPositions: new Map(),
|
|
16
|
+
canvasObjectsStartPos: new Map(),
|
|
17
|
+
offsetX: 0,
|
|
18
|
+
offsetY: 0,
|
|
19
|
+
});
|
|
20
|
+
const rafIdRef = useRef(null);
|
|
21
|
+
const overlayRef = useRef(null);
|
|
22
|
+
// ── Sync props → local state ────────────────────────────────────────────────
|
|
23
|
+
useEffect(() => { setLocalTasks(tasks); }, [tasks]);
|
|
24
|
+
useEffect(() => { setLocalDocuments(documents); }, [documents]);
|
|
25
|
+
// ── Event Forwarding (Fixes Zooming on Nodes) ───────────────────────────────
|
|
26
|
+
const handleOverlayWheel = (e) => {
|
|
27
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey) {
|
|
28
|
+
const canvas = fabricCanvas?.current;
|
|
29
|
+
if (!canvas)
|
|
30
|
+
return;
|
|
31
|
+
const nativeEvent = e.nativeEvent;
|
|
32
|
+
// getScenePoint handles the transformation from screen to canvas space
|
|
33
|
+
const scenePoint = canvas.getScenePoint(nativeEvent);
|
|
34
|
+
// Viewport point is simply the mouse position relative to the canvas element
|
|
35
|
+
const rect = canvas.getElement().getBoundingClientRect();
|
|
36
|
+
const viewportPoint = {
|
|
37
|
+
x: nativeEvent.clientX - rect.left,
|
|
38
|
+
y: nativeEvent.clientY - rect.top,
|
|
39
|
+
};
|
|
40
|
+
// We cast to 'any' here because we are manually triggering an internal
|
|
41
|
+
// event bus, and Fabric's internal types for .fire() can be overly strict.
|
|
42
|
+
canvas.fire("mouse:wheel", {
|
|
43
|
+
e: nativeEvent,
|
|
44
|
+
scenePoint,
|
|
45
|
+
viewportPoint,
|
|
46
|
+
});
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
e.stopPropagation();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const overlayEl = overlayRef.current;
|
|
53
|
+
const canvas = fabricCanvas?.current;
|
|
54
|
+
if (!overlayEl || !canvas)
|
|
55
|
+
return;
|
|
56
|
+
const handleGlobalWheel = (e) => {
|
|
57
|
+
// Check if the user is hovering over an element that has pointer-events: auto
|
|
58
|
+
// (meaning they are hovering over a Task or Document)
|
|
59
|
+
const target = e.target;
|
|
60
|
+
const isOverNode = target !== overlayEl;
|
|
61
|
+
if ((e.ctrlKey || e.metaKey) && isOverNode) {
|
|
62
|
+
// 1. Prevent Browser Zoom immediately
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
// 2. Calculate coordinates for Fabric
|
|
66
|
+
const scenePoint = canvas.getScenePoint(e);
|
|
67
|
+
const rect = canvas.getElement().getBoundingClientRect();
|
|
68
|
+
const viewportPoint = {
|
|
69
|
+
x: e.clientX - rect.left,
|
|
70
|
+
y: e.clientY - rect.top,
|
|
71
|
+
};
|
|
72
|
+
// 3. Manually fire the event into Fabric
|
|
73
|
+
canvas.fire("mouse:wheel", {
|
|
74
|
+
e: e,
|
|
75
|
+
scenePoint,
|
|
76
|
+
viewportPoint,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
// CRITICAL: { passive: false } allows us to cancel the browser's zoom
|
|
81
|
+
overlayEl.addEventListener("wheel", handleGlobalWheel, { passive: false });
|
|
82
|
+
return () => {
|
|
83
|
+
overlayEl.removeEventListener("wheel", handleGlobalWheel);
|
|
84
|
+
};
|
|
85
|
+
}, [fabricCanvas, canvasZoom]); // Re-bind when zoom changes to keep closure fresh
|
|
86
|
+
// ── Fabric → Overlay Sync (Fixes Dragging from Fabric area) ──────────────────
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const canvas = fabricCanvas?.current;
|
|
89
|
+
if (!canvas)
|
|
90
|
+
return;
|
|
91
|
+
const handleObjectMoving = (e) => {
|
|
92
|
+
const target = e.transform?.target || e.target;
|
|
93
|
+
if (!target)
|
|
94
|
+
return;
|
|
95
|
+
// 1. Calculate delta in raw Scene Coordinates
|
|
96
|
+
// We do NOT divide by zoom here because target.left/top are world units.
|
|
97
|
+
const deltaX = target.left - (target._prevLeft ?? target.left);
|
|
98
|
+
const deltaY = target.top - (target._prevTop ?? target.top);
|
|
99
|
+
target._prevLeft = target.left;
|
|
100
|
+
target._prevTop = target.top;
|
|
101
|
+
if (deltaX === 0 && deltaY === 0)
|
|
102
|
+
return;
|
|
103
|
+
// 2. Apply the raw delta to HTML items
|
|
104
|
+
setLocalTasks((prev) => prev.map((t) => (selectedIds.has(t.id) ? { ...t, x: t.x + deltaX, y: t.y + deltaY } : t)));
|
|
105
|
+
setLocalDocuments((prev) => prev.map((d) => (selectedIds.has(d.id) ? { ...d, x: d.x + deltaX, y: d.y + deltaY } : d)));
|
|
106
|
+
};
|
|
107
|
+
const handleMouseDown = (e) => {
|
|
108
|
+
const target = e.target;
|
|
109
|
+
if (target) {
|
|
110
|
+
target._prevLeft = target.left;
|
|
111
|
+
target._prevTop = target.top;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
canvas.on("object:moving", handleObjectMoving);
|
|
115
|
+
canvas.on("mouse:down", handleMouseDown);
|
|
116
|
+
return () => {
|
|
117
|
+
canvas.off("object:moving", handleObjectMoving);
|
|
118
|
+
canvas.off("mouse:down", handleMouseDown);
|
|
119
|
+
};
|
|
120
|
+
}, [canvasZoom, selectedIds, fabricCanvas]);
|
|
121
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
122
|
+
const getItemPosition = (id) => {
|
|
123
|
+
const task = localTasks.find((t) => t.id === id);
|
|
124
|
+
if (task)
|
|
125
|
+
return { x: task.x, y: task.y };
|
|
126
|
+
const doc = localDocuments.find((d) => d.id === id);
|
|
127
|
+
if (doc)
|
|
128
|
+
return { x: doc.x, y: doc.y };
|
|
129
|
+
return undefined;
|
|
130
|
+
};
|
|
131
|
+
const isItemInSelectionBox = (x, y, width, height, box) => {
|
|
132
|
+
const itemX1 = x * canvasZoom + canvasViewport.x;
|
|
133
|
+
const itemY1 = y * canvasZoom + canvasViewport.y;
|
|
134
|
+
const itemX2 = itemX1 + width * canvasZoom;
|
|
135
|
+
const itemY2 = itemY1 + height * canvasZoom;
|
|
136
|
+
const boxX1 = Math.min(box.x1, box.x2);
|
|
137
|
+
const boxY1 = Math.min(box.y1, box.y2);
|
|
138
|
+
const boxX2 = Math.max(box.x1, box.x2);
|
|
139
|
+
const boxY2 = Math.max(box.y1, box.y2);
|
|
140
|
+
return !(boxX2 < itemX1 || boxX1 > itemX2 || boxY2 < itemY1 || boxY1 > itemY2);
|
|
141
|
+
};
|
|
142
|
+
// ── Selection box detection ──────────────────────────────────────────────────
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (!selectionBox)
|
|
145
|
+
return;
|
|
146
|
+
const newSelected = new Set();
|
|
147
|
+
localTasks.forEach((task) => {
|
|
148
|
+
if (isItemInSelectionBox(task.x, task.y, 300, 140, selectionBox))
|
|
149
|
+
newSelected.add(task.id);
|
|
150
|
+
});
|
|
151
|
+
localDocuments.forEach((doc) => {
|
|
152
|
+
if (isItemInSelectionBox(doc.x, doc.y, 300, 160, selectionBox))
|
|
153
|
+
newSelected.add(doc.id);
|
|
154
|
+
});
|
|
155
|
+
setSelectedIds(newSelected);
|
|
156
|
+
}, [selectionBox, localTasks, localDocuments, canvasZoom, canvasViewport]);
|
|
157
|
+
// ── Drag start (HTML Node side) ──────────────────────────────────────────────
|
|
158
|
+
// Helper to extract coordinates regardless of event type
|
|
159
|
+
const getPointerEvent = (e) => {
|
|
160
|
+
if ('touches' in e && e.touches.length > 0)
|
|
161
|
+
return e.touches[0];
|
|
162
|
+
return e;
|
|
163
|
+
};
|
|
164
|
+
const handleDragStart = (itemId, e) => {
|
|
165
|
+
// Prevent browser scrolling while dragging
|
|
166
|
+
if (e.cancelable)
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
const pointer = getPointerEvent(e);
|
|
169
|
+
let itemsToDrag;
|
|
170
|
+
if (selectedIds.has(itemId)) {
|
|
171
|
+
itemsToDrag = Array.from(selectedIds);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
itemsToDrag = [itemId];
|
|
175
|
+
setSelectedIds(new Set([itemId]));
|
|
176
|
+
}
|
|
177
|
+
const startPositions = new Map();
|
|
178
|
+
itemsToDrag.forEach((id) => {
|
|
179
|
+
const pos = getItemPosition(id);
|
|
180
|
+
if (pos)
|
|
181
|
+
startPositions.set(id, pos);
|
|
182
|
+
});
|
|
183
|
+
const canvasObjectsStartPos = new Map();
|
|
184
|
+
selectedCanvasObjects.forEach((obj) => {
|
|
185
|
+
canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
|
|
186
|
+
});
|
|
187
|
+
const clickedPos = getItemPosition(itemId);
|
|
188
|
+
if (!clickedPos)
|
|
189
|
+
return;
|
|
190
|
+
const screenX = clickedPos.x * canvasZoom + canvasViewport.x;
|
|
191
|
+
const screenY = clickedPos.y * canvasZoom + canvasViewport.y;
|
|
192
|
+
dragStateRef.current = {
|
|
193
|
+
isDragging: true,
|
|
194
|
+
itemIds: itemsToDrag,
|
|
195
|
+
startPositions,
|
|
196
|
+
canvasObjectsStartPos,
|
|
197
|
+
offsetX: pointer.clientX - screenX,
|
|
198
|
+
offsetY: pointer.clientY - screenY,
|
|
199
|
+
};
|
|
200
|
+
setDragging({ itemIds: itemsToDrag });
|
|
201
|
+
document.body.style.cursor = "grabbing";
|
|
202
|
+
document.body.style.userSelect = "none";
|
|
203
|
+
// Prevents mobile pull-to-refresh
|
|
204
|
+
document.body.style.touchAction = "none";
|
|
205
|
+
};
|
|
206
|
+
// ── Drag move (HTML Node side) ───────────────────────────────────────────────
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (!dragging)
|
|
209
|
+
return;
|
|
210
|
+
const handleMove = (e) => {
|
|
211
|
+
if (!dragStateRef.current.isDragging)
|
|
212
|
+
return;
|
|
213
|
+
// Prevent mobile scrolling
|
|
214
|
+
if (e.cancelable)
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
const pointer = getPointerEvent(e);
|
|
217
|
+
if (rafIdRef.current !== null)
|
|
218
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
219
|
+
rafIdRef.current = requestAnimationFrame(() => {
|
|
220
|
+
const { itemIds, startPositions, canvasObjectsStartPos, offsetX, offsetY } = dragStateRef.current;
|
|
221
|
+
const firstId = itemIds[0];
|
|
222
|
+
const firstStart = startPositions.get(firstId);
|
|
223
|
+
if (!firstStart)
|
|
224
|
+
return;
|
|
225
|
+
// Calculate new world coordinates
|
|
226
|
+
const newX = (pointer.clientX - offsetX - canvasViewport.x) / canvasZoom;
|
|
227
|
+
const newY = (pointer.clientY - offsetY - canvasViewport.y) / canvasZoom;
|
|
228
|
+
const deltaX = newX - firstStart.x;
|
|
229
|
+
const deltaY = newY - firstStart.y;
|
|
230
|
+
// Batch state updates for React
|
|
231
|
+
setLocalTasks((prev) => prev.map((t) => (itemIds.includes(t.id) ? {
|
|
232
|
+
...t,
|
|
233
|
+
x: (startPositions.get(t.id)?.x || 0) + deltaX,
|
|
234
|
+
y: (startPositions.get(t.id)?.y || 0) + deltaY
|
|
235
|
+
} : t)));
|
|
236
|
+
setLocalDocuments((prev) => prev.map((d) => (itemIds.includes(d.id) ? {
|
|
237
|
+
...d,
|
|
238
|
+
x: (startPositions.get(d.id)?.x || 0) + deltaX,
|
|
239
|
+
y: (startPositions.get(d.id)?.y || 0) + deltaY
|
|
240
|
+
} : d)));
|
|
241
|
+
if (fabricCanvas?.current) {
|
|
242
|
+
canvasObjectsStartPos.forEach((startPos, obj) => {
|
|
243
|
+
obj.set({
|
|
244
|
+
left: startPos.left + deltaX,
|
|
245
|
+
top: startPos.top + deltaY,
|
|
246
|
+
});
|
|
247
|
+
obj.setCoords();
|
|
248
|
+
});
|
|
249
|
+
fabricCanvas.current.requestRenderAll();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
const handleEnd = () => {
|
|
254
|
+
if (rafIdRef.current !== null)
|
|
255
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
256
|
+
dragStateRef.current.isDragging = false;
|
|
257
|
+
setDragging(null);
|
|
258
|
+
document.body.style.cursor = "";
|
|
259
|
+
document.body.style.userSelect = "";
|
|
260
|
+
document.body.style.touchAction = "";
|
|
261
|
+
onTasksUpdate?.(localTasks);
|
|
262
|
+
onDocumentsUpdate?.(localDocuments);
|
|
263
|
+
};
|
|
264
|
+
window.addEventListener("mousemove", handleMove, { passive: false });
|
|
265
|
+
window.addEventListener("mouseup", handleEnd);
|
|
266
|
+
window.addEventListener("touchmove", handleMove, { passive: false });
|
|
267
|
+
window.addEventListener("touchend", handleEnd);
|
|
268
|
+
window.addEventListener("touchcancel", handleEnd);
|
|
269
|
+
return () => {
|
|
270
|
+
window.removeEventListener("mousemove", handleMove);
|
|
271
|
+
window.removeEventListener("mouseup", handleEnd);
|
|
272
|
+
window.removeEventListener("touchmove", handleMove);
|
|
273
|
+
window.removeEventListener("touchend", handleEnd);
|
|
274
|
+
window.removeEventListener("touchcancel", handleEnd);
|
|
275
|
+
};
|
|
276
|
+
}, [dragging, canvasZoom, canvasViewport, localTasks, localDocuments, fabricCanvas]);
|
|
277
|
+
// ── Selection, Status, Keyboard Logic ────────────────────────────────────────
|
|
278
|
+
const handleSelect = (id, e) => {
|
|
279
|
+
if (e?.shiftKey || e?.ctrlKey || e?.metaKey) {
|
|
280
|
+
setSelectedIds((prev) => {
|
|
281
|
+
const next = new Set(prev);
|
|
282
|
+
next.has(id) ? next.delete(id) : next.add(id);
|
|
283
|
+
return next;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
setSelectedIds(new Set([id]));
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const handleStatusChange = (taskId, newStatus) => {
|
|
291
|
+
const updated = localTasks.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t));
|
|
292
|
+
setLocalTasks(updated);
|
|
293
|
+
onTasksUpdate?.(updated);
|
|
294
|
+
};
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
const handleKeyDown = (e) => {
|
|
297
|
+
// Don't trigger if typing in input
|
|
298
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
|
|
299
|
+
return;
|
|
300
|
+
// Select All
|
|
301
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "a") {
|
|
302
|
+
e.preventDefault();
|
|
303
|
+
setSelectedIds(new Set([...localTasks.map((t) => t.id), ...localDocuments.map((d) => d.id)]));
|
|
304
|
+
}
|
|
305
|
+
// Clear selection
|
|
306
|
+
if (e.key === "Escape") {
|
|
307
|
+
setSelectedIds(new Set());
|
|
308
|
+
}
|
|
309
|
+
// ← ADD THIS: Delete selected nodes
|
|
310
|
+
if ((e.key === "Delete" || e.key === "Backspace") && selectedIds.size > 0) {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
const updatedTasks = localTasks.filter((t) => !selectedIds.has(t.id));
|
|
313
|
+
const updatedDocs = localDocuments.filter((d) => !selectedIds.has(d.id));
|
|
314
|
+
setLocalTasks(updatedTasks);
|
|
315
|
+
setLocalDocuments(updatedDocs);
|
|
316
|
+
setSelectedIds(new Set());
|
|
317
|
+
onTasksUpdate?.(updatedTasks);
|
|
318
|
+
onDocumentsUpdate?.(updatedDocs);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
322
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
323
|
+
}, [localTasks, localDocuments, selectedIds, onTasksUpdate, onDocumentsUpdate]);
|
|
324
|
+
// ── Render helper ────────────────────────────────────────────────────────────
|
|
325
|
+
const renderItem = (id, x, y, children) => {
|
|
326
|
+
const screenX = x * canvasZoom;
|
|
327
|
+
const screenY = y * canvasZoom;
|
|
328
|
+
// 1. Detect if the user is interacting with the canvas at all
|
|
329
|
+
// 'dragging' is your existing state.
|
|
330
|
+
// You might want to pass 'isZooming' or 'isPanning' from your main canvas component here.
|
|
331
|
+
const isDragging = dragging?.itemIds.includes(id);
|
|
332
|
+
return (_jsx("div", { className: "pointer-events-auto absolute", style: {
|
|
333
|
+
left: 0,
|
|
334
|
+
top: 0,
|
|
335
|
+
// 2. Use translate3d for GPU performance
|
|
336
|
+
transform: `translate3d(${screenX}px, ${screenY}px, 0) scale(${canvasZoom})`,
|
|
337
|
+
transformOrigin: "top left",
|
|
338
|
+
// 3. THE FIX: Remove transition entirely during any viewport change
|
|
339
|
+
// Any 'ease' during zoom causes the "shaking" behavior.
|
|
340
|
+
transition: "none",
|
|
341
|
+
// 4. Optimization
|
|
342
|
+
willChange: "transform",
|
|
343
|
+
zIndex: isDragging ? 1000 : 1,
|
|
344
|
+
}, children: children }, id));
|
|
345
|
+
};
|
|
346
|
+
return (_jsx("div", { ref: overlayRef, className: "absolute inset-0 pointer-events-none", style: { zIndex: 50 }, onWheel: handleOverlayWheel, onClick: (e) => {
|
|
347
|
+
if (e.target === e.currentTarget)
|
|
348
|
+
setSelectedIds(new Set());
|
|
349
|
+
}, children: _jsxs("div", { className: "absolute top-0 left-0 pointer-events-none", style: {
|
|
350
|
+
transform: `translate(${canvasViewport.x}px, ${canvasViewport.y}px)`,
|
|
351
|
+
transformOrigin: "top left",
|
|
352
|
+
}, 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 })))] }) }));
|
|
353
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface TaskNodeProps {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
status: "todo" | "in-progress" | "done";
|
|
5
|
+
assignee?: string;
|
|
6
|
+
project?: string;
|
|
7
|
+
priority?: "low" | "medium" | "high";
|
|
8
|
+
dueDate?: string;
|
|
9
|
+
isSelected?: boolean;
|
|
10
|
+
onSelect?: (id: string, e?: React.MouseEvent) => void;
|
|
11
|
+
onDragStart?: (id: string, e: React.MouseEvent | React.TouchEvent) => void;
|
|
12
|
+
onStatusChange?: (id: string, newStatus: "todo" | "in-progress" | "done") => void;
|
|
13
|
+
zoom?: number;
|
|
14
|
+
}
|
|
15
|
+
export default function TaskNode({ id, title, status, assignee, project, priority, dueDate, isSelected, onSelect, onDragStart, onStatusChange, zoom, }: TaskNodeProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=custom-node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom-node.d.ts","sourceRoot":"","sources":["../../../src/components/node/custom-node.tsx"],"names":[],"mappings":"AAYA,UAAU,aAAa;IACrB,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;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACtD,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAC3E,cAAc,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,KAAK,IAAI,CAAC;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,EAC/B,EAAE,EACF,KAAK,EACL,MAAM,EACN,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,OAAO,EACP,UAAkB,EAClB,QAAQ,EACR,WAAW,EACX,cAAc,EACd,IAAQ,GACT,EAAE,aAAa,2CAgJf"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { Check, ListCheck, Calendar, User2, AlertCircle } from "lucide-react";
|
|
5
|
+
export default function TaskNode({ id, title, status, assignee, project, priority, dueDate, isSelected = false, onSelect, onDragStart, onStatusChange, zoom = 1, // Default to 1:1 scale
|
|
6
|
+
}) {
|
|
7
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
8
|
+
const statusConfig = {
|
|
9
|
+
todo: { accent: "bg-[#3A3A3C]", pill: "bg-[#2C2C2E] text-gray-400 border-gray-700", label: "To Do" },
|
|
10
|
+
"in-progress": { accent: "bg-[#029AFF]", pill: "bg-[#029AFF]/10 text-[#029AFF] border-[#029AFF]/20", label: "In Progress" },
|
|
11
|
+
done: { accent: "bg-[#10B981]", pill: "bg-[#10B981]/10 text-[#10B981] border-[#10B981]/20", label: "Completed" },
|
|
12
|
+
};
|
|
13
|
+
const currentStatus = statusConfig[status];
|
|
14
|
+
// Logic: Increase border thickness if zoomed out to keep it visible
|
|
15
|
+
const dynamicBorderWidth = zoom < 0.6 ? "2px" : "1px";
|
|
16
|
+
const focusRingSize = isSelected ? (zoom < 0.5 ? "6px" : "3px") : "0px";
|
|
17
|
+
// 2. Update the internal handler
|
|
18
|
+
const handleStart = (e) => {
|
|
19
|
+
const target = e.target;
|
|
20
|
+
// Don't drag if clicking buttons or the scrollable text
|
|
21
|
+
if (target.closest('.interactive-element'))
|
|
22
|
+
return;
|
|
23
|
+
// For touch events, we don't want the browser to scroll the page
|
|
24
|
+
// but we only preventDefault if it's not an interactive element
|
|
25
|
+
if (e.type === 'touchstart' && e.cancelable) {
|
|
26
|
+
// We don't preventDefault here because it might block clicks on
|
|
27
|
+
// internal non-interactive elements, let handleDragStart handle it.
|
|
28
|
+
}
|
|
29
|
+
setIsDragging(true);
|
|
30
|
+
if (onDragStart)
|
|
31
|
+
onDragStart(id, e);
|
|
32
|
+
if (onSelect)
|
|
33
|
+
onSelect(id, e); // Cast for compatibility
|
|
34
|
+
};
|
|
35
|
+
// 3. Update the cleanup effect
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const handleUp = () => setIsDragging(false);
|
|
38
|
+
if (isDragging) {
|
|
39
|
+
window.addEventListener("mouseup", handleUp);
|
|
40
|
+
window.addEventListener("touchend", handleUp); // Add touch cleanup
|
|
41
|
+
return () => {
|
|
42
|
+
window.removeEventListener("mouseup", handleUp);
|
|
43
|
+
window.removeEventListener("touchend", handleUp);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}, [isDragging]);
|
|
47
|
+
return (_jsxs("div", { onMouseDown: handleStart, onTouchStart: handleStart, className: `
|
|
48
|
+
relative w-[310px] bg-[#161617] rounded-2xl transition-all duration-300 ease-out
|
|
49
|
+
${isDragging ? "cursor-grabbing scale-[1.04] z-[100] shadow-2xl" : "cursor-grab shadow-xl"}
|
|
50
|
+
`, style: {
|
|
51
|
+
border: `${dynamicBorderWidth} solid ${isSelected ? "#10B981" : "#2C2C2E"}`,
|
|
52
|
+
boxShadow: isSelected ? `0 0 25px rgba(16,185,129, ${0.2 / zoom})` : 'none',
|
|
53
|
+
outline: `${focusRingSize} solid rgba(16,185,129, 0.2)`
|
|
54
|
+
}, children: [_jsx("div", { className: `absolute left-0 top-6 bottom-6 w-1 rounded-r-full transition-colors duration-500 ${currentStatus.accent}` }), _jsxs("div", { className: "p-5 pl-6", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "w-5 h-5 flex items-center justify-center rounded-md bg-[#10B981]/10 border border-[#10B981]/20", children: _jsx(ListCheck, { className: "w-3 h-3 text-[#10B981]" }) }), zoom > 0.4 && (_jsx("span", { className: "text-[10px] font-bold text-gray-500 uppercase tracking-widest truncate max-w-[120px]", children: project || "Task" }))] }), _jsx("div", { className: `interactive-element px-2 py-0.5 rounded-lg text-[9px] font-bold border uppercase ${currentStatus.pill}`, children: currentStatus.label })] }), _jsx("div", { className: "mb-4", children: _jsx("h3", { className: `text-[15px] font-semibold leading-snug transition-all duration-500 ${status === "done" ? "text-gray-600 line-through opacity-70" : "text-white"}`, children: title }) }), zoom > 0.5 && (_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [priority && (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(AlertCircle, { className: `w-3 h-3 ${priority === 'high' ? 'text-red-500' : 'text-gray-500'}` }), _jsx("span", { className: "text-[11px] text-gray-400 font-medium capitalize", children: priority })] })), dueDate && (_jsxs("div", { className: "flex items-center gap-1.5 text-gray-500", children: [_jsx(Calendar, { className: "w-3 h-3" }), _jsx("span", { className: "text-[11px] font-medium", children: dueDate })] }))] })), _jsxs("div", { className: "mt-5 pt-4 border-t border-[#252526] flex items-center justify-between", children: [_jsx("div", { className: "flex items-center gap-2", children: assignee ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "w-6 h-6 rounded-full bg-[#10B981] flex items-center justify-center text-[10px] font-bold text-black ring-2 ring-[#161617]", children: assignee.charAt(0) }), zoom > 0.6 && _jsx("span", { className: "text-[11px] text-gray-400 font-medium", children: assignee })] })) : (_jsx(User2, { className: "w-3.5 h-3.5 text-gray-600" })) }), _jsx("button", { onClick: (e) => {
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
onStatusChange?.(id, status === "done" ? "todo" : "done");
|
|
57
|
+
}, className: `
|
|
58
|
+
interactive-element w-8 h-8 rounded-xl border flex items-center justify-center transition-all duration-300
|
|
59
|
+
${status === "done"
|
|
60
|
+
? "bg-[#10B981] border-[#10B981] shadow-lg shadow-[#10B981]/20"
|
|
61
|
+
: "bg-transparent border-[#333] hover:border-[#10B981] text-transparent hover:text-[#10B981]/50"}
|
|
62
|
+
`, children: _jsx(Check, { className: `w-4 h-4 ${status === "done" ? "text-black" : "text-current"}`, strokeWidth: 3 }) })] })] })] }));
|
|
63
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface DocumentNodeProps {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
project: string;
|
|
5
|
+
breadcrumb?: string[];
|
|
6
|
+
preview: string;
|
|
7
|
+
updatedAt?: string;
|
|
8
|
+
isSelected?: boolean;
|
|
9
|
+
onSelect?: (id: string, e?: React.MouseEvent) => void;
|
|
10
|
+
onDragStart?: (id: string, e: React.MouseEvent | React.TouchEvent) => void;
|
|
11
|
+
}
|
|
12
|
+
export default function DocumentNode({ id, title, project, breadcrumb, preview, updatedAt, isSelected, onSelect, onDragStart, }: DocumentNodeProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=document-node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-node.d.ts","sourceRoot":"","sources":["../../../src/components/node/document-node.tsx"],"names":[],"mappings":"AAKA,UAAU,iBAAiB;IACzB,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,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACtD,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;CAC5E;AAED,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EACnC,EAAE,EACF,KAAK,EACL,OAAO,EACP,UAAe,EACf,OAAO,EACP,SAAS,EACT,UAAkB,EAClB,QAAQ,EACR,WAAW,GACZ,EAAE,iBAAiB,2CAuJnB"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { ChevronDown, ChevronUp, Clock, ExternalLink, MoreHorizontal, Layers } from "lucide-react";
|
|
5
|
+
export default function DocumentNode({ id, title, project, breadcrumb = [], preview, updatedAt, isSelected = false, onSelect, onDragStart, }) {
|
|
6
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
7
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
8
|
+
// 2. Update the internal handler
|
|
9
|
+
const handleStart = (e) => {
|
|
10
|
+
const target = e.target;
|
|
11
|
+
// Don't drag if clicking buttons or the scrollable text
|
|
12
|
+
if (target.closest('.interactive-element'))
|
|
13
|
+
return;
|
|
14
|
+
// For touch events, we don't want the browser to scroll the page
|
|
15
|
+
// but we only preventDefault if it's not an interactive element
|
|
16
|
+
if (e.type === 'touchstart' && e.cancelable) {
|
|
17
|
+
// We don't preventDefault here because it might block clicks on
|
|
18
|
+
// internal non-interactive elements, let handleDragStart handle it.
|
|
19
|
+
}
|
|
20
|
+
setIsDragging(true);
|
|
21
|
+
if (onDragStart)
|
|
22
|
+
onDragStart(id, e);
|
|
23
|
+
if (onSelect)
|
|
24
|
+
onSelect(id, e); // Cast for compatibility
|
|
25
|
+
};
|
|
26
|
+
// 3. Update the cleanup effect
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const handleUp = () => setIsDragging(false);
|
|
29
|
+
if (isDragging) {
|
|
30
|
+
window.addEventListener("mouseup", handleUp);
|
|
31
|
+
window.addEventListener("touchend", handleUp); // Add touch cleanup
|
|
32
|
+
return () => {
|
|
33
|
+
window.removeEventListener("mouseup", handleUp);
|
|
34
|
+
window.removeEventListener("touchend", handleUp);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}, [isDragging]);
|
|
38
|
+
return (_jsxs("div", { onMouseDown: handleStart, onTouchStart: handleStart, className: `
|
|
39
|
+
relative w-[320px] bg-[#121214] rounded-2xl border transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)]
|
|
40
|
+
${isDragging ? "cursor-grabbing scale-[1.03] z-50 shadow-2xl" : "cursor-grab shadow-lg hover:border-[#3e3e42]"}
|
|
41
|
+
${isSelected ? "border-[#029AFF] ring-1 ring-[#029AFF]" : "border-[#28282b]"}
|
|
42
|
+
${isExpanded ? "w-[400px]" : "w-[320px]"}
|
|
43
|
+
`, children: [isSelected && (_jsx("div", { className: "absolute -inset-px bg-gradient-to-r from-[#029AFF]/20 to-[#7000FF]/20 rounded-2xl blur-md -z-10" })), _jsxs("div", { className: "flex items-center justify-between p-3 pb-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "p-1.5 bg-[#029AFF]/10 rounded-lg", children: _jsx(Layers, { className: "w-3.5 h-3.5 text-[#029AFF]" }) }), _jsx("span", { className: "text-[10px] font-bold text-gray-500 tracking-tighter uppercase", children: project })] }), _jsx("button", { className: "interactive-element p-1 hover:bg-white/5 rounded-md text-gray-600 transition-colors", children: _jsx(MoreHorizontal, { className: "w-4 h-4" }) })] }), _jsxs("div", { className: "p-4 pt-2", children: [_jsxs("div", { onClick: () => setIsExpanded(!isExpanded), className: "interactive-element flex items-start justify-between group/title cursor-pointer", children: [_jsx("h3", { className: "text-[16px] font-semibold text-white leading-tight group-hover/title:text-[#029AFF] transition-colors pr-2", children: title }), _jsx("div", { className: "mt-1", children: isExpanded ?
|
|
44
|
+
_jsx(ChevronUp, { className: "w-4 h-4 text-gray-500" }) :
|
|
45
|
+
_jsx(ChevronDown, { className: "w-4 h-4 text-gray-500" }) })] }), _jsx("div", { className: "flex items-center gap-2 mt-2 overflow-x-auto no-scrollbar pb-1", children: breadcrumb.map((item, i) => (_jsx("div", { className: "flex items-center gap-2 flex-shrink-0", children: _jsx("span", { className: "text-[10px] px-2 py-0.5 bg-[#1C1C1E] border border-[#2C2C2E] text-gray-400 rounded-full", children: item }) }, i))) }), _jsxs("div", { className: `
|
|
46
|
+
mt-4 relative transition-all duration-500 ease-in-out overflow-hidden
|
|
47
|
+
${isExpanded ? "h-[300px]" : "h-[100px]"}
|
|
48
|
+
`, children: [_jsxs("div", { onMouseDown: (e) => e.stopPropagation(), className: "interactive-element h-full w-full bg-[#09090A] border border-[#232326] rounded-xl overflow-y-auto p-3 text-[13px] text-gray-400 leading-relaxed custom-scrollbar selection:bg-[#029AFF]/40", children: [preview, isExpanded && (_jsxs("div", { className: "mt-4 pt-4 border-t border-[#232326] flex items-center justify-between", children: [_jsx("span", { className: "text-[11px] text-gray-500 italic", children: "End of document preview" }), _jsxs("button", { className: "flex items-center gap-2 text-[11px] text-[#029AFF] hover:underline font-medium", children: ["Open Full File ", _jsx(ExternalLink, { className: "w-3 h-3" })] })] }))] }), !isExpanded && (_jsx("div", { className: "absolute bottom-0 left-0 right-0 h-10 bg-gradient-to-t from-[#09090A] to-transparent pointer-events-none" }))] }), _jsxs("div", { className: "mt-4 flex items-center justify-between opacity-60", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[10px] text-gray-400", children: [_jsx(Clock, { className: "w-3 h-3" }), _jsxs("span", { children: ["Updated ", updatedAt || "recently"] })] }), _jsx("div", { className: "flex -space-x-1.5", children: [1, 2].map(i => (_jsxs("div", { className: "w-5 h-5 rounded-full border-2 border-[#121214] bg-[#2C2C2E] flex items-center justify-center text-[8px] text-white", children: ["U", i] }, i))) })] })] }), _jsx("style", { children: `
|
|
49
|
+
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
50
|
+
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
|
51
|
+
.custom-scrollbar::-webkit-scrollbar { width: 5px; }
|
|
52
|
+
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
53
|
+
background: #232326;
|
|
54
|
+
border-radius: 10px;
|
|
55
|
+
}
|
|
56
|
+
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #029AFF; }
|
|
57
|
+
` })] }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DocumentTemplate {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
project: string;
|
|
5
|
+
breadcrumb?: string[];
|
|
6
|
+
preview: string;
|
|
7
|
+
updatedAt?: string;
|
|
8
|
+
}
|
|
9
|
+
interface DocumentDropdownProps {
|
|
10
|
+
onAddDocument: (doc: DocumentTemplate) => void;
|
|
11
|
+
}
|
|
12
|
+
export default function DocumentDropdown({ onAddDocument, }: DocumentDropdownProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=document-dropdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-dropdown.d.ts","sourceRoot":"","sources":["../../../src/components/toolbar/document-dropdown.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAC/B,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;AAED,UAAU,qBAAqB;IAC7B,aAAa,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAChD;AAkDD,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,aAAa,GACd,EAAE,qBAAqB,2CA2FvB"}
|