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
@@ -556,6 +556,16 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl
|
||||
type="button"
|
||||
className={footerIconButtonClass}
|
||||
onClick={props.onOpenMobileSheet}
|
||||
// Same guard as PermissionAutoAcceptButton: keep the tap
|
||||
// from dismissing the keyboard. On Android's
|
||||
// resizes-content viewport the keyboard-close relayout
|
||||
// moves this button mid-tap and the click never lands.
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onPointerDownCapture={(event) => {
|
||||
if (event.pointerType === 'touch') {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
title={t('chat.chatInput.actions.addAttachment')}
|
||||
aria-label={t('chat.chatInput.actions.addAttachment')}
|
||||
>
|
||||
@@ -4087,7 +4097,12 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
}, [newSessionDraftOpen, openNewSessionDraft, currentDirectory]);
|
||||
|
||||
const openMobileAttachSheet = React.useCallback(() => {
|
||||
// Same order as handleOpenMobilePanel: mark the sheet open BEFORE the
|
||||
// blur so the collapse watcher sees an overlay when the keyboard-close
|
||||
// lands. The trigger button blocks the tap's own focus transfer, so
|
||||
// the keyboard must be dismissed explicitly here.
|
||||
setMobileAttachMenuOpen(true);
|
||||
textareaRef.current?.blur();
|
||||
}, []);
|
||||
|
||||
const mobileComposerExpandedRef = React.useRef(mobileComposerExpanded);
|
||||
|
||||
@@ -22,6 +22,16 @@ export const MobileModelButton: React.FC<MobileModelButtonProps> = ({ onOpenMode
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenModel}
|
||||
// Same guard as PermissionAutoAcceptButton/MobileAgentButton: block
|
||||
// the focus transfer so the tap doesn't dismiss the keyboard. With
|
||||
// interactive-widget=resizes-content (Android), the keyboard-close
|
||||
// relayout moves this button mid-tap and the click never lands.
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onPointerDownCapture={(event) => {
|
||||
if (event.pointerType === 'touch') {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'inline-flex min-w-0 items-stretch',
|
||||
'rounded-lg',
|
||||
|
||||
@@ -386,7 +386,15 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
const [isAgentSelectorOpen, setIsAgentSelectorOpen] = React.useState(false);
|
||||
const { favoriteModelsList, recentModelsList } = useModelLists();
|
||||
|
||||
const { isMobile } = useDeviceInfo();
|
||||
const { isMobile: deviceIsMobile } = useDeviceInfo();
|
||||
// The composer decides whether it renders the mobile layout from the UI
|
||||
// store (the Capacitor shell forces it true even on tablets/iPad, where
|
||||
// useDeviceInfo classifies the wide screen as non-mobile). The bottom-sheet
|
||||
// panels must follow the SAME source: with the device flag alone, tapping
|
||||
// the model/agent chip on an iPad set the panel state while the sheet
|
||||
// itself rendered null.
|
||||
const uiIsMobile = useUIStore((state) => state.isMobile);
|
||||
const isMobile = deviceIsMobile || uiIsMobile;
|
||||
const isDesktop = React.useMemo(() => isDesktopShell(), []);
|
||||
const isVSCodeRuntime = useIsVSCodeRuntime();
|
||||
// Only use mobile panels on actual mobile devices, VSCode uses desktop dropdowns
|
||||
|
||||
Reference in New Issue
Block a user