Files
2026-01-24 16:52:19 -06:00

376 lines
10 KiB
TypeScript

'use client';
import { useState, useRef, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {
Bold,
Italic,
Strikethrough,
Heading1,
Heading2,
Heading3,
Link,
Image,
List,
ListOrdered,
Quote,
Code,
FileCode,
Table,
Undo2,
Redo2,
ChevronDown,
Plus,
Minus,
FileText,
AlertTriangle,
Info,
CheckCircle,
} from 'lucide-react';
interface EditorToolbarProps {
onBold: () => void;
onItalic: () => void;
onStrikethrough: () => void;
onHeading: (level: number) => void;
onLink: () => void;
onImage: () => void;
onBulletList: () => void;
onNumberedList: () => void;
onBlockquote: () => void;
onCode: () => void;
onCodeBlock: () => void;
onTable: () => void;
onUndo: () => void;
onRedo: () => void;
canUndo: boolean;
canRedo: boolean;
disabled?: boolean;
onInsertTemplate?: (template: string) => void;
}
const TEMPLATES = {
infobox: `> **Note:** This is an informational callout.`,
warning: `> **Warning:** This requires attention.`,
tip: `> **Tip:** Here's a helpful suggestion.`,
divider: `\n---\n`,
footnote: `[^1]: Reference text here.`,
definition: `**Term**\n: Definition of the term goes here.`,
collapse: `<details>\n<summary>Click to expand</summary>\n\nHidden content goes here.\n\n</details>`,
};
export function EditorToolbar({
onBold,
onItalic,
onStrikethrough,
onHeading,
onLink,
onImage,
onBulletList,
onNumberedList,
onBlockquote,
onCode,
onCodeBlock,
onTable,
onUndo,
onRedo,
canUndo,
canRedo,
disabled = false,
onInsertTemplate,
}: EditorToolbarProps) {
const [showHeadingMenu, setShowHeadingMenu] = useState(false);
const [showInsertMenu, setShowInsertMenu] = useState(false);
const headingRef = useRef<HTMLDivElement>(null);
const insertRef = useRef<HTMLDivElement>(null);
// Close menus when clicking outside
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (headingRef.current && !headingRef.current.contains(e.target as Node)) {
setShowHeadingMenu(false);
}
if (insertRef.current && !insertRef.current.contains(e.target as Node)) {
setShowInsertMenu(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="editor-toolbar-enhanced">
{/* Undo/Redo group */}
<div className="toolbar-group">
<Button
variant="ghost"
size="icon-sm"
onClick={onUndo}
disabled={disabled || !canUndo}
title="Undo (Ctrl+Z)"
className="toolbar-btn"
>
<Undo2 className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={onRedo}
disabled={disabled || !canRedo}
title="Redo (Ctrl+Shift+Z)"
className="toolbar-btn"
>
<Redo2 className="w-4 h-4" />
</Button>
</div>
<div className="toolbar-divider" />
{/* Heading dropdown */}
<div className="toolbar-group heading-dropdown-container" ref={headingRef}>
<Button
variant="ghost"
size="sm"
onClick={() => setShowHeadingMenu(!showHeadingMenu)}
disabled={disabled}
className="toolbar-btn heading-btn"
>
<Heading1 className="w-4 h-4" />
<ChevronDown className="w-3 h-3 ml-1" />
</Button>
{showHeadingMenu && (
<div className="heading-dropdown">
<button
className="heading-option"
onClick={() => { onHeading(1); setShowHeadingMenu(false); }}
>
<Heading1 className="w-5 h-5" />
<span>Heading 1</span>
</button>
<button
className="heading-option"
onClick={() => { onHeading(2); setShowHeadingMenu(false); }}
>
<Heading2 className="w-5 h-5" />
<span>Heading 2</span>
</button>
<button
className="heading-option"
onClick={() => { onHeading(3); setShowHeadingMenu(false); }}
>
<Heading3 className="w-5 h-5" />
<span>Heading 3</span>
</button>
</div>
)}
</div>
<div className="toolbar-divider" />
{/* Text formatting group */}
<div className="toolbar-group">
<Button
variant="ghost"
size="icon-sm"
onClick={onBold}
disabled={disabled}
title="Bold (Ctrl+B)"
className="toolbar-btn"
>
<Bold className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={onItalic}
disabled={disabled}
title="Italic (Ctrl+I)"
className="toolbar-btn"
>
<Italic className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={onStrikethrough}
disabled={disabled}
title="Strikethrough"
className="toolbar-btn"
>
<Strikethrough className="w-4 h-4" />
</Button>
</div>
<div className="toolbar-divider" />
{/* Links and media group */}
<div className="toolbar-group">
<Button
variant="ghost"
size="icon-sm"
onClick={onLink}
disabled={disabled}
title="Insert Link (Ctrl+K)"
className="toolbar-btn"
>
<Link className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={onImage}
disabled={disabled}
title="Insert Image"
className="toolbar-btn"
>
<Image className="w-4 h-4" />
</Button>
</div>
<div className="toolbar-divider" />
{/* Lists and quotes group */}
<div className="toolbar-group">
<Button
variant="ghost"
size="icon-sm"
onClick={onBulletList}
disabled={disabled}
title="Bullet List"
className="toolbar-btn"
>
<List className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={onNumberedList}
disabled={disabled}
title="Numbered List"
className="toolbar-btn"
>
<ListOrdered className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={onBlockquote}
disabled={disabled}
title="Blockquote"
className="toolbar-btn"
>
<Quote className="w-4 h-4" />
</Button>
</div>
<div className="toolbar-divider" />
{/* Code group */}
<div className="toolbar-group">
<Button
variant="ghost"
size="icon-sm"
onClick={onCode}
disabled={disabled}
title="Inline Code"
className="toolbar-btn"
>
<Code className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={onCodeBlock}
disabled={disabled}
title="Code Block"
className="toolbar-btn"
>
<FileCode className="w-4 h-4" />
</Button>
</div>
<div className="toolbar-divider" />
{/* Table */}
<div className="toolbar-group">
<Button
variant="ghost"
size="icon-sm"
onClick={onTable}
disabled={disabled}
title="Insert Table"
className="toolbar-btn"
>
<Table className="w-4 h-4" />
</Button>
</div>
<div className="toolbar-divider" />
{/* Insert templates menu */}
{onInsertTemplate && (
<div className="toolbar-group heading-dropdown-container" ref={insertRef}>
<Button
variant="ghost"
size="sm"
onClick={() => setShowInsertMenu(!showInsertMenu)}
disabled={disabled}
className="toolbar-btn heading-btn"
>
<Plus className="w-4 h-4" />
<span className="text-xs ml-1">Insert</span>
<ChevronDown className="w-3 h-3 ml-1" />
</Button>
{showInsertMenu && (
<div className="heading-dropdown insert-dropdown">
<button
className="heading-option"
onClick={() => { onInsertTemplate(TEMPLATES.infobox); setShowInsertMenu(false); }}
>
<Info className="w-4 h-4 text-blue-500" />
<span>Info Box</span>
</button>
<button
className="heading-option"
onClick={() => { onInsertTemplate(TEMPLATES.warning); setShowInsertMenu(false); }}
>
<AlertTriangle className="w-4 h-4 text-yellow-500" />
<span>Warning Box</span>
</button>
<button
className="heading-option"
onClick={() => { onInsertTemplate(TEMPLATES.tip); setShowInsertMenu(false); }}
>
<CheckCircle className="w-4 h-4 text-green-500" />
<span>Tip Box</span>
</button>
<div className="dropdown-divider" />
<button
className="heading-option"
onClick={() => { onInsertTemplate(TEMPLATES.divider); setShowInsertMenu(false); }}
>
<Minus className="w-4 h-4" />
<span>Horizontal Rule</span>
</button>
<button
className="heading-option"
onClick={() => { onInsertTemplate(TEMPLATES.definition); setShowInsertMenu(false); }}
>
<FileText className="w-4 h-4" />
<span>Definition</span>
</button>
<button
className="heading-option"
onClick={() => { onInsertTemplate(TEMPLATES.collapse); setShowInsertMenu(false); }}
>
<ChevronDown className="w-4 h-4" />
<span>Collapsible Section</span>
</button>
</div>
)}
</div>
)}
</div>
);
}