+
+
Export as
{diagramId && (
)}
+
+
Import
+
)}
diff --git a/frontend/src/components/network/KeyboardShortcutsOverlay.tsx b/frontend/src/components/network/KeyboardShortcutsOverlay.tsx
new file mode 100644
index 00000000..de151d7c
--- /dev/null
+++ b/frontend/src/components/network/KeyboardShortcutsOverlay.tsx
@@ -0,0 +1,129 @@
+import { useEffect } from 'react'
+import { X } from 'lucide-react'
+
+interface ShortcutRow {
+ keys: string[]
+ label: string
+}
+
+interface ShortcutGroup {
+ title: string
+ rows: ShortcutRow[]
+}
+
+const GROUPS: ShortcutGroup[] = [
+ {
+ title: 'Modes',
+ rows: [
+ { keys: ['V'], label: 'Select mode' },
+ { keys: ['H'], label: 'Pan mode' },
+ { keys: ['C'], label: 'Connect mode' },
+ { keys: ['Space'], label: 'Temporary pan (hold)' },
+ ],
+ },
+ {
+ title: 'Canvas',
+ rows: [
+ { keys: ['Ctrl', 'Shift', 'F'], label: 'Fit view' },
+ { keys: ['Ctrl', 'A'], label: 'Select all' },
+ { keys: ['Ctrl', 'Z'], label: 'Undo' },
+ { keys: ['Ctrl', 'Y'], label: 'Redo' },
+ ],
+ },
+ {
+ title: 'Nodes',
+ rows: [
+ { keys: ['Ctrl', 'C'], label: 'Copy' },
+ { keys: ['Ctrl', 'V'], label: 'Paste' },
+ { keys: ['Ctrl', 'D'], label: 'Duplicate' },
+ { keys: ['Del'], label: 'Delete selected' },
+ { keys: [']'], label: 'Bring to front' },
+ { keys: ['['], label: 'Send to back' },
+ ],
+ },
+ {
+ title: 'Nudge',
+ rows: [
+ { keys: ['↑', '↓', '←', '→'], label: 'Move 1px' },
+ { keys: ['Shift', '↑↓←→'], label: 'Move 10px' },
+ ],
+ },
+]
+
+interface KeyboardShortcutsOverlayProps {
+ onClose: () => void
+}
+
+function Kbd({ children }: { children: string }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function KeyboardShortcutsOverlay({ onClose }: KeyboardShortcutsOverlayProps) {
+ useEffect(() => {
+ const handler = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') onClose()
+ }
+ document.addEventListener('keydown', handler)
+ return () => document.removeEventListener('keydown', handler)
+ }, [onClose])
+
+ return (
+
{ if (e.target === e.currentTarget) onClose() }}
+ >
+
+ {/* Header */}
+
+
+
Keyboard Shortcuts
+
Press ? anytime to open this
+
+
+
+
+ {/* Shortcut grid */}
+
+ {GROUPS.map((group, gi) => (
+
= 2 ? 'border-t border-default' : ''}>
+
+
+ {group.title}
+
+
+ {group.rows.map(row => (
+
+
{row.label}
+
+ {row.keys.map((k, i) => (
+
+ {i > 0 && +}
+ {k}
+
+ ))}
+
+
+ ))}
+
+
+
+ ))}
+
+
+ {/* Footer hint */}
+
+ On Mac, Ctrl = ⌘ Cmd
+
+
+
+ )
+}
diff --git a/frontend/src/components/network/hooks/useCanvasShortcuts.ts b/frontend/src/components/network/hooks/useCanvasShortcuts.ts
index da33bfb8..ff997473 100644
--- a/frontend/src/components/network/hooks/useCanvasShortcuts.ts
+++ b/frontend/src/components/network/hooks/useCanvasShortcuts.ts
@@ -37,6 +37,7 @@ export function useCanvasShortcuts({
onRedo,
onNudge,
onSetMode,
+ onToggleShortcuts,
}: {
nodes: Node[]
edges: Edge[]
@@ -48,6 +49,7 @@ export function useCanvasShortcuts({
onRedo: () => void
onNudge: (dx: number, dy: number) => void
onSetMode: (mode: 'select' | 'pan' | 'connect') => void
+ onToggleShortcuts: () => void
}) {
const { getNodes, fitView, screenToFlowPosition, setNodes: rfSetNodes } = useReactFlow()
const clipboardRef = useRef
(null)
@@ -279,12 +281,15 @@ export function useCanvasShortcuts({
} else if (e.key === '[' && !ctrl) {
e.preventDefault()
sendSelectedToBack()
+ } else if (e.key === '?' && !ctrl) {
+ e.preventDefault()
+ onToggleShortcuts()
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
- }, [copyNodes, pasteNodes, duplicateNodes, selectAll, fitView, bringSelectedToFront, sendSelectedToBack, onUndo, onRedo, onNudge, onSetMode])
+ }, [copyNodes, pasteNodes, duplicateNodes, selectAll, fitView, bringSelectedToFront, sendSelectedToBack, onUndo, onRedo, onNudge, onSetMode, onToggleShortcuts])
return {
copyNodes,
diff --git a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx
index 689a9039..ed3e1661 100644
--- a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx
+++ b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx
@@ -24,6 +24,7 @@ import { DeviceToolbar } from '@/components/network/panels/DeviceToolbar'
import { PropertiesPanel } from '@/components/network/panels/PropertiesPanel'
import { AIAssistPanel } from '@/components/network/panels/AIAssistPanel'
import { CanvasEmptyPrompt } from '@/components/network/CanvasEmptyPrompt'
+import { KeyboardShortcutsOverlay } from '@/components/network/KeyboardShortcutsOverlay'
import { networkDiagramsApi, deviceTypesApi } from '@/api'
import { toast } from '@/lib/toast'
import { exportToDrawio } from '@/lib/drawio-export'
@@ -73,6 +74,7 @@ function DiagramEditorInner() {
const [isDragOver, setIsDragOver] = useState(false)
const [interactionMode, setInteractionMode] = useState('select')
+ const [showShortcuts, setShowShortcuts] = useState(false)
const canvasRef = useRef(null)
const drawioImportRef = useRef(null)
@@ -161,6 +163,7 @@ function DiagramEditorInner() {
onRedo: redo,
onNudge,
onSetMode: setInteractionMode,
+ onToggleShortcuts: () => setShowShortcuts(v => !v),
})
const handleNodesChange: typeof onNodesChange = useCallback((changes) => {
@@ -850,6 +853,14 @@ function DiagramEditorInner() {
{nodes.length === 0 && !loading && (
)}
+ {/* Keyboard shortcut hint button — bottom-right corner */}
+