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 { Type, AlignLeft, AlignCenter, AlignRight, Bold, Italic } 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' },
],
};
// Flatten for backward compatibility
const fontFamilies = Object.values(fontCategories).flat();
const textPresets = [
{
name: 'Headline',
fontSize: 72,
fontWeight: 700,
fontFamily: 'Inter, sans-serif',
letterSpacing: -2,
},
{
name: 'Subheadline',
fontSize: 36,
fontWeight: 500,
fontFamily: 'Inter, sans-serif',
letterSpacing: -0.5,
},
{
name: 'Body',
fontSize: 24,
fontWeight: 400,
fontFamily: 'Inter, sans-serif',
letterSpacing: 0,
},
{
name: 'Caption',
fontSize: 16,
fontWeight: 400,
fontFamily: 'Inter, sans-serif',
letterSpacing: 0.5,
},
{
name: 'Bold Statement',
fontSize: 48,
fontWeight: 800,
fontFamily: 'Montserrat, sans-serif',
letterSpacing: -1,
},
];
export function TextPanel() {
const [showColorPicker, setShowColorPicker] = useState(false);
const [textColor, setTextColor] = useState('#ffffff');
const [fontSize, setFontSize] = useState(48);
const [fontWeight, setFontWeight] = useState(600);
const [fontFamily, setFontFamily] = useState('Inter, sans-serif');
const [textAlign, setTextAlign] = useState<'left' | 'center' | 'right'>('center');
const { addLayer, canvas } = useEditorStore();
const handleAddText = (preset?: (typeof textPresets)[0]) => {
const data: TextLayerData = {
type: 'text',
text: 'Your text here',
fontFamily: preset?.fontFamily || fontFamily,
fontSize: preset?.fontSize || fontSize,
fontWeight: preset?.fontWeight || fontWeight,
color: textColor,
align: textAlign,
lineHeight: 1.2,
letterSpacing: preset?.letterSpacing || 0,
};
const width = canvas.width * 0.8;
const height = (preset?.fontSize || fontSize) * 1.5;
addLayer({
type: 'text',
name: preset?.name || 'Text',
visible: true,
locked: false,
opacity: 1,
position: {
x: (canvas.width - width) / 2,
y: canvas.height / 2 - height / 2,
},
size: { width, height },
rotation: 0,
data,
});
};
return (
<div className="p-4">
<h2 className="text-lg font-semibold text-white mb-4">Text</h2>
{/* Quick presets */}
<div className="mb-6">
<label className="text-sm text-neutral-400 block mb-2">Quick Add</label>
<div className="space-y-2">
{textPresets.map((preset) => (
<button
key={preset.name}
onClick={() => handleAddText(preset)}
className="
w-full p-3 rounded-lg text-left
bg-neutral-800/50 hover:bg-neutral-800
border border-neutral-700/50 hover:border-neutral-600
transition-all group
"
>
<div
className="text-white group-hover:text-indigo-400 transition-colors truncate"
style={{
fontFamily: preset.fontFamily,
fontSize: `${Math.min(preset.fontSize / 4, 20)}px`,
fontWeight: preset.fontWeight,
}}
>
{preset.name}
</div>
<div className="text-xs text-neutral-500 mt-1">
{preset.fontSize}px / {preset.fontWeight}
</div>
</button>
))}
</div>
</div>
{/* Custom text options */}
<div className="space-y-4">
<div>
<label className="text-sm text-neutral-400 block mb-2">Font Family</label>
<select
value={fontFamily}
onChange={(e) => setFontFamily(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
"
>
{Object.entries(fontCategories).map(([category, fonts]) => (
<optgroup key={category} label={category}>
{fonts.map((font) => (
<option key={font.value} value={font.value}>
{font.name}
</option>
))}
</optgroup>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-sm text-neutral-400 block mb-2">Size</label>
<input
type="number"
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
min={8}
max={200}
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>
<div>
<label className="text-sm text-neutral-400 block mb-2">Weight</label>
<select
value={fontWeight}
onChange={(e) => setFontWeight(Number(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
"
>
<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}>Extra Bold</option>
<option value={900}>Black</option>
</select>
</div>
</div>
{/* Text alignment */}
<div>
<label className="text-sm text-neutral-400 block mb-2">Alignment</label>
<div className="flex gap-2">
{[
{ value: 'left', icon: AlignLeft },
{ value: 'center', icon: AlignCenter },
{ value: 'right', icon: AlignRight },
].map(({ value, icon: Icon }) => (
<button
key={value}
onClick={() => setTextAlign(value as 'left' | 'center' | 'right')}
className={`
flex-1 p-2 rounded-lg flex items-center justify-center
transition-colors
${
textAlign === value
? 'bg-indigo-500/20 text-indigo-400 border border-indigo-500/50'
: 'bg-neutral-800 text-neutral-400 border border-neutral-700 hover:text-white'
}
`}
>
<Icon className="w-4 h-4" />
</button>
))}
</div>
</div>
{/* Color */}
<div>
<label className="text-sm text-neutral-400 block mb-2">Color</label>
<button
onClick={() => setShowColorPicker(!showColorPicker)}
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: textColor }}
/>
<span className="text-white font-mono text-sm">{textColor.toUpperCase()}</span>
</button>
{showColorPicker && (
<div className="mt-2 p-3 bg-neutral-800 rounded-lg border border-neutral-700">
<HexColorPicker color={textColor} onChange={setTextColor} />
<input
type="text"
value={textColor}
onChange={(e) => setTextColor(e.target.value)}
className="
w-full mt-3 px-3 py-2 rounded
bg-neutral-900 border border-neutral-700
text-white text-sm font-mono
focus:outline-none focus:border-indigo-500
"
/>
</div>
)}
</div>
{/* Add custom text button */}
<button
onClick={() => handleAddText()}
className="
w-full py-3 rounded-lg
bg-indigo-500 hover:bg-indigo-600
text-white font-medium
transition-colors flex items-center justify-center gap-2
"
>
<Type className="w-4 h-4" />
Add Text
</button>
</div>
</div>
);
}
About
A web app to help you design things, local, offline, on device. In your browser.
0 stars
0 forks