From 166863918ae3031828c1af44fa42e425c8cc3915 Mon Sep 17 00:00:00 2001 From: "t3-code[bot]" <269035359+t3-code[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:47:23 +0000 Subject: [PATCH] feat(web): reset right panel width on double click Reimplement the behavior proposed in #11219 on the current per-thread resize hook. Co-authored-by: cmacias Co-authored-by: Exotic <118054752+extoci@users.noreply.github.com> --- apps/web/src/hooks/useResizableWidth.test.tsx | 63 ++++++++++++++++++- apps/web/src/hooks/useResizableWidth.ts | 18 +++++- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/apps/web/src/hooks/useResizableWidth.test.tsx b/apps/web/src/hooks/useResizableWidth.test.tsx index 11ae6265de55..ad7957cb8217 100644 --- a/apps/web/src/hooks/useResizableWidth.test.tsx +++ b/apps/web/src/hooks/useResizableWidth.test.tsx @@ -43,16 +43,18 @@ function pointer(clientX = 100) { function Panel({ edge = "left", + defaultWidth = 400, maxWidth = 800, storageKey = "test-panel-width", }: { edge?: "left" | "right"; + defaultWidth?: number; maxWidth?: number; storageKey?: string; }) { const resize = useResizableWidth({ storageKey, - defaultWidth: 400, + defaultWidth, minWidth: 200, maxWidth, edge, @@ -74,7 +76,11 @@ beforeEach(async () => { vi.stubGlobal("window", { addEventListener: events.addEventListener.bind(events), removeEventListener: events.removeEventListener.bind(events), - localStorage: { getItem: (key: string) => savedWidths.get(key) ?? null, setItem }, + localStorage: { + getItem: (key: string) => savedWidths.get(key) ?? null, + setItem, + removeItem: (key: string) => savedWidths.delete(key), + }, }); vi.stubGlobal("document", { body: { style } }); vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { @@ -194,6 +200,59 @@ describe("panel resize cleanup", () => { }); describe("panel width storage changes", () => { + it("resets to the latest clamped default without freezing the next mount's default", async () => { + await act(() => renderer.update()); + await act(() => result.handlers.onDoubleClick()); + expect(result.width).toBe(500); + expect(savedWidths.has("test-panel-width")).toBe(false); + await act(() => renderer.unmount()); + await act(() => { + renderer = create(); + }); + expect(result.width).toBe(700); + }); + + it("still resets the visible width when storage removal fails", async () => { + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + const remove = vi.spyOn(window.localStorage, "removeItem").mockImplementation(() => { + throw new Error("storage unavailable"); + }); + try { + await act(() => { + result.handlers.onPointerDown(pointer()); + result.handlers.onPointerUp(pointer(0)); + }); + expect(result.width).toBe(500); + await act(() => result.handlers.onDoubleClick()); + expect(result.width).toBe(400); + expect(error).toHaveBeenCalledOnce(); + } finally { + remove.mockRestore(); + error.mockRestore(); + } + }); + + it("double-click resets only the current thread width and survives remount", async () => { + savedWidths.set("thread-b", "650"); + await act(() => renderer.update()); + for (let click = 0; click < 2; click++) { + await act(() => { + result.handlers.onPointerDown(pointer()); + result.handlers.onPointerUp(pointer()); + }); + } + savedWidths.set("test-panel-width", "500"); + await act(() => result.handlers.onDoubleClick()); + expect(result.width).toBe(400); + expect(savedWidths.has("thread-b")).toBe(false); + expect(savedWidths.get("test-panel-width")).toBe("500"); + await act(() => renderer.unmount()); + await act(() => { + renderer = create(); + }); + expect(result.width).toBe(400); + }); + it("restores separate thread widths without remounting and retains them after reload", async () => { await act(() => { result.handlers.onPointerDown(pointer()); diff --git a/apps/web/src/hooks/useResizableWidth.ts b/apps/web/src/hooks/useResizableWidth.ts index 22ab44bdead8..47e90d639efc 100644 --- a/apps/web/src/hooks/useResizableWidth.ts +++ b/apps/web/src/hooks/useResizableWidth.ts @@ -7,7 +7,11 @@ import { useState, } from "react"; -import { getLocalStorageItem, setLocalStorageItem } from "./useLocalStorage"; +import { + getLocalStorageItem, + removeLocalStorageItem, + setLocalStorageItem, +} from "./useLocalStorage"; import { useResizeDrag } from "./useResizeDrag"; const WidthSchema = Schema.Finite; @@ -27,6 +31,7 @@ export interface UseResizableWidthOptions { } export interface ResizableWidthHandlers { + readonly onDoubleClick: () => void; readonly onPointerDown: (event: ReactPointerEvent) => void; readonly onPointerMove: (event: ReactPointerEvent) => void; readonly onPointerUp: (event: ReactPointerEvent) => void; @@ -101,5 +106,14 @@ export function useResizableWidth(options: UseResizableWidthOptions): { storageKey, ); - return { width: clampedWidth, handlers }; + const onDoubleClick = useCallback(() => { + setWidthState({ storageKey, width: clamp(defaultWidth) }); + try { + removeLocalStorageItem(storageKey); + } catch (error) { + console.error("Could not reset persisted panel width.", error); + } + }, [clamp, defaultWidth, storageKey]); + + return { width: clampedWidth, handlers: { ...handlers, onDoubleClick } }; }