From 3e2c92c8712776c5445b061c1ded374920c825c2 Mon Sep 17 00:00:00 2001 From: charlieshoelace Date: Tue, 10 Feb 2026 21:42:43 +0100 Subject: [PATCH] feat: add keyboard shortcuts help dialog - New KeyboardShortcutsDialog component showing all available shortcuts - Accessible via ? button in header and ? key on canvas - Groups shortcuts by category: General, Add Nodes, Layout, Canvas - Auto-detects Mac vs Windows/Linux for modifier key display - Follows existing modal patterns (Escape to close, backdrop click) --- src/components/Header.tsx | 17 +++ src/components/KeyboardShortcutsDialog.tsx | 149 +++++++++++++++++++++ src/components/WorkflowCanvas.tsx | 11 +- src/store/workflowStore.ts | 12 ++ 4 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 src/components/KeyboardShortcutsDialog.tsx diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 0029cba1..20989752 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -4,6 +4,7 @@ import { useState, useRef, useMemo, useCallback } from "react"; import { useWorkflowStore, WorkflowFile } from "@/store/workflowStore"; import { ProjectSetupModal } from "./ProjectSetupModal"; import { CostIndicator } from "./CostIndicator"; +import { KeyboardShortcutsDialog } from "./KeyboardShortcutsDialog"; function CommentsNavigationIcon() { // Subscribe to nodes so we re-render when comments change @@ -69,6 +70,8 @@ export function Header() { loadWorkflow, previousWorkflowSnapshot, revertToSnapshot, + shortcutsDialogOpen, + setShortcutsDialogOpen, } = useWorkflowStore(); const [showProjectModal, setShowProjectModal] = useState(false); @@ -401,6 +404,16 @@ export function Header() { Made by Willie · + + · + setShortcutsDialogOpen(false)} + /> ); } diff --git a/src/components/KeyboardShortcutsDialog.tsx b/src/components/KeyboardShortcutsDialog.tsx new file mode 100644 index 00000000..9fea1570 --- /dev/null +++ b/src/components/KeyboardShortcutsDialog.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { useEffect } from "react"; + +interface ShortcutItem { + keys: string[]; + description: string; +} + +interface ShortcutGroup { + title: string; + shortcuts: ShortcutItem[]; +} + +const isMac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent); +const modKey = isMac ? "⌘" : "Ctrl"; + +const shortcutGroups: ShortcutGroup[] = [ + { + title: "General", + shortcuts: [ + { keys: [`${modKey}`, "Enter"], description: "Run workflow" }, + { keys: [`${modKey}`, "C"], description: "Copy selected nodes" }, + { keys: [`${modKey}`, "V"], description: "Paste nodes / image / text" }, + { keys: ["?"], description: "Show keyboard shortcuts" }, + ], + }, + { + title: "Add Nodes", + shortcuts: [ + { keys: ["Shift", "P"], description: "Add Prompt node" }, + { keys: ["Shift", "I"], description: "Add Image Input node" }, + { keys: ["Shift", "G"], description: "Add Generate Image node" }, + { keys: ["Shift", "V"], description: "Add Generate Video node" }, + { keys: ["Shift", "L"], description: "Add LLM Text node" }, + { keys: ["Shift", "A"], description: "Add Annotation node" }, + ], + }, + { + title: "Layout (select 2+ nodes first)", + shortcuts: [ + { keys: ["V"], description: "Stack selected vertically" }, + { keys: ["H"], description: "Stack selected horizontally" }, + { keys: ["G"], description: "Arrange selected as grid" }, + ], + }, + { + title: "Canvas", + shortcuts: [ + { keys: ["Scroll"], description: "Zoom in / out" }, + { keys: ["Trackpad"], description: "Pan (macOS)" }, + { keys: ["Delete"], description: "Delete selected nodes" }, + ], + }, +]; + +function Kbd({ children }: { children: string }) { + return ( + + {children} + + ); +} + +interface KeyboardShortcutsDialogProps { + isOpen: boolean; + onClose: () => void; +} + +export function KeyboardShortcutsDialog({ isOpen, onClose }: KeyboardShortcutsDialogProps) { + useEffect(() => { + if (!isOpen) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

+ Keyboard Shortcuts +

+ +
+ + {/* Content */} +
+ {shortcutGroups.map((group) => ( +
+

+ {group.title} +

+
+ {group.shortcuts.map((shortcut, idx) => ( +
+ + {shortcut.description} + +
+ {shortcut.keys.map((key, keyIdx) => ( + + {keyIdx > 0 && ( + + + )} + {key} + + ))} +
+
+ ))} +
+
+ ))} +
+ + {/* Footer */} +
+ +
+
+
+ ); +} + diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 8d6d0671..7ec871d1 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -210,7 +210,7 @@ const findScrollableAncestor = (target: HTMLElement, deltaX: number, deltaY: num }; export function WorkflowCanvas() { - const { nodes, edges, groups, onNodesChange, onEdgesChange, onConnect, addNode, updateNodeData, loadWorkflow, getNodeById, addToGlobalHistory, setNodeGroupId, executeWorkflow, isModalOpen, showQuickstart, setShowQuickstart, navigationTarget, setNavigationTarget, captureSnapshot, applyEditOperations, setWorkflowMetadata } = + const { nodes, edges, groups, onNodesChange, onEdgesChange, onConnect, addNode, updateNodeData, loadWorkflow, getNodeById, addToGlobalHistory, setNodeGroupId, executeWorkflow, isModalOpen, showQuickstart, setShowQuickstart, navigationTarget, setNavigationTarget, captureSnapshot, applyEditOperations, setWorkflowMetadata, setShortcutsDialogOpen } = useWorkflowStore(); const { screenToFlowPosition, getViewport, zoomIn, zoomOut, setViewport, setCenter } = useReactFlow(); const { show: showToast } = useToast(); @@ -982,6 +982,13 @@ export function WorkflowCanvas() { return; } + // Handle keyboard shortcuts dialog (? key) + if (event.key === "?" && !event.ctrlKey && !event.metaKey) { + event.preventDefault(); + setShortcutsDialogOpen(true); + return; + } + // Handle workflow execution (Ctrl/Cmd + Enter) if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { event.preventDefault(); @@ -1225,7 +1232,7 @@ export function WorkflowCanvas() { ]); }); } - }, [nodes, onNodesChange, copySelectedNodes, pasteNodes, clearClipboard, clipboard, getViewport, addNode, updateNodeData, executeWorkflow]); + }, [nodes, onNodesChange, copySelectedNodes, pasteNodes, clearClipboard, clipboard, getViewport, addNode, updateNodeData, executeWorkflow, setShortcutsDialogOpen]); useEffect(() => { window.addEventListener("keydown", handleKeyDown); diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 88e760a0..57082f2a 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -209,6 +209,10 @@ interface WorkflowStore { modelSearchOpen: boolean; modelSearchProvider: ProviderType | null; + // Keyboard shortcuts dialog state + shortcutsDialogOpen: boolean; + setShortcutsDialogOpen: (open: boolean) => void; + // Model search dialog actions setModelSearchOpen: (open: boolean, provider?: ProviderType | null) => void; @@ -320,6 +324,9 @@ export const useWorkflowStore = create((set, get) => ({ modelSearchOpen: false, modelSearchProvider: null, + // Keyboard shortcuts dialog initial state + shortcutsDialogOpen: false, + // Recent models initial state recentModels: getRecentModels(), @@ -1544,6 +1551,11 @@ export const useWorkflowStore = create((set, get) => ({ saveProviderSettings(updated); }, + // Keyboard shortcuts dialog actions + setShortcutsDialogOpen: (open: boolean) => { + set({ shortcutsDialogOpen: open }); + }, + // Model search dialog actions setModelSearchOpen: (open: boolean, provider?: ProviderType | null) => { set({