376 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|