feat: iPad split layout for the Capacitor app (#2104)
* fix: open mobile model/agent panels on tablet-width Capacitor shells and keep composer taps from dismissing the keyboard * feat: add iPadOS-style split layout to the Capacitor app - classify the Capacitor shell as mobile in device detection so shared surfaces (draft starters, panels) stop falling into tablet branches - add isIPadApp() and useOrientation() helpers - iPad: persistent full-height sessions sidebar (mobile sessions surface inline), Changes/Files in a right sidebar with header shortcut toggles - animate sidebar open/close like the desktop sidebars and add finger-sized drag-resize with persisted widths - anchor the overflow menu and the usage/metadata popover next to their header buttons regardless of open sidebars * fix: re-anchor metadata popover on layout shifts and untangle sidebar toggle updates - recompute the iPad metadata popover anchor via a ResizeObserver on its wrapper so sidebar toggles/resizes while it is open cannot leave it misplaced - move the portrait right-panel close out of the setIpadSidebarOpen updater into plain sequential state updates
This commit is contained in:
committed by
GitHub
parent
3d32ac7989
commit
ac93a52e21
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { isCapacitorApp } from '@/lib/platform';
|
||||
|
||||
type DeviceType = 'desktop' | 'mobile' | 'tablet';
|
||||
|
||||
@@ -122,6 +123,15 @@ export function getDeviceInfo(): DeviceInfo {
|
||||
isTablet = false;
|
||||
isDesktop = true;
|
||||
deviceType = 'desktop';
|
||||
} else if (isCapacitorApp()) {
|
||||
// The Capacitor shell IS the phone UI: every surface in that bundle is
|
||||
// built mobile-first, so wide devices (iPad, Android tablets) must not
|
||||
// fall into tablet/desktop branches scattered across shared components.
|
||||
// iPad-specific layout upgrades gate on isIPadApp()/orientation instead.
|
||||
isMobile = true;
|
||||
isTablet = false;
|
||||
isDesktop = false;
|
||||
deviceType = 'mobile';
|
||||
} else if (isMobile) {
|
||||
deviceType = 'mobile';
|
||||
} else if (isTablet) {
|
||||
@@ -348,6 +358,27 @@ export function useTabletStandalonePwaRuntime(): boolean {
|
||||
return value;
|
||||
}
|
||||
|
||||
export type Orientation = 'portrait' | 'landscape';
|
||||
|
||||
const getOrientation = (): Orientation => {
|
||||
if (typeof window === 'undefined') return 'portrait';
|
||||
return window.matchMedia?.('(orientation: landscape)')?.matches ? 'landscape' : 'portrait';
|
||||
};
|
||||
|
||||
export function useOrientation(): Orientation {
|
||||
const [orientation, setOrientation] = React.useState<Orientation>(getOrientation);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
||||
const query = window.matchMedia('(orientation: landscape)');
|
||||
const update = () => setOrientation(query.matches ? 'landscape' : 'portrait');
|
||||
update();
|
||||
return attachMediaQueryListener(query, update);
|
||||
}, []);
|
||||
|
||||
return orientation;
|
||||
}
|
||||
|
||||
export function useDeviceInfo(): DeviceInfo {
|
||||
return React.useSyncExternalStore(
|
||||
subscribeDeviceInfo,
|
||||
|
||||
@@ -7,6 +7,21 @@ export const isCapacitorApp = (): boolean => {
|
||||
return capacitor?.isNativePlatform?.() === true || window.location.protocol === 'capacitor:';
|
||||
};
|
||||
|
||||
/**
|
||||
* True when running inside the native Capacitor shell on an iPad.
|
||||
* Capacitor reports 'ios' for both iPhone and iPad; iPadOS WKWebView
|
||||
* masquerades as macOS Safari, so the only reliable tell is a Mac-like
|
||||
* platform with real touch points (or a legacy explicit iPad UA).
|
||||
*/
|
||||
export const isIPadApp = (): boolean => {
|
||||
if (typeof window === 'undefined' || !isCapacitorApp()) return false;
|
||||
if (getClientPlatform() !== 'ios') return false;
|
||||
const userAgent = navigator.userAgent || '';
|
||||
const maxTouchPoints = navigator.maxTouchPoints ?? 0;
|
||||
return /iPad/i.test(userAgent)
|
||||
|| (/Macintosh|MacIntel/i.test(userAgent) && maxTouchPoints > 1);
|
||||
};
|
||||
|
||||
export type ClientPlatform = 'ios' | 'android' | 'vscode' | 'desktop' | 'web';
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user