Replace the placeholder diagram editor with a professional Studio layout featuring an interactive @xyflow/react canvas, unified graph data model with bidirectional converters, Zustand state management, and oklch design tokens. Includes 25 unit tests for converters and store, with all code review fixes applied. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
30 KiB
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
-
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). -
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.
-
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.
-
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).
-
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
-
Task 1: Install @xyflow/react and configure CSS (AC: #1, #3)
- 1.1: Install
@xyflow/reactinapps/web - 1.2: Add
@xyflow/react/dist/style.cssimport in the app's global CSS within@layer base - 1.3: Verify no peer dependency conflicts with React 19 and Zustand 5
- 1.1: Install
-
Task 2: Define unified graph model TypeScript types (AC: #2)
- 2.1: Create
apps/web/src/modules/diagram/types/graph.tswithDiagramNode,DiagramEdge,DiagramMetatypes matching architecture Decision 1 (hybrid schema with type prefixes) - 2.2: Create converter functions
graphToFlow()andflowToGraph()to transform between the unified graph model and @xyflow/react'sNode[]/Edge[]format inapps/web/src/modules/diagram/lib/graph-converter.ts
- 2.1: Create
-
Task 3: Build Studio layout shell for the diagram editor (AC: #1, #4, #5)
- 3.1: Refactor
apps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx— extract a server component that fetches diagram data, wraps a clientDiagramEditorcomponent - 3.2: Create
apps/web/src/modules/diagram/components/editor/DiagramEditor.tsx— the main Studio layout: left sidebar area, center canvas, right panel placeholder - 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 - 3.4: Create
apps/web/src/modules/diagram/components/editor/EditorStatusBar.tsx— 28px status bar with zoom %, diagram type indicator, node count - 3.5: Create
apps/web/src/modules/diagram/components/editor/RightPanel.tsx— 320px collapsible right panel with placeholder tabs (Chat | Inspector | Annotations) - 3.6: Implement keyboard shortcuts: Cmd+B toggle sidebar, Cmd+J toggle right panel
- 3.7: Apply design tokens:
--canvas-bg,--canvas-grid,--node-bg,--node-borderas CSS custom properties
- 3.1: Refactor
-
Task 4: Implement the canvas with @xyflow/react (AC: #1, #2, #3, #4)
- 4.1: Create
apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx—ReactFlowProvider+ReactFlowwrapper with Background (dot grid), Controls (zoom +/-/reset), MiniMap - 4.2: Wire canvas to diagram's
graphData— load from API response, convert viagraphToFlow(), render as nodes/edges - 4.3: Handle empty diagrams — show dot grid background with no nodes, right panel shows "Start a conversation" placeholder
- 4.4: Configure @xyflow/react:
fitView,colorModebased on system theme, selection support, zoom/pan defaults
- 4.1: Create
-
Task 5: Create Zustand store for graph state (AC: #2, #3)
- 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 - 5.2: Wire
onNodesChange,onEdgesChangeto Zustand store viaapplyNodeChanges/applyEdgeChanges - 5.3: Expose
zoomLevel,nodeCount,diagramTypeas derived state for the status bar
- 5.1: Create
-
Task 6: Tests (AC: all)
- 6.1: Unit tests for
graphToFlow()andflowToGraph()converter functions with all 6 diagram types - 6.2: Unit tests for Zustand store actions (add/update/remove nodes, zoom tracking)
- 6.3: Verify existing 141 tests still pass
- 6.1: Unit tests for
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:
-
Unified Graph Data Model (Decision 1): Nodes use type-prefixed
typefield (bpmn:activity,er:entity,flow:decision, etc.). Edges usefrom/to(notsource/target) in the stored model. The converter functions bridge between the lean stored format and @xyflow/react's required format. -
Zustand Store Pattern: Follow the architecture's
useGraphStorepattern. 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 aMap<string, DiagramNode>, edges as aMap<string, DiagramEdge>. -
Component Structure: Feature code in
~/modules/diagram/, NOT co-located in route directories. The page.tsx stays minimal — it fetches data and renders<DiagramEditor>. -
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:
/** 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:
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):
@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.tsxat thediagram/[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
useEffectwith 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 /> nodeTypesobject 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
zoomLevelandnodeCountfrom Zustand store - Displays diagram type badge (reuse
diagramTypeConfigfromDiagramCard.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:
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:
zustand5.0.8 (inapps/web) — for graph state management@tanstack/react-query(catalog) — for diagram API data fetchingsonner2.0.7 — for toast notifications
NOT needed yet:
@liveblocks/*— Story 4.1elkjs— 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:
import { ReactFlow, ReactFlowProvider, MiniMap, Controls, Background, Panel } from "@xyflow/react";
CSS import (for Tailwind CSS 4):
@layer base {
@import "@xyflow/react/dist/style.css";
}
CRITICAL — Parent container must have explicit dimensions:
<div style={{ width: '100%', height: '100%' }}>
<ReactFlow ... />
</div>
CRITICAL — nodeTypes must be defined OUTSIDE the component:
const nodeTypes = { /* ... */ }; // Outside component to prevent re-renders
function DiagramCanvas() {
return <ReactFlow nodeTypes={nodeTypes} ... />;
}
v12 API changes from v11:
node.parentNode→node.parentIdnode.width/height(measured) →node.measured.width/heightupdateEdge()→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:
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 inlib/, state instores/, UI incomponents/editor/ - The
editor/subfolder undercomponents/separates canvas-specific components from the existing dashboard components (DiagramCard, DiagramGrid, sidebar, etc.) - Path aliases: use
~/modules/diagram/...for imports withinapps/web
Anti-Patterns to Avoid
- NEVER put
nodeTypesinside 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
useRefto 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.heightfor measured dimensions — usenode.measured.width/node.measured.heightin 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:
DropdownMenupreferred overContextMenufor action menus (more discoverable, accessible)- Hono RPC client pattern:
api.diagrams[":id"].$get({ param: { id } })for fetching toast()fromsonnerfor user feedback on mutations- React Query invalidation:
queryClient.invalidateQueries({ queryKey: ["diagrams"] }) DiagramResponsetype exported fromDiagramCard.tsx— reuse for data typingdiagramTypeConfigobject has icons and colors for all 6 types — reuse in status bartimeAgo()utility exported fromDiagramCard.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 organizatione9cd685 feat: implement Story 1.3 — diagram access control and management85e06c2 feat: implement Story 1.2 — organize diagrams into projects392da38 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) colorModeprop:"light"|"dark"|"system"— applies appropriate CSS classesReactFlowProviderrequired when using hooks likeuseReactFlow()in child components- New v12 hooks:
useHandleConnections,useNodesData,useReactFlow().updateNode() nodragandnowheelCSS classes prevent drag/scroll interference on interactive elements inside custom nodes
Next.js 16 compatibility notes:
'use client'directive required on all @xyflow/react componentsparamsis async in Next.js 16 — useconst { 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.dataisRecord<string, unknown>in @xyflow/react v12, requiredas unknown ascasts for type safety - Fixed
labelproperty ordering ingraphNodeToFlowNode— spread operator must come before explicitlabelto 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/baseviamergeConfig() - Code review fix M2: Removed
renameMutationfrom useCallback deps to prevent re-creation every render - Code review fix M3: Moved tests to co-located paths (
src/modules/diagram/lib/andstores/) - 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)