import Placeholder from '@tiptap/extension-placeholder'; import { EditorContent, useEditor, useEditorState } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import React, { useEffect, useMemo } from 'react'; import { normalizeSceneDescriptionHtml } from '../editor/sceneDescriptionHtml'; import modalStyles from '../editor/SceneDescriptionModal.module.css'; import { useEditorI18n } from '../editor/i18n/EditorI18nContext'; import styles from './NpcsEditorApp.module.css'; import { readTipTapHtmlSafe } from './tiptapEditorSafe'; type NpcDescriptionFieldProps = { html: string; onCommit: (html: string) => void; }; function ToolButton({ active = false, title, onClick, children, }: { active?: boolean; title: string; onClick: () => void; children: React.ReactNode; }) { return ( ); } export function NpcDescriptionField({ html, onCommit }: NpcDescriptionFieldProps) { const { t } = useEditorI18n(); const extensions = useMemo( () => [ StarterKit.configure({ heading: { levels: [2, 3] }, codeBlock: false, link: false, }), Placeholder.configure({ placeholder: t('npcs.descriptionPlaceholder'), }), ], [t], ); const editor = useEditor({ extensions, content: html || '', // StrictMode + true даёт destroy/recreate с null schema → падение getHTML (чёрный экран окна НПС). immediatelyRender: false, shouldRerenderOnTransaction: true, editorProps: { attributes: { class: [modalStyles.prose, 'tiptap'].join(' '), 'aria-label': t('npcs.description'), }, }, onBlur: ({ editor: ed }) => { const raw = readTipTapHtmlSafe(ed); if (raw == null) return; onCommit(normalizeSceneDescriptionHtml(raw)); }, }); useEffect(() => { if (!editor || editor.isDestroyed) return; const raw = readTipTapHtmlSafe(editor); if (raw == null) return; const current = normalizeSceneDescriptionHtml(raw); const next = normalizeSceneDescriptionHtml(html); if (current !== next) { editor.commands.setContent(html || '', { emitUpdate: false }); } }, [editor, html]); const toolbarState = useEditorState({ editor, selector: ({ editor: ed }) => ({ bold: Boolean(ed && !ed.isDestroyed && ed.isActive('bold')), italic: Boolean(ed && !ed.isDestroyed && ed.isActive('italic')), bulletList: Boolean(ed && !ed.isDestroyed && ed.isActive('bulletList')), orderedList: Boolean(ed && !ed.isDestroyed && ed.isActive('orderedList')), h2: Boolean(ed && !ed.isDestroyed && ed.isActive('heading', { level: 2 })), h3: Boolean(ed && !ed.isDestroyed && ed.isActive('heading', { level: 3 })), }), }); if (!editor || editor.isDestroyed) return null; return (