Files
paint-ui/components/editor/shortcuts-help-panel.tsx
Sebastian Krüger 5c4763cb62 feat(phase-12): add professional UI polish with status bar, navigator, and shortcuts help
Implements comprehensive quality-of-life improvements for professional editing experience:

**1. Status Bar Component** (`components/editor/status-bar.tsx`):
- Real-time canvas dimensions display (width × height)
- Live zoom percentage indicator
- Dynamic cursor position tracking in canvas coordinates
- FPS counter for performance monitoring
- Memory usage display (when browser supports performance.memory)
- Icons for each metric (Maximize2, ZoomIn, MousePointer, Activity, HardDrive)
- Fixed bottom position with clean UI
- Updates at 60 FPS for smooth cursor tracking
- Memory updates every 2 seconds to reduce overhead

**2. Mini-Map / Navigator** (`components/canvas/mini-map.tsx`):
- Live thumbnail preview of entire canvas
- Renders all visible layers with proper stacking order
- Checkerboard background for transparency visualization
- Interactive viewport indicator (blue rectangle with semi-transparent fill)
- Click or drag to pan viewport to different canvas areas
- Collapsible with expand/minimize toggle button
- Maintains canvas aspect ratio (max 200px)
- Positioned in bottom-right corner as floating overlay
- Zoom percentage display at bottom
- Smart scaling for optimal thumbnail size
- Cursor changes to pointer/grabbing during interaction

**3. Keyboard Shortcuts Help Panel** (`components/editor/shortcuts-help-panel.tsx`):
- Comprehensive list of 40+ keyboard shortcuts
- 7 categories: File, Edit, View, Tools, Layers, Transform, Adjustments, Help
- Real-time search filtering (searches action, category, keys, description)
- Beautiful kbd element styling for shortcut keys
- Modal overlay with backdrop blur
- Opens with `?` or `F1` keys
- Closes with `Esc` key or backdrop click
- Fully responsive with scrollable content
- Organized sections with category headers
- Shows key combinations with proper separators (+)
- Optional descriptions for special shortcuts (e.g., "Hold to pan")
- Footer with helpful hints

**Integration Changes:**

**Canvas Component** (`canvas-with-tools.tsx`):
- Added `onCursorMove` prop callback for cursor position reporting
- Modified `handlePointerMove` to report canvas coordinates
- Created `handlePointerLeave` to clear cursor when leaving canvas
- Integrated MiniMap component as overlay

**Editor Layout** (`editor-layout.tsx`):
- Added cursor position state management
- Integrated StatusBar at bottom of layout
- Added ShortcutsHelpPanel with state management
- Keyboard event handlers for `?` and `F1` to open shortcuts
- Cursor position passed down to CanvasWithTools and up to StatusBar

**Features:**
- Non-intrusive overlays that don't block canvas interaction
- All components optimized for performance
- Responsive design adapts to different screen sizes
- Professional appearance matching app theme
- Smooth animations and transitions
- Real-time updates without lag

**User Experience Improvements:**
- Quick access to all shortcuts via `?` or `F1`
- Always-visible status information in bottom bar
- Easy canvas navigation with mini-map
- Performance monitoring at a glance
- Professional editor feel with polished UI

All features tested and working smoothly with no performance impact.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 17:51:32 +01:00

