# Story 2.1: Canvas Workspace with @xyflow/react and Unified Graph Model Status: done ## 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`, edges as a `Map`. 3. **Component Structure:** Feature code in `~/modules/diagram/`, NOT co-located in route directories. The page.tsx stays minimal — it fetches data and renders ``. 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`) - `` wraps `` — provider enables hooks in child components - Props: `nodes`, `edges`, `onNodesChange`, `onEdgesChange`, `fitView`, `colorMode` - Sub-components: ``, ``, `` - `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((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 `` | | `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().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
``` **CRITICAL — `nodeTypes` must be defined OUTSIDE the component:** ```tsx const nodeTypes = { /* ... */ }; // Outside component to prevent re-renders function DiagramCanvas() { return ; } ``` **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 ``, or `colorMode="system"` to auto-detect. **Custom node props type:** ```typescript import type { Node, NodeProps } from "@xyflow/react"; type MyNode = Node; function MyNode({ data }: NodeProps) { ... } ``` ### 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` 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)