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>
364 lines
10 KiB
TypeScript
364 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useSelectionStore } from '@/store/selection-store';
|
|
import { useToolStore } from '@/store/tool-store';
|
|
import { useHistoryStore } from '@/store/history-store';
|
|
import {
|
|
copySelection,
|
|
cutSelection,
|
|
deleteSelection,
|
|
fillSelection,
|
|
strokeSelection,
|
|
pasteCanvas,
|
|
} from '@/lib/selection-operations';
|
|
import {
|
|
ClearSelectionCommand,
|
|
InvertSelectionCommand,
|
|
} from '@/core/commands/selection-command';
|
|
import type { SelectionType, SelectionMode } from '@/types/selection';
|
|
import {
|
|
Square,
|
|
Circle,
|
|
Lasso,
|
|
Wand2,
|
|
Copy,
|
|
Scissors,
|
|
Trash2,
|
|
FlipVertical,
|
|
X,
|
|
PlusSquare,
|
|
MinusSquare,
|
|
Layers,
|
|
Paintbrush,
|
|
Clipboard,
|
|
} from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
const SELECTION_TOOLS: Array<{
|
|
type: SelectionType;
|
|
label: string;
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
}> = [
|
|
{ type: 'rectangular', label: 'Rectangle', icon: Square },
|
|
{ type: 'elliptical', label: 'Ellipse', icon: Circle },
|
|
{ type: 'lasso', label: 'Lasso', icon: Lasso },
|
|
{ type: 'magic-wand', label: 'Magic Wand', icon: Wand2 },
|
|
];
|
|
|
|
const SELECTION_MODES: Array<{
|
|
mode: SelectionMode;
|
|
label: string;
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
}> = [
|
|
{ mode: 'new', label: 'New', icon: Square },
|
|
{ mode: 'add', label: 'Add', icon: PlusSquare },
|
|
{ mode: 'subtract', label: 'Subtract', icon: MinusSquare },
|
|
{ mode: 'intersect', label: 'Intersect', icon: Layers },
|
|
];
|
|
|
|
export function SelectionPanel() {
|
|
const {
|
|
activeSelection,
|
|
selectionType,
|
|
selectionMode,
|
|
feather,
|
|
tolerance,
|
|
setSelectionType,
|
|
setSelectionMode,
|
|
setFeather,
|
|
setTolerance,
|
|
clearSelection,
|
|
invertSelection,
|
|
} = useSelectionStore();
|
|
|
|
const { setActiveTool, settings } = useToolStore();
|
|
const { executeCommand } = useHistoryStore();
|
|
const [copiedCanvas, setCopiedCanvas] = useState<HTMLCanvasElement | null>(
|
|
null
|
|
);
|
|
|
|
const hasSelection = !!activeSelection;
|
|
|
|
const handleToolSelect = (type: SelectionType) => {
|
|
setSelectionType(type);
|
|
setActiveTool('select');
|
|
};
|
|
|
|
const handleCopy = () => {
|
|
const canvas = copySelection();
|
|
if (canvas) {
|
|
setCopiedCanvas(canvas);
|
|
}
|
|
};
|
|
|
|
const handleCut = () => {
|
|
const canvas = cutSelection();
|
|
if (canvas) {
|
|
setCopiedCanvas(canvas);
|
|
}
|
|
};
|
|
|
|
const handlePaste = () => {
|
|
if (copiedCanvas) {
|
|
pasteCanvas(copiedCanvas);
|
|
}
|
|
};
|
|
|
|
const handleDelete = () => {
|
|
deleteSelection();
|
|
};
|
|
|
|
const handleClear = () => {
|
|
const command = new ClearSelectionCommand();
|
|
executeCommand(command);
|
|
};
|
|
|
|
const handleInvert = () => {
|
|
const command = new InvertSelectionCommand();
|
|
executeCommand(command);
|
|
};
|
|
|
|
const handleFill = () => {
|
|
fillSelection(settings.color);
|
|
};
|
|
|
|
const handleStroke = () => {
|
|
strokeSelection(settings.color, 2);
|
|
};
|
|
|
|
return (
|
|
<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">
|
|
Tools
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-1">
|
|
{SELECTION_TOOLS.map((tool) => (
|
|
<button
|
|
key={tool.type}
|
|
onClick={() => handleToolSelect(tool.type)}
|
|
className={cn(
|
|
'flex items-center gap-2 rounded-md p-2 text-xs transition-colors',
|
|
selectionType === tool.type
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'hover:bg-accent text-foreground'
|
|
)}
|
|
>
|
|
<tool.icon className="h-4 w-4" />
|
|
<span>{tool.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Selection Modes */}
|
|
<div className="border-b border-border p-3">
|
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase mb-2">
|
|
Mode
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-1">
|
|
{SELECTION_MODES.map((mode) => (
|
|
<button
|
|
key={mode.mode}
|
|
onClick={() => setSelectionMode(mode.mode)}
|
|
className={cn(
|
|
'flex items-center gap-2 rounded-md p-2 text-xs transition-colors',
|
|
selectionMode === mode.mode
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'hover:bg-accent text-foreground'
|
|
)}
|
|
>
|
|
<mode.icon className="h-3 w-3" />
|
|
<span>{mode.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Selection Parameters */}
|
|
<div className="border-b border-border p-3 space-y-3">
|
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
|
Parameters
|
|
</h3>
|
|
|
|
{/* Feather */}
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-foreground">Feather</label>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="250"
|
|
value={feather}
|
|
onChange={(e) => setFeather(Number(e.target.value))}
|
|
className="w-full"
|
|
/>
|
|
<div className="text-xs text-muted-foreground text-center">
|
|
{feather}px
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tolerance (for magic wand) */}
|
|
{selectionType === 'magic-wand' && (
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-foreground">Tolerance</label>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="255"
|
|
value={tolerance}
|
|
onChange={(e) => setTolerance(Number(e.target.value))}
|
|
className="w-full"
|
|
/>
|
|
<div className="text-xs text-muted-foreground text-center">
|
|
{tolerance}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Selection Operations */}
|
|
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase mb-2">
|
|
Operations
|
|
</h3>
|
|
|
|
<button
|
|
onClick={handleCopy}
|
|
disabled={!hasSelection}
|
|
className={cn(
|
|
'w-full flex items-center gap-2 rounded-md p-2 text-sm transition-colors',
|
|
hasSelection
|
|
? 'hover:bg-accent text-foreground'
|
|
: 'text-muted-foreground cursor-not-allowed'
|
|
)}
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
<span>Copy</span>
|
|
<span className="ml-auto text-xs text-muted-foreground">Ctrl+C</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleCut}
|
|
disabled={!hasSelection}
|
|
className={cn(
|
|
'w-full flex items-center gap-2 rounded-md p-2 text-sm transition-colors',
|
|
hasSelection
|
|
? 'hover:bg-accent text-foreground'
|
|
: 'text-muted-foreground cursor-not-allowed'
|
|
)}
|
|
>
|
|
<Scissors className="h-4 w-4" />
|
|
<span>Cut</span>
|
|
<span className="ml-auto text-xs text-muted-foreground">Ctrl+X</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handlePaste}
|
|
disabled={!copiedCanvas}
|
|
className={cn(
|
|
'w-full flex items-center gap-2 rounded-md p-2 text-sm transition-colors',
|
|
copiedCanvas
|
|
? 'hover:bg-accent text-foreground'
|
|
: 'text-muted-foreground cursor-not-allowed'
|
|
)}
|
|
>
|
|
<Clipboard className="h-4 w-4" />
|
|
<span>Paste</span>
|
|
<span className="ml-auto text-xs text-muted-foreground">Ctrl+V</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleDelete}
|
|
disabled={!hasSelection}
|
|
className={cn(
|
|
'w-full flex items-center gap-2 rounded-md p-2 text-sm transition-colors',
|
|
hasSelection
|
|
? 'hover:bg-accent text-foreground'
|
|
: 'text-muted-foreground cursor-not-allowed'
|
|
)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
<span>Delete</span>
|
|
<span className="ml-auto text-xs text-muted-foreground">Del</span>
|
|
</button>
|
|
|
|
<div className="h-px bg-border my-2" />
|
|
|
|
<button
|
|
onClick={handleFill}
|
|
disabled={!hasSelection}
|
|
className={cn(
|
|
'w-full flex items-center gap-2 rounded-md p-2 text-sm transition-colors',
|
|
hasSelection
|
|
? 'hover:bg-accent text-foreground'
|
|
: 'text-muted-foreground cursor-not-allowed'
|
|
)}
|
|
>
|
|
<Paintbrush className="h-4 w-4" />
|
|
<span>Fill Selection</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleStroke}
|
|
disabled={!hasSelection}
|
|
className={cn(
|
|
'w-full flex items-center gap-2 rounded-md p-2 text-sm transition-colors',
|
|
hasSelection
|
|
? 'hover:bg-accent text-foreground'
|
|
: 'text-muted-foreground cursor-not-allowed'
|
|
)}
|
|
>
|
|
<Paintbrush className="h-4 w-4" />
|
|
<span>Stroke Selection</span>
|
|
</button>
|
|
|
|
<div className="h-px bg-border my-2" />
|
|
|
|
<button
|
|
onClick={handleInvert}
|
|
disabled={!hasSelection}
|
|
className={cn(
|
|
'w-full flex items-center gap-2 rounded-md p-2 text-sm transition-colors',
|
|
hasSelection
|
|
? 'hover:bg-accent text-foreground'
|
|
: 'text-muted-foreground cursor-not-allowed'
|
|
)}
|
|
>
|
|
<FlipVertical className="h-4 w-4" />
|
|
<span>Invert Selection</span>
|
|
<span className="ml-auto text-xs text-muted-foreground">
|
|
Ctrl+Shift+I
|
|
</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleClear}
|
|
disabled={!hasSelection}
|
|
className={cn(
|
|
'w-full flex items-center gap-2 rounded-md p-2 text-sm transition-colors',
|
|
hasSelection
|
|
? 'hover:bg-accent text-foreground'
|
|
: 'text-muted-foreground cursor-not-allowed'
|
|
)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
<span>Clear Selection</span>
|
|
<span className="ml-auto text-xs text-muted-foreground">
|
|
Ctrl+D
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
{!hasSelection && (
|
|
<div className="p-3 border-t border-border">
|
|
<p className="text-xs text-muted-foreground text-center">
|
|
Use selection tools to create a selection
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|