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:
Bohdan Triapitsyn
2026-07-08 21:52:18 +03:00
committed by GitHub
parent 3d32ac7989
commit ac93a52e21
7 changed files with 568 additions and 55 deletions
+33 -9
View File
@@ -60,6 +60,9 @@ import { MobileSurfaceShell } from './MobileSurfaceShell';
type MobileSessionsSheetProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
/** 'sheet' (default) wraps the content in the swipe-dismiss MobileSurfaceShell;
'sidebar' renders the same content inline for the iPad persistent sidebar. */
variant?: 'sheet' | 'sidebar';
};
type ProjectMeta = {
@@ -511,7 +514,7 @@ const SortableProjectRow: React.FC<{
);
};
export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open, onOpenChange }) => {
export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open, onOpenChange, variant = 'sheet' }) => {
const { t } = useI18n();
const { git } = useRuntimeAPIs();
const liveSessions = useAllLiveSessions();
@@ -999,14 +1002,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
</>
) : null;
return (
<MobileSurfaceShell
open={open}
onClose={() => onOpenChange(false)}
ariaLabel={t('mobile.sessions.sheet.title')}
title={t('mobile.sessions.sheet.title')}
trailing={trailingActions}
>
const surfaceContent = (
<div className="flex h-full flex-col">
<div className={cn('shrink-0 px-4 pb-2 pt-1', editingOrder && 'hidden')}>
<div className="relative">
@@ -1294,6 +1290,34 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
onWorktreesChanged={() => setWorktreeRefreshKey((value) => value + 1)}
/>
</div>
);
if (variant === 'sidebar') {
if (!open) return null;
return (
<div className="flex h-full min-h-0 flex-col">
<div className="flex h-[var(--oc-header-height,56px)] shrink-0 items-center justify-between gap-2 border-b border-border/30 px-4">
<h2 className="truncate typography-ui-label font-semibold text-foreground">
{t('mobile.sessions.sheet.title')}
</h2>
{trailingActions ? (
<div className="flex shrink-0 items-center gap-2">{trailingActions}</div>
) : null}
</div>
{surfaceContent}
</div>
);
}
return (
<MobileSurfaceShell
open={open}
onClose={() => onOpenChange(false)}
ariaLabel={t('mobile.sessions.sheet.title')}
title={t('mobile.sessions.sheet.title')}
trailing={trailingActions}
>
{surfaceContent}
</MobileSurfaceShell>
);
};