From 7210ff9797770b5990da9798758c9d58a63031bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Krakowski?= <48633090+bart-krakowski@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:05:41 +0200 Subject: [PATCH 1/3] feat(devtools): reset splitter panes on double click --- .../src/components/plugin-workspace.tsx | 6 ++++ packages/devtools/src/utils/layout-tree.ts | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/packages/devtools/src/components/plugin-workspace.tsx b/packages/devtools/src/components/plugin-workspace.tsx index 9f630e5e..6f563294 100644 --- a/packages/devtools/src/components/plugin-workspace.tsx +++ b/packages/devtools/src/components/plugin-workspace.tsx @@ -34,6 +34,7 @@ import { findGroupOfTab, layoutRects, moveTab, + resetSplit, resize, resizeFromPointer, setTabs, @@ -600,6 +601,10 @@ export const PluginWorkspace = (props: { document.addEventListener('pointerup', up) } + const resetSplitterDrag = (handle: SplitterHandle) => { + setLayout(resetSplit(layout(), handle.path, handle.gutterIndex)) + } + const resizeFromKeyboard = (handle: SplitterHandle, event: KeyboardEvent) => { const step = event.shiftKey ? KEYBOARD_STEP_COARSE : KEYBOARD_STEP const grows = @@ -867,6 +872,7 @@ export const PluginWorkspace = (props: { // Read through the accessor at gesture time, so a gutter that has // been re-measured since render still moves the right sizes. onPointerDown={(event) => startSplitterDrag(handle(), event)} + onDblClick={() => resetSplitterDrag(handle())} onKeyDown={(event) => resizeFromKeyboard(handle(), event)} /> ) diff --git a/packages/devtools/src/utils/layout-tree.ts b/packages/devtools/src/utils/layout-tree.ts index abb5dbda..08b417f6 100644 --- a/packages/devtools/src/utils/layout-tree.ts +++ b/packages/devtools/src/utils/layout-tree.ts @@ -427,6 +427,37 @@ export const resize = ( return replace(tree, 0) } +/** Evens out the two panes either side of a gutter, e.g. on splitter double-click. */ +export const resetSplit = ( + tree: LayoutNode | null, + path: Path, + gutterIndex: number, +): LayoutNode | null => { + const target = nodeAtPath(tree, path) + if (tree === null || target === null || !isSplit(target)) return tree + const before = target.sizes[gutterIndex] + const after = target.sizes[gutterIndex + 1] + if (before === undefined || after === undefined) return tree + + const even = (before + after) / 2 + if (Math.abs(before - even) < EPSILON && Math.abs(after - even) < EPSILON) { + return tree + } + + const sizes = [...target.sizes] + sizes[gutterIndex] = even + sizes[gutterIndex + 1] = even + + const replace = (node: LayoutNode, depth: number): LayoutNode => { + if (depth === path.length) return { ...(node as SplitNode), sizes } + const index = path[depth]! + const children = [...(node as SplitNode).children] + children[index] = replace(children[index]!, depth + 1) + return { ...(node as SplitNode), children } + } + return replace(tree, 0) +} + /** * Apply a pointer drag to the layout as it was at pointer-down. `deltaPx` is * the total movement from that start, not a per-frame increment. Always pass From a0c291ad4b63c0b80f6ababdc00dcc25d4ce489c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Krakowski?= <48633090+bart-krakowski@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:13:45 +0200 Subject: [PATCH 2/3] feat(devtools): reset splitter panes on double click --- .changeset/beige-snakes-call.md | 5 ++ .../devtools/src/utils/layout-tree.test.ts | 59 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 .changeset/beige-snakes-call.md diff --git a/.changeset/beige-snakes-call.md b/.changeset/beige-snakes-call.md new file mode 100644 index 00000000..4f60028c --- /dev/null +++ b/.changeset/beige-snakes-call.md @@ -0,0 +1,5 @@ +--- +'@tanstack/devtools': patch +--- + +Reset splitter panes on double click diff --git a/packages/devtools/src/utils/layout-tree.test.ts b/packages/devtools/src/utils/layout-tree.test.ts index 327ba71c..34592893 100644 --- a/packages/devtools/src/utils/layout-tree.test.ts +++ b/packages/devtools/src/utils/layout-tree.test.ts @@ -13,6 +13,7 @@ import { nextGroupId, paneRects, repairLayout, + resetSplit, resize, resizeFromPointer, singleGroup, @@ -354,6 +355,64 @@ describe('resize', () => { }) }) +describe('resetSplit', () => { + it('evens out an unequal pair', () => { + const tree = split( + 'row', + [group('g0', ['a']), group('g1', ['b'])], + [0.7, 0.3], + ) + const next = resetSplit(tree, [], 0) as SplitNode + expect(next.sizes[0]).toBeCloseTo(0.5, 10) + expect(next.sizes[1]).toBeCloseTo(0.5, 10) + expectWellFormed(next) + }) + + it('only touches the gutter pair, leaving the rest alone', () => { + const tree = split( + 'row', + [group('g0', ['a']), group('g1', ['b']), group('g2', ['c'])], + [0.1, 0.7, 0.2], + ) + const next = resetSplit(tree, [], 0) as SplitNode + expect(next.sizes[0]).toBeCloseTo(0.4, 10) + expect(next.sizes[1]).toBeCloseTo(0.4, 10) + expect(next.sizes[2]).toBeCloseTo(0.2, 10) + expectWellFormed(next) + }) + + it('resets a nested split by path', () => { + const tree = split('row', [ + group('g0', ['a']), + split('col', [group('g1', ['b']), group('g2', ['c'])], [0.8, 0.2]), + ]) + const next = resetSplit(tree, [1], 0) as SplitNode + const inner = next.children[1] as SplitNode + expect(inner.sizes[0]).toBeCloseTo(0.5, 10) + expect(inner.sizes[1]).toBeCloseTo(0.5, 10) + // The outer split is untouched. + expect(next.sizes[0]).toBeCloseTo(0.5, 10) + expectWellFormed(next) + }) + + it('is a no-op when the pair is already even', () => { + const tree = split( + 'row', + [group('g0', ['a']), group('g1', ['b'])], + [0.5, 0.5], + ) + expect(resetSplit(tree, [], 0)).toEqual(tree) + }) + + it('is a no-op for a bad path or gutter', () => { + const tree = split('row', [group('g0', ['a']), group('g1', ['b'])]) + expect(resetSplit(tree, [9], 0)).toEqual(tree) + expect(resetSplit(tree, [], 5)).toEqual(tree) + expect(resetSplit(group('g0', ['a']), [], 0)).toEqual(group('g0', ['a'])) + expect(resetSplit(null, [], 0)).toBeNull() + }) +}) + describe('resizeFromPointer', () => { it('applies total mouse travel to the snapshot so later moves do not compound', () => { const tree = split( From 18f05c547ff81cf5ac65ecefa353f33d4aa81685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Krakowski?= <48633090+bart-krakowski@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:24:17 +0200 Subject: [PATCH 3/3] Update .changeset/beige-snakes-call.md --- .changeset/beige-snakes-call.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/beige-snakes-call.md b/.changeset/beige-snakes-call.md index 4f60028c..257ed2c4 100644 --- a/.changeset/beige-snakes-call.md +++ b/.changeset/beige-snakes-call.md @@ -2,4 +2,4 @@ '@tanstack/devtools': patch --- -Reset splitter panes on double click +Reset splitter panes on double-click