From 344481cc1e93356405de95975fe8882eb4ce5a7c Mon Sep 17 00:00:00 2001 From: DHX <2587450776@qq.com> Date: Thu, 14 May 2026 20:41:42 +0800 Subject: [PATCH 1/2] fix(settings): make popup components respect UI zoom scaling Modal, Toast, and dropdown menus were rendered to document.body via createPortal, bypassing the CSS zoom applied to .app-shell. Now all overlay components portal to #app-overlay-root inside AppShell so they inherit the zoom transform. Co-Authored-By: Claude Opus 4.7 --- .../components/controls/PermissionModeSelector.tsx | 3 ++- desktop/src/components/layout/AppShell.tsx | 1 + desktop/src/components/shared/DirectoryPicker.tsx | 3 ++- desktop/src/components/shared/Modal.tsx | 3 ++- .../components/shared/RepositoryLaunchControls.tsx | 3 ++- desktop/src/components/shared/Toast.tsx | 7 ++++--- desktop/src/lib/overlayRoot.tsx | 14 ++++++++++++++ 7 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 desktop/src/lib/overlayRoot.tsx diff --git a/desktop/src/components/controls/PermissionModeSelector.tsx b/desktop/src/components/controls/PermissionModeSelector.tsx index 1fb2e083a6..2a56f5da05 100644 --- a/desktop/src/components/controls/PermissionModeSelector.tsx +++ b/desktop/src/components/controls/PermissionModeSelector.tsx @@ -10,6 +10,7 @@ import type { PermissionMode } from '../../types/settings' import { useMobileViewport } from '../../hooks/useMobileViewport' import { isTauriRuntime } from '../../lib/desktopRuntime' import { MobileBottomSheet } from '../shared/MobileBottomSheet' +import { getOverlayRoot } from '../../lib/overlayRoot' const MODE_ICONS: Record = { default: 'verified_user', @@ -278,7 +279,7 @@ export function PermissionModeSelector({ workDir: workDirProp, compact = false, , - document.body, + getOverlayRoot(), )} ) diff --git a/desktop/src/components/layout/AppShell.tsx b/desktop/src/components/layout/AppShell.tsx index af09547bf1..256d4b02b8 100644 --- a/desktop/src/components/layout/AppShell.tsx +++ b/desktop/src/components/layout/AppShell.tsx @@ -274,6 +274,7 @@ export function AppShell() { +
) } diff --git a/desktop/src/components/shared/DirectoryPicker.tsx b/desktop/src/components/shared/DirectoryPicker.tsx index 3d063955f0..9f4590df45 100644 --- a/desktop/src/components/shared/DirectoryPicker.tsx +++ b/desktop/src/components/shared/DirectoryPicker.tsx @@ -5,6 +5,7 @@ import { filesystemApi } from '../../api/filesystem' import { useTranslation } from '../../i18n' import { useMobileViewport } from '../../hooks/useMobileViewport' import { MobileBottomSheet } from './MobileBottomSheet' +import { getOverlayRoot } from '../../lib/overlayRoot' type Props = { value: string @@ -345,7 +346,7 @@ export function DirectoryPicker({ value, onChange, variant = 'chip', isGitProjec > {dropdownContent} , - document.body, + getOverlayRoot(), ) )} diff --git a/desktop/src/components/shared/Modal.tsx b/desktop/src/components/shared/Modal.tsx index adaa82c596..50da9213a4 100644 --- a/desktop/src/components/shared/Modal.tsx +++ b/desktop/src/components/shared/Modal.tsx @@ -1,5 +1,6 @@ import { useEffect, type ReactNode } from 'react' import { createPortal } from 'react-dom' +import { getOverlayRoot } from '../../lib/overlayRoot' type ModalProps = { open: boolean @@ -63,6 +64,6 @@ export function Modal({ open, onClose, title, children, width = 560, footer }: M )} , - document.body, + getOverlayRoot(), ) } diff --git a/desktop/src/components/shared/RepositoryLaunchControls.tsx b/desktop/src/components/shared/RepositoryLaunchControls.tsx index de78fdb7e7..3ecd6af793 100644 --- a/desktop/src/components/shared/RepositoryLaunchControls.tsx +++ b/desktop/src/components/shared/RepositoryLaunchControls.tsx @@ -19,6 +19,7 @@ import { DirectoryPicker } from './DirectoryPicker' import { useMobileViewport } from '../../hooks/useMobileViewport' import { isTauriRuntime } from '../../lib/desktopRuntime' import { MobileBottomSheet } from './MobileBottomSheet' +import { getOverlayRoot } from '../../lib/overlayRoot' type Props = { workDir: string @@ -555,7 +556,7 @@ export function RepositoryLaunchControls({ })} , - document.body, + getOverlayRoot(), ) )} diff --git a/desktop/src/components/shared/Toast.tsx b/desktop/src/components/shared/Toast.tsx index a48b1735fe..57da6a7a4f 100644 --- a/desktop/src/components/shared/Toast.tsx +++ b/desktop/src/components/shared/Toast.tsx @@ -1,4 +1,5 @@ import { useUIStore, type Toast as ToastType } from '../../stores/uiStore' +import { createOverlayPortal } from '../../lib/overlayRoot' const typeStyles: Record = { success: 'border-l-4 border-l-[var(--color-success)]', @@ -37,11 +38,11 @@ export function ToastContainer() { if (toasts.length === 0) return null - return ( + return createOverlayPortal(
{toasts.map((toast) => ( ))} -
+ , ) -} +} \ No newline at end of file diff --git a/desktop/src/lib/overlayRoot.tsx b/desktop/src/lib/overlayRoot.tsx new file mode 100644 index 0000000000..2141f1f1dd --- /dev/null +++ b/desktop/src/lib/overlayRoot.tsx @@ -0,0 +1,14 @@ +import { createPortal } from 'react-dom' + +let overlayRoot: HTMLElement | null = null + +export function getOverlayRoot(): HTMLElement { + if (!overlayRoot) { + overlayRoot = document.getElementById('app-overlay-root') ?? document.body + } + return overlayRoot +} + +export function createOverlayPortal(children: React.ReactNode): React.ReactPortal { + return createPortal(children, getOverlayRoot()) +} \ No newline at end of file From b03b5709fd046c31b9dac84a8368ae3c21e5d9eb Mon Sep 17 00:00:00 2001 From: DHX <2587450776@qq.com> Date: Thu, 14 May 2026 22:54:43 +0800 Subject: [PATCH 2/2] fix(contextmenu): correct context menu positioning for UI zoom scaling - Use mousedown with button check (e.button !== 0) to prevent menu closing on right-click - Add onClick stopPropagation on all context menu divs to prevent event bubbling - Apply uiZoom scaling correction to context menu positioning across Sidebar, TabBar, and WorkspacePanel Co-Authored-By: Claude Opus 4.7 --- desktop/src/components/layout/Sidebar.tsx | 18 ++++++++++--- desktop/src/components/layout/TabBar.tsx | 21 ++++++++++++--- .../components/workspace/WorkspacePanel.tsx | 26 ++++++++++++++++--- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/desktop/src/components/layout/Sidebar.tsx b/desktop/src/components/layout/Sidebar.tsx index 15f9897691..7a85a5dd55 100644 --- a/desktop/src/components/layout/Sidebar.tsx +++ b/desktop/src/components/layout/Sidebar.tsx @@ -1,6 +1,7 @@ import { useEffect, useState, useCallback, useMemo, useRef } from 'react' import { useSessionStore } from '../../stores/sessionStore' import { useUIStore } from '../../stores/uiStore' +import { useSettingsStore } from '../../stores/settingsStore' import { useTranslation } from '../../i18n' import { ProjectFilter } from './ProjectFilter' import { ConfirmDialog } from '../shared/ConfirmDialog' @@ -43,8 +44,10 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) { const activeTabId = useTabStore((s) => s.activeTabId) const closeTab = useTabStore((s) => s.closeTab) const disconnectSession = useChatStore((s) => s.disconnectSession) + const uiZoom = useSettingsStore((s) => s.uiZoom) const [searchQuery, setSearchQuery] = useState('') const [contextMenu, setContextMenu] = useState<{ id: string; x: number; y: number } | null>(null) + const [rightClickedSessionId, setRightClickedSessionId] = useState(null) const [pendingDeleteSessionId, setPendingDeleteSessionId] = useState(null) const [pendingBatchDeleteSessionIds, setPendingBatchDeleteSessionIds] = useState(null) const [isBatchDeleting, setIsBatchDeleting] = useState(false) @@ -64,9 +67,13 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) { useEffect(() => { if (!contextMenu) return - const close = () => setContextMenu(null) - document.addEventListener('click', close) - return () => document.removeEventListener('click', close) + const close = (e: MouseEvent) => { + if (e.button !== 0) return + setContextMenu(null) + setRightClickedSessionId(null) + } + document.addEventListener('mousedown', close) + return () => document.removeEventListener('mousedown', close) }, [contextMenu]) const filteredSessions = useMemo(() => { @@ -100,6 +107,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) { e.preventDefault() if (isBatchMode) return setContextMenu({ id, x: e.clientX, y: e.clientY }) + setRightClickedSessionId(id) }, [isBatchMode]) const handleDelete = useCallback((id: string) => { @@ -529,6 +537,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) { ? 'sidebar-session-row--active border-transparent bg-[var(--color-sidebar-item-active)] text-[var(--color-text-primary)]' : 'sidebar-session-row--idle border-transparent text-[var(--color-text-secondary)] hover:bg-[var(--color-sidebar-item-hover)]' } + ${session.id === rightClickedSessionId ? 'ring-2 ring-[var(--color-brand)] ring-offset-1 ring-offset-[var(--color-surface)]' : ''} `} aria-pressed={isBatchMode ? selectedSessionIds.has(session.id) : undefined} > @@ -602,8 +611,9 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) { {contextMenu && (
e.stopPropagation()} className="fixed z-50 min-w-[140px] rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] py-1" - style={{ left: contextMenu.x, top: contextMenu.y, boxShadow: 'var(--shadow-dropdown)' }} + style={{ left: contextMenu.x / uiZoom, top: contextMenu.y / uiZoom, boxShadow: 'var(--shadow-dropdown)' }} >