feat: canvas history (undo/redo), copy/paste nodes, node search, shortcuts modal

- Undo/Redo (Ctrl+Z / Ctrl+Y): 50-entry snapshot stack in canvasStore; snapshot before all mutations and on node drag stop
- Copy/Paste (Ctrl+C / Ctrl+V): copy selected nodes to clipboard, paste with +50px offset and new IDs
- Node search (Ctrl+K): spotlight overlay — fuzzy search by label/IP/hostname, jumps + focuses matched node
- Shortcuts modal (?): lists all keyboard shortcuts, accessible via ? key or toolbar ? button
- Toolbar: undo/redo buttons (disabled when stack empty), ? help button
This commit is contained in:
Pouzor
2026-03-12 11:56:38 +01:00
parent 68b35a0c30
commit ba032a45af
8 changed files with 455 additions and 18 deletions
@@ -0,0 +1,84 @@
import { X } from 'lucide-react'
import { Button } from '@/components/ui/button'
const SHORTCUTS = [
{
group: 'Canvas',
items: [
{ keys: ['Ctrl', 'S'], description: 'Save canvas' },
{ keys: ['Ctrl', 'Z'], description: 'Undo' },
{ keys: ['Ctrl', 'Y'], description: 'Redo' },
{ keys: ['Ctrl', 'K'], description: 'Search nodes' },
{ keys: ['?'], description: 'Show this help' },
],
},
{
group: 'Nodes',
items: [
{ keys: ['Ctrl', 'C'], description: 'Copy selected nodes' },
{ keys: ['Ctrl', 'V'], description: 'Paste nodes' },
{ keys: ['Del'], description: 'Delete selected node/edge' },
],
},
{
group: 'Navigation',
items: [
{ keys: ['Scroll'], description: 'Zoom in / out' },
{ keys: ['Space', '+', 'Drag'], description: 'Pan canvas' },
{ keys: ['Ctrl', 'Shift', 'F'], description: 'Fit view' },
],
},
]
interface ShortcutsModalProps {
open: boolean
onClose: () => void
}
export function ShortcutsModal({ open, onClose }: ShortcutsModalProps) {
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center" onClick={onClose}>
<div
className="bg-[#161b22] border border-border rounded-lg shadow-2xl w-full max-w-sm"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<h2 className="text-sm font-semibold text-foreground">Keyboard Shortcuts</h2>
<Button size="sm" variant="ghost" className="h-6 w-6 p-0" onClick={onClose}>
<X size={14} />
</Button>
</div>
<div className="p-4 space-y-4">
{SHORTCUTS.map((group) => (
<div key={group.group}>
<p className="text-xs text-[#00d4ff] font-semibold mb-2 uppercase tracking-wide">
{group.group}
</p>
<div className="space-y-1.5">
{group.items.map((item) => (
<div key={item.description} className="flex items-center justify-between gap-4">
<span className="text-sm text-muted-foreground">{item.description}</span>
<div className="flex items-center gap-1 shrink-0">
{item.keys.map((k, i) => (
k === '+' ? (
<span key={i} className="text-xs text-muted-foreground">+</span>
) : (
<kbd key={k} className="text-[11px] text-foreground border border-border rounded px-1.5 py-0.5 font-mono bg-[#0d1117]">
{k}
</kbd>
)
))}
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
)
}