Add automatic diagram layout via ELK.js running in a dedicated Web Worker. Nodes animate smoothly (200ms ease-out) to computed positions using the Sugiyama/layered algorithm. Includes layout direction controls (DOWN/RIGHT/ LEFT/UP), edge routing modes (orthogonal/splines/polyline), 200-node soft cap warning, single-flight race condition protection, and 10s worker timeout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
26 KiB
Story 2.2: ELK.js Auto-Layout Engine in Web Worker
Status: done
Story
As a user, I want my diagrams to be automatically laid out in a clean, professional arrangement, so that I never have to manually arrange nodes and my diagrams always look presentable.
Acceptance Criteria
-
Given a diagram has nodes and edges, When auto-layout is triggered (on load, after AI mutation, or via manual trigger), Then ELK.js computes a Sugiyama/layered layout in a Web Worker thread, And layout completes in < 500ms for diagrams with < 50 nodes (NFR2), And nodes animate smoothly to their new positions (200ms ease-out transition).
-
Given a layout is being computed, When the Web Worker is processing, Then the main thread remains responsive (no UI jank), And a subtle loading indicator appears if layout takes > 200ms.
-
Given a diagram has > 200 nodes, When auto-layout is triggered, Then a soft cap warning suggests splitting the diagram, And layout still completes (may take longer) without crashing.
-
Given a diagram supports multiple layout directions, When I select a direction (DOWN, RIGHT, LEFT, UP) from the status bar, Then ELK.js re-computes layout with the selected direction, And edge routing follows the selected mode (orthogonal default, splines, polyline).
-
Given nodes have manual position overrides (set by the user via drag in Story 2.9), When auto-layout is triggered, Then only nodes WITHOUT manual position overrides are repositioned by ELK, And nodes with manual overrides retain their user-set positions.
Tasks / Subtasks
-
Task 1: Install elkjs and configure Web Worker (AC: #1, #2)
- 1.1: Install
elkjs@0.11.0inapps/web - 1.2: Create
apps/web/src/modules/diagram/lib/elk-worker.ts— Web Worker wrapper that importselkjs/lib/elk.bundled.jsand accepts layout messages - 1.3: Verify Web Worker loads correctly in Next.js 16 with Turbopack
- 1.1: Install
-
Task 2: Create ELK layout engine module (AC: #1, #4, #5)
- 2.1: Create
apps/web/src/modules/diagram/lib/elk-layout.ts— main layout module withcomputeLayout(nodes, edges, options)function - 2.2: Implement
buildElkGraph()— converts DiagramNode[]/DiagramEdge[] into ELK graph format (childrenwithwidth/height,edgeswithsources/targets) - 2.3: Implement
resolvePositions()— maps ELK output coordinates back to @xyflow/react Node positions - 2.4: Support layout options: direction (DOWN/RIGHT/LEFT/UP), edge routing (ORTHOGONAL/SPLINES/POLYLINE), spacing
- 2.5: Implement manual position override logic — skip ELK positioning for nodes where
node.data.positionwas set by user drag
- 2.1: Create
-
Task 3: Create
useAutoLayouthook (AC: #1, #2, #3, #4)- 3.1: Create
apps/web/src/modules/diagram/hooks/useAutoLayout.ts— React hook wrapping the layout engine with debouncing (300ms) - 3.2: Add
isLayoutingstate for loading indicator - 3.3: Add 200-node soft cap warning via
toast()from sonner - 3.4: Implement smooth node animation — apply new positions with CSS transition (200ms ease-out) via @xyflow/react's
setNodeswith position updates
- 3.1: Create
-
Task 4: Integrate auto-layout into DiagramCanvas (AC: #1, #2, #4)
- 4.1: Wire
useAutoLayoutintoDiagramCanvas.tsx— trigger layout on initial load when diagram has nodes - 4.2: Expose
triggerLayout()function viauseAutoLayouthook for manual trigger and future AI mutation integration - 4.3: Add subtle loading overlay when
isLayoutingis true (Panel component with loading indicator)
- 4.1: Wire
-
Task 5: Add layout direction controls to EditorStatusBar (AC: #4)
- 5.1: Add layout direction dropdown to
EditorStatusBar.tsx— selector for DOWN/RIGHT/LEFT/UP - 5.2: Add edge routing dropdown — selector for ORTHOGONAL/SPLINES/POLYLINE
- 5.3: Store layout direction and edge routing in
useGraphStoreaslayoutDirectionandedgeRoutingstate - 5.4: Direction/routing changes trigger re-layout via
useAutoLayout
- 5.1: Add layout direction dropdown to
-
Task 6: Tests (AC: all)
- 6.1: Unit tests for
buildElkGraph()— converting diagram nodes/edges to ELK format (10 tests) - 6.2: Unit tests for
resolvePositions()— mapping ELK output back to @xyflow positions (6 tests) - 6.3: Unit tests for layout options (direction, edge routing, spacing) — covered in buildElkGraph tests
- 6.4: Unit test for SOFT_CAP_NODE_COUNT constant (1 test)
- 6.5: All 49 tests pass (15 graph-converter + 17 elk-layout + 17 store including 7 new layout state tests)
- 6.1: Unit tests for
Dev Notes
Overview — What This Story Builds
This story adds automatic diagram layout via ELK.js running in a Web Worker. When a diagram loads with nodes (or when the user triggers layout manually), ELK.js computes optimal positions using the Sugiyama/layered algorithm and the canvas smoothly animates nodes to their new positions. The Web Worker ensures the main thread stays responsive even for large diagrams.
This story does NOT implement:
- Custom node renderers (Stories 2.3-2.8)
- BPMN compound layout with pools/lanes (Story 2.3 will extend the ELK graph builder)
- Liveblocks/CRDT integration (Epic 4)
- AI-triggered layout (Epic 3 will call
triggerLayout())
Architecture Compliance
MANDATORY patterns from Architecture Decision Document:
-
ELK.js in Web Worker (Architecture Overview): ELK.js MUST run in a Web Worker to keep the main thread responsive (NFR2). Use
elkjs/lib/elk.bundled.jsinside the worker — the built-in worker mode viaworkerFactoryis the cleanest approach for Next.js 16. -
200-node soft cap: Architecture specifies a 200-node soft cap for v1. Layout should still work beyond 200 nodes but show a warning toast suggesting the user split the diagram.
-
Debounce layout recalculations (300ms): Per the epics file technical notes, rapid mutations should not trigger redundant layout calls.
-
Node position transitions: Smooth 200ms ease-out animation when nodes move to new positions after layout.
-
Component Structure: Feature code in
~/modules/diagram/, NOT co-located in route directories. Layout logic in~/modules/diagram/lib/, hook in~/modules/diagram/hooks/. -
Lean JSON data model: The stored graph data does NOT contain x/y positions for auto-laid-out nodes. Position is computed at render time by ELK. Only manual position overrides (from user drag, Story 2.9) are stored in the node data.
ELK.js Web Worker — Implementation Approach
Package: elkjs@0.11.0 (latest stable, September 2025)
Worker setup: Use elkjs/lib/elk.bundled.js inside a dedicated Web Worker file. The worker receives graph data via postMessage, runs elk.layout(), and returns the layouted graph.
Why elk.bundled.js in a custom worker vs workerFactory: The workerFactory approach with elk-api.js + elk-worker.min.js creates a nested worker (worker-in-worker) which has limited browser support and causes issues with some bundlers. Using elk.bundled.js in our own Web Worker is simpler and more reliable.
// elk-worker.ts — Web Worker file
import ELK from 'elkjs/lib/elk.bundled.js';
const elk = new ELK();
self.onmessage = async (event: MessageEvent) => {
try {
const result = await elk.layout(event.data);
self.postMessage({ type: 'result', graph: result });
} catch (error) {
self.postMessage({ type: 'error', message: String(error) });
}
};
Worker instantiation in the layout module:
// elk-layout.ts
const worker = new Worker(
new URL('./elk-worker.ts', import.meta.url),
{ type: 'module' }
);
This pattern works with Turbopack (Next.js 16's default bundler) which supports new URL('./file', import.meta.url) for Web Workers.
ELK Graph Building — buildElkGraph()
Converts the unified graph model to ELK's expected input format:
interface ElkLayoutOptions {
direction: 'DOWN' | 'RIGHT' | 'LEFT' | 'UP';
edgeRouting: 'ORTHOGONAL' | 'SPLINES' | 'POLYLINE';
nodeSpacing?: number; // default: 80
layerSpacing?: number; // default: 100
}
function buildElkGraph(
nodes: DiagramNode[],
edges: DiagramEdge[],
options: ElkLayoutOptions
): ElkNode {
return {
id: 'root',
layoutOptions: {
'elk.algorithm': 'layered',
'elk.direction': options.direction,
'elk.edgeRouting': options.edgeRouting,
'elk.spacing.nodeNode': String(options.nodeSpacing ?? 80),
'elk.layered.spacing.nodeNodeBetweenLayers': String(options.layerSpacing ?? 100),
'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF',
},
children: nodes.map(node => ({
id: node.id,
width: node.w ?? 150, // Use node's width hint or default
height: 50, // Default height (custom nodes will override in Stories 2.3-2.8)
})),
edges: edges.map(edge => ({
id: edge.id,
sources: [edge.from],
targets: [edge.to],
})),
};
}
Key points:
- Node
widthuses thewfield from the graph data model (Flexicar convention: only width is stored, height is derived from content) - Default width 150px, height 50px — these will be refined when custom node renderers are added (Stories 2.3-2.8)
- ELK layout options values must be strings
- Edge format uses
sources/targetsarrays (ELK extended format)
Position Resolution — resolvePositions()
Maps ELK output back to @xyflow/react node positions:
function resolvePositions(
elkGraph: ElkNode,
originalNodes: Node[]
): Node[] {
const positionMap = new Map<string, { x: number; y: number }>();
for (const child of elkGraph.children ?? []) {
if (child.x !== undefined && child.y !== undefined) {
positionMap.set(child.id, { x: child.x, y: child.y });
}
}
return originalNodes.map(node => {
// Skip nodes with manual position overrides
const hasManualPosition = (node.data as DiagramNode).position !== undefined;
if (hasManualPosition) return node;
const elkPos = positionMap.get(node.id);
if (!elkPos) return node;
return {
...node,
position: elkPos,
};
});
}
Coordinate system: For flat (non-hierarchical) graphs, ELK positions are already absolute. For BPMN compound graphs (Story 2.3), positions are relative to parent — that story will extend resolvePositions to handle compound layouts.
Smooth Node Animation
@xyflow/react does not have built-in animated position transitions. The approach:
- After ELK returns positions, update nodes via
setNodes()in the Zustand store - Apply CSS transition on the
.react-flow__nodeelements:
.react-flow__node {
transition: transform 200ms ease-out;
}
- The
transitionproperty animates the CSStransform: translate(x, y)that @xyflow/react uses for node positioning
IMPORTANT: Disable the CSS transition during user drag operations (Story 2.9) to prevent laggy drag behavior. The transition should only be active during layout animations.
Add to globals.css:
.react-flow__node.layouting {
transition: transform 200ms ease-out;
}
The layouting class is added temporarily during layout animation and removed after 200ms.
Layout Direction Controls in Status Bar
Add a dropdown to EditorStatusBar.tsx for layout direction:
[BPMN icon] BPMN | [3 nodes] | [DOWN ▼] [ORTHOGONAL ▼] | [zoom 100%]
Use shadcn/ui DropdownMenu (consistent with Story 1.4's pattern choice of DropdownMenu over ContextMenu).
Zustand Store Extensions
Add to useGraphStore:
interface GraphState {
// ... existing fields ...
layoutDirection: 'DOWN' | 'RIGHT' | 'LEFT' | 'UP';
edgeRouting: 'ORTHOGONAL' | 'SPLINES' | 'POLYLINE';
isLayouting: boolean;
setLayoutDirection: (direction: 'DOWN' | 'RIGHT' | 'LEFT' | 'UP') => void;
setEdgeRouting: (routing: 'ORTHOGONAL' | 'SPLINES' | 'POLYLINE') => void;
setIsLayouting: (isLayouting: boolean) => void;
}
Defaults: layoutDirection: 'DOWN', edgeRouting: 'ORTHOGONAL'.
If the diagram has meta.layoutDirection or meta.edgeRouting, use those values on initialization.
Existing Code to Reuse / Modify
| File | Action | What |
|---|---|---|
apps/web/src/modules/diagram/stores/useGraphStore.ts |
MODIFY | Add layoutDirection, edgeRouting, isLayouting state + setters |
apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx |
MODIFY | Wire useAutoLayout hook, add layouting CSS class logic |
apps/web/src/modules/diagram/components/editor/DiagramEditor.tsx |
READ | Understand initialization flow (graphData → graphToFlow → initializeFromGraphData) |
apps/web/src/modules/diagram/components/editor/EditorStatusBar.tsx |
MODIFY | Add layout direction and edge routing dropdowns |
apps/web/src/modules/diagram/types/graph.ts |
READ | DiagramNode, DiagramEdge, DiagramMeta types (layoutDirection/edgeRouting already defined in DiagramMeta) |
apps/web/src/modules/diagram/lib/graph-converter.ts |
READ | Understand how graphToFlow/flowToGraph bridge stored format and @xyflow format |
apps/web/src/assets/styles/globals.css |
MODIFY | Add layout animation CSS transition |
apps/web/package.json |
MODIFY | Add elkjs dependency |
Library & Framework Requirements
| Package | Version | Purpose | Install Command |
|---|---|---|---|
elkjs |
0.11.0 | Auto-layout engine (Sugiyama/layered algorithm) | pnpm --filter web add elkjs@0.11.0 |
Already installed:
@xyflow/react12.10.1 — canvas renderingzustand5.0.8 — state managementsonner2.0.7 — toast notifications for soft cap warning
NOT needed yet:
@liveblocks/*— Story 4.1@deepgram/sdk— Story 5.1
ELK.js 0.11.0 — Key Implementation Details
Package: elkjs (NOT elk, NOT @kieler/elkjs)
Import for Web Worker (bundled mode):
import ELK from 'elkjs/lib/elk.bundled.js';
Import for main thread API only (if using workerFactory):
import ELK from 'elkjs/lib/elk-api.js';
TypeScript types: ELK.js ships with types. Key types:
ElkNode— graph/node structure withid,children,edges,layoutOptions,x,y,width,heightElkEdge/ElkExtendedEdge— edge withsources/targetsarraysElkLabel— text labels on nodes/edgesLayoutOptions—Record<string, string>for ELK options
ELK graph input format:
{
id: "root",
layoutOptions: { 'elk.algorithm': 'layered', ... },
children: [
{ id: "n1", width: 150, height: 50 },
{ id: "n2", width: 150, height: 50 }
],
edges: [
{ id: "e1", sources: ["n1"], targets: ["n2"] }
]
}
ELK output: Same structure as input but with x/y added to each child and sections with startPoint/endPoint/bendPoints added to each edge.
All layout option values must be strings — e.g., '80' not 80.
License: EPL 2.0 — compatible with commercial SaaS (confirmed in architecture doc).
File Structure for This Story
New files:
apps/web/src/modules/diagram/
├── lib/
│ ├── elk-worker.ts # Web Worker running ELK layout
│ ├── elk-layout.ts # Layout engine: buildElkGraph, resolvePositions, computeLayout
│ ├── elk-layout.test.ts # Unit tests for layout functions
│ └── (existing) graph-converter.ts
├── hooks/
│ └── useAutoLayout.ts # React hook wrapping layout engine with debouncing
└── (existing) stores/
└── useGraphStore.ts # Modified: add layout state
Modified files:
apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx # Wire useAutoLayout
apps/web/src/modules/diagram/components/editor/EditorStatusBar.tsx # Add direction/routing dropdowns
apps/web/src/modules/diagram/stores/useGraphStore.ts # Add layout state
apps/web/src/assets/styles/globals.css # Add layout animation CSS
apps/web/package.json # Add elkjs dependency
pnpm-lock.yaml # Updated lockfile
Project Structure Notes
- Layout logic (
elk-layout.ts,elk-worker.ts) goes in~/modules/diagram/lib/— utilities/engines - The hook (
useAutoLayout.ts) goes in~/modules/diagram/hooks/— new directory for diagram-specific hooks (forward-compatible withuseDeepgramSTT,useAIStream, etc. from future epics) - Web Worker file at
elk-worker.ts— Turbopack resolvesnew URL('./elk-worker.ts', import.meta.url)at build time - Tests co-located:
elk-layout.test.tsnext toelk-layout.ts
Anti-Patterns to Avoid
- NEVER run ELK on the main thread — always via Web Worker. The
elk.bundled.jsimport must be inside the worker file, not the main thread module - NEVER use
elk-api.js+workerFactorywith nested workers — this pattern has compatibility issues with Turbopack. Useelk.bundled.jsin a custom worker instead - NEVER hardcode node dimensions — use the
wfield from DiagramNode when available, fall back to defaults. Custom node renderers (Stories 2.3-2.8) will provide accurate dimensions - NEVER store ELK-computed positions in the persisted graph data — positions are ephemeral and recomputed on load. Only manual overrides (from user drag) should be stored
- NEVER trigger layout synchronously — always debounce (300ms) and run async via the Web Worker
- NEVER apply CSS transitions to
.react-flow__nodeduring drag — only during layout animation (use the.layoutingclass toggle) - 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/ - DO NOT implement BPMN compound layout (pools/lanes) — that's Story 2.3. This story handles flat graph layout only
- DO NOT implement Liveblocks CRDT — that's Story 4.1
- DO NOT implement manual node repositioning persistence — that's Story 2.9. Just support the
positionfield check to skip nodes with manual overrides - DO NOT break existing tests — run full test suite after changes
Previous Story Intelligence (Story 2.1)
Key learnings to carry forward:
useGraphStoremanages nodes, edges, viewport asNode[]/Edge[]from @xyflow/react — layout results must produceNode[]updates compatible with this storegraphToFlow()converts stored DiagramNode (withfrom/toedges) to @xyflow format (withsource/target) — ELK works with the stored format (from/to→sources/targets), then positions map back to @xyflow format- DiagramCanvas uses
ReactFlowProviderwrappingReactFlow— any hooks usinguseReactFlow()must be inside this provider initializeFromGraphData(nodes, edges)is called inDiagramEditor.tsxuseEffect — layout should trigger AFTER this initializationcolorMode="system"is set on ReactFlow — dark mode works automaticallynodeTypesobject is defined OUTSIDE the component (performance critical) — any new node types must follow this pattern- 166 tests currently pass (141 original + 25 from Story 2.1) — don't break them
sonnertoast is used for user feedback — use it for the 200-node soft cap warningDiagramMetaalready haslayoutDirectionandedgeRoutingfields — read these from diagram data on initialization- Code review fix from 2.1:
vitest.config.tsextends@turbostarter/vitest-config/baseviamergeConfig()
Git Intelligence
Recent commits:
5033109 feat: implement Story 2.1 — canvas workspace with @xyflow/react and unified graph model098f496 feat: implement Story 1.4 — recent view and drag-and-drop organization
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
DropdownMenufrom shadcn/ui for selection controlsdiagramTypeConfigobject for diagram type metadata
Latest Tech Information
elkjs 0.11.0 (latest stable, September 2025):
- Based on ELK 0.11.0 layout framework
- Ships with TypeScript types (
ElkNode,ElkEdge,LayoutOptions) - Three builds:
elk.bundled.js(single file, main thread or worker),elk-api.js(API only),elk-worker.min.js(layout algorithm) elk.layout(graph)returns a Promise that resolves to the same graph structure with computed positions- All layout option values must be strings
- License: EPL 2.0 (safe for commercial SaaS)
Next.js 16 + Turbopack Web Worker support:
- Turbopack supports
new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' })syntax - Worker files are bundled separately at build time
- TypeScript workers work directly — no need for separate tsconfig or compilation step
@xyflow/react 12.10.1 node positioning:
- Nodes use
position: { x, y }for absolute positioning node.measured.width/node.measured.heightfor actual DOM dimensions (after rendering)setNodes()triggers re-render with new positions- No built-in animation for position changes — must use CSS transitions
fitView()available viauseReactFlow()hook to auto-zoom to fit all nodes after layout
References
- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.2] — Full AC and technical notes
- [Source: _bmad-output/planning-artifacts/architecture.md#Decision 1] — Unified Graph Data Model (hybrid schema, no x/y stored)
- [Source: _bmad-output/planning-artifacts/architecture.md#Flexicar Prototype] — Lean JSON data model, ELK.js patterns from Flexicar
- [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/epics.md#NFR2] — ELK layout < 500ms for < 50 nodes
- [Source: _bmad-output/implementation-artifacts/2-1-canvas-workspace-with-xyflow-react-and-unified-graph-model.md] — Previous story: Zustand store shape, graph converter, DiagramCanvas structure
- [Source: _bmad-output/project-context.md] — 62 critical implementation rules
- [Source: apps/web/src/modules/diagram/types/graph.ts] — DiagramMeta.layoutDirection and DiagramMeta.edgeRouting types
- [Source: apps/web/src/modules/diagram/stores/useGraphStore.ts] — Current store shape to extend
- [Source: apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx] — Canvas to integrate layout hook
- [Source: apps/web/src/modules/diagram/components/editor/EditorStatusBar.tsx] — Status bar to add direction controls
- [Source: elkjs@0.11.0 npm] — Latest stable version, Web Worker support
- [Source: eclipse.dev/elk/reference/algorithms/org-eclipse-elk-layered.html] — Layered algorithm options
- [Source: reactflow.dev/examples/layout/elkjs] — Official @xyflow/react + ELK example
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (claude-opus-4-6)
Debug Log References
- Fixed
pnpm installfailure: sherif workspace lint required alphabetically sorted devDependencies inapps/web/package.json. Moved@turbostarter/vitest-configandvitestto correct alphabetical positions. - Used
elk.bundled.jsin custom Web Worker instead ofworkerFactorypattern (nested workers have compatibility issues with Turbopack). triggerLayoutexposed viauseAutoLayouthook return value (not on store) since it depends on React context (useReactFlow).
Completion Notes List
- All 6 tasks complete with 50 tests passing (18 elk-layout + 15 graph-converter + 17 store)
- TypeScript typecheck passes clean
- Layout animation uses CSS class toggling (
.layoutingclass) — only active during layout, not during drag - Worker singleton pattern with
getWorker()/terminateWorker()for lifecycle management - DiagramEditor initializes layout settings from
graphData.meta.layoutDirectionandgraphData.meta.edgeRouting - Task 4.2 deviation:
triggerLayoutreturned from hook, not exposed on store — cleaner since it needs React context
Code Review Fixes Applied (Claude Opus 4.6)
- H1 (Race condition): Added single-flight pattern to
computeLayout— cancels in-flight layout before starting a new one viapendingReject+settledflag - H2 (Performance): Replaced
nodes/edgessubscriptions inuseAutoLayoutwithnodeCountselector; removed unusededgessubscription - H3 (AC#2 loading indicator): Loading indicator now only appears after 200ms delay via
setTimeout, matching AC#2 requirement - M1 (Manual override): Changed
resolvePositionsto checkdata.manuallyPositionedflag instead ofdata.position !== undefined; addedmanuallyPositioned?: booleanto DiagramNode type - M2 (Timeout): Added 10s timeout to
computeLayoutPromise — rejects with "ELK layout timed out" if worker hangs - M3 (Test quality): Added test for
manuallyPositionedflag behavior + test provingpositionfield alone does NOT trigger skip - L1/L2 (Low): Accepted as-is — DOM class toggling is the recommended @xyflow pattern; eslint-disable is safe with stable Zustand selectors
File List
New files:
apps/web/src/modules/diagram/lib/elk-worker.ts— Web Worker running ELK layout engineapps/web/src/modules/diagram/lib/elk-layout.ts— Layout module: buildElkGraph, resolvePositions, computeLayout, worker managementapps/web/src/modules/diagram/lib/elk-layout.test.ts— 18 unit tests for layout functionsapps/web/src/modules/diagram/hooks/useAutoLayout.ts— React hook with debouncing, animation, soft cap warning
Modified files:
apps/web/package.json— Addedelkjs@0.11.0dependency, fixed devDependencies sort orderapps/web/src/modules/diagram/types/graph.ts— AddedmanuallyPositioned?: booleanto DiagramNode (code review fix M1)apps/web/src/modules/diagram/stores/useGraphStore.ts— Added layoutDirection, edgeRouting, isLayouting state + settersapps/web/src/modules/diagram/stores/useGraphStore.test.ts— Added 7 layout state testsapps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx— Wired useAutoLayout hook, added loading Panelapps/web/src/modules/diagram/components/editor/EditorStatusBar.tsx— Added direction/routing dropdown controlsapps/web/src/modules/diagram/components/editor/DiagramEditor.tsx— Initialize layout settings from diagram metadataapps/web/src/assets/styles/globals.css— Added.react-flow__node.layoutinganimation CSSpnpm-lock.yaml— Updated lockfile