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%
BrandPanel.tsx 301 lines (10 KB)
'use client';

import React, { useState } from 'react';
import { Plus, Trash2, Upload, Palette, Type as TypeIcon, Image } from 'lucide-react';
import { HexColorPicker } from 'react-colorful';
import { useEditorStore } from '@/lib/store/editor-store';
import type { BrandKit, BrandColor, BrandFont } from '@/types';
import { v4 as uuidv4 } from 'uuid';

const defaultFonts = [
  { name: 'Inter', family: 'Inter, sans-serif' },
  { name: 'SF Pro', family: '-apple-system, BlinkMacSystemFont, sans-serif' },
  { name: 'Roboto', family: 'Roboto, sans-serif' },
  { name: 'Poppins', family: 'Poppins, sans-serif' },
  { name: 'Montserrat', family: 'Montserrat, sans-serif' },
];

export function BrandPanel() {
  const { brandKit, setBrandKit } = useEditorStore();

  const [showColorPicker, setShowColorPicker] = useState<string | null>(null);
  const [newColor, setNewColor] = useState('#4f46e5');

  // Initialize brand kit if not exists
  const initBrandKit = (): BrandKit => ({
    id: uuidv4(),
    name: 'My Brand',
    colors: [],
    fonts: [],
    logos: [],
  });

  const currentBrandKit = brandKit || initBrandKit();

  const handleAddColor = () => {
    const newBrandColor: BrandColor = {
      id: uuidv4(),
      name: `Color ${currentBrandKit.colors.length + 1}`,
      hex: newColor,
      usage: 'custom',
    };

    setBrandKit({
      ...currentBrandKit,
      colors: [...currentBrandKit.colors, newBrandColor],
    });
  };

  const handleRemoveColor = (colorId: string) => {
    setBrandKit({
      ...currentBrandKit,
      colors: currentBrandKit.colors.filter((c) => c.id !== colorId),
    });
  };

  const handleUpdateColor = (colorId: string, hex: string) => {
    setBrandKit({
      ...currentBrandKit,
      colors: currentBrandKit.colors.map((c) =>
        c.id === colorId ? { ...c, hex } : c
      ),
    });
  };

  const handleAddFont = (font: (typeof defaultFonts)[0]) => {
    const existing = currentBrandKit.fonts.find((f) => f.family === font.family);
    if (existing) return;

    const newBrandFont: BrandFont = {
      id: uuidv4(),
      name: font.name,
      family: font.family,
      weights: [400, 500, 600, 700],
      usage: 'custom',
    };

    setBrandKit({
      ...currentBrandKit,
      fonts: [...currentBrandKit.fonts, newBrandFont],
    });
  };

  const handleRemoveFont = (fontId: string) => {
    setBrandKit({
      ...currentBrandKit,
      fonts: currentBrandKit.fonts.filter((f) => f.id !== fontId),
    });
  };

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

      {/* Brand name */}
      <div className="mb-6">
        <label className="text-sm text-neutral-400 block mb-2">Brand Name</label>
        <input
          type="text"
          value={currentBrandKit.name}
          onChange={(e) => setBrandKit({ ...currentBrandKit, name: e.target.value })}
          className="
            w-full px-3 py-2 rounded-lg
            bg-neutral-800 border border-neutral-700
            text-white focus:outline-none focus:border-indigo-500
          "
        />
      </div>

      {/* Colors */}
      <div className="mb-6">
        <div className="flex items-center justify-between mb-3">
          <label className="text-sm text-neutral-400 flex items-center gap-2">
            <Palette className="w-4 h-4" />
            Brand Colors
          </label>
          <span className="text-xs text-neutral-500">{currentBrandKit.colors.length} colors</span>
        </div>

        <div className="space-y-2 mb-3">
          {currentBrandKit.colors.map((color) => (
            <div
              key={color.id}
              className="flex items-center gap-2 p-2 rounded-lg bg-neutral-800/50 border border-neutral-700/50"
            >
              <button
                onClick={() =>
                  setShowColorPicker(showColorPicker === color.id ? null : color.id)
                }
                className="w-8 h-8 rounded-lg border border-neutral-600"
                style={{ backgroundColor: color.hex }}
              />
              <div className="flex-1">
                <input
                  type="text"
                  value={color.name}
                  onChange={(e) =>
                    setBrandKit({
                      ...currentBrandKit,
                      colors: currentBrandKit.colors.map((c) =>
                        c.id === color.id ? { ...c, name: e.target.value } : c
                      ),
                    })
                  }
                  className="
                    w-full px-2 py-1 rounded bg-transparent
                    text-white text-sm focus:outline-none focus:bg-neutral-700
                  "
                />
                <div className="text-xs text-neutral-500 font-mono">{color.hex.toUpperCase()}</div>
              </div>
              <button
                onClick={() => handleRemoveColor(color.id)}
                className="p-1 text-neutral-500 hover:text-red-400 transition-colors"
              >
                <Trash2 className="w-4 h-4" />
              </button>

              {showColorPicker === color.id && (
                <div className="absolute z-10 mt-32 ml-10">
                  <div
                    className="fixed inset-0"
                    onClick={() => setShowColorPicker(null)}
                  />
                  <div className="relative p-3 bg-neutral-800 rounded-lg border border-neutral-700 shadow-xl">
                    <HexColorPicker
                      color={color.hex}
                      onChange={(hex) => handleUpdateColor(color.id, hex)}
                    />
                  </div>
                </div>
              )}
            </div>
          ))}
        </div>

        {/* Add new color */}
        <div className="flex gap-2">
          <button
            onClick={() => setShowColorPicker(showColorPicker === 'new' ? null : 'new')}
            className="w-10 h-10 rounded-lg border border-neutral-600"
            style={{ backgroundColor: newColor }}
          />
          <button
            onClick={handleAddColor}
            className="
              flex-1 py-2 rounded-lg
              bg-neutral-800 hover:bg-neutral-700
              border border-neutral-700 border-dashed
              text-neutral-400 hover:text-white
              flex items-center justify-center gap-2
              transition-colors
            "
          >
            <Plus className="w-4 h-4" />
            Add Color
          </button>

          {showColorPicker === 'new' && (
            <div className="absolute z-10 mt-12">
              <div
                className="fixed inset-0"
                onClick={() => setShowColorPicker(null)}
              />
              <div className="relative p-3 bg-neutral-800 rounded-lg border border-neutral-700 shadow-xl">
                <HexColorPicker color={newColor} onChange={setNewColor} />
              </div>
            </div>
          )}
        </div>
      </div>

      {/* Fonts */}
      <div className="mb-6">
        <div className="flex items-center justify-between mb-3">
          <label className="text-sm text-neutral-400 flex items-center gap-2">
            <TypeIcon className="w-4 h-4" />
            Brand Fonts
          </label>
          <span className="text-xs text-neutral-500">{currentBrandKit.fonts.length} fonts</span>
        </div>

        <div className="space-y-2 mb-3">
          {currentBrandKit.fonts.map((font) => (
            <div
              key={font.id}
              className="flex items-center justify-between p-2 rounded-lg bg-neutral-800/50 border border-neutral-700/50"
            >
              <div>
                <div className="text-sm text-white" style={{ fontFamily: font.family }}>
                  {font.name}
                </div>
                <div className="text-xs text-neutral-500">
                  {font.weights.join(', ')}
                </div>
              </div>
              <button
                onClick={() => handleRemoveFont(font.id)}
                className="p-1 text-neutral-500 hover:text-red-400 transition-colors"
              >
                <Trash2 className="w-4 h-4" />
              </button>
            </div>
          ))}
        </div>

        {/* Available fonts */}
        <div className="space-y-1">
          {defaultFonts
            .filter((f) => !currentBrandKit.fonts.some((bf) => bf.family === f.family))
            .map((font) => (
              <button
                key={font.family}
                onClick={() => handleAddFont(font)}
                className="
                  w-full flex items-center justify-between p-2 rounded-lg
                  hover:bg-neutral-800 text-left transition-colors
                "
              >
                <span className="text-sm text-neutral-400" style={{ fontFamily: font.family }}>
                  {font.name}
                </span>
                <Plus className="w-4 h-4 text-neutral-500" />
              </button>
            ))}
        </div>
      </div>

      {/* Logos */}
      <div>
        <div className="flex items-center justify-between mb-3">
          <label className="text-sm text-neutral-400 flex items-center gap-2">
            <Image className="w-4 h-4" />
            Logos
          </label>
        </div>

        <button
          className="
            w-full p-4 rounded-xl
            border-2 border-dashed border-neutral-700 hover:border-neutral-600
            bg-neutral-800/30 hover:bg-neutral-800/50
            flex flex-col items-center gap-2
            transition-colors
          "
        >
          <Upload className="w-6 h-6 text-neutral-500" />
          <span className="text-sm text-neutral-400">Upload Logo</span>
          <span className="text-xs text-neutral-500">SVG, PNG, or JPG</span>
        </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: Brand colors and fonts will appear as quick-access options throughout the editor.
        </p>
      </div>
    </div>
  );
}

About

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

0 stars
0 forks