Add support for interactive panel docking (#4015)

* Add interactive panel docking

* Preserve active tab when a panel group is docked

* Add inter-panel gutter hover color

* Code review fixes

* More code review
This commit is contained in:
Keavon Chambers
2026-04-08 06:09:20 -07:00
committed by GitHub
parent 39656d4c73
commit b099e2faca
9 changed files with 489 additions and 64 deletions
+36 -6
View File
@@ -1,25 +1,36 @@
import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import type { PanelType } from "/wrapper/pkg/graphite_wasm_wrapper";
export type DockingEdge = "Left" | "Right" | "Top" | "Bottom" | "Center";
export type PanelDragState = {
active: boolean;
sourcePanelId: string | undefined;
draggedTabLabel: string | undefined;
draggedTabs: PanelType[];
sourceTabIndex: number;
// Which panel's tab bar the pointer is currently hovering over (undefined if none)
// Whether we're dragging an entire tab group (via the tab bar background) vs a single tab
draggingGroup: boolean;
// Hover state for tab bar insertion (existing behavior)
hoverTargetPanelId: string | undefined;
hoverInsertionIndex: number | undefined;
hoverInsertionMarkerLeft: number | undefined;
// Hover state for edge docking (new split creation)
hoverDockingPanelId: string | undefined;
hoverDockingEdge: DockingEdge | undefined;
};
const initialState: PanelDragState = {
active: false,
sourcePanelId: undefined,
draggedTabLabel: undefined,
draggedTabs: [],
sourceTabIndex: 0,
draggingGroup: false,
hoverTargetPanelId: undefined,
hoverInsertionIndex: undefined,
hoverInsertionMarkerLeft: undefined,
hoverDockingPanelId: undefined,
hoverDockingEdge: undefined,
};
// Store state persisted across HMR to maintain reactive subscriptions in the component tree
@@ -28,12 +39,13 @@ if (import.meta.hot) import.meta.hot.data.store = store;
export const panelDrag = store;
export function startCrossPanelDrag(sourcePanelId: string, draggedTabLabel: string, sourceTabIndex: number) {
export function startCrossPanelDrag(sourcePanelId: string, draggedTabs: PanelType[], sourceTabIndex: number, draggingGroup: boolean) {
store.update((state) => {
state.active = true;
state.sourcePanelId = sourcePanelId;
state.draggedTabLabel = draggedTabLabel;
state.draggedTabs = draggedTabs;
state.sourceTabIndex = sourceTabIndex;
state.draggingGroup = draggingGroup;
return state;
});
}
@@ -42,11 +54,14 @@ export function endCrossPanelDrag() {
store.update((state) => {
state.active = false;
state.sourcePanelId = undefined;
state.draggedTabLabel = undefined;
state.draggedTabs = [];
state.sourceTabIndex = 0;
state.draggingGroup = false;
state.hoverTargetPanelId = undefined;
state.hoverInsertionIndex = undefined;
state.hoverInsertionMarkerLeft = undefined;
state.hoverDockingPanelId = undefined;
state.hoverDockingEdge = undefined;
return state;
});
}
@@ -56,6 +71,21 @@ export function updateCrossPanelHover(hoverTargetPanelId: string | undefined, ho
state.hoverTargetPanelId = hoverTargetPanelId;
state.hoverInsertionIndex = hoverInsertionIndex;
state.hoverInsertionMarkerLeft = hoverInsertionMarkerLeft;
// Clear docking state when hovering a tab bar
state.hoverDockingPanelId = undefined;
state.hoverDockingEdge = undefined;
return state;
});
}
export function updateDockingHover(panelId: string | undefined, edge: DockingEdge | undefined) {
store.update((state) => {
state.hoverDockingPanelId = panelId;
state.hoverDockingEdge = edge;
// Clear tab bar insertion state when hovering an edge
state.hoverTargetPanelId = undefined;
state.hoverInsertionIndex = undefined;
state.hoverInsertionMarkerLeft = undefined;
return state;
});
}