feat: implement Story 2.8 — Flowchart diagram type renderer

Add the 6th and final core diagram type: flowcharts with standard
ISO 5807 shapes (process, decision, terminal, I/O, subprocess),
orthogonal edge routing with decision outcome labels, and ELK
layered auto-layout.

Code review fixes included: decision diamond ELK height corrected
(80→130px), icon rendering made conditional, data.color border
override added to all nodes, ELK sizing ternary refactored to
getNodeDimensions() helper, constants unified to lookup maps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-27 01:59:49 +00:00
parent 1ff8ff8f06
commit 0ff5450e0f
16 changed files with 1607 additions and 36 deletions

View File

@@ -0,0 +1,876 @@
# Story 2.8: Flowchart Diagram Type Renderer
Status: done
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## Story
As a user,
I want to create and view flowcharts with decision nodes, process steps, and terminals,
so that I can model logic flows and decision processes.
## Acceptance Criteria
1. **Given** I open or create a flowchart, **When** the canvas renders, **Then** I see standard flowchart shapes: process (rectangle), decision (diamond), start/end terminals (rounded rectangle/stadium), I/O (parallelogram), subprocess (double-bordered rectangle), **And** each node displays its label text.
2. **Given** a flowchart has decision nodes, **When** rendered, **Then** decision diamonds show the question/condition text, **And** outgoing edges are labeled with the decision outcomes (Yes/No, True/False, or custom).
3. **Given** auto-layout runs on a flowchart, **When** the diagram has branching paths, **Then** ELK.js produces a clean top-down flow with branches clearly separated, **And** merge points are visually clear.
## Tasks / Subtasks
- [x] Task 1: Create flowchart constants and type registry (AC: #1, #2)
- [x] 1.1: Create `apps/web/src/modules/diagram/types/flowchart/constants.ts` with `FLOW_SIZES`, `resolveFlowchartNodeType()`, `resolveFlowchartEdgeType()`, `getFlowNodeSize()`
- [x] 1.2: Define sizes for 5 node subtypes: process (160x60), decision (140x80), terminal (140x50), io (160x60), subprocess (160x60)
- [x] Task 2: Create FlowProcessNode custom @xyflow/react component (AC: #1)
- [x] 2.1: Create `apps/web/src/modules/diagram/types/flowchart/FlowProcessNode.tsx` — standard rectangle with label text, icon support, rose accent border
- [x] Task 3: Create FlowDecisionNode custom @xyflow/react component (AC: #1, #2)
- [x] 3.1: Create `apps/web/src/modules/diagram/types/flowchart/FlowDecisionNode.tsx` — diamond shape via CSS `transform: rotate(45deg)` on a wrapper with counter-rotated content, condition/question text centered
- [x] Task 4: Create FlowTerminalNode custom @xyflow/react component (AC: #1)
- [x] 4.1: Create `apps/web/src/modules/diagram/types/flowchart/FlowTerminalNode.tsx` — rounded rectangle/stadium shape (`border-radius: 999px` sides), label text, used for both start and end nodes. `data.tag` distinguishes "start" vs "end" for optional styling (e.g., start=green tint, end=red tint, or both use accent)
- [x] Task 5: Create FlowIoNode custom @xyflow/react component (AC: #1)
- [x] 5.1: Create `apps/web/src/modules/diagram/types/flowchart/FlowIoNode.tsx` — parallelogram shape via CSS `transform: skewX(-10deg)` on wrapper with counter-skewed content, label text
- [x] Task 6: Create FlowSubprocessNode custom @xyflow/react component (AC: #1)
- [x] 6.1: Create `apps/web/src/modules/diagram/types/flowchart/FlowSubprocessNode.tsx` — double-bordered rectangle (outer border + inner inset border), label text
- [x] Task 7: Create FlowEdge custom @xyflow/react component (AC: #2, #3)
- [x] 7.1: Create `apps/web/src/modules/diagram/types/flowchart/FlowEdge.tsx` — solid arrow using `getSmoothStepPath` (orthogonal routing), label from `edge.label` shown at midpoint (for decision outcomes Yes/No), rose accent color, markerEnd `url(#flow-arrow)`
- [x] Task 8: Update graph converter for flowchart type resolution (AC: #1, #2)
- [x] 8.1: Import `resolveFlowchartNodeType`, `resolveFlowchartEdgeType` from `../types/flowchart/constants` in `graph-converter.ts`
- [x] 8.2: Replace `// Future: flowchart` comment (line 50) with: `if (diagramType === "flowchart" || nodeType.startsWith("flow:")) { return resolveFlowchartNodeType(nodeType); }`
- [x] 8.3: Add flowchart diagramType handling in `resolveFlowEdgeType()` before the default return
- [x] Task 9: Register flowchart types in DiagramCanvas (AC: #1, #2)
- [x] 9.1: Import FlowProcessNode, FlowDecisionNode, FlowTerminalNode, FlowIoNode, FlowSubprocessNode, FlowEdge in `DiagramCanvas.tsx`
- [x] 9.2: Add flowchart entries to `nodeTypes` object: `flowProcess`, `flowDecision`, `flowTerminal`, `flowIo`, `flowSubprocess` (OUTSIDE component)
- [x] 9.3: Add flowchart entry to `edgeTypes` object: `flowEdge` (OUTSIDE component)
- [x] 9.4: Add flowchart arrow marker to `MarkerDefs`: `#flow-arrow` with rose accent color
- [x] Task 10: Integrate flowchart node sizing in ELK layout (AC: #3)
- [x] 10.1: Import `getFlowNodeSize` from `../types/flowchart/constants` in `elk-layout.ts`
- [x] 10.2: Add flowchart sizing to the width/height computation chain in `buildElkGraph()` — after `seqSize` check, add `flowSize` check
- [x] Task 11: Add flowchart CSS styles (AC: #1, #2)
- [x] 11.1: Add `.flow-process`, `.flow-decision`, `.flow-decision-diamond`, `.flow-terminal`, `.flow-io`, `.flow-io-skew`, `.flow-subprocess`, `.flow-subprocess-inner` styles to `globals.css`
- [x] 11.2: Use `--diagram-flowchart` rose accent color for all flowchart elements (already defined at line 29)
- [x] Task 12: Tests (AC: all)
- [x] 12.1: Unit tests for flowchart constants — `resolveFlowchartNodeType` for all 5 subtypes (prefixed and bare), `resolveFlowchartEdgeType`, `getFlowNodeSize`
- [x] 12.2: Unit tests for graph converter flowchart type mapping — node types correctly resolved for `diagramType === "flowchart"`, edge types resolved, flat layout (no containers)
- [x] 12.3: Update "all 6 diagram types" test (line 150 of graph-converter.test.ts): change assertion from `"default"` to `"flowProcess"` for `flow:process` node
- [x] 12.4: Unit tests for ELK layout flowchart node sizing — `buildElkGraph` produces correct dimensions for flowchart nodes
- [x] 12.5: All tests pass — no regressions
## Dev Notes
### Overview — What This Story Builds
This story adds the Flowchart diagram type renderer: the last of the 6 core diagram types. Flowcharts use standard ISO 5807-inspired shapes (process, decision, terminal, I/O, subprocess) with the standard ELK layered algorithm for top-down flow layout.
**This story builds:**
- 5 custom flowchart node components (FlowProcessNode, FlowDecisionNode, FlowTerminalNode, FlowIoNode, FlowSubprocessNode)
- 1 custom flowchart edge component (FlowEdge) with label support for decision outcomes
- Flowchart constants and type registry (5 node subtypes + 1 edge type)
- Graph converter flowchart type resolution
- ELK layout flowchart node sizing
- Flowchart CSS styles with rose `--diagram-flowchart` accent
- Flowchart arrow marker in MarkerDefs
**This story does NOT implement:**
- Other diagram types (all 6 complete after this)
- Smart Inspector for flowchart field editing (future epic)
- Manual node repositioning (Story 2.9)
- Liveblocks/CRDT integration (Epic 4)
- AI-triggered mutations (Epic 3)
- Nested diagrams (Epic 7)
### Architecture Compliance
**MANDATORY patterns from Architecture Decision Document:**
1. **Unified Graph Data Model (Decision 1):** Flowchart nodes use type-prefixed `type` field (`flow:process`, `flow:decision`, `flow:terminal`, `flow:io`, `flow:subprocess`). Existing DiagramNode fields are semantically reused: `label` = step description/question text, `tag` = category (e.g., "start"/"end" for terminals), `icon` = optional step emoji, `color` = custom accent override. Edge types use a single value: `sequence` (or default). NO new fields on DiagramNode or DiagramEdge needed.
2. **Component Structure:** Feature code in `~/modules/diagram/types/flowchart/` — follows the BPMN (`types/bpmn/`), E-R (`types/er/`), Org Chart (`types/orgchart/`), Architecture (`types/architecture/`), Sequence (`types/sequence/`) pattern.
3. **@xyflow/react Custom Nodes:** All custom node components use `NodeProps` typing. The `nodeTypes` and `edgeTypes` objects MUST be defined OUTSIDE the component (performance critical — established in Stories 2.1-2.7).
4. **Standard ELK Layout:** Flowchart uses the standard ELK layered algorithm — no custom layout needed (unlike sequence diagrams). `buildElkGraph()` needs flowchart node sizing via `getFlowNodeSize()`. Default direction: `DOWN` for top-down flow.
5. **Lean JSON Data Model:** No x/y positions stored for flowchart nodes. All positioning computed by ELK at render time.
6. **Type Prefixing Convention (Enforcement Rule #5):** `flow:` prefix on DiagramNode.type. Edge type is bare lowercase: `sequence` (or undefined for default).
### Flowchart Diagram Data Model — Field Mapping
The existing DiagramNode and DiagramEdge fields satisfy flowchart needs:
```typescript
// flow:process node
{
id: "step1",
type: "flow:process",
label: "Process Payment", // Step description
icon: "💳", // Optional step icon
color: "#e11d48", // Custom accent (optional)
}
// flow:decision node
{
id: "check1",
type: "flow:decision",
label: "Payment Valid?", // Question/condition text
icon: "❓", // Optional icon
}
// flow:terminal node (start)
{
id: "start",
type: "flow:terminal",
label: "Start",
tag: "start", // Distinguishes start vs end
}
// flow:terminal node (end)
{
id: "end",
type: "flow:terminal",
label: "End",
tag: "end", // Distinguishes start vs end
}
// flow:io node
{
id: "input1",
type: "flow:io",
label: "Read User Input",
}
// flow:subprocess node
{
id: "sub1",
type: "flow:subprocess",
label: "Validate Credentials",
}
```
**Edge model:**
```typescript
// Standard flow edge
{
id: "e1",
from: "start",
to: "step1",
// No label needed for sequential flow
}
// Decision outcome edge
{
id: "e2",
from: "check1",
to: "step2",
label: "Yes", // Decision outcome label
}
// Decision outcome edge (alternative path)
{
id: "e3",
from: "check1",
to: "error1",
label: "No", // Alternative outcome
}
```
### Flowchart Node Types — Size Constants
```typescript
export const FLOW_SIZES = {
process: { w: 160, h: 60 }, // Standard rectangle
decision: { w: 140, h: 80 }, // Diamond (wider for rotation)
terminal: { w: 140, h: 50 }, // Rounded rectangle / stadium
io: { w: 160, h: 60 }, // Parallelogram
subprocess: { w: 160, h: 60 }, // Double-bordered rectangle
} as const;
```
### Flowchart Node Type → @xyflow/react Type Mapping
| DiagramNode.type | @xyflow Node type | Component | Visual |
|---|---|---|---|
| `flow:process` | `flowProcess` | `FlowProcessNode` | Rectangle with label |
| `flow:decision` | `flowDecision` | `FlowDecisionNode` | Diamond with condition text |
| `flow:terminal` | `flowTerminal` | `FlowTerminalNode` | Rounded rectangle / stadium |
| `flow:io` | `flowIo` | `FlowIoNode` | Parallelogram |
| `flow:subprocess` | `flowSubprocess` | `FlowSubprocessNode` | Double-bordered rectangle |
**Type resolution:**
```typescript
export function resolveFlowchartNodeType(type: string): string {
const bare = type.startsWith("flow:") ? type.slice(5) : type;
switch (bare) {
case "process":
return "flowProcess";
case "decision":
return "flowDecision";
case "terminal":
return "flowTerminal";
case "io":
return "flowIo";
case "subprocess":
return "flowSubprocess";
default:
return "flowProcess"; // Default to process
}
}
export function resolveFlowchartEdgeType(_type: string | undefined): string {
return "flowEdge"; // Single edge type for flowcharts
}
export function getFlowNodeSize(
flowType: string | undefined,
): { w: number; h: number } | null {
switch (flowType) {
case "flowProcess":
return FLOW_SIZES.process;
case "flowDecision":
return FLOW_SIZES.decision;
case "flowTerminal":
return FLOW_SIZES.terminal;
case "flowIo":
return FLOW_SIZES.io;
case "flowSubprocess":
return FLOW_SIZES.subprocess;
default:
return null; // Not a flowchart node
}
}
```
### Flowchart Edge Type
| DiagramEdge.type | @xyflow Edge type | Visual |
|---|---|---|
| `sequence` (or default/undefined) | `flowEdge` | Solid orthogonal path with filled arrowhead, optional label at midpoint |
Only one edge type needed. Decision outcomes use the `label` field on the edge (e.g., "Yes", "No", "True", "False").
### Standard ELK Layout — Flowchart Uses the Default Path
**Flowchart diagrams USE the standard ELK layered algorithm.** No custom layout function needed. The `computeLayout()` function in `elk-layout.ts` routes through the default path — the only change needed is adding `getFlowNodeSize()` to the `buildElkGraph()` sizing chain.
**ELK settings for flowcharts:**
- Algorithm: `layered` (default)
- Direction: `DOWN` (top-down flow — best for flowcharts)
- Edge routing: `ORTHOGONAL` (right-angle connectors — standard for flowcharts)
- Crossing minimization: `LAYER_SWEEP` (default)
- Node placement: `BRANDES_KOEPF` (default)
**Integration in `buildElkGraph()` — sizing chain update:**
```typescript
// After existing seqSize check:
const flowSize = getFlowNodeSize(node.type);
const height =
isErEntity && data.columns
? getErEntityHeight(data.columns)
: isOcPerson
? OC_SIZES.person.h
: archSize
? archSize.h
: seqSize
? seqSize.h
: flowSize
? flowSize.h
: (node.measured?.height ?? DEFAULT_NODE_HEIGHT);
const width = archSize
? (data.w ?? archSize.w)
: seqSize
? (data.w ?? seqSize.w)
: flowSize
? (data.w ?? flowSize.w)
: isOcPerson
? (data.w ?? OC_SIZES.person.w)
: (data.w ?? node.measured?.width ?? DEFAULT_NODE_WIDTH);
```
### Node Component Patterns
All 5 node components follow the established pattern:
```typescript
import { Handle, Position } from "@xyflow/react";
import type { NodeProps } from "@xyflow/react";
import type { DiagramNode } from "../graph";
import { HIDDEN_HANDLE } from "../architecture/constants";
export function FlowProcessNode({ data }: NodeProps) {
const d = data as unknown as DiagramNode & { label: string };
const icon = d.icon || "⚙️";
return (
<div className="flow-process">
<span className="flow-node-icon">{icon}</span>
<span className="flow-node-label">{d.label}</span>
<Handle type="target" position={Position.Top} style={HIDDEN_HANDLE} />
<Handle type="target" position={Position.Left} id="left" style={HIDDEN_HANDLE} />
<Handle type="source" position={Position.Bottom} style={HIDDEN_HANDLE} />
<Handle type="source" position={Position.Right} id="right" style={HIDDEN_HANDLE} />
</div>
);
}
```
**Decision node — Diamond shape via CSS:**
```typescript
export function FlowDecisionNode({ data }: NodeProps) {
const d = data as unknown as DiagramNode & { label: string };
return (
<div className="flow-decision">
<div className="flow-decision-diamond">
<span className="flow-decision-label">{d.label}</span>
</div>
<Handle type="target" position={Position.Top} style={HIDDEN_HANDLE} />
<Handle type="target" position={Position.Left} id="left" style={HIDDEN_HANDLE} />
<Handle type="source" position={Position.Bottom} style={HIDDEN_HANDLE} />
<Handle type="source" position={Position.Right} id="right" style={HIDDEN_HANDLE} />
</div>
);
}
```
**Terminal node — Stadium/pill shape:**
```typescript
export function FlowTerminalNode({ data }: NodeProps) {
const d = data as unknown as DiagramNode & { label: string };
const isStart = d.tag === "start";
const isEnd = d.tag === "end";
return (
<div className={`flow-terminal ${isStart ? "flow-terminal-start" : ""} ${isEnd ? "flow-terminal-end" : ""}`}>
<span className="flow-node-label">{d.label}</span>
<Handle type="target" position={Position.Top} style={HIDDEN_HANDLE} />
<Handle type="target" position={Position.Left} id="left" style={HIDDEN_HANDLE} />
<Handle type="source" position={Position.Bottom} style={HIDDEN_HANDLE} />
<Handle type="source" position={Position.Right} id="right" style={HIDDEN_HANDLE} />
</div>
);
}
```
**I/O node — Parallelogram via CSS skew:**
```typescript
export function FlowIoNode({ data }: NodeProps) {
const d = data as unknown as DiagramNode & { label: string };
return (
<div className="flow-io">
<div className="flow-io-skew">
<span className="flow-node-label">{d.label}</span>
</div>
<Handle type="target" position={Position.Top} style={HIDDEN_HANDLE} />
<Handle type="target" position={Position.Left} id="left" style={HIDDEN_HANDLE} />
<Handle type="source" position={Position.Bottom} style={HIDDEN_HANDLE} />
<Handle type="source" position={Position.Right} id="right" style={HIDDEN_HANDLE} />
</div>
);
}
```
**Subprocess node — Double-bordered rectangle:**
```typescript
export function FlowSubprocessNode({ data }: NodeProps) {
const d = data as unknown as DiagramNode & { label: string };
return (
<div className="flow-subprocess">
<div className="flow-subprocess-inner">
<span className="flow-node-label">{d.label}</span>
</div>
<Handle type="target" position={Position.Top} style={HIDDEN_HANDLE} />
<Handle type="target" position={Position.Left} id="left" style={HIDDEN_HANDLE} />
<Handle type="source" position={Position.Bottom} style={HIDDEN_HANDLE} />
<Handle type="source" position={Position.Right} id="right" style={HIDDEN_HANDLE} />
</div>
);
}
```
**Reuses `HIDDEN_HANDLE`** from `architecture/constants.ts` (established in Story 2.6).
### Edge Component — Standard Orthogonal with Label
```typescript
import { BaseEdge, getSmoothStepPath, EdgeLabelRenderer } from "@xyflow/react";
import type { EdgeProps } from "@xyflow/react";
export function FlowEdge(props: EdgeProps) {
const [edgePath, labelX, labelY] = getSmoothStepPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition,
});
return (
<>
<BaseEdge
id={props.id}
path={edgePath}
style={{ stroke: "var(--diagram-flowchart)", strokeWidth: 1.5 }}
markerEnd="url(#flow-arrow)"
/>
{props.label && (
<EdgeLabelRenderer>
<div
className="flow-edge-label"
style={{
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
position: "absolute",
}}
>
{props.label}
</div>
</EdgeLabelRenderer>
)}
</>
);
}
```
Uses `getSmoothStepPath` for orthogonal routing (right-angle connectors). `EdgeLabelRenderer` for HTML label at midpoint — critical for decision outcome labels (Yes/No).
### Graph Converter Updates
Replace the `// Future: flowchart` comment with real dispatch:
```typescript
// In resolveFlowNodeType():
if (diagramType === "flowchart" || nodeType.startsWith("flow:")) {
return resolveFlowchartNodeType(nodeType);
}
// Remove: // Future: flowchart
return "default"; // Truly unknown types
// In resolveFlowEdgeType():
if (diagramType === "flowchart") {
return resolveFlowchartEdgeType(edgeType);
}
return "default";
```
**Import from flowchart/constants.ts** — same pattern as BPMN, E-R, Org Chart, Architecture, Sequence.
**graphToFlow for flowchart:** Uses the standard default path (flat node mapping). No container nodes. No compound layout.
**No CONTAINER_TYPES update needed:** Flowchart has no container nodes (unlike BPMN pools/lanes or sequence fragments).
### DiagramCanvas Updates
```typescript
import { FlowProcessNode } from "../../types/flowchart/FlowProcessNode";
import { FlowDecisionNode } from "../../types/flowchart/FlowDecisionNode";
import { FlowTerminalNode } from "../../types/flowchart/FlowTerminalNode";
import { FlowIoNode } from "../../types/flowchart/FlowIoNode";
import { FlowSubprocessNode } from "../../types/flowchart/FlowSubprocessNode";
import { FlowEdge } from "../../types/flowchart/FlowEdge";
const nodeTypes = {
// Existing BPMN + E-R + Org Chart + Architecture + Sequence types...
flowProcess: FlowProcessNode,
flowDecision: FlowDecisionNode,
flowTerminal: FlowTerminalNode,
flowIo: FlowIoNode,
flowSubprocess: FlowSubprocessNode,
};
const edgeTypes = {
// Existing types...
flowEdge: FlowEdge,
};
// CONTAINER_TYPES — NO CHANGE needed (no flowchart containers)
```
**Arrow marker needed in MarkerDefs:**
```tsx
{/* Flowchart markers */}
<marker
id="flow-arrow"
viewBox="0 0 10 10"
refX={10}
refY={5}
markerWidth={8}
markerHeight={8}
orient="auto-start-reverse"
>
<path
d="M 0 0 L 10 5 L 0 10 Z"
fill="var(--diagram-flowchart, #e11d48)"
/>
</marker>
```
### CSS Styles for Flowchart
Add to `globals.css`. The flowchart theme uses `--diagram-flowchart` (rose accent: `oklch(0.645 0.246 16)`, already defined at line 29).
```css
/* ── Flowchart Diagram Styles ─────────────────────────────────── */
.flow-process {
display: flex;
align-items: center;
gap: 8px;
background: var(--node-bg);
border: 1.5px solid var(--diagram-flowchart);
border-radius: 6px;
padding: 10px 16px;
min-width: 120px;
cursor: pointer;
}
.flow-process:hover {
background: color-mix(in oklch, var(--diagram-flowchart) 8%, var(--node-bg));
}
.flow-decision {
display: flex;
align-items: center;
justify-content: center;
width: 140px;
height: 80px;
cursor: pointer;
}
.flow-decision-diamond {
width: 90px;
height: 90px;
transform: rotate(45deg);
background: var(--node-bg);
border: 1.5px solid var(--diagram-flowchart);
display: flex;
align-items: center;
justify-content: center;
}
.flow-decision-diamond:hover {
background: color-mix(in oklch, var(--diagram-flowchart) 8%, var(--node-bg));
}
.flow-decision-label {
transform: rotate(-45deg);
font-weight: 600;
font-size: 11px;
color: var(--foreground);
text-align: center;
max-width: 80px;
word-wrap: break-word;
line-height: 1.3;
}
.flow-terminal {
display: flex;
align-items: center;
justify-content: center;
background: var(--node-bg);
border: 1.5px solid var(--diagram-flowchart);
border-radius: 999px;
padding: 10px 24px;
min-width: 100px;
cursor: pointer;
}
.flow-terminal:hover {
background: color-mix(in oklch, var(--diagram-flowchart) 8%, var(--node-bg));
}
.flow-terminal-start {
border-color: var(--diagram-flowchart);
border-width: 2px;
}
.flow-terminal-end {
border-color: var(--diagram-flowchart);
border-width: 2.5px;
}
.flow-io {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.flow-io-skew {
display: flex;
align-items: center;
gap: 8px;
background: var(--node-bg);
border: 1.5px solid var(--diagram-flowchart);
padding: 10px 20px;
transform: skewX(-10deg);
min-width: 120px;
}
.flow-io-skew:hover {
background: color-mix(in oklch, var(--diagram-flowchart) 8%, var(--node-bg));
}
.flow-io-skew .flow-node-label {
transform: skewX(10deg);
}
.flow-subprocess {
display: flex;
align-items: center;
justify-content: center;
background: var(--node-bg);
border: 2px solid var(--diagram-flowchart);
border-radius: 6px;
padding: 4px;
cursor: pointer;
}
.flow-subprocess:hover {
background: color-mix(in oklch, var(--diagram-flowchart) 8%, var(--node-bg));
}
.flow-subprocess-inner {
display: flex;
align-items: center;
gap: 8px;
border: 1px solid var(--diagram-flowchart);
border-radius: 4px;
padding: 8px 14px;
width: 100%;
}
.flow-node-icon {
font-size: 16px;
flex-shrink: 0;
}
.flow-node-label {
font-weight: 600;
font-size: 12px;
color: var(--foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.flow-edge-label {
font-size: 11px;
color: var(--diagram-flowchart);
background: var(--node-bg);
padding: 2px 8px;
border-radius: 4px;
border: 1px solid color-mix(in oklch, var(--diagram-flowchart) 30%, transparent);
font-weight: 600;
pointer-events: none;
white-space: nowrap;
}
```
### Existing Code to Reuse / Modify
| File | Action | What |
|------|--------|------|
| `apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx` | **MODIFY** | Register flowchart node types, edge type, arrow marker |
| `apps/web/src/modules/diagram/lib/graph-converter.ts` | **MODIFY** | Add flowchart type resolution for nodes and edges (replace `// Future: flowchart` comment) |
| `apps/web/src/modules/diagram/lib/elk-layout.ts` | **MODIFY** | Add `getFlowNodeSize()` to buildElkGraph sizing chain |
| `apps/web/src/assets/styles/globals.css` | **MODIFY** | Add flowchart CSS styles |
| `apps/web/src/modules/diagram/types/graph.ts` | **READ** | DiagramNode (tag, icon, color) — already defined, no changes |
| `apps/web/src/modules/diagram/lib/bfs-path.ts` | **REUSE** | Path highlighting works for flowchart — diagram-type-agnostic |
| `apps/web/src/modules/diagram/stores/useGraphStore.ts` | **REUSE** | highlightedNodeId — no changes needed |
| `apps/web/src/modules/diagram/hooks/useAutoLayout.ts` | **REUSE** | Auto-layout triggers computeLayout → standard ELK path for flowchart |
| `apps/web/src/modules/diagram/types/architecture/constants.ts` | **REUSE** | `HIDDEN_HANDLE` constant for handle styles |
### Library & Framework Requirements
**No new packages required.** Everything built with existing dependencies:
- `@xyflow/react` 12.10.1 — custom nodes, edges, handles, EdgeLabelRenderer, BaseEdge, getSmoothStepPath
- `elkjs` 0.11.0 — standard layered algorithm for flowchart layout
- `zustand` 5.0.8 — highlight state (reuse existing)
### File Structure for This Story
New files:
```
apps/web/src/modules/diagram/
├── types/flowchart/
│ ├── constants.ts # FLOW_SIZES, type resolution functions, getFlowNodeSize
│ ├── constants.test.ts # Tests for flowchart constants
│ ├── FlowProcessNode.tsx # Rectangle - standard process step
│ ├── FlowDecisionNode.tsx # Diamond - decision/branching
│ ├── FlowTerminalNode.tsx # Stadium/pill - start and end nodes
│ ├── FlowIoNode.tsx # Parallelogram - input/output
│ ├── FlowSubprocessNode.tsx # Double-bordered rectangle - subprocess
│ └── FlowEdge.tsx # Orthogonal edge with label support
```
Modified files:
```
apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx # Register flowchart types + marker
apps/web/src/modules/diagram/lib/graph-converter.ts # Flowchart type mapping (replace // Future: flowchart)
apps/web/src/modules/diagram/lib/graph-converter.test.ts # Add flowchart converter tests + fix line 150 assertion
apps/web/src/modules/diagram/lib/elk-layout.ts # Flowchart node sizing in buildElkGraph
apps/web/src/modules/diagram/lib/elk-layout.test.ts # Add flowchart sizing tests
apps/web/src/assets/styles/globals.css # Flowchart CSS styles
```
### Project Structure Notes
- Flowchart node components go in `~/modules/diagram/types/flowchart/` — follows BPMN/E-R/Org Chart/Architecture/Sequence pattern
- Uses standard ELK layout — no separate layout module needed (unlike sequence)
- Tests co-located next to source files
- No barrel files — import from specific subpaths
### Anti-Patterns to Avoid
- **NEVER put `nodeTypes` or `edgeTypes` inside the component** — causes re-renders (established pattern)
- **NEVER hardcode positions for flowchart nodes in GraphData** — all positioning from ELK layout
- **NEVER import from `reactflow`** — use `@xyflow/react` (v12+)
- **NEVER use `require()`** — ESM-only project
- **NEVER co-locate feature code in route directories** — use `~/modules/diagram/`
- **NEVER store layout-computed positions in persisted graph data** — positions are ephemeral
- **NEVER add new fields to DiagramNode or DiagramEdge** — use existing fields (`tag`, `icon`, `color`) with flowchart semantics
- **NEVER create inline style objects in render** — use CSS classes; conditionally apply inline styles only when data-driven
- **NEVER use `??` for empty string fallbacks** — use `||` so falsy `""` falls back correctly (lesson from Story 2.5 code review)
- **NEVER create barrel `index.ts` files** — per project rules, no barrel files in feature modules
- **DO NOT implement Smart Inspector for flowchart** — future story
- **DO NOT implement other diagram type renderers** — all 6 are now complete
- **DO NOT break existing tests** — 476 tests must continue passing (29 test files across monorepo)
- **DO NOT create a custom layout for flowchart** — ELK layered algorithm handles flowcharts perfectly
- **DO NOT add arrow markers inline** — use SVG `<marker>` in `MarkerDefs` with `markerEnd="url(#flow-arrow)"`
- **DO NOT create multiple edge types** — flowcharts need only one edge type with optional labels for decision outcomes
### Previous Story Intelligence (Story 2.7 — Sequence)
**Key learnings to carry forward:**
- Constants file pattern: `SEQ_SIZES``FLOW_SIZES` equivalent. Type resolution functions: `resolve*NodeType()`, `resolve*EdgeType()`, `get*NodeSize()`
- Node component pattern: Cast `data` as `DiagramNode & { label: string }`, use `Handle` with `style={HIDDEN_HANDLE}` (constant from architecture/constants.ts — reuse it)
- Edge component: For flowchart, use `BaseEdge` + `getSmoothStepPath` (unlike sequence which uses custom SVG paths). This is simpler — matches the E-R/Architecture/OrgChart pattern
- Graph converter: `resolveFlowNodeType` switch on `diagramType`, import type resolver from `types/[type]/constants`
- DiagramCanvas: import components, add to `nodeTypes`/`edgeTypes` objects OUTSIDE component
- CSS: Use `--diagram-[type]` CSS variable for accent colors. Use `color-mix()` for hover/bg tints
- `graphNodeToFlowNode` already spreads all DiagramNode fields into `data` — custom nodes access `data.tag`, `data.icon`, `data.color` directly
- `flowNodeToGraphNode` already preserves `tag`, `icon`, `color` fields — roundtrip works
- Code review lessons from previous stories: Remove dead helper functions, use `||` not `??` for empty string color guards, reuse `HIDDEN_HANDLE` constant
- 476 tests currently pass across monorepo — don't break them
- `computeLayout` returns `LayoutResult{nodes, edges?}` — flowchart uses standard path so `edges` is not returned (only ELK node positions updated)
### Previous Story Intelligence (Story 2.3 — BPMN)
**Key learnings relevant to flowchart:**
- BPMN's `MarkerDefs` pattern: `#bpmn-arrow-filled` — reuse same SVG marker structure for `#flow-arrow`
- Standard edge with `BaseEdge` + `getSmoothStepPath` is the simplest edge pattern — use it for flowchart
- BPMN's compound detection in `computeLayout()` shows the detection routing pattern — flowchart does NOT need this (uses standard path)
### Git Intelligence
Recent commits:
- `1ff8ff8 feat: implement Stories 2.4-2.7 — E-R, Org Chart, Architecture, Sequence diagram type renderers`
- `0a7838a feat: implement Story 2.3 — BPMN diagram type renderer`
- `7dd5af1 feat: implement Story 2.2 — ELK.js auto-layout engine in Web Worker`
- `5033109 feat: implement Story 2.1 — canvas workspace with @xyflow/react and unified graph model`
Established patterns:
- Commit message: `feat: implement Story X.Y — description`
- Feature code in `apps/web/src/modules/diagram/`
- Co-located tests next to source files
- `diagramTypeConfig` in DiagramCard.tsx already has flowchart config: `{ label: "Flowchart", icon: Icons.GitBranch, color: "text-rose-500" }`
- CSS variable `--diagram-flowchart: oklch(0.645 0.246 16)` already defined in globals.css (line 29)
- Graph-converter.ts line 50 has `// Future: flowchart` — the exact hook point to replace
- Graph-converter.test.ts line 150 expects `"default"` for `flow:process` — must update to `"flowProcess"` after adding resolver
### Latest Tech Information
**@xyflow/react 12.10.1 — BaseEdge + getSmoothStepPath:**
- `getSmoothStepPath` produces orthogonal (right-angle) edge paths — ideal for flowcharts
- Returns `[path, labelX, labelY]` tuple — `labelX`/`labelY` are the midpoint coordinates for label placement
- `BaseEdge` wraps the path in a proper SVG `<path>` with all @xyflow/react edge features (selection, animation, etc.)
- `EdgeLabelRenderer` renders HTML labels in a separate div layer above the SVG — use for decision labels
- `markerEnd` on `BaseEdge` references SVG markers from `<defs>` — same as all other diagram types
**CSS Diamond Shape for Decision Nodes:**
- Standard technique: `transform: rotate(45deg)` on container, `transform: rotate(-45deg)` on content
- Container dimensions should be square (the diamond "width" = side * √2)
- Handle positions work correctly with transformed containers in @xyflow/react — handles are positioned on the outer bounding box
- Alternative: Use CSS `clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)` — simpler but may have handle positioning issues
**CSS Parallelogram for I/O Nodes:**
- `transform: skewX(-10deg)` on container, `transform: skewX(10deg)` on content (counter-skew)
- Keeps text readable while giving the parallelogram visual
- Handles work correctly since @xyflow/react uses bounding box positioning
### References
- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.8] — Full AC: process, decision, terminal, I/O, subprocess shapes; decision labels; ELK top-down layout
- [Source: _bmad-output/planning-artifacts/epics.md#Technical Notes] — Port Flexicar FlowDiagram nodes; custom nodes at types/flowchart/; rose accent; standard ELK layered; parallel development
- [Source: _bmad-output/planning-artifacts/architecture.md#Decision 1] — Unified Graph Data Model: `flow:` prefix, shared base fields
- [Source: _bmad-output/planning-artifacts/architecture.md#Enforcement Guidelines] — 7 mandatory rules, type prefixing: `flow:`
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Diagram Type Color Accents] — Flowchart: rose oklch(0.645 0.246 16)
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#DiagramEdgeRenderer] — Flowchart edges: standard directed flow
- [Source: _bmad-output/implementation-artifacts/2-7-sequence-diagram-type-renderer.md] — Sequence patterns: constants, nodes, edges, converter, canvas registration, HIDDEN_HANDLE, code review learnings
- [Source: apps/web/src/modules/diagram/types/graph.ts] — DiagramNode (tag, icon, color), DiagramType includes "flowchart" (line 7)
- [Source: apps/web/src/modules/diagram/lib/graph-converter.ts] — Line 50: `// Future: flowchart` — exact hook point to replace
- [Source: apps/web/src/modules/diagram/lib/graph-converter.test.ts] — Line 150: `flow:process` currently resolves to "default" — update to "flowProcess"
- [Source: apps/web/src/modules/diagram/lib/elk-layout.ts] — buildElkGraph() sizing chain needs flowSize addition after seqSize
- [Source: apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx] — Canvas to register flowchart types + marker
- [Source: apps/web/src/modules/diagram/components/DiagramCard.tsx] — Flowchart config with Icons.GitBranch and text-rose-500
- [Source: apps/web/src/assets/styles/globals.css] — Line 29: `--diagram-flowchart: oklch(0.645 0.246 16)` already defined
## Dev Agent Record
### Agent Model Used
Claude Opus 4.6 (1M context)
### Debug Log References
No issues encountered. One pre-existing test (`graphNodeToFlowNode` basic node) expected `"default"` type for `flow:process` — updated to `"flowProcess"` since the `flow:` prefix now correctly resolves via the new flowchart resolver.
### Completion Notes List
- Created flowchart constants with FLOW_SIZES (5 subtypes), resolveFlowchartNodeType, resolveFlowchartEdgeType, getFlowNodeSize using FLOW_TYPE_MAP for O(1) lookup
- Implemented FlowProcessNode: rectangle with icon + label, rose accent border, 4 hidden handles
- Implemented FlowDecisionNode: diamond shape via CSS rotate(45deg) with counter-rotated label
- Implemented FlowTerminalNode: stadium/pill shape (border-radius: 999px), data.tag-based start/end distinction with varying border widths
- Implemented FlowIoNode: parallelogram shape via CSS skewX(-10deg) with counter-skewed label
- Implemented FlowSubprocessNode: double-bordered rectangle (outer 2px + inner 1px borders)
- Implemented FlowEdge: BaseEdge + getSmoothStepPath for orthogonal routing, EdgeLabelRenderer for decision outcome labels (Yes/No), markerEnd flow-arrow
- Updated graph-converter.ts: replaced `// Future: flowchart` with real dispatch for both node and edge resolution, added flowchart edge type handling
- Registered all flowchart node/edge types in DiagramCanvas nodeTypes/edgeTypes (OUTSIDE component), added #flow-arrow SVG marker to MarkerDefs
- Added getFlowNodeSize to elk-layout.ts buildElkGraph sizing chain after seqSize
- Added flowchart CSS styles with --diagram-flowchart rose accent, color-mix hover states, all 5 node shapes + edge label styling
- Updated existing test assertions: flow:process now resolves to flowProcess (previously "default")
- Added 15 flowchart constants tests, 6 graph converter flowchart tests, 4 ELK layout flowchart sizing tests
- All 497 tests pass across 30 test files (0 regressions, +21 new tests)
### File List
**New files:**
- apps/web/src/modules/diagram/types/flowchart/constants.ts
- apps/web/src/modules/diagram/types/flowchart/constants.test.ts
- apps/web/src/modules/diagram/types/flowchart/FlowProcessNode.tsx
- apps/web/src/modules/diagram/types/flowchart/FlowDecisionNode.tsx
- apps/web/src/modules/diagram/types/flowchart/FlowTerminalNode.tsx
- apps/web/src/modules/diagram/types/flowchart/FlowIoNode.tsx
- apps/web/src/modules/diagram/types/flowchart/FlowSubprocessNode.tsx
- apps/web/src/modules/diagram/types/flowchart/FlowEdge.tsx
**Modified files:**
- apps/web/src/modules/diagram/lib/graph-converter.ts
- apps/web/src/modules/diagram/lib/graph-converter.test.ts
- apps/web/src/modules/diagram/lib/elk-layout.ts
- apps/web/src/modules/diagram/lib/elk-layout.test.ts
- apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx
- apps/web/src/assets/styles/globals.css
- _bmad-output/implementation-artifacts/sprint-status.yaml
### Change Log
- 2026-02-27: Implemented Story 2.8 — Flowchart Diagram Type Renderer. Added 5 node components (process, decision, terminal, I/O, subprocess), 1 edge component with label support, constants + type registry, graph converter integration, ELK layout sizing, CSS styles with rose accent, DiagramCanvas registration + arrow marker. 497 tests passing.
- 2026-02-27: Code review (adversarial) — 7 issues found (4M, 3L), all fixed. M1: Decision diamond ELK height increased 80→130 to fit rotated 90px square. M2: Removed forced ⚙️ icon default from FlowProcessNode, now only renders when data.icon is truthy. M3: Added data.color border override support to all 5 node components. M4: Extracted getNodeDimensions() helper in elk-layout.ts to replace 6-level nested ternary. L1: Unified constants.ts to use lookup maps for both type resolution and sizing. L2: Removed dead gap:8px from .flow-io-skew CSS. L3: Added sprint-status.yaml to File List. 180 web tests passing, 0 regressions.

View File

@@ -57,7 +57,7 @@ development_status:
2-5-org-chart-diagram-type-renderer: done
2-6-architecture-diagram-type-renderer: done
2-7-sequence-diagram-type-renderer: done
2-8-flowchart-diagram-type-renderer: backlog
2-8-flowchart-diagram-type-renderer: done
2-9-node-selection-and-manual-repositioning: backlog
epic-2-retrospective: optional