Sets up the Socrata project repo with: docs/ — strategy and design documents - idea.md: full product vision - implementation-plan.md: Phase 0 + Phase 1 MVP plan - phase-0-validation.md: 2-week validation experiment strategy - phase-0-plan.md: concrete Phase 0 build plan - phase-0-results.md: Phase 0 gate outcome — GO for MVP - sysml-modeling.md: metamodel + SE discipline + validation rules - socrates.md: agent character, surfaces, modes, prompts, lifecycle - sync.md: bidirectional text↔diagram sync engineering - design-source/: HTML/CSS/JS handoff bundle from Claude Design phase-0/ — validated harness (CLI, no UI, no DB) - LM Studio (local OpenAI-compatible) generation + detection + judge - PlantUML rendering for SysML model visualization - 10-seed corpus (8 working + 2 holdouts) - 5 corpus runs with iteration history in reports/ - Final gate: 10/10 pass, mean 4.32/5, holdouts validated Phase 1 MVP scope and milestones documented in implementation-plan.md.
143 lines
6.2 KiB
JavaScript
143 lines
6.2 KiB
JavaScript
// DiagramCanvas — softened SysML diagram (rounded blocks w/ compartments,
|
|
// associations as gentle splines). Themed entirely via CSS variables read
|
|
// from the wrapping aesthetic. Pure visual, with hover + selection.
|
|
|
|
function DiagramCanvas({ data, density = "comfortable", variant = "softened", focusBlockId, onSelect }) {
|
|
const W = 720, H = 460;
|
|
const pad = density === "compact" ? 8 : 12;
|
|
const titleSize = density === "compact" ? 12 : 13;
|
|
const propSize = density === "compact" ? 10.5 : 11.5;
|
|
const stroke = variant === "formal" ? 1.4 : 1;
|
|
|
|
const blockMap = React.useMemo(
|
|
() => Object.fromEntries(data.blocks.map((b) => [b.id, b])),
|
|
[data]
|
|
);
|
|
|
|
function blockRect(b) {
|
|
return { x: b.x * W, y: b.y * H, w: b.w, h: b.h };
|
|
}
|
|
|
|
// Compute a clean orthogonal-ish path between blocks: exit center→nearest edge,
|
|
// then a soft S-curve.
|
|
function pathFor(a) {
|
|
const f = blockRect(blockMap[a.from]);
|
|
const t = blockRect(blockMap[a.to]);
|
|
const fc = { x: f.x + f.w / 2, y: f.y + f.h / 2 };
|
|
const tc = { x: t.x + t.w / 2, y: t.y + t.h / 2 };
|
|
// Exit/enter on horizontal axis if dx dominates, else vertical.
|
|
const dx = tc.x - fc.x, dy = tc.y - fc.y;
|
|
let p1, p2, mid;
|
|
if (Math.abs(dx) > Math.abs(dy)) {
|
|
p1 = { x: fc.x + Math.sign(dx) * f.w / 2, y: fc.y };
|
|
p2 = { x: tc.x - Math.sign(dx) * t.w / 2, y: tc.y };
|
|
const cx = (p1.x + p2.x) / 2;
|
|
mid = `C ${cx} ${p1.y}, ${cx} ${p2.y}, ${p2.x} ${p2.y}`;
|
|
} else {
|
|
p1 = { x: fc.x, y: fc.y + Math.sign(dy) * f.h / 2 };
|
|
p2 = { x: tc.x, y: tc.y - Math.sign(dy) * t.h / 2 };
|
|
const cy = (p1.y + p2.y) / 2;
|
|
mid = `C ${p1.x} ${cy}, ${p2.x} ${cy}, ${p2.x} ${p2.y}`;
|
|
}
|
|
return { d: `M ${p1.x} ${p1.y} ${mid}`, mid: { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 } };
|
|
}
|
|
|
|
const radius = variant === "formal" ? 3 : variant === "graph" ? 14 : 8;
|
|
|
|
return (
|
|
<div className="diagram-wrap" style={{ width: "100%", height: "100%", position: "relative", overflow: "hidden", background: "var(--diagram-bg)" }}>
|
|
<div className="diagram-grid" />
|
|
<svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid meet" width="100%" height="100%" style={{ display: "block" }}>
|
|
<defs>
|
|
<marker id="arrow-soft" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
<path d="M 0 1 L 9 5 L 0 9 z" fill="var(--edge)" />
|
|
</marker>
|
|
<marker id="diamond-soft" viewBox="0 0 12 10" refX="11" refY="5" markerWidth="10" markerHeight="8" orient="auto-start-reverse">
|
|
<path d="M 0 5 L 6 1 L 12 5 L 6 9 z" fill="var(--surface)" stroke="var(--edge)" strokeWidth="1" />
|
|
</marker>
|
|
</defs>
|
|
|
|
{/* Edges */}
|
|
{data.associations.map((a) => {
|
|
const p = pathFor(a);
|
|
const isComposition = a.kind === "composition";
|
|
const isConstraint = a.kind === "constraint";
|
|
return (
|
|
<g key={a.id}>
|
|
<path
|
|
d={p.d}
|
|
fill="none"
|
|
stroke="var(--edge)"
|
|
strokeWidth={stroke}
|
|
strokeDasharray={isConstraint ? "4 4" : null}
|
|
markerEnd={isComposition ? "url(#diamond-soft)" : "url(#arrow-soft)"}
|
|
opacity={0.85}
|
|
/>
|
|
<g transform={`translate(${p.mid.x}, ${p.mid.y})`}>
|
|
<rect x={-a.label.length * 3.4 - 5} y={-8} width={a.label.length * 6.8 + 10} height={16}
|
|
rx={4} fill="var(--diagram-bg)" stroke="var(--edge-soft)" strokeWidth="0.75" />
|
|
<text x={0} y={3} textAnchor="middle" fontSize="10.5" fontFamily="var(--font-mono)" fill="var(--edge-label)">
|
|
{a.label}
|
|
</text>
|
|
</g>
|
|
</g>
|
|
);
|
|
})}
|
|
|
|
{/* Blocks */}
|
|
{data.blocks.map((b) => {
|
|
const r = blockRect(b);
|
|
const focus = focusBlockId === b.id;
|
|
const isConstraint = b.kind === "constraint";
|
|
const isActor = b.kind === "actor";
|
|
return (
|
|
<g key={b.id} style={{ cursor: "pointer" }} onClick={() => onSelect && onSelect(b.id)}>
|
|
{/* shadow */}
|
|
{variant !== "formal" && (
|
|
<rect x={r.x + 1.5} y={r.y + 3} width={r.w} height={r.h} rx={radius}
|
|
fill="var(--shadow)" opacity="0.5" />
|
|
)}
|
|
<rect
|
|
x={r.x}
|
|
y={r.y}
|
|
width={r.w}
|
|
height={r.h}
|
|
rx={radius}
|
|
fill={isConstraint ? "var(--block-constraint-bg)" : isActor ? "var(--block-actor-bg)" : "var(--block-bg)"}
|
|
stroke={focus ? "var(--accent)" : "var(--block-border)"}
|
|
strokeWidth={focus ? 1.75 : stroke}
|
|
strokeDasharray={isConstraint ? "5 3" : null}
|
|
/>
|
|
{/* Header bar */}
|
|
{!isConstraint && (
|
|
<line x1={r.x} y1={r.y + 32} x2={r.x + r.w} y2={r.y + 32} stroke="var(--block-divider)" strokeWidth="0.75" />
|
|
)}
|
|
{/* Stereotype */}
|
|
<text x={r.x + pad} y={r.y + 15} fontSize="9" fontFamily="var(--font-mono)" fill="var(--muted)" letterSpacing="0.04em">
|
|
«{isConstraint ? "constraint" : isActor ? "actor" : "block"}»
|
|
</text>
|
|
{/* Title */}
|
|
<text x={r.x + pad} y={r.y + (isConstraint ? 40 : 28)} fontSize={titleSize} fontFamily="var(--font-display)" fontWeight="600" fill="var(--fg)">
|
|
{b.label}
|
|
</text>
|
|
{/* Properties */}
|
|
{!isConstraint && b.properties.slice(0, 4).map((p, i) => (
|
|
<text key={i} x={r.x + pad} y={r.y + 50 + i * (propSize + 5)} fontSize={propSize} fontFamily="var(--font-mono)" fill="var(--muted-strong)">
|
|
· {p}
|
|
</text>
|
|
))}
|
|
{isConstraint && (
|
|
<text x={r.x + pad} y={r.y + 58} fontSize={propSize} fontFamily="var(--font-mono)" fill="var(--muted-strong)">
|
|
{"{ tenancy = institutional }"}
|
|
</text>
|
|
)}
|
|
</g>
|
|
);
|
|
})}
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
window.DiagramCanvas = DiagramCanvas;
|