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 { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo, useTabletStandalonePwaRuntime } from '@/lib/device';
|
||||||
import { cn, hasModifier } from '@/lib/utils';
|
import { cn, hasModifier } from '@/lib/utils';
|
||||||
import { McpDropdownContent } from '@/components/mcp/McpDropdown';
|
import { McpDropdownContent } from '@/components/mcp/McpDropdown';
|
||||||
import { McpIcon } from '@/components/icons/McpIcon';
|
import { McpIcon } from '@/components/icons/McpIcon';
|
||||||
@@ -720,6 +720,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
}
|
}
|
||||||
return isDesktopShell();
|
return isDesktopShell();
|
||||||
});
|
});
|
||||||
|
const isTabletStandalonePwa = useTabletStandalonePwaRuntime();
|
||||||
const [isDesktopWindowFullscreen, setIsDesktopWindowFullscreen] = React.useState(false);
|
const [isDesktopWindowFullscreen, setIsDesktopWindowFullscreen] = React.useState(false);
|
||||||
|
|
||||||
const isMacPlatform = React.useMemo(() => {
|
const isMacPlatform = React.useMemo(() => {
|
||||||
@@ -1328,11 +1329,11 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
const mobileHeaderIconButtonClass = MOBILE_HEADER_ICON_BUTTON_CLASS;
|
const mobileHeaderIconButtonClass = MOBILE_HEADER_ICON_BUTTON_CLASS;
|
||||||
|
|
||||||
const desktopPaddingClass = React.useMemo(() => {
|
const desktopPaddingClass = React.useMemo(() => {
|
||||||
if (!isSidebarOpen && isDesktopApp && isMacPlatform && !isDesktopWindowFullscreen) {
|
if (!isSidebarOpen && ((isDesktopApp && isMacPlatform && !isDesktopWindowFullscreen) || isTabletStandalonePwa)) {
|
||||||
return 'pl-[5.5rem]';
|
return 'pl-[5.5rem]';
|
||||||
}
|
}
|
||||||
return 'pl-3';
|
return 'pl-3';
|
||||||
}, [isDesktopApp, isDesktopWindowFullscreen, isMacPlatform, isSidebarOpen]);
|
}, [isDesktopApp, isDesktopWindowFullscreen, isMacPlatform, isSidebarOpen, isTabletStandalonePwa]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDesktopApp || !isMacPlatform) {
|
if (!isDesktopApp || !isMacPlatform) {
|
||||||
@@ -1400,12 +1401,14 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
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))',
|
paddingRight: 'calc(0.75rem + var(--oc-wco-right-inset, 0px))',
|
||||||
minHeight: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
|
minHeight: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
|
||||||
height: '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(() => {
|
const updateHeaderHeight = React.useCallback(() => {
|
||||||
if (typeof document === 'undefined') {
|
if (typeof document === 'undefined') {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { AgentSelector } from './AgentSelector';
|
|||||||
import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from '@/components/chat/CommandAutocomplete';
|
import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from '@/components/chat/CommandAutocomplete';
|
||||||
import { FileMentionAutocomplete, type FileMentionHandle } from '@/components/chat/FileMentionAutocomplete';
|
import { FileMentionAutocomplete, type FileMentionHandle } from '@/components/chat/FileMentionAutocomplete';
|
||||||
import { isDesktopShell } from '@/lib/desktop';
|
import { isDesktopShell } from '@/lib/desktop';
|
||||||
|
import { useTabletStandalonePwaRuntime } from '@/lib/device';
|
||||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
|
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
|
||||||
import type { ProjectEntry } from '@/lib/api/types';
|
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 || '');
|
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
|
||||||
}, []);
|
}, []);
|
||||||
|
const isTabletStandalonePwa = useTabletStandalonePwaRuntime();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@@ -250,12 +252,12 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const desktopHeaderPaddingClass = React.useMemo(() => {
|
const desktopHeaderPaddingClass = React.useMemo(() => {
|
||||||
if (isDesktopApp && isMacPlatform) {
|
if ((isDesktopApp && isMacPlatform) || isTabletStandalonePwa) {
|
||||||
// Match main app header: reserve space for Mac traffic lights.
|
// Match main app header: reserve space for Mac/iPadOS traffic lights.
|
||||||
return 'pl-[5.5rem]';
|
return 'pl-[5.5rem]';
|
||||||
}
|
}
|
||||||
return 'pl-3';
|
return 'pl-3';
|
||||||
}, [isDesktopApp, isMacPlatform]);
|
}, [isDesktopApp, isMacPlatform, isTabletStandalonePwa]);
|
||||||
|
|
||||||
const macosHeaderSizeClass = React.useMemo(() => {
|
const macosHeaderSizeClass = React.useMemo(() => {
|
||||||
if (!isDesktopApp || !isMacPlatform || macosMajorVersion === null) {
|
if (!isDesktopApp || !isMacPlatform || macosMajorVersion === null) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { RiLayoutLeftLine } from '@remixicon/react';
|
|||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo, useTabletStandalonePwaRuntime } from '@/lib/device';
|
||||||
import { isDesktopShell } from '@/lib/desktop';
|
import { isDesktopShell } from '@/lib/desktop';
|
||||||
import { isDesktopWindowFullscreen as getDesktopWindowFullscreen, onDesktopWindowResized, startDesktopWindowDrag } from '@/lib/desktopNative';
|
import { isDesktopWindowFullscreen as getDesktopWindowFullscreen, onDesktopWindowResized, startDesktopWindowDrag } from '@/lib/desktopNative';
|
||||||
import { sessionEvents } from '@/lib/sessionEvents';
|
import { sessionEvents } from '@/lib/sessionEvents';
|
||||||
@@ -435,6 +435,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []);
|
const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []);
|
||||||
|
const isTabletStandalonePwa = useTabletStandalonePwaRuntime();
|
||||||
const [isDesktopWindowFullscreen, setIsDesktopWindowFullscreen] = React.useState(false);
|
const [isDesktopWindowFullscreen, setIsDesktopWindowFullscreen] = React.useState(false);
|
||||||
|
|
||||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
@@ -448,7 +449,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
const isWebRuntime = !mobileVariant && !isVSCode && !isDesktopShellRuntime;
|
const isWebRuntime = !mobileVariant && !isVSCode && !isDesktopShellRuntime;
|
||||||
const showDesktopSidebarChrome = !mobileVariant && !isVSCode && !isWebRuntime;
|
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';
|
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(() => {
|
React.useEffect(() => {
|
||||||
@@ -1680,6 +1681,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
onToggleSelectionMode={handleToggleSelectionMode}
|
onToggleSelectionMode={handleToggleSelectionMode}
|
||||||
showSidebarToggle={isWebRuntime}
|
showSidebarToggle={isWebRuntime}
|
||||||
onToggleSidebar={toggleSidebar}
|
onToggleSidebar={toggleSidebar}
|
||||||
|
avoidWindowControlsOverlay={isTabletStandalonePwa}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SidebarProjectsList
|
<SidebarProjectsList
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ type Props = {
|
|||||||
onToggleSelectionMode: () => void;
|
onToggleSelectionMode: () => void;
|
||||||
showSidebarToggle?: boolean;
|
showSidebarToggle?: boolean;
|
||||||
onToggleSidebar?: () => void;
|
onToggleSidebar?: () => void;
|
||||||
|
avoidWindowControlsOverlay?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SidebarHeader(props: Props): React.ReactNode {
|
export function SidebarHeader(props: Props): React.ReactNode {
|
||||||
@@ -75,6 +76,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
|
|||||||
onToggleSelectionMode,
|
onToggleSelectionMode,
|
||||||
showSidebarToggle = false,
|
showSidebarToggle = false,
|
||||||
onToggleSidebar,
|
onToggleSidebar,
|
||||||
|
avoidWindowControlsOverlay = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const displayMode = useSessionDisplayStore((state) => state.displayMode);
|
const displayMode = useSessionDisplayStore((state) => state.displayMode);
|
||||||
@@ -90,7 +92,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'select-none flex-shrink-0',
|
'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 ? (
|
{reserveHeaderActionsSpace ? (
|
||||||
|
|||||||
@@ -141,6 +141,69 @@ export function isMobileDeviceViaCSS(): boolean {
|
|||||||
return isMobileValue === '1' || isMobileValue === 'true';
|
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 {
|
export function useDeviceInfo(): DeviceInfo {
|
||||||
const [deviceInfo, setDeviceInfo] = React.useState<DeviceInfo>(() => {
|
const [deviceInfo, setDeviceInfo] = React.useState<DeviceInfo>(() => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
|
|||||||
Reference in New Issue
Block a user