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>
282 lines
8.8 KiB
TypeScript
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>
|
|
);
|
|
}
|