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 { ToolType, ToolSettings, ToolState } from '@/types';
interface ToolStore extends ToolState {
@@ -31,77 +32,89 @@ const DEFAULT_SETTINGS: ToolSettings = {
spacing: 0.25,
};
export const useToolStore = create<ToolStore>((set) => ({
activeTool: 'brush',
settings: { ...DEFAULT_SETTINGS },
cursor: 'crosshair',
export const useToolStore = create<ToolStore>()(
persist(
(set) => ({
activeTool: 'brush',
settings: { ...DEFAULT_SETTINGS },
cursor: 'crosshair',
setActiveTool: (tool) => {
const cursors: Record<ToolType, string> = {
select: 'crosshair',
move: 'move',
pencil: 'crosshair',
brush: 'crosshair',
eraser: 'crosshair',
fill: 'crosshair',
eyedropper: 'crosshair',
text: 'text',
shape: 'crosshair',
crop: 'crosshair',
clone: 'crosshair',
blur: 'crosshair',
sharpen: 'crosshair',
};
setActiveTool: (tool) => {
const cursors: Record<ToolType, string> = {
select: 'crosshair',
move: 'move',
pencil: 'crosshair',
brush: 'crosshair',
eraser: 'crosshair',
fill: 'crosshair',
eyedropper: 'crosshair',
text: 'text',
shape: 'crosshair',
crop: 'crosshair',
clone: 'crosshair',
blur: 'crosshair',
sharpen: 'crosshair',
};
set({
activeTool: tool,
cursor: cursors[tool],
});
},
set({
activeTool: tool,
cursor: cursors[tool],
});
},
updateSettings: (settings) => {
set((state) => ({
settings: { ...state.settings, ...settings },
}));
},
updateSettings: (settings) => {
set((state) => ({
settings: { ...state.settings, ...settings },
}));
},
setSize: (size) => {
set((state) => ({
settings: { ...state.settings, size: Math.max(1, Math.min(1000, size)) },
}));
},
setSize: (size) => {
set((state) => ({
settings: { ...state.settings, size: Math.max(1, Math.min(1000, size)) },
}));
},
setOpacity: (opacity) => {
set((state) => ({
settings: { ...state.settings, opacity: Math.max(0, Math.min(1, opacity)) },
}));
},
setOpacity: (opacity) => {
set((state) => ({
settings: { ...state.settings, opacity: Math.max(0, Math.min(1, opacity)) },
}));
},
setHardness: (hardness) => {
set((state) => ({
settings: { ...state.settings, hardness: Math.max(0, Math.min(1, hardness)) },
}));
},
setHardness: (hardness) => {
set((state) => ({
settings: { ...state.settings, hardness: Math.max(0, Math.min(1, hardness)) },
}));
},
setColor: (color) => {
set((state) => ({
settings: { ...state.settings, color },
}));
},
setColor: (color) => {
set((state) => ({
settings: { ...state.settings, color },
}));
},
setFlow: (flow) => {
set((state) => ({
settings: { ...state.settings, flow: Math.max(0, Math.min(1, flow)) },
}));
},
setFlow: (flow) => {
set((state) => ({
settings: { ...state.settings, flow: Math.max(0, Math.min(1, flow)) },
}));
},
setSpacing: (spacing) => {
set((state) => ({
settings: { ...state.settings, spacing: Math.max(0.01, Math.min(10, spacing)) },
}));
},
setSpacing: (spacing) => {
set((state) => ({
settings: { ...state.settings, spacing: Math.max(0.01, Math.min(10, spacing)) },
}));
},
resetSettings: () => {
set({ settings: { ...DEFAULT_SETTINGS } });
},
}));
resetSettings: () => {
set({ settings: { ...DEFAULT_SETTINGS } });
},
}),
{
name: 'tool-storage',
partialize: (state) => ({
activeTool: state.activeTool,
settings: state.settings,
// Exclude cursor - it's derived from activeTool
}),
}
)
);