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