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:
2026-04-28 22:54:36 +02:00
parent f1c4566576
commit a0566ce64c
38 changed files with 7988 additions and 0 deletions

View 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">
&ldquo;Refuses to produce solutions&rdquo; is a strong constraint. Have you decided what counts as a &ldquo;solution&rdquo; vs. a &ldquo;scaffold&rdquo;? This boundary will determine whether the refusal policy is enforceable.
</span>
</span>
</div>
</ChipFocusContext.Provider>
);
}