2025-11-21 02:36:50 +01:00
|
|
|
|
'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 (
|
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
|
|
|
|
<div className="w-full border-l border-border bg-card flex flex-col">
|
2025-11-21 02:36:50 +01:00
|
|
|
|
{/* 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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|