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) => (
))}
);
}