summaryrefslogtreecommitdiff
path: root/frontend/src/components/document/DocumentEditor.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-04-28 19:12:52 +0100
committerGitHub <noreply@github.com>2026-04-28 19:12:52 +0100
commitd1fdfb140cc440664f77a24886172f9976a05a31 (patch)
tree454739f80dde60fc6c1cd97acbaef3223ac041c6 /frontend/src/components/document/DocumentEditor.tsx
parent636694182fe9381479f2e9062229dda3838c5421 (diff)
downloadsoryu-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.tsx236
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>
- );
-}
-