refactor: drop mobile keyboard/viewport hacks, rely on browser

Remove body position:fixed lock, visualViewport listener in MainLayout
(keyboard-inset heuristics, scroll-to-zero lock, focusin/focusout RAF),
[data-keyboard-avoid-active] translateY rule, and related CSS vars
(--oc-keyboard-inset, --oc-keyboard-avoid-offset, --oc-keyboard-home-indicator,
--oc-visual-viewport-offset-top). Strip data-keyboard-avoid* attrs and
keyboardAvoid props from Dialog/ScrollableOverlay and consumers. Drop
isKeyboardOpen from useUIStore.

With body unlocked and the layout plain flex-col (h-dvh), the browser
shrinks the viewport on keyboard open naturally — composer sits above
the keyboard, header stops lagging, input no longer jitters.

FilesView.nudgeEditorSelectionAboveKeyboard now derives the occluded
bottom locally from visualViewport + documentElement.clientHeight.
This commit is contained in:
Bohdan Triapitsyn
2026-04-24 12:30:11 +03:00
parent 9d9af7e263
commit 6a70d51ea7
34 changed files with 134 additions and 496 deletions
@@ -640,39 +640,33 @@ export const ChatContainer: React.FC = () => {
void load();
}, [currentSessionId, hasLoadedSessionMessages, isPinned, loadMessages, resumeToLatestInstant, sessionStatusForCurrent.type]);
if (!currentSessionId && !draftOpen) {
return (
<div
className="flex flex-col h-full bg-background"
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
>
<ChatEmptyState />
</div>
);
}
if (!currentSessionId && !draftOpen) {
return (
<div className="flex flex-col h-full bg-background">
<ChatEmptyState />
</div>
);
}
if (!currentSessionId && draftOpen) {
return (
<div
className="relative flex flex-col h-full bg-background transform-gpu"
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
>
{!isDesktopExpandedInput ? (
<div className="flex-1 flex items-center justify-center">
<ChatEmptyState />
</div>
) : null}
if (!currentSessionId && draftOpen) {
return (
<div className="relative flex flex-col h-full bg-background transform-gpu">
{!isDesktopExpandedInput ? (
<div className="flex-1 flex items-center justify-center">
<ChatEmptyState />
</div>
) : null}
<div
className={cn(
'relative z-10',
isDesktopExpandedInput
? 'flex-1 min-h-0 bg-background'
: 'bg-background'
)}
>
<ChatInput scrollToBottom={resumeToLatestInstant} />
</div>
</div>
isDesktopExpandedInput
? 'flex-1 min-h-0 bg-background'
: 'bg-background'
)}
>
<ChatInput scrollToBottom={resumeToLatestInstant} />
</div>
</div>
);
}
@@ -680,16 +674,13 @@ export const ChatContainer: React.FC = () => {
return null;
}
if (isSessionHydrating && sessionMessages.length === 0 && !streamingMessageId) {
return (
<div
className="relative flex flex-col h-full bg-background"
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
>
{returnToParentButton}
<div
className={cn(
'relative min-h-0',
if (isSessionHydrating && sessionMessages.length === 0 && !streamingMessageId) {
return (
<div className="relative flex flex-col h-full bg-background">
{returnToParentButton}
<div
className={cn(
'relative min-h-0',
isDesktopExpandedInput
? 'absolute inset-0 opacity-0 pointer-events-none'
: 'flex-1'
@@ -728,26 +719,23 @@ export const ChatContainer: React.FC = () => {
<div
className={cn(
'relative z-10',
isDesktopExpandedInput
? 'flex-1 min-h-0 bg-background'
: 'bg-background'
)}
>
<ChatInput scrollToBottom={resumeToLatestInstant} />
</div>
isDesktopExpandedInput
? 'flex-1 min-h-0 bg-background'
: 'bg-background'
)}
>
<ChatInput scrollToBottom={resumeToLatestInstant} />
</div>
</div>
);
}
if (sessionMessages.length === 0 && !streamingMessageId) {
return (
<div
className="relative flex flex-col h-full bg-background transform-gpu"
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
>
{returnToParentButton}
<div
className={cn(
if (sessionMessages.length === 0 && !streamingMessageId) {
return (
<div className="relative flex flex-col h-full bg-background transform-gpu">
{returnToParentButton}
<div
className={cn(
'relative min-h-0',
isDesktopExpandedInput
? 'absolute inset-0 opacity-0 pointer-events-none'
@@ -764,25 +752,22 @@ export const ChatContainer: React.FC = () => {
<div
className={cn(
'relative z-10',
isDesktopExpandedInput
? 'flex-1 min-h-0 bg-background'
: 'bg-background'
)}
>
<ChatInput scrollToBottom={resumeToLatestInstant} />
</div>
isDesktopExpandedInput
? 'flex-1 min-h-0 bg-background'
: 'bg-background'
)}
>
<ChatInput scrollToBottom={resumeToLatestInstant} />
</div>
</div>
);
}
return (
<div
className="relative flex flex-col h-full bg-background"
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
>
{returnToParentButton}
<ChatViewport
currentSessionId={currentSessionId}
return (
<div className="relative flex flex-col h-full bg-background">
{returnToParentButton}
<ChatViewport
currentSessionId={currentSessionId}
isDesktopExpandedInput={isDesktopExpandedInput}
isMobile={isMobile}
stickyUserHeader={stickyUserHeader}
@@ -809,13 +794,13 @@ export const ChatContainer: React.FC = () => {
<div
className={cn(
'relative z-10',
isDesktopExpandedInput
? 'flex-1 min-h-0 bg-background'
: 'bg-background'
)}
>
{!isDesktopExpandedInput && sessionMessages.length > 0 && (
<ScrollToBottomButton
isDesktopExpandedInput
? 'flex-1 min-h-0 bg-background'
: 'bg-background'
)}
>
{!isDesktopExpandedInput && sessionMessages.length > 0 && (
<ScrollToBottomButton
visible={timelineController.showScrollToBottom}
onClick={navigation.resumeToLatest}
/>