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:
@@ -102,8 +102,8 @@ export function CanvasWithTools() {
|
||||
ctx.scale(zoom, zoom);
|
||||
ctx.translate(-width / 2, -height / 2);
|
||||
|
||||
// Draw checkerboard background
|
||||
drawCheckerboard(ctx, 10, '#ffffff', '#e0e0e0');
|
||||
// Draw checkerboard background (only within canvas bounds)
|
||||
drawCheckerboard(ctx, 10, '#ffffff', '#e0e0e0', width, height);
|
||||
|
||||
// Draw background color if not transparent
|
||||
if (backgroundColor && backgroundColor !== 'transparent') {
|
||||
@@ -127,9 +127,9 @@ export function CanvasWithTools() {
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
|
||||
// Draw grid if enabled
|
||||
// Draw grid if enabled (only within canvas bounds)
|
||||
if (showGrid) {
|
||||
drawGrid(ctx, gridSize, 'rgba(0, 0, 0, 0.15)');
|
||||
drawGrid(ctx, gridSize, 'rgba(0, 0, 0, 0.15)', width, height);
|
||||
}
|
||||
|
||||
// Draw selection if active (marching ants)
|
||||
@@ -172,7 +172,7 @@ export function CanvasWithTools() {
|
||||
|
||||
const screenX = e.clientX - rect.left;
|
||||
const screenY = e.clientY - rect.top;
|
||||
const canvasPos = screenToCanvas(screenX, screenY);
|
||||
const canvasPos = screenToCanvas(screenX, screenY, rect.width, rect.height);
|
||||
|
||||
// Check for panning
|
||||
if (e.button === 1 || (e.button === 0 && e.shiftKey)) {
|
||||
@@ -259,7 +259,7 @@ export function CanvasWithTools() {
|
||||
|
||||
const screenX = e.clientX - rect.left;
|
||||
const screenY = e.clientY - rect.top;
|
||||
const canvasPos = screenToCanvas(screenX, screenY);
|
||||
const canvasPos = screenToCanvas(screenX, screenY, rect.width, rect.height);
|
||||
|
||||
// Panning
|
||||
if (isPanning) {
|
||||
|
||||
@@ -51,8 +51,8 @@ export function CanvasWrapper() {
|
||||
ctx.scale(zoom, zoom);
|
||||
ctx.translate(-width / 2, -height / 2);
|
||||
|
||||
// Draw checkerboard background
|
||||
drawCheckerboard(ctx, 10, '#ffffff', '#e0e0e0');
|
||||
// Draw checkerboard background (only within canvas bounds)
|
||||
drawCheckerboard(ctx, 10, '#ffffff', '#e0e0e0', width, height);
|
||||
|
||||
// Draw background color if not transparent
|
||||
if (backgroundColor && backgroundColor !== 'transparent') {
|
||||
@@ -76,9 +76,9 @@ export function CanvasWrapper() {
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
|
||||
// Draw grid if enabled
|
||||
// Draw grid if enabled (only within canvas bounds)
|
||||
if (showGrid) {
|
||||
drawGrid(ctx, gridSize, 'rgba(0, 0, 0, 0.15)');
|
||||
drawGrid(ctx, gridSize, 'rgba(0, 0, 0, 0.15)', width, height);
|
||||
}
|
||||
|
||||
// Draw selection if active
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useColorStore } from '@/store/color-store';
|
||||
import { useToolStore } from '@/store';
|
||||
import { ColorPicker } from './color-picker';
|
||||
import { ColorSwatches } from './color-swatches';
|
||||
import { ArrowLeftRight, Palette, Clock } from 'lucide-react';
|
||||
import { ArrowLeftRight, Palette, Clock, Grid3x3 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type Tab = 'picker' | 'swatches' | 'recent';
|
||||
@@ -32,15 +32,10 @@ export function ColorPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-card border-r border-border w-64">
|
||||
{/* Header */}
|
||||
<div className="border-b border-border p-3">
|
||||
<h2 className="text-sm font-semibold text-card-foreground">Colors</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col h-full bg-card w-full">
|
||||
{/* Current colors display */}
|
||||
<div className="p-3 border-b border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="border-b border-border">
|
||||
<div className="flex items-center gap-2 p-3">
|
||||
{/* Primary/Secondary color squares */}
|
||||
<div className="relative">
|
||||
<button
|
||||
@@ -75,42 +70,45 @@ export function ColorPanel() {
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-border">
|
||||
<button
|
||||
onClick={() => setActiveTab('picker')}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2 text-sm transition-colors',
|
||||
activeTab === 'picker'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Palette className="h-4 w-4" />
|
||||
Picker
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('swatches')}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2 text-sm transition-colors',
|
||||
activeTab === 'swatches'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
Swatches
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('recent')}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2 text-sm transition-colors',
|
||||
activeTab === 'recent'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
Recent
|
||||
</button>
|
||||
<div className="border-b border-border">
|
||||
<div className="flex">
|
||||
<button
|
||||
onClick={() => setActiveTab('picker')}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2 text-sm transition-colors',
|
||||
activeTab === 'picker'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Palette className="h-4 w-4" />
|
||||
Picker
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('swatches')}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2 text-sm transition-colors',
|
||||
activeTab === 'swatches'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Grid3x3 className="h-4 w-4" />
|
||||
Swatches
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('recent')}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2 text-sm transition-colors',
|
||||
activeTab === 'recent'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
Recent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CanvasWithTools } from '@/components/canvas/canvas-with-tools';
|
||||
import { FileMenu } from './file-menu';
|
||||
import { ToolOptions } from './tool-options';
|
||||
import { PanelDock } from './panel-dock';
|
||||
import { ThemeToggle } from './theme-toggle';
|
||||
import { ToolPalette } from '@/components/tools';
|
||||
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts';
|
||||
import { useFileOperations } from '@/hooks/use-file-operations';
|
||||
@@ -155,6 +156,11 @@ export function EditorLayout() {
|
||||
|
||||
<div className="w-px h-6 bg-border mx-1" />
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<ThemeToggle />
|
||||
|
||||
<div className="w-px h-6 bg-border mx-1" />
|
||||
|
||||
{/* New Layer */}
|
||||
<button
|
||||
onClick={handleNewLayer}
|
||||
@@ -167,7 +173,7 @@ export function EditorLayout() {
|
||||
</div>
|
||||
|
||||
{/* Tool Adjustments Bar */}
|
||||
<div className="flex h-10 items-center border-b border-border bg-card/50 px-4">
|
||||
<div className="flex h-10 items-center border-b border-border bg-card/50">
|
||||
<ToolOptions />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,13 +8,6 @@ export function HistoryPanel() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-card">
|
||||
<div className="border-b border-border p-3">
|
||||
<h2 className="flex items-center gap-2 text-sm font-semibold text-card-foreground">
|
||||
<History className="h-4 w-4" />
|
||||
History
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-2 space-y-2">
|
||||
{undoStack.length === 0 && redoStack.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { ChevronDown, ChevronRight, Wand2, Square, Move, Hexagon, History, Sliders, Wrench } from 'lucide-react';
|
||||
import { useUIStore } from '@/store';
|
||||
import { LayersPanel } from '@/components/layers/layers-panel';
|
||||
import { ColorPanel } from '@/components/colors';
|
||||
import { FilterPanel } from '@/components/filters';
|
||||
@@ -12,73 +13,208 @@ import { HistoryPanel } from './history-panel';
|
||||
|
||||
interface CollapsibleSectionProps {
|
||||
title: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
children: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
id: 'filters' | 'selection' | 'transform' | 'shapeSettings';
|
||||
}
|
||||
|
||||
function CollapsibleSection({ title, children, defaultOpen = true }: CollapsibleSectionProps) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
function CollapsibleSection({ title, icon: Icon, children, id }: CollapsibleSectionProps) {
|
||||
const { collapsed, toggleCollapsed } = useUIStore();
|
||||
const isOpen = collapsed[id];
|
||||
|
||||
return (
|
||||
<div className="border-b border-border">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-full flex items-center justify-between p-3 hover:bg-accent/50 transition-colors"
|
||||
onClick={() => toggleCollapsed(id)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<h3 className="text-sm font-semibold text-card-foreground">{title}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{Icon && <Icon className="h-4 w-4 text-primary" />}
|
||||
<h3 className="text-sm font-semibold text-card-foreground">{title}</h3>
|
||||
</div>
|
||||
{isOpen ? (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
{isOpen && <div className="pb-2">{children}</div>}
|
||||
{isOpen && <div>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PanelDock() {
|
||||
const [activeTab, setActiveTab] = useState<'adjustments' | 'tools' | 'history'>('adjustments');
|
||||
const {
|
||||
panelDock,
|
||||
setActiveTab,
|
||||
setPanelWidth,
|
||||
setLayersHeight,
|
||||
setColorsHeight,
|
||||
} = useUIStore();
|
||||
const { activeTab, width, layersHeight, colorsHeight } = panelDock;
|
||||
|
||||
const [isResizingWidth, setIsResizingWidth] = useState(false);
|
||||
const [isResizingLayersHeight, setIsResizingLayersHeight] = useState(false);
|
||||
const [isResizingColorsHeight, setIsResizingColorsHeight] = useState(false);
|
||||
const dockRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle width resize
|
||||
useEffect(() => {
|
||||
if (!isResizingWidth) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!dockRef.current) return;
|
||||
const containerRect = dockRef.current.parentElement?.getBoundingClientRect();
|
||||
if (!containerRect) return;
|
||||
|
||||
const newWidth = containerRect.right - e.clientX;
|
||||
setPanelWidth(newWidth);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizingWidth(false);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isResizingWidth]);
|
||||
|
||||
// Handle layers height resize
|
||||
useEffect(() => {
|
||||
if (!isResizingLayersHeight) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!dockRef.current) return;
|
||||
const dockRect = dockRef.current.getBoundingClientRect();
|
||||
const relativeY = e.clientY - dockRect.top;
|
||||
const percentage = (relativeY / dockRect.height) * 100;
|
||||
setLayersHeight(percentage);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizingLayersHeight(false);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isResizingLayersHeight]);
|
||||
|
||||
// Handle colors height resize
|
||||
useEffect(() => {
|
||||
if (!isResizingColorsHeight) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!dockRef.current) return;
|
||||
const dockRect = dockRef.current.getBoundingClientRect();
|
||||
const layersBottom = (layersHeight / 100) * dockRect.height;
|
||||
const relativeY = e.clientY - dockRect.top - layersBottom;
|
||||
const percentage = (relativeY / dockRect.height) * 100;
|
||||
setColorsHeight(percentage);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizingColorsHeight(false);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isResizingColorsHeight, layersHeight]);
|
||||
|
||||
return (
|
||||
<div className="w-[280px] border-l border-border bg-card flex flex-col overflow-hidden">
|
||||
{/* Always visible: Layers Panel */}
|
||||
<div className="border-b border-border" style={{ height: '50%', minHeight: '300px' }}>
|
||||
<div
|
||||
ref={dockRef}
|
||||
className="border-l border-border bg-card flex flex-col overflow-hidden relative"
|
||||
style={{ width: `${width}px` }}
|
||||
>
|
||||
{/* Width resize handle (left edge) */}
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-primary/50 transition-colors z-10"
|
||||
onMouseDown={() => setIsResizingWidth(true)}
|
||||
/>
|
||||
|
||||
{/* Layers Panel */}
|
||||
<div
|
||||
className="overflow-hidden"
|
||||
style={{ height: `${layersHeight}%` }}
|
||||
>
|
||||
<LayersPanel />
|
||||
</div>
|
||||
|
||||
{/* Height resize handle (between layers and colors) */}
|
||||
<div
|
||||
className="h-1 cursor-row-resize hover:bg-primary/50 transition-colors border-b border-border flex-shrink-0"
|
||||
onMouseDown={() => setIsResizingLayersHeight(true)}
|
||||
/>
|
||||
|
||||
{/* Colors Panel */}
|
||||
<div
|
||||
className="overflow-hidden flex flex-col bg-card"
|
||||
style={{ height: `${colorsHeight}%` }}
|
||||
>
|
||||
<div className="border-b border-border p-3 flex-shrink-0">
|
||||
<h2 className="text-sm font-semibold text-card-foreground">Colors</h2>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<ColorPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Height resize handle (between colors and tabs) */}
|
||||
<div
|
||||
className="h-1 cursor-row-resize hover:bg-primary/50 transition-colors border-b border-border flex-shrink-0"
|
||||
onMouseDown={() => setIsResizingColorsHeight(true)}
|
||||
/>
|
||||
|
||||
{/* Tabbed section */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="flex-1 flex flex-col overflow-hidden" style={{ height: `${100 - layersHeight - colorsHeight}%` }}>
|
||||
{/* Tab buttons */}
|
||||
<div className="flex border-b border-border">
|
||||
<button
|
||||
onClick={() => setActiveTab('adjustments')}
|
||||
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
activeTab === 'adjustments'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
|
||||
}`}
|
||||
>
|
||||
<Sliders className="h-4 w-4" />
|
||||
Adjustments
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('tools')}
|
||||
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
activeTab === 'tools'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
|
||||
}`}
|
||||
>
|
||||
<Wrench className="h-4 w-4" />
|
||||
Tools
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('history')}
|
||||
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
activeTab === 'history'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
|
||||
}`}
|
||||
>
|
||||
<History className="h-4 w-4" />
|
||||
History
|
||||
</button>
|
||||
</div>
|
||||
@@ -87,16 +223,13 @@ export function PanelDock() {
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{activeTab === 'adjustments' && (
|
||||
<div>
|
||||
<CollapsibleSection title="Colors">
|
||||
<ColorPanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection title="Filters">
|
||||
<CollapsibleSection title="Filters" icon={Wand2} id="filters">
|
||||
<FilterPanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection title="Selection">
|
||||
<CollapsibleSection title="Selection" icon={Square} id="selection">
|
||||
<SelectionPanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection title="Transform">
|
||||
<CollapsibleSection title="Transform" icon={Move} id="transform">
|
||||
<TransformPanel />
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
@@ -104,7 +237,7 @@ export function PanelDock() {
|
||||
|
||||
{activeTab === 'tools' && (
|
||||
<div>
|
||||
<CollapsibleSection title="Shape Settings">
|
||||
<CollapsibleSection title="Shape Settings" icon={Hexagon} id="shapeSettings">
|
||||
<ShapePanel />
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
|
||||
55
components/editor/theme-toggle.tsx
Normal file
55
components/editor/theme-toggle.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
|
||||
export function ThemeToggle() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const currentTheme = savedTheme === 'dark' || (!savedTheme && prefersDark) ? 'dark' : 'light';
|
||||
setTheme(currentTheme);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme === 'light' ? 'dark' : 'light';
|
||||
setTheme(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
|
||||
if (newTheme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
};
|
||||
|
||||
// Prevent hydration mismatch
|
||||
if (!mounted) {
|
||||
return (
|
||||
<button
|
||||
className="rounded-md p-2 text-muted-foreground"
|
||||
disabled
|
||||
>
|
||||
<Sun className="h-4 w-4" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="rounded-md p-2 hover:bg-accent transition-colors"
|
||||
title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
|
||||
>
|
||||
{theme === 'light' ? (
|
||||
<Moon className="h-4 w-4" />
|
||||
) : (
|
||||
<Sun className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -109,13 +109,7 @@ export function FilterPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 border-l border-border bg-card flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 border-b border-border p-3">
|
||||
<Wand2 className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-sm font-semibold">Filters</h2>
|
||||
</div>
|
||||
|
||||
<div className="w-full border-l border-border bg-card flex flex-col">
|
||||
{/* Filter list */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="p-2 space-y-1">
|
||||
|
||||
@@ -128,13 +128,7 @@ export function SelectionPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 border-l border-border bg-card flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 border-b border-border p-3">
|
||||
<Square className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-sm font-semibold">Selection</h2>
|
||||
</div>
|
||||
|
||||
<div className="w-full border-l border-border bg-card flex flex-col">
|
||||
{/* Selection Tools */}
|
||||
<div className="border-b border-border p-3">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase mb-2">
|
||||
|
||||
@@ -63,13 +63,7 @@ export function ShapePanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 border-l border-border bg-card flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 border-b border-border p-3">
|
||||
<Square className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-sm font-semibold">Shapes</h2>
|
||||
</div>
|
||||
|
||||
<div className="w-full border-l border-border bg-card flex flex-col">
|
||||
{/* Shape Types */}
|
||||
<div className="border-b border-border p-3">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase mb-2">
|
||||
|
||||
@@ -51,13 +51,7 @@ export function TransformPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 border-l border-border bg-card flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 border-b border-border p-3">
|
||||
<Move className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-sm font-semibold">Transform</h2>
|
||||
</div>
|
||||
|
||||
<div className="w-full border-l border-border bg-card flex flex-col">
|
||||
{/* Transform Tools */}
|
||||
<div className="border-b border-border p-3">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase mb-2">
|
||||
|
||||
@@ -41,12 +41,15 @@ export function drawCheckerboard(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
squareSize = 10,
|
||||
color1 = '#ffffff',
|
||||
color2 = '#cccccc'
|
||||
color2 = '#cccccc',
|
||||
width?: number,
|
||||
height?: number
|
||||
): void {
|
||||
const { width, height } = ctx.canvas;
|
||||
const w = width ?? ctx.canvas.width;
|
||||
const h = height ?? ctx.canvas.height;
|
||||
|
||||
for (let y = 0; y < height; y += squareSize) {
|
||||
for (let x = 0; x < width; x += squareSize) {
|
||||
for (let y = 0; y < h; y += squareSize) {
|
||||
for (let x = 0; x < w; x += squareSize) {
|
||||
const isEven = (Math.floor(x / squareSize) + Math.floor(y / squareSize)) % 2 === 0;
|
||||
ctx.fillStyle = isEven ? color1 : color2;
|
||||
ctx.fillRect(x, y, squareSize, squareSize);
|
||||
@@ -160,26 +163,29 @@ export async function loadImageFromFile(file: File): Promise<HTMLImageElement> {
|
||||
export function drawGrid(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
gridSize: number,
|
||||
color = 'rgba(0, 0, 0, 0.1)'
|
||||
color = 'rgba(0, 0, 0, 0.1)',
|
||||
width?: number,
|
||||
height?: number
|
||||
): void {
|
||||
const { width, height } = ctx.canvas;
|
||||
const w = width ?? ctx.canvas.width;
|
||||
const h = height ?? ctx.canvas.height;
|
||||
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
// Vertical lines
|
||||
for (let x = 0; x <= width; x += gridSize) {
|
||||
for (let x = 0; x <= w; x += gridSize) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 0.5, 0);
|
||||
ctx.lineTo(x + 0.5, height);
|
||||
ctx.lineTo(x + 0.5, h);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Horizontal lines
|
||||
for (let y = 0; y <= height; y += gridSize) {
|
||||
for (let y = 0; y <= h; y += gridSize) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y + 0.5);
|
||||
ctx.lineTo(width, y + 0.5);
|
||||
ctx.lineTo(w, y + 0.5);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from './color-store';
|
||||
export * from './selection-store';
|
||||
export * from './transform-store';
|
||||
export * from './shape-store';
|
||||
export * from './ui-store';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { ShapeSettings, ShapeType, ShapeStore as IShapeStore } from '@/types/shape';
|
||||
|
||||
const DEFAULT_SETTINGS: ShapeSettings = {
|
||||
@@ -15,66 +16,73 @@ const DEFAULT_SETTINGS: ShapeSettings = {
|
||||
arrowHeadAngle: 30,
|
||||
};
|
||||
|
||||
export const useShapeStore = create<IShapeStore>((set) => ({
|
||||
settings: { ...DEFAULT_SETTINGS },
|
||||
export const useShapeStore = create<IShapeStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
settings: { ...DEFAULT_SETTINGS },
|
||||
|
||||
setShapeType: (type) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, type },
|
||||
})),
|
||||
setShapeType: (type) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, type },
|
||||
})),
|
||||
|
||||
setFill: (fill) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, fill },
|
||||
})),
|
||||
setFill: (fill) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, fill },
|
||||
})),
|
||||
|
||||
setFillColor: (fillColor) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, fillColor },
|
||||
})),
|
||||
setFillColor: (fillColor) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, fillColor },
|
||||
})),
|
||||
|
||||
setStroke: (stroke) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, stroke },
|
||||
})),
|
||||
setStroke: (stroke) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, stroke },
|
||||
})),
|
||||
|
||||
setStrokeColor: (strokeColor) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, strokeColor },
|
||||
})),
|
||||
setStrokeColor: (strokeColor) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, strokeColor },
|
||||
})),
|
||||
|
||||
setStrokeWidth: (strokeWidth) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, strokeWidth: Math.max(1, Math.min(100, strokeWidth)) },
|
||||
})),
|
||||
setStrokeWidth: (strokeWidth) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, strokeWidth: Math.max(1, Math.min(100, strokeWidth)) },
|
||||
})),
|
||||
|
||||
setCornerRadius: (cornerRadius) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, cornerRadius: Math.max(0, Math.min(100, cornerRadius)) },
|
||||
})),
|
||||
setCornerRadius: (cornerRadius) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, cornerRadius: Math.max(0, Math.min(100, cornerRadius)) },
|
||||
})),
|
||||
|
||||
setSides: (sides) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, sides: Math.max(3, Math.min(20, sides)) },
|
||||
})),
|
||||
setSides: (sides) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, sides: Math.max(3, Math.min(20, sides)) },
|
||||
})),
|
||||
|
||||
setInnerRadius: (innerRadius) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, innerRadius: Math.max(0.1, Math.min(0.9, innerRadius)) },
|
||||
})),
|
||||
setInnerRadius: (innerRadius) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, innerRadius: Math.max(0.1, Math.min(0.9, innerRadius)) },
|
||||
})),
|
||||
|
||||
setArrowHeadSize: (arrowHeadSize) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, arrowHeadSize: Math.max(5, Math.min(100, arrowHeadSize)) },
|
||||
})),
|
||||
setArrowHeadSize: (arrowHeadSize) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, arrowHeadSize: Math.max(5, Math.min(100, arrowHeadSize)) },
|
||||
})),
|
||||
|
||||
setArrowHeadAngle: (arrowHeadAngle) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, arrowHeadAngle: Math.max(10, Math.min(60, arrowHeadAngle)) },
|
||||
})),
|
||||
setArrowHeadAngle: (arrowHeadAngle) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, arrowHeadAngle: Math.max(10, Math.min(60, arrowHeadAngle)) },
|
||||
})),
|
||||
|
||||
updateSettings: (settings) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, ...settings },
|
||||
})),
|
||||
}));
|
||||
updateSettings: (settings) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, ...settings },
|
||||
})),
|
||||
}),
|
||||
{
|
||||
name: 'shape-storage',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
92
store/ui-store.ts
Normal file
92
store/ui-store.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
type PanelTab = 'adjustments' | 'tools' | 'history';
|
||||
|
||||
interface CollapsibleState {
|
||||
filters: boolean;
|
||||
selection: boolean;
|
||||
transform: boolean;
|
||||
shapeSettings: boolean;
|
||||
}
|
||||
|
||||
interface PanelDockState {
|
||||
activeTab: PanelTab;
|
||||
width: number;
|
||||
layersHeight: number;
|
||||
colorsHeight: number;
|
||||
}
|
||||
|
||||
interface UIStore {
|
||||
panelDock: PanelDockState;
|
||||
collapsed: CollapsibleState;
|
||||
|
||||
/** Set active tab in panel dock */
|
||||
setActiveTab: (tab: PanelTab) => void;
|
||||
/** Set panel dock width */
|
||||
setPanelWidth: (width: number) => void;
|
||||
/** Set layers panel height percentage */
|
||||
setLayersHeight: (height: number) => void;
|
||||
/** Set colors panel height percentage */
|
||||
setColorsHeight: (height: number) => void;
|
||||
/** Toggle collapsible section */
|
||||
toggleCollapsed: (section: keyof CollapsibleState) => void;
|
||||
/** Set collapsible section state */
|
||||
setCollapsed: (section: keyof CollapsibleState, collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_PANEL_DOCK: PanelDockState = {
|
||||
activeTab: 'adjustments',
|
||||
width: 280,
|
||||
layersHeight: 40,
|
||||
colorsHeight: 20,
|
||||
};
|
||||
|
||||
const DEFAULT_COLLAPSED: CollapsibleState = {
|
||||
filters: true,
|
||||
selection: true,
|
||||
transform: true,
|
||||
shapeSettings: true,
|
||||
};
|
||||
|
||||
export const useUIStore = create<UIStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
panelDock: { ...DEFAULT_PANEL_DOCK },
|
||||
collapsed: { ...DEFAULT_COLLAPSED },
|
||||
|
||||
setActiveTab: (tab) =>
|
||||
set((state) => ({
|
||||
panelDock: { ...state.panelDock, activeTab: tab },
|
||||
})),
|
||||
|
||||
setPanelWidth: (width) =>
|
||||
set((state) => ({
|
||||
panelDock: { ...state.panelDock, width: Math.max(280, Math.min(600, width)) },
|
||||
})),
|
||||
|
||||
setLayersHeight: (height) =>
|
||||
set((state) => ({
|
||||
panelDock: { ...state.panelDock, layersHeight: Math.max(15, Math.min(70, height)) },
|
||||
})),
|
||||
|
||||
setColorsHeight: (height) =>
|
||||
set((state) => ({
|
||||
panelDock: { ...state.panelDock, colorsHeight: Math.max(10, Math.min(40, height)) },
|
||||
})),
|
||||
|
||||
toggleCollapsed: (section) =>
|
||||
set((state) => ({
|
||||
collapsed: { ...state.collapsed, [section]: !state.collapsed[section] },
|
||||
})),
|
||||
|
||||
setCollapsed: (section, collapsed) =>
|
||||
set((state) => ({
|
||||
collapsed: { ...state.collapsed, [section]: collapsed },
|
||||
})),
|
||||
}),
|
||||
{
|
||||
name: 'ui-storage',
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user