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.
48 lines
1.7 KiB
TypeScript
48 lines
1.7 KiB
TypeScript
// Convert FixtureData → React Flow nodes/edges.
|
||
// Positions in the fixture are normalized 0..1 against a 720×460 board; we map
|
||
// to absolute pixel coordinates so React Flow can render them.
|
||
|
||
import { MarkerType, type Edge, type Node } from "@xyflow/react";
|
||
import type { BlockNodeData } from "./nodes/BlockNode";
|
||
import type { SysmlEdgeData } from "./edges/SysmlEdge";
|
||
import type { FixtureData, AssociationKind } from "../../lib/fixtures/aristotle";
|
||
|
||
const BOARD_W = 720;
|
||
const BOARD_H = 460;
|
||
|
||
function markerForKind(kind: AssociationKind) {
|
||
if (kind === "composition") {
|
||
// React Flow doesn't ship a diamond marker; use the closed-arrow as the
|
||
// closest stock alternative. M3 ships this — a custom diamond can be
|
||
// added later if needed.
|
||
return { type: MarkerType.ArrowClosed, color: "var(--edge)", width: 18, height: 18 };
|
||
}
|
||
if (kind === "constraint") return undefined;
|
||
return { type: MarkerType.Arrow, color: "var(--edge)", width: 18, height: 18 };
|
||
}
|
||
|
||
export function fixtureToFlow(data: FixtureData): { nodes: Node<BlockNodeData>[]; edges: Edge<SysmlEdgeData>[] } {
|
||
const nodes: Node<BlockNodeData>[] = data.blocks.map(b => ({
|
||
id: b.id,
|
||
type: "sysmlBlock",
|
||
position: { x: b.x * BOARD_W, y: b.y * BOARD_H },
|
||
data: {
|
||
label: b.label,
|
||
kind: b.kind,
|
||
properties: [...b.properties],
|
||
...(b.kind === "constraint" ? { expression: "{ tenancy = institutional }" } : {}),
|
||
},
|
||
}));
|
||
|
||
const edges: Edge<SysmlEdgeData>[] = data.associations.map(a => ({
|
||
id: a.id,
|
||
source: a.from,
|
||
target: a.to,
|
||
type: "sysml",
|
||
data: { label: a.label, kind: a.kind },
|
||
markerEnd: markerForKind(a.kind),
|
||
}));
|
||
|
||
return { nodes, edges };
|
||
}
|