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>
195 lines
6.7 KiB
TypeScript
195 lines
6.7 KiB
TypeScript
'use client';
|
||
|
||
import { useTransformStore } from '@/store/transform-store';
|
||
import { useLayerStore } from '@/store/layer-store';
|
||
import { useHistoryStore } from '@/store/history-store';
|
||
import { useToolStore } from '@/store/tool-store';
|
||
import { TransformCommand } from '@/core/commands/transform-command';
|
||
import { Move, RotateCw, Maximize2, Check, X, Lock, Unlock } from 'lucide-react';
|
||
import { cn } from '@/lib/utils';
|
||
|
||
export function TransformPanel() {
|
||
const {
|
||
activeTransform,
|
||
transformType,
|
||
maintainAspectRatio,
|
||
setTransformType,
|
||
setMaintainAspectRatio,
|
||
applyTransform,
|
||
cancelTransform,
|
||
} = useTransformStore();
|
||
|
||
const { activeLayerId, layers } = useLayerStore();
|
||
const { executeCommand } = useHistoryStore();
|
||
const { setActiveTool } = useToolStore();
|
||
|
||
const activeLayer = layers.find((l) => l.id === activeLayerId);
|
||
const hasActiveLayer = !!activeLayer && !activeLayer.locked;
|
||
|
||
const handleApply = () => {
|
||
if (!activeTransform || !activeLayer) return;
|
||
|
||
const command = TransformCommand.applyToLayer(
|
||
activeLayer,
|
||
activeTransform.currentState,
|
||
activeTransform.originalBounds
|
||
);
|
||
executeCommand(command);
|
||
applyTransform();
|
||
};
|
||
|
||
const handleCancel = () => {
|
||
cancelTransform();
|
||
};
|
||
|
||
const handleMoveTool = () => {
|
||
setActiveTool('move');
|
||
};
|
||
|
||
const handleTransformTool = () => {
|
||
setActiveTool('move'); // Will be updated to 'transform' when tool types are updated
|
||
};
|
||
|
||
return (
|
||
<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">
|
||
Tools
|
||
</h3>
|
||
<div className="space-y-1">
|
||
<button
|
||
onClick={handleMoveTool}
|
||
disabled={!hasActiveLayer}
|
||
className={cn(
|
||
'w-full flex items-center gap-2 rounded-md p-2 text-sm transition-colors',
|
||
transformType === 'move'
|
||
? 'bg-primary text-primary-foreground'
|
||
: 'hover:bg-accent text-foreground',
|
||
!hasActiveLayer && 'opacity-50 cursor-not-allowed'
|
||
)}
|
||
>
|
||
<Move className="h-4 w-4" />
|
||
<span>Move Layer</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={handleTransformTool}
|
||
disabled={!hasActiveLayer}
|
||
className={cn(
|
||
'w-full flex items-center gap-2 rounded-md p-2 text-sm transition-colors',
|
||
transformType === 'free-transform'
|
||
? 'bg-primary text-primary-foreground'
|
||
: 'hover:bg-accent text-foreground',
|
||
!hasActiveLayer && 'opacity-50 cursor-not-allowed'
|
||
)}
|
||
>
|
||
<Maximize2 className="h-4 w-4" />
|
||
<span>Free Transform</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Transform Options */}
|
||
{activeTransform && (
|
||
<div className="border-b border-border p-3 space-y-3">
|
||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||
Options
|
||
</h3>
|
||
|
||
{/* Maintain Aspect Ratio */}
|
||
<button
|
||
onClick={() => setMaintainAspectRatio(!maintainAspectRatio)}
|
||
className="w-full flex items-center justify-between rounded-md p-2 hover:bg-accent transition-colors"
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
{maintainAspectRatio ? (
|
||
<Lock className="h-4 w-4" />
|
||
) : (
|
||
<Unlock className="h-4 w-4" />
|
||
)}
|
||
<span className="text-sm">Constrain Proportions</span>
|
||
</div>
|
||
{maintainAspectRatio && (
|
||
<div className="h-2 w-2 rounded-full bg-primary" />
|
||
)}
|
||
</button>
|
||
|
||
{/* Transform State Display */}
|
||
<div className="space-y-2 text-xs">
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Position:</span>
|
||
<span>
|
||
{Math.round(activeTransform.currentState.x)},{' '}
|
||
{Math.round(activeTransform.currentState.y)}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Scale:</span>
|
||
<span>
|
||
{(activeTransform.currentState.scaleX * 100).toFixed(0)}% ×{' '}
|
||
{(activeTransform.currentState.scaleY * 100).toFixed(0)}%
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Rotation:</span>
|
||
<span>{activeTransform.currentState.rotation.toFixed(1)}°</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Apply/Cancel Buttons */}
|
||
<div className="flex gap-2 pt-2">
|
||
<button
|
||
onClick={handleApply}
|
||
className="flex-1 flex items-center justify-center gap-2 rounded-md bg-primary p-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
|
||
>
|
||
<Check className="h-4 w-4" />
|
||
Apply
|
||
</button>
|
||
<button
|
||
onClick={handleCancel}
|
||
className="flex-1 flex items-center justify-center gap-2 rounded-md bg-muted p-2 text-sm font-medium text-muted-foreground hover:bg-muted/80 transition-colors"
|
||
>
|
||
<X className="h-4 w-4" />
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Instructions */}
|
||
<div className="flex-1 overflow-y-auto p-3">
|
||
<h3 className="text-xs font-semibold text-muted-foreground uppercase mb-2">
|
||
Instructions
|
||
</h3>
|
||
<div className="space-y-2 text-xs text-muted-foreground">
|
||
{transformType === 'move' && (
|
||
<>
|
||
<p>• Click and drag to move the layer</p>
|
||
<p>• Arrow keys for precise movement</p>
|
||
<p>• Hold Shift for 10px increments</p>
|
||
</>
|
||
)}
|
||
{transformType === 'free-transform' && (
|
||
<>
|
||
<p>• Drag corners to scale</p>
|
||
<p>• Drag edges to scale in one direction</p>
|
||
<p>• Drag rotate handle to rotate</p>
|
||
<p>• Drag inside to move</p>
|
||
<p>• Hold Shift to constrain proportions</p>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{!hasActiveLayer && (
|
||
<div className="p-3 border-t border-border">
|
||
<p className="text-xs text-muted-foreground text-center">
|
||
Select an unlocked layer to transform
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|