diff --git a/apps/desktop/src/components/layout/EditorGroupTabBar.vue b/apps/desktop/src/components/layout/EditorGroupTabBar.vue index d71b235fcd..d3246ed27a 100644 --- a/apps/desktop/src/components/layout/EditorGroupTabBar.vue +++ b/apps/desktop/src/components/layout/EditorGroupTabBar.vue @@ -175,13 +175,12 @@ const isWrapLayout = computed(() => !isVerticalLayout.value && settingsStore.edi // The icon-only collapse only exists in the vertical toolbar; horizontal // placements must ignore the persisted collapse state entirely. const isTabBarCollapsed = computed(() => isVerticalLayout.value && !!props.tabBarCollapsed); +// Outer [data-workspace-tab-navigation] / [data-special-page-navigation] owns +// the shared vertical rail width; fill that rail so nested pane bars stay in +// sync while the resize handle updates the outer panel (issue #9977). const tabBarStyle = computed(() => { if (!isVerticalLayout.value) return undefined; - if (props.tabBarCollapsed) { - return { width: "100%", flex: "0 0 100%" }; - } - const width = props.tabBarWidth ?? 240; - return { width: `${width}px`, flex: `0 0 ${width}px` }; + return { width: "100%", flex: "0 0 100%" }; }); const tabBarCollapseIcon = computed(() => { const isLeft = settingsStore.editorSettings.tabPlacement === "left"; diff --git a/apps/desktop/src/components/layout/__tests__/AppTabBar.portal.spec.ts b/apps/desktop/src/components/layout/__tests__/AppTabBar.portal.spec.ts index ff3bd171fc..16ade06b52 100644 --- a/apps/desktop/src/components/layout/__tests__/AppTabBar.portal.spec.ts +++ b/apps/desktop/src/components/layout/__tests__/AppTabBar.portal.spec.ts @@ -356,11 +356,11 @@ describe("AppTabBar group navigation portal", () => { const rail = element(host, "[data-workspace-tab-navigation]"); const bar = element(rail, "[data-main-tab-bar]"); expect(rail.style.width).toBe("260px"); - expect(bar.style.width).toBe("260px"); + expect(bar.style.width).toBe("100%"); navigation.width = 320; await settle(); expect(rail.style.width).toBe("320px"); - expect(bar.style.width).toBe("320px"); + expect(bar.style.width).toBe("100%"); navigation.collapsed = true; await settle(); expect(rail.style.width).toBe("var(--collapsed-tab-rail-width)"); @@ -369,7 +369,7 @@ describe("AppTabBar group navigation portal", () => { navigation.collapsed = false; await settle(); expect(rail.style.width).toBe("320px"); - expect(bar.style.width).toBe("320px"); + expect(bar.style.width).toBe("100%"); expect(warnings).toEqual([]); }); diff --git a/apps/desktop/src/composables/__tests__/usePanelResize.spec.ts b/apps/desktop/src/composables/__tests__/usePanelResize.spec.ts index d90b33001b..5803023e84 100644 --- a/apps/desktop/src/composables/__tests__/usePanelResize.spec.ts +++ b/apps/desktop/src/composables/__tests__/usePanelResize.spec.ts @@ -145,4 +145,61 @@ describe("usePanelResize", () => { restored.setTabBarCollapsed(false); expect(localStorage.getItem("dbx-tab-bar-collapsed")).toBe("false"); }); + + it("updates the outer workspace tab rail width and flex while dragging (issue #9977)", () => { + localStorage.setItem("dbx-tab-bar-width", "240"); + + const rail = document.createElement("div"); + rail.setAttribute("data-workspace-tab-navigation", ""); + rail.style.width = "240px"; + rail.style.flex = "0 0 240px"; + const target = document.createElement("div"); + const tabBar = document.createElement("div"); + tabBar.className = "app-tab-bar"; + const handle = document.createElement("div"); + tabBar.append(handle); + target.append(tabBar); + rail.append(target); + document.body.append(rail); + + vi.spyOn(rail, "getBoundingClientRect").mockReturnValue(rect(0, 240)); + + const { tabBarWidth, startLeftTabBarResize } = usePanelResize(); + handle.addEventListener("pointerdown", startLeftTabBarResize); + handle.dispatchEvent(pointerEvent("pointerdown", 240)); + document.dispatchEvent(pointerEvent("pointermove", 320)); + + expect(rail.style.width).toBe("320px"); + expect(rail.style.flex).toBe("0 0 320px"); + expect(tabBar.style.width).toBe(""); + + document.dispatchEvent(pointerEvent("pointerup", 320)); + expect(tabBarWidth.value).toBe(320); + expect(localStorage.getItem("dbx-tab-bar-width")).toBe("320"); + }); + + it("shrinks a right-side tab rail toward the left edge while dragging", () => { + localStorage.setItem("dbx-tab-bar-width", "300"); + + const rail = document.createElement("div"); + rail.setAttribute("data-special-page-navigation", ""); + const tabBar = document.createElement("div"); + const handle = document.createElement("div"); + tabBar.append(handle); + rail.append(tabBar); + document.body.append(rail); + + vi.spyOn(rail, "getBoundingClientRect").mockReturnValue(rect(900, 300)); + + const { tabBarWidth, startRightTabBarResize } = usePanelResize(); + handle.addEventListener("pointerdown", startRightTabBarResize); + handle.dispatchEvent(pointerEvent("pointerdown", 900)); + document.dispatchEvent(pointerEvent("pointermove", 960)); + + expect(rail.style.width).toBe("240px"); + expect(rail.style.flex).toBe("0 0 240px"); + + document.dispatchEvent(pointerEvent("pointerup", 960)); + expect(tabBarWidth.value).toBe(240); + }); }); diff --git a/apps/desktop/src/composables/usePanelResize.ts b/apps/desktop/src/composables/usePanelResize.ts index 2f33cd59e5..6d39eeb3a8 100644 --- a/apps/desktop/src/composables/usePanelResize.ts +++ b/apps/desktop/src/composables/usePanelResize.ts @@ -6,12 +6,33 @@ const PANEL_MIN_WIDTH = 240; const DEFAULT_PANEL_MAX_WIDTH = 800; type PanelMaxWidth = number | ((handle: HTMLElement | null) => number); +/** Vertical tab rails size via width + flex-basis; sidebar/AI panels only need width. */ +const TAB_NAV_PANEL_SELECTOR = "[data-workspace-tab-navigation], [data-special-page-navigation]"; +const RESIZE_PANEL_SELECTOR = `${TAB_NAV_PANEL_SELECTOR}, [data-app-sidebar]`; + function restoredPanelWidth(storageKey: string, fallback: number): number { return Math.max(PANEL_MIN_WIDTH, Number(safeLocalStorageGet(storageKey)) || fallback); } +/** + * Prefer the layout-owning panel root over the handle's immediate parent. + * Vertical tab handles live inside a nested `.app-tab-bar`, while width is + * owned by `[data-workspace-tab-navigation]` / `[data-special-page-navigation]`. + */ +function resolveResizePanel(handle: HTMLElement | null): HTMLElement | null { + return handle?.closest(RESIZE_PANEL_SELECTOR) ?? handle?.parentElement ?? null; +} + +function applyPanelWidth(panel: HTMLElement | null, width: number) { + if (!panel) return; + panel.style.setProperty("width", `${width}px`); + if (panel.matches(TAB_NAV_PANEL_SELECTOR)) { + panel.style.setProperty("flex", `0 0 ${width}px`); + } +} + function availableAiPanelMaxWidth(handle: HTMLElement | null) { - const panel = handle?.parentElement; + const panel = resolveResizePanel(handle); const flexibleContent = panel?.previousElementSibling as HTMLElement | null; if (!panel || !flexibleContent) return DEFAULT_PANEL_MAX_WIDTH; @@ -49,11 +70,11 @@ export function usePanelResize() { 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 panelElement = resolveResizePanel(resizeHandle); + const renderedWidth = panelElement?.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; let rafId: number | null = null; @@ -63,12 +84,12 @@ export function usePanelResize() { // happy-dom unit tests assert styles synchronously (no frame advance), // so apply immediately under the test mode instead of via rAF. if (import.meta.env.MODE === "test") { - panelElement?.style.setProperty("width", `${currentWidth}px`); + applyPanelWidth(panelElement, currentWidth); return; } if (rafId !== null) return; rafId = requestAnimationFrame(() => { - panelElement?.style.setProperty("width", `${currentWidth}px`); + applyPanelWidth(panelElement, currentWidth); rafId = null; }); }; @@ -78,7 +99,7 @@ export function usePanelResize() { cancelAnimationFrame(rafId); rafId = null; } - panelElement?.style.setProperty("width", `${currentWidth}px`); + applyPanelWidth(panelElement, currentWidth); document.removeEventListener("pointermove", onPointerMove); document.removeEventListener("pointerup", finishResize); document.removeEventListener("pointercancel", finishResize);