feat: implement UI state persistence and theme toggle

Major improvements to UI state management and user preferences:

- Add theme toggle with dark/light mode support
- Implement Zustand persist middleware for UI state
- Add ui-store for panel layout preferences (dock width, heights, tabs)
- Persist tool settings (active tool, size, opacity, hardness, etc.)
- Persist canvas view preferences (grid, rulers, snap-to-grid)
- Persist shape tool settings
- Persist collapsible section states
- Fix canvas coordinate transformation for centered rendering
- Constrain checkerboard and grid to canvas bounds
- Add icons to all tab buttons and collapsible sections
- Restructure panel-dock to use persisted state

Storage impact: ~3.5KB total across all preferences
Storage keys: tool-storage, canvas-view-storage, shape-storage, ui-storage

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-21 09:03:14 +01:00
parent 50bfd2940f
commit cd59f0606b
17 changed files with 570 additions and 264 deletions

View File

@@ -1,4 +1,5 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { CanvasState, CanvasSelection, Point } from '@/types';
interface CanvasStore extends CanvasState {
@@ -38,7 +39,7 @@ interface CanvasStore extends CanvasState {
/** Clear selection */
clearSelection: () => void;
/** Convert screen coordinates to canvas coordinates */
screenToCanvas: (screenX: number, screenY: number) => Point;
screenToCanvas: (screenX: number, screenY: number, containerWidth?: number, containerHeight?: number) => Point;
/** Convert canvas coordinates to screen coordinates */
canvasToScreen: (canvasX: number, canvasY: number) => Point;
}
@@ -49,24 +50,26 @@ const MIN_ZOOM = 0.1;
const MAX_ZOOM = 10;
const ZOOM_STEP = 1.2;
export const useCanvasStore = create<CanvasStore>((set, get) => ({
width: DEFAULT_CANVAS_WIDTH,
height: DEFAULT_CANVAS_HEIGHT,
zoom: 1,
offsetX: 0,
offsetY: 0,
backgroundColor: '#ffffff',
showGrid: false,
gridSize: 20,
showRulers: true,
snapToGrid: false,
selection: {
active: false,
x: 0,
y: 0,
width: 0,
height: 0,
},
export const useCanvasStore = create<CanvasStore>()(
persist(
(set, get) => ({
width: DEFAULT_CANVAS_WIDTH,
height: DEFAULT_CANVAS_HEIGHT,
zoom: 1,
offsetX: 0,
offsetY: 0,
backgroundColor: '#ffffff',
showGrid: false,
gridSize: 20,
showRulers: true,
snapToGrid: false,
selection: {
active: false,
x: 0,
y: 0,
width: 0,
height: 0,
},
setDimensions: (width, height) => {
set({ width, height });
@@ -153,11 +156,20 @@ export const useCanvasStore = create<CanvasStore>((set, get) => ({
});
},
screenToCanvas: (screenX, screenY) => {
const { zoom, offsetX, offsetY } = get();
screenToCanvas: (screenX, screenY, containerWidth = 0, containerHeight = 0) => {
const { zoom, offsetX, offsetY, width, height } = get();
// The canvas is rendered with this transformation:
// 1. translate(offsetX + containerWidth/2, offsetY + containerHeight/2) - center in viewport with offset
// 2. scale(zoom) - apply zoom
// 3. translate(-width/2, -height/2) - position canvas so (0,0) is at top-left
//
// To reverse:
// 1. Subtract container center and offset
// 2. Divide by zoom
// 3. Add canvas center
return {
x: (screenX - offsetX) / zoom,
y: (screenY - offsetY) / zoom,
x: (screenX - containerWidth / 2 - offsetX) / zoom + width / 2,
y: (screenY - containerHeight / 2 - offsetY) / zoom + height / 2,
};
},
@@ -168,4 +180,17 @@ export const useCanvasStore = create<CanvasStore>((set, get) => ({
y: canvasY * zoom + offsetY,
};
},
}));
}),
{
name: 'canvas-view-storage',
partialize: (state) => ({
backgroundColor: state.backgroundColor,
showGrid: state.showGrid,
gridSize: state.gridSize,
showRulers: state.showRulers,
snapToGrid: state.snapToGrid,
// Exclude: width, height, zoom, offsetX, offsetY, selection
}),
}
)
);