Recipe Studio: full-height canvas and in-app maximize control (#7394)
* studio recipes: full-height canvas and in-app maximize control - Recipe editor fills its container (drop the outer padding and the fixed 75vh height); the canvas reaches the window edges - Viewport controls: the fit button now reads as center (it always fit/centered); add an expand-to-full-view button that collapses the sidebar and maximizes the canvas in-app, toggling back to restore * recipe studio: exit full view when leaving the editor tab Addresses review: the Exit full view control lives inside the editor canvas, which unmounts on the Easy/Runs tabs. Clear maximized (and restore the sidebar) when activeView leaves "editor" so those views aren't left stuck under the fixed full-view overlay. * recipe studio: keep full view below titlebar and off the sidebar state
This commit is contained in:
parent
91a89806d7
commit
938e786eb9
2 changed files with 77 additions and 10 deletions
|
|
@ -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"
|
||||
>
|
||||
<Maximize2 className="size-4" />
|
||||
<Focus className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={RECIPE_FLOATING_ICON_BUTTON_CLASS}
|
||||
onClick={onToggleMaximize}
|
||||
aria-label={maximized ? "Exit full view" : "Expand to full view"}
|
||||
>
|
||||
{maximized ? (
|
||||
<Minimize2 className="size-4" />
|
||||
) : (
|
||||
<Maximize2 className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -74,7 +100,11 @@ export function ViewportControls({
|
|||
onClick={onToggleInteractive}
|
||||
aria-label={interactive ? "Lock interaction" : "Unlock interaction"}
|
||||
>
|
||||
{interactive ? <LockOpen className="size-4" /> : <Lock className="size-4" />}
|
||||
{interactive ? (
|
||||
<LockOpen className="size-4" />
|
||||
) : (
|
||||
<Lock className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</Panel>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<RecipeExecutionRecord | null>(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 (
|
||||
<div className="min-h-[calc(100dvh-var(--studio-titlebar-height,0px))] bg-background">
|
||||
<main className="w-full px-6 py-8">
|
||||
<div
|
||||
className={
|
||||
maximized
|
||||
? "fixed inset-x-0 bottom-0 z-50 flex flex-col bg-background"
|
||||
: "flex h-full min-h-0 flex-1 flex-col bg-background"
|
||||
}
|
||||
style={
|
||||
maximized
|
||||
? {
|
||||
// Start below the custom/mac window titlebar so the header and
|
||||
// its controls aren't hidden under (or click-blocked by) it.
|
||||
top: "var(--studio-non-chat-content-top-inset, var(--studio-content-top-inset, 0px))",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<main className="flex min-h-0 w-full flex-1 flex-col">
|
||||
<div
|
||||
className="relative w-full overflow-hidden rounded-2xl corner-squircle border"
|
||||
className="relative flex min-h-0 w-full flex-1 flex-col overflow-hidden border"
|
||||
ref={setSheetContainer}
|
||||
>
|
||||
<RecipeStudioHeader
|
||||
|
|
@ -794,7 +831,7 @@ export function RecipeStudioPage({
|
|||
}}
|
||||
/>
|
||||
<div
|
||||
className="h-[75vh] w-full rounded-t-none"
|
||||
className="flex min-h-0 w-full flex-1 rounded-t-none"
|
||||
ref={flowContainerRef}
|
||||
>
|
||||
{activeView === "easy" ? (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue