fix(desktop): reserve titlebar drag region

This commit is contained in:
KoNit-K
2026-09-16 22:55:06 +08:00
committed by Teknium
parent 713a9377cc
commit 5aa1e353ef
4 changed files with 69 additions and 4 deletions

View File

@@ -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))')
})

View File

@@ -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

View File

@@ -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: () => <div>{id}</div>,
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(<TreeGroup leftEdge node={{ ...terminalGroup(false), panes: paneIds }} rightEdge topEdge />)
const handle = container!.querySelector<HTMLElement>('[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',

View File

@@ -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({
</PaneTabStrip>
</ZoneMenu>
) : null}
{topEdge && (!headerVisible || tabsBelowControls) && (
{topEdge && (
<div
className="min-w-0 flex-1 self-start [-webkit-app-region:drag]"
aria-hidden="true"
className={cn(
'self-start [-webkit-app-region:drag]',
headerVisible ? 'shrink-0' : 'min-w-0 flex-1'
)}
data-window-drag-handle=""
style={{ height: TITLEBAR_HEIGHT }}
style={{
height: TITLEBAR_HEIGHT,
width: headerVisible ? TITLEBAR_DRAG_HANDLE_WIDTH : undefined
}}
/>
)}
{topEdge && (