# Story 2.1: Canvas Workspace with @xyflow/react and Unified Graph Model

Status: done

<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->

## Story

As a user,
I want to open a diagram and see a professional interactive canvas with a Studio layout,
so that I can view and interact with my diagram visually.

## Acceptance Criteria

1. **Given** I navigate to a diagram editor route (`/dashboard/diagram/[id]`), **When** the page loads, **Then** I see the Studio layout: collapsible sidebar (left), canvas area (center), right panel placeholder (320px), **And** the canvas renders using @xyflow/react with a subtle dot grid background, **And** a header bar shows the diagram name (editable), breadcrumb, and status bar (zoom %, diagram type, node count).

2. **Given** a diagram has graph data stored as JSON, **When** it loads in the editor, **Then** the unified graph model (nodes[] with type discriminator + edges[] with routing metadata) is parsed into @xyflow/react nodes and edges, **And** the diagram renders with correct positions.

3. **Given** I am on the canvas, **When** I use mouse wheel to zoom and drag to pan, **Then** the canvas responds at 60fps with smooth zoom/pan (NFR1), **And** zoom controls (+/-/reset) are visible in the bottom-right corner.

4. **Given** the diagram has no data yet (new diagram), **When** the editor loads, **Then** the canvas shows the dot grid background (not blank white), **And** the right panel shows a placeholder for the AI chat (to be built in Epic 3).

5. **Given** I am in the Studio layout, **When** I press Cmd+B, **Then** the left sidebar toggles between collapsed (icon-only) and expanded states. **When** I press Cmd+J, **Then** the right panel toggles between visible (320px) and hidden.

## Tasks / Subtasks

