Files
paint-ui/components/selection/selection-panel.tsx
Sebastian Krüger cd59f0606b 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>
2025-11-21 09:03:14 +01:00

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>
);
}