feat(app): maximize the Explorer pane
Maximization isolates the existing Explorer split without entering workspace focus mode, and automatically restores the split if that pane disappears.
This commit is contained in:
@@ -34,4 +34,26 @@ describe("split focus root", () => {
|
||||
resolveSplitContainerRoot({ root, focusedPaneId: "right", focusModeEnabled: false }),
|
||||
).toEqual({ root, usesFallbackStrip: false });
|
||||
});
|
||||
|
||||
it("renders only a maximized pane without entering focus mode", () => {
|
||||
expect(
|
||||
resolveSplitContainerRoot({
|
||||
root,
|
||||
focusedPaneId: "left",
|
||||
focusModeEnabled: false,
|
||||
maximizedPaneId: "right",
|
||||
}),
|
||||
).toEqual({ root: pane("right"), usesFallbackStrip: false });
|
||||
});
|
||||
|
||||
it("keeps normal splits when the maximized pane is unavailable", () => {
|
||||
expect(
|
||||
resolveSplitContainerRoot({
|
||||
root,
|
||||
focusedPaneId: "left",
|
||||
focusModeEnabled: false,
|
||||
maximizedPaneId: "missing",
|
||||
}),
|
||||
).toEqual({ root, usesFallbackStrip: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,15 @@ export function resolveSplitContainerRoot(input: {
|
||||
root: SplitNode;
|
||||
focusedPaneId: string | null;
|
||||
focusModeEnabled: boolean | undefined;
|
||||
maximizedPaneId?: string | null;
|
||||
}): { root: SplitNode; usesFallbackStrip: boolean } {
|
||||
if (!input.focusModeEnabled) return { root: input.root, usesFallbackStrip: false };
|
||||
const focusedPane = input.focusedPaneId ? findPane(input.root, input.focusedPaneId) : null;
|
||||
if (!focusedPane) return { root: input.root, usesFallbackStrip: true };
|
||||
return { root: { kind: "pane", pane: focusedPane }, usesFallbackStrip: false };
|
||||
const isolatedPaneId = input.focusModeEnabled ? input.focusedPaneId : input.maximizedPaneId;
|
||||
if (!isolatedPaneId) return { root: input.root, usesFallbackStrip: false };
|
||||
const isolatedPane = findPane(input.root, isolatedPaneId);
|
||||
if (!isolatedPane || isolatedPane.hidden === true) {
|
||||
return { root: input.root, usesFallbackStrip: Boolean(input.focusModeEnabled) };
|
||||
}
|
||||
return { root: { kind: "pane", pane: isolatedPane }, usesFallbackStrip: false };
|
||||
}
|
||||
|
||||
function findPane(node: SplitNode, paneId: string): SplitPane | null {
|
||||
|
||||
@@ -76,6 +76,7 @@ import {
|
||||
} from "@/screens/workspace/workspace-tab-presentation";
|
||||
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
||||
import {
|
||||
findPaneById,
|
||||
useWorkspaceLayoutStore,
|
||||
type SplitNode,
|
||||
type SplitPane,
|
||||
@@ -85,6 +86,8 @@ import type { WorkspaceTab } from "@/workspace-tabs/model";
|
||||
import { RenderProfile } from "@/utils/render-profiler";
|
||||
import { workspaceTabTargetsEqual } from "@/workspace-tabs/identity";
|
||||
import { isNative } from "@/constants/platform";
|
||||
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
|
||||
|
||||
interface SplitContainerProps {
|
||||
layout: WorkspaceLayout;
|
||||
@@ -171,6 +174,9 @@ interface SplitNodeViewProps extends Omit<SplitContainerProps, "layout" | "onMov
|
||||
node: SplitNode;
|
||||
uiTabs: WorkspaceTab[];
|
||||
focusedPaneId: string | null;
|
||||
explorerPaneId: string | null;
|
||||
maximizedPaneId: string | null;
|
||||
onTogglePaneMaximized: (paneId: string) => void;
|
||||
activeDragTabId: string | null;
|
||||
showDropZones: boolean;
|
||||
dropPreview: SplitDropZoneHover | null;
|
||||
@@ -409,6 +415,61 @@ export function SplitContainer({
|
||||
const [activeDragTabId, setActiveDragTabId] = useState<string | null>(null);
|
||||
const [dropPreview, setDropPreview] = useState<SplitDropZoneHover | null>(null);
|
||||
const [tabDropPreview, setTabDropPreview] = useState<TabDropPreview | null>(null);
|
||||
const [maximizedPane, setMaximizedPane] = useState<{
|
||||
workspaceKey: string;
|
||||
paneId: string;
|
||||
} | null>(null);
|
||||
const maximizedPaneId =
|
||||
maximizedPane?.workspaceKey === workspaceKey ? maximizedPane.paneId : null;
|
||||
const explorerPaneId = useWorkspaceLayoutStore(
|
||||
(state) => state.explorerPaneIdByWorkspace[workspaceKey] ?? null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!maximizedPaneId) {
|
||||
return;
|
||||
}
|
||||
const isolatedPane = findPaneById(layout.root, maximizedPaneId);
|
||||
if (focusModeEnabled || !isolatedPane || isolatedPane.hidden === true) {
|
||||
setMaximizedPane(null);
|
||||
}
|
||||
}, [focusModeEnabled, layout.root, maximizedPaneId]);
|
||||
|
||||
const handleTogglePaneMaximized = useCallback(
|
||||
(paneId: string) => {
|
||||
setMaximizedPane((currentPane) =>
|
||||
currentPane?.workspaceKey === workspaceKey && currentPane.paneId === paneId
|
||||
? null
|
||||
: { workspaceKey, paneId },
|
||||
);
|
||||
},
|
||||
[workspaceKey],
|
||||
);
|
||||
|
||||
const handleExplorerMaximizeAction = useCallback(
|
||||
(action: KeyboardActionDefinition): boolean => {
|
||||
if (action.id !== "workspace.explorer.maximize.toggle") {
|
||||
return false;
|
||||
}
|
||||
const explorerPane = findPaneById(layout.root, explorerPaneId);
|
||||
if (focusModeEnabled || !explorerPane || explorerPane.hidden === true) {
|
||||
return true;
|
||||
}
|
||||
onFocusPane(explorerPane.id);
|
||||
handleTogglePaneMaximized(explorerPane.id);
|
||||
return true;
|
||||
},
|
||||
[explorerPaneId, focusModeEnabled, handleTogglePaneMaximized, layout.root, onFocusPane],
|
||||
);
|
||||
|
||||
useKeyboardActionHandler({
|
||||
handlerId: `workspace-explorer-maximize:${workspaceKey}`,
|
||||
actions: ["workspace.explorer.maximize.toggle"] as const,
|
||||
enabled: isWorkspaceFocused,
|
||||
priority: 100,
|
||||
isActive: () => true,
|
||||
handle: handleExplorerMaximizeAction,
|
||||
});
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
@@ -428,8 +489,9 @@ export function SplitContainer({
|
||||
root: layout.root,
|
||||
focusedPaneId: layout.focusedPaneId,
|
||||
focusModeEnabled,
|
||||
maximizedPaneId,
|
||||
}),
|
||||
[focusModeEnabled, layout.focusedPaneId, layout.root],
|
||||
[focusModeEnabled, layout.focusedPaneId, layout.root, maximizedPaneId],
|
||||
);
|
||||
const renderRoot = useMemo(() => wrapRootPaneForStableMount(splitRoot.root), [splitRoot.root]);
|
||||
|
||||
@@ -592,6 +654,9 @@ export function SplitContainer({
|
||||
workspaceKey={workspaceKey}
|
||||
uiTabs={uiTabs}
|
||||
focusedPaneId={layout.focusedPaneId}
|
||||
explorerPaneId={explorerPaneId}
|
||||
maximizedPaneId={maximizedPaneId}
|
||||
onTogglePaneMaximized={handleTogglePaneMaximized}
|
||||
normalizedServerId={normalizedServerId}
|
||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||
isWorkspaceFocused={isWorkspaceFocused}
|
||||
@@ -798,6 +863,9 @@ function SplitNodeView({
|
||||
workspaceKey,
|
||||
uiTabs,
|
||||
focusedPaneId,
|
||||
explorerPaneId,
|
||||
maximizedPaneId,
|
||||
onTogglePaneMaximized,
|
||||
normalizedServerId,
|
||||
normalizedWorkspaceId,
|
||||
isWorkspaceFocused,
|
||||
@@ -874,6 +942,9 @@ function SplitNodeView({
|
||||
pane={node.pane}
|
||||
uiTabs={uiTabs}
|
||||
isFocused={node.pane.id === focusedPaneId}
|
||||
explorerPaneId={explorerPaneId}
|
||||
maximizedPaneId={maximizedPaneId}
|
||||
onTogglePaneMaximized={onTogglePaneMaximized}
|
||||
normalizedServerId={normalizedServerId}
|
||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||
isWorkspaceFocused={isWorkspaceFocused}
|
||||
@@ -923,6 +994,9 @@ function SplitNodeView({
|
||||
workspaceKey={workspaceKey}
|
||||
uiTabs={uiTabs}
|
||||
focusedPaneId={focusedPaneId}
|
||||
explorerPaneId={explorerPaneId}
|
||||
maximizedPaneId={maximizedPaneId}
|
||||
onTogglePaneMaximized={onTogglePaneMaximized}
|
||||
normalizedServerId={normalizedServerId}
|
||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||
isWorkspaceFocused={isWorkspaceFocused}
|
||||
@@ -1014,6 +1088,9 @@ function SplitPaneView({
|
||||
showDropZones,
|
||||
dropPreview,
|
||||
tabDropPreview,
|
||||
explorerPaneId,
|
||||
maximizedPaneId,
|
||||
onTogglePaneMaximized,
|
||||
focusModeEnabled,
|
||||
onExitFocusMode,
|
||||
}: SplitPaneViewProps) {
|
||||
@@ -1114,6 +1191,10 @@ function SplitPaneView({
|
||||
},
|
||||
[onReorderTabsInPane, paneId],
|
||||
);
|
||||
const handleToggleMaximized = useCallback(
|
||||
() => onTogglePaneMaximized(paneId),
|
||||
[onTogglePaneMaximized, paneId],
|
||||
);
|
||||
return (
|
||||
<RenderProfile id={`SplitPaneView:${pane.id}`}>
|
||||
<View ref={paneRef} collapsable={false} style={styles.pane}>
|
||||
@@ -1121,7 +1202,7 @@ function SplitPaneView({
|
||||
<TitlebarDragRegion />
|
||||
<WorkspaceDesktopTabsRow
|
||||
paneId={pane.id}
|
||||
isFocused={isFocused}
|
||||
isFocused={isFocused && isWorkspaceFocused}
|
||||
tabs={desktopTabRowItems}
|
||||
normalizedServerId={normalizedServerId}
|
||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||
@@ -1147,6 +1228,9 @@ function SplitPaneView({
|
||||
tabDropPreviewIndex={
|
||||
tabDropPreview?.paneId === pane.id ? tabDropPreview.indicatorIndex : null
|
||||
}
|
||||
showPaneMaximizeAction={pane.id === explorerPaneId && !focusModeEnabled}
|
||||
paneMaximized={pane.id === maximizedPaneId}
|
||||
onTogglePaneMaximized={handleToggleMaximized}
|
||||
focusModeEnabled={Boolean(focusModeEnabled)}
|
||||
onExitFocusMode={onExitFocusMode}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user