import { useState, useRef, useEffect } from 'react' import { Bold, Italic, Underline as UnderlineIcon, Strikethrough, Heading1, Heading2, Heading3, Type, AlignLeft, AlignCenter, AlignRight, AlignJustify, List, ListOrdered, Quote, Code2, Link2, ImageIcon, Video, Table2, Undo2, Redo2, Palette, Highlighter, ChevronDown, Minus, } from 'lucide-react' const FONT_SIZES = ['10', '11', '12', '14', '16', '18', '20', '24', '28', '32', '36', '48', '64', '72'] /* ── Primitive components ── */ function Btn({ onClick, active, disabled, title, children }) { return ( ) } function Sep() { return
} /* ── Hook: позиция дропдауна через fixed (вырывается из overflow-контейнеров) ── */ function useFixedDropdown() { const [open, setOpen] = useState(false) const [pos, setPos] = useState({ top: 0, left: 0 }) const btnRef = useRef(null) const toggle = e => { e.preventDefault() if (!open && btnRef.current) { const r = btnRef.current.getBoundingClientRect() setPos({ top: r.bottom + 4, left: r.left }) } setOpen(o => !o) } useEffect(() => { if (!open) return const close = e => { if (!btnRef.current?.contains(e.target)) setOpen(false) } document.addEventListener('mousedown', close) return () => document.removeEventListener('mousedown', close) }, [open]) return { open, setOpen, pos, btnRef, toggle } } /* ── Font family dropdown ── */ function FontDropdown({ editor, fonts }) { const { open, setOpen, pos, btnRef, toggle } = useFixedDropdown() const current = editor.getAttributes('textStyle').fontFamily || 'Шрифт' return (
{open && (
{fonts.map(f => ( ))}
)}
) } /* ── Font size dropdown ── */ function SizeDropdown({ editor }) { const { open, setOpen, pos, btnRef, toggle } = useFixedDropdown() const current = editor.getAttributes('textStyle').fontSize?.replace('px', '') || '—' return (
{open && (
{FONT_SIZES.map(s => ( ))}
)}
) } /* ── Table menu ── */ function TableMenu({ editor }) { const { open, setOpen, pos, btnRef, toggle } = useFixedDropdown() const items = [ { label: 'Вставить таблицу 3×3', fn: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() }, null, { label: 'Добавить строку ниже', fn: () => editor.chain().focus().addRowAfter().run() }, { label: 'Добавить строку выше', fn: () => editor.chain().focus().addRowBefore().run() }, { label: 'Удалить строку', fn: () => editor.chain().focus().deleteRow().run(), danger: true }, null, { label: 'Добавить столбец справа', fn: () => editor.chain().focus().addColumnAfter().run() }, { label: 'Добавить столбец слева', fn: () => editor.chain().focus().addColumnBefore().run() }, { label: 'Удалить столбец', fn: () => editor.chain().focus().deleteColumn().run(), danger: true }, null, { label: 'Удалить таблицу', fn: () => editor.chain().focus().deleteTable().run(), danger: true }, ] return (
toggle({ preventDefault: () => {}, ...e })} active={editor.isActive('table')} title="Таблица" > {/* Используем ref на обёртке для Btn */} {open && (
{items.map((item, i) => item === null ? (
) : ( ) )}
)}
) } /* ── Main Toolbar ── */ export function Toolbar({ editor, onInsertImage, onInsertVideo, fonts }) { if (!editor) return null const setLink = () => { const prev = editor.getAttributes('link').href || '' const url = window.prompt('URL ссылки:', prev) if (url === null) return if (!url) editor.chain().focus().unsetLink().run() else editor.chain().focus().setLink({ href: url }).run() } return (
{/* History */} editor.chain().focus().undo().run()} disabled={!editor.can().undo()} title="Отменить (Ctrl+Z)"> editor.chain().focus().redo().run()} disabled={!editor.can().redo()} title="Повторить (Ctrl+Y)"> {/* Text format */} editor.chain().focus().toggleBold().run()} active={editor.isActive('bold')} title="Жирный (Ctrl+B)"> editor.chain().focus().toggleItalic().run()} active={editor.isActive('italic')} title="Курсив (Ctrl+I)"> editor.chain().focus().toggleUnderline().run()} active={editor.isActive('underline')} title="Подчёркивание (Ctrl+U)"> editor.chain().focus().toggleStrike().run()} active={editor.isActive('strike')} title="Зачёркивание"> {/* Headings */} editor.chain().focus().toggleHeading({ level: 1 }).run()} active={editor.isActive('heading', { level: 1 })} title="Заголовок 1"> editor.chain().focus().toggleHeading({ level: 2 }).run()} active={editor.isActive('heading', { level: 2 })} title="Заголовок 2"> editor.chain().focus().toggleHeading({ level: 3 }).run()} active={editor.isActive('heading', { level: 3 })} title="Заголовок 3"> editor.chain().focus().setParagraph().run()} active={editor.isActive('paragraph') && !editor.isActive('heading')} title="Обычный текст"> {/* Alignment */} editor.chain().focus().setTextAlign('left').run()} active={editor.isActive({ textAlign: 'left' })} title="По левому краю"> editor.chain().focus().setTextAlign('center').run()} active={editor.isActive({ textAlign: 'center' })} title="По центру"> editor.chain().focus().setTextAlign('right').run()} active={editor.isActive({ textAlign: 'right' })} title="По правому краю"> editor.chain().focus().setTextAlign('justify').run()} active={editor.isActive({ textAlign: 'justify' })} title="По ширине"> {/* Lists + blocks */} editor.chain().focus().toggleBulletList().run()} active={editor.isActive('bulletList')} title="Маркированный список"> editor.chain().focus().toggleOrderedList().run()} active={editor.isActive('orderedList')} title="Нумерованный список"> editor.chain().focus().toggleBlockquote().run()} active={editor.isActive('blockquote')} title="Цитата"> editor.chain().focus().toggleCodeBlock().run()} active={editor.isActive('codeBlock')} title="Блок кода"> editor.chain().focus().setHorizontalRule().run()} title="Разделитель"> {/* Insert media */} {/* Typography */} {/* Text color */} {/* Highlight color */}
) }