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 { createPortal } from 'react-dom'; import { Button } from '../shared/ui/controls'; import styles from './EditorApp.module.css'; import { useEditorI18n } from './i18n/EditorI18nContext'; import { normalizeSceneDescriptionHtml } from './sceneDescriptionHtml'; import modalStyles from './SceneDescriptionModal.module.css'; type SceneDescriptionModalProps = { initialHtml: string; onClose: () => void; onSave: (html: string) => void; }; function ToolbarIcon({ path, size = 14 }: { path: string; size?: number }) { return ( ); } function ToolButton({ active = false, disabled = false, title, onClick, children, }: { active?: boolean; disabled?: boolean; title: string; onClick: () => void; children: React.ReactNode; }) { return ( ); } export function SceneDescriptionModal({ initialHtml, onClose, onSave }: SceneDescriptionModalProps) { const { t } = useEditorI18n(); const extensions = useMemo( () => [ StarterKit.configure({ heading: { levels: [2, 3] }, codeBlock: false, link: false, }), Placeholder.configure({ placeholder: t('scene.descriptionPlaceholder'), }), ], [t], ); const editor = useEditor({ extensions, content: initialHtml || '', immediatelyRender: false, shouldRerenderOnTransaction: true, editorProps: { attributes: { class: [modalStyles.prose, 'tiptap'].join(' '), 'aria-label': t('scene.descriptionModalTitle'), }, }, }); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); const toolbarState = useEditorState({ editor, selector: ({ editor: ed }) => ({ bold: Boolean(ed && !ed.isDestroyed && ed.isActive('bold')), italic: Boolean(ed && !ed.isDestroyed && ed.isActive('italic')), underline: Boolean(ed && !ed.isDestroyed && ed.isActive('underline')), 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 })), blockquote: Boolean(ed && !ed.isDestroyed && ed.isActive('blockquote')), }), }); const handleSave = () => { if (!editor || editor.isDestroyed) return; let raw = ''; try { raw = editor.getHTML(); } catch { return; } onSave(normalizeSceneDescriptionHtml(raw)); }; if (!editor || editor.isDestroyed) { return createPortal( <>