From 8b1bccab7a300e459420376e6094ad7ac7787dd5 Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Thu, 19 Feb 2026 12:14:02 +0000 Subject: [PATCH] workspaces panel swap --- .../components/Workspace/PanelSlot.tsx | 102 ++++++++++++++---- .../components/Workspace/WorkspaceContext.ts | 8 ++ .../components/Workspace/WorkspaceLayout.tsx | 2 +- .../components/Workspace/WorkspaceView.tsx | 33 +++++- src/workspaces/components/Workspace/index.ts | 2 +- .../components/Workspace/layout-utils.ts | 16 +++ 6 files changed, 140 insertions(+), 23 deletions(-) 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)} + > +
+ + Swap here +
+
+ ); +}; + +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( -
+
+ {swapOverlays}
, ); } return contextMenu( -
+
+ {swapOverlays}
, ); }; 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);