Files
paint-ui/components/transform/transform-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

195 lines
6.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}