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

import React, { useState } from 'react';
import {
  AlignLeft,
  AlignCenter,
  AlignRight,
  Bold,
  ChevronDown,
} from 'lucide-react';
import { HexColorPicker } from 'react-colorful';
import { useEditorStore } from '@/lib/store/editor-store';
import type { TextLayerData } from '@/types';

// Font families organized by category
const fontCategories = {
  'Sans-Serif (Modern)': [
    { name: 'Inter', value: 'Inter, sans-serif' },
    { name: 'DM Sans', value: '"DM Sans", sans-serif' },
    { name: 'Space Grotesk', value: '"Space Grotesk", sans-serif' },
    { name: 'Manrope', value: 'Manrope, sans-serif' },
    { name: 'Work Sans', value: '"Work Sans", sans-serif' },
    { name: 'Plus Jakarta Sans', value: '"Plus Jakarta Sans", sans-serif' },
    { name: 'Outfit', value: 'Outfit, sans-serif' },
  ],
  'Sans-Serif (Classic)': [
    { name: 'SF Pro', value: '-apple-system, BlinkMacSystemFont, sans-serif' },
    { name: 'Roboto', value: 'Roboto, sans-serif' },
    { name: 'Open Sans', value: '"Open Sans", sans-serif' },
    { name: 'Lato', value: 'Lato, sans-serif' },
    { name: 'Poppins', value: 'Poppins, sans-serif' },
    { name: 'Nunito', value: 'Nunito, sans-serif' },
  ],
  'Serif (Editorial)': [
    { name: 'Playfair Display', value: '"Playfair Display", serif' },
    { name: 'Libre Baskerville', value: '"Libre Baskerville", serif' },
    { name: 'Lora', value: 'Lora, serif' },
    { name: 'Merriweather', value: 'Merriweather, serif' },
    { name: 'Cormorant', value: 'Cormorant, serif' },
    { name: 'Georgia', value: 'Georgia, serif' },
  ],
  'Display (Headlines)': [
    { name: 'Montserrat', value: 'Montserrat, sans-serif' },
    { name: 'Bebas Neue', value: '"Bebas Neue", sans-serif' },
    { name: 'Oswald', value: 'Oswald, sans-serif' },
    { name: 'Archivo Black', value: '"Archivo Black", sans-serif' },
    { name: 'Righteous', value: 'Righteous, sans-serif' },
  ],
  Monospace: [
    { name: 'JetBrains Mono', value: '"JetBrains Mono", monospace' },
    { name: 'Fira Code', value: '"Fira Code", monospace' },
    { name: 'Source Code Pro', value: '"Source Code Pro", monospace' },
    { name: 'Courier', value: '"Courier New", monospace' },
  ],
};

const allFonts = Object.values(fontCategories).flat();

export function SelectionToolbar() {
  const [showFontDropdown, setShowFontDropdown] = useState(false);
  const [showColorPicker, setShowColorPicker] = useState(false);

  const { layers, selectedLayerIds, updateLayerData, saveToHistory } = useEditorStore();

  // Get selected layer(s)
  const selectedLayers = selectedLayerIds
    .map((id) => layers.find((l) => l.id === id))
    .filter(Boolean);

  // Only show for text layers
  const textLayer = selectedLayers.length === 1 && selectedLayers[0]?.type === 'text'
    ? selectedLayers[0]
    : null;

  if (!textLayer) {
    return null;
  }

  const data = textLayer.data as TextLayerData;

  const updateTextProperty = (updates: Partial<TextLayerData>) => {
    updateLayerData(textLayer.id, updates);
    saveToHistory('Updated text style');
  };

  // Get display name for current font
  const currentFontName = allFonts.find((f) => f.value === data.fontFamily)?.name || 'Inter';

  return (
    <div className="h-10 bg-neutral-800/90 backdrop-blur border-b border-neutral-700 flex items-center px-3 gap-2">
      {/* Font Family Dropdown */}
      <div className="relative">
        <button
          onClick={() => {
            setShowFontDropdown(!showFontDropdown);
            setShowColorPicker(false);
          }}
          className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-neutral-700 hover:bg-neutral-600 transition-colors min-w-[140px]"
        >
          <span
            className="text-sm text-white truncate"
            style={{ fontFamily: data.fontFamily }}
          >
            {currentFontName}
          </span>
          <ChevronDown className="w-3.5 h-3.5 text-neutral-400 flex-shrink-0" />
        </button>

        {showFontDropdown && (
          <>
            <div
              className="fixed inset-0 z-40"
              onClick={() => setShowFontDropdown(false)}
            />
            <div className="absolute top-full left-0 mt-1 w-64 bg-neutral-800 rounded-lg border border-neutral-700 shadow-xl z-50 max-h-80 overflow-y-auto">
              {Object.entries(fontCategories).map(([category, fonts]) => (
                <div key={category}>
                  <div className="px-3 py-1.5 text-xs text-neutral-500 uppercase tracking-wider sticky top-0 bg-neutral-800">
                    {category}
                  </div>
                  {fonts.map((font) => (
                    <button
                      key={font.value}
                      onClick={() => {
                        updateTextProperty({ fontFamily: font.value });
                        setShowFontDropdown(false);
                      }}
                      className={`
                        w-full px-3 py-2 text-left text-sm hover:bg-neutral-700 transition-colors
                        ${data.fontFamily === font.value ? 'text-indigo-400 bg-indigo-500/10' : 'text-white'}
                      `}
                      style={{ fontFamily: font.value }}
                    >
                      {font.name}
                    </button>
                  ))}
                </div>
              ))}
            </div>
          </>
        )}
      </div>

      {/* Divider */}
      <div className="w-px h-5 bg-neutral-600" />

      {/* Font Size */}
      <div className="flex items-center gap-1">
        <input
          type="number"
          value={data.fontSize}
          onChange={(e) => updateTextProperty({ fontSize: Number(e.target.value) })}
          min={8}
          max={400}
          className="w-14 px-2 py-1 rounded-md bg-neutral-700 border border-neutral-600 text-white text-sm text-center focus:outline-none focus:border-indigo-500"
        />
        <span className="text-xs text-neutral-400">px</span>
      </div>

      {/* Divider */}
      <div className="w-px h-5 bg-neutral-600" />

      {/* Font Weight */}
      <select
        value={data.fontWeight}
        onChange={(e) => updateTextProperty({ fontWeight: Number(e.target.value) })}
        className="px-2 py-1.5 rounded-md bg-neutral-700 border border-neutral-600 text-white text-sm focus:outline-none focus:border-indigo-500"
      >
        <option value={300}>Light</option>
        <option value={400}>Regular</option>
        <option value={500}>Medium</option>
        <option value={600}>Semibold</option>
        <option value={700}>Bold</option>
        <option value={800}>ExtraBold</option>
        <option value={900}>Black</option>
      </select>

      {/* Divider */}
      <div className="w-px h-5 bg-neutral-600" />

      {/* Text Alignment */}
      <div className="flex bg-neutral-700 rounded-md p-0.5">
        {[
          { value: 'left', icon: AlignLeft },
          { value: 'center', icon: AlignCenter },
          { value: 'right', icon: AlignRight },
        ].map(({ value, icon: Icon }) => (
          <button
            key={value}
            onClick={() => updateTextProperty({ align: value as 'left' | 'center' | 'right' })}
            className={`
              p-1.5 rounded transition-colors
              ${data.align === value ? 'bg-indigo-500 text-white' : 'text-neutral-400 hover:text-white'}
            `}
            title={`Align ${value}`}
          >
            <Icon className="w-4 h-4" />
          </button>
        ))}
      </div>

      {/* Divider */}
      <div className="w-px h-5 bg-neutral-600" />

      {/* Text Color */}
      <div className="relative">
        <button
          onClick={() => {
            setShowColorPicker(!showColorPicker);
            setShowFontDropdown(false);
          }}
          className="flex items-center gap-2 px-2 py-1.5 rounded-md bg-neutral-700 hover:bg-neutral-600 transition-colors"
          title="Text Color"
        >
          <div
            className="w-5 h-5 rounded border border-neutral-500"
            style={{ backgroundColor: data.color }}
          />
          <span className="text-xs text-neutral-300 font-mono uppercase">
            {data.color}
          </span>
        </button>

        {showColorPicker && (
          <>
            <div
              className="fixed inset-0 z-40"
              onClick={() => setShowColorPicker(false)}
            />
            <div className="absolute top-full left-0 mt-1 p-3 bg-neutral-800 rounded-lg border border-neutral-700 shadow-xl z-50">
              <HexColorPicker
                color={data.color}
                onChange={(color) => updateTextProperty({ color })}
              />
              <input
                type="text"
                value={data.color}
                onChange={(e) => updateTextProperty({ color: e.target.value })}
                className="w-full mt-2 px-2 py-1.5 rounded bg-neutral-700 border border-neutral-600 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
              />
            </div>
          </>
        )}
      </div>

      {/* Divider */}
      <div className="w-px h-5 bg-neutral-600" />

      {/* Letter Spacing */}
      <div className="flex items-center gap-1">
        <span className="text-xs text-neutral-400">Spacing</span>
        <input
          type="number"
          value={data.letterSpacing}
          onChange={(e) => updateTextProperty({ letterSpacing: Number(e.target.value) })}
          min={-10}
          max={50}
          step={0.5}
          className="w-12 px-1.5 py-1 rounded-md bg-neutral-700 border border-neutral-600 text-white text-sm text-center focus:outline-none focus:border-indigo-500"
        />
      </div>

      {/* Line Height */}
      <div className="flex items-center gap-1">
        <span className="text-xs text-neutral-400">Line</span>
        <input
          type="number"
          value={data.lineHeight}
          onChange={(e) => updateTextProperty({ lineHeight: Number(e.target.value) })}
          min={0.5}
          max={3}
          step={0.1}
          className="w-12 px-1.5 py-1 rounded-md bg-neutral-700 border border-neutral-600 text-white text-sm text-center focus:outline-none focus:border-indigo-500"
        />
      </div>
    </div>
  );
}

About

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

0 stars
0 forks