import { useCurrentEditor, useEditorState } from '@tiptap/react'; import { Bold, Code, FileCode2, Heading1, Heading2, Heading3, Italic, List, ListOrdered, Minus, Quote, Redo2, Strikethrough, Undo2, } from 'lucide-react'; import { cn } from '@renderer/lib/utils'; import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'; import type { ToolbarConfig } from './types'; interface TiptapToolbarProps { config?: ToolbarConfig; } function ToolbarButton({ icon, active, disabled, onClick, label, }: { icon: React.ReactNode; active?: boolean; disabled?: boolean; onClick: () => void; label: string; }) { return ( {label} ); } function Divider() { return
; } export function TiptapToolbar({ config }: TiptapToolbarProps) { const { editor } = useCurrentEditor(); // useEditorState — КРИТИЧНО для v3! // Без этого active state НЕ обновляется (shouldRerenderOnTransaction: false) const state = useEditorState({ editor, selector: ({ editor: e }) => { if (!e) return null; return { isBold: e.isActive('bold'), isItalic: e.isActive('italic'), isStrike: e.isActive('strike'), isCode: e.isActive('code'), isCodeBlock: e.isActive('codeBlock'), isBulletList: e.isActive('bulletList'), isOrderedList: e.isActive('orderedList'), isBlockquote: e.isActive('blockquote'), headingLevel: ([1, 2, 3] as const).find((l) => e.isActive('heading', { level: l })) ?? 0, canUndo: e.can().undo(), canRedo: e.can().redo(), }; }, }); if (!editor || !state) return null; const c = { bold: true, italic: true, strike: true, code: true, codeBlock: true, heading: { levels: [1, 2, 3] as (1 | 2 | 3)[] }, bulletList: true, orderedList: true, blockquote: true, horizontalRule: true, undoRedo: true, ...config, }; const headingLevels = c.heading === false ? [] : (c.heading?.levels ?? [1, 2, 3]); const groups: React.ReactNode[][] = []; // Group 1: Text formatting const textGroup: React.ReactNode[] = []; if (c.bold) textGroup.push( } active={state.isBold} onClick={() => editor.chain().focus().toggleBold().run()} label="Bold (⌘B)" /> ); if (c.italic) textGroup.push( } active={state.isItalic} onClick={() => editor.chain().focus().toggleItalic().run()} label="Italic (⌘I)" /> ); if (c.strike) textGroup.push( } active={state.isStrike} onClick={() => editor.chain().focus().toggleStrike().run()} label="Strikethrough (⌘⇧S)" /> ); if (textGroup.length) groups.push(textGroup); // Group 2: Code const codeGroup: React.ReactNode[] = []; if (c.code) codeGroup.push( } active={state.isCode} onClick={() => editor.chain().focus().toggleCode().run()} label="Code (⌘E)" /> ); if (c.codeBlock) codeGroup.push( } active={state.isCodeBlock} onClick={() => editor.chain().focus().toggleCodeBlock().run()} label="Code Block (⌘⌥C)" /> ); if (codeGroup.length) groups.push(codeGroup); // Group 3: Headings const headingIcons = { 1: Heading1, 2: Heading2, 3: Heading3 } as const; const headingGroup: React.ReactNode[] = headingLevels.map((level) => { const Icon = headingIcons[level]; return ( } active={state.headingLevel === level} onClick={() => editor.chain().focus().toggleHeading({ level }).run()} label={`Heading ${level}`} /> ); }); if (headingGroup.length) groups.push(headingGroup); // Group 4: Lists const listGroup: React.ReactNode[] = []; if (c.bulletList) listGroup.push( } active={state.isBulletList} onClick={() => editor.chain().focus().toggleBulletList().run()} label="Bullet List (⌘⇧8)" /> ); if (c.orderedList) listGroup.push( } active={state.isOrderedList} onClick={() => editor.chain().focus().toggleOrderedList().run()} label="Ordered List (⌘⇧7)" /> ); if (listGroup.length) groups.push(listGroup); // Group 5: Blocks const blockGroup: React.ReactNode[] = []; if (c.blockquote) blockGroup.push( } active={state.isBlockquote} onClick={() => editor.chain().focus().toggleBlockquote().run()} label="Blockquote (⌘⇧B)" /> ); if (c.horizontalRule) blockGroup.push( } onClick={() => editor.chain().focus().setHorizontalRule().run()} label="Horizontal Rule" /> ); if (blockGroup.length) groups.push(blockGroup); // Group 6: Undo/Redo if (c.undoRedo) { groups.push([ } disabled={!state.canUndo} onClick={() => editor.chain().focus().undo().run()} label="Undo (⌘Z)" />, } disabled={!state.canRedo} onClick={() => editor.chain().focus().redo().run()} label="Redo (⌘⇧Z)" />, ]); } return (
{groups.map((group, i) => (
{i > 0 && } {group}
))}
); }