diff --git a/_bmad-output/implementation-artifacts/2-8-flowchart-diagram-type-renderer.md b/_bmad-output/implementation-artifacts/2-8-flowchart-diagram-type-renderer.md
new file mode 100644
index 0000000..59f3831
--- /dev/null
+++ b/_bmad-output/implementation-artifacts/2-8-flowchart-diagram-type-renderer.md
@@ -0,0 +1,876 @@
+# Story 2.8: Flowchart Diagram Type Renderer
+
+Status: done
+
+
+
+## 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 (
+
+ {icon}
+ {d.label}
+
+
+
+
+
+ );
+}
+```
+
+**Decision node — Diamond shape via CSS:**
+```typescript
+export function FlowDecisionNode({ data }: NodeProps) {
+ const d = data as unknown as DiagramNode & { label: string };
+
+ return (
+
+
+ {d.label}
+
+
+
+
+
+
+ );
+}
+```
+
+**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 (
+
+ {d.label}
+
+
+
+
+
+ );
+}
+```
+
+**I/O node — Parallelogram via CSS skew:**
+```typescript
+export function FlowIoNode({ data }: NodeProps) {
+ const d = data as unknown as DiagramNode & { label: string };
+
+ return (
+
+
+ {d.label}
+
+
+
+
+
+
+ );
+}
+```
+
+**Subprocess node — Double-bordered rectangle:**
+```typescript
+export function FlowSubprocessNode({ data }: NodeProps) {
+ const d = data as unknown as DiagramNode & { label: string };
+
+ return (
+
+
+ {d.label}
+
+
+
+
+
+
+ );
+}
+```
+
+**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 (
+ <>
+
+ {props.label && (
+
+
+ {props.label}
+
+
+ )}
+ >
+ );
+}
+```
+
+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 */}
+
+
+
+```
+
+### 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 `` 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 `` 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 `` — 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.
diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml
index 4b37744..8b9983c 100644
--- a/_bmad-output/implementation-artifacts/sprint-status.yaml
+++ b/_bmad-output/implementation-artifacts/sprint-status.yaml
@@ -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
diff --git a/apps/web/src/assets/styles/globals.css b/apps/web/src/assets/styles/globals.css
index ec6c535..e114191 100644
--- a/apps/web/src/assets/styles/globals.css
+++ b/apps/web/src/assets/styles/globals.css
@@ -650,6 +650,153 @@
position: absolute;
}
+ /* ── 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;
+ 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;
+ }
+
/* ── Path Highlighting ────────────────────────────────────────────────── */
.react-flow__node.dimmed,
diff --git a/apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx b/apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx
index f508f02..19eaacb 100644
--- a/apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx
+++ b/apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx
@@ -47,6 +47,12 @@ import { SeqFragmentNode } from "../../types/sequence/SeqFragmentNode";
import { SeqSyncEdge } from "../../types/sequence/SeqSyncEdge";
import { SeqAsyncEdge } from "../../types/sequence/SeqAsyncEdge";
import { SeqReturnEdge } from "../../types/sequence/SeqReturnEdge";
+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 = {
bpmnActivity: BpmnActivityNode,
@@ -70,6 +76,11 @@ const nodeTypes = {
archExternal: ArchExternalNode,
seqParticipant: SeqParticipantNode,
seqFragment: SeqFragmentNode,
+ flowProcess: FlowProcessNode,
+ flowDecision: FlowDecisionNode,
+ flowTerminal: FlowTerminalNode,
+ flowIo: FlowIoNode,
+ flowSubprocess: FlowSubprocessNode,
};
const edgeTypes = {
@@ -82,6 +93,7 @@ const edgeTypes = {
seqSync: SeqSyncEdge,
seqAsync: SeqAsyncEdge,
seqReturn: SeqReturnEdge,
+ flowEdge: FlowEdge,
};
/** Container node types that should not participate in BFS highlighting */
@@ -183,6 +195,21 @@ function MarkerDefs() {
strokeWidth={1.5}
/>
+ {/* Flowchart markers */}
+
+
+
);
diff --git a/apps/web/src/modules/diagram/lib/elk-layout.test.ts b/apps/web/src/modules/diagram/lib/elk-layout.test.ts
index 438d224..9ba7402 100644
--- a/apps/web/src/modules/diagram/lib/elk-layout.test.ts
+++ b/apps/web/src/modules/diagram/lib/elk-layout.test.ts
@@ -347,6 +347,79 @@ describe("buildElkGraph", () => {
expect(result.children?.[0]?.height).toBe(50);
});
+ it("should use FLOW_SIZES for flowchart node subtypes", () => {
+ const flowNodes: Node[] = [
+ {
+ id: "step1",
+ type: "flowProcess",
+ position: { x: 0, y: 0 },
+ data: { id: "step1", type: "flow:process", label: "Step 1" },
+ },
+ {
+ id: "check1",
+ type: "flowDecision",
+ position: { x: 0, y: 0 },
+ data: { id: "check1", type: "flow:decision", label: "OK?" },
+ },
+ {
+ id: "start",
+ type: "flowTerminal",
+ position: { x: 0, y: 0 },
+ data: { id: "start", type: "flow:terminal", label: "Start", tag: "start" },
+ },
+ {
+ id: "input1",
+ type: "flowIo",
+ position: { x: 0, y: 0 },
+ data: { id: "input1", type: "flow:io", label: "Read Input" },
+ },
+ {
+ id: "sub1",
+ type: "flowSubprocess",
+ position: { x: 0, y: 0 },
+ data: { id: "sub1", type: "flow:subprocess", label: "Validate" },
+ },
+ ];
+
+ const result = buildElkGraph(flowNodes, []);
+
+ // flowProcess: w=160, h=60
+ expect(result.children?.[0]?.width).toBe(160);
+ expect(result.children?.[0]?.height).toBe(60);
+ // flowDecision: w=140, h=130
+ expect(result.children?.[1]?.width).toBe(140);
+ expect(result.children?.[1]?.height).toBe(130);
+ // flowTerminal: w=140, h=50
+ expect(result.children?.[2]?.width).toBe(140);
+ expect(result.children?.[2]?.height).toBe(50);
+ // flowIo: w=160, h=60
+ expect(result.children?.[3]?.width).toBe(160);
+ expect(result.children?.[3]?.height).toBe(60);
+ // flowSubprocess: w=160, h=60
+ expect(result.children?.[4]?.width).toBe(160);
+ expect(result.children?.[4]?.height).toBe(60);
+ });
+
+ it("should respect data.w override for flowchart nodes", () => {
+ const flowNode: Node = {
+ id: "step1",
+ type: "flowProcess",
+ position: { x: 0, y: 0 },
+ data: { id: "step1", type: "flow:process", label: "Wide Step", w: 250 },
+ };
+ const result = buildElkGraph([flowNode], []);
+ expect(result.children?.[0]?.width).toBe(250);
+ expect(result.children?.[0]?.height).toBe(60);
+ });
+
+ it("should NOT use flowchart sizing for non-flowchart types", () => {
+ const regularNode = createNode("n1");
+ const result = buildElkGraph([regularNode], []);
+ // Regular node: default dimensions (150x50), NOT flowchart sizes
+ expect(result.children?.[0]?.width).toBe(150);
+ expect(result.children?.[0]?.height).toBe(50);
+ });
+
it("should handle empty nodes and edges", () => {
const result = buildElkGraph([], []);
diff --git a/apps/web/src/modules/diagram/lib/elk-layout.ts b/apps/web/src/modules/diagram/lib/elk-layout.ts
index 406d061..df0e9b8 100644
--- a/apps/web/src/modules/diagram/lib/elk-layout.ts
+++ b/apps/web/src/modules/diagram/lib/elk-layout.ts
@@ -12,6 +12,7 @@ import { getErEntityHeight } from "../types/er/constants";
import { OC_SIZES } from "../types/orgchart/constants";
import { getArchNodeSize } from "../types/architecture/constants";
import { getSeqNodeSize } from "../types/sequence/constants";
+import { getFlowNodeSize } from "../types/flowchart/constants";
import { computeSequenceLayout } from "./sequence-layout";
// ── Layout Options ──────────────────────────────────────────────────────────
@@ -37,6 +38,46 @@ const DEFAULT_NODE_WIDTH = 150;
const DEFAULT_NODE_HEIGHT = 50;
export const SOFT_CAP_NODE_COUNT = 200;
+// ── Node Dimension Resolution ─────────────────────────────────────────────
+
+function getNodeDimensions(node: Node): { w: number; h: number } {
+ const data = node.data as unknown as DiagramNode;
+
+ // E-R entities: height computed from column count
+ if (node.type === "erEntity" && data.columns) {
+ return { w: data.w ?? node.measured?.width ?? DEFAULT_NODE_WIDTH, h: getErEntityHeight(data.columns) };
+ }
+
+ // Org chart persons: fixed dimensions
+ if (node.type === "orgchartPerson") {
+ return { w: data.w ?? OC_SIZES.person.w, h: OC_SIZES.person.h };
+ }
+
+ // Architecture nodes: per-subtype dimensions
+ const archSize = getArchNodeSize(node.type);
+ if (archSize) {
+ return { w: data.w ?? archSize.w, h: archSize.h };
+ }
+
+ // Sequence nodes: per-subtype dimensions
+ const seqSize = getSeqNodeSize(node.type);
+ if (seqSize) {
+ return { w: data.w ?? seqSize.w, h: seqSize.h };
+ }
+
+ // Flowchart nodes: per-subtype dimensions
+ const flowSize = getFlowNodeSize(node.type);
+ if (flowSize) {
+ return { w: data.w ?? flowSize.w, h: flowSize.h };
+ }
+
+ // Default: measured or fallback
+ return {
+ w: data.w ?? node.measured?.width ?? DEFAULT_NODE_WIDTH,
+ h: node.measured?.height ?? DEFAULT_NODE_HEIGHT,
+ };
+}
+
// ── ELK Graph Building ─────────────────────────────────────────────────────
export function buildElkGraph(
@@ -60,37 +101,8 @@ export function buildElkGraph(
"elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
},
children: nodes.map((node) => {
- const data = node.data as unknown as DiagramNode;
- // E-R entities: compute height from columns for correct ELK spacing
- const isErEntity = node.type === "erEntity";
- // Org chart persons: fixed dimensions
- const isOcPerson = node.type === "orgchartPerson";
- // Architecture nodes: per-subtype dimensions
- const archSize = getArchNodeSize(node.type);
- // Sequence nodes: per-subtype dimensions
- const seqSize = getSeqNodeSize(node.type);
- const height =
- isErEntity && data.columns
- ? getErEntityHeight(data.columns)
- : isOcPerson
- ? OC_SIZES.person.h
- : archSize
- ? archSize.h
- : seqSize
- ? seqSize.h
- : (node.measured?.height ?? DEFAULT_NODE_HEIGHT);
- const width = archSize
- ? (data.w ?? archSize.w)
- : seqSize
- ? (data.w ?? seqSize.w)
- : isOcPerson
- ? (data.w ?? OC_SIZES.person.w)
- : (data.w ?? node.measured?.width ?? DEFAULT_NODE_WIDTH);
- return {
- id: node.id,
- width,
- height,
- };
+ const { w, h } = getNodeDimensions(node);
+ return { id: node.id, width: w, height: h };
}),
edges: edges.map(
(edge) =>
diff --git a/apps/web/src/modules/diagram/lib/graph-converter.test.ts b/apps/web/src/modules/diagram/lib/graph-converter.test.ts
index 285ec09..520cad3 100644
--- a/apps/web/src/modules/diagram/lib/graph-converter.test.ts
+++ b/apps/web/src/modules/diagram/lib/graph-converter.test.ts
@@ -24,7 +24,7 @@ describe("graphNodeToFlowNode", () => {
const result = graphNodeToFlowNode(node);
expect(result).toEqual({
id: "n1",
- type: "default",
+ type: "flowProcess",
position: { x: 100, y: 200 },
data: { ...node, label: "Start" },
});
@@ -146,8 +146,8 @@ describe("graphToFlow", () => {
expect(result.nodes[3]!.type).toBe("archService");
// seq: prefix resolves to sequence node type even without diagramType context
expect(result.nodes[4]!.type).toBe("seqParticipant");
- // Non-prefixed types without diagramType context stay default
- expect(result.nodes[5]!.type).toBe("default");
+ // flow: prefix resolves to flowchart node type even without diagramType context
+ expect(result.nodes[5]!.type).toBe("flowProcess");
});
it("should resolve architecture node types when diagramType is architecture", () => {
@@ -440,6 +440,75 @@ describe("graphToFlow", () => {
expect(result.edges).toHaveLength(2);
});
+ it("should resolve flowchart node types when diagramType is flowchart", () => {
+ const data: GraphData = {
+ meta: {
+ version: "1.0",
+ title: "Flowchart Test",
+ diagramType: "flowchart",
+ },
+ nodes: [
+ { id: "n1", type: "process", label: "Step 1" },
+ { id: "n2", type: "flow:decision", label: "Check?" },
+ { id: "n3", type: "flow:terminal", label: "Start", tag: "start" },
+ { id: "n4", type: "flow:io", label: "Read Input" },
+ { id: "n5", type: "flow:subprocess", label: "Validate" },
+ ],
+ edges: [],
+ };
+ const result = graphToFlow(data);
+ expect(result.nodes[0]!.type).toBe("flowProcess");
+ expect(result.nodes[1]!.type).toBe("flowDecision");
+ expect(result.nodes[2]!.type).toBe("flowTerminal");
+ expect(result.nodes[3]!.type).toBe("flowIo");
+ expect(result.nodes[4]!.type).toBe("flowSubprocess");
+ });
+
+ it("should resolve flowchart edge types when diagramType is flowchart", () => {
+ const data: GraphData = {
+ meta: {
+ version: "1.0",
+ title: "Flowchart Edge Test",
+ diagramType: "flowchart",
+ },
+ nodes: [
+ { id: "start", type: "flow:terminal", label: "Start" },
+ { id: "step1", type: "flow:process", label: "Step 1" },
+ ],
+ edges: [
+ { id: "e1", from: "start", to: "step1", type: "sequence" },
+ { id: "e2", from: "start", to: "step1" },
+ ],
+ };
+ const result = graphToFlow(data);
+ expect(result.edges[0]!.type).toBe("flowEdge");
+ expect(result.edges[1]!.type).toBe("flowEdge");
+ });
+
+ it("should use flat layout for flowchart diagrams (no container nodes)", () => {
+ const data: GraphData = {
+ meta: {
+ version: "1.0",
+ title: "Flowchart Flat Layout",
+ diagramType: "flowchart",
+ },
+ nodes: [
+ { id: "start", type: "flow:terminal", label: "Start", tag: "start" },
+ { id: "step1", type: "flow:process", label: "Process" },
+ { id: "check", type: "flow:decision", label: "OK?" },
+ { id: "end", type: "flow:terminal", label: "End", tag: "end" },
+ ],
+ edges: [
+ { id: "e1", from: "start", to: "step1" },
+ { id: "e2", from: "step1", to: "check" },
+ { id: "e3", from: "check", to: "end", label: "Yes" },
+ ],
+ };
+ const result = graphToFlow(data);
+ expect(result.nodes).toHaveLength(4);
+ expect(result.edges).toHaveLength(3);
+ });
+
it("should use flat layout for org chart diagrams (no container nodes)", () => {
const data: GraphData = {
meta: {
diff --git a/apps/web/src/modules/diagram/lib/graph-converter.ts b/apps/web/src/modules/diagram/lib/graph-converter.ts
index e12c07b..8b0f3df 100644
--- a/apps/web/src/modules/diagram/lib/graph-converter.ts
+++ b/apps/web/src/modules/diagram/lib/graph-converter.ts
@@ -25,6 +25,10 @@ import {
resolveSequenceNodeType,
resolveSequenceEdgeType,
} from "../types/sequence/constants";
+import {
+ resolveFlowchartNodeType,
+ resolveFlowchartEdgeType,
+} from "../types/flowchart/constants";
// ── Node Type Resolution ───────────────────────────────────────────────────
@@ -47,7 +51,9 @@ function resolveFlowNodeType(
if (diagramType === "sequence" || nodeType.startsWith("seq:")) {
return resolveSequenceNodeType(nodeType);
}
- // Future: flowchart
+ if (diagramType === "flowchart" || nodeType.startsWith("flow:")) {
+ return resolveFlowchartNodeType(nodeType);
+ }
return "default";
}
@@ -70,6 +76,9 @@ function resolveFlowEdgeType(
if (diagramType === "sequence") {
return resolveSequenceEdgeType(edgeType);
}
+ if (diagramType === "flowchart") {
+ return resolveFlowchartEdgeType(edgeType);
+ }
return "default";
}
diff --git a/apps/web/src/modules/diagram/types/flowchart/FlowDecisionNode.tsx b/apps/web/src/modules/diagram/types/flowchart/FlowDecisionNode.tsx
new file mode 100644
index 0000000..e6f404a
--- /dev/null
+++ b/apps/web/src/modules/diagram/types/flowchart/FlowDecisionNode.tsx
@@ -0,0 +1,37 @@
+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 FlowDecisionNode({ data }: NodeProps) {
+ const d = data as unknown as DiagramNode & { label: string };
+
+ return (
+
+
+ {d.label}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/modules/diagram/types/flowchart/FlowEdge.tsx b/apps/web/src/modules/diagram/types/flowchart/FlowEdge.tsx
new file mode 100644
index 0000000..39f67e2
--- /dev/null
+++ b/apps/web/src/modules/diagram/types/flowchart/FlowEdge.tsx
@@ -0,0 +1,37 @@
+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 (
+ <>
+
+ {props.label && (
+
+
+ {props.label}
+
+
+ )}
+ >
+ );
+}
diff --git a/apps/web/src/modules/diagram/types/flowchart/FlowIoNode.tsx b/apps/web/src/modules/diagram/types/flowchart/FlowIoNode.tsx
new file mode 100644
index 0000000..4847662
--- /dev/null
+++ b/apps/web/src/modules/diagram/types/flowchart/FlowIoNode.tsx
@@ -0,0 +1,37 @@
+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 FlowIoNode({ data }: NodeProps) {
+ const d = data as unknown as DiagramNode & { label: string };
+
+ return (
+
+
+ {d.label}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/modules/diagram/types/flowchart/FlowProcessNode.tsx b/apps/web/src/modules/diagram/types/flowchart/FlowProcessNode.tsx
new file mode 100644
index 0000000..7fbd7fe
--- /dev/null
+++ b/apps/web/src/modules/diagram/types/flowchart/FlowProcessNode.tsx
@@ -0,0 +1,36 @@
+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 };
+
+ return (
+
+ {d.icon && {d.icon}}
+ {d.label}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/modules/diagram/types/flowchart/FlowSubprocessNode.tsx b/apps/web/src/modules/diagram/types/flowchart/FlowSubprocessNode.tsx
new file mode 100644
index 0000000..f198ede
--- /dev/null
+++ b/apps/web/src/modules/diagram/types/flowchart/FlowSubprocessNode.tsx
@@ -0,0 +1,40 @@
+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 FlowSubprocessNode({ data }: NodeProps) {
+ const d = data as unknown as DiagramNode & { label: string };
+
+ return (
+
+
+ {d.label}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/modules/diagram/types/flowchart/FlowTerminalNode.tsx b/apps/web/src/modules/diagram/types/flowchart/FlowTerminalNode.tsx
new file mode 100644
index 0000000..a0c096f
--- /dev/null
+++ b/apps/web/src/modules/diagram/types/flowchart/FlowTerminalNode.tsx
@@ -0,0 +1,37 @@
+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 FlowTerminalNode({ data }: NodeProps) {
+ const d = data as unknown as DiagramNode & { label: string };
+ const isStart = d.tag === "start";
+ const isEnd = d.tag === "end";
+
+ return (
+
+ {d.label}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/modules/diagram/types/flowchart/constants.test.ts b/apps/web/src/modules/diagram/types/flowchart/constants.test.ts
new file mode 100644
index 0000000..f189d21
--- /dev/null
+++ b/apps/web/src/modules/diagram/types/flowchart/constants.test.ts
@@ -0,0 +1,89 @@
+import { describe, it, expect } from "vitest";
+import {
+ FLOW_SIZES,
+ resolveFlowchartNodeType,
+ resolveFlowchartEdgeType,
+ getFlowNodeSize,
+} from "./constants";
+
+describe("FLOW_SIZES", () => {
+ it("should define sizes for all 5 flowchart node subtypes", () => {
+ expect(FLOW_SIZES.process).toEqual({ w: 160, h: 60 });
+ expect(FLOW_SIZES.decision).toEqual({ w: 140, h: 130 });
+ expect(FLOW_SIZES.terminal).toEqual({ w: 140, h: 50 });
+ expect(FLOW_SIZES.io).toEqual({ w: 160, h: 60 });
+ expect(FLOW_SIZES.subprocess).toEqual({ w: 160, h: 60 });
+ });
+});
+
+describe("resolveFlowchartNodeType", () => {
+ it("should resolve flow:process to flowProcess", () => {
+ expect(resolveFlowchartNodeType("flow:process")).toBe("flowProcess");
+ });
+
+ it("should resolve flow:decision to flowDecision", () => {
+ expect(resolveFlowchartNodeType("flow:decision")).toBe("flowDecision");
+ });
+
+ it("should resolve flow:terminal to flowTerminal", () => {
+ expect(resolveFlowchartNodeType("flow:terminal")).toBe("flowTerminal");
+ });
+
+ it("should resolve flow:io to flowIo", () => {
+ expect(resolveFlowchartNodeType("flow:io")).toBe("flowIo");
+ });
+
+ it("should resolve flow:subprocess to flowSubprocess", () => {
+ expect(resolveFlowchartNodeType("flow:subprocess")).toBe("flowSubprocess");
+ });
+
+ it("should resolve bare type without prefix", () => {
+ expect(resolveFlowchartNodeType("process")).toBe("flowProcess");
+ expect(resolveFlowchartNodeType("decision")).toBe("flowDecision");
+ expect(resolveFlowchartNodeType("terminal")).toBe("flowTerminal");
+ expect(resolveFlowchartNodeType("io")).toBe("flowIo");
+ expect(resolveFlowchartNodeType("subprocess")).toBe("flowSubprocess");
+ });
+
+ it("should default unknown types to flowProcess", () => {
+ expect(resolveFlowchartNodeType("unknown")).toBe("flowProcess");
+ expect(resolveFlowchartNodeType("flow:unknown")).toBe("flowProcess");
+ });
+});
+
+describe("resolveFlowchartEdgeType", () => {
+ it("should always return flowEdge", () => {
+ expect(resolveFlowchartEdgeType("sequence")).toBe("flowEdge");
+ expect(resolveFlowchartEdgeType("conditional")).toBe("flowEdge");
+ expect(resolveFlowchartEdgeType(undefined)).toBe("flowEdge");
+ });
+});
+
+describe("getFlowNodeSize", () => {
+ it("should return correct size for flowProcess", () => {
+ expect(getFlowNodeSize("flowProcess")).toEqual({ w: 160, h: 60 });
+ });
+
+ it("should return correct size for flowDecision", () => {
+ expect(getFlowNodeSize("flowDecision")).toEqual({ w: 140, h: 130 });
+ });
+
+ it("should return correct size for flowTerminal", () => {
+ expect(getFlowNodeSize("flowTerminal")).toEqual({ w: 140, h: 50 });
+ });
+
+ it("should return correct size for flowIo", () => {
+ expect(getFlowNodeSize("flowIo")).toEqual({ w: 160, h: 60 });
+ });
+
+ it("should return correct size for flowSubprocess", () => {
+ expect(getFlowNodeSize("flowSubprocess")).toEqual({ w: 160, h: 60 });
+ });
+
+ it("should return null for non-flowchart types", () => {
+ expect(getFlowNodeSize("archService")).toBeNull();
+ expect(getFlowNodeSize("erEntity")).toBeNull();
+ expect(getFlowNodeSize("default")).toBeNull();
+ expect(getFlowNodeSize(undefined)).toBeNull();
+ });
+});
diff --git a/apps/web/src/modules/diagram/types/flowchart/constants.ts b/apps/web/src/modules/diagram/types/flowchart/constants.ts
new file mode 100644
index 0000000..00301e3
--- /dev/null
+++ b/apps/web/src/modules/diagram/types/flowchart/constants.ts
@@ -0,0 +1,45 @@
+/** Flowchart diagram node dimensions for ELK layout spacing. */
+export const FLOW_SIZES = {
+ process: { w: 160, h: 60 },
+ decision: { w: 140, h: 130 },
+ terminal: { w: 140, h: 50 },
+ io: { w: 160, h: 60 },
+ subprocess: { w: 160, h: 60 },
+} as const;
+
+const FLOW_TYPE_MAP: Record = {
+ flowProcess: FLOW_SIZES.process,
+ flowDecision: FLOW_SIZES.decision,
+ flowTerminal: FLOW_SIZES.terminal,
+ flowIo: FLOW_SIZES.io,
+ flowSubprocess: FLOW_SIZES.subprocess,
+};
+
+const FLOW_NODE_TYPE_MAP: Record = {
+ process: "flowProcess",
+ decision: "flowDecision",
+ terminal: "flowTerminal",
+ io: "flowIo",
+ subprocess: "flowSubprocess",
+};
+
+/** Get flowchart node dimensions by @xyflow/react node type. Returns null if not a flowchart type. */
+export function getFlowNodeSize(
+ flowType: string | undefined,
+): { w: number; h: number } | null {
+ if (!flowType) return null;
+ return FLOW_TYPE_MAP[flowType] ?? null;
+}
+
+/** Map DiagramNode.type (with or without flow: prefix) to @xyflow/react node type string. */
+export function resolveFlowchartNodeType(type: string): string {
+ const bare = type.startsWith("flow:") ? type.slice(5) : type;
+ return FLOW_NODE_TYPE_MAP[bare] ?? "flowProcess";
+}
+
+/** Map DiagramEdge.type to @xyflow/react edge type string for flowchart diagrams. */
+export function resolveFlowchartEdgeType(
+ _type: string | undefined,
+): string {
+ return "flowEdge";
+}