@@ -20,6 +20,7 @@ import HoverVisionAid from './components/HoverVisionAid.jsx'; // Import the Hove
2020import { getNodeDimensions } from './utils.js';
2121import { getTextColor, hexToHsl } from './utils/colorUtils.js';
2222import { getPrototypeIdFromItem } from './utils/abstraction.js';
23+ import { copySelection, pasteClipboard } from './utils/clipboard.js';
2324import { analyzeNodeDistribution, getClusterBoundingBox } from './utils/clusterAnalysis.js';
2425import { v4 as uuidv4 } from 'uuid'; // Import UUID generator
2526import { Edit3, Trash2, Link, Package, PackageOpen, Expand, ArrowUpFromDot, Triangle, Layers, ArrowLeft, SendToBack, ArrowBigRightDash, Palette, MoreHorizontal, Bookmark, Plus, CornerUpLeft, CornerDownLeft, Merge, Undo2, Clock } from 'lucide-react'; // Icons for PieMenu
@@ -1832,6 +1833,9 @@ function NodeCanvas() {
18321833 // --- Local UI State (Keep these) ---
18331834 const [selectedInstanceIds, setSelectedInstanceIds] = useState(new Set());
18341835
1836+ // Clipboard ref for copy/paste operations
1837+ const clipboardRef = useRef(null);
1838+
18351839 // Onboarding modal state
18361840 const [showOnboardingModal, setShowOnboardingModal] = useState(false);
18371841 const [showStorageSetupModal, setShowStorageSetupModal] = useState(false);
@@ -8076,6 +8080,85 @@ function NodeCanvas() {
80768080 }
80778081 }
80788082
8083+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
8084+ const cmdOrCtrl = isMac ? e.metaKey : e.ctrlKey;
8085+
8086+ // Copy (Ctrl/Cmd+C)
8087+ if (cmdOrCtrl && e.key === 'c' && selectedInstanceIds.size > 0) {
8088+ e.preventDefault();
8089+ const currentGraph = graphsMap.get(activeGraphId);
8090+ if (currentGraph) {
8091+ const copied = copySelection(selectedInstanceIds, currentGraph, nodePrototypesMap, edgesMap);
8092+ clipboardRef.current = copied;
8093+ }
8094+ return;
8095+ }
8096+
8097+ // Paste (Ctrl/Cmd+V)
8098+ if (cmdOrCtrl && e.key === 'v' && clipboardRef.current) {
8099+ e.preventDefault();
8100+ const currentGraph = graphsMap.get(activeGraphId);
8101+ if (currentGraph) {
8102+ // Determine target position
8103+ let targetPos;
8104+ const svgElement = document.querySelector('.node-canvas-svg');
8105+ const rect = svgElement?.getBoundingClientRect();
8106+
8107+ if (rect && !isTouchDeviceRef.current && mousePositionRef.current) {
8108+ // Desktop: use mouse position converted to canvas coords
8109+ const clientX = mousePositionRef.current.x;
8110+ const clientY = mousePositionRef.current.y;
8111+ targetPos = {
8112+ x: (clientX - rect.left - panOffset.x) / zoomLevel + canvasSize.offsetX,
8113+ y: (clientY - rect.top - panOffset.y) / zoomLevel + canvasSize.offsetY
8114+ };
8115+ } else {
8116+ // Mobile fallback: offset from original center
8117+ targetPos = {
8118+ x: clipboardRef.current.originalCenter.x + 50,
8119+ y: clipboardRef.current.originalCenter.y + 50
8120+ };
8121+ }
8122+
8123+ const result = pasteClipboard(
8124+ clipboardRef.current,
8125+ activeGraphId,
8126+ targetPos,
8127+ storeActions,
8128+ currentGraph,
8129+ getNodeDimensions
8130+ );
8131+ setSelectedInstanceIds(new Set(result.newInstanceIds));
8132+ }
8133+ return;
8134+ }
8135+
8136+ // Duplicate (Ctrl/Cmd+D)
8137+ if (cmdOrCtrl && e.key === 'd' && selectedInstanceIds.size > 0) {
8138+ e.preventDefault();
8139+ const currentGraph = graphsMap.get(activeGraphId);
8140+ if (currentGraph) {
8141+ const copied = copySelection(selectedInstanceIds, currentGraph, nodePrototypesMap, edgesMap);
8142+ if (copied) {
8143+ // Paste immediately with fixed offset
8144+ const targetPos = {
8145+ x: copied.originalCenter.x + 50,
8146+ y: copied.originalCenter.y + 50
8147+ };
8148+ const result = pasteClipboard(
8149+ copied,
8150+ activeGraphId,
8151+ targetPos,
8152+ storeActions,
8153+ currentGraph,
8154+ getNodeDimensions
8155+ );
8156+ setSelectedInstanceIds(new Set(result.newInstanceIds));
8157+ }
8158+ }
8159+ return;
8160+ }
8161+
80798162 const isDeleteKey = e.key === 'Delete' || e.key === 'Backspace';
80808163 const nodesSelected = selectedInstanceIds.size > 0;
80818164 const edgeSelected = selectedEdgeId !== null || selectedEdgeIds.size > 0;
@@ -8121,7 +8204,7 @@ function NodeCanvas() {
81218204 };
81228205 window.addEventListener('keydown', handleKeyDown);
81238206 return () => window.removeEventListener('keydown', handleKeyDown);
8124- }, [selectedInstanceIds, selectedEdgeId, selectedEdgeIds, isHeaderEditing, isRightPanelInputFocused, isLeftPanelInputFocused, nodeNamePrompt.visible, connectionNamePrompt.visible, activeGraphId, storeActions.removeNodeInstance, storeActions.removeEdge, storeActions.clearSelectedEdgeIds ]);
8207+ }, [selectedInstanceIds, selectedEdgeId, selectedEdgeIds, isHeaderEditing, isRightPanelInputFocused, isLeftPanelInputFocused, nodeNamePrompt.visible, connectionNamePrompt.visible, activeGraphId, storeActions, graphsMap, nodePrototypesMap, edgesMap, panOffset, zoomLevel, canvasSize ]);
81258208
81268209 const handleProjectTitleChange = (newTitle) => {
81278210 // Get CURRENT activeGraphId directly from store
0 commit comments