Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 5a5e326

Browse filesBrowse files
improvement: add Classic/Freehand scroll mode toggle to canvas (#669)
- Added ScrollModeToggle component with Radix UI Switch (same design as InteractionModeToggle) - Classic mode: scroll wheel zooms (default ReactFlow behavior) - Freehand mode: scroll wheel pans canvas (Miro/Figma-style), Ctrl+scroll to zoom - Setting persists via localStorage - Applied to both WorkflowEditor and SubAgentFlowDialog canvases Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e99e624 commit 5a5e326
Copy full SHA for 5a5e326

10 files changed

+200Lines changed: 200 additions & 0 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file
+155Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/**
2+
* Claude Code Workflow Studio - Scroll Mode Toggle Component
3+
*
4+
* Canvas scroll mode toggle (classic/freehand)
5+
*/
6+
7+
import * as Switch from '@radix-ui/react-switch';
8+
import { Move, ZoomIn } from 'lucide-react';
9+
import type React from 'react';
10+
import { useTranslation } from '../i18n/i18n-context';
11+
import { useWorkflowStore } from '../stores/workflow-store';
12+
import { StyledTooltipItem, StyledTooltipProvider } from './common/StyledTooltip';
13+
14+
/**
15+
* ScrollModeToggle Component
16+
*
17+
* Provides UI to switch between classic (scroll = zoom) and freehand (scroll = pan) modes
18+
*/
19+
export const ScrollModeToggle: React.FC = () => {
20+
const { t } = useTranslation();
21+
const { scrollMode, toggleScrollMode } = useWorkflowStore();
22+
23+
return (
24+
<StyledTooltipProvider>
25+
<div
26+
style={{
27+
display: 'flex',
28+
alignItems: 'center',
29+
gap: '6px',
30+
backgroundColor: 'var(--vscode-editor-background)',
31+
border: '1px solid var(--vscode-panel-border)',
32+
borderRadius: '20px',
33+
padding: '4px 6px',
34+
opacity: 0.85,
35+
}}
36+
>
37+
{/* Classic Mode Icon (Left) */}
38+
<StyledTooltipItem content={t('toolbar.scrollMode.switchToClassic')}>
39+
<div
40+
onClick={() => {
41+
if (scrollMode !== 'classic') {
42+
toggleScrollMode();
43+
}
44+
}}
45+
onKeyDown={(e) => {
46+
if ((e.key === 'Enter' || e.key === ' ') && scrollMode !== 'classic') {
47+
e.preventDefault();
48+
toggleScrollMode();
49+
}
50+
}}
51+
role="button"
52+
tabIndex={scrollMode === 'classic' ? -1 : 0}
53+
aria-label={t('toolbar.scrollMode.switchToClassic')}
54+
style={{
55+
display: 'flex',
56+
alignItems: 'center',
57+
justifyContent: 'center',
58+
width: '20px',
59+
height: '20px',
60+
borderRadius: '50%',
61+
backgroundColor:
62+
scrollMode === 'classic' ? 'var(--vscode-badge-background)' : 'transparent',
63+
transition: 'background-color 150ms',
64+
cursor: scrollMode === 'classic' ? 'default' : 'pointer',
65+
}}
66+
>
67+
<ZoomIn
68+
size={12}
69+
style={{
70+
color:
71+
scrollMode === 'classic'
72+
? 'var(--vscode-badge-foreground)'
73+
: 'var(--vscode-disabledForeground)',
74+
}}
75+
/>
76+
</div>
77+
</StyledTooltipItem>
78+
79+
{/* Switch */}
80+
<Switch.Root
81+
checked={scrollMode === 'freehand'}
82+
onCheckedChange={toggleScrollMode}
83+
aria-label="Canvas scroll mode"
84+
style={{
85+
all: 'unset',
86+
width: '32px',
87+
height: '18px',
88+
backgroundColor: 'var(--vscode-input-background)',
89+
borderRadius: '9px',
90+
position: 'relative',
91+
border: '1px solid var(--vscode-input-border)',
92+
cursor: 'pointer',
93+
}}
94+
>
95+
<Switch.Thumb
96+
style={{
97+
all: 'unset',
98+
display: 'block',
99+
width: '14px',
100+
height: '14px',
101+
backgroundColor: 'var(--vscode-button-background)',
102+
borderRadius: '7px',
103+
transition: 'transform 100ms',
104+
transform: scrollMode === 'freehand' ? 'translateX(16px)' : 'translateX(2px)',
105+
willChange: 'transform',
106+
margin: '1px',
107+
}}
108+
/>
109+
</Switch.Root>
110+
111+
{/* Freehand Mode Icon (Right) */}
112+
<StyledTooltipItem content={t('toolbar.scrollMode.switchToFreehand')}>
113+
<div
114+
onClick={() => {
115+
if (scrollMode !== 'freehand') {
116+
toggleScrollMode();
117+
}
118+
}}
119+
onKeyDown={(e) => {
120+
if ((e.key === 'Enter' || e.key === ' ') && scrollMode !== 'freehand') {
121+
e.preventDefault();
122+
toggleScrollMode();
123+
}
124+
}}
125+
role="button"
126+
tabIndex={scrollMode === 'freehand' ? -1 : 0}
127+
aria-label={t('toolbar.scrollMode.switchToFreehand')}
128+
style={{
129+
display: 'flex',
130+
alignItems: 'center',
131+
justifyContent: 'center',
132+
width: '20px',
133+
height: '20px',
134+
borderRadius: '50%',
135+
backgroundColor:
136+
scrollMode === 'freehand' ? 'var(--vscode-badge-background)' : 'transparent',
137+
transition: 'background-color 150ms',
138+
cursor: scrollMode === 'freehand' ? 'default' : 'pointer',
139+
}}
140+
>
141+
<Move
142+
size={12}
143+
style={{
144+
color:
145+
scrollMode === 'freehand'
146+
? 'var(--vscode-badge-foreground)'
147+
: 'var(--vscode-disabledForeground)',
148+
}}
149+
/>
150+
</div>
151+
</StyledTooltipItem>
152+
</div>
153+
</StyledTooltipProvider>
154+
);
155+
};
Collapse file

‎src/webview/src/components/WorkflowEditor.tsx‎

Copy file name to clipboardExpand all lines: src/webview/src/components/WorkflowEditor.tsx
+8Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import ReactFlow, {
1919
type Node,
2020
type NodeTypes,
2121
Panel,
22+
PanOnScrollMode,
2223
} from 'reactflow';
2324
import { CURRENT_ANNOUNCEMENT, cleanupDismissedAnnouncements } from '../constants/announcements';
2425
import { useAutoFocusNode } from '../hooks/useAutoFocusNode';
@@ -46,6 +47,7 @@ import { StartNode } from './nodes/StartNode';
4647
import { SubAgentFlowNodeComponent } from './nodes/SubAgentFlowNode';
4748
import { SubAgentNodeComponent } from './nodes/SubAgentNode';
4849
import { SwitchNodeComponent } from './nodes/SwitchNode';
50+
import { ScrollModeToggle } from './ScrollModeToggle';
4951

5052
/**
5153
* Node types registration (memoized outside component for performance)
@@ -122,6 +124,7 @@ export const WorkflowEditor: React.FC<WorkflowEditorProps> = ({
122124
syncSelectedNodeId,
123125
selectedNodeId,
124126
interactionMode,
127+
scrollMode,
125128
onNodeDragStop,
126129
isHighlightEnabled,
127130
toggleHighlightEnabled,
@@ -338,6 +341,10 @@ export const WorkflowEditor: React.FC<WorkflowEditorProps> = ({
338341
snapGrid={snapGrid}
339342
panOnDrag={panOnDrag}
340343
selectionOnDrag={selectionOnDrag}
344+
panOnScroll={scrollMode === 'freehand'}
345+
panOnScrollMode={PanOnScrollMode.Free}
346+
zoomOnScroll={scrollMode === 'classic'}
347+
zoomOnPinch={true}
341348
fitView
342349
attributionPosition="bottom-left"
343350
>
@@ -396,6 +403,7 @@ export const WorkflowEditor: React.FC<WorkflowEditorProps> = ({
396403
{/* Interaction Mode Toggle & Edge Animation Toggle */}
397404
<Panel position="top-left">
398405
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
406+
<ScrollModeToggle />
399407
<InteractionModeToggle />
400408
<StyledTooltipProvider>
401409
<StyledTooltipItem
Collapse file

‎src/webview/src/components/dialogs/SubAgentFlowDialog.tsx‎

Copy file name to clipboardExpand all lines: src/webview/src/components/dialogs/SubAgentFlowDialog.tsx
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import ReactFlow, {
2121
type Node,
2222
type NodeTypes,
2323
Panel,
24+
PanOnScrollMode,
2425
ReactFlowProvider,
2526
} from 'reactflow';
2627
import { useAutoFocusNode } from '../../hooks/useAutoFocusNode';
@@ -110,6 +111,7 @@ const SubAgentFlowDialogContent: React.FC<SubAgentFlowDialogProps> = ({ isOpen,
110111
onEdgesChange,
111112
onConnect,
112113
interactionMode,
114+
scrollMode,
113115
activeSubAgentFlowId,
114116
subAgentFlows,
115117
updateSubAgentFlow,
@@ -639,6 +641,10 @@ const SubAgentFlowDialogContent: React.FC<SubAgentFlowDialogProps> = ({ isOpen,
639641
snapGrid={snapGrid}
640642
panOnDrag={panOnDrag}
641643
selectionOnDrag={selectionOnDrag}
644+
panOnScroll={scrollMode === 'freehand'}
645+
panOnScrollMode={PanOnScrollMode.Free}
646+
zoomOnScroll={scrollMode === 'classic'}
647+
zoomOnPinch={true}
642648
fitView
643649
attributionPosition="bottom-left"
644650
>
Collapse file

‎src/webview/src/i18n/translation-keys.ts‎

Copy file name to clipboardExpand all lines: src/webview/src/i18n/translation-keys.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export interface WebviewTranslationKeys {
4343
'toolbar.edgeAnimation.disable': string;
4444
'toolbar.highlight.enable': string;
4545
'toolbar.highlight.disable': string;
46+
'toolbar.scrollMode.switchToClassic': string;
47+
'toolbar.scrollMode.switchToFreehand': string;
4648

4749
// Toolbar minimap toggle
4850
'toolbar.minimapToggle.show': string;
Collapse file

‎src/webview/src/i18n/translations/en.ts‎

Copy file name to clipboardExpand all lines: src/webview/src/i18n/translations/en.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export const enWebviewTranslations: WebviewTranslationKeys = {
4545
'toolbar.edgeAnimation.disable': 'Disable edge animation',
4646
'toolbar.highlight.enable': 'Enable group node highlight',
4747
'toolbar.highlight.disable': 'Disable group node highlight',
48+
'toolbar.scrollMode.switchToClassic': 'Switch to Classic mode (scroll = zoom)',
49+
'toolbar.scrollMode.switchToFreehand': 'Switch to Freehand mode (scroll = pan)',
4850

4951
// Toolbar minimap toggle
5052
'toolbar.minimapToggle.show': 'Show Minimap',
Collapse file

‎src/webview/src/i18n/translations/ja.ts‎

Copy file name to clipboardExpand all lines: src/webview/src/i18n/translations/ja.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export const jaWebviewTranslations: WebviewTranslationKeys = {
4545
'toolbar.edgeAnimation.disable': 'エッジアニメーションを無効化',
4646
'toolbar.highlight.enable': 'グループノードハイライトを有効化',
4747
'toolbar.highlight.disable': 'グループノードハイライトを無効化',
48+
'toolbar.scrollMode.switchToClassic': 'Classicモードに切り替え(スクロール=ズーム)',
49+
'toolbar.scrollMode.switchToFreehand': 'Freehandモードに切り替え(スクロール=パン)',
4850

4951
// Toolbar minimap toggle
5052
'toolbar.minimapToggle.show': 'ミニマップを表示',
Collapse file

‎src/webview/src/i18n/translations/ko.ts‎

Copy file name to clipboardExpand all lines: src/webview/src/i18n/translations/ko.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export const koWebviewTranslations: WebviewTranslationKeys = {
4545
'toolbar.edgeAnimation.disable': '엣지 애니메이션 비활성화',
4646
'toolbar.highlight.enable': '그룹 노드 하이라이트 활성화',
4747
'toolbar.highlight.disable': '그룹 노드 하이라이트 비활성화',
48+
'toolbar.scrollMode.switchToClassic': 'Classic 모드로 전환 (스크롤 = 줌)',
49+
'toolbar.scrollMode.switchToFreehand': 'Freehand 모드로 전환 (스크롤 = 팬)',
4850

4951
// Toolbar minimap toggle
5052
'toolbar.minimapToggle.show': '미니맵 표시',
Collapse file

‎src/webview/src/i18n/translations/zh-CN.ts‎

Copy file name to clipboardExpand all lines: src/webview/src/i18n/translations/zh-CN.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export const zhCNWebviewTranslations: WebviewTranslationKeys = {
4545
'toolbar.edgeAnimation.disable': '禁用边动画',
4646
'toolbar.highlight.enable': '启用组节点高亮',
4747
'toolbar.highlight.disable': '禁用组节点高亮',
48+
'toolbar.scrollMode.switchToClassic': '切换到Classic模式(滚动=缩放)',
49+
'toolbar.scrollMode.switchToFreehand': '切换到Freehand模式(滚动=平移)',
4850

4951
// Toolbar minimap toggle
5052
'toolbar.minimapToggle.show': '显示迷你地图',
Collapse file

‎src/webview/src/i18n/translations/zh-TW.ts‎

Copy file name to clipboardExpand all lines: src/webview/src/i18n/translations/zh-TW.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export const zhTWWebviewTranslations: WebviewTranslationKeys = {
4545
'toolbar.edgeAnimation.disable': '停用邊動畫',
4646
'toolbar.highlight.enable': '啟用群組節點高亮',
4747
'toolbar.highlight.disable': '停用群組節點高亮',
48+
'toolbar.scrollMode.switchToClassic': '切換到Classic模式(滾動=縮放)',
49+
'toolbar.scrollMode.switchToFreehand': '切換到Freehand模式(滾動=平移)',
4850

4951
// Toolbar minimap toggle
5052
'toolbar.minimapToggle.show': '顯示迷你地圖',
Collapse file

‎src/webview/src/stores/workflow-store.ts‎

Copy file name to clipboardExpand all lines: src/webview/src/stores/workflow-store.ts
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ import { create } from 'zustand';
3434
*/
3535
export type InteractionMode = 'pan' | 'selection';
3636

37+
/**
38+
* Canvas scroll mode
39+
* - classic: Scroll wheel zooms (default ReactFlow behavior)
40+
* - freehand: Scroll wheel pans canvas (Miro/Figma-style), Ctrl+scroll to zoom
41+
*/
42+
export type ScrollMode = 'classic' | 'freehand';
43+
3744
/**
3845
* Snapshot of main workflow state for restoration after Sub-Agent Flow editing
3946
*/
@@ -53,6 +60,7 @@ interface WorkflowStore {
5360
pendingDeleteNodeIds: string[];
5461
activeWorkflow: Workflow | null;
5562
interactionMode: InteractionMode;
63+
scrollMode: ScrollMode;
5664
workflowName: string;
5765
workflowDescription: string;
5866
isPropertyOverlayOpen: boolean;
@@ -89,6 +97,7 @@ interface WorkflowStore {
8997
syncSelectedNodeId: (id: string | null) => void;
9098
setInteractionMode: (mode: InteractionMode) => void;
9199
toggleInteractionMode: () => void;
100+
toggleScrollMode: () => void;
92101
setWorkflowName: (name: string) => void;
93102
setWorkflowDescription: (description: string) => void;
94103
openPropertyOverlay: () => void;
@@ -293,6 +302,10 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
293302
pendingDeleteNodeIds: [],
294303
activeWorkflow: null,
295304
interactionMode: 'pan', // Default: pan mode
305+
scrollMode: (() => {
306+
const saved = localStorage.getItem('cc-wf-studio.scrollMode');
307+
return saved === 'freehand' ? 'freehand' : 'classic'; // Default: classic
308+
})() as ScrollMode,
296309
workflowName: 'my-workflow', // Default workflow name
297310
workflowDescription: '', // Default workflow description
298311
isPropertyOverlayOpen: true, // Property overlay is open by default
@@ -404,6 +417,12 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
404417
set({ interactionMode: currentMode === 'pan' ? 'selection' : 'pan' });
405418
},
406419

420+
toggleScrollMode: () => {
421+
const newMode = get().scrollMode === 'classic' ? 'freehand' : 'classic';
422+
localStorage.setItem('cc-wf-studio.scrollMode', newMode);
423+
set({ scrollMode: newMode });
424+
},
425+
407426
setWorkflowName: (workflowName) => set({ workflowName }),
408427

409428
setWorkflowDescription: (workflowDescription) => set({ workflowDescription }),

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.