From 5aa1e353ef05b9cab86cdffede0c8b45c2e6575e Mon Sep 17 00:00:00 2001 From: KoNit-K <124019182+KoNit-K@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:55:06 +0800 Subject: [PATCH] fix(desktop): reserve titlebar drag region --- apps/desktop/src/app/shell/titlebar.test.ts | 5 ++ apps/desktop/src/app/shell/titlebar.ts | 2 + .../tree/renderer/tree-group.test.tsx | 51 +++++++++++++++++++ .../pane-shell/tree/renderer/tree-group.tsx | 15 ++++-- 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/app/shell/titlebar.test.ts b/apps/desktop/src/app/shell/titlebar.test.ts index 3531adb08b..73d7e46e25 100644 --- a/apps/desktop/src/app/shell/titlebar.test.ts +++ b/apps/desktop/src/app/shell/titlebar.test.ts @@ -4,6 +4,7 @@ import { MACOS_TAHOE_DARWIN_MAJOR, TITLEBAR_CONTROL_OFFSET_X, TITLEBAR_CONTROL_SIZE, + TITLEBAR_DRAG_HANDLE_WIDTH, TITLEBAR_EDGE_INSET, TITLEBAR_FALLBACK_WINDOW_BUTTON_X, TITLEBAR_ICON_SIZE, @@ -22,6 +23,10 @@ describe('titlebar sizing', () => { expect(titlebarIconSizeCss()).toBe('13.9px') }) + it('reserves a fixed native drag target beside titlebar tabs', () => { + expect(TITLEBAR_DRAG_HANDLE_WIDTH).toBe(48) + }) + it('reserves width from abutting hit targets only', () => { expect(titlebarToolsWidthCss(4)).toBe('calc(4 * var(--titlebar-control-size))') }) diff --git a/apps/desktop/src/app/shell/titlebar.ts b/apps/desktop/src/app/shell/titlebar.ts index e23ed4a381..1d58896d59 100644 --- a/apps/desktop/src/app/shell/titlebar.ts +++ b/apps/desktop/src/app/shell/titlebar.ts @@ -1,6 +1,8 @@ import type { HermesConnection } from '@/global' export const TITLEBAR_HEIGHT = 34 +/** Permanent native drag target beside a top-edge tab strip. */ +export const TITLEBAR_DRAG_HANDLE_WIDTH = 48 export const MACOS_TRAFFIC_LIGHTS_HEIGHT = 14 /** Titlebar tool hit target (both axes). */ export const TITLEBAR_CONTROL_SIZE = 24 diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.test.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.test.tsx index 663d771625..2bb454aacc 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.test.tsx +++ b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.test.tsx @@ -53,10 +53,61 @@ afterEach(() => { root = null container = null disposePane = null + globalThis.document.querySelectorAll('[data-titlebar-cluster]').forEach(element => element.remove()) + vi.restoreAllMocks() vi.unstubAllGlobals() }) describe('TreeGroup', () => { + it('keeps a fixed native drag handle beside crowded top-edge tabs', () => { + const paneIds = ['terminal', 'terminal-2', 'terminal-3', 'terminal-4', 'terminal-5'] + + const disposers = paneIds.map(id => + registry.register({ + area: 'panes', + data: { placement: 'main' }, + id, + render: () =>
{id}
, + title: id + }) + ) + + disposePane = () => disposers.forEach(dispose => dispose()) + vi.stubGlobal('CSS', { escape: (value: string) => value }) + vi.stubGlobal( + 'ResizeObserver', + class { + observe() {} + unobserve() {} + disconnect() {} + } + ) + vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function () { + if (this.matches('[data-titlebar-cluster="left"]')) { + return { left: 0, right: 100 } as DOMRect + } + + if (this.matches('[data-titlebar-cluster="right"]')) { + return { left: 700, right: 800 } as DOMRect + } + + return { left: 0, right: 800, width: 800 } as DOMRect + }) + const leftControls = globalThis.document.createElement('div') + leftControls.dataset.titlebarCluster = 'left' + const rightControls = globalThis.document.createElement('div') + rightControls.dataset.titlebarCluster = 'right' + globalThis.document.body.append(leftControls, rightControls) + + render() + + const handle = container!.querySelector('[data-window-drag-handle]')! + expect(container!.querySelectorAll('[data-tree-tab]').length).toBe(paneIds.length) + expect(handle).toBeTruthy() + expect(handle.style.width).not.toBe('') + expect(handle.className).toContain('shrink-0') + }) + it('keeps a top-edge strip inside its panel and yields native drag while moving a pane', () => { disposePane = registry.register({ area: 'panes', diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx index 469f129df5..43e8811e13 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx +++ b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx @@ -12,7 +12,7 @@ import { useStore } from '@nanostores/react' import { type CSSProperties, Fragment, type ReactNode, type RefObject, useEffect, useRef, useState } from 'react' -import { TITLEBAR_HEIGHT } from '@/app/shell/titlebar' +import { TITLEBAR_DRAG_HANDLE_WIDTH, TITLEBAR_HEIGHT } from '@/app/shell/titlebar' import { ActionsContextMenu, type MenuKit, renderActionItem } from '@/components/ui/actions-menu' import { Codicon } from '@/components/ui/codicon' import { DecodeText } from '@/components/ui/decode-text' @@ -696,11 +696,18 @@ export function TreeGroup({ ) : null} - {topEdge && (!headerVisible || tabsBelowControls) && ( + {topEdge && (