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:
@@ -484,7 +484,7 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({ children }) =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isTunnelLocked && (
|
{!isTunnelLocked && (
|
||||||
<form onSubmit={handleSubmit} className="w-full space-y-2" data-keyboard-avoid="true">
|
<form onSubmit={handleSubmit} className="w-full space-y-2">
|
||||||
{canUsePasskey && (
|
{canUsePasskey && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -640,39 +640,33 @@ export const ChatContainer: React.FC = () => {
|
|||||||
void load();
|
void load();
|
||||||
}, [currentSessionId, hasLoadedSessionMessages, isPinned, loadMessages, resumeToLatestInstant, sessionStatusForCurrent.type]);
|
}, [currentSessionId, hasLoadedSessionMessages, isPinned, loadMessages, resumeToLatestInstant, sessionStatusForCurrent.type]);
|
||||||
|
|
||||||
if (!currentSessionId && !draftOpen) {
|
if (!currentSessionId && !draftOpen) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex flex-col h-full bg-background">
|
||||||
className="flex flex-col h-full bg-background"
|
<ChatEmptyState />
|
||||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
</div>
|
||||||
>
|
);
|
||||||
<ChatEmptyState />
|
}
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentSessionId && draftOpen) {
|
if (!currentSessionId && draftOpen) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative flex flex-col h-full bg-background transform-gpu">
|
||||||
className="relative flex flex-col h-full bg-background transform-gpu"
|
{!isDesktopExpandedInput ? (
|
||||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
<div className="flex-1 flex items-center justify-center">
|
||||||
>
|
<ChatEmptyState />
|
||||||
{!isDesktopExpandedInput ? (
|
</div>
|
||||||
<div className="flex-1 flex items-center justify-center">
|
) : null}
|
||||||
<ChatEmptyState />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-10',
|
'relative z-10',
|
||||||
isDesktopExpandedInput
|
isDesktopExpandedInput
|
||||||
? 'flex-1 min-h-0 bg-background'
|
? 'flex-1 min-h-0 bg-background'
|
||||||
: 'bg-background'
|
: 'bg-background'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ChatInput scrollToBottom={resumeToLatestInstant} />
|
<ChatInput scrollToBottom={resumeToLatestInstant} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,16 +674,13 @@ export const ChatContainer: React.FC = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSessionHydrating && sessionMessages.length === 0 && !streamingMessageId) {
|
if (isSessionHydrating && sessionMessages.length === 0 && !streamingMessageId) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative flex flex-col h-full bg-background">
|
||||||
className="relative flex flex-col h-full bg-background"
|
{returnToParentButton}
|
||||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
<div
|
||||||
>
|
className={cn(
|
||||||
{returnToParentButton}
|
'relative min-h-0',
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'relative min-h-0',
|
|
||||||
isDesktopExpandedInput
|
isDesktopExpandedInput
|
||||||
? 'absolute inset-0 opacity-0 pointer-events-none'
|
? 'absolute inset-0 opacity-0 pointer-events-none'
|
||||||
: 'flex-1'
|
: 'flex-1'
|
||||||
@@ -728,26 +719,23 @@ export const ChatContainer: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-10',
|
'relative z-10',
|
||||||
isDesktopExpandedInput
|
isDesktopExpandedInput
|
||||||
? 'flex-1 min-h-0 bg-background'
|
? 'flex-1 min-h-0 bg-background'
|
||||||
: 'bg-background'
|
: 'bg-background'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ChatInput scrollToBottom={resumeToLatestInstant} />
|
<ChatInput scrollToBottom={resumeToLatestInstant} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionMessages.length === 0 && !streamingMessageId) {
|
if (sessionMessages.length === 0 && !streamingMessageId) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative flex flex-col h-full bg-background transform-gpu">
|
||||||
className="relative flex flex-col h-full bg-background transform-gpu"
|
{returnToParentButton}
|
||||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
<div
|
||||||
>
|
className={cn(
|
||||||
{returnToParentButton}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'relative min-h-0',
|
'relative min-h-0',
|
||||||
isDesktopExpandedInput
|
isDesktopExpandedInput
|
||||||
? 'absolute inset-0 opacity-0 pointer-events-none'
|
? 'absolute inset-0 opacity-0 pointer-events-none'
|
||||||
@@ -764,25 +752,22 @@ export const ChatContainer: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-10',
|
'relative z-10',
|
||||||
isDesktopExpandedInput
|
isDesktopExpandedInput
|
||||||
? 'flex-1 min-h-0 bg-background'
|
? 'flex-1 min-h-0 bg-background'
|
||||||
: 'bg-background'
|
: 'bg-background'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ChatInput scrollToBottom={resumeToLatestInstant} />
|
<ChatInput scrollToBottom={resumeToLatestInstant} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative flex flex-col h-full bg-background">
|
||||||
className="relative flex flex-col h-full bg-background"
|
{returnToParentButton}
|
||||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
<ChatViewport
|
||||||
>
|
currentSessionId={currentSessionId}
|
||||||
{returnToParentButton}
|
|
||||||
<ChatViewport
|
|
||||||
currentSessionId={currentSessionId}
|
|
||||||
isDesktopExpandedInput={isDesktopExpandedInput}
|
isDesktopExpandedInput={isDesktopExpandedInput}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
stickyUserHeader={stickyUserHeader}
|
stickyUserHeader={stickyUserHeader}
|
||||||
@@ -809,13 +794,13 @@ export const ChatContainer: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-10',
|
'relative z-10',
|
||||||
isDesktopExpandedInput
|
isDesktopExpandedInput
|
||||||
? 'flex-1 min-h-0 bg-background'
|
? 'flex-1 min-h-0 bg-background'
|
||||||
: 'bg-background'
|
: 'bg-background'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isDesktopExpandedInput && sessionMessages.length > 0 && (
|
{!isDesktopExpandedInput && sessionMessages.length > 0 && (
|
||||||
<ScrollToBottomButton
|
<ScrollToBottomButton
|
||||||
visible={timelineController.showScrollToBottom}
|
visible={timelineController.showScrollToBottom}
|
||||||
onClick={navigation.resumeToLatest}
|
onClick={navigation.resumeToLatest}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -787,7 +787,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
const primaryAgents = React.useMemo(() => agents.filter((agent) => agent.mode === 'primary'), [agents]);
|
const primaryAgents = React.useMemo(() => agents.filter((agent) => agent.mode === 'primary'), [agents]);
|
||||||
const isMobile = useUIStore((state) => state.isMobile);
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
const inputBarOffset = useUIStore((state) => state.inputBarOffset);
|
const inputBarOffset = useUIStore((state) => state.inputBarOffset);
|
||||||
const isKeyboardOpen = useUIStore((state) => state.isKeyboardOpen);
|
|
||||||
const persistChatDraft = useUIStore((state) => state.persistChatDraft);
|
const persistChatDraft = useUIStore((state) => state.persistChatDraft);
|
||||||
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
|
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
|
||||||
const isExpandedInput = useUIStore((state) => state.isExpandedInput);
|
const isExpandedInput = useUIStore((state) => state.isExpandedInput);
|
||||||
@@ -1170,50 +1169,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
// Session activity for queue availability and controls
|
// Session activity for queue availability and controls
|
||||||
const { phase: sessionPhase } = useCurrentSessionActivity();
|
const { phase: sessionPhase } = useCurrentSessionActivity();
|
||||||
|
|
||||||
const handleTextareaPointerDownCapture = React.useCallback((event: React.PointerEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (!isMobile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.pointerType !== 'touch') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const textarea = textareaRef.current;
|
|
||||||
if (!textarea) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.activeElement === textarea) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent iOS from scrolling the page to reveal the input.
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const scroller = document.scrollingElement;
|
|
||||||
if (scroller && scroller.scrollTop !== 0) {
|
|
||||||
scroller.scrollTop = 0;
|
|
||||||
}
|
|
||||||
if (window.scrollY !== 0) {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
textarea.focus({ preventScroll: true });
|
|
||||||
} catch {
|
|
||||||
textarea.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
const len = textarea.value.length;
|
|
||||||
try {
|
|
||||||
textarea.setSelectionRange(len, len);
|
|
||||||
} catch {
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}, [isMobile]);
|
|
||||||
|
|
||||||
const handleOpenMobileControls = React.useCallback(() => {
|
const handleOpenMobileControls = React.useCallback(() => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
return;
|
return;
|
||||||
@@ -1226,16 +1181,12 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
|
|
||||||
setMobileControlsPanel(null);
|
setMobileControlsPanel(null);
|
||||||
|
|
||||||
if (isKeyboardOpen) {
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
textareaRef.current?.blur();
|
document.activeElement.blur();
|
||||||
requestAnimationFrame(() => {
|
|
||||||
setMobileControlsOpen(true);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setMobileControlsOpen(true);
|
setMobileControlsOpen(true);
|
||||||
}, [isMobile, isKeyboardOpen, mobileControlsOpen]);
|
}, [isMobile, mobileControlsOpen]);
|
||||||
|
|
||||||
const handleCloseMobileControls = React.useCallback(() => {
|
const handleCloseMobileControls = React.useCallback(() => {
|
||||||
setMobileControlsOpen(false);
|
setMobileControlsOpen(false);
|
||||||
@@ -2136,7 +2087,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
const isWordBoundary = !charBefore || /\s/.test(charBefore);
|
const isWordBoundary = !charBefore || /\s/.test(charBefore);
|
||||||
if (isWordBoundary && !textAfterAt.includes(' ') && !textAfterAt.includes('\n')) {
|
if (isWordBoundary && !textAfterAt.includes(' ') && !textAfterAt.includes('\n')) {
|
||||||
setMentionQuery(textAfterAt);
|
setMentionQuery(textAfterAt);
|
||||||
setAutocompleteTab('agents');
|
setAutocompleteTab((current) => current === 'files' ? 'files' : 'agents');
|
||||||
setShowFileMention(true);
|
setShowFileMention(true);
|
||||||
} else {
|
} else {
|
||||||
setShowFileMention(false);
|
setShowFileMention(false);
|
||||||
@@ -2179,22 +2130,31 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const cursorPosition = textarea?.selectionStart ?? message.length;
|
||||||
|
const textBeforeCursor = message.substring(0, cursorPosition);
|
||||||
|
const lastAtSymbol = textBeforeCursor.lastIndexOf('@');
|
||||||
|
const nextMentionQuery = lastAtSymbol !== -1
|
||||||
|
? textBeforeCursor.substring(lastAtSymbol + 1).replace(/[\s\n].*$/, '')
|
||||||
|
: '';
|
||||||
|
|
||||||
setAutocompleteTab(tab);
|
setAutocompleteTab(tab);
|
||||||
setCommandQuery('');
|
setCommandQuery('');
|
||||||
setMentionQuery('');
|
|
||||||
if (tab === 'commands') {
|
if (tab === 'commands') {
|
||||||
|
setMentionQuery('');
|
||||||
applyAutocompletePrefix('/');
|
applyAutocompletePrefix('/');
|
||||||
}
|
}
|
||||||
if (tab === 'agents') {
|
if (tab === 'agents') {
|
||||||
|
setMentionQuery(nextMentionQuery);
|
||||||
applyAutocompletePrefix('@');
|
applyAutocompletePrefix('@');
|
||||||
}
|
}
|
||||||
if (tab === 'files') {
|
if (tab === 'files') {
|
||||||
|
setMentionQuery(nextMentionQuery);
|
||||||
applyAutocompletePrefix('@');
|
applyAutocompletePrefix('@');
|
||||||
}
|
}
|
||||||
setShowSkillAutocomplete(false);
|
setShowSkillAutocomplete(false);
|
||||||
setShowCommandAutocomplete(tab === 'commands');
|
setShowCommandAutocomplete(tab === 'commands');
|
||||||
setShowFileMention(tab === 'agents' || tab === 'files');
|
setShowFileMention(tab === 'agents' || tab === 'files');
|
||||||
}, [applyAutocompletePrefix, isMobile, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]);
|
}, [applyAutocompletePrefix, isMobile, message, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]);
|
||||||
|
|
||||||
const handleOpenCommandMenu = React.useCallback(() => {
|
const handleOpenCommandMenu = React.useCallback(() => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
@@ -3331,10 +3291,10 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
className={cn(
|
className={cn(
|
||||||
"relative pt-0 pb-4",
|
"relative pt-0 pb-4",
|
||||||
isDesktopExpanded && 'flex h-full min-h-0 flex-col pt-4',
|
isDesktopExpanded && 'flex h-full min-h-0 flex-col pt-4',
|
||||||
isMobile && (isKeyboardOpen ? 'ios-keyboard-safe-area' : 'bottom-safe-area')
|
isMobile && 'bottom-safe-area'
|
||||||
)}
|
)}
|
||||||
data-keyboard-avoid="true"
|
|
||||||
style={isMobile && inputBarOffset > 0 && !isKeyboardOpen ? { marginBottom: `${inputBarOffset}px` } : undefined}
|
style={isMobile && inputBarOffset > 0 ? { marginBottom: `${inputBarOffset}px` } : undefined}
|
||||||
>
|
>
|
||||||
<div className={cn('chat-column relative overflow-visible', isDesktopExpanded && 'flex flex-1 min-h-0 flex-col')}>
|
<div className={cn('chat-column relative overflow-visible', isDesktopExpanded && 'flex flex-1 min-h-0 flex-col')}>
|
||||||
<AttachedFilesList />
|
<AttachedFilesList />
|
||||||
@@ -3689,7 +3649,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
onDropCapture={handleDropCapture}
|
onDropCapture={handleDropCapture}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
onPointerDownCapture={handleTextareaPointerDownCapture}
|
|
||||||
onKeyUp={updateAutocompleteOverlayPosition}
|
onKeyUp={updateAutocompleteOverlayPosition}
|
||||||
onClick={updateAutocompleteOverlayPosition}
|
onClick={updateAutocompleteOverlayPosition}
|
||||||
onScroll={(event) => {
|
onScroll={(event) => {
|
||||||
|
|||||||
@@ -350,259 +350,6 @@ export const MainLayout: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = document.documentElement;
|
|
||||||
|
|
||||||
let stickyKeyboardInset = 0;
|
|
||||||
let ignoreOpenUntilZero = false;
|
|
||||||
let previousHeight = 0;
|
|
||||||
let keyboardAvoidTarget: HTMLElement | null = null;
|
|
||||||
|
|
||||||
const setKeyboardOpen = useUIStore.getState().setKeyboardOpen;
|
|
||||||
|
|
||||||
const clearKeyboardAvoidTarget = () => {
|
|
||||||
if (!keyboardAvoidTarget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
keyboardAvoidTarget.style.setProperty('--oc-keyboard-avoid-offset', '0px');
|
|
||||||
keyboardAvoidTarget.removeAttribute('data-keyboard-avoid-active');
|
|
||||||
keyboardAvoidTarget = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveKeyboardAvoidTarget = (active: HTMLElement | null) => {
|
|
||||||
if (!active) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const explicitTargetId = active.getAttribute('data-keyboard-avoid-target-id');
|
|
||||||
if (explicitTargetId) {
|
|
||||||
const explicitTarget = document.getElementById(explicitTargetId);
|
|
||||||
if (explicitTarget instanceof HTMLElement) {
|
|
||||||
return explicitTarget;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const markedTarget = active.closest('[data-keyboard-avoid]') as HTMLElement | null;
|
|
||||||
if (markedTarget) {
|
|
||||||
// data-keyboard-avoid="none" opts out of translateY avoidance entirely.
|
|
||||||
// Used by components with their own scroll (e.g. CodeMirror).
|
|
||||||
if (markedTarget.getAttribute('data-keyboard-avoid') === 'none') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return markedTarget;
|
|
||||||
}
|
|
||||||
if (active.classList.contains('overlay-scrollbar-container')) {
|
|
||||||
const parent = active.parentElement;
|
|
||||||
if (parent instanceof HTMLElement) {
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return active;
|
|
||||||
};
|
|
||||||
|
|
||||||
const forceKeyboardClosed = () => {
|
|
||||||
stickyKeyboardInset = 0;
|
|
||||||
ignoreOpenUntilZero = true;
|
|
||||||
root.style.setProperty('--oc-keyboard-inset', '0px');
|
|
||||||
setKeyboardOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Batch visualViewport updates to once per animation frame to avoid
|
|
||||||
// layout thrashing during keyboard open/close animations.
|
|
||||||
let rafId = 0;
|
|
||||||
|
|
||||||
const updateVisualViewport = () => {
|
|
||||||
const viewport = window.visualViewport;
|
|
||||||
|
|
||||||
const height = viewport ? Math.round(viewport.height) : window.innerHeight;
|
|
||||||
const offsetTop = viewport ? Math.max(0, Math.round(viewport.offsetTop)) : 0;
|
|
||||||
|
|
||||||
root.style.setProperty('--oc-visual-viewport-offset-top', `${offsetTop}px`);
|
|
||||||
|
|
||||||
const active = document.activeElement as HTMLElement | null;
|
|
||||||
const tagName = active?.tagName;
|
|
||||||
const isInput = tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT';
|
|
||||||
const isTextTarget = isInput || Boolean(active?.isContentEditable);
|
|
||||||
|
|
||||||
const layoutHeight = Math.round(root.clientHeight || window.innerHeight);
|
|
||||||
const viewportSum = height + offsetTop;
|
|
||||||
const rawInset = Math.max(0, layoutHeight - viewportSum);
|
|
||||||
|
|
||||||
// Keyboard heuristic:
|
|
||||||
// - when an input is focused, smaller deltas can still be keyboard
|
|
||||||
// - when not focused, treat only big deltas as keyboard (ignore toolbars)
|
|
||||||
const openThreshold = isTextTarget ? 120 : 180;
|
|
||||||
const measuredInset = rawInset >= openThreshold ? rawInset : 0;
|
|
||||||
|
|
||||||
// Make the UI stable: treat keyboard inset as a step function.
|
|
||||||
// - When opening: take the first big inset and hold it.
|
|
||||||
// - When closing starts: immediately drop to 0 (even if keyboard animation continues).
|
|
||||||
// Closing start signals:
|
|
||||||
// - focus lost (handled via focusout)
|
|
||||||
// - visual viewport height starts increasing while inset is non-zero
|
|
||||||
if (ignoreOpenUntilZero) {
|
|
||||||
if (measuredInset === 0) {
|
|
||||||
ignoreOpenUntilZero = false;
|
|
||||||
}
|
|
||||||
stickyKeyboardInset = 0;
|
|
||||||
} else if (stickyKeyboardInset === 0) {
|
|
||||||
if (measuredInset > 0 && isTextTarget) {
|
|
||||||
stickyKeyboardInset = measuredInset;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Only detect closing-by-height when focus is NOT on text input
|
|
||||||
// (prevents false positives during Android keyboard animation)
|
|
||||||
const closingByHeight = !isTextTarget && height > previousHeight + 6;
|
|
||||||
|
|
||||||
if (measuredInset === 0) {
|
|
||||||
stickyKeyboardInset = 0;
|
|
||||||
setKeyboardOpen(false);
|
|
||||||
} else if (closingByHeight) {
|
|
||||||
forceKeyboardClosed();
|
|
||||||
} else if (measuredInset > 0 && isTextTarget) {
|
|
||||||
// When focus is on text input, track actual inset (allows settling
|
|
||||||
// to correct value after Android animation fluctuations)
|
|
||||||
stickyKeyboardInset = measuredInset;
|
|
||||||
setKeyboardOpen(true);
|
|
||||||
} else if (measuredInset > stickyKeyboardInset) {
|
|
||||||
stickyKeyboardInset = measuredInset;
|
|
||||||
setKeyboardOpen(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
root.style.setProperty('--oc-keyboard-inset', `${stickyKeyboardInset}px`);
|
|
||||||
previousHeight = height;
|
|
||||||
|
|
||||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
||||||
const keyboardHomeIndicator = isIOS && stickyKeyboardInset > 0 ? 34 : 0;
|
|
||||||
root.style.setProperty('--oc-keyboard-home-indicator', `${keyboardHomeIndicator}px`);
|
|
||||||
|
|
||||||
const avoidTarget = isTextTarget ? resolveKeyboardAvoidTarget(active) : null;
|
|
||||||
|
|
||||||
if (!isMobile || !avoidTarget || !active) {
|
|
||||||
clearKeyboardAvoidTarget();
|
|
||||||
} else {
|
|
||||||
if (avoidTarget !== keyboardAvoidTarget) {
|
|
||||||
clearKeyboardAvoidTarget();
|
|
||||||
keyboardAvoidTarget = avoidTarget;
|
|
||||||
}
|
|
||||||
const viewportBottom = offsetTop + height;
|
|
||||||
const rect = active.getBoundingClientRect();
|
|
||||||
const overlap = rect.bottom - viewportBottom;
|
|
||||||
const clearance = 8;
|
|
||||||
const keyboardInset = Math.max(stickyKeyboardInset, measuredInset);
|
|
||||||
const avoidOffset = overlap > clearance && keyboardInset > 0
|
|
||||||
? Math.min(overlap, keyboardInset)
|
|
||||||
: 0;
|
|
||||||
const target = keyboardAvoidTarget;
|
|
||||||
if (target) {
|
|
||||||
target.style.setProperty('--oc-keyboard-avoid-offset', `${avoidOffset}px`);
|
|
||||||
target.setAttribute('data-keyboard-avoid-active', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only force-scroll lock while an input is focused.
|
|
||||||
if (isMobile && isTextTarget) {
|
|
||||||
const scroller = document.scrollingElement;
|
|
||||||
if (scroller && scroller.scrollTop !== 0) {
|
|
||||||
scroller.scrollTop = 0;
|
|
||||||
}
|
|
||||||
if (window.scrollY !== 0) {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleVisualViewportUpdate = () => {
|
|
||||||
if (rafId) return;
|
|
||||||
rafId = requestAnimationFrame(() => {
|
|
||||||
rafId = 0;
|
|
||||||
updateVisualViewport();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
updateVisualViewport();
|
|
||||||
|
|
||||||
const viewport = window.visualViewport;
|
|
||||||
viewport?.addEventListener('resize', scheduleVisualViewportUpdate);
|
|
||||||
viewport?.addEventListener('scroll', scheduleVisualViewportUpdate);
|
|
||||||
window.addEventListener('resize', scheduleVisualViewportUpdate);
|
|
||||||
window.addEventListener('orientationchange', scheduleVisualViewportUpdate);
|
|
||||||
const isTextInputTarget = (element: HTMLElement | null) => {
|
|
||||||
if (!element) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const tagName = element.tagName;
|
|
||||||
const isInput = tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT';
|
|
||||||
return isInput || element.isContentEditable;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reset ignoreOpenUntilZero when focus moves to a text input.
|
|
||||||
// This allows keyboard detection to work when user taps input quickly
|
|
||||||
// while keyboard is still closing (common on Android).
|
|
||||||
const handleFocusIn = (event: FocusEvent) => {
|
|
||||||
const target = event.target as HTMLElement | null;
|
|
||||||
if (isTextInputTarget(target)) {
|
|
||||||
ignoreOpenUntilZero = false;
|
|
||||||
}
|
|
||||||
scheduleVisualViewportUpdate();
|
|
||||||
};
|
|
||||||
document.addEventListener('focusin', handleFocusIn, true);
|
|
||||||
|
|
||||||
const handleFocusOut = (event: FocusEvent) => {
|
|
||||||
const target = event.target as HTMLElement | null;
|
|
||||||
if (!isTextInputTarget(target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if focus is moving to another input - if so, don't close keyboard
|
|
||||||
const related = event.relatedTarget as HTMLElement | null;
|
|
||||||
if (isTextInputTarget(related)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// On mobile contenteditable editors (CodeMirror), focus can momentarily
|
|
||||||
// leave and return during drag-selection handles. Defer closing until the
|
|
||||||
// next frame and only close when no text target is focused and the
|
|
||||||
// visual viewport inset is actually zero.
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
if (isTextInputTarget(document.activeElement as HTMLElement | null)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentViewport = window.visualViewport;
|
|
||||||
const height = currentViewport ? Math.round(currentViewport.height) : window.innerHeight;
|
|
||||||
const offsetTop = currentViewport ? Math.max(0, Math.round(currentViewport.offsetTop)) : 0;
|
|
||||||
const layoutHeight = Math.round(root.clientHeight || window.innerHeight);
|
|
||||||
const viewportSum = height + offsetTop;
|
|
||||||
const rawInset = Math.max(0, layoutHeight - viewportSum);
|
|
||||||
|
|
||||||
if (rawInset > 0) {
|
|
||||||
updateVisualViewport();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
forceKeyboardClosed();
|
|
||||||
updateVisualViewport();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('focusout', handleFocusOut, true);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (rafId) cancelAnimationFrame(rafId);
|
|
||||||
viewport?.removeEventListener('resize', scheduleVisualViewportUpdate);
|
|
||||||
viewport?.removeEventListener('scroll', scheduleVisualViewportUpdate);
|
|
||||||
window.removeEventListener('resize', scheduleVisualViewportUpdate);
|
|
||||||
window.removeEventListener('orientationchange', scheduleVisualViewportUpdate);
|
|
||||||
document.removeEventListener('focusin', handleFocusIn, true);
|
|
||||||
document.removeEventListener('focusout', handleFocusOut, true);
|
|
||||||
clearKeyboardAvoidTarget();
|
|
||||||
};
|
|
||||||
}, [isMobile]);
|
|
||||||
|
|
||||||
const secondaryView = React.useMemo(() => {
|
const secondaryView = React.useMemo(() => {
|
||||||
switch (activeMainTab) {
|
switch (activeMainTab) {
|
||||||
case 'plan':
|
case 'plan':
|
||||||
|
|||||||
@@ -585,7 +585,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
const configuredSetupCount = setupCommands.filter(cmd => cmd.trim()).length;
|
const configuredSetupCount = setupCommands.filter(cmd => cmd.trim()).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col h-full bg-background" data-keyboard-avoid="true">
|
<form onSubmit={handleSubmit} className="flex flex-col h-full bg-background">
|
||||||
{!isWindowed ? (
|
{!isWindowed ? (
|
||||||
<header
|
<header
|
||||||
onMouseDown={handleDragStart}
|
onMouseDown={handleDragStart}
|
||||||
|
|||||||
@@ -593,7 +593,7 @@ export const AgentsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||||
|
|
||||||
{/* Header & Actions */}
|
{/* Header & Actions */}
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export const CommandsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export const GitPage: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full bg-background">
|
<ScrollableOverlay outerClassName="h-full" className="w-full bg-background">
|
||||||
<div className="mx-auto w-full max-w-3xl space-y-6 p-3 sm:p-6 sm:pt-8">
|
<div className="mx-auto w-full max-w-3xl space-y-6 p-3 sm:p-6 sm:pt-8">
|
||||||
<GitHubSettings />
|
<GitHubSettings />
|
||||||
|
|
||||||
|
|||||||
@@ -1253,7 +1253,7 @@ export const McpPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -1755,7 +1755,7 @@ export const McpPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="max-w-md" keyboardAvoid>
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Import JSON Snippet</DialogTitle>
|
<DialogTitle>Import JSON Snippet</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
|||||||
if (!section) {
|
if (!section) {
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay
|
<ScrollableOverlay
|
||||||
keyboardAvoid
|
|
||||||
outerClassName="h-full"
|
outerClassName="h-full"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
@@ -95,7 +94,6 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay
|
<ScrollableOverlay
|
||||||
keyboardAvoid
|
|
||||||
outerClassName="h-full"
|
outerClassName="h-full"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ export const ProjectsPage: React.FC = () => {
|
|||||||
|
|
||||||
if (!selectedProject) {
|
if (!selectedProject) {
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||||
<div className="mx-auto w-full max-w-4xl p-3 sm:p-6 sm:pt-8">
|
<div className="mx-auto w-full max-w-4xl p-3 sm:p-6 sm:pt-8">
|
||||||
<p className="typography-meta text-muted-foreground">No projects available.</p>
|
<p className="typography-meta text-muted-foreground">No projects available.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -244,7 +244,7 @@ export const ProjectsPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full bg-background">
|
<ScrollableOverlay outerClassName="h-full" className="w-full bg-background">
|
||||||
<div className="mx-auto w-full max-w-4xl p-3 sm:p-6 sm:pt-8">
|
<div className="mx-auto w-full max-w-4xl p-3 sm:p-6 sm:pt-8">
|
||||||
|
|
||||||
{/* Top Header & Actions */}
|
{/* Top Header & Actions */}
|
||||||
|
|||||||
@@ -519,7 +519,7 @@ export const ProvidersPage: React.FC = () => {
|
|||||||
|
|
||||||
if (isAddMode) {
|
if (isAddMode) {
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h1 className="typography-ui-header font-semibold text-foreground">Connect Provider</h1>
|
<h1 className="typography-ui-header font-semibold text-foreground">Connect Provider</h1>
|
||||||
@@ -791,7 +791,7 @@ export const ProvidersPage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ const SkillsInstalledPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -558,7 +558,7 @@ const SkillsInstalledPage: React.FC = () => {
|
|||||||
setIsFileDialogOpen(open);
|
setIsFileDialogOpen(open);
|
||||||
if (!open) setEditingFilePath(null);
|
if (!open) setEditingFilePath(null);
|
||||||
}}>
|
}}>
|
||||||
<DialogContent className="max-w-3xl max-h-[85vh] flex flex-col" keyboardAvoid>
|
<DialogContent className="max-w-3xl max-h-[85vh] flex flex-col">
|
||||||
<DialogHeader className="flex-shrink-0">
|
<DialogHeader className="flex-shrink-0">
|
||||||
<DialogTitle>{editingFilePath ? 'Edit Supporting File' : 'Add Supporting File'}</DialogTitle>
|
<DialogTitle>{editingFilePath ? 'Edit Supporting File' : 'Add Supporting File'}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export const AddCatalogDialog: React.FC<AddCatalogDialogProps> = ({ open, onOpen
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-xl" keyboardAvoid>
|
<DialogContent className="max-w-xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add skills catalog</DialogTitle>
|
<DialogTitle>Add skills catalog</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export const InstallSkillDialog: React.FC<InstallSkillDialogProps> = ({ open, on
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-md" keyboardAvoid>
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Install skill</DialogTitle>
|
<DialogTitle>Install skill</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export const SkillsCatalogPage: React.FC<SkillsCatalogPageProps> = ({ mode, onMo
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export const UsagePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ const SessionFolderItemBase = <TSessionNode,>({
|
|||||||
{renaming ? (
|
{renaming ? (
|
||||||
<form
|
<form
|
||||||
className="flex min-w-0 flex-1 items-center gap-1"
|
className="flex min-w-0 flex-1 items-center gap-1"
|
||||||
data-keyboard-avoid="true"
|
|
||||||
onPointerDown={(event) => event.stopPropagation()}
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
? 'group-hover:pr-5'
|
? 'group-hover:pr-5'
|
||||||
: 'group-hover:pr-5 group-focus-within:pr-5');
|
: 'group-hover:pr-5 group-focus-within:pr-5');
|
||||||
const suppressNextSelectRef = React.useRef(false);
|
const suppressNextSelectRef = React.useRef(false);
|
||||||
|
const [isTouchPressed, setIsTouchPressed] = React.useState(false);
|
||||||
|
|
||||||
const session = node.session;
|
const session = node.session;
|
||||||
const liveSession = useSession(session.id);
|
const liveSession = useSession(session.id);
|
||||||
@@ -356,7 +357,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
<div className="flex min-w-0 flex-1 flex-col gap-0">
|
<div className="flex min-w-0 flex-1 flex-col gap-0">
|
||||||
<form
|
<form
|
||||||
className="flex w-full items-center gap-2"
|
className="flex w-full items-center gap-2"
|
||||||
data-keyboard-avoid="true"
|
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleSaveEdit();
|
handleSaveEdit();
|
||||||
@@ -516,6 +517,16 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
suppressNextSelectRef.current = true;
|
suppressNextSelectRef.current = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleRowPointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
|
||||||
|
if (mobileVariant && event.pointerType === 'touch') {
|
||||||
|
setIsTouchPressed(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleRowPointerEnd = (event: React.PointerEvent<HTMLButtonElement>) => {
|
||||||
|
if (mobileVariant && event.pointerType === 'touch') {
|
||||||
|
setIsTouchPressed(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const sessionMenuContent = (
|
const sessionMenuContent = (
|
||||||
<DropdownMenuContent align="end" className="min-w-[180px]" onCloseAutoFocus={(event) => { if (renamingFolderId) event.preventDefault(); }}>
|
<DropdownMenuContent align="end" className="min-w-[180px]" onCloseAutoFocus={(event) => { if (renamingFolderId) event.preventDefault(); }}>
|
||||||
@@ -639,15 +650,19 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isMissingDirectory}
|
disabled={isMissingDirectory}
|
||||||
onMouseDown={handleRowMouseDown}
|
onPointerDown={handleRowPointerDown}
|
||||||
onClick={(event) => handleRowSelect(event)}
|
onPointerUp={handleRowPointerEnd}
|
||||||
|
onPointerCancel={handleRowPointerEnd}
|
||||||
|
onMouseDown={handleRowMouseDown}
|
||||||
|
onClick={(event) => handleRowSelect(event)}
|
||||||
onDoubleClick={(e) => {
|
onDoubleClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleSessionDoubleClick();
|
handleSessionDoubleClick();
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]',
|
'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]',
|
||||||
|
isTouchPressed && 'bg-interactive-hover/70',
|
||||||
mobileVariant
|
mobileVariant
|
||||||
? (isVSCode ? revealPaddingClass : 'pr-7')
|
? (isVSCode ? revealPaddingClass : 'pr-7')
|
||||||
: revealPaddingClass,
|
: revealPaddingClass,
|
||||||
@@ -700,15 +715,19 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isMissingDirectory}
|
disabled={isMissingDirectory}
|
||||||
onMouseDown={handleRowMouseDown}
|
onPointerDown={handleRowPointerDown}
|
||||||
onClick={(event) => handleRowSelect(event)}
|
onPointerUp={handleRowPointerEnd}
|
||||||
|
onPointerCancel={handleRowPointerEnd}
|
||||||
|
onMouseDown={handleRowMouseDown}
|
||||||
|
onClick={(event) => handleRowSelect(event)}
|
||||||
onDoubleClick={(e) => {
|
onDoubleClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleSessionDoubleClick();
|
handleSessionDoubleClick();
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]',
|
'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]',
|
||||||
|
isTouchPressed && 'bg-interactive-hover/70',
|
||||||
mobileVariant
|
mobileVariant
|
||||||
? (isVSCode ? revealPaddingClass : 'pr-7')
|
? (isVSCode ? revealPaddingClass : 'pr-7')
|
||||||
: revealPaddingClass
|
: revealPaddingClass
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ interface TerminalViewportProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
enableTouchScroll?: boolean;
|
enableTouchScroll?: boolean;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
keyboardAvoidTargetId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportProps>(
|
const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportProps>(
|
||||||
@@ -92,7 +91,6 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
|||||||
className,
|
className,
|
||||||
enableTouchScroll,
|
enableTouchScroll,
|
||||||
autoFocus = true,
|
autoFocus = true,
|
||||||
keyboardAvoidTargetId,
|
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
@@ -1514,7 +1512,6 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
enterKeyHint="send"
|
enterKeyHint="send"
|
||||||
data-terminal-hidden-input="true"
|
data-terminal-hidden-input="true"
|
||||||
data-keyboard-avoid-target-id={keyboardAvoidTargetId}
|
|
||||||
aria-label="Terminal input"
|
aria-label="Terminal input"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
style={{
|
style={{
|
||||||
@@ -1542,7 +1539,6 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
enterKeyHint="send"
|
enterKeyHint="send"
|
||||||
data-terminal-hidden-input="true"
|
data-terminal-hidden-input="true"
|
||||||
data-keyboard-avoid-target-id={keyboardAvoidTargetId}
|
|
||||||
aria-label="Terminal input"
|
aria-label="Terminal input"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
|
|||||||
'mx-auto max-w-lg',
|
'mx-auto max-w-lg',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
data-keyboard-avoid="true"
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ type ScrollableOverlayProps = React.HTMLAttributes<HTMLElement> & {
|
|||||||
disableHorizontal?: boolean;
|
disableHorizontal?: boolean;
|
||||||
observeMutations?: boolean;
|
observeMutations?: boolean;
|
||||||
fillContainer?: boolean;
|
fillContainer?: boolean;
|
||||||
keyboardAvoid?: boolean;
|
|
||||||
/** Prevent scroll from propagating to parent when at boundaries */
|
/** Prevent scroll from propagating to parent when at boundaries */
|
||||||
preventOverscroll?: boolean;
|
preventOverscroll?: boolean;
|
||||||
useScrollShadow?: boolean;
|
useScrollShadow?: boolean;
|
||||||
@@ -34,7 +33,6 @@ export const ScrollableOverlay = React.forwardRef<HTMLElement, ScrollableOverlay
|
|||||||
disableHorizontal = false,
|
disableHorizontal = false,
|
||||||
observeMutations = true,
|
observeMutations = true,
|
||||||
fillContainer = true,
|
fillContainer = true,
|
||||||
keyboardAvoid = false,
|
|
||||||
preventOverscroll = false,
|
preventOverscroll = false,
|
||||||
useScrollShadow = false,
|
useScrollShadow = false,
|
||||||
scrollShadowSize,
|
scrollShadowSize,
|
||||||
@@ -51,7 +49,6 @@ export const ScrollableOverlay = React.forwardRef<HTMLElement, ScrollableOverlay
|
|||||||
preventOverscroll && "overscroll-none",
|
preventOverscroll && "overscroll-none",
|
||||||
outerClassName
|
outerClassName
|
||||||
)}
|
)}
|
||||||
data-keyboard-avoid={keyboardAvoid ? "true" : undefined}
|
|
||||||
>
|
>
|
||||||
{useScrollShadow ? (
|
{useScrollShadow ? (
|
||||||
<ScrollShadow
|
<ScrollShadow
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ DialogOverlay.displayName = "DialogOverlay";
|
|||||||
|
|
||||||
type DialogContentProps = Omit<React.ComponentProps<typeof BaseDialog.Popup>, "children"> & {
|
type DialogContentProps = Omit<React.ComponentProps<typeof BaseDialog.Popup>, "children"> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean
|
||||||
keyboardAvoid?: boolean
|
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
onOpenAutoFocus?: (event: Event) => void
|
onOpenAutoFocus?: (event: Event) => void
|
||||||
onCloseAutoFocus?: (event: Event) => void
|
onCloseAutoFocus?: (event: Event) => void
|
||||||
@@ -90,7 +89,6 @@ function DialogContent({
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
keyboardAvoid = false,
|
|
||||||
onOpenAutoFocus,
|
onOpenAutoFocus,
|
||||||
onCloseAutoFocus,
|
onCloseAutoFocus,
|
||||||
...props
|
...props
|
||||||
@@ -104,7 +102,6 @@ function DialogContent({
|
|||||||
<BaseDialog.Popup
|
<BaseDialog.Popup
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
data-state-slot="dialog"
|
data-state-slot="dialog"
|
||||||
data-keyboard-avoid={keyboardAvoid ? "true" : undefined}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background text-foreground fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border p-6 shadow-none overflow-hidden pwa-dialog-content",
|
"bg-background text-foreground fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border p-6 shadow-none overflow-hidden pwa-dialog-content",
|
||||||
className
|
className
|
||||||
|
|||||||
@@ -2150,10 +2150,8 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootStyles = getComputedStyle(document.documentElement);
|
const layoutHeight = document.documentElement.clientHeight || window.innerHeight;
|
||||||
const keyboardInset = Number.parseFloat(rootStyles.getPropertyValue('--oc-keyboard-inset')) || 0;
|
const occludedBottom = Math.max(0, layoutHeight - (viewport.offsetTop + viewport.height));
|
||||||
const keyboardHomeIndicator = Number.parseFloat(rootStyles.getPropertyValue('--oc-keyboard-home-indicator')) || 0;
|
|
||||||
const occludedBottom = keyboardInset + keyboardHomeIndicator;
|
|
||||||
if (occludedBottom <= 0) {
|
if (occludedBottom <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2965,8 +2963,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
<div
|
<div
|
||||||
className={cn('relative h-full', shouldMaskEditorForPendingNavigation && 'overflow-hidden')}
|
className={cn('relative h-full', shouldMaskEditorForPendingNavigation && 'overflow-hidden')}
|
||||||
ref={editorWrapperRef}
|
ref={editorWrapperRef}
|
||||||
data-keyboard-avoid="none"
|
|
||||||
style={isMobile ? { height: 'calc(100% - var(--oc-keyboard-inset, 0px))' } : undefined}
|
|
||||||
>
|
>
|
||||||
<div className={cn('h-full', shouldMaskEditorForPendingNavigation && 'invisible')}>
|
<div className={cn('h-full', shouldMaskEditorForPendingNavigation && 'invisible')}>
|
||||||
<CodeMirrorEditor
|
<CodeMirrorEditor
|
||||||
|
|||||||
@@ -1996,7 +1996,7 @@ export const GitView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex h-full flex-col overflow-hidden', 'bg-sidebar')} data-keyboard-avoid="true">
|
<div className={cn('flex h-full flex-col overflow-hidden', 'bg-sidebar')}>
|
||||||
<GitHeader
|
<GitHeader
|
||||||
status={status}
|
status={status}
|
||||||
localBranches={localBranches}
|
localBranches={localBranches}
|
||||||
|
|||||||
@@ -604,7 +604,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
// No sidebar available; fall back to direct content.
|
// No sidebar available; fall back to direct content.
|
||||||
const fallback = renderPageContent(settingsSlug);
|
const fallback = renderPageContent(settingsSlug);
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 min-h-0 overflow-hidden bg-background" data-keyboard-avoid="true">
|
<div className="flex-1 min-h-0 overflow-hidden bg-background">
|
||||||
<ErrorBoundary>{fallback}</ErrorBoundary>
|
<ErrorBoundary>{fallback}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -622,7 +622,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
const content = renderPageContent(settingsSlug);
|
const content = renderPageContent(settingsSlug);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 min-h-0 overflow-hidden bg-background" data-keyboard-avoid="true">
|
<div className="flex-1 min-h-0 overflow-hidden bg-background">
|
||||||
<ErrorBoundary>{content}</ErrorBoundary>
|
<ErrorBoundary>{content}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ export const TerminalView: React.FC = () => {
|
|||||||
const [activeModifier, setActiveModifier] = React.useState<Modifier | null>(null);
|
const [activeModifier, setActiveModifier] = React.useState<Modifier | null>(null);
|
||||||
const [isRestarting, setIsRestarting] = React.useState(false);
|
const [isRestarting, setIsRestarting] = React.useState(false);
|
||||||
const [viewportLayoutVersion, setViewportLayoutVersion] = React.useState(0);
|
const [viewportLayoutVersion, setViewportLayoutVersion] = React.useState(0);
|
||||||
const keyboardAvoidTargetId = React.useId();
|
|
||||||
|
|
||||||
const streamCleanupRef = React.useRef<(() => void) | null>(null);
|
const streamCleanupRef = React.useRef<(() => void) | null>(null);
|
||||||
const activeTerminalIdRef = React.useRef<string | null>(null);
|
const activeTerminalIdRef = React.useRef<string | null>(null);
|
||||||
@@ -1069,8 +1068,6 @@ export const TerminalView: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
className="relative flex-1 overflow-hidden"
|
className="relative flex-1 overflow-hidden"
|
||||||
style={{ backgroundColor: xtermTheme.background }}
|
style={{ backgroundColor: xtermTheme.background }}
|
||||||
id={keyboardAvoidTargetId}
|
|
||||||
data-keyboard-avoid="true"
|
|
||||||
>
|
>
|
||||||
<div className="h-full w-full box-border pl-7 pr-5 pt-3 pb-4">
|
<div className="h-full w-full box-border pl-7 pr-5 pt-3 pb-4">
|
||||||
{shouldRenderViewport ? (
|
{shouldRenderViewport ? (
|
||||||
@@ -1088,7 +1085,6 @@ export const TerminalView: React.FC = () => {
|
|||||||
fontSize={terminalFontSize}
|
fontSize={terminalFontSize}
|
||||||
enableTouchScroll={hasTouchInput}
|
enableTouchScroll={hasTouchInput}
|
||||||
autoFocus={isTerminalVisible}
|
autoFocus={isTerminalVisible}
|
||||||
keyboardAvoidTargetId={keyboardAvoidTargetId}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={Boolean(worktreeDialog)} onOpenChange={(open) => { if (!open) setWorktreeDialog(null); }}>
|
<Dialog open={Boolean(worktreeDialog)} onOpenChange={(open) => { if (!open) setWorktreeDialog(null); }}>
|
||||||
<DialogContent className="max-w-md" keyboardAvoid>
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{worktreeDialog?.kind === 'remove' ? 'Remove worktree' : 'Remove other worktrees'}
|
{worktreeDialog?.kind === 'remove' ? 'Remove worktree' : 'Remove other worktrees'}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const AgentGroupItem: React.FC<AgentGroupItemProps> = ({ group, isSelected, isBu
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||||
<DialogContent className="max-w-md" keyboardAvoid>
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Delete agent group</DialogTitle>
|
<DialogTitle>Delete agent group</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
|||||||
const contentClassName = 'flex flex-col gap-3 px-0 pt-1 pb-3';
|
const contentClassName = 'flex flex-col gap-3 px-0 pt-1 pb-3';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={containerClassName} data-keyboard-avoid="true">
|
<section className={containerClassName}>
|
||||||
<div className={headerClassName}>
|
<div className={headerClassName}>
|
||||||
<h3 className="typography-ui-header font-semibold text-foreground">Commit</h3>
|
<h3 className="typography-ui-header font-semibold text-foreground">Commit</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -223,10 +223,6 @@ textarea[data-chat-input="true"]:focus-visible {
|
|||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-keyboard-avoid-active="true"] {
|
|
||||||
transform: translateY(calc(-1 * var(--oc-keyboard-avoid-offset, 0px)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scroll shadow (HeroUI) fallback styling to ensure visible gradients without the Tailwind plugin */
|
/* Scroll shadow (HeroUI) fallback styling to ensure visible gradients without the Tailwind plugin */
|
||||||
[data-scroll-shadow="true"] {
|
[data-scroll-shadow="true"] {
|
||||||
--scroll-shadow-size: var(--scroll-shadow-size, 48px);
|
--scroll-shadow-size: var(--scroll-shadow-size, 48px);
|
||||||
|
|||||||
@@ -482,7 +482,6 @@ interface UIStore {
|
|||||||
pendingFileNavigation: PendingFileNavigation | null;
|
pendingFileNavigation: PendingFileNavigation | null;
|
||||||
pendingFileFocusPath: string | null;
|
pendingFileFocusPath: string | null;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
isKeyboardOpen: boolean;
|
|
||||||
isQuickOpenOpen: boolean;
|
isQuickOpenOpen: boolean;
|
||||||
isCommandPaletteOpen: boolean;
|
isCommandPaletteOpen: boolean;
|
||||||
isHelpDialogOpen: boolean;
|
isHelpDialogOpen: boolean;
|
||||||
@@ -637,7 +636,6 @@ interface UIStore {
|
|||||||
setPadding: (size: number) => void;
|
setPadding: (size: number) => void;
|
||||||
setCornerRadius: (radius: number) => void;
|
setCornerRadius: (radius: number) => void;
|
||||||
setInputBarOffset: (offset: number) => void;
|
setInputBarOffset: (offset: number) => void;
|
||||||
setKeyboardOpen: (open: boolean) => void;
|
|
||||||
applyTypography: () => void;
|
applyTypography: () => void;
|
||||||
applyPadding: () => void;
|
applyPadding: () => void;
|
||||||
updateProportionalSidebarWidths: () => void;
|
updateProportionalSidebarWidths: () => void;
|
||||||
@@ -724,7 +722,6 @@ export const useUIStore = create<UIStore>()(
|
|||||||
pendingFileNavigation: null,
|
pendingFileNavigation: null,
|
||||||
pendingFileFocusPath: null,
|
pendingFileFocusPath: null,
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
isKeyboardOpen: false,
|
|
||||||
isQuickOpenOpen: false,
|
isQuickOpenOpen: false,
|
||||||
isCommandPaletteOpen: false,
|
isCommandPaletteOpen: false,
|
||||||
isHelpDialogOpen: false,
|
isHelpDialogOpen: false,
|
||||||
@@ -1470,10 +1467,6 @@ export const useUIStore = create<UIStore>()(
|
|||||||
set({ inputBarOffset: offset });
|
set({ inputBarOffset: offset });
|
||||||
},
|
},
|
||||||
|
|
||||||
setKeyboardOpen: (open) => {
|
|
||||||
set({ isKeyboardOpen: open });
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleFavoriteModel: (providerID, modelID) => {
|
toggleFavoriteModel: (providerID, modelID) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const exists = state.favoriteModels.some(
|
const exists = state.favoriteModels.some(
|
||||||
|
|||||||
@@ -7,10 +7,6 @@
|
|||||||
--oc-safe-area-bottom-visual: 0px;
|
--oc-safe-area-bottom-visual: 0px;
|
||||||
--oc-safe-area-left: 0px;
|
--oc-safe-area-left: 0px;
|
||||||
--oc-header-height: 56px;
|
--oc-header-height: 56px;
|
||||||
--oc-visual-viewport-offset-top: 0px;
|
|
||||||
--oc-keyboard-inset: 0px;
|
|
||||||
--oc-keyboard-avoid-offset: 0px;
|
|
||||||
--oc-keyboard-home-indicator: 0px;
|
|
||||||
--oc-wco-left-inset: 0px;
|
--oc-wco-left-inset: 0px;
|
||||||
--oc-wco-right-inset: 0px;
|
--oc-wco-right-inset: 0px;
|
||||||
--oc-wco-titlebar-height: 0px;
|
--oc-wco-titlebar-height: 0px;
|
||||||
|
|||||||
@@ -165,16 +165,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent iOS keyboard from scrolling the page */
|
|
||||||
:root.device-mobile:not(.desktop-runtime) body,
|
|
||||||
:root.device-mobile:not(.desktop-runtime) #root {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
overscroll-behavior-y: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix main layout container */
|
/* Fix main layout container */
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-screen {
|
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-screen {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@@ -182,16 +172,6 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix header positioning */
|
|
||||||
:root.device-mobile:not(.desktop-runtime) .header-safe-area {
|
|
||||||
position: relative;
|
|
||||||
top: auto;
|
|
||||||
left: auto;
|
|
||||||
right: auto;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix main content area */
|
/* Fix main content area */
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .flex-1.overflow-hidden {
|
:root.mobile-pointer:not(.desktop-runtime) .flex-1.overflow-hidden {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -287,22 +267,6 @@
|
|||||||
padding-bottom: var(--oc-safe-area-bottom-visual);
|
padding-bottom: var(--oc-safe-area-bottom-visual);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* iOS keyboard home indicator safe area - used when keyboard is open */
|
|
||||||
:root.device-mobile:not(.desktop-runtime) .ios-keyboard-safe-area {
|
|
||||||
padding-bottom: calc(var(--oc-keyboard-home-indicator, 34px) + var(--oc-safe-area-bottom-visual, 0px)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prevent keyboard-induced page scroll on iOS PWA */
|
|
||||||
:root.device-mobile:not(.desktop-runtime) body {
|
|
||||||
min-height: 100vh;
|
|
||||||
min-height: -webkit-fill-available;
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
overscroll-behavior-y: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix iOS viewport issues */
|
/* Fix iOS viewport issues */
|
||||||
:root.device-mobile:not(.desktop-runtime) .flex.flex-col.h-screen {
|
:root.device-mobile:not(.desktop-runtime) .flex.flex-col.h-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|||||||
Reference in New Issue
Block a user