diff options
| author | soryu <soryu@soryu.co> | 2026-04-28 19:12:52 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-28 19:12:52 +0100 |
| commit | d1fdfb140cc440664f77a24886172f9976a05a31 (patch) | |
| tree | 454739f80dde60fc6c1cd97acbaef3223ac041c6 /frontend/src/components/document/DocumentEditor.tsx | |
| parent | 636694182fe9381479f2e9062229dda3838c5421 (diff) | |
| download | soryu-d1fdfb140cc440664f77a24886172f9976a05a31.tar.gz soryu-d1fdfb140cc440664f77a24886172f9976a05a31.zip | |
feat: revert broken directive PRs, re-implement Lexical document orchestrator (#98)
* feat: soryu-co/soryu - makima: Revert broken directive PRs and verify clean build
* feat: soryu-co/soryu - makima: Re-implement frontend: Lexical document editor with feature flag and base components
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Add contract blocks, expandable log rows, and interaction controls
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: End-to-end build verification and integration polish
Diffstat (limited to 'frontend/src/components/document/DocumentEditor.tsx')
| -rw-r--r-- | frontend/src/components/document/DocumentEditor.tsx | 236 |
1 files changed, 0 insertions, 236 deletions
diff --git a/frontend/src/components/document/DocumentEditor.tsx b/frontend/src/components/document/DocumentEditor.tsx deleted file mode 100644 index 2ef37fe..0000000 --- a/frontend/src/components/document/DocumentEditor.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { useCallback, useRef, useState } from 'react'; -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 { HeadingNode } from '@lexical/rich-text'; -import { ListNode, ListItemNode } from '@lexical/list'; -import { LinkNode } from '@lexical/link'; -import { - $getRoot, - $createParagraphNode, - $createTextNode, - type EditorState, - type LexicalEditor, -} from 'lexical'; -import { $createHeadingNode } from '@lexical/rich-text'; - -import { StepsDiagramNode, $isStepsDiagramNode, $createStepsDiagramNode } from './nodes/StepsDiagramNode'; -import { ContractBlockNode, $isContractBlockNode } from './nodes/ContractBlockNode'; -import editorTheme from './EditorTheme'; -import AutoSavePlugin from './AutoSavePlugin'; -import ContextMenu, { type ContextMenuAction } from './ContextMenu'; -import './DocumentEditor.css'; -import './nodes/StepsDiagram.css'; -import './nodes/ContractBlock.css'; - -interface DocumentEditorProps { - directiveId: string; - title: string; - goal: string; - status: string; - prBranch?: string | null; - onGoalChange?: (newGoal: string) => void; - onTitleChange?: (newTitle: string) => void; - onCleanup?: () => void; - onCreatePr?: () => void; - onPlanOrders?: () => void; - onTogglePause?: () => void; - readOnly?: boolean; -} - -function buildInitialEditorState(directiveId: string, title: string, goal: string) { - return () => { - const root = $getRoot(); - - // Title as H1 - const heading = $createHeadingNode('h1'); - heading.append($createTextNode(title)); - root.append(heading); - - // Goal as paragraph(s), split by newlines - const lines = goal.split('\n'); - for (const line of lines) { - const paragraph = $createParagraphNode(); - if (line.trim()) { - paragraph.append($createTextNode(line)); - } - root.append(paragraph); - } - - // Insert steps diagram node after the goal content - const stepsNode = $createStepsDiagramNode(directiveId); - root.append(stepsNode); - - // Add a trailing paragraph so the user can type below the diagram - const trailingParagraph = $createParagraphNode(); - root.append(trailingParagraph); - }; -} - -function onError(error: Error) { - console.error('[DocumentEditor] Lexical error:', error); -} - -export default function DocumentEditor({ - directiveId, - title, - goal, - status, - prBranch, - onGoalChange, - onTitleChange, - onCleanup, - onCreatePr, - onPlanOrders, - onTogglePause, - readOnly = false, -}: DocumentEditorProps) { - const editorRef = useRef<LexicalEditor | null>(null); - const latestGoalRef = useRef(goal); - const latestTitleRef = useRef(title); - - // Context menu state - const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number } | null>(null); - - const initialConfig = { - namespace: `DocumentEditor-${directiveId}`, - theme: editorTheme, - editorState: buildInitialEditorState(directiveId, title, goal), - nodes: [HeadingNode, ListNode, ListItemNode, LinkNode, StepsDiagramNode, ContractBlockNode], - onError, - editable: !readOnly, - }; - - const handleChange = useCallback( - (_editorState: EditorState, editor: LexicalEditor) => { - editorRef.current = editor; - - editor.getEditorState().read(() => { - const root = $getRoot(); - const children = root.getChildren(); - - let newTitle = ''; - const goalLines: string[] = []; - - for (let i = 0; i < children.length; i++) { - const child = children[i]; - // Skip decorator nodes (steps diagram, contract blocks) when extracting text - if ($isStepsDiagramNode(child)) continue; - if ($isContractBlockNode(child)) continue; - - const text = child.getTextContent(); - - if (i === 0 && child.getType() === 'heading') { - newTitle = text; - } else { - goalLines.push(text); - } - } - - const newGoal = goalLines.join('\n'); - - if (newTitle !== latestTitleRef.current) { - latestTitleRef.current = newTitle; - onTitleChange?.(newTitle); - } - - if (newGoal !== latestGoalRef.current) { - latestGoalRef.current = newGoal; - onGoalChange?.(newGoal); - } - }); - }, - [onGoalChange, onTitleChange] - ); - - const getContent = useCallback(() => { - return latestGoalRef.current; - }, []); - - const handleAutoSave = useCallback( - (content: string) => { - onGoalChange?.(content); - }, - [onGoalChange] - ); - - // Context menu handler - const handleContextMenu = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - setCtxMenu({ x: e.clientX, y: e.clientY }); - }, - [] - ); - - const closeCtxMenu = useCallback(() => setCtxMenu(null), []); - - const isPaused = status === 'paused'; - const isIdle = status === 'idle'; - - const ctxActions: ContextMenuAction[] = [ - { - label: 'Clean Up', - icon: '\uD83E\uDDF9', // broom - disabled: !isIdle, - onClick: () => onCleanup?.(), - }, - { - label: 'Update PR', - icon: '\uD83D\uDD00', // shuffle - disabled: !prBranch, - onClick: () => onCreatePr?.(), - }, - { - label: 'Plan Orders', - icon: '\uD83D\uDCCB', // clipboard - onClick: () => onPlanOrders?.(), - }, - { - label: isPaused ? 'Resume Directive' : 'Pause Directive', - icon: isPaused ? '\u25B6' : '\u23F8', - onClick: () => onTogglePause?.(), - }, - ]; - - return ( - <div className="document-editor-container" onContextMenu={handleContextMenu}> - <LexicalComposer initialConfig={initialConfig}> - <div className="doc-editor-input"> - <RichTextPlugin - contentEditable={ - <ContentEditable className="doc-editor-content-editable doc-editor-root" /> - } - placeholder={ - <div className="doc-editor-placeholder">Start writing...</div> - } - ErrorBoundary={LexicalErrorBoundary} - /> - </div> - <HistoryPlugin /> - <OnChangePlugin onChange={handleChange} /> - {!readOnly && ( - <AutoSavePlugin - onAutoSave={handleAutoSave} - getContent={getContent} - enabled={!readOnly} - /> - )} - </LexicalComposer> - - {ctxMenu && ( - <ContextMenu - x={ctxMenu.x} - y={ctxMenu.y} - actions={ctxActions} - dividerAfter={[2]} - onClose={closeCtxMenu} - /> - )} - </div> - ); -} - |
