feat: add comprehensive keyboard shortcuts system
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<ConversionFormat[]>([]);
|
||||
const [conversionJobs, setConversionJobs] = React.useState<ConversionJob[]>([]);
|
||||
const [conversionOptions, setConversionOptions] = React.useState<ConversionOptions>({});
|
||||
const [showShortcutsModal, setShowShortcutsModal] = React.useState(false);
|
||||
|
||||
const fileInputRef = React.useRef<HTMLInputElement>(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 (
|
||||
<div className="w-full max-w-4xl mx-auto space-y-6">
|
||||
{/* 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() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Keyboard Shortcuts Button */}
|
||||
<Button
|
||||
onClick={() => setShowShortcutsModal(true)}
|
||||
className="fixed bottom-6 right-6 rounded-full w-12 h-12 p-0 shadow-lg"
|
||||
title="Keyboard Shortcuts (Ctrl+/)"
|
||||
>
|
||||
<Keyboard className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
{/* Keyboard Shortcuts Modal */}
|
||||
<KeyboardShortcutsModal
|
||||
shortcuts={shortcuts}
|
||||
isOpen={showShortcutsModal}
|
||||
onClose={() => setShowShortcutsModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface FileUploadProps {
|
||||
accept?: string;
|
||||
maxSizeMB?: number;
|
||||
disabled?: boolean;
|
||||
inputRef?: React.RefObject<HTMLInputElement | null>;
|
||||
}
|
||||
|
||||
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<HTMLInputElement>(null);
|
||||
const fileInputRef = inputRef || React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleDragEnter = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user