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%
DevicePanel.tsx 193 lines (6 KB)
'use client';

import React, { useState } from 'react';
import { Smartphone, Tablet, Monitor, Watch } from 'lucide-react';
import { useEditorStore } from '@/lib/store/editor-store';
import { DEVICE_SPECS, DEVICE_COLORS, getDevicesByCategory } from '@/lib/devices/device-specs';
import type { DeviceType, DeviceColor, DeviceLayerData } from '@/types';

const categories = [
  { id: 'iphone', label: 'iPhone', icon: Smartphone },
  { id: 'ipad', label: 'iPad', icon: Tablet },
  { id: 'mac', label: 'Mac', icon: Monitor },
  { id: 'android', label: 'Android', icon: Smartphone },
  { id: 'watch', label: 'Watch', icon: Watch },
] as const;

export function DevicePanel() {
  const [activeCategory, setActiveCategory] = useState<(typeof categories)[number]['id']>('iphone');
  const [selectedColor, setSelectedColor] = useState<DeviceColor>('black');
  const [showFrame, setShowFrame] = useState(true);

  const { addLayer, canvas } = useEditorStore();

  const devices = getDevicesByCategory(activeCategory);

  const handleAddDevice = (deviceId: DeviceType) => {
    const spec = DEVICE_SPECS[deviceId];
    if (!spec) return;

    // Calculate a good default size that fits in the canvas
    const scale = Math.min(
      (canvas.width * 0.6) / spec.frameSize.width,
      (canvas.height * 0.7) / spec.frameSize.height
    );

    const width = spec.frameSize.width * scale;
    const height = spec.frameSize.height * scale;

    const layerData: DeviceLayerData = {
      type: 'device',
      deviceId,
      color: selectedColor,
      showFrame,
    };

    addLayer({
      type: 'device',
      name: spec.name,
      visible: true,
      locked: false,
      opacity: 1,
      position: {
        x: (canvas.width - width) / 2,
        y: (canvas.height - height) / 2,
      },
      size: { width, height },
      rotation: 0,
      data: layerData,
    });
  };

  return (
    <div className="p-4">
      <h2 className="text-lg font-semibold text-white mb-4">Device Mockups</h2>

      {/* Category tabs */}
      <div className="flex gap-1 mb-4 overflow-x-auto pb-1">
        {categories.map((cat) => {
          const Icon = cat.icon;
          return (
            <button
              key={cat.id}
              onClick={() => setActiveCategory(cat.id)}
              className={`
                flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm whitespace-nowrap
                transition-colors
                ${
                  activeCategory === cat.id
                    ? 'bg-indigo-500/20 text-indigo-400'
                    : 'text-neutral-400 hover:text-white hover:bg-neutral-800'
                }
              `}
            >
              <Icon className="w-4 h-4" />
              {cat.label}
            </button>
          );
        })}
      </div>

      {/* Options */}
      <div className="space-y-4 mb-6">
        {/* Color selection */}
        <div>
          <label className="text-sm text-neutral-400 block mb-2">Device Color</label>
          <div className="flex gap-2 flex-wrap">
            {['black', 'white', 'silver', 'gold', 'blue', 'purple', 'pink', 'green'].map(
              (color) => (
                <button
                  key={color}
                  onClick={() => setSelectedColor(color as DeviceColor)}
                  className={`
                    w-7 h-7 rounded-full border-2 transition-all
                    ${
                      selectedColor === color
                        ? 'border-indigo-500 scale-110'
                        : 'border-transparent hover:border-neutral-500'
                    }
                  `}
                  style={{ backgroundColor: DEVICE_COLORS[color] }}
                  title={color.charAt(0).toUpperCase() + color.slice(1)}
                />
              )
            )}
          </div>
        </div>

        {/* Frame toggle */}
        <div className="flex items-center gap-3">
          <button
            onClick={() => setShowFrame(!showFrame)}
            className={`
              relative w-10 h-6 rounded-full transition-colors
              ${showFrame ? 'bg-indigo-500' : 'bg-neutral-700'}
            `}
          >
            <div
              className={`
                absolute top-1 w-4 h-4 rounded-full bg-white transition-transform
                ${showFrame ? 'left-5' : 'left-1'}
              `}
            />
          </button>
          <span className="text-sm text-neutral-300">Show device frame</span>
        </div>
      </div>

      {/* Device list */}
      <div className="space-y-2">
        {devices.map((device) => (
          <button
            key={device.id}
            onClick={() => handleAddDevice(device.id)}
            className="
              w-full p-3 rounded-lg bg-neutral-800/50 hover:bg-neutral-800
              border border-neutral-700/50 hover:border-neutral-600
              transition-all group text-left
            "
          >
            <div className="flex items-center gap-3">
              {/* Device preview */}
              <div
                className="w-10 h-16 rounded-lg flex items-center justify-center"
                style={{ backgroundColor: DEVICE_COLORS[selectedColor] }}
              >
                <div className="w-8 h-12 bg-black rounded" />
              </div>

              <div className="flex-1">
                <div className="text-sm font-medium text-white group-hover:text-indigo-400 transition-colors">
                  {device.name}
                </div>
                <div className="text-xs text-neutral-500">
                  {device.screenSize.width} x {device.screenSize.height}
                </div>
              </div>

              <div className="text-neutral-500 group-hover:text-indigo-400 transition-colors">
                <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth={2}
                    d="M12 4v16m8-8H4"
                  />
                </svg>
              </div>
            </div>
          </button>
        ))}
      </div>

      {/* Tips */}
      <div className="mt-6 p-3 rounded-lg bg-indigo-500/10 border border-indigo-500/20">
        <p className="text-xs text-indigo-300">
          Tip: Drag and drop screenshots onto devices to fill them. You can also double-click a
          device to import an image.
        </p>
      </div>
    </div>
  );
}

About

A web app to help you design things, local, offline, on device. In your browser.

0 stars
0 forks