diff --git a/src/workspaces/components/Workspace/PanelSlot.tsx b/src/workspaces/components/Workspace/PanelSlot.tsx
index a1f5bbf1..8587248a 100644
--- a/src/workspaces/components/Workspace/PanelSlot.tsx
+++ b/src/workspaces/components/Workspace/PanelSlot.tsx
@@ -1,4 +1,6 @@
+import { ArrowLeftRight } from 'lucide-react';
import type { LayoutPanel, AppRegistry, PanelComponents } from './types';
+import { useWorkspace } from './WorkspaceContext';
import { Card } from '../Card';
import { AppPicker } from './AppPicker';
import {
@@ -6,6 +8,7 @@ import {
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
+ ContextMenuSeparator,
} from '../ui/context-menu';
type PanelSlotProps = {
@@ -29,21 +32,72 @@ type PanelContextMenuProps = {
children: React.ReactNode;
};
-const PanelContextMenu = ({ panelId, hasApp, isLastPanel, onSplit, onRemove, onClearApp, children }: PanelContextMenuProps) => (
-
- {children}
-
- onSplit(panelId, 'horizontal')}>Split horizontal
- onSplit(panelId, 'vertical')}>Split vertical
- {hasApp && Clear app}
- {!isLastPanel && (
- onRemove(panelId)}>
- Remove panel
-
- )}
-
-
-);
+const PanelContextMenu = ({ panelId, hasApp, isLastPanel, onSplit, onRemove, onClearApp, children }: PanelContextMenuProps) => {
+ const { swapSourceId, setSwapSourceId } = useWorkspace();
+
+ return (
+
+ {children}
+
+ onSplit(panelId, 'horizontal')}>Split horizontal
+ onSplit(panelId, 'vertical')}>Split vertical
+ {hasApp && Clear app}
+ {!isLastPanel && (
+ onRemove(panelId)}>
+ Remove panel
+
+ )}
+
+ {swapSourceId === panelId ? (
+ setSwapSourceId(null)}>Cancel swap
+ ) : swapSourceId ? (
+ setSwapSourceId(null)}>Cancel swap
+ ) : (
+ setSwapSourceId(panelId)}>Swap with...
+ )}
+
+
+ );
+};
+
+const SwapOverlay = ({ panelId }: { panelId: string }) => {
+ const { swapSourceId, setSwapSourceId, onSwap } = useWorkspace();
+
+ if (!swapSourceId || swapSourceId === panelId) return null;
+
+ return (
+
onSwap(swapSourceId, panelId)}
+ >
+
+
+ );
+};
+
+const SwapSourceIndicator = ({ panelId }: { panelId: string }) => {
+ const { swapSourceId, setSwapSourceId } = useWorkspace();
+
+ if (swapSourceId !== panelId) return null;
+
+ return (
+
+
+
+
+
+ );
+};
export const PanelSlot = ({ panel, registry, components, interactive, isLastPanel, onSetApp, onSplit, onRemove }: PanelSlotProps) => {
const PanelComponent = components?.[panel.id];
@@ -58,6 +112,15 @@ export const PanelSlot = ({ panel, registry, components, interactive, isLastPane
)
: (content: React.ReactNode) => <>{content}>;
+ const swapOverlays = interactive
+ ? (
+ <>
+
+
+ >
+ )
+ : null;
+
if (!AppComponent) {
if (!interactive) {
return (
@@ -68,26 +131,28 @@ export const PanelSlot = ({ panel, registry, components, interactive, isLastPane
}
return contextMenu(
-
+
onSetApp(panel.id, type)} />
+ {swapOverlays}
,
);
}
if (entry?.transparent) {
return contextMenu(
-
+
,
);
}
return contextMenu(
-
,
);
};
diff --git a/src/workspaces/components/Workspace/WorkspaceContext.ts b/src/workspaces/components/Workspace/WorkspaceContext.ts
index 9fc351c1..c9a9f8f6 100644
--- a/src/workspaces/components/Workspace/WorkspaceContext.ts
+++ b/src/workspaces/components/Workspace/WorkspaceContext.ts
@@ -3,11 +3,19 @@ import { createContext, useContext } from 'react';
type WorkspaceContextValue = {
workspaceId: string | null;
cwd: string;
+ swapSourceId: string | null;
+ setSwapSourceId: (id: string | null) => void;
+ onSwap: (sourceId: string, targetId: string) => void;
};
+const noop = () => {};
+
const WorkspaceContext = createContext
({
workspaceId: null,
cwd: '~',
+ swapSourceId: null,
+ setSwapSourceId: noop,
+ onSwap: noop,
});
export const WorkspaceProvider = WorkspaceContext.Provider;
diff --git a/src/workspaces/components/Workspace/WorkspaceLayout.tsx b/src/workspaces/components/Workspace/WorkspaceLayout.tsx
index e23f1724..48978d7a 100644
--- a/src/workspaces/components/Workspace/WorkspaceLayout.tsx
+++ b/src/workspaces/components/Workspace/WorkspaceLayout.tsx
@@ -24,7 +24,7 @@ export const WorkspaceLayout = ({ layout, onLayoutChange, registry, components,
);
return (
-
+
{
+ const [swapSourceId, setSwapSourceId] = useState(null);
+
const handleSetApp = useCallback(
(panelId: string, appType: string | null) => {
onLayoutChange(setApp(layout, panelId, appType));
@@ -41,8 +43,33 @@ export const WorkspaceView = ({ workspace, layout, onLayoutChange, registry }: W
[layout, onLayoutChange],
);
+ const handleSwap = useCallback(
+ (sourceId: string, targetId: string) => {
+ onLayoutChange(swapPanels(layout, sourceId, targetId));
+ setSwapSourceId(null);
+ },
+ [layout, onLayoutChange],
+ );
+
+ useEffect(() => {
+ if (!swapSourceId) return;
+ const onKeyDown = (ev: KeyboardEvent) => {
+ if (ev.key === 'Escape') setSwapSourceId(null);
+ };
+ window.addEventListener('keydown', onKeyDown);
+ return () => window.removeEventListener('keydown', onKeyDown);
+ }, [swapSourceId]);
+
return (
-
+
sum + countPanels(child.node), 0);