A web app to help you design things, local, offline, on device. In your browser.
TypeScript
53.1%
JSON
45.6%
CSS
0.9%
Markdown
0.3%
JavaScript
0.1%
'use client';
import React, { useState } from 'react';
import { Square, Circle, Triangle, Star, ArrowRight, Hexagon } from 'lucide-react';
import { HexColorPicker } from 'react-colorful';
import { useEditorStore } from '@/lib/store/editor-store';
import type { ShapeLayerData } from '@/types';
const shapes = [
{ id: 'rectangle', name: 'Rectangle', icon: Square },
{ id: 'circle', name: 'Circle', icon: Circle },
{ id: 'ellipse', name: 'Ellipse', icon: Circle },
{ id: 'polygon', name: 'Polygon', icon: Hexagon },
{ id: 'star', name: 'Star', icon: Star },
{ id: 'arrow', name: 'Arrow', icon: ArrowRight },
] as const;
const gradientPresets = [
{
name: 'Sunset',
gradient: {
type: 'linear' as const,
angle: 135,
stops: [
{ offset: 0, color: '#ff6b6b' },
{ offset: 1, color: '#feca57' },
],
},
},
{
name: 'Ocean',
gradient: {
type: 'linear' as const,
angle: 135,
stops: [
{ offset: 0, color: '#4facfe' },
{ offset: 1, color: '#00f2fe' },
],
},
},
{
name: 'Purple',
gradient: {
type: 'linear' as const,
angle: 135,
stops: [
{ offset: 0, color: '#667eea' },
{ offset: 1, color: '#764ba2' },
],
},
},
{
name: 'Forest',
gradient: {
type: 'linear' as const,
angle: 135,
stops: [
{ offset: 0, color: '#134e5e' },
{ offset: 1, color: '#71b280' },
],
},
},
];
export function ShapesPanel() {
const [fillColor, setFillColor] = useState('#4f46e5');
const [strokeColor, setStrokeColor] = useState('#ffffff');
const [strokeWidth, setStrokeWidth] = useState(0);
const [cornerRadius, setCornerRadius] = useState(0);
const [showFillPicker, setShowFillPicker] = useState(false);
const [showStrokePicker, setShowStrokePicker] = useState(false);
const [useGradient, setUseGradient] = useState(false);
const [selectedGradient, setSelectedGradient] = useState(gradientPresets[0]);
const { addLayer, canvas } = useEditorStore();
const handleAddShape = (shapeType: (typeof shapes)[number]['id']) => {
const size = Math.min(canvas.width, canvas.height) * 0.3;
const data: ShapeLayerData = {
type: 'shape',
shapeType: shapeType as ShapeLayerData['shapeType'],
fill: useGradient ? selectedGradient.gradient : fillColor,
stroke: strokeColor,
strokeWidth,
cornerRadius: shapeType === 'rectangle' ? cornerRadius : undefined,
points: shapeType === 'polygon' ? 6 : shapeType === 'star' ? 5 : undefined,
};
addLayer({
type: 'shape',
name: shapes.find((s) => s.id === shapeType)?.name || 'Shape',
visible: true,
locked: false,
opacity: 1,
position: {
x: (canvas.width - size) / 2,
y: (canvas.height - size) / 2,
},
size: { width: size, height: size },
rotation: 0,
data,
});
};
return (
<div className="p-4">
<h2 className="text-lg font-semibold text-white mb-4">Shapes</h2>
{/* Shape selection */}
<div className="grid grid-cols-3 gap-2 mb-6">
{shapes.map((shape) => {
const Icon = shape.icon;
return (
<button
key={shape.id}
onClick={() => handleAddShape(shape.id)}
className="
aspect-square rounded-lg
bg-neutral-800/50 hover:bg-neutral-800
border border-neutral-700/50 hover:border-indigo-500/50
flex flex-col items-center justify-center gap-1
transition-all group
"
>
<Icon className="w-6 h-6 text-neutral-400 group-hover:text-indigo-400 transition-colors" />
<span className="text-[10px] text-neutral-500 group-hover:text-neutral-300">
{shape.name}
</span>
</button>
);
})}
</div>
{/* Fill options */}
<div className="space-y-4">
{/* Solid vs Gradient toggle */}
<div className="flex gap-2">
<button
onClick={() => setUseGradient(false)}
className={`
flex-1 py-2 rounded-lg text-sm transition-colors
${
!useGradient
? 'bg-indigo-500/20 text-indigo-400 border border-indigo-500/50'
: 'bg-neutral-800 text-neutral-400 border border-neutral-700'
}
`}
>
Solid
</button>
<button
onClick={() => setUseGradient(true)}
className={`
flex-1 py-2 rounded-lg text-sm transition-colors
${
useGradient
? 'bg-indigo-500/20 text-indigo-400 border border-indigo-500/50'
: 'bg-neutral-800 text-neutral-400 border border-neutral-700'
}
`}
>
Gradient
</button>
</div>
{!useGradient ? (
/* Solid fill color */
<div>
<label className="text-sm text-neutral-400 block mb-2">Fill Color</label>
<button
onClick={() => setShowFillPicker(!showFillPicker)}
className="
w-full px-3 py-2 rounded-lg
bg-neutral-800 border border-neutral-700
flex items-center gap-3 hover:border-neutral-600 transition-colors
"
>
<div
className="w-6 h-6 rounded border border-neutral-600"
style={{ backgroundColor: fillColor }}
/>
<span className="text-white font-mono text-sm">{fillColor.toUpperCase()}</span>
</button>
{showFillPicker && (
<div className="mt-2 p-3 bg-neutral-800 rounded-lg border border-neutral-700">
<HexColorPicker color={fillColor} onChange={setFillColor} />
</div>
)}
</div>
) : (
/* Gradient presets */
<div>
<label className="text-sm text-neutral-400 block mb-2">Gradient</label>
<div className="grid grid-cols-2 gap-2">
{gradientPresets.map((preset) => (
<button
key={preset.name}
onClick={() => setSelectedGradient(preset)}
className={`
h-12 rounded-lg border-2 transition-all
${
selectedGradient.name === preset.name
? 'border-indigo-500 scale-105'
: 'border-transparent hover:border-neutral-500'
}
`}
style={{
background: `linear-gradient(${preset.gradient.angle}deg, ${preset.gradient.stops
.map((s) => s.color)
.join(', ')})`,
}}
/>
))}
</div>
</div>
)}
{/* Stroke */}
<div>
<label className="text-sm text-neutral-400 block mb-2">Stroke</label>
<div className="flex gap-2">
<button
onClick={() => setShowStrokePicker(!showStrokePicker)}
className="
px-3 py-2 rounded-lg
bg-neutral-800 border border-neutral-700
flex items-center gap-2 hover:border-neutral-600 transition-colors
"
>
<div
className="w-5 h-5 rounded border border-neutral-600"
style={{ backgroundColor: strokeColor }}
/>
</button>
<input
type="number"
value={strokeWidth}
onChange={(e) => setStrokeWidth(Number(e.target.value))}
min={0}
max={20}
placeholder="Width"
className="
flex-1 px-3 py-2 rounded-lg
bg-neutral-800 border border-neutral-700
text-white text-sm
focus:outline-none focus:border-indigo-500
"
/>
</div>
{showStrokePicker && (
<div className="mt-2 p-3 bg-neutral-800 rounded-lg border border-neutral-700">
<HexColorPicker color={strokeColor} onChange={setStrokeColor} />
</div>
)}
</div>
{/* Corner radius (for rectangles) */}
<div>
<label className="text-sm text-neutral-400 block mb-2">Corner Radius</label>
<input
type="range"
min={0}
max={100}
value={cornerRadius}
onChange={(e) => setCornerRadius(Number(e.target.value))}
className="w-full"
/>
<div className="text-xs text-neutral-500 text-right">{cornerRadius}px</div>
</div>
</div>
</div>
);
}
About
A web app to help you design things, local, offline, on device. In your browser.
0 stars
0 forks