From 895c848eaff3b4f98d1c43ac11bba767cc239be8 Mon Sep 17 00:00:00 2001 From: Willie Date: Sat, 14 Feb 2026 21:36:56 +1300 Subject: [PATCH] feat: add customizable canvas navigation settings (#65) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add customizable canvas navigation settings Add a new settings modal that allows users to configure canvas navigation preferences: - Pan Mode: Space + Drag (default), Middle Mouse, or Always On (ComfyUI-style) - Zoom Mode: Alt + Scroll (default), Ctrl + Scroll, or Scroll (no modifier) - Selection Mode: Click (default) or Alt + Drag Features: - Settings persist in localStorage - New canvas settings button in header (monitor icon) - Modal UI with radio button groups for each setting - React Flow props dynamically configured based on user preferences - Updated wheel event handler to respect zoom mode settings This addresses user requests for ComfyUI/TouchDesigner-style navigation where panning and zooming work without holding modifier keys. Co-Authored-By: Claude Sonnet 4.5 * fix: PR #65 review — altDrag selection bug, icon, dedup, tests - Fix altDrag selection mode: use selectionKeyCode="Alt" instead of selectionOnDrag=true which made drag-select work without any key - Replace misleading video camera icon with arrows-pointing-out for canvas navigation settings button - Extract duplicated settings buttons JSX into settingsButtons variable shared by both configured/unconfigured project branches - Add defensive useEffect to sync CanvasSettingsModal state on open - Add missing semicolon in types/index.ts canvas re-export - Add localStorage tests for getCanvasNavigationSettings and saveCanvasNavigationSettings - Add integration tests for updateCanvasNavigationSettings - Fix WorkflowCanvas test mocks to include canvasNavigationSettings Co-Authored-By: Claude Opus 4.6 * refactor: move canvas navigation settings into Project Settings modal Consolidate canvas settings from standalone CanvasSettingsModal into a new "Canvas" tab in ProjectSetupModal. Remove "like ComfyUI" text, add Shift+Drag selection mode, and fix save button to bottom of dialog. Co-Authored-By: Claude Opus 4.6 * fix: remove double semicolon in types/index.ts Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Sonnet 4.5 --- CLAUDE.md | 8 + src/components/Header.tsx | 81 ++++----- src/components/ProjectSetupModal.tsx | 155 +++++++++++++++++- src/components/WorkflowCanvas.tsx | 92 ++++++++--- .../__tests__/WorkflowCanvas.test.tsx | 4 + .../workflowStore.integration.test.ts | 33 ++++ .../utils/__tests__/localStorage.test.ts | 45 +++++ src/store/utils/localStorage.ts | 22 +++ src/store/workflowStore.ts | 18 ++ src/types/canvas.ts | 21 +++ src/types/index.ts | 1 + 11 files changed, 398 insertions(+), 82 deletions(-) create mode 100644 src/types/canvas.ts diff --git a/CLAUDE.md b/CLAUDE.md index 63eca49d..409912a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -210,6 +210,14 @@ All routes in `src/app/api/`: - `node-banana-workflow-costs` - Cost tracking per workflow - `node-banana-nanoBanana-defaults` - Sticky generation settings +## Git Workflow + +- The primary development branch is `develop`, NOT `main` or `master` +- Always checkout `develop` before creating feature branches: `git checkout develop` +- Create feature branches from `develop` using: `feature/` or `fix/` +- All PRs MUST target `develop`: use `gh pr create --base develop` +- Never push directly to `main`, `master`, or `develop` + ## Commits - The .planning directory is untracked, do not attempt to commit any changes to the files in this directory. diff --git a/src/components/Header.tsx b/src/components/Header.tsx index ff12ea13..aeb28f80 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -171,6 +171,35 @@ export function Header() { } }, [revertToSnapshot]); + const settingsButtons = ( +
+ +
+ ); + return ( <> - {/* Settings - separated */} - + {settingsButtons} ) : ( <> @@ -340,31 +345,7 @@ export function Header() { - {/* Settings - separated */} - + {settingsButtons} )} diff --git a/src/components/ProjectSetupModal.tsx b/src/components/ProjectSetupModal.tsx index f234080a..4f12c64a 100644 --- a/src/components/ProjectSetupModal.tsx +++ b/src/components/ProjectSetupModal.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from "react"; import { generateWorkflowId, useWorkflowStore } from "@/store/workflowStore"; import { ProviderType, ProviderSettings, NodeDefaultsConfig, LLMProvider, LLMModelType } from "@/types"; +import { CanvasNavigationSettings, PanMode, ZoomMode, SelectionMode } from "@/types/canvas"; import { EnvStatusResponse } from "@/app/api/env-status/route"; import { loadNodeDefaults, saveNodeDefaults } from "@/store/utils/localStorage"; import { ProviderModel } from "@/lib/providers/types"; @@ -94,10 +95,12 @@ export function ProjectSetupModal({ toggleProvider, maxConcurrentCalls, setMaxConcurrentCalls, + canvasNavigationSettings, + updateCanvasNavigationSettings, } = useWorkflowStore(); // Tab state - const [activeTab, setActiveTab] = useState<"project" | "providers" | "nodeDefaults">("project"); + const [activeTab, setActiveTab] = useState<"project" | "providers" | "nodeDefaults" | "canvas">("project"); // Project tab state const [name, setName] = useState(""); @@ -132,6 +135,9 @@ export function ProjectSetupModal({ const [showImageModelDialog, setShowImageModelDialog] = useState(false); const [showVideoModelDialog, setShowVideoModelDialog] = useState(false); + // Canvas tab state + const [localCanvasSettings, setLocalCanvasSettings] = useState(canvasNavigationSettings); + // Pre-fill when opening in settings mode useEffect(() => { if (isOpen) { @@ -169,13 +175,16 @@ export function ProjectSetupModal({ setShowImageModelDialog(false); setShowVideoModelDialog(false); + // Sync canvas settings + setLocalCanvasSettings(canvasNavigationSettings); + // Fetch env status fetch("/api/env-status") .then((res) => res.json()) .then((data: EnvStatusResponse) => setEnvStatus(data)) .catch(() => setEnvStatus(null)); } - }, [isOpen, mode, workflowName, saveDirectoryPath, useExternalImageStorage, providerSettings]); + }, [isOpen, mode, workflowName, saveDirectoryPath, useExternalImageStorage, providerSettings, canvasNavigationSettings]); const handleBrowse = async () => { setIsBrowsing(true); @@ -285,11 +294,18 @@ export function ProjectSetupModal({ onClose(); }; + const handleSaveCanvas = () => { + updateCanvasNavigationSettings(localCanvasSettings); + onClose(); + }; + const handleSave = () => { if (activeTab === "project") { handleSaveProject(); } else if (activeTab === "providers") { handleSaveProviders(); + } else if (activeTab === "canvas") { + handleSaveCanvas(); } else { handleSaveNodeDefaults(); } @@ -324,15 +340,16 @@ export function ProjectSetupModal({ return (
-

- {mode === "new" ? "New Project" : "Project Settings"} -

+
+

+ {mode === "new" ? "New Project" : "Project Settings"} +

- {/* Tab Bar */} -
+ {/* Tab Bar */} +
+ +
+ {/* Scrollable tab content area */} +
+ {/* Project Tab Content */} {activeTab === "project" && (
@@ -961,7 +988,117 @@ export function ProjectSetupModal({
)} -
+ {/* Canvas Tab Content */} + {activeTab === "canvas" && ( +
+ {/* Pan Mode */} +
+

Pan Mode

+
+ {([ + { value: "space" as PanMode, label: "Space + Drag", description: "Hold Space and drag to pan (default)" }, + { value: "middleMouse" as PanMode, label: "Middle Mouse", description: "Click and drag with middle mouse button" }, + { value: "always" as PanMode, label: "Always On", description: "Pan without holding any keys" }, + ] as const).map((option) => ( + + ))} +
+
+ + {/* Zoom Mode */} +
+

Zoom Mode

+
+ {([ + { value: "altScroll" as ZoomMode, label: "Alt + Scroll", description: "Hold Alt and scroll to zoom (default)" }, + { value: "ctrlScroll" as ZoomMode, label: "Ctrl + Scroll", description: "Hold Ctrl/Cmd and scroll to zoom" }, + { value: "scroll" as ZoomMode, label: "Scroll", description: "Scroll to zoom without holding any keys" }, + ] as const).map((option) => ( + + ))} +
+
+ + {/* Selection Mode */} +
+

Selection Mode

+
+ {([ + { value: "click" as SelectionMode, label: "Click", description: "Click to select nodes (default)" }, + { value: "altDrag" as SelectionMode, label: "Alt + Drag", description: "Hold Alt and drag to select multiple nodes" }, + { value: "shiftDrag" as SelectionMode, label: "Shift + Drag", description: "Hold Shift and drag to select multiple nodes" }, + ] as const).map((option) => ( + + ))} +
+
+
+ )} + +
+ + {/* Fixed footer */} +