// The dual-canvas workspace shell. // Composes TopBar / SocratesDock / LeftRail / TextCanvas / DiagramCanvas / StatusBar. // M4 adds: SysML model derivation + validation, IssuesPanel, optional ?break=... // query param to demonstrate the validator surfacing rule violations. "use client"; import { useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; import { TopBar } from "./TopBar"; import { LeftRail } from "./LeftRail"; import { CanvasHeader } from "./CanvasHeader"; import { StatusBar } from "./StatusBar"; import { IssuesPanel } from "./IssuesPanel"; import { TextCanvas } from "../text-canvas/TextCanvas"; import { DiagramCanvas, type DiagramVariant } from "../diagram-canvas/DiagramCanvas"; import { SocratesDock, type Density, type SocratesPresence } from "../socrates/SocratesDock"; import type { MarkupStyle } from "../text-canvas/Chip"; import type { FixtureData } from "../../lib/fixtures/aristotle"; import { fromFixture } from "../../lib/sysml/fromFixture"; import { validate, type ValidationIssue } from "../../lib/sysml/validate"; import { applyBreaks, BREAKS, type BreakName } from "../../lib/sysml/breaks"; interface EditorShellProps { data: FixtureData; density?: Density; markupStyle?: MarkupStyle; diagramStyle?: DiagramVariant; presence?: SocratesPresence; } export function EditorShell({ data, density = "comfortable", markupStyle = "color", diagramStyle = "softened", presence = "default", }: EditorShellProps) { const [focusBlockId, setFocusBlockId] = useState(null); const searchParams = useSearchParams(); // Parse `?break=S1-dangling,M2-cycle&...` into a list of named corruptions. const breaks = useMemo(() => { const raw = searchParams?.get("break") ?? ""; if (!raw) return []; return raw .split(",") .map(s => s.trim()) .filter((s): s is BreakName => s in BREAKS); }, [searchParams]); // Derive the SysML model from the fixture, optionally apply breaks, validate. const { issues, issuesByElement } = useMemo(() => { const baseModel = fromFixture(data); const broken = breaks.length > 0 ? applyBreaks(baseModel, breaks) : baseModel; const issues = validate(broken); const issuesByElement = new Map(); for (const i of issues) { const key = anchorKey(i.anchor); if (!key) continue; const prev = issuesByElement.get(key) ?? []; prev.push(i); issuesByElement.set(key, prev); } return { issues, issuesByElement }; }, [data, breaks]); return (
0 ? `SysML · breaks active: ${breaks.join(", ")}` : "SysML · 6 blocks · 6 associations · 1 constraint"} right={
Fit 100% Layout
} />
setFocusBlockId(id)} />
); } /** Map an issue's anchor to a single string key matching either a block id or * a synthetic key (`assoc:a1`, `req:req-001`, `constraint:ferpa`). The UI uses * block ids most often, so block anchors return the bare id. */ function anchorKey(anchor: ValidationIssue["anchor"]): string | null { switch (anchor.kind) { case "block": return anchor.id; case "association": return `assoc:${anchor.id}`; case "constraint": return `constraint:${anchor.id}`; case "requirement": return `req:${anchor.id}`; case "property": return anchor.blockId; case "model": return null; } }