Files
Socrates/apps/web/components/editor/LeftRail.tsx
dtoro a0566ce64c MVP M1–M3: visual port, TipTap text editor, React Flow diagram
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.
2026-04-28 22:54:36 +02:00

141 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Outline / Model / Requirements sections, each independently collapsible.
// The whole rail can also collapse to a 36px vertical strip.
// Ported from docs/design-source/socrata/project/editor-shell.jsx (LeftRail).
"use client";
import { useState } from "react";
import type { FixtureData } from "../../lib/fixtures/aristotle";
interface LeftRailProps {
data: FixtureData;
focusBlockId: string | null;
setFocusBlockId: (id: string | null) => void;
}
export function LeftRail({ data, focusBlockId, setFocusBlockId }: 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] }));
if (collapsed) {
return (
<nav className="leftrail leftrail-collapsed">
<button
className="rail-collapse-btn"
onClick={() => setCollapsed(false)}
title="Expand rail"
type="button"
>
</button>
<div className="rail-collapsed-stack">
<span className="rail-collapsed-tag" title="Outline">OUT</span>
<span className="rail-collapsed-tag" title="Model · 6 blocks">MOD</span>
<span className="rail-collapsed-tag" title="Requirements">REQ</span>
</div>
</nav>
);
}
return (
<nav className="leftrail">
<div className="rail-section">
<button className="rail-section-head" onClick={() => toggle("outline")} type="button">
<span className={`rail-caret ${open.outline ? "rail-caret-open" : ""}`}></span>
<span className="rail-label">Outline</span>
</button>
{open.outline && (
<ul className="rail-list">
<li className="rail-item rail-item-active">
<span className="rail-item-text">Problem framing</span>
</li>
<li className="rail-item">
<span className="rail-item-text">Constraints</span>
<span className="rail-count rail-count-req">3</span>
</li>
<li className="rail-item">
<span className="rail-item-text">Why now</span>
</li>
<li className="rail-item rail-item-muted">
<span className="rail-item-text">Hypotheses</span>
<span className="rail-count rail-count-asm">3</span>
</li>
<li className="rail-item rail-item-muted">
<span className="rail-item-text">Open questions</span>
<span className="rail-count rail-count-q">5</span>
</li>
<li className="rail-item rail-item-muted">
<span className="rail-item-text">Risks</span>
<span className="rail-count rail-count-risk">2</span>
</li>
</ul>
)}
</div>
<div className="rail-section">
<button className="rail-section-head" onClick={() => toggle("model")} type="button">
<span className={`rail-caret ${open.model ? "rail-caret-open" : ""}`}></span>
<span className="rail-label">Model</span>
</button>
{open.model && (
<ul className="rail-list rail-blocks">
{data.blocks.map(b => (
<li
key={b.id}
className={`rail-block rail-${b.kind} ${focusBlockId === b.id ? "rail-block-active" : ""}`}
onMouseEnter={() => setFocusBlockId(b.id)}
onMouseLeave={() => setFocusBlockId(null)}
>
<span className="rail-block-glyph">
{b.kind === "constraint" ? "{}" : b.kind === "actor" ? "◐" : "▢"}
</span>
<span className="rail-block-label">{b.label}</span>
<span
className="rail-block-count"
title={`${b.properties.length} ${b.properties.length === 1 ? "property" : "properties"}`}
>
<span className="rail-block-count-glyph">·</span>
{b.properties.length}
</span>
</li>
))}
</ul>
)}
</div>
<div className="rail-section">
<button className="rail-section-head" onClick={() => toggle("requirements")} type="button">
<span className={`rail-caret ${open.requirements ? "rail-caret-open" : ""}`}></span>
<span className="rail-label">Requirements</span>
</button>
{open.requirements && (
<ul className="rail-list">
<li className="rail-req">
<span className="req-tag">REQ-001</span>
<span className="req-status req-traced" />
</li>
<li className="rail-req">
<span className="req-tag">REQ-002</span>
<span className="req-status req-traced" />
</li>
<li className="rail-req">
<span className="req-tag">REQ-003</span>
<span className="req-status req-untraced" />
</li>
</ul>
)}
</div>
<button
className="rail-collapse-btn rail-collapse-btn-bottom"
onClick={() => setCollapsed(true)}
title="Collapse rail"
type="button"
>
</button>
</nav>
);
}