fix: restore Windows custom desktop chrome

Use frameless Electron chrome only on Windows and remove the native Windows application menu so the packaged app matches the previous Tauri-style desktop surface.

Add a Windows-only manual drag fallback for desktop drag regions, while excluding tab reorder targets and preserving macOS/Linux native chrome and menu behavior.

Tested: cd desktop && bun run test --run electron/services/menu.test.ts electron/services/windows.test.ts src/hooks/useElectronWindowDragRegions.test.tsx src/components/layout/TabBar.test.tsx src/components/layout/Sidebar.test.tsx src/lib/desktopHost/electronHost.test.ts electron/ipc/capabilities.test.ts

Tested: cd desktop && SKIP_INSTALL=1 bun run build:windows-x64

Tested: Computer Use Windows packaged app smoke verified no native menu, custom controls, drag regions, tab reorder, close-to-background, and deepseek-v4-pro provider response FINAL_WINDOWS_ELECTRON_REAL_PROVIDER_OK.

Not-tested: full bun run verify.

Constraint: keep custom frameless behavior scoped to win32 so macOS and Linux native chrome paths remain intact.

Confidence: high

Scope-risk: moderate
This commit is contained in:
Relakkes Yang 2026-06-03 23:22:35 +08:00
parent 8bca6985fe
commit 98b79ce51a
16 changed files with 406 additions and 16 deletions

View File

@ -23,6 +23,10 @@ describe('Electron IPC capabilities', () => {
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.shellOpen, { url: 'https://example.com' })).toBe(false)
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowClose, undefined)).toBe(true)
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowClose, {})).toBe(false)
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowStartDragging, undefined)).toBe(true)
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowStartDragging, { deltaX: 4, deltaY: -2 })).toBe(true)
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowStartDragging, { deltaX: 4, deltaY: Number.NaN })).toBe(false)
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowStartDragging, { deltaX: 4, deltaY: -2, extra: true })).toBe(false)
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.terminalWrite, { sessionId: 1, data: 'pwd\n' })).toBe(true)
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.terminalWrite, { sessionId: '1', data: 'pwd\n' })).toBe(false)
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.terminalSpawn, { cols: 80, rows: 24, cwd: '/tmp' })).toBe(true)

View File

@ -50,6 +50,17 @@ const boundsPayload: Validator = value =>
&& typeof value.width === 'number'
&& typeof value.height === 'number'
const windowDragMovePayload: Validator = value =>
value === undefined
|| (
isRecord(value)
&& hasOnlyKeys(value, ['deltaX', 'deltaY'])
&& typeof value.deltaX === 'number'
&& Number.isFinite(value.deltaX)
&& typeof value.deltaY === 'number'
&& Number.isFinite(value.deltaY)
)
const urlWithOptionalBounds: Validator = value =>
isRecord(value)
&& typeof value.url === 'string'
@ -84,7 +95,7 @@ export const ELECTRON_IPC_VALIDATORS = {
[ELECTRON_IPC_CHANNELS.windowMinimize]: noPayload,
[ELECTRON_IPC_CHANNELS.windowToggleMaximize]: noPayload,
[ELECTRON_IPC_CHANNELS.windowClose]: noPayload,
[ELECTRON_IPC_CHANNELS.windowStartDragging]: noPayload,
[ELECTRON_IPC_CHANNELS.windowStartDragging]: windowDragMovePayload,
[ELECTRON_IPC_CHANNELS.windowRequestAttention]: noPayload,
[ELECTRON_IPC_CHANNELS.windowFocus]: noPayload,
[ELECTRON_IPC_CHANNELS.windowIsMaximized]: noPayload,

View File

@ -38,6 +38,7 @@ import {
restoreWindowMaximized,
saveWindowState,
showMainWindow,
windowChromeOptionsForPlatform,
windowOptionsFromState,
MIN_WINDOW_HEIGHT,
MIN_WINDOW_WIDTH,
@ -243,7 +244,19 @@ function registerIpcHandlers() {
else window.maximize()
})
registerHandler(ELECTRON_IPC_CHANNELS.windowClose, event => currentWindow(event).close())
registerHandler(ELECTRON_IPC_CHANNELS.windowStartDragging, () => undefined)
registerHandler(ELECTRON_IPC_CHANNELS.windowStartDragging, (event, payload) => {
if (!payload || typeof payload !== 'object') return undefined
const { deltaX, deltaY } = payload as { deltaX?: number, deltaY?: number }
if (!Number.isFinite(deltaX) || !Number.isFinite(deltaY)) return undefined
const window = currentWindow(event)
if (window.isMaximized()) window.unmaximize()
const bounds = window.getBounds()
window.setPosition(
Math.round(bounds.x + deltaX!),
Math.round(bounds.y + deltaY!),
)
return undefined
})
registerHandler(ELECTRON_IPC_CHANNELS.windowRequestAttention, event => currentWindow(event).flashFrame(true))
registerHandler(ELECTRON_IPC_CHANNELS.windowFocus, event => currentWindow(event).focus())
registerHandler(ELECTRON_IPC_CHANNELS.windowIsMaximized, event => currentWindow(event).isMaximized())
@ -293,8 +306,7 @@ async function createMainWindow() {
minWidth: MIN_WINDOW_WIDTH,
minHeight: MIN_WINDOW_HEIGHT,
show: false,
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
fullscreenable: process.platform !== 'darwin',
...windowChromeOptionsForPlatform(process.platform),
webPreferences: {
preload: preloadPath(),
contextIsolation: true,

View File

@ -125,6 +125,7 @@ describe('Electron application menu service', () => {
await installApplicationMenu(
{ name: 'Claude Code Haha' } as never,
() => ({ webContents: { send } }) as never,
'darwin',
)
expect(menuMocks.buildFromTemplate).toHaveBeenCalledTimes(1)
@ -142,6 +143,39 @@ describe('Electron application menu service', () => {
expect(send).toHaveBeenCalledWith(ELECTRON_EVENT_CHANNELS.nativeMenuNavigate, 'settings')
})
it('clears the native application menu on Windows so custom chrome owns the top bar', async () => {
const menuMocks = getElectronMenuMocks()
menuMocks.buildFromTemplate.mockClear()
menuMocks.setApplicationMenu.mockClear()
await installApplicationMenu(
{ name: 'Claude Code Haha' } as never,
() => ({ webContents: { send: vi.fn() } }) as never,
'win32',
)
expect(menuMocks.buildFromTemplate).not.toHaveBeenCalled()
expect(menuMocks.setApplicationMenu).toHaveBeenCalledWith(null)
})
it('keeps the native application menu installed on Linux', async () => {
const menuMocks = getElectronMenuMocks()
menuMocks.buildFromTemplate.mockClear()
menuMocks.setApplicationMenu.mockClear()
const send = vi.fn()
await installApplicationMenu(
{ name: 'Claude Code Haha' } as never,
() => ({ webContents: { send } }) as never,
'linux',
)
expect(menuMocks.buildFromTemplate).toHaveBeenCalledTimes(1)
expect(menuMocks.setApplicationMenu).toHaveBeenCalledWith({
template: menuMocks.buildFromTemplate.mock.calls[0]?.[0],
})
})
it('installs hide as a safe fullscreen-aware window hide before app hide', async () => {
const appHide = vi.fn()
const onceHandlers = new Map<string, (...args: never[]) => void>()
@ -161,6 +195,7 @@ describe('Electron application menu service', () => {
await installApplicationMenu(
{ name: 'Claude Code Haha', hide: appHide } as never,
() => window as never,
'darwin',
)
const template = menuMocks.buildFromTemplate.mock.calls[0]?.[0] as MenuItemConstructorOptions[]
@ -191,6 +226,7 @@ describe('Electron application menu service', () => {
await installApplicationMenu(
{ name: 'Claude Code Haha' } as never,
() => window as never,
'darwin',
)
const template = menuMocks.buildFromTemplate.mock.calls[0]?.[0] as MenuItemConstructorOptions[]

View File

@ -76,11 +76,20 @@ export function buildApplicationMenuTemplate(
]
}
export async function installApplicationMenu(app: App, getMainWindow: () => BrowserWindow | null) {
export async function installApplicationMenu(
app: App,
getMainWindow: () => BrowserWindow | null,
platform: NodeJS.Platform = process.platform,
) {
const { Menu } = await import('electron')
if (platform === 'win32') {
Menu.setApplicationMenu(null)
return
}
const template = buildApplicationMenuTemplate(app.name || 'Claude Code Haha', destination => {
getMainWindow()?.webContents.send(ELECTRON_EVENT_CHANNELS.nativeMenuNavigate, destination)
}, process.platform, {
}, platform, {
hide: () => {
const window = getMainWindow()
if (!window) {
@ -94,7 +103,7 @@ export async function installApplicationMenu(app: App, getMainWindow: () => Brow
},
toggleFullScreen: () => {
const window = getMainWindow()
if (window) toggleWindowFullScreen(window)
if (window) toggleWindowFullScreen(window, platform)
},
})
Menu.setApplicationMenu(Menu.buildFromTemplate(template))

View File

@ -14,6 +14,7 @@ import {
restoreWindowMaximized,
showMainWindow,
toggleWindowFullScreen,
windowChromeOptionsForPlatform,
windowOptionsFromState,
windowStatePath,
writeWindowState,
@ -126,6 +127,22 @@ describe('Electron window service', () => {
expect(maximize).toHaveBeenCalledTimes(1)
})
it('uses frameless custom chrome only on Windows', () => {
expect(windowChromeOptionsForPlatform('win32')).toEqual({
frame: false,
autoHideMenuBar: true,
fullscreenable: true,
})
expect(windowChromeOptionsForPlatform('darwin')).toEqual({
titleBarStyle: 'hiddenInset',
fullscreenable: false,
})
expect(windowChromeOptionsForPlatform('linux')).toEqual({
titleBarStyle: 'default',
fullscreenable: true,
})
})
it('shows, restores, and focuses the hidden main window when a tray or notification action reopens it', () => {
const window = {
isVisible: () => false,

View File

@ -1,6 +1,6 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import type { App, BrowserWindow, Display } from 'electron'
import type { App, BrowserWindow, BrowserWindowConstructorOptions, Display } from 'electron'
export const WINDOW_STATE_FILE = 'window-state.json'
export const DEFAULT_WINDOW_WIDTH = 1280
@ -21,6 +21,10 @@ export type WindowStateBounds = Pick<StoredWindowState, 'x' | 'y' | 'width' | 'h
export type WindowCreateBounds =
& Partial<Pick<StoredWindowState, 'x' | 'y'>>
& Pick<StoredWindowState, 'width' | 'height'>
export type WindowChromeOptions = Pick<
BrowserWindowConstructorOptions,
'autoHideMenuBar' | 'frame' | 'fullscreenable' | 'titleBarStyle'
>
export function windowStatePath(app: App, env: NodeJS.ProcessEnv = process.env): string {
return path.join(env.CLAUDE_CONFIG_DIR || app.getPath('userData'), WINDOW_STATE_FILE)
@ -134,6 +138,30 @@ export function windowOptionsFromState(state: StoredWindowState | null): WindowC
: { width: DEFAULT_WINDOW_WIDTH, height: DEFAULT_WINDOW_HEIGHT }
}
export function windowChromeOptionsForPlatform(
platform: NodeJS.Platform = process.platform,
): WindowChromeOptions {
if (platform === 'darwin') {
return {
titleBarStyle: 'hiddenInset',
fullscreenable: false,
}
}
if (platform === 'win32') {
return {
frame: false,
autoHideMenuBar: true,
fullscreenable: true,
}
}
return {
titleBarStyle: 'default',
fullscreenable: true,
}
}
export function restoreWindowMaximized(window: BrowserWindow, state: StoredWindowState | null) {
if (state?.maximized) window.maximize()
}

View File

@ -15,7 +15,7 @@
content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data: blob: https: http://127.0.0.1:* http://localhost:*; connect-src 'self' https: http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:*; worker-src 'self' blob:; object-src 'none'; base-uri 'self'"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Claude Code Companion</title>
<title>Claude Code Haha</title>
<!-- Fonts are self-hosted in /fonts/ and declared via @font-face in globals.css -->
<style>
/* Prevent a warm flash before the theme is hydrated. */

View File

@ -6,6 +6,7 @@ import { UpdateChecker } from '../shared/UpdateChecker'
import { useSettingsStore } from '../../stores/settingsStore'
import { useUIStore, type SettingsTab } from '../../stores/uiStore'
import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts'
import { useElectronWindowDragRegions } from '../../hooks/useElectronWindowDragRegions'
import {
H5ConnectionRequiredError,
initializeDesktopServerUrl,
@ -130,6 +131,7 @@ export function AppShell() {
}, [])
useKeyboardShortcuts()
useElectronWindowDragRegions()
useEffect(() => {
if (isMobileShell && !wasMobileShellRef.current) {

View File

@ -310,7 +310,9 @@ describe('TabBar', () => {
expect(screen.getByTestId('tab-bar')).toHaveAttribute('data-desktop-drag-region')
expect(screen.getByTestId('tab-bar-scroll-region')).toHaveAttribute('data-desktop-drag-region')
expect(screen.getByTestId('tab-bar-drag-gutter')).toHaveAttribute('data-desktop-drag-region')
expect(screen.getByText('Untitled Session').closest('.tab-bar-interactive')).toBeInTheDocument()
const tab = screen.getByText('Untitled Session').closest('.tab-bar-interactive')
expect(tab).toBeInTheDocument()
expect(tab).not.toHaveAttribute('data-desktop-drag-region')
})
it('keeps the desktop tab strip at a roomier titlebar height', async () => {

View File

@ -0,0 +1,159 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { shouldUseManualWindowDrag, useElectronWindowDragRegions } from './useElectronWindowDragRegions'
const desktopHostMock = vi.hoisted(() => ({
startDragging: vi.fn().mockResolvedValue(undefined),
host: {
isDesktop: true,
capabilities: {
windowControls: true,
},
window: {
startDragging: vi.fn().mockResolvedValue(undefined),
},
},
}))
vi.mock('../lib/desktopHost', () => ({
getDesktopHost: () => desktopHostMock.host,
}))
function Harness() {
useElectronWindowDragRegions()
return (
<div data-testid="drag-region" data-desktop-drag-region>
<div data-testid="blank-space">blank</div>
<button type="button">Button</button>
<div data-testid="tab" className="tab-bar-interactive">
Tab
</div>
</div>
)
}
describe('useElectronWindowDragRegions', () => {
const originalPlatform = navigator.platform
beforeEach(() => {
Object.defineProperty(navigator, 'platform', {
configurable: true,
value: 'Win32',
})
delete document.documentElement.dataset.desktopDragMode
desktopHostMock.host.isDesktop = true
desktopHostMock.host.capabilities.windowControls = true
desktopHostMock.host.window.startDragging.mockReset()
desktopHostMock.host.window.startDragging.mockResolvedValue(undefined)
})
afterEach(() => {
Object.defineProperty(navigator, 'platform', {
configurable: true,
value: originalPlatform,
})
delete document.documentElement.dataset.desktopDragMode
})
it('uses the manual drag fallback on Windows only', () => {
expect(shouldUseManualWindowDrag('Win32')).toBe(true)
expect(shouldUseManualWindowDrag('MacIntel')).toBe(false)
expect(shouldUseManualWindowDrag('Linux x86_64')).toBe(false)
})
it('moves the native window while dragging a desktop drag region', async () => {
render(<Harness />)
expect(document.documentElement.dataset.desktopDragMode).toBe('manual')
fireEvent.mouseDown(screen.getByTestId('blank-space'), {
button: 0,
screenX: 100,
screenY: 120,
})
fireEvent.mouseMove(window, {
screenX: 130,
screenY: 150,
})
await waitFor(() => {
expect(desktopHostMock.host.window.startDragging).toHaveBeenCalledWith({
deltaX: 30,
deltaY: 30,
})
})
fireEvent.mouseUp(window)
fireEvent.mouseMove(window, {
screenX: 180,
screenY: 210,
})
expect(desktopHostMock.host.window.startDragging).toHaveBeenCalledTimes(1)
})
it('keeps buttons and tab reorder targets out of the window drag fallback', () => {
render(<Harness />)
fireEvent.mouseDown(screen.getByRole('button', { name: 'Button' }), {
button: 0,
screenX: 100,
screenY: 120,
})
fireEvent.mouseMove(window, {
screenX: 130,
screenY: 150,
})
fireEvent.mouseDown(screen.getByTestId('tab'), {
button: 0,
screenX: 100,
screenY: 120,
})
fireEvent.mouseMove(window, {
screenX: 130,
screenY: 150,
})
expect(desktopHostMock.host.window.startDragging).not.toHaveBeenCalled()
})
it('does nothing outside the Electron custom chrome runtime', () => {
desktopHostMock.host.isDesktop = false
render(<Harness />)
fireEvent.mouseDown(screen.getByTestId('blank-space'), {
button: 0,
screenX: 100,
screenY: 120,
})
fireEvent.mouseMove(window, {
screenX: 130,
screenY: 150,
})
expect(desktopHostMock.host.window.startDragging).not.toHaveBeenCalled()
expect(document.documentElement.dataset.desktopDragMode).toBeUndefined()
})
it('leaves native app-region handling active on non-Windows platforms', () => {
Object.defineProperty(navigator, 'platform', {
configurable: true,
value: 'MacIntel',
})
render(<Harness />)
fireEvent.mouseDown(screen.getByTestId('blank-space'), {
button: 0,
screenX: 100,
screenY: 120,
})
fireEvent.mouseMove(window, {
screenX: 130,
screenY: 150,
})
expect(desktopHostMock.host.window.startDragging).not.toHaveBeenCalled()
expect(document.documentElement.dataset.desktopDragMode).toBeUndefined()
})
})

View File

@ -0,0 +1,86 @@
import { useEffect } from 'react'
import { getDesktopHost } from '../lib/desktopHost'
const DRAG_REGION_SELECTOR = '[data-desktop-drag-region]'
const MANUAL_DRAG_MODE = 'manual'
const NO_DRAG_SELECTOR = [
'[data-desktop-no-drag-region]',
'button',
'input',
'textarea',
'select',
'a',
'[role="button"]',
'[draggable="true"]',
'.tab-bar-interactive',
'.tab-bar-interactive *',
].join(',')
export function shouldUseManualWindowDrag(platform = typeof navigator === 'undefined' ? '' : navigator.platform) {
return /Win/i.test(platform)
}
export function isDesktopDragStartTarget(target: EventTarget | null): target is Element {
if (!(target instanceof Element)) return false
if (target.closest(NO_DRAG_SELECTOR)) return false
return Boolean(target.closest(DRAG_REGION_SELECTOR))
}
export function useElectronWindowDragRegions() {
useEffect(() => {
const host = getDesktopHost()
if (!host.isDesktop || !host.capabilities?.windowControls || !host.window?.startDragging) return
if (!shouldUseManualWindowDrag()) return
const previousDragMode = document.documentElement.dataset.desktopDragMode
document.documentElement.dataset.desktopDragMode = MANUAL_DRAG_MODE
let dragging = false
let lastScreenX = 0
let lastScreenY = 0
const stopDragging = () => {
dragging = false
}
const handleMouseDown = (event: MouseEvent) => {
if (event.button !== 0) return
if (!isDesktopDragStartTarget(event.target)) return
dragging = true
lastScreenX = event.screenX
lastScreenY = event.screenY
event.preventDefault()
}
const handleMouseMove = (event: MouseEvent) => {
if (!dragging) return
const deltaX = event.screenX - lastScreenX
const deltaY = event.screenY - lastScreenY
if (deltaX === 0 && deltaY === 0) return
lastScreenX = event.screenX
lastScreenY = event.screenY
void host.window.startDragging({ deltaX, deltaY }).catch(error => {
console.error('Window drag fallback failed', error)
stopDragging()
})
}
document.addEventListener('mousedown', handleMouseDown, true)
window.addEventListener('mousemove', handleMouseMove, true)
window.addEventListener('mouseup', stopDragging, true)
window.addEventListener('blur', stopDragging)
return () => {
if (previousDragMode === undefined) {
delete document.documentElement.dataset.desktopDragMode
} else {
document.documentElement.dataset.desktopDragMode = previousDragMode
}
document.removeEventListener('mousedown', handleMouseDown, true)
window.removeEventListener('mousemove', handleMouseMove, true)
window.removeEventListener('mouseup', stopDragging, true)
window.removeEventListener('blur', stopDragging)
}
}, [])
}

View File

@ -36,13 +36,28 @@ describe('electron desktop host', () => {
expect(invoke).not.toHaveBeenCalled()
})
it('does not advertise custom window chrome until Electron frameless drag is implemented', () => {
it('advertises custom window chrome for the Electron frameless shell', () => {
const host = createElectronHost({
invoke: vi.fn(),
subscribe: vi.fn(),
})
expect(host.capabilities.windowControls).toBe(false)
expect(host.capabilities.windowControls).toBe(true)
})
it('forwards drag-region fallback movement through the window dragging IPC channel', async () => {
const invoke = vi.fn().mockResolvedValue(undefined)
const host = createElectronHost({
invoke,
subscribe: vi.fn(),
})
await host.window.startDragging({ deltaX: 12, deltaY: -8 })
expect(invoke).toHaveBeenCalledWith(ELECTRON_IPC_CHANNELS.windowStartDragging, {
deltaX: 12,
deltaY: -8,
})
})
it('keeps event subscriptions behind named event channels', async () => {

View File

@ -69,7 +69,7 @@ export function createElectronHost(bridge: ElectronHostBridge): DesktopHost {
shell: true,
terminal: true,
updates: true,
windowControls: false,
windowControls: true,
zoom: true,
},
runtime: {
@ -115,7 +115,7 @@ export function createElectronHost(bridge: ElectronHostBridge): DesktopHost {
minimize: () => invoke(ELECTRON_IPC_CHANNELS.windowMinimize),
toggleMaximize: () => invoke(ELECTRON_IPC_CHANNELS.windowToggleMaximize),
close: () => invoke(ELECTRON_IPC_CHANNELS.windowClose),
startDragging: () => invoke(ELECTRON_IPC_CHANNELS.windowStartDragging),
startDragging: input => invoke(ELECTRON_IPC_CHANNELS.windowStartDragging, input),
requestAttention: () => invoke(ELECTRON_IPC_CHANNELS.windowRequestAttention),
focus: () => invoke(ELECTRON_IPC_CHANNELS.windowFocus),
isMaximized: () => invoke(ELECTRON_IPC_CHANNELS.windowIsMaximized),

View File

@ -130,6 +130,11 @@ export type PreviewHostMessage = PreviewCaptureMessage | PreviewPickerMessage
export type AppModeConfig = SettingsAppModeConfig
export type WindowDragMoveInput = {
deltaX: number
deltaY: number
}
export type AppModeSetInput = {
mode: SettingsAppMode
portableDir: string | null
@ -184,7 +189,7 @@ export type DesktopHost = {
minimize(): Promise<void>
toggleMaximize(): Promise<void>
close(): Promise<void>
startDragging(): Promise<void>
startDragging(input?: WindowDragMoveInput): Promise<void>
requestAttention(): Promise<void>
focus(): Promise<void>
isMaximized(): Promise<boolean>

View File

@ -959,10 +959,14 @@ html, body, #root {
}
/* Desktop drag region */
[data-desktop-drag-region] {
html:not([data-desktop-drag-mode="manual"]) [data-desktop-drag-region] {
app-region: drag;
-webkit-app-region: drag;
}
html[data-desktop-drag-mode="manual"] [data-desktop-drag-region] {
app-region: no-drag;
-webkit-app-region: no-drag;
}
button, input, textarea, select, a, [role="button"] {
app-region: no-drag;
-webkit-app-region: no-drag;