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:
@@ -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 ? (
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user