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%
ShapesPanel.tsx 277 lines (9 KB)
'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