| import { useEffect, useState, useCallback, useRef } from 'react'; |
| import { useFileStore } from '../../stores/fileStore'; |
| import { LexicalComposer } from '@lexical/react/LexicalComposer'; |
| import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; |
| import { ContentEditable } from '@lexical/react/LexicalContentEditable'; |
| import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; |
| import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; |
| import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'; |
|
|
| import LineNumbers from './LineNumberPlugin'; |
| import HoverToolbar from './HoverToolbar'; |
| import AutoHighlighterPlugin from '../../plugins/AutoHighlighterPlugin'; |
| import { CloudSavingDone01Icon, Tick02Icon } from 'hugeicons-react'; |
| import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; |
| import { INSERT_LINE_BREAK_COMMAND, COMMAND_PRIORITY_EDITOR, $createLineBreakNode, $getSelection, $isRangeSelection } from 'lexical'; |
|
|
| |
| const theme = { |
| paragraph: 'editor-paragraph', |
| text: { |
| bold: 'editor-text-bold', |
| italic: 'editor-text-italic', |
| strikethrough: 'editor-text-strikethrough', |
| underline: 'editor-text-underline', |
| }, |
| }; |
|
|
| export default function EditorCore() { |
| const { files, activeFileId, updateFileContent } = useFileStore(); |
| const [editorKey, setEditorKey] = useState(0); |
| const [isSaving, setIsSaving] = useState(false); |
| const debounceRef = useRef<any>(null); |
|
|
| const ShiftEnterPlugin = () => { |
| const [editor] = useLexicalComposerContext(); |
| useEffect(() => { |
| return editor.registerCommand( |
| INSERT_LINE_BREAK_COMMAND, |
| () => { |
| editor.update(() => { |
| const selection = $getSelection(); |
| if ($isRangeSelection(selection)) { |
| selection.insertNodes([$createLineBreakNode()]); |
| } |
| }); |
| return true; |
| }, |
| COMMAND_PRIORITY_EDITOR |
| ); |
| }, [editor]); |
| return null; |
| }; |
|
|
| const activeFile = files.find(f => f.id === activeFileId && f.type === 'file'); |
|
|
| |
| useEffect(() => { |
| setEditorKey(k => k + 1); |
| }, [activeFileId]); |
|
|
| |
| useEffect(() => { |
| const handleCopy = (e: ClipboardEvent) => { |
| const selection = window.getSelection(); |
| if (!selection || selection.rangeCount === 0) return; |
| |
| let text = selection.toString(); |
| if (!text) return; |
|
|
| |
| |
| const lines = text.split('\n'); |
| const transformed = lines.map(line => { |
| if (line.trim().length > 0) { |
| return `— ${line}`; |
| } |
| return line; |
| }).join('\n'); |
|
|
| e.clipboardData?.setData('text/plain', transformed); |
| e.preventDefault(); |
| }; |
|
|
| document.addEventListener('copy', handleCopy); |
| return () => document.removeEventListener('copy', handleCopy); |
| }, []); |
|
|
| const handleOnChange = useCallback((editorState: any) => { |
| setIsSaving(true); |
| if (debounceRef.current) clearTimeout(debounceRef.current); |
| |
| debounceRef.current = setTimeout(() => { |
| editorState.read(() => { |
| const jsonString = JSON.stringify(editorState.toJSON()); |
| if (activeFileId) { |
| updateFileContent(activeFileId, jsonString).then(() => { |
| setIsSaving(false); |
| }); |
| } else { |
| setIsSaving(false); |
| } |
| }); |
| }, 300); |
| }, [activeFileId, updateFileContent]); |
|
|
| if (!activeFile) { |
| return ( |
| <div className="editor-placeholder" style={{ |
| display: 'flex', flexDirection: 'column', gap: '8px', opacity: 0.4, |
| fontFamily: 'var(--font-ibm-plex)' |
| }}> |
| <div style={{ fontSize: '14px', fontWeight: 500 }}>No active document</div> |
| <div style={{ fontSize: '11px' }}>Select a file from the sidebar to begin editing.</div> |
| </div> |
| ); |
| } |
|
|
| const initialConfig = { |
| namespace: 'HybridDocEditor', |
| theme, |
| nodes: [], |
| onError(error: Error) { |
| console.error('Lexical Error:', error); |
| }, |
| editorState: activeFile.content ? ( |
| activeFile.content.includes('"root":{') ? activeFile.content : undefined |
| ) : undefined, |
| }; |
|
|
| return ( |
| <div key={`editor-${editorKey}`} className="editor-shell" style={{ display: 'flex', flexDirection: 'column', height: '100%', flex: 1 }}> |
| <LexicalComposer initialConfig={initialConfig}> |
| <div className="editor-scroller" style={{ flex: 1, overflowY: 'auto', display: 'flex', position: 'relative' }}> |
| <LineNumbers /> |
| <div className="editor-input-container" style={{ flex: 1, padding: '24px 32px', position: 'relative' }}> |
| <RichTextPlugin |
| contentEditable={ |
| <ContentEditable |
| className="ContentEditable" |
| spellCheck={true} |
| data-gramm="true" |
| data-gramm_editor="true" |
| data-enable-grammarly="true" |
| style={{ |
| minHeight: '100%', |
| outline: 'none', |
| lineHeight: '1.5', |
| fontSize: '15px' |
| }} |
| /> |
| } |
| placeholder={ |
| <div className="editor-placeholder" style={{ |
| position: 'absolute', top: 24, left: 32, pointerEvents: 'none', opacity: 0.3, fontSize: '15px' |
| }}> |
| Start your thinking here... |
| </div> |
| } |
| ErrorBoundary={LexicalErrorBoundary} |
| /> |
| <HistoryPlugin /> |
| <OnChangePlugin onChange={handleOnChange} ignoreSelectionChange /> |
| <ShiftEnterPlugin /> |
| <HoverToolbar /> |
| <AutoHighlighterPlugin /> |
| |
| {/* Save Status Indicator */} |
| <div style={{ |
| position: 'fixed', |
| bottom: '24px', |
| right: '24px', |
| display: 'flex', |
| alignItems: 'center', |
| gap: '8px', |
| padding: '6px 12px', |
| background: 'var(--bg-panel)', |
| border: '1px solid var(--border-color)', |
| borderRadius: '20px', |
| fontSize: '11px', |
| fontWeight: 600, |
| color: 'var(--text-secondary)', |
| opacity: isSaving ? 1 : 0.4, |
| transition: 'opacity 0.3s ease', |
| pointerEvents: 'none', |
| boxShadow: '0 4px 12px rgba(0,0,0,0.05)', |
| zIndex: 10 |
| }}> |
| {isSaving ? <CloudSavingDone01Icon size={14} className="spin" /> : <Tick02Icon size={14} />} |
| {isSaving ? 'Syncing...' : 'Saved'} |
| </div> |
| </div> |
| </div> |
| </LexicalComposer> |
| </div> |
| ); |
| } |
|
|