From a9cf5822fa785de14414367ae7be2d13f49d776e Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 15 Sep 2026 12:03:11 +0800 Subject: [PATCH] fix(layout): keep panel resizing smooth across plugin iframes --- apps/desktop/src/App.vue | 10 ++-- .../src/components/layout/AppSidebar.vue | 4 +- .../src/components/layout/EditorGroup.vue | 2 +- .../components/layout/EditorGroupTabBar.vue | 4 +- .../components/layout/SqlEditorWorkspace.vue | 2 +- .../__tests__/usePanelResize.spec.ts | 47 ++++++++++++++----- .../desktop/src/composables/usePanelResize.ts | 38 +++++++++++---- apps/desktop/src/styles/globals.css | 1 + 8 files changed, 77 insertions(+), 31 deletions(-) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index f75523371c..8bd2b63deb 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1009,7 +1009,7 @@ const appVersion = ref(""); const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic"); // Every pane's vertical strip writes back to this shared width/collapse state. -function startTabBarResize(event: MouseEvent) { +function startTabBarResize(event: PointerEvent) { if (settingsStore.editorSettings.tabPlacement === "right") { startRightTabBarResize(event); return; @@ -3918,7 +3918,7 @@ onUnmounted(() => { :class="[isClassicLayout ? 'h-full relative z-30 isolate bg-background' : 'h-full relative z-30 isolate rounded-md border border-border/80 bg-background', isAiPanelMaximized ? 'min-w-0 flex-1' : 'min-w-[240px] max-w-full']" :style="isAiPanelMaximized ? {} : { width: aiPanelWidth + 'px' }" > -
+
{ :class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'" :style="{ width: historyWidth + 'px' }" > -
+
@@ -3957,7 +3957,7 @@ onUnmounted(() => { :class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'" :style="{ width: sqlLibraryWidth + 'px' }" > -
+
@@ -3969,7 +3969,7 @@ onUnmounted(() => { :class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'" :style="{ width: sqlFilePanelWidth + 'px' }" > -
+
diff --git a/apps/desktop/src/components/layout/AppSidebar.vue b/apps/desktop/src/components/layout/AppSidebar.vue index 6aae3a1395..0b65aa9602 100644 --- a/apps/desktop/src/components/layout/AppSidebar.vue +++ b/apps/desktop/src/components/layout/AppSidebar.vue @@ -23,7 +23,7 @@ defineProps<{ const emit = defineEmits<{ import: [source: "dbx" | "navicat" | "dbeaver" | "datagrip"]; export: []; - startResize: [event: MouseEvent]; + startResize: [event: PointerEvent]; collapse: []; "open-settings": [initialTab: string]; "add-to-ai": [nodes: TreeNode | TreeNode[]]; @@ -296,7 +296,7 @@ defineExpose({ focusSearch, locateTabInSidebar });
-
+
diff --git a/apps/desktop/src/components/layout/EditorGroup.vue b/apps/desktop/src/components/layout/EditorGroup.vue index 47bf8ef32d..cf1a166956 100644 --- a/apps/desktop/src/components/layout/EditorGroup.vue +++ b/apps/desktop/src/components/layout/EditorGroup.vue @@ -43,7 +43,7 @@ const emit = defineEmits< "activate-tab": [tabId: string]; "locate-tab": [tab: QueryTab]; "toggle-zen-mode": []; - "start-resize": [event: MouseEvent]; + "start-resize": [event: PointerEvent]; "toggle-collapse": []; "detach-tab": [tab: QueryTab]; } diff --git a/apps/desktop/src/components/layout/EditorGroupTabBar.vue b/apps/desktop/src/components/layout/EditorGroupTabBar.vue index 792ce61902..6a94da2dec 100644 --- a/apps/desktop/src/components/layout/EditorGroupTabBar.vue +++ b/apps/desktop/src/components/layout/EditorGroupTabBar.vue @@ -107,7 +107,7 @@ const emit = defineEmits<{ "activate-tab": [tabId: string]; "locate-tab": [tab: QueryTab]; "toggle-zen-mode": []; - "start-resize": [event: MouseEvent]; + "start-resize": [event: PointerEvent]; "toggle-collapse": []; "detach-tab": [tab: QueryTab]; "activate-settings": []; @@ -1778,7 +1778,7 @@ watch([() => props.specialPageTabs?.settingsActive, () => props.specialPageTabs?
-
+
diff --git a/apps/desktop/src/components/layout/SqlEditorWorkspace.vue b/apps/desktop/src/components/layout/SqlEditorWorkspace.vue index cdb951a750..2f627326c8 100644 --- a/apps/desktop/src/components/layout/SqlEditorWorkspace.vue +++ b/apps/desktop/src/components/layout/SqlEditorWorkspace.vue @@ -34,7 +34,7 @@ const emit = defineEmits< ContentAreaSurfaceEmits & { "locate-tab": [tab: QueryTab]; "toggle-zen-mode": []; - "start-resize": [event: MouseEvent]; + "start-resize": [event: PointerEvent]; "toggle-collapse": []; "detach-tab": [tab: QueryTab]; } diff --git a/apps/desktop/src/composables/__tests__/usePanelResize.spec.ts b/apps/desktop/src/composables/__tests__/usePanelResize.spec.ts index 1870d1f763..d90b33001b 100644 --- a/apps/desktop/src/composables/__tests__/usePanelResize.spec.ts +++ b/apps/desktop/src/composables/__tests__/usePanelResize.spec.ts @@ -25,6 +25,12 @@ function rect(left: number, width: number): DOMRect { } as DOMRect; } +function pointerEvent(type: string, clientX: number, pointerId = 1): MouseEvent { + const event = new MouseEvent(type, { bubbles: true, clientX }); + Object.defineProperty(event, "pointerId", { value: pointerId }); + return event; +} + describe("usePanelResize", () => { beforeEach(() => { vi.stubGlobal("localStorage", localStorageMock); @@ -49,10 +55,10 @@ describe("usePanelResize", () => { vi.spyOn(panel, "getBoundingClientRect").mockReturnValue(rect(1000, 360)); const { aiPanelWidth, startAiPanelResize } = usePanelResize(); - handle.addEventListener("mousedown", startAiPanelResize); - handle.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: 1000 })); - document.dispatchEvent(new MouseEvent("mousemove", { clientX: -1000 })); - document.dispatchEvent(new MouseEvent("mouseup")); + handle.addEventListener("pointerdown", startAiPanelResize); + handle.dispatchEvent(pointerEvent("pointerdown", 1000)); + document.dispatchEvent(pointerEvent("pointermove", -1000)); + document.dispatchEvent(pointerEvent("pointerup", -1000)); expect(aiPanelWidth.value).toBe(1060); expect(localStorage.getItem("dbx-ai-panel-width")).toBe("1060"); @@ -71,10 +77,10 @@ describe("usePanelResize", () => { vi.spyOn(panel, "getBoundingClientRect").mockReturnValue(rect(300, 700)); const { aiPanelWidth, startAiPanelResize } = usePanelResize(); - handle.addEventListener("mousedown", startAiPanelResize); - handle.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: 300 })); - document.dispatchEvent(new MouseEvent("mousemove", { clientX: 400 })); - document.dispatchEvent(new MouseEvent("mouseup")); + handle.addEventListener("pointerdown", startAiPanelResize); + handle.dispatchEvent(pointerEvent("pointerdown", 300)); + document.dispatchEvent(pointerEvent("pointermove", 400)); + document.dispatchEvent(pointerEvent("pointerup", 400)); expect(aiPanelWidth.value).toBe(600); expect(localStorage.getItem("dbx-ai-panel-width")).toBe("600"); @@ -89,15 +95,32 @@ describe("usePanelResize", () => { vi.spyOn(panel, "getBoundingClientRect").mockReturnValue(rect(0, 260)); const { sidebarWidth, startSidebarResize } = usePanelResize(); - handle.addEventListener("mousedown", startSidebarResize); - handle.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: 260 })); - document.dispatchEvent(new MouseEvent("mousemove", { clientX: 0 })); - document.dispatchEvent(new MouseEvent("mouseup")); + handle.addEventListener("pointerdown", startSidebarResize); + handle.dispatchEvent(pointerEvent("pointerdown", 260)); + document.dispatchEvent(pointerEvent("pointermove", 0)); + + expect(panel.style.width).toBe("240px"); + document.dispatchEvent(pointerEvent("pointerup", 0)); expect(sidebarWidth.value).toBe(240); expect(localStorage.getItem("dbx-sidebar-width")).toBe("240"); }); + it("removes the drag overlay when the window loses focus", () => { + const panel = document.createElement("div"); + const handle = document.createElement("div"); + panel.append(handle); + document.body.append(panel); + + const { startSidebarResize } = usePanelResize(); + handle.addEventListener("pointerdown", startSidebarResize); + handle.dispatchEvent(pointerEvent("pointerdown", 260)); + + expect(document.body.querySelector('[aria-hidden="true"]')).not.toBeNull(); + window.dispatchEvent(new Event("blur")); + expect(document.body.querySelector('[aria-hidden="true"]')).toBeNull(); + }); + it("raises persisted panel widths below the toolbar-safe minimum", () => { localStorage.setItem("dbx-sidebar-width", "180"); localStorage.setItem("dbx-ai-panel-width", "200"); diff --git a/apps/desktop/src/composables/usePanelResize.ts b/apps/desktop/src/composables/usePanelResize.ts index a6d1ecae68..343417bd1d 100644 --- a/apps/desktop/src/composables/usePanelResize.ts +++ b/apps/desktop/src/composables/usePanelResize.ts @@ -29,30 +29,52 @@ export function usePanelResize() { const tabBarCollapsed = ref(safeLocalStorageGet("dbx-tab-bar-collapsed") === "true"); function startPanelResize(widthRef: Ref, storageKey: string, direction: "left" | "right", maxWidth: PanelMaxWidth = DEFAULT_PANEL_MAX_WIDTH) { - return (e: MouseEvent) => { + return (e: PointerEvent) => { e.preventDefault(); const startX = e.clientX; const resizeHandle = e.currentTarget as HTMLElement | null; + resizeHandle?.setPointerCapture(e.pointerId); + const resizeOverlay = document.createElement("div"); + resizeOverlay.setAttribute("aria-hidden", "true"); + Object.assign(resizeOverlay.style, { + position: "fixed", + inset: "0", + zIndex: "2147483647", + cursor: "col-resize", + userSelect: "none", + touchAction: "none", + }); + document.body.append(resizeOverlay); const resolvedMaxWidth = typeof maxWidth === "function" ? maxWidth(resizeHandle) : maxWidth; const upperBound = Number.isFinite(resolvedMaxWidth) ? Math.max(PANEL_MIN_WIDTH, resolvedMaxWidth) : DEFAULT_PANEL_MAX_WIDTH; const renderedWidth = resizeHandle?.parentElement?.getBoundingClientRect().width; const requestedStartWidth = typeof renderedWidth === "number" && Number.isFinite(renderedWidth) && renderedWidth > 0 ? renderedWidth : widthRef.value; const startWidth = Math.max(PANEL_MIN_WIDTH, Math.min(upperBound, requestedStartWidth)); widthRef.value = startWidth; + const panelElement = resizeHandle?.parentElement; + let currentWidth = startWidth; - const onMouseMove = (ev: MouseEvent) => { + const onPointerMove = (ev: PointerEvent) => { const delta = ev.clientX - startX; - widthRef.value = Math.max(PANEL_MIN_WIDTH, Math.min(upperBound, startWidth + (direction === "right" ? delta : -delta))); + currentWidth = Math.max(PANEL_MIN_WIDTH, Math.min(upperBound, startWidth + (direction === "right" ? delta : -delta))); + panelElement?.style.setProperty("width", `${currentWidth}px`); }; - const onMouseUp = () => { - document.removeEventListener("mousemove", onMouseMove); - document.removeEventListener("mouseup", onMouseUp); + const finishResize = () => { + document.removeEventListener("pointermove", onPointerMove); + document.removeEventListener("pointerup", finishResize); + document.removeEventListener("pointercancel", finishResize); + window.removeEventListener("blur", finishResize); + if (resizeHandle?.hasPointerCapture(e.pointerId)) resizeHandle.releasePointerCapture(e.pointerId); + resizeOverlay.remove(); + widthRef.value = currentWidth; safeLocalStorageSet(storageKey, String(widthRef.value)); }; - document.addEventListener("mousemove", onMouseMove); - document.addEventListener("mouseup", onMouseUp); + document.addEventListener("pointermove", onPointerMove); + document.addEventListener("pointerup", finishResize); + document.addEventListener("pointercancel", finishResize); + window.addEventListener("blur", finishResize, { once: true }); }; } diff --git a/apps/desktop/src/styles/globals.css b/apps/desktop/src/styles/globals.css index b612ac835a..998dd89929 100644 --- a/apps/desktop/src/styles/globals.css +++ b/apps/desktop/src/styles/globals.css @@ -2522,6 +2522,7 @@ html.theme-vscode .app-tab-scroll.wrap-mode .app-tab-pill[data-active-tab="false bottom: 0; width: 7px; cursor: col-resize; + touch-action: none; z-index: 10; background: transparent; }