* 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
42 lines
1.8 KiB
TypeScript
42 lines
1.8 KiB
TypeScript
import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
|
|
|
|
/** True when running inside the native Capacitor shell (iOS/Android app), not the web/PWA. */
|
|
export const isCapacitorApp = (): boolean => {
|
|
if (typeof window === 'undefined') return false;
|
|
const capacitor = (window as typeof window & { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor;
|
|
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';
|
|
|
|
/**
|
|
* The runtime surface this client is. Used by the push presence model: only 'ios'/'android'
|
|
* count as mobile (push recipients); everything else is an interactive surface that suppresses
|
|
* mobile push while visible.
|
|
*/
|
|
export const getClientPlatform = (): ClientPlatform => {
|
|
if (typeof window !== 'undefined') {
|
|
const capacitor = (window as typeof window & { Capacitor?: { getPlatform?: () => string } }).Capacitor;
|
|
const native = capacitor?.getPlatform?.();
|
|
if (native === 'ios' || native === 'android') return native;
|
|
}
|
|
if (isVSCodeRuntime()) return 'vscode';
|
|
if (isDesktopShell()) return 'desktop';
|
|
return 'web';
|
|
};
|