221 lines
9.2 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { X, Search, Keyboard } from 'lucide-react';
import { cn } from '@/lib/utils';
interface Shortcut {
category: string;
action: string;
keys: string[];
description?: string;
}
const SHORTCUTS: Shortcut[] = [
// File Operations
{ category: 'File', action: 'New', keys: ['Ctrl', 'N'] },
{ category: 'File', action: 'Open', keys: ['Ctrl', 'O'] },
{ category: 'File', action: 'Save', keys: ['Ctrl', 'S'] },
{ category: 'File', action: 'Save As', keys: ['Ctrl', 'Shift', 'S'] },
{ category: 'File', action: 'Export', keys: ['Ctrl', 'E'] },
// Edit Operations
{ category: 'Edit', action: 'Undo', keys: ['Ctrl', 'Z'] },
{ category: 'Edit', action: 'Redo', keys: ['Ctrl', 'Shift', 'Z'] },
{ category: 'Edit', action: 'Cut', keys: ['Ctrl', 'X'] },
{ category: 'Edit', action: 'Copy', keys: ['Ctrl', 'C'] },
{ category: 'Edit', action: 'Paste', keys: ['Ctrl', 'V'] },
{ category: 'Edit', action: 'Select All', keys: ['Ctrl', 'A'] },
{ category: 'Edit', action: 'Deselect', keys: ['Ctrl', 'D'] },
// View Operations
{ category: 'View', action: 'Zoom In', keys: ['Ctrl', '+'] },
{ category: 'View', action: 'Zoom Out', keys: ['Ctrl', '-'] },
{ category: 'View', action: 'Zoom to 100%', keys: ['Ctrl', '0'] },
{ category: 'View', action: 'Fit to Screen', keys: ['Ctrl', '1'] },
{ category: 'View', action: 'Toggle Grid', keys: ['Ctrl', 'G'] },
// Tools (Single Key)
{ category: 'Tools', action: 'Move Tool', keys: ['V'] },
{ category: 'Tools', action: 'Rectangle Select', keys: ['M'] },
{ category: 'Tools', action: 'Lasso Select', keys: ['L'] },
{ category: 'Tools', action: 'Wand Select', keys: ['W'] },
{ category: 'Tools', action: 'Pencil Tool', keys: ['P'] },
{ category: 'Tools', action: 'Brush Tool', keys: ['B'] },
{ category: 'Tools', action: 'Eraser Tool', keys: ['E'] },
{ category: 'Tools', action: 'Fill Bucket', keys: ['G'] },
{ category: 'Tools', action: 'Eyedropper', keys: ['I'] },
{ category: 'Tools', action: 'Text Tool', keys: ['T'] },
{ category: 'Tools', action: 'Shape Tool', keys: ['U'] },
{ category: 'Tools', action: 'Hand Tool (Pan)', keys: ['H'] },
{ category: 'Tools', action: 'Temporary Hand', keys: ['Space'], description: 'Hold to pan' },
// Layers
{ category: 'Layers', action: 'New Layer', keys: ['Ctrl', 'Shift', 'N'] },
{ category: 'Layers', action: 'Duplicate Layer', keys: ['Ctrl', 'J'] },
{ category: 'Layers', action: 'Merge Down', keys: ['Ctrl', 'E'] },
{ category: 'Layers', action: 'Next Layer', keys: ['['] },
{ category: 'Layers', action: 'Previous Layer', keys: [']'] },
// Transform
{ category: 'Transform', action: 'Free Transform', keys: ['Ctrl', 'T'] },
{ category: 'Transform', action: 'Rotate 90° CW', keys: ['Ctrl', 'R'] },
{ category: 'Transform', action: 'Flip Horizontal', keys: ['Ctrl', 'Shift', 'H'] },
{ category: 'Transform', action: 'Flip Vertical', keys: ['Ctrl', 'Shift', 'V'] },
// Adjustments
{ category: 'Adjustments', action: 'Brightness/Contrast', keys: ['Ctrl', 'M'] },
{ category: 'Adjustments', action: 'Hue/Saturation', keys: ['Ctrl', 'U'] },
{ category: 'Adjustments', action: 'Invert Colors', keys: ['Ctrl', 'I'] },
// Help
{ category: 'Help', action: 'Keyboard Shortcuts', keys: ['?'] },
{ category: 'Help', action: 'Keyboard Shortcuts (Alt)', keys: ['F1'] },
];
interface ShortcutsHelpPanelProps {
isOpen: boolean;
onClose: () => void;
}
export function ShortcutsHelpPanel({ isOpen, onClose }: ShortcutsHelpPanelProps) {
const [searchQuery, setSearchQuery] = useState('');
// Handle Escape key to close
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;
// Filter shortcuts based on search
const filteredShortcuts = SHORTCUTS.filter((shortcut) => {
const query = searchQuery.toLowerCase();
return (
shortcut.action.toLowerCase().includes(query) ||
shortcut.category.toLowerCase().includes(query) ||
shortcut.keys.some((key) => key.toLowerCase().includes(query)) ||
shortcut.description?.toLowerCase().includes(query)
);
});
// Group by category
const categories = Array.from(new Set(filteredShortcuts.map((s) => s.category)));
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Panel */}
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
<div className="w-full max-w-3xl max-h-[80vh] bg-card border border-border rounded-lg shadow-xl flex flex-col pointer-events-auto">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border">
<div className="flex items-center gap-2">
<Keyboard className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold text-foreground">Keyboard Shortcuts</h2>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-accent rounded-md transition-colors"
aria-label="Close"
>
<X className="h-4 w-4" />
</button>
</div>
{/* Search */}
<div className="p-4 border-b border-border">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
type="text"
placeholder="Search shortcuts..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-md text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
autoFocus
/>
</div>
</div>
{/* Shortcuts List */}
<div className="flex-1 overflow-y-auto p-4">
{categories.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
No shortcuts found matching &quot;{searchQuery}&quot;
</div>
) : (
<div className="space-y-6">
{categories.map((category) => {
const categoryShortcuts = filteredShortcuts.filter(
(s) => s.category === category
);
return (
<div key={category}>
<h3 className="text-sm font-semibold text-foreground mb-3 uppercase tracking-wide">
{category}
</h3>
<div className="space-y-2">
{categoryShortcuts.map((shortcut, index) => (
<div
key={`${category}-${index}`}
className="flex items-center justify-between py-2 px-3 rounded-md hover:bg-accent/50 transition-colors"
>
<div className="flex-1">
<div className="text-sm text-foreground">{shortcut.action}</div>
{shortcut.description && (
<div className="text-xs text-muted-foreground mt-0.5">
{shortcut.description}
</div>
)}
</div>
<div className="flex items-center gap-1">
{shortcut.keys.map((key, keyIndex) => (
<div key={keyIndex} className="flex items-center gap-1">
<kbd className="px-2 py-1 text-xs font-mono bg-background border border-border rounded shadow-sm">
{key}
</kbd>
{keyIndex < shortcut.keys.length - 1 && (
<span className="text-muted-foreground">+</span>
)}
</div>
))}
</div>
</div>
))}
</div>
</div>
);
})}
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-border bg-accent/30">
<p className="text-xs text-center text-muted-foreground">
Press <kbd className="px-1.5 py-0.5 text-xs font-mono bg-background border border-border rounded">Esc</kbd> to close
or <kbd className="px-1.5 py-0.5 text-xs font-mono bg-background border border-border rounded">?</kbd> / <kbd className="px-1.5 py-0.5 text-xs font-mono bg-background border border-border rounded">F1</kbd> to open
</p>
</div>
</div>
</div>
</>
);
}