Skip to content

Commit c684542

Browse files
committed
feat: default to expanded algorithm overview and tighten node layout
- Auto-select algorithm-overview with all steps shown on initial load - Selecting algorithm-overview always shows it fully expanded - Reduce node padding, spacing, and dimensions for tighter layout - Increase label text size to 13px for better readability - Add ResizeObserver mock for test compatibility - Update store tests for new default state
1 parent 6e1b8dd commit c684542

6 files changed

Lines changed: 61 additions & 27 deletions

File tree

src/components/FlowCanvas.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ const nodeTypes = {
1919
};
2020

2121
// Node dimensions for layout calculation
22-
const NODE_WIDTH = 200;
23-
const NODE_HEIGHT = 60;
22+
const NODE_WIDTH = 180;
23+
const NODE_HEIGHT = 44;
2424

2525
// Layout options for different modes
2626
interface LayoutOptions {
@@ -42,8 +42,8 @@ function getLayoutedElements(
4242
rankdir: options.direction,
4343
nodesep: options.nodesep,
4444
ranksep: options.ranksep,
45-
marginx: 50,
46-
marginy: 50,
45+
marginx: 30,
46+
marginy: 30,
4747
ranker: 'tight-tree', // Better for complex graphs
4848
});
4949

@@ -207,8 +207,8 @@ export function FlowCanvas() {
207207

208208
// Apply dagre layout with different settings for overview
209209
const layoutOptions: LayoutOptions = isOverview
210-
? { direction: 'TB', nodesep: 100, ranksep: 100, isOverview: true }
211-
: { direction: 'TB', nodesep: 60, ranksep: 80 };
210+
? { direction: 'TB', nodesep: 60, ranksep: 60, isOverview: true }
211+
: { direction: 'TB', nodesep: 40, ranksep: 50 };
212212

213213
return getLayoutedElements(nodeArray, edgeList, layoutOptions);
214214
}, [executionLog, currentStep, selectedNodeId, isOverview]);

src/components/FlowNode.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ function FlowNode({ data, selected }: FlowNodeProps) {
9292
return (
9393
<div
9494
className={clsx(
95-
'px-4 py-3 rounded-lg border-2 min-w-[180px] max-w-[240px] transition-all duration-300',
95+
'px-3 py-2 rounded-lg border-2 min-w-[160px] max-w-[220px] transition-all duration-300',
9696
colorClass,
9797
statusClasses[data.status],
9898
selected && 'ring-2 ring-[#58a6ff] ring-offset-2 ring-offset-[#0d1117]',
@@ -101,26 +101,27 @@ function FlowNode({ data, selected }: FlowNodeProps) {
101101
<Handle
102102
type="target"
103103
position={Position.Top}
104-
className="!bg-[#30363d] !border-[#58a6ff] !w-3 !h-3"
104+
className="!bg-[#30363d] !border-[#58a6ff] !w-2.5 !h-2.5"
105105
/>
106106
<div className="flex items-center gap-2">
107107
<Icon
108-
size={18}
108+
size={16}
109109
className={clsx(
110+
'flex-none',
110111
data.status === 'active' && 'text-green-400',
111112
data.status === 'error' && 'text-red-400',
112113
data.status === 'completed' && 'text-[#c9d1d9]',
113114
)}
114115
/>
115-
<span className="text-sm font-medium truncate">{data.label}</span>
116+
<span className="text-[13px] font-medium leading-tight">{data.label}</span>
116117
</div>
117118
{data.description && data.status === 'active' && (
118119
<p className="text-xs text-[#8b949e] mt-1 truncate">{data.description}</p>
119120
)}
120121
<Handle
121122
type="source"
122123
position={Position.Bottom}
123-
className="!bg-[#30363d] !border-[#58a6ff] !w-3 !h-3"
124+
className="!bg-[#30363d] !border-[#58a6ff] !w-2.5 !h-2.5"
124125
/>
125126
</div>
126127
);

src/hooks/useScenarioFromUrl.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ export function useScenarioFromUrl() {
88
const selectScenario = useVisualizerStore((s) => s.selectScenario);
99
const scenarios = useVisualizerStore((s) => s.scenarios);
1010

11-
// On mount: read scenario from URL
11+
// On mount: read scenario from URL (skip if already selected)
1212
useEffect(() => {
1313
const scenarioId = searchParams.get('scenario');
14-
if (scenarioId && scenarios.some((s) => s.id === scenarioId)) {
14+
if (
15+
scenarioId &&
16+
scenarioId !== currentScenario?.id &&
17+
scenarios.some((s) => s.id === scenarioId)
18+
) {
1519
selectScenario(scenarioId);
1620
}
1721
// Only run on mount

src/store.test.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,41 @@ describe('useVisualizerStore', () => {
55
useVisualizerStore.setState(useVisualizerStore.getInitialState());
66
});
77

8-
it('initializes with correct defaults', () => {
8+
it('initializes with algorithm-overview selected and fully expanded', () => {
99
const state = useVisualizerStore.getState();
10-
expect(state.currentScenario).toBeNull();
10+
expect(state.currentScenario).not.toBeNull();
11+
expect(state.currentScenario!.id).toBe('algorithm-overview');
1112
expect(state.scenarios.length).toBeGreaterThan(0);
12-
expect(state.currentStep).toBe(-1);
13-
expect(state.playbackState).toBe('idle');
13+
expect(state.currentStep).toBe(state.currentScenario!.steps.length - 1);
14+
expect(state.playbackState).toBe('finished');
1415
expect(state.playbackSpeed).toBe(1000);
1516
});
1617

1718
it('selects a scenario by id', () => {
1819
const state = useVisualizerStore.getState();
19-
const firstScenario = state.scenarios[0];
20+
// Select a non-overview scenario
21+
const scenario = state.scenarios.find((s) => s.id === 'simple-read')!;
2022

21-
state.selectScenario(firstScenario.id);
23+
state.selectScenario(scenario.id);
2224

2325
const updated = useVisualizerStore.getState();
2426
expect(updated.currentScenario).not.toBeNull();
25-
expect(updated.currentScenario!.id).toBe(firstScenario.id);
27+
expect(updated.currentScenario!.id).toBe('simple-read');
2628
expect(updated.currentStep).toBe(-1);
2729
expect(updated.playbackState).toBe('idle');
2830
});
31+
32+
it('selects algorithm-overview fully expanded', () => {
33+
const state = useVisualizerStore.getState();
34+
35+
// First select a different scenario
36+
state.selectScenario('simple-read');
37+
// Then select overview
38+
useVisualizerStore.getState().selectScenario('algorithm-overview');
39+
40+
const updated = useVisualizerStore.getState();
41+
expect(updated.currentScenario!.id).toBe('algorithm-overview');
42+
expect(updated.currentStep).toBe(updated.currentScenario!.steps.length - 1);
43+
expect(updated.playbackState).toBe('finished');
44+
});
2945
});

src/store.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,16 @@ const defaultSandbox: SandboxConfig = {
9191

9292
const initialNodes = new Map<string, AlgorithmNode>();
9393

94+
// Default to algorithm-overview with all steps shown
95+
const defaultScenario = scenarios.find((s) => s.id === 'algorithm-overview')!;
96+
9497
export const useVisualizerStore = create<VisualizerState>((set, get) => ({
95-
currentScenario: null,
98+
currentScenario: defaultScenario,
9699
scenarios,
97100
nodes: initialNodes,
98-
currentStep: -1,
99-
executionLog: [],
100-
playbackState: 'idle',
101+
currentStep: defaultScenario.steps.length - 1,
102+
executionLog: defaultScenario.steps,
103+
playbackState: 'finished' as PlaybackState,
101104
playbackSpeed: 1000,
102105
sandbox: defaultSandbox,
103106
selectedNodeId: null,
@@ -115,12 +118,15 @@ export const useVisualizerStore = create<VisualizerState>((set, get) => ({
115118
...scenario.sandboxOverrides,
116119
};
117120

121+
// Algorithm overview starts fully expanded
122+
const isOverview = scenarioId === 'algorithm-overview';
123+
118124
set({
119125
currentScenario: scenario,
120126
nodes,
121-
currentStep: -1,
122-
executionLog: [],
123-
playbackState: 'idle',
127+
currentStep: isOverview ? scenario.steps.length - 1 : -1,
128+
executionLog: isOverview ? scenario.steps : [],
129+
playbackState: isOverview ? 'finished' : 'idle',
124130
sandbox,
125131
selectedNodeId: null,
126132
});

src/test/setup.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import '@testing-library/jest-dom/vitest';
22

3+
// Mock ResizeObserver for jsdom (required by @xyflow/react)
4+
window.ResizeObserver = class ResizeObserver {
5+
observe() {}
6+
unobserve() {}
7+
disconnect() {}
8+
} as unknown as typeof ResizeObserver;
9+
310
// Mock window.matchMedia for jsdom
411
Object.defineProperty(window, 'matchMedia', {
512
writable: true,

0 commit comments

Comments
 (0)