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 {
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