diff --git a/studio/frontend/src/features/recipe-studio/components/controls/viewport-controls.tsx b/studio/frontend/src/features/recipe-studio/components/controls/viewport-controls.tsx index 0a0eff849b..e777826315 100644 --- a/studio/frontend/src/features/recipe-studio/components/controls/viewport-controls.tsx +++ b/studio/frontend/src/features/recipe-studio/components/controls/viewport-controls.tsx @@ -1,10 +1,18 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 -import { type ReactElement, useCallback } from "react"; -import { Lock, LockOpen, Maximize2, Minus, Plus } from "lucide-react"; -import { Panel, useReactFlow } from "@xyflow/react"; import { Button } from "@/components/ui/button"; +import { Panel, useReactFlow } from "@xyflow/react"; +import { + Focus, + Lock, + LockOpen, + Maximize2, + Minimize2, + Minus, + Plus, +} from "lucide-react"; +import { type ReactElement, useCallback } from "react"; import { buildFitViewOptions } from "../../utils/graph/fit-view"; import { RECIPE_FLOATING_ICON_BUTTON_CLASS } from "../recipe-floating-icon-button-class"; @@ -12,12 +20,16 @@ type ViewportControlsProps = { interactive: boolean; lockDisabled?: boolean; onToggleInteractive: () => void; + maximized: boolean; + onToggleMaximize: () => void; }; export function ViewportControls({ interactive, lockDisabled = false, onToggleInteractive, + maximized, + onToggleMaximize, }: ViewportControlsProps): ReactElement { const { zoomIn, zoomOut, fitView, getNodes } = useReactFlow(); @@ -61,9 +73,23 @@ export function ViewportControls({ size="icon" className={RECIPE_FLOATING_ICON_BUTTON_CLASS} onClick={handleFitView} - aria-label="Fit view" + aria-label="Center view" > - + + + + {maximized ? ( + + ) : ( + + )} - {interactive ? : } + {interactive ? ( + + ) : ( + + )} ); diff --git a/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx b/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx index 30302b86a7..f1f5e0958e 100644 --- a/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx +++ b/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx @@ -237,6 +237,7 @@ export function RecipeStudioPage({ }, [setActiveView]); const [processorsOpen, setProcessorsOpen] = useState(false); const [interactive, setInteractive] = useState(true); + const [maximized, setMaximized] = useState(false); const [runtimeIslandMinimized, setRuntimeIslandMinimized] = useState(false); const [recentCompletedExecution, setRecentCompletedExecution] = useState(null); @@ -569,6 +570,16 @@ export function RecipeStudioPage({ [reactFlowInstance], ); + const toggleMaximize = useCallback(() => { + // The maximized surface is a fixed z-50 overlay that already covers the + // app sidebar (z-10/z-20), so we don't touch the sidebar's own state — that + // state is persisted in pin mode and mutating it here would leak the + // temporary collapse into the next page/session. + setMaximized((prev) => !prev); + // Container size changes; refit once the layout settles. + scheduleFitView({ delayMs: TAB_SWITCH_FIT_DELAY_MS }); + }, [scheduleFitView]); + useEffect(() => { if ( previousActiveViewRef.current !== activeView && @@ -587,6 +598,15 @@ export function RecipeStudioPage({ } }, [activeView, reactFlowInstance]); + // The "Exit full view" control lives inside the editor canvas, which unmounts + // on other tabs. Drop full-view mode when leaving the editor so Easy/Runs + // aren't left under the fixed overlay. + useEffect(() => { + if (activeView !== "editor" && maximized) { + setMaximized(false); + } + }, [activeView, maximized]); + useEffect(() => { if ( !reactFlowInstance || @@ -732,6 +752,8 @@ export function RecipeStudioPage({ interactive={canvasInteractive} lockDisabled={executionLocked} onToggleInteractive={toggleInteractive} + maximized={maximized} + onToggleMaximize={toggleMaximize} /> {islandExecution && (isExecutionInProgress(islandExecution.status) || @@ -773,10 +795,25 @@ export function RecipeStudioPage({ } return ( - - + + {activeView === "easy" ? (