|
| 1 | +# Keyboard Shortcuts & Command Palette |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +ResCanvas now features a comprehensive keyboard shortcuts system and VS Code-style command palette for power users to work efficiently without mouse/toolbar interactions. |
| 6 | + |
| 7 | +## Features |
| 8 | + |
| 9 | +### ⌨️ Keyboard Shortcuts |
| 10 | + |
| 11 | +- **Tools**: Single-key shortcuts (P, E, R, C, L, A) for quick tool switching |
| 12 | +- **Edit**: Ctrl+Z/Ctrl+Shift+Z for undo/redo |
| 13 | +- **Canvas**: Ctrl+R refresh, Ctrl+Shift+K clear |
| 14 | +- **Commands**: Ctrl+K command palette, Ctrl+/ shortcuts help |
| 15 | + |
| 16 | +### 🎯 Command Palette (Ctrl+K) |
| 17 | + |
| 18 | +VS Code-style quick access to all commands: |
| 19 | +- Fuzzy search with keyword matching |
| 20 | +- Keyboard navigation (↑↓ arrows, Enter to select) |
| 21 | +- Recent commands tracking |
| 22 | +- Category grouping |
| 23 | +- Shortcut display for each command |
| 24 | + |
| 25 | +### 📖 Keyboard Shortcuts Help (Ctrl+/) |
| 26 | + |
| 27 | +Comprehensive shortcut reference: |
| 28 | +- Organized by category (Tools, Edit, Canvas, Commands) |
| 29 | +- Tab navigation between categories |
| 30 | +- Search/filter functionality |
| 31 | +- Platform-specific display (⌘ for Mac, Ctrl for Windows/Linux) |
| 32 | + |
| 33 | +## Usage |
| 34 | + |
| 35 | +### Opening Dialogs |
| 36 | + |
| 37 | +```javascript |
| 38 | +// Command Palette |
| 39 | +Press: Ctrl+K (Cmd+K on Mac) |
| 40 | + |
| 41 | +// Shortcuts Help |
| 42 | +Press: Ctrl+/ (Cmd+/ on Mac) |
| 43 | +
|
| 44 | +// Cancel/Close |
| 45 | +Press: Escape |
| 46 | +``` |
| 47 | + |
| 48 | +### Available Shortcuts |
| 49 | + |
| 50 | +#### Tools (No modifiers) |
| 51 | +- `P` - Pen tool |
| 52 | +- `E` - Eraser |
| 53 | +- `R` - Rectangle |
| 54 | +- `C` - Circle |
| 55 | +- `L` - Line |
| 56 | +- `A` - Arrow |
| 57 | + |
| 58 | +#### Edit Operations |
| 59 | +- `Ctrl+Z` - Undo |
| 60 | +- `Ctrl+Shift+Z` - Redo |
| 61 | + |
| 62 | +#### Canvas Operations |
| 63 | +- `Ctrl+R` - Refresh canvas |
| 64 | +- `Ctrl+Shift+K` - Clear canvas |
| 65 | +- `Ctrl+,` - Canvas settings (if available) |
| 66 | + |
| 67 | +#### Commands |
| 68 | +- `Ctrl+K` - Command palette |
| 69 | +- `Ctrl+/` - Keyboard shortcuts help |
| 70 | +- `Escape` - Cancel current action |
| 71 | + |
| 72 | +## Architecture |
| 73 | + |
| 74 | +### Core Services |
| 75 | + |
| 76 | +#### KeyboardShortcutManager (`frontend/src/services/KeyboardShortcuts.js`) |
| 77 | + |
| 78 | +Manages registration and execution of keyboard shortcuts: |
| 79 | + |
| 80 | +```javascript |
| 81 | +import { KeyboardShortcutManager } from '../services/KeyboardShortcuts'; |
| 82 | + |
| 83 | +const manager = new KeyboardShortcutManager(); |
| 84 | + |
| 85 | +// Register a shortcut |
| 86 | +manager.register( |
| 87 | + 'k', // key |
| 88 | + { ctrl: true }, // modifiers |
| 89 | + () => openCommandPalette(), // action |
| 90 | + 'Open Command Palette', // description |
| 91 | + 'Commands' // category |
| 92 | +); |
| 93 | + |
| 94 | +// Handle key events |
| 95 | +document.addEventListener('keydown', (e) => manager.handleKeyDown(e)); |
| 96 | +``` |
| 97 | + |
| 98 | +Features: |
| 99 | +- Conflict detection and warnings |
| 100 | +- Input field detection (shortcuts disabled in text inputs) |
| 101 | +- Platform-aware display (⌘/Ctrl) |
| 102 | +- Enable/disable shortcuts dynamically |
| 103 | +- Category grouping |
| 104 | + |
| 105 | +#### CommandRegistry (`frontend/src/services/CommandRegistry.js`) |
| 106 | + |
| 107 | +Central registry for all executable commands: |
| 108 | + |
| 109 | +```javascript |
| 110 | +import { commandRegistry } from '../services/CommandRegistry'; |
| 111 | + |
| 112 | +// Register a command |
| 113 | +commandRegistry.register({ |
| 114 | + id: 'canvas.clear', |
| 115 | + label: 'Clear Canvas', |
| 116 | + description: 'Remove all strokes from canvas', |
| 117 | + keywords: ['delete', 'erase', 'reset'], |
| 118 | + action: () => clearCanvas(), |
| 119 | + category: 'Canvas', |
| 120 | + shortcut: { key: 'k', modifiers: { ctrl: true, shift: true } }, |
| 121 | + enabled: () => editingEnabled // Optional condition |
| 122 | +}); |
| 123 | + |
| 124 | +// Execute command |
| 125 | +await commandRegistry.execute('canvas.clear'); |
| 126 | + |
| 127 | +// Search commands |
| 128 | +const results = commandRegistry.search('clear'); |
| 129 | +``` |
| 130 | + |
| 131 | +Features: |
| 132 | +- Command search with keyword matching |
| 133 | +- Enabled/visible state functions |
| 134 | +- Event listeners for execution tracking |
| 135 | +- Category organization |
| 136 | +- Batch registration |
| 137 | + |
| 138 | +### Components |
| 139 | + |
| 140 | +#### CommandPalette (`frontend/src/components/CommandPalette.jsx`) |
| 141 | + |
| 142 | +VS Code-style command search and execution: |
| 143 | + |
| 144 | +```jsx |
| 145 | +<CommandPalette |
| 146 | + open={commandPaletteOpen} |
| 147 | + onClose={() => setCommandPaletteOpen(false)} |
| 148 | + commands={commandRegistry.getAll()} |
| 149 | + onExecute={(command) => command.action()} |
| 150 | +/> |
| 151 | +``` |
| 152 | + |
| 153 | +Features: |
| 154 | +- Fuzzy search with keyword matching |
| 155 | +- Keyboard navigation (↑↓, Enter, Escape) |
| 156 | +- Recent commands tracking (localStorage) |
| 157 | +- Category headers |
| 158 | +- Shortcut display |
| 159 | +- Empty state messaging |
| 160 | + |
| 161 | +#### KeyboardShortcutsHelp (`frontend/src/components/KeyboardShortcutsHelp.jsx`) |
| 162 | + |
| 163 | +Comprehensive shortcuts reference: |
| 164 | + |
| 165 | +```jsx |
| 166 | +<KeyboardShortcutsHelp |
| 167 | + open={shortcutsHelpOpen} |
| 168 | + onClose={() => setShortcutsHelpOpen(false)} |
| 169 | + shortcuts={manager.getAllShortcuts()} |
| 170 | +/> |
| 171 | +``` |
| 172 | + |
| 173 | +Features: |
| 174 | +- Tab navigation by category |
| 175 | +- Search/filter shortcuts |
| 176 | +- Platform-specific key display |
| 177 | +- Quick tips section |
| 178 | +- Responsive design |
| 179 | + |
| 180 | +### Configuration |
| 181 | + |
| 182 | +#### Default Shortcuts (`frontend/src/config/shortcuts.js`) |
| 183 | + |
| 184 | +Centralized shortcut definitions: |
| 185 | + |
| 186 | +```javascript |
| 187 | +export const DEFAULT_SHORTCUTS = [ |
| 188 | + { |
| 189 | + id: 'tool.pen', |
| 190 | + key: 'p', |
| 191 | + modifiers: {}, |
| 192 | + label: 'Select Pen Tool', |
| 193 | + description: 'Switch to pen/brush tool for drawing', |
| 194 | + category: 'Tools', |
| 195 | + keywords: ['draw', 'brush', 'pencil'] |
| 196 | + }, |
| 197 | + // ... more shortcuts |
| 198 | +]; |
| 199 | +``` |
| 200 | + |
| 201 | +## Integration |
| 202 | + |
| 203 | +### Canvas Component |
| 204 | + |
| 205 | +The Canvas component registers all shortcuts on mount: |
| 206 | + |
| 207 | +```javascript |
| 208 | +// In Canvas.js |
| 209 | +useEffect(() => { |
| 210 | + const manager = new KeyboardShortcutManager(); |
| 211 | + |
| 212 | + // Register commands |
| 213 | + commandRegistry.register({ |
| 214 | + id: 'edit.undo', |
| 215 | + label: 'Undo', |
| 216 | + action: undo, |
| 217 | + shortcut: { key: 'z', modifiers: { ctrl: true } } |
| 218 | + }); |
| 219 | + |
| 220 | + // Register keyboard shortcuts |
| 221 | + manager.register('z', { ctrl: true }, undo, 'Undo', 'Edit'); |
| 222 | + |
| 223 | + // Add global listener |
| 224 | + const handleKeyDown = (e) => manager.handleKeyDown(e); |
| 225 | + document.addEventListener('keydown', handleKeyDown); |
| 226 | + |
| 227 | + return () => { |
| 228 | + document.removeEventListener('keydown', handleKeyDown); |
| 229 | + manager.clear(); |
| 230 | + }; |
| 231 | +}, [dependencies]); |
| 232 | +``` |
| 233 | + |
| 234 | +### Adding New Shortcuts |
| 235 | + |
| 236 | +1. **Define the action function** in your component |
| 237 | +2. **Add command to registry** in the useEffect hook: |
| 238 | + |
| 239 | +```javascript |
| 240 | +commandRegistry.register({ |
| 241 | + id: 'feature.myAction', |
| 242 | + label: 'My Action', |
| 243 | + description: 'Description of what it does', |
| 244 | + keywords: ['search', 'terms'], |
| 245 | + action: myActionFunction, |
| 246 | + category: 'Feature', |
| 247 | + shortcut: { key: 'm', modifiers: { ctrl: true } }, |
| 248 | + enabled: () => someCondition // Optional |
| 249 | +}); |
| 250 | +``` |
| 251 | + |
| 252 | +3. **Register keyboard shortcut**: |
| 253 | + |
| 254 | +```javascript |
| 255 | +manager.register( |
| 256 | + 'm', |
| 257 | + { ctrl: true }, |
| 258 | + myActionFunction, |
| 259 | + 'My Action', |
| 260 | + 'Feature' |
| 261 | +); |
| 262 | +``` |
| 263 | + |
| 264 | +## Best Practices |
| 265 | + |
| 266 | +### 1. Use Descriptive Labels |
| 267 | +```javascript |
| 268 | +// Good |
| 269 | +label: 'Clear Canvas' |
| 270 | +description: 'Remove all strokes from canvas' |
| 271 | + |
| 272 | +// Bad |
| 273 | +label: 'Clear' |
| 274 | +description: 'Clears stuff' |
| 275 | +``` |
| 276 | + |
| 277 | +### 2. Add Keywords for Searchability |
| 278 | +```javascript |
| 279 | +keywords: ['delete', 'erase', 'reset', 'remove'] // Good |
| 280 | +keywords: [] // Bad |
| 281 | +``` |
| 282 | + |
| 283 | +### 3. Check Conditions Before Execution |
| 284 | +```javascript |
| 285 | +action: () => { |
| 286 | + if (!editingEnabled) { |
| 287 | + showSnackbar('Action disabled in view-only mode'); |
| 288 | + return; |
| 289 | + } |
| 290 | + performAction(); |
| 291 | +} |
| 292 | +``` |
| 293 | + |
| 294 | +### 4. Provide Feedback |
| 295 | +```javascript |
| 296 | +action: () => { |
| 297 | + setDrawMode('pen'); |
| 298 | + showSnackbar('Pen tool selected'); // User feedback |
| 299 | +} |
| 300 | +``` |
| 301 | + |
| 302 | +### 5. Avoid Input Field Conflicts |
| 303 | +The KeyboardShortcutManager automatically disables shortcuts when typing in input fields. For special cases: |
| 304 | + |
| 305 | +```javascript |
| 306 | +manager.register( |
| 307 | + 'enter', |
| 308 | + {}, |
| 309 | + submitForm, |
| 310 | + 'Submit Form', |
| 311 | + 'Actions', |
| 312 | + false // allowInInput = false (default) |
| 313 | +); |
| 314 | +``` |
| 315 | + |
| 316 | +## Testing |
| 317 | + |
| 318 | +### Manual Testing Checklist |
| 319 | + |
| 320 | +- [ ] Ctrl+K opens command palette |
| 321 | +- [ ] Ctrl+/ opens shortcuts help |
| 322 | +- [ ] Escape closes dialogs |
| 323 | +- [ ] Tool shortcuts (P, E, R, C, L, A) switch tools |
| 324 | +- [ ] Ctrl+Z / Ctrl+Shift+Z perform undo/redo |
| 325 | +- [ ] Ctrl+R refreshes canvas |
| 326 | +- [ ] Shortcuts disabled when typing in text fields |
| 327 | +- [ ] Command palette search works |
| 328 | +- [ ] Recent commands are tracked |
| 329 | +- [ ] Arrow keys navigate command palette |
| 330 | +- [ ] Enter executes selected command |
| 331 | + |
| 332 | +### Unit Tests |
| 333 | + |
| 334 | +```javascript |
| 335 | +// Example test for KeyboardShortcutManager |
| 336 | +import { KeyboardShortcutManager } from '../services/KeyboardShortcuts'; |
| 337 | + |
| 338 | +test('registers and executes shortcut', () => { |
| 339 | + const manager = new KeyboardShortcutManager(); |
| 340 | + const mockAction = jest.fn(); |
| 341 | + |
| 342 | + manager.register('k', { ctrl: true }, mockAction); |
| 343 | + |
| 344 | + const event = new KeyboardEvent('keydown', { |
| 345 | + key: 'k', |
| 346 | + ctrlKey: true |
| 347 | + }); |
| 348 | + |
| 349 | + manager.handleKeyDown(event); |
| 350 | + |
| 351 | + expect(mockAction).toHaveBeenCalled(); |
| 352 | +}); |
| 353 | +``` |
| 354 | + |
| 355 | +## Troubleshooting |
| 356 | + |
| 357 | +### Shortcuts Not Working |
| 358 | + |
| 359 | +1. **Check console for conflicts**: The manager logs conflicts when registering duplicate shortcuts |
| 360 | +2. **Verify enabledcondition**: Commands with `enabled: () => false` won't execute |
| 361 | +3. **Check input focus**: Shortcuts are disabled in text inputs by default |
| 362 | +4. **Inspect event listeners**: Ensure the global keydown listener is attached |
| 363 | + |
| 364 | +### Command Palette Empty |
| 365 | + |
| 366 | +1. **Verify command registration**: Check `commandRegistry.getAll()` in console |
| 367 | +2. **Check visible conditions**: Commands with `visible: () => false` won't appear |
| 368 | +3. **Clear browser cache**: Recent commands stored in localStorage may cause issues |
| 369 | + |
| 370 | +### Platform-Specific Issues |
| 371 | + |
| 372 | +- **Mac**: Uses `⌘` (Command) key instead of Ctrl |
| 373 | +- **Detection**: Based on `navigator.platform.toUpperCase().indexOf('MAC')` |
| 374 | +- **Both supported**: `event.ctrlKey || event.metaKey` handles both |
| 375 | + |
| 376 | +## Future Enhancements |
| 377 | + |
| 378 | +- [ ] User-customizable shortcut mappings |
| 379 | +- [ ] Macro recording (repeat action sequences) |
| 380 | +- [ ] Vim-mode keybindings |
| 381 | +- [ ] Quick command history (Ctrl+P style) |
| 382 | +- [ ] Contextual command suggestions |
| 383 | +- [ ] Shortcut conflict resolution UI |
| 384 | +- [ ] Export/import shortcut configurations |
| 385 | +- [ ] Workspace-specific shortcuts |
| 386 | + |
| 387 | +## Resources |
| 388 | + |
| 389 | +- **VS Code Shortcuts**: Inspiration for command palette UX |
| 390 | +- **Figma Shortcuts**: Reference for design tool workflows |
| 391 | +- **Material-UI**: Component library used for dialogs |
| 392 | +- **Web Keyboard API**: MDN documentation for key events |
0 commit comments