feat(ui/perf): implement loading states, keyboard navigation, and lazy-loaded tools

Add comprehensive UX and performance improvements:

**Loading States & Feedback:**
- Add loading overlay with spinner and custom messages
- Integrate loading states into all file operations (open, save, export)
- Create loading-store.ts for centralized loading state management

**Keyboard Navigation:**
- Expand keyboard shortcuts to include tool selection (1-7)
- Add layer navigation with Arrow Up/Down
- Add layer operations (Ctrl+D duplicate, Delete/Backspace remove)
- Display keyboard shortcuts in tool tooltips
- Enhanced keyboard shortcut system with proper key conflict handling

**Performance - Code Splitting:**
- Implement dynamic tool loader with lazy loading
- Tools load on-demand when first selected
- Preload common tools (pencil, brush, eraser) for instant access
- Add tool caching to prevent redundant loads
- Reduces initial bundle size and improves startup time

**Integration:**
- Add LoadingOverlay to app layout
- Update canvas-with-tools to use lazy-loaded tool instances
- Add keyboard shortcut hints to tool palette UI

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-21 16:08:24 +01:00
parent 108dfb5cec
commit 2e18f43453
8 changed files with 397 additions and 107 deletions

View File

@@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { useCanvasStore, useLayerStore } from '@/store';
import { useHistoryStore } from '@/store/history-store';
import { useLoadingStore } from '@/store/loading-store';
import {
openImageFile,
exportCanvasAsImage,
@@ -18,6 +19,7 @@ export function useFileOperations() {
const { width, height, setDimensions } = useCanvasStore();
const { layers, createLayer, clearLayers } = useLayerStore();
const { clearHistory } = useHistoryStore();
const { setLoading } = useLoadingStore();
/**
* Create new image
@@ -43,6 +45,7 @@ export function useFileOperations() {
*/
const openImage = useCallback(
async (file: File) => {
setLoading(true, `Opening ${file.name}...`);
try {
const img = await openImageFile(file);
@@ -69,9 +72,11 @@ export function useFileOperations() {
} catch (error) {
console.error('Failed to open image:', error);
toast.error('Failed to open image file');
} finally {
setLoading(false);
}
},
[clearLayers, clearHistory, setDimensions, createLayer]
[clearLayers, clearHistory, setDimensions, createLayer, setLoading]
);
/**
@@ -79,6 +84,7 @@ export function useFileOperations() {
*/
const openProject = useCallback(
async (file: File) => {
setLoading(true, `Opening project ${file.name}...`);
try {
const projectData = await loadProject(file);
@@ -114,9 +120,11 @@ export function useFileOperations() {
} catch (error) {
console.error('Failed to open project:', error);
toast.error('Failed to open project file');
} finally {
setLoading(false);
}
},
[clearLayers, clearHistory, setDimensions, createLayer]
[clearLayers, clearHistory, setDimensions, createLayer, setLoading]
);
/**
@@ -124,6 +132,7 @@ export function useFileOperations() {
*/
const exportImage = useCallback(
async (format: 'png' | 'jpeg' | 'webp', quality: number, filename: string) => {
setLoading(true, `Exporting ${filename}.${format}...`);
try {
// Create temporary canvas with all layers
const tempCanvas = document.createElement('canvas');
@@ -155,9 +164,11 @@ export function useFileOperations() {
} catch (error) {
console.error('Failed to export image:', error);
toast.error('Failed to export image');
} finally {
setLoading(false);
}
},
[layers, width, height]
[layers, width, height, setLoading]
);
/**
@@ -165,15 +176,18 @@ export function useFileOperations() {
*/
const saveProject = useCallback(
async (filename: string) => {
setLoading(true, `Saving project ${filename}.json...`);
try {
await exportProject(layers, width, height, filename);
toast.success(`Saved project ${filename}.json`);
} catch (error) {
console.error('Failed to save project:', error);
toast.error('Failed to save project');
} finally {
setLoading(false);
}
},
[layers, width, height]
[layers, width, height, setLoading]
);
/**