- [x] Task 1: Install @xyflow/react and configure CSS (AC: #1, #3)
  - [x] 1.1: Install `@xyflow/react` in `apps/web`
  - [x] 1.2: Add `@xyflow/react/dist/style.css` import in the app's global CSS within `@layer base`
  - [x] 1.3: Verify no peer dependency conflicts with React 19 and Zustand 5

- [x] Task 2: Define unified graph model TypeScript types (AC: #2)
  - [x] 2.1: Create `apps/web/src/modules/diagram/types/graph.ts` with `DiagramNode`, `DiagramEdge`, `DiagramMeta` types matching architecture Decision 1 (hybrid schema with type prefixes)
  - [x] 2.2: Create converter functions `graphToFlow()` and `flowToGraph()` to transform between the unified graph model and @xyflow/react's `Node[]`/`Edge[]` format in `apps/web/src/modules/diagram/lib/graph-converter.ts`

- [x] Task 3: Build Studio layout shell for the diagram editor (AC: #1, #4, #5)
  - [x] 3.1: Refactor `apps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx` — extract a server component that fetches diagram data, wraps a client `DiagramEditor` component
  - [x] 3.2: Create `apps/web/src/modules/diagram/components/editor/DiagramEditor.tsx` — the main Studio layout: left sidebar area, center canvas, right panel placeholder
  - [x] 3.3: Create `apps/web/src/modules/diagram/components/editor/EditorHeader.tsx` — 48px header with diagram name (inline editable), breadcrumb (dashboard > diagram name), and diagram type badge
  - [x] 3.4: Create `apps/web/src/modules/diagram/components/editor/EditorStatusBar.tsx` — 28px status bar with zoom %, diagram type indicator, node count
  - [x] 3.5: Create `apps/web/src/modules/diagram/components/editor/RightPanel.tsx` — 320px collapsible right panel with placeholder tabs (Chat | Inspector | Annotations)
  - [x] 3.6: Implement keyboard shortcuts: Cmd+B toggle sidebar, Cmd+J toggle right panel
  - [x] 3.7: Apply design tokens: `--canvas-bg`, `--canvas-grid`, `--node-bg`, `--node-border` as CSS custom properties

- [x] Task 4: Implement the canvas with @xyflow/react (AC: #1, #2, #3, #4)
  - [x] 4.1: Create `apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx` — `ReactFlowProvider` + `ReactFlow` wrapper with Background (dot grid), Controls (zoom +/-/reset), MiniMap
  - [x] 4.2: Wire canvas to diagram's `graphData` — load from API response, convert via `graphToFlow()`, render as nodes/edges
  - [x] 4.3: Handle empty diagrams — show dot grid background with no nodes, right panel shows "Start a conversation" placeholder
  - [x] 4.4: Configure @xyflow/react: `fitView`, `colorMode` based on system theme, selection support, zoom/pan defaults

- [x] Task 5: Create Zustand store for graph state (AC: #2, #3)
  - [x] 5.1: Create `apps/web/src/modules/diagram/stores/useGraphStore.ts` — local Zustand store (no Liveblocks yet, that's Epic 4) managing nodes, edges, viewport state
  - [x] 5.2: Wire `onNodesChange`, `onEdgesChange` to Zustand store via `applyNodeChanges`/`applyEdgeChanges`
  - [x] 5.3: Expose `zoomLevel`, `nodeCount`, `diagramType` as derived state for the status bar

- [x] Task 6: Tests (AC: all)
  - [x] 6.1: Unit tests for `graphToFlow()` and `flowToGraph()` converter functions with all 6 diagram types
  - [x] 6.2: Unit tests for Zustand store actions (add/update/remove nodes, zoom tracking)
  - [x] 6.3: Verify existing 141 tests still pass

## Dev Notes

### Overview — What This Story Builds

This is the foundation story for the entire canvas experience. It replaces the current placeholder page (`"The diagram editor canvas will be implemented in Epic 2."`) with a professional Studio layout containing an interactive @xyflow/react canvas. **No diagram type renderers yet** (those are Stories 2.3-2.8) — this story uses default node rendering. **No AI chat yet** (Epic 3) — the right panel shows a placeholder. **No Liveblocks/CRDT yet** (Epic 4) — graph state lives in a local Zustand store.

### Architecture Compliance

**MANDATORY patterns from Architecture Decision Document:**

1. **Unified Graph Data Model (Decision 1):** Nodes use type-prefixed `type` field (`bpmn:activity`, `er:entity`, `flow:decision`, etc.). Edges use `from`/`to` (not `source`/`target`) in the stored model. The converter functions bridge between the lean stored format and @xyflow/react's required format.

2. **Zustand Store Pattern:** Follow the architecture's `useGraphStore` pattern. For this story, use a plain Zustand store (no Liveblocks middleware — that's Story 4.1). The store shape should be forward-compatible with Liveblocks: nodes as a `Map<string, DiagramNode>`, edges as a `Map<string, DiagramEdge>`.

3. **Component Structure:** Feature code in `~/modules/diagram/`, NOT co-located in route directories. The page.tsx stays minimal — it fetches data and renders `<DiagramEditor>`.

4. **Design Tokens:** Must use the oklch design tokens from the UX spec (listed below). Apply via Tailwind CSS custom properties.

### Unified Graph Data Model — Types

Create `apps/web/src/modules/diagram/types/graph.ts`:

```typescript
/** Stored in DB as jsonb in diagram.graphData and later in Liveblocks CRDT */

export type DiagramType = "bpmn" | "er" | "orgchart" | "architecture" | "sequence" | "flowchart";

/** Column definition for E-R entities */
export interface Column {
  name: string;
  type: string;
  isPrimaryKey?: boolean;
  isForeignKey?: boolean;
  isNullable?: boolean;
  isUnique?: boolean;
  references?: string; // "tableName.columnName"
}

/** Core node — hybrid schema per Architecture Decision 1 */
export interface DiagramNode {
  id: string;
  type: string;            // prefixed: "bpmn:activity", "er:entity", "flow:decision", etc.
  tag?: string;            // short header/category
  label: string;           // description text
  icon?: string;           // emoji or icon identifier
  color?: string;          // hex color
  w?: number;              // content width hint (only layout hint stored)
  // Position override (set when user manually repositions)
  position?: { x: number; y: number };
  // BPMN-specific
  lane?: string;
  group?: string;
  // E-R-specific
  columns?: Column[];
  // Sequence-specific
  lifeline?: boolean;
  // Shared optional
  parentId?: string;       // for nested diagrams
}

/** Core edge */
export interface DiagramEdge {
  id: string;
  from: string;            // source node id
  to: string;              // target node id
  label?: string;
  color?: string;
  type?: string;           // "sequence", "message", "association", "inheritance"
  // E-R-specific
  cardinality?: string;    // "1:N", "N:M", etc.
}

/** Diagram metadata */
export interface DiagramMeta {
  version: string;         // schema version, e.g. "1.0"
  title: string;
  description?: string;
  diagramType: DiagramType;
  layoutDirection?: "DOWN" | "RIGHT" | "LEFT" | "UP";
  edgeRouting?: "ORTHOGONAL" | "SPLINES" | "POLYLINE";
}

/** Complete graph data stored in diagram.graphData jsonb */
export interface GraphData {
  meta?: DiagramMeta;
  nodes: DiagramNode[];
  edges: DiagramEdge[];
  // BPMN extensions
  pools?: Array<{ id: string; label: string; lanes: Array<{ id: string; label: string }> }>;
  groups?: Array<{ id: string; label: string; color?: string }>;
}
```

### Graph Converter — graphToFlow / flowToGraph

Create `apps/web/src/modules/diagram/lib/graph-converter.ts`:

```typescript
import type { Node, Edge } from "@xyflow/react";
import type { DiagramNode, DiagramEdge, GraphData } from "../types/graph";

/** Default position for nodes without manual position override */
const DEFAULT_POSITION = { x: 0, y: 0 };

/** Convert stored DiagramNode to @xyflow/react Node */
export function graphNodeToFlowNode(node: DiagramNode): Node {
  return {
    id: node.id,
    type: "default", // Custom node types registered in Stories 2.3-2.8
    position: node.position ?? DEFAULT_POSITION,
    data: {
      label: node.label,
      // Preserve all original data for type-specific renderers
      ...node,
    },
  };
}

/** Convert stored DiagramEdge to @xyflow/react Edge */
export function graphEdgeToFlowEdge(edge: DiagramEdge): Edge {
  return {
    id: edge.id,
    source: edge.from,
    target: edge.to,
    label: edge.label,
    type: "default",
    data: { ...edge },
  };
}

/** Convert full GraphData to @xyflow/react format */
export function graphToFlow(data: GraphData): { nodes: Node[]; edges: Edge[] } {
  return {
    nodes: (data.nodes ?? []).map(graphNodeToFlowNode),
    edges: (data.edges ?? []).map(graphEdgeToFlowEdge),
  };
}

/** Convert @xyflow/react Node back to stored DiagramNode */
export function flowNodeToGraphNode(node: Node): DiagramNode {
  const { label, id: _id, position: _pos, ...rest } = node.data as DiagramNode & { label: string };
  return {
    id: node.id,
    type: (node.data as DiagramNode).type ?? "flow:process",
    label,
    position: node.position,
    ...rest,
  };
}

/** Convert @xyflow/react Edge back to stored DiagramEdge */
export function flowEdgeToGraphEdge(edge: Edge): DiagramEdge {
  return {
    id: edge.id,
    from: edge.source,
    to: edge.target,
    label: typeof edge.label === "string" ? edge.label : undefined,
    type: (edge.data as DiagramEdge)?.type,
    color: (edge.data as DiagramEdge)?.color,
    cardinality: (edge.data as DiagramEdge)?.cardinality,
  };
}

/** Convert full @xyflow/react state back to GraphData */
export function flowToGraph(nodes: Node[], edges: Edge[], meta?: GraphData["meta"]): GraphData {
  return {
    meta,
    nodes: nodes.map(flowNodeToGraphNode),
    edges: edges.map(flowEdgeToGraphEdge),
  };
}
```

### Design Tokens — CSS Custom Properties

Add to the app's global CSS file (likely `apps/web/src/app/globals.css` or equivalent):

```css
@layer base {
  @import "@xyflow/react/dist/style.css";

  :root {
    /* Canvas */
    --canvas-bg: oklch(0.985 0.002 247);
    --canvas-grid: oklch(0.92 0.004 286 / 30%);
    /* Nodes */
    --node-bg: oklch(1 0 0);
    --node-border: oklch(0.85 0.01 260);
    --node-selected: oklch(0.623 0.214 260);
    --node-hover: oklch(0.623 0.214 260 / 12%);
    /* Edges */
    --edge-default: oklch(0.65 0.01 286);
    --edge-selected: oklch(0.623 0.214 260);
    /* AI (placeholders for future epics) */
    --ai-accent: oklch(0.623 0.214 260);
    /* Diagram type accents */
    --diagram-bpmn: oklch(0.623 0.214 260);
    --diagram-er: oklch(0.606 0.25 293);
    --diagram-orgchart: oklch(0.723 0.219 150);
    --diagram-architecture: oklch(0.552 0.016 286);
    --diagram-sequence: oklch(0.795 0.184 86);
    --diagram-flowchart: oklch(0.645 0.246 16);
  }

  .dark {
    --canvas-bg: oklch(0.16 0.005 285);
    --node-bg: oklch(0.24 0.006 286);
    /* Other dark mode overrides will match the UX spec */
  }
}
```

### Studio Layout — Structure

The editor page must replace the existing TurboStarter dashboard shell (`SidebarProvider` + `DashboardSidebar` + `DashboardInset`) with a custom Studio layout for the diagram editor. The diagram editor route should NOT render inside the standard dashboard shell — it needs the full viewport for the three-panel canvas experience.

**Approach:** Create a separate layout for the diagram editor route that does NOT inherit the dashboard sidebar. The diagram `[id]/page.tsx` should be a Server Component that fetches diagram data and renders the client `DiagramEditor`.

```
/dashboard/diagram/[id]/page.tsx  (Server Component)
  └── DiagramEditor.tsx            (Client Component - full viewport)
        ├── EditorHeader (48px)
        ├── Main area (flex-1)
        │   ├── DiagramSidebar (56px collapsed / 240px expanded)
        │   ├── DiagramCanvas (flex-1)
        │   └── RightPanel (320px, collapsible)
        └── EditorStatusBar (28px)
```

**CRITICAL:** The diagram editor route at `apps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/` currently lives under the `(user)` layout which renders `DashboardSidebar`. To get a full-viewport Studio layout, you need to either:
- Move the diagram route outside `(user)` to avoid the dashboard shell, OR
- Create a `layout.tsx` at the `diagram/[id]/` level that overrides the parent layout

The cleanest approach: Keep it under `(user)` for auth protection, but the `DiagramEditor` component renders as a fixed full-viewport overlay that covers the dashboard shell. Use `fixed inset-0 z-50` positioning.

### Component Implementation Notes

**DiagramEditor.tsx** — Main wrapper:
- Fixed full-viewport (`fixed inset-0 z-50 bg-[var(--canvas-bg)]`)
- Manages panel collapse state via local React state
- Registers keyboard shortcuts (Cmd+B, Cmd+J) via `useEffect` with keydown listener
- Receives diagram data as prop from server component

**DiagramCanvas.tsx** — @xyflow/react wrapper:
- `'use client'` directive required
- **CRITICAL:** Parent element must have explicit width and height (`w-full h-full`)
- `<ReactFlowProvider>` wraps `<ReactFlow>` — provider enables hooks in child components
- Props: `nodes`, `edges`, `onNodesChange`, `onEdgesChange`, `fitView`, `colorMode`
- Sub-components: `<Background variant="dots" />`, `<Controls />`, `<MiniMap />`
- `nodeTypes` object defined OUTSIDE the component (memoization critical for performance)

**EditorHeader.tsx** — Header bar:
- Reuse existing inline rename pattern from the current page.tsx (move the rename logic here)
- Breadcrumb: "Dashboard > [Diagram Name]" with link back to `/dashboard/diagrams`
- Presence avatar stack placeholder (for Epic 4)
- Share button placeholder (for Epic 6)

**EditorStatusBar.tsx** — Status bar:
- Reads `zoomLevel` and `nodeCount` from Zustand store
- Displays diagram type badge (reuse `diagramTypeConfig` from `DiagramCard.tsx`)
- Zoom % calculated from @xyflow/react viewport transform

**RightPanel.tsx** — Collapsible right panel:
- 320px width, `xl:360px`
- Tab headers: Chat | Inspector | Annotations (all placeholder content for now)
- Chat tab shows: "AI Copilot coming soon" message
- Collapsible with smooth transition (200ms ease-out)

### Zustand Store — useGraphStore

Create `apps/web/src/modules/diagram/stores/useGraphStore.ts`:

```typescript
import { create } from "zustand";
import { applyNodeChanges, applyEdgeChanges } from "@xyflow/react";
import type { Node, Edge, OnNodesChange, OnEdgesChange, Viewport } from "@xyflow/react";

interface GraphState {
  // Graph data
  nodes: Node[];
  edges: Edge[];
  // Viewport state
  viewport: Viewport;
  // Derived
  nodeCount: number;
  zoomLevel: number;
  // Actions
  setNodes: (nodes: Node[]) => void;
  setEdges: (edges: Edge[]) => void;
  onNodesChange: OnNodesChange;
  onEdgesChange: OnEdgesChange;
  onViewportChange: (viewport: Viewport) => void;
  // Initialization
  initializeFromGraphData: (nodes: Node[], edges: Edge[]) => void;
}

export const useGraphStore = create<GraphState>((set, get) => ({
  nodes: [],
  edges: [],
  viewport: { x: 0, y: 0, zoom: 1 },
  nodeCount: 0,
  zoomLevel: 100,

  setNodes: (nodes) => set({ nodes, nodeCount: nodes.length }),
  setEdges: (edges) => set({ edges }),

  onNodesChange: (changes) => {
    const updatedNodes = applyNodeChanges(changes, get().nodes);
    set({ nodes: updatedNodes, nodeCount: updatedNodes.length });
  },

  onEdgesChange: (changes) => {
    set({ edges: applyEdgeChanges(changes, get().edges) });
  },

  onViewportChange: (viewport) => {
    set({ viewport, zoomLevel: Math.round(viewport.zoom * 100) });
  },

  initializeFromGraphData: (nodes, edges) => {
    set({ nodes, edges, nodeCount: nodes.length });
  },
}));
```

**Forward-compatibility note:** When Story 4.1 (Liveblocks integration) is implemented, this store will be wrapped with the Liveblocks Zustand middleware. The store shape is designed to be compatible: nodes/edges as arrays that map to LiveMap collections.

### Existing Code to Reuse / Modify

| File | Action | What |
|------|--------|------|
| `apps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx` | **REPLACE** | Current placeholder → server component that fetches diagram + renders `<DiagramEditor>` |
| `apps/web/src/modules/diagram/components/DiagramCard.tsx` | **REUSE** | `diagramTypeConfig`, `timeAgo()`, `DiagramResponse` type — import from here |
| `apps/web/src/config/paths.ts` | **READ ONLY** | Path `pathsConfig.dashboard.user.diagram(id)` already exists |
| `packages/api/src/modules/diagram/router.ts` | **READ ONLY** | `GET /diagrams/:id` endpoint already returns diagram with `graphData` |
| `packages/db/src/schema/diagram.ts` | **READ ONLY** | `graphData: jsonb().$type<object>().default({})` — stores the unified graph model |
| `apps/web/src/lib/api/client.tsx` | **READ ONLY** | Hono RPC client via `api.diagrams[":id"].$get(...)` |

### Library & Framework Requirements

| Package | Version | Purpose | Install Command |
|---------|---------|---------|-----------------|
| `@xyflow/react` | ^12.10.1 | Canvas rendering (nodes, edges, viewport, controls) | `pnpm --filter web add @xyflow/react` |

**Already installed:**
- `zustand` 5.0.8 (in `apps/web`) — for graph state management
- `@tanstack/react-query` (catalog) — for diagram API data fetching
- `sonner` 2.0.7 — for toast notifications

**NOT needed yet:**
- `@liveblocks/*` — Story 4.1
- `elkjs` — Story 2.2
- `@deepgram/sdk` — Story 5.1

### @xyflow/react v12 — Key Implementation Details

**Package name:** `@xyflow/react` (NOT the old `reactflow`)

**All imports are named exports:**
```typescript
import { ReactFlow, ReactFlowProvider, MiniMap, Controls, Background, Panel } from "@xyflow/react";
```

**CSS import (for Tailwind CSS 4):**
```css
@layer base {
  @import "@xyflow/react/dist/style.css";
}
```

**CRITICAL — Parent container must have explicit dimensions:**
```tsx
<div style={{ width: '100%', height: '100%' }}>
  <ReactFlow ... />
</div>
```

**CRITICAL — `nodeTypes` must be defined OUTSIDE the component:**
```tsx
const nodeTypes = { /* ... */ }; // Outside component to prevent re-renders

function DiagramCanvas() {
  return <ReactFlow nodeTypes={nodeTypes} ... />;
}
```

**v12 API changes from v11:**
- `node.parentNode` → `node.parentId`
- `node.width/height` (measured) → `node.measured.width/height`
- `updateEdge()` → `reconnectEdge()`
- No object mutation — always spread/create new objects for state updates

**Dark mode:** Use `colorMode="dark"` prop on `<ReactFlow>`, or `colorMode="system"` to auto-detect.

**Custom node props type:**
```typescript
import type { Node, NodeProps } from "@xyflow/react";
type MyNode = Node<MyNodeData, "myType">;
function MyNode({ data }: NodeProps<MyNode>) { ... }
```

### File Structure for This Story

New files:
```
apps/web/src/modules/diagram/
├── types/
│   └── graph.ts                          # DiagramNode, DiagramEdge, GraphData types
├── lib/
│   └── graph-converter.ts                # graphToFlow / flowToGraph converters
├── stores/
│   └── useGraphStore.ts                  # Zustand store for graph state
└── components/
    └── editor/
        ├── DiagramEditor.tsx             # Main Studio layout wrapper
        ├── DiagramCanvas.tsx             # @xyflow/react canvas
        ├── EditorHeader.tsx              # 48px header bar
        ├── EditorStatusBar.tsx           # 28px status bar
        └── RightPanel.tsx                # 320px collapsible right panel
```

Modified files:
```
apps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx  # Replace placeholder
apps/web/src/app/globals.css (or equivalent)                       # Add design tokens + xyflow CSS
apps/web/package.json                                              # Add @xyflow/react dependency
pnpm-lock.yaml                                                    # Updated lockfile
```

### Project Structure Notes

- All new files follow the `~/modules/diagram/` convention (NOT co-located in route dirs)
- Types go in `types/` subfolder, utilities in `lib/`, state in `stores/`, UI in `components/editor/`
- The `editor/` subfolder under `components/` separates canvas-specific components from the existing dashboard components (DiagramCard, DiagramGrid, sidebar, etc.)
- Path aliases: use `~/modules/diagram/...` for imports within `apps/web`

### Anti-Patterns to Avoid

- **NEVER put `nodeTypes` inside the component** — causes re-renders on every state change, killing canvas performance
- **NEVER use `require()` or CommonJS** — all packages are ESM-only
- **NEVER import from `reactflow`** — the package is `@xyflow/react` (v12+)
- **NEVER use `useRef` to store nodes/edges** — use Zustand store (or @xyflow's built-in state)
- **NEVER create a canvas without explicit parent dimensions** — ReactFlow silently fails
- **NEVER use `node.width`/`node.height` for measured dimensions** — use `node.measured.width`/`node.measured.height` in v12
- **NEVER mutate node/edge objects directly** — always create new objects via spread
- **NEVER co-locate feature code in route directories** — use `~/modules/diagram/`
- **DO NOT break existing 141 tests** — run full test suite after changes
- **DO NOT implement custom node renderers** — those are Stories 2.3-2.8 (use `"default"` type for now)
- **DO NOT implement Liveblocks/CRDT** — that's Story 4.1 (local Zustand store only)
- **DO NOT implement ELK.js layout** — that's Story 2.2
- **DO NOT implement AI chat** — that's Epic 3 (right panel is a placeholder)

### Previous Story Intelligence (Story 1.4)

**Key learnings to carry forward:**
- `DropdownMenu` preferred over `ContextMenu` for action menus (more discoverable, accessible)
- Hono RPC client pattern: `api.diagrams[":id"].$get({ param: { id } })` for fetching
- `toast()` from `sonner` for user feedback on mutations
- React Query invalidation: `queryClient.invalidateQueries({ queryKey: ["diagrams"] })`
- `DiagramResponse` type exported from `DiagramCard.tsx` — reuse for data typing
- `diagramTypeConfig` object has icons and colors for all 6 types — reuse in status bar
- `timeAgo()` utility exported from `DiagramCard.tsx` — reuse if needed
- 141 tests currently pass — don't break them
- The current diagram editor page already handles: loading state, 403 forbidden, 404 not found, inline rename. **Preserve all this behavior** in the refactored version.

### Git Intelligence

Recent commits (all Epic 1):
- `098f496 feat: implement Story 1.4 — recent view and drag-and-drop organization`
- `e9cd685 feat: implement Story 1.3 — diagram access control and management`
- `85e06c2 feat: implement Story 1.2 — organize diagrams into projects`
- `392da38 feat: implement Story 1.1 — create and view diagrams`

Established patterns:
- Commit message: `feat: implement Story X.Y — description`
- Feature code in `apps/web/src/modules/diagram/`
- Co-located tests or in `packages/api/tests/`
- Zod schemas exported from router files

### Latest Tech Information

**@xyflow/react 12.10.1 (current stable, February 2026):**
- Peer deps: `react >=17` — React 19 is fully supported
- Internal dep: `zustand ^4.4.0` — this is xyflow's internal Zustand, separate from your app's Zustand 5.x. Both coexist safely (xyflow's is bundled internally)
- CSS import: `@xyflow/react/dist/style.css` (full) or `@xyflow/react/dist/base.css` (minimal)
- `colorMode` prop: `"light"` | `"dark"` | `"system"` — applies appropriate CSS classes
- `ReactFlowProvider` required when using hooks like `useReactFlow()` in child components
- New v12 hooks: `useHandleConnections`, `useNodesData`, `useReactFlow().updateNode()`
- `nodrag` and `nowheel` CSS classes prevent drag/scroll interference on interactive elements inside custom nodes

**Next.js 16 compatibility notes:**
- `'use client'` directive required on all @xyflow/react components
- `params` is async in Next.js 16 — use `const { id } = await params;` in server components
- Turbopack (default in Next.js 16) works with @xyflow/react without issues

### References

- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.1] — Full AC and technical notes
- [Source: _bmad-output/planning-artifacts/architecture.md#Decision 1] — Unified Graph Data Model (hybrid schema)
- [Source: _bmad-output/planning-artifacts/architecture.md#Decision 2] — Liveblocks Storage structure (LiveMap)
- [Source: _bmad-output/planning-artifacts/architecture.md#Enforcement Guidelines] — 7 mandatory rules
- [Source: _bmad-output/planning-artifacts/architecture.md#Implementation Patterns] — Naming, structure rules
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Studio Layout] — Three-panel layout (48px header, 56/240px sidebar, 320px right panel, 28px status bar)
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Design Tokens] — oklch color values for canvas, nodes, edges
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Responsive Strategy] — Desktop 1024px+, Tablet 768-1023px
- [Source: _bmad-output/project-context.md] — 62 critical implementation rules
- [Source: _bmad-output/implementation-artifacts/1-4-recent-view-and-drag-and-drop-organization.md] — Previous story learnings
- [Source: packages/db/src/schema/diagram.ts] — Current diagram table schema (graphData jsonb)
- [Source: packages/api/src/modules/diagram/router.ts] — GET /diagrams/:id endpoint
- [Source: apps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx] — Current placeholder to replace
- [Source: apps/web/src/modules/diagram/components/DiagramCard.tsx] — diagramTypeConfig, DiagramResponse type
- [Source: apps/web/src/config/paths.ts] — pathsConfig.dashboard.user.diagram(id)
- [Source: @xyflow/react v12 documentation] — API reference, migration guide, custom nodes
- [Source: npm @xyflow/react@12.10.1] — Latest stable version, peer deps

## Dev Agent Record

### Agent Model Used

Claude Opus 4.6

### Debug Log References

- Fixed TypeScript errors in graph-converter.ts: `node.data` is `Record<string, unknown>` in @xyflow/react v12, required `as unknown as` casts for type safety
- Fixed `label` property ordering in `graphNodeToFlowNode` — spread operator must come before explicit `label` to avoid TS2783
- Code review fix H1: Replaced shared mutable DEFAULT_POSITION with inline `{ x: 0, y: 0 }` per node
- Code review fix H2: Added Array.isArray() runtime guards for graphData parsing from DB jsonb
- Code review fix H3: Added `reset()` action to useGraphStore + cleanup on unmount to prevent stale state
- Code review fix H4: vitest.config.ts now extends `@turbostarter/vitest-config/base` via `mergeConfig()`
- Code review fix M2: Removed `renameMutation` from useCallback deps to prevent re-creation every render
- Code review fix M3: Moved tests to co-located paths (`src/modules/diagram/lib/` and `stores/`)
- Code review fix L3: RightPanel now uses width transition animation (200ms ease-out) instead of conditional rendering

### Completion Notes List

- Installed @xyflow/react 12.10.1 with no peer dependency conflicts (React 19 supported, internal zustand ^4.4.0 coexists with app's zustand 5.x)
- Added design tokens (oklch colors for canvas, nodes, edges, diagram type accents) to globals.css with light/dark mode support
- Created unified graph model types (DiagramNode, DiagramEdge, DiagramMeta, GraphData) matching Architecture Decision 1
- Created bidirectional converter functions (graphToFlow/flowToGraph) bridging stored format (from/to) and @xyflow/react format (source/target)
- Built Studio layout with fixed full-viewport overlay (z-50) covering dashboard shell while preserving auth protection
- Implemented DiagramEditor, EditorHeader (inline rename, breadcrumb, type badge), EditorStatusBar (zoom %, node count), RightPanel (3 tabs), DiagramCanvas (@xyflow/react with dot grid, controls, minimap)
- Keyboard shortcuts: Cmd+B toggles sidebar, Cmd+J toggles right panel
- Zustand store (useGraphStore) manages nodes, edges, viewport with derived zoomLevel and nodeCount — forward-compatible with Liveblocks middleware
- 25 new tests (15 converter + 10 store) all passing, 141 existing tests unaffected
- TypeScript compiles clean with no errors

### Change Log

- 2026-02-24: Story 2.1 implemented — Canvas workspace with @xyflow/react and unified graph model
- 2026-02-24: Code review fixes applied — 4 High, 2 Medium, 1 Low issues resolved; status → done

### File List

New files:
- apps/web/src/modules/diagram/types/graph.ts
- apps/web/src/modules/diagram/lib/graph-converter.ts
- apps/web/src/modules/diagram/stores/useGraphStore.ts
- apps/web/src/modules/diagram/components/editor/DiagramEditor.tsx
- apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx
- apps/web/src/modules/diagram/components/editor/EditorHeader.tsx
- apps/web/src/modules/diagram/components/editor/EditorStatusBar.tsx
- apps/web/src/modules/diagram/components/editor/RightPanel.tsx
- apps/web/src/modules/diagram/lib/graph-converter.test.ts
- apps/web/src/modules/diagram/stores/useGraphStore.test.ts
- apps/web/vitest.config.ts

Modified files:
- apps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx (replaced placeholder with DiagramEditor integration)
- apps/web/src/assets/styles/globals.css (added @xyflow/react CSS + design tokens)
- apps/web/package.json (added @xyflow/react dependency, vitest + @turbostarter/vitest-config devDependencies, test script)
- pnpm-lock.yaml (updated lockfile)
