apps/web/lib/sysml — pure engine (no UI deps) - model.ts: canonical types ported from phase-0 (Block, Property, Association, Constraint, Requirement, SysMLModel) - validate.ts: 12 rules grouped Structural (S1–S5) / Semantic (M1–M5) / Traceability (T1–T2). Pure function returning element-anchored issues. Cycle detection via DFS with canonical-key dedup. T3 deferred to Phase 1.5 (depends on narrative chip references). - depgraph.ts: directed graph over Blocks / Properties / Constraints / Requirements with `dependentsOf()` and `neighborhood()` for impact analysis (substrate for M7). - fromFixture.ts: loose FixtureData → strict SysMLModel converter (interim until M5 unifies state). - breaks.ts: 8 named model corruptions (S1/S2/S3/M2/M3/M5/T1/T2) for ?break=... demonstrations. UI integration in EditorShell + LeftRail + DiagramCanvas + IssuesPanel - Floating IssuesPanel (bottom-right) with severity-grouped counts and collapse. Click an issue to focus its anchor across the rail and diagram. - LeftRail: severity dots on each block in the Model section and on each requirement entry. Tooltips show full issue text. - DiagramCanvas: red/blue/dotted ring around offending blocks via issuesByElement → BlockNode.data.issueSeverity. - ?break=S1-dangling,M2-cycle,M5-dup-property,... query param injects corruptions for verification of the "done when" criterion. - Suspense boundary added at the editor route for useSearchParams. Two pre-existing TipTap typing fixes (editor.storage cast through unknown).
95 lines
3.2 KiB
TypeScript
95 lines
3.2 KiB
TypeScript
// 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 unknown 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>
|
|
);
|
|
}
|