MVP M1–M3: visual port, TipTap text editor, React Flow diagram
apps/web — Next.js 16 + TypeScript + React 19 (no Tailwind) M1 Visual port - Manuscript theme + base styles ported verbatim from design-source - 100dvh layout with internal scroll regions (rail / narrative / dock thread) - TopBar, LeftRail (collapsible sections + collapsed strip), CanvasHeader, StatusBar, EditorShell composing the dual-canvas - SocratesDock (subtle/default/prominent) with bubbles + numbered options - Sigil (Σ + laurel) used across editor and seed screens - SeedScreen with emerging-seed rail, mini-graph, confidence bar, thread, typing indicator, input row - Routes: / (homepage), /editor/[projectId], /seed - Aristotle fixture in lib/fixtures (ported from data.js) M2 Real text editor - TipTap (StarterKit + custom Chip atom inline node + ReactNodeViewRenderer) - Slash-menu insertion via @tiptap/suggestion with arrow / number-key shortcuts - ChipFocusContext bridges narrative ↔ diagram hover/selection across the TipTap render boundary - fixtureToDoc converts the fixture narrative → ProseMirror JSON M3 Real diagram - React Flow (@xyflow/react) custom node (block / actor / constraint / system) matching prototype's softened look - Custom edge with bezier path + label renderer (association / composition / constraint variants) - Drag-create from a left-side palette via HTML5 DnD + screenToFlowPosition - NodeInspector panel: edit kind / label / properties (or expression for constraints); delete cascades to incident edges - Bidirectional focus highlighting between chips (narrative) and blocks (diagram) - Removed redundant legend (stereotype labels on nodes serve same purpose) State for M3 lives in component memory; persistence + bidirectional sync land in M4–M5.
This commit is contained in:
94
apps/web/components/text-canvas/TextCanvas.tsx
Normal file
94
apps/web/components/text-canvas/TextCanvas.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
// TipTap-backed narrative editor.
|
||||
// Renders the same prose surface as the static port, but typing actually works
|
||||
// and chips can be inserted via the slash menu (`/block`, `/property`, etc.).
|
||||
|
||||
"use client";
|
||||
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { ChipNode } from "./ChipNode";
|
||||
import { SlashExtension } from "./SlashExtension";
|
||||
import { ChipFocusContext } from "./FocusContext";
|
||||
import { fixtureToDoc } from "./fixtureToDoc";
|
||||
import type { Density } from "../socrates/SocratesDock";
|
||||
import type { FixtureData } from "../../lib/fixtures/aristotle";
|
||||
import type { MarkupStyle } from "./Chip";
|
||||
|
||||
interface TextCanvasProps {
|
||||
data: FixtureData;
|
||||
density: Density;
|
||||
markupStyle: MarkupStyle;
|
||||
focusBlockId: string | null;
|
||||
setFocusBlockId: (id: string | null) => void;
|
||||
}
|
||||
|
||||
export function TextCanvas({ data, density, markupStyle, focusBlockId, setFocusBlockId }: TextCanvasProps) {
|
||||
const padY = density === "compact" ? 10 : 18;
|
||||
const padX = density === "compact" ? 22 : 36;
|
||||
const focusValue = useMemo(
|
||||
() => ({ focusBlockId, setFocusBlockId }),
|
||||
[focusBlockId, setFocusBlockId]
|
||||
);
|
||||
|
||||
const editor = useEditor({
|
||||
immediatelyRender: false,
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: { levels: [1, 2] },
|
||||
// Drop features we don't need yet
|
||||
codeBlock: false,
|
||||
blockquote: false,
|
||||
horizontalRule: false,
|
||||
bulletList: false,
|
||||
orderedList: false,
|
||||
listItem: false,
|
||||
strike: false,
|
||||
code: false,
|
||||
link: false,
|
||||
}),
|
||||
ChipNode,
|
||||
SlashExtension,
|
||||
],
|
||||
content: fixtureToDoc(data),
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "text-canvas tiptap",
|
||||
style: `padding: ${padY}px ${padX}px;`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Push the markup style into the editor so the ChipView NodeView can read it.
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
(editor.storage as Record<string, unknown>).markupStyle = markupStyle;
|
||||
// Force a re-render of all chip node views so they pick up the new style.
|
||||
editor.view.dispatch(editor.state.tr.setMeta("force-update", true));
|
||||
}, [editor, markupStyle]);
|
||||
|
||||
if (!editor) {
|
||||
return (
|
||||
<div className="text-canvas" style={{ padding: `${padY}px ${padX}px` }}>
|
||||
<div style={{ color: "var(--muted)", fontFamily: "var(--font-mono)", fontSize: 12 }}>
|
||||
loading editor…
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChipFocusContext.Provider value={focusValue}>
|
||||
<EditorContent editor={editor} />
|
||||
<div className="margin-note" style={{ margin: `0 ${padX}px ${padY}px ${padX}px`, maxWidth: 720 }}>
|
||||
<span className="margin-note-glyph">Σ</span>
|
||||
<span>
|
||||
<span className="margin-note-who">Socrates · margin</span>
|
||||
<span className="margin-note-text">
|
||||
“Refuses to produce solutions” is a strong constraint. Have you decided what counts as a “solution” vs. a “scaffold”? This boundary will determine whether the refusal policy is enforceable.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</ChipFocusContext.Provider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user