import {type Editor} from '@tiptap/react';
import {
AlignCenter,
AlignJustify,
AlignLeft,
AlignRight,
Bold,
Code,
Heading1,
Heading2,
Heading3,
Image,
Italic,
Link,
List,
ListOrdered,
Palette,
Quote,
Redo,
Strikethrough,
Undo,
Variable,
} from 'lucide-react';
import {Button, Input} from '@plunk/ui';
import {useEffect, useState} from 'react';
interface ToolbarProps {
editor: Editor | null;
onInsertVariable: () => void;
onInsertImage: () => void;
canUploadImages: boolean;
}
export function Toolbar({editor, onInsertVariable, onInsertImage, canUploadImages}: ToolbarProps) {
const [showLinkInput, setShowLinkInput] = useState(false);
const [linkUrl, setLinkUrl] = useState('');
const [showColorPicker, setShowColorPicker] = useState(false);
const [selectedColor, setSelectedColor] = useState('#000000');
const [customColor, setCustomColor] = useState('');
const [, forceUpdate] = useState({});
// Force re-render when editor state changes
useEffect(() => {
if (!editor) return;
const updateHandler = () => {
forceUpdate({});
};
editor.on('selectionUpdate', updateHandler);
editor.on('transaction', updateHandler);
return () => {
editor.off('selectionUpdate', updateHandler);
editor.off('transaction', updateHandler);
};
}, [editor]);
if (!editor) {
return null;
}
const addLink = () => {
if (linkUrl) {
// If updating an existing link, extend selection to cover the entire link first
if (editor.isActive('link')) {
editor.chain().focus().extendMarkRange('link').setLink({href: linkUrl}).run();
} else {
editor.chain().focus().setLink({href: linkUrl}).run();
}
setLinkUrl('');
setShowLinkInput(false);
}
};
const removeLink = () => {
editor.chain().focus().unsetLink().run();
setLinkUrl('');
setShowLinkInput(false);
};
const setColor = (color: string) => {
editor.chain().focus().setColor(color).run();
setSelectedColor(color);
};
const applyCustomColor = () => {
if (customColor && /^#[0-9A-F]{6}$/i.test(customColor)) {
setColor(customColor);
setCustomColor('');
setShowColorPicker(false);
}
};
// Tailwind color palette organized by hue
const colorGroups = [
{
name: 'Neutrals',
colors: ['#000000', '#374151', '#6B7280', '#9CA3AF', '#D1D5DB', '#F3F4F6', '#FFFFFF'],
},
{
name: 'Reds',
colors: ['#7F1D1D', '#991B1B', '#DC2626', '#EF4444', '#F87171', '#FCA5A5', '#FEE2E2'],
},
{
name: 'Oranges',
colors: ['#7C2D12', '#C2410C', '#EA580C', '#F97316', '#FB923C', '#FDBA74', '#FED7AA'],
},
{
name: 'Yellows',
colors: ['#713F12', '#A16207', '#CA8A04', '#EAB308', '#FACC15', '#FDE047', '#FEF08A'],
},
{
name: 'Greens',
colors: ['#14532D', '#15803D', '#16A34A', '#22C55E', '#4ADE80', '#86EFAC', '#BBF7D0'],
},
{
name: 'Blues',
colors: ['#1E3A8A', '#1D4ED8', '#2563EB', '#3B82F6', '#60A5FA', '#93C5FD', '#DBEAFE'],
},
{
name: 'Purples',
colors: ['#581C87', '#6B21A8', '#7C3AED', '#8B5CF6', '#A78BFA', '#C4B5FD', '#E9D5FF'],
},
{
name: 'Pinks',
colors: ['#831843', '#9F1239', '#DB2777', '#EC4899', '#F472B6', '#F9A8D4', '#FBCFE8'],
},
];
return (
{/* History */}
{/* Text formatting */}
{/* Headings */}
{/* Lists */}
{/* Alignment */}
{/* Color picker */}
{showColorPicker && (
{/* Custom color input */}
{/* Color palette */}
{colorGroups.map(group => (
{group.colors.map(color => (
))}
))}
)}
{/* Link */}
{showLinkInput && (
setLinkUrl(e.target.value)}
placeholder="https://example.com"
className="px-2 py-1 text-sm border border-neutral-200 rounded w-64"
onKeyDown={e => {
if (e.key === 'Enter') {
addLink();
} else if (e.key === 'Escape') {
setShowLinkInput(false);
setLinkUrl('');
}
}}
autoFocus
/>
{editor.isActive('link') && (
)}
)}
{/* Image */}
{/* Variable */}
);
}