rem-notepad / src /components /Editor /EditorCore.tsx
algorembrant's picture
Upload 31 files
4af09f9 verified
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';
// Premium Theme mapping for 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');
// Ensure editor re-initializes exactly on file change
useEffect(() => {
setEditorKey(k => k + 1);
}, [activeFileId]);
// Clipboard Transformation (Emdash Prefix)
useEffect(() => {
const handleCopy = (e: ClipboardEvent) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return;
let text = selection.toString();
if (!text) return;
// SPEC: Emdash as very beginning of number line with content
// Logic: Prefix lines with content with "— " (emdash + space)
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); // 300ms debounce for near-instant premium persistence
}, [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>
);
}