From 3f4fcf39bc8ec632867a1dfc6162933a968a9a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 17 Nov 2025 13:34:11 +0100 Subject: [PATCH] feat: add comprehensive keyboard shortcuts system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keyboard shortcuts: - **Ctrl/Cmd + O**: Open file dialog - **Ctrl/Cmd + Enter**: Start conversion - **Ctrl/Cmd + S**: Download results (ZIP if multiple) - **Ctrl/Cmd + R**: Reset converter - **Ctrl/Cmd + /**: Show keyboard shortcuts help - **?**: Show keyboard shortcuts help - **Escape**: Close shortcuts modal Implementation: - Created useKeyboardShortcuts custom hook - Platform-aware keyboard handling (Cmd on Mac, Ctrl elsewhere) - KeyboardShortcutsModal component with beautiful UI - Floating keyboard icon button (bottom-right) - Visual shortcut display with kbd tags - Context-aware shortcut execution (respects disabled states) - Prevents default browser behavior for shortcuts User experience: - Floating help button always accessible - Clean modal with shortcut descriptions - Platform-specific key symbols (⌘ on Mac) - Shortcuts disabled when modal is open - Clear visual feedback for shortcuts - Non-intrusive button placement Features: - Smart ref forwarding to FileUpload component - Conditional shortcut execution based on app state - Professional kbd styling for key combinations - Responsive modal with backdrop - Smooth animations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- components/converter/FileConverter.tsx | 87 +++++++++++++++++++++++- components/converter/FileUpload.tsx | 4 +- components/ui/KeyboardShortcutsModal.tsx | 66 ++++++++++++++++++ lib/hooks/useKeyboardShortcuts.ts | 65 ++++++++++++++++++ 4 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 components/ui/KeyboardShortcutsModal.tsx create mode 100644 lib/hooks/useKeyboardShortcuts.ts diff --git a/components/converter/FileConverter.tsx b/components/converter/FileConverter.tsx index 8df8f97..b0be09d 100644 --- a/components/converter/FileConverter.tsx +++ b/components/converter/FileConverter.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import { ArrowRight, ArrowDown } from 'lucide-react'; +import { ArrowRight, ArrowDown, Keyboard } from 'lucide-react'; import { Button } from '@/components/ui/Button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'; import { FileUpload } from './FileUpload'; @@ -22,6 +22,8 @@ import { convertWithImageMagick } from '@/lib/converters/imagemagickService'; import { addToHistory } from '@/lib/storage/history'; import { downloadBlobsAsZip, generateOutputFilename } from '@/lib/utils/fileUtils'; import { getPresetById, type FormatPreset } from '@/lib/utils/formatPresets'; +import { useKeyboardShortcuts, type KeyboardShortcut } from '@/lib/hooks/useKeyboardShortcuts'; +import { KeyboardShortcutsModal } from '@/components/ui/KeyboardShortcutsModal'; import type { ConversionJob, ConversionFormat, ConversionOptions } from '@/types/conversion'; export function FileConverter() { @@ -33,6 +35,9 @@ export function FileConverter() { const [compatibleFormats, setCompatibleFormats] = React.useState([]); const [conversionJobs, setConversionJobs] = React.useState([]); const [conversionOptions, setConversionOptions] = React.useState({}); + const [showShortcutsModal, setShowShortcutsModal] = React.useState(false); + + const fileInputRef = React.useRef(null); // Detect input format when files are selected React.useEffect(() => { @@ -364,6 +369,69 @@ export function FileConverter() { const isConvertDisabled = selectedFiles.length === 0 || !outputFormat || isConverting; const completedCount = conversionJobs.filter(job => job.status === 'completed').length; + // Define keyboard shortcuts + const shortcuts: KeyboardShortcut[] = [ + { + key: 'o', + ctrl: true, + description: 'Open file dialog', + action: () => { + if (!isConverting) { + fileInputRef.current?.click(); + } + }, + }, + { + key: 'Enter', + ctrl: true, + description: 'Start conversion', + action: () => { + if (!isConvertDisabled) { + handleConvert(); + } + }, + }, + { + key: 's', + ctrl: true, + description: 'Download results', + action: () => { + if (completedCount > 0) { + handleDownloadAll(); + } + }, + }, + { + key: 'r', + ctrl: true, + description: 'Reset converter', + action: () => { + if (!isConverting) { + handleReset(); + } + }, + }, + { + key: '/', + ctrl: true, + description: 'Show keyboard shortcuts', + action: () => setShowShortcutsModal(true), + }, + { + key: 'Escape', + description: 'Close shortcuts modal', + action: () => setShowShortcutsModal(false), + }, + { + key: '?', + description: 'Show keyboard shortcuts', + action: () => setShowShortcutsModal(true), + }, + ]; + + // Enable keyboard shortcuts + useKeyboardShortcuts(shortcuts, !showShortcutsModal); + return (
{/* Header */} @@ -381,6 +449,7 @@ export function FileConverter() { onFileRemove={handleFileRemove} selectedFiles={selectedFiles} disabled={isConverting} + inputRef={fileInputRef} /> {/* File Info - show first file */} @@ -489,6 +558,22 @@ export function FileConverter() { ))}
)} + + {/* Keyboard Shortcuts Button */} + + + {/* Keyboard Shortcuts Modal */} + setShowShortcutsModal(false)} + /> ); } diff --git a/components/converter/FileUpload.tsx b/components/converter/FileUpload.tsx index d30eaf0..655bbd0 100644 --- a/components/converter/FileUpload.tsx +++ b/components/converter/FileUpload.tsx @@ -13,6 +13,7 @@ export interface FileUploadProps { accept?: string; maxSizeMB?: number; disabled?: boolean; + inputRef?: React.RefObject; } export function FileUpload({ @@ -22,9 +23,10 @@ export function FileUpload({ accept, maxSizeMB = 500, disabled = false, + inputRef, }: FileUploadProps) { const [isDragging, setIsDragging] = React.useState(false); - const fileInputRef = React.useRef(null); + const fileInputRef = inputRef || React.useRef(null); const handleDragEnter = (e: React.DragEvent) => { e.preventDefault(); diff --git a/components/ui/KeyboardShortcutsModal.tsx b/components/ui/KeyboardShortcutsModal.tsx new file mode 100644 index 0000000..4c42afd --- /dev/null +++ b/components/ui/KeyboardShortcutsModal.tsx @@ -0,0 +1,66 @@ +'use client'; + +import * as React from 'react'; +import { X, Keyboard } from 'lucide-react'; +import { Button } from './Button'; +import { Card } from './Card'; +import { formatShortcut, type KeyboardShortcut } from '@/lib/hooks/useKeyboardShortcuts'; + +interface KeyboardShortcutsModalProps { + shortcuts: KeyboardShortcut[]; + isOpen: boolean; + onClose: () => void; +} + +export function KeyboardShortcutsModal({ shortcuts, isOpen, onClose }: KeyboardShortcutsModalProps) { + if (!isOpen) return null; + + return ( + <> + {/* Backdrop */} +
+ + {/* Modal */} +
+ +
+ {/* Header */} +
+
+ +

Keyboard Shortcuts

+
+ +
+ + {/* Shortcuts List */} +
+ {shortcuts.map((shortcut, index) => ( +
+ {shortcut.description} + + {formatShortcut(shortcut)} + +
+ ))} +
+ + {/* Footer */} +
+ Press Esc or{' '} + ? to close +
+
+
+
+ + ); +} diff --git a/lib/hooks/useKeyboardShortcuts.ts b/lib/hooks/useKeyboardShortcuts.ts new file mode 100644 index 0000000..611dc94 --- /dev/null +++ b/lib/hooks/useKeyboardShortcuts.ts @@ -0,0 +1,65 @@ +import { useEffect } from 'react'; + +export interface KeyboardShortcut { + key: string; + ctrl?: boolean; + alt?: boolean; + shift?: boolean; + description: string; + action: () => void; +} + +/** + * Hook for managing keyboard shortcuts + */ +export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[], enabled: boolean = true) { + useEffect(() => { + if (!enabled) return; + + const handleKeyDown = (event: KeyboardEvent) => { + // Find matching shortcut + const shortcut = shortcuts.find((s) => { + const keyMatch = s.key.toLowerCase() === event.key.toLowerCase(); + const ctrlMatch = s.ctrl ? (event.ctrlKey || event.metaKey) : !event.ctrlKey && !event.metaKey; + const altMatch = s.alt ? event.altKey : !event.altKey; + const shiftMatch = s.shift ? event.shiftKey : !event.shiftKey; + + return keyMatch && ctrlMatch && altMatch && shiftMatch; + }); + + if (shortcut) { + event.preventDefault(); + shortcut.action(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [shortcuts, enabled]); +} + +/** + * Format shortcut key combination for display + */ +export function formatShortcut(shortcut: KeyboardShortcut): string { + const parts: string[] = []; + + // Use Cmd on Mac, Ctrl on others + const isMac = typeof window !== 'undefined' && /Mac|iPhone|iPod|iPad/.test(navigator.platform); + + if (shortcut.ctrl) { + parts.push(isMac ? '⌘' : 'Ctrl'); + } + + if (shortcut.alt) { + parts.push(isMac ? '⌥' : 'Alt'); + } + + if (shortcut.shift) { + parts.push(isMac ? '⇧' : 'Shift'); + } + + parts.push(shortcut.key.toUpperCase()); + + return parts.join(' + '); +}