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 */}
)
}