// Outline / Model / Requirements sections, each independently collapsible. // The whole rail can also collapse to a 36px vertical strip. // // M5: Model + Requirements sections read from the canonical SysMLModel via // useModelStore() so renames in either canvas reflect here immediately. // Outline section is still narrative-derived and uses the fixture (M6 will // migrate it to the live ProseMirror outline). "use client"; import { useState } from "react"; import type { FixtureData } from "../../lib/fixtures/aristotle"; import type { ValidationIssue, Severity } from "../../lib/sysml/validate"; import { useModel } from "../../lib/sync/ModelStore"; interface LeftRailProps { data: FixtureData; focusBlockId: string | null; setFocusBlockId: (id: string | null) => void; /** Map keyed by element key (block id, `req:`, `assoc:`, …) → issues. */ issuesByElement?: Map; } function maxSeverityForKey(map: Map | undefined, key: string): Severity | null { const items = map?.get(key); if (!items || items.length === 0) return null; if (items.some(i => i.severity === "error")) return "error"; if (items.some(i => i.severity === "warning")) return "warning"; return "soft"; } function IssueDot({ severity, title }: { severity: Severity | null; title?: string }) { if (!severity) return null; return ; } export function LeftRail({ focusBlockId, setFocusBlockId, issuesByElement }: LeftRailProps) { const [collapsed, setCollapsed] = useState(false); const [open, setOpen] = useState({ outline: true, model: true, requirements: true }); const toggle = (k: keyof typeof open) => setOpen(s => ({ ...s, [k]: !s[k] })); const model = useModel(); if (collapsed) { return ( ); } // Combine blocks + constraints in the Model section, sorted by kind for a // predictable order: system → block → actor → constraint. const kindRank: Record = { system: 0, block: 1, actor: 2, constraint: 3 }; const modelEntries: Array<{ id: string; label: string; kind: "block" | "actor" | "constraint" | "system"; propertyCount: number; }> = [ ...model.blocks.map(b => ({ id: b.id, label: b.label, kind: b.kind, propertyCount: b.properties.length, })), ...model.constraints.map(c => ({ id: c.id, label: c.label, kind: "constraint" as const, propertyCount: 0, })), ].sort((a, b) => { const r = (kindRank[a.kind] ?? 99) - (kindRank[b.kind] ?? 99); return r !== 0 ? r : a.label.localeCompare(b.label); }); return ( ); }