fix: avoid iPad PWA traffic light overlap

Adds tablet standalone PWA detection
Reuses macOS traffic-light header spacing
Protects session sidebar and multirun headers
This commit is contained in:
Bohdan Triapitsyn
2026-05-03 21:39:30 +03:00
parent 9964dcdb99
commit fc2f4746e5
5 changed files with 83 additions and 11 deletions
+8 -5
View File
@@ -34,7 +34,7 @@ import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
import { useDeviceInfo } from '@/lib/device';
import { useDeviceInfo, useTabletStandalonePwaRuntime } from '@/lib/device';
import { cn, hasModifier } from '@/lib/utils';
import { McpDropdownContent } from '@/components/mcp/McpDropdown';
import { McpIcon } from '@/components/icons/McpIcon';
@@ -720,6 +720,7 @@ export const Header: React.FC<HeaderProps> = ({
}
return isDesktopShell();
});
const isTabletStandalonePwa = useTabletStandalonePwaRuntime();
const [isDesktopWindowFullscreen, setIsDesktopWindowFullscreen] = React.useState(false);
const isMacPlatform = React.useMemo(() => {
@@ -1328,11 +1329,11 @@ export const Header: React.FC<HeaderProps> = ({
const mobileHeaderIconButtonClass = MOBILE_HEADER_ICON_BUTTON_CLASS;
const desktopPaddingClass = React.useMemo(() => {
if (!isSidebarOpen && isDesktopApp && isMacPlatform && !isDesktopWindowFullscreen) {
if (!isSidebarOpen && ((isDesktopApp && isMacPlatform && !isDesktopWindowFullscreen) || isTabletStandalonePwa)) {
return 'pl-[5.5rem]';
}
return 'pl-3';
}, [isDesktopApp, isDesktopWindowFullscreen, isMacPlatform, isSidebarOpen]);
}, [isDesktopApp, isDesktopWindowFullscreen, isMacPlatform, isSidebarOpen, isTabletStandalonePwa]);
useEffect(() => {
if (!isDesktopApp || !isMacPlatform) {
@@ -1400,12 +1401,14 @@ export const Header: React.FC<HeaderProps> = ({
}
return {
paddingLeft: 'calc(0.75rem + var(--oc-wco-left-inset, 0px))',
paddingLeft: isTabletStandalonePwa
? 'max(calc(0.75rem + var(--oc-wco-left-inset, 0px)), 5.5rem)'
: 'calc(0.75rem + var(--oc-wco-left-inset, 0px))',
paddingRight: 'calc(0.75rem + var(--oc-wco-right-inset, 0px))',
minHeight: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
height: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
};
}, [isDesktopApp, isVSCode]);
}, [isDesktopApp, isTabletStandalonePwa, isVSCode]);
const updateHeaderHeight = React.useCallback(() => {
if (typeof document === 'undefined') {
@@ -22,6 +22,7 @@ import { AgentSelector } from './AgentSelector';
import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from '@/components/chat/CommandAutocomplete';
import { FileMentionAutocomplete, type FileMentionHandle } from '@/components/chat/FileMentionAutocomplete';
import { isDesktopShell } from '@/lib/desktop';
import { useTabletStandalonePwaRuntime } from '@/lib/device';
import { useThemeSystem } from '@/contexts/useThemeSystem';
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
import type { ProjectEntry } from '@/lib/api/types';
@@ -215,6 +216,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
}
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
}, []);
const isTabletStandalonePwa = useTabletStandalonePwaRuntime();
React.useEffect(() => {
if (typeof window === 'undefined') {
@@ -250,12 +252,12 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
}, []);
const desktopHeaderPaddingClass = React.useMemo(() => {
if (isDesktopApp && isMacPlatform) {
// Match main app header: reserve space for Mac traffic lights.
if ((isDesktopApp && isMacPlatform) || isTabletStandalonePwa) {
// Match main app header: reserve space for Mac/iPadOS traffic lights.
return 'pl-[5.5rem]';
}
return 'pl-3';
}, [isDesktopApp, isMacPlatform]);
}, [isDesktopApp, isMacPlatform, isTabletStandalonePwa]);
const macosHeaderSizeClass = React.useMemo(() => {
if (!isDesktopApp || !isMacPlatform || macosMajorVersion === null) {
@@ -4,7 +4,7 @@ import { RiLayoutLeftLine } from '@remixicon/react';
import { toast } from '@/components/ui';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useI18n } from '@/lib/i18n';
import { useDeviceInfo } from '@/lib/device';
import { useDeviceInfo, useTabletStandalonePwaRuntime } from '@/lib/device';
import { isDesktopShell } from '@/lib/desktop';
import { isDesktopWindowFullscreen as getDesktopWindowFullscreen, onDesktopWindowResized, startDesktopWindowDrag } from '@/lib/desktopNative';
import { sessionEvents } from '@/lib/sessionEvents';
@@ -435,6 +435,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
}, []);
const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []);
const isTabletStandalonePwa = useTabletStandalonePwaRuntime();
const [isDesktopWindowFullscreen, setIsDesktopWindowFullscreen] = React.useState(false);
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
@@ -448,7 +449,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
}, []);
const isWebRuntime = !mobileVariant && !isVSCode && !isDesktopShellRuntime;
const showDesktopSidebarChrome = !mobileVariant && !isVSCode && !isWebRuntime;
const desktopSidebarTopPaddingClass = isDesktopShellRuntime && isMacPlatform && !isDesktopWindowFullscreen ? 'pl-[5.5rem]' : 'pl-3';
const desktopSidebarTopPaddingClass = (isDesktopShellRuntime && isMacPlatform && !isDesktopWindowFullscreen) || isTabletStandalonePwa ? 'pl-[5.5rem]' : 'pl-3';
const desktopSidebarToggleButtonClass = 'app-region-no-drag inline-flex h-8 w-8 items-center justify-center rounded-md typography-ui-label font-medium text-foreground transition-colors hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50';
React.useEffect(() => {
@@ -1680,6 +1681,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
onToggleSelectionMode={handleToggleSelectionMode}
showSidebarToggle={isWebRuntime}
onToggleSidebar={toggleSidebar}
avoidWindowControlsOverlay={isTabletStandalonePwa}
/>
<SidebarProjectsList
@@ -48,6 +48,7 @@ type Props = {
onToggleSelectionMode: () => void;
showSidebarToggle?: boolean;
onToggleSidebar?: () => void;
avoidWindowControlsOverlay?: boolean;
};
export function SidebarHeader(props: Props): React.ReactNode {
@@ -75,6 +76,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
onToggleSelectionMode,
showSidebarToggle = false,
onToggleSidebar,
avoidWindowControlsOverlay = false,
} = props;
const displayMode = useSessionDisplayStore((state) => state.displayMode);
@@ -90,7 +92,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
<div
className={cn(
'select-none flex-shrink-0',
showSidebarToggle ? 'pl-3 pr-3' : 'px-2.5 py-1',
showSidebarToggle ? (avoidWindowControlsOverlay ? 'pl-[5.5rem] pr-3' : 'pl-3 pr-3') : 'px-2.5 py-1',
)}
>
{reserveHeaderActionsSpace ? (
+63
View File
@@ -141,6 +141,69 @@ export function isMobileDeviceViaCSS(): boolean {
return isMobileValue === '1' || isMobileValue === 'true';
}
export const isStandalonePwaRuntime = (): boolean => {
if (typeof window === 'undefined') return false;
const standaloneNavigator = navigator as Navigator & { standalone?: boolean };
return Boolean(
standaloneNavigator.standalone === true
|| window.matchMedia?.('(display-mode: standalone)')?.matches
|| window.matchMedia?.('(display-mode: fullscreen)')?.matches
);
};
export const isTabletStandalonePwaRuntime = (): boolean => {
if (typeof window === 'undefined' || isDesktopShell()) return false;
const maxTouchPoints = typeof navigator !== 'undefined' ? navigator.maxTouchPoints ?? 0 : 0;
return isStandalonePwaRuntime() && maxTouchPoints > 0 && window.innerWidth > BREAKPOINTS.md;
};
export function useTabletStandalonePwaRuntime(): boolean {
const [value, setValue] = React.useState<boolean>(() => isTabletStandalonePwaRuntime());
React.useEffect(() => {
if (typeof window === 'undefined') return;
const update = () => setValue(isTabletStandalonePwaRuntime());
const standaloneQuery = window.matchMedia?.('(display-mode: standalone)');
const fullscreenQuery = window.matchMedia?.('(display-mode: fullscreen)');
update();
window.addEventListener('resize', update);
window.addEventListener('focus', update);
const addQueryListener = (query: MediaQueryList | undefined) => {
if (!query) return;
if (typeof query.addEventListener === 'function') {
query.addEventListener('change', update);
} else if (typeof query.addListener === 'function') {
query.addListener(update);
}
};
const removeQueryListener = (query: MediaQueryList | undefined) => {
if (!query) return;
if (typeof query.removeEventListener === 'function') {
query.removeEventListener('change', update);
} else if (typeof query.removeListener === 'function') {
query.removeListener(update);
}
};
addQueryListener(standaloneQuery);
addQueryListener(fullscreenQuery);
return () => {
window.removeEventListener('resize', update);
window.removeEventListener('focus', update);
removeQueryListener(standaloneQuery);
removeQueryListener(fullscreenQuery);
};
}, []);
return value;
}
export function useDeviceInfo(): DeviceInfo {
const [deviceInfo, setDeviceInfo] = React.useState<DeviceInfo>(() => {
if (typeof window === 'undefined') {