Files
lemonspace_app/docs/plans/2026-04-03-canvas-modularization.md

7.7 KiB

Canvas Modularization Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Split components/canvas/canvas.tsx into clear, testable domain hooks while preserving all current canvas behavior and public APIs.

Architecture: Keep components/canvas/canvas.tsx as the composition root and extract the largest logic clusters into domain-specific hooks. Start with sync and reconciliation because they are the most complex and most reusable, then move connection and drop flows, and only then extract a presentational view if it still adds value.

Tech Stack: Next.js 16, React 19, Convex, React Flow, TypeScript


Task 1: Lock in the current modularization seams

Files:

  • Modify: components/canvas/canvas.tsx
  • Reference: components/canvas/CLAUDE.md
  • Reference: docs/plans/2026-04-03-canvas-modularization-design.md

Step 1: Annotate the major responsibility boundaries inside components/canvas/canvas.tsx by grouping the existing code into the future modules: sync engine, reconciliation, interactions, connections, drop, and render.

Step 2: List the exact state, refs, callbacks, and mutations owned by each future hook in a scratch note or temporary checklist before moving code.

Step 3: Identify cross-cutting refs that must stay shared at the orchestrator level, especially optimistic ID handoff state and React Flow instance access.

Step 4: Remove the scratch note once the extraction map is clear.

Task 2: Extract pure helper logic from effect bodies first

Files:

  • Modify: components/canvas/canvas.tsx
  • Modify: components/canvas/canvas-helpers.ts
  • Create: components/canvas/canvas-flow-reconciliation-helpers.ts
  • Test: components/canvas/__tests__/canvas-flow-reconciliation-helpers.test.ts

Step 1: Move deterministic edge reconciliation helpers out of useLayoutEffect bodies into components/canvas/canvas-flow-reconciliation-helpers.ts.

Step 2: Move deterministic node reconciliation helpers out of useLayoutEffect bodies into components/canvas/canvas-flow-reconciliation-helpers.ts.

Step 3: Add tests for optimistic edge carry, endpoint remapping, and position pin cleanup.

Step 4: Update components/canvas/canvas.tsx to call the extracted helpers without changing behavior.

Task 3: Extract the sync engine hook

Files:

  • Create: components/canvas/use-canvas-sync-engine.ts
  • Modify: components/canvas/canvas.tsx
  • Reference: lib/canvas-op-queue.ts
  • Reference: lib/canvas-local-persistence.ts
  • Test: components/canvas/__tests__/use-canvas-sync-engine.test.ts

Step 1: Create useCanvasSyncEngine with the queue state, online state, and mutation wrappers that are currently centered around components/canvas/canvas.tsx:258 and components/canvas/canvas.tsx:984.

Step 2: Move deferred optimistic-node handling into the new hook, including pending resize, pending data, and pending move-after-create behavior.

Step 3: Return a clear API from the hook for create, move, resize, update-data, remove-edge, batch-remove, and split-edge flows.

Step 4: Replace the local inline sync logic in components/canvas/canvas.tsx with the hook.

Step 5: Add focused tests for optimistic create handoff and deferred resize/update behavior.

Task 4: Extract the flow reconciliation hook

Files:

  • Create: components/canvas/use-canvas-flow-reconciliation.ts
  • Modify: components/canvas/canvas.tsx
  • Modify: components/canvas/canvas-flow-reconciliation-helpers.ts
  • Test: components/canvas/__tests__/use-canvas-flow-reconciliation.test.ts

Step 1: Create useCanvasFlowReconciliation to own the current Convex-to-local useLayoutEffect synchronization for nodes and edges.

Step 2: Pass in only the data it needs: Convex nodes, Convex edges, storage URLs, theme mode, local setters, and shared optimistic refs.

Step 3: Keep drag-lock and resize-lock behavior unchanged while moving the effects.

Step 4: Add tests or targeted assertions for the drag-lock and optimistic carry behavior that are hardest to reason about from the UI.

Task 5: Extract node interaction logic

Files:

  • Create: components/canvas/use-canvas-node-interactions.ts
  • Modify: components/canvas/canvas.tsx
  • Reference: components/canvas/canvas-node-change-helpers.ts

Step 1: Move onNodesChange, edge intersection highlighting, drag start, drag, and drag stop into useCanvasNodeInteractions.

Step 2: Keep the returned API explicit: handlers plus any local highlight state helpers.

Step 3: Preserve all resize side effects and split-edge-on-drop behavior exactly.

Step 4: Manually verify drag, resize, and edge split interactions on the canvas page.

Task 6: Extract connection flows

Files:

  • Create: components/canvas/use-canvas-connections.ts
  • Modify: components/canvas/canvas.tsx
  • Reference: components/canvas/canvas-reconnect.ts
  • Reference: components/canvas/canvas-connection-drop-menu.tsx

Step 1: Move onConnect, onConnectEnd, connection drop menu state, and handleConnectionDropPick into useCanvasConnections.

Step 2: Keep validation centralized around validateCanvasConnection and validateCanvasConnectionByType.

Step 3: Reuse the existing reconnect hook by adapting it through the new connection hook rather than rewriting reconnect behavior.

Step 4: Manually verify valid connection creation, invalid connection rejection, reconnect, and connection-drop node creation.

Task 7: Extract drag-and-drop creation flows

Files:

  • Create: components/canvas/use-canvas-drop.ts
  • Modify: components/canvas/canvas.tsx
  • Reference: components/canvas/canvas-media-utils.ts

Step 1: Move onDragOver and onDrop into useCanvasDrop.

Step 2: Keep support for both sidebar/browser JSON payloads and raw node type strings.

Step 3: Preserve file upload drop behavior, including image dimension lookup and upload error toasts.

Step 4: Manually verify node drop and image file drop flows.

Task 8: Decide whether a presentational view extraction is still useful

Files:

  • Create: components/canvas/canvas-view.tsx
  • Modify: components/canvas/canvas.tsx

Step 1: Measure the remaining size and readability of components/canvas/canvas.tsx after hook extraction.

Step 2: If the file still mixes visual structure with too much prop assembly, move the JSX tree into components/canvas/canvas-view.tsx.

Step 3: Keep canvas-view.tsx presentational only; it should not own data-fetching, queue logic, or reconciliation effects.

Step 4: Skip this task entirely if the orchestrator is already clearly readable.

Task 9: Verify behavior after each extraction phase

Files:

  • Verify only

Step 1: Run pnpm lint components/canvas/canvas.tsx components/canvas/*.ts components/canvas/*.tsx or the project-equivalent lint command for touched files.

Step 2: Run pnpm tsc --noEmit or the project-equivalent type-check command.

Step 3: Run the targeted canvas tests added during the refactor.

Step 4: Manually verify these flows on the real canvas page: create node, drag node, resize node, connect nodes, reconnect edge, delete node, drop node from sidebar, and drop image file.

Task 10: Finish with small, reviewable commits

Files:

  • Commit only

Step 1: Commit after Task 3 with a message like refactor(canvas): extract sync engine hook.

Step 2: Commit after Task 4 with a message like refactor(canvas): extract flow reconciliation hook.

Step 3: Commit after Tasks 5 to 7 with messages scoped to interactions, connections, and drop handling.

Step 4: If Task 8 is done, commit it separately as refactor(canvas): extract canvas view.