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

282 lines
8.8 KiB
TypeScript

'use client';
import { useShapeStore } from '@/store/shape-store';
import { useToolStore } from '@/store/tool-store';
import { useColorStore } from '@/store/color-store';
import type { ShapeType } from '@/types/shape';
import {
Square,
Circle,
Minus,
ArrowRight,
Pentagon,
Star,
Triangle,
Paintbrush,
PenTool,
} from 'lucide-react';
import { cn } from '@/lib/utils';
const SHAPES: Array<{
type: ShapeType;
label: string;
icon: React.ComponentType<{ className?: string }>;
}> = [
{ type: 'rectangle', label: 'Rectangle', icon: Square },
{ type: 'ellipse', label: 'Ellipse', icon: Circle },
{ type: 'line', label: 'Line', icon: Minus },
{ type: 'arrow', label: 'Arrow', icon: ArrowRight },
{ type: 'polygon', label: 'Polygon', icon: Pentagon },
{ type: 'star', label: 'Star', icon: Star },
{ type: 'triangle', label: 'Triangle', icon: Triangle },
];
export function ShapePanel() {
const {
settings,
setShapeType,
setFill,
setStroke,
setStrokeWidth,
setCornerRadius,
setSides,
setInnerRadius,
setArrowHeadSize,
setArrowHeadAngle,
} = useShapeStore();
const { setActiveTool } = useToolStore();
const { primaryColor, setPrimaryColor } = useColorStore();
const handleShapeSelect = (type: ShapeType) => {
setShapeType(type);
setActiveTool('shape');
};
const handleFillColorChange = (color: string) => {
setPrimaryColor(color);
useShapeStore.getState().setFillColor(color);
};
const handleStrokeColorChange = (color: string) => {
useShapeStore.getState().setStrokeColor(color);
};
return (
<div className="w-full border-l border-border bg-card flex flex-col">
{/* Shape Types */}
<div className="border-b border-border p-3">
<h3 className="text-xs font-semibold text-muted-foreground uppercase mb-2">
Type
</h3>
<div className="grid grid-cols-2 gap-1">
{SHAPES.map((shape) => (
<button
key={shape.type}
onClick={() => handleShapeSelect(shape.type)}
className={cn(
'flex items-center gap-2 rounded-md p-2 text-xs transition-colors',
settings.type === shape.type
? 'bg-primary text-primary-foreground'
: 'hover:bg-accent text-foreground'
)}
>
<shape.icon className="h-4 w-4" />
<span>{shape.label}</span>
</button>
))}
</div>
</div>
{/* Fill and Stroke */}
<div className="border-b border-border p-3 space-y-3">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Style
</h3>
{/* Fill */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<button
onClick={() => setFill(!settings.fill)}
className="flex items-center gap-2 text-sm"
>
<Paintbrush className="h-4 w-4" />
<span>Fill</span>
</button>
<input
type="checkbox"
checked={settings.fill}
onChange={(e) => setFill(e.target.checked)}
className="rounded"
/>
</div>
{settings.fill && (
<input
type="color"
value={settings.fillColor}
onChange={(e) => handleFillColorChange(e.target.value)}
className="w-full h-8 rounded cursor-pointer"
/>
)}
</div>
{/* Stroke */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<button
onClick={() => setStroke(!settings.stroke)}
className="flex items-center gap-2 text-sm"
>
<PenTool className="h-4 w-4" />
<span>Stroke</span>
</button>
<input
type="checkbox"
checked={settings.stroke}
onChange={(e) => setStroke(e.target.checked)}
className="rounded"
/>
</div>
{settings.stroke && (
<>
<input
type="color"
value={settings.strokeColor}
onChange={(e) => handleStrokeColorChange(e.target.value)}
className="w-full h-8 rounded cursor-pointer"
/>
<div className="space-y-1">
<label className="text-xs text-muted-foreground">Width</label>
<input
type="range"
min="1"
max="100"
value={settings.strokeWidth}
onChange={(e) => setStrokeWidth(Number(e.target.value))}
className="w-full"
/>
<div className="text-xs text-muted-foreground text-center">
{settings.strokeWidth}px
</div>
</div>
</>
)}
</div>
</div>
{/* Shape-specific settings */}
<div className="flex-1 overflow-y-auto p-3 space-y-3">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Properties
</h3>
{/* Rectangle: Corner Radius */}
{settings.type === 'rectangle' && (
<div className="space-y-2">
<label className="text-xs text-foreground">Corner Radius</label>
<input
type="range"
min="0"
max="100"
value={settings.cornerRadius}
onChange={(e) => setCornerRadius(Number(e.target.value))}
className="w-full"
/>
<div className="text-xs text-muted-foreground text-center">
{settings.cornerRadius}px
</div>
</div>
)}
{/* Polygon: Sides */}
{settings.type === 'polygon' && (
<div className="space-y-2">
<label className="text-xs text-foreground">Sides</label>
<input
type="range"
min="3"
max="20"
value={settings.sides}
onChange={(e) => setSides(Number(e.target.value))}
className="w-full"
/>
<div className="text-xs text-muted-foreground text-center">
{settings.sides}
</div>
</div>
)}
{/* Star: Points and Inner Radius */}
{settings.type === 'star' && (
<>
<div className="space-y-2">
<label className="text-xs text-foreground">Points</label>
<input
type="range"
min="3"
max="20"
value={settings.sides}
onChange={(e) => setSides(Number(e.target.value))}
className="w-full"
/>
<div className="text-xs text-muted-foreground text-center">
{settings.sides}
</div>
</div>
<div className="space-y-2">
<label className="text-xs text-foreground">Inner Radius</label>
<input
type="range"
min="10"
max="90"
step="5"
value={settings.innerRadius * 100}
onChange={(e) => setInnerRadius(Number(e.target.value) / 100)}
className="w-full"
/>
<div className="text-xs text-muted-foreground text-center">
{Math.round(settings.innerRadius * 100)}%
</div>
</div>
</>
)}
{/* Arrow: Head Size and Angle */}
{settings.type === 'arrow' && (
<>
<div className="space-y-2">
<label className="text-xs text-foreground">Head Size</label>
<input
type="range"
min="5"
max="100"
value={settings.arrowHeadSize}
onChange={(e) => setArrowHeadSize(Number(e.target.value))}
className="w-full"
/>
<div className="text-xs text-muted-foreground text-center">
{settings.arrowHeadSize}px
</div>
</div>
<div className="space-y-2">
<label className="text-xs text-foreground">Head Angle</label>
<input
type="range"
min="10"
max="60"
value={settings.arrowHeadAngle}
onChange={(e) => setArrowHeadAngle(Number(e.target.value))}
className="w-full"
/>
<div className="text-xs text-muted-foreground text-center">
{settings.arrowHeadAngle}°
</div>
</div>
</>
)}
</div>
</div>
);
}