refactor: simplify mobile keyboard handling
Removed fragile global keyboard viewport manager Restored native mobile viewport behavior Fixed textarea content shifting outside inputs
This commit is contained in:
@@ -792,7 +792,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
|||||||
const agents = getVisibleAgents();
|
const agents = getVisibleAgents();
|
||||||
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 isKeyboardOpen = useUIStore((state) => state.isKeyboardOpen);
|
|
||||||
const inputBarOffset = useUIStore((state) => state.inputBarOffset);
|
const inputBarOffset = useUIStore((state) => state.inputBarOffset);
|
||||||
const persistChatDraft = useUIStore((state) => state.persistChatDraft);
|
const persistChatDraft = useUIStore((state) => state.persistChatDraft);
|
||||||
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
|
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
|
||||||
@@ -3303,10 +3302,9 @@ 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="none"
|
style={isMobile && inputBarOffset > 0 ? { marginBottom: `${inputBarOffset}px` } : undefined}
|
||||||
style={isMobile && inputBarOffset > 0 && !isKeyboardOpen ? { 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 />
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { useUIStore } from '@/stores/useUIStore';
|
|||||||
import { useUpdateStore } from '@/stores/useUpdateStore';
|
import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||||
import { useMobileKeyboardManager } from '@/hooks/useMobileKeyboardManager';
|
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { isDesktopShell } from '@/lib/desktop';
|
import { isDesktopShell } from '@/lib/desktop';
|
||||||
@@ -354,8 +353,6 @@ export const MainLayout: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useMobileKeyboardManager(isMobile);
|
|
||||||
|
|
||||||
const secondaryView = React.useMemo(() => {
|
const secondaryView = React.useMemo(() => {
|
||||||
switch (activeMainTab) {
|
switch (activeMainTab) {
|
||||||
case 'plan':
|
case 'plan':
|
||||||
@@ -387,8 +384,7 @@ export const MainLayout: React.FC = () => {
|
|||||||
<DiffWorkerProvider>
|
<DiffWorkerProvider>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'main-content-safe-area',
|
'main-content-safe-area h-[100dvh]',
|
||||||
isMobile ? 'h-full' : 'h-[100dvh]',
|
|
||||||
isMobile ? 'flex flex-col' : 'flex',
|
isMobile ? 'flex flex-col' : 'flex',
|
||||||
'bg-background'
|
'bg-background'
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,262 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detects mobile keyboard open/close via the Visual Viewport API and manages:
|
|
||||||
* - `--oc-keyboard-inset` CSS variable on :root
|
|
||||||
* - `--oc-visual-viewport-height` / `--oc-visual-viewport-offset-top` CSS variables
|
|
||||||
* - `--oc-keyboard-home-indicator` CSS variable (iOS home bar)
|
|
||||||
* - `--oc-keyboard-avoid-offset` on the active input's keyboard-avoid target
|
|
||||||
* - `isKeyboardOpen` state in the UI store
|
|
||||||
*
|
|
||||||
* Platform-specific handling:
|
|
||||||
* - iOS: sticky inset with home indicator padding
|
|
||||||
* - Android: resize-based detection with `maxObservedLayoutHeight` tracking
|
|
||||||
*/
|
|
||||||
export function useMobileKeyboardManager(isMobile: boolean): void {
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = document.documentElement;
|
|
||||||
|
|
||||||
let stickyKeyboardInset = 0;
|
|
||||||
let ignoreOpenUntilZero = false;
|
|
||||||
let previousHeight = 0;
|
|
||||||
let maxObservedLayoutHeight = 0;
|
|
||||||
let previousOrientation = '';
|
|
||||||
let keyboardAvoidTarget: HTMLElement | null = null;
|
|
||||||
|
|
||||||
const setKeyboardOpen = useUIStore.getState().setKeyboardOpen;
|
|
||||||
const userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent;
|
|
||||||
const isAndroid = /Android/i.test(userAgent);
|
|
||||||
const isIOS = /iPad|iPhone|iPod/.test(userAgent);
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
const orientation = window.innerWidth >= window.innerHeight ? 'landscape' : 'portrait';
|
|
||||||
|
|
||||||
root.style.setProperty('--oc-visual-viewport-offset-top', `${offsetTop}px`);
|
|
||||||
root.style.setProperty('--oc-visual-viewport-height', `${height}px`);
|
|
||||||
|
|
||||||
const active = document.activeElement as HTMLElement | null;
|
|
||||||
const isTextTarget = isTextInputTarget(active);
|
|
||||||
|
|
||||||
const layoutHeight = Math.round(root.clientHeight || window.innerHeight);
|
|
||||||
if (previousOrientation !== orientation) {
|
|
||||||
previousOrientation = orientation;
|
|
||||||
maxObservedLayoutHeight = layoutHeight;
|
|
||||||
} else if (layoutHeight > maxObservedLayoutHeight || maxObservedLayoutHeight === 0) {
|
|
||||||
maxObservedLayoutHeight = layoutHeight;
|
|
||||||
}
|
|
||||||
const viewportSum = height + offsetTop;
|
|
||||||
const rawInset = Math.max(0, layoutHeight - viewportSum);
|
|
||||||
const rawAndroidResizeInset = isAndroid
|
|
||||||
? Math.max(0, maxObservedLayoutHeight - layoutHeight)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const openThreshold = isTextTarget ? 120 : 180;
|
|
||||||
const measuredInset = rawInset >= openThreshold ? rawInset : 0;
|
|
||||||
const androidResizeInset = isTextTarget && rawAndroidResizeInset >= openThreshold
|
|
||||||
? rawAndroidResizeInset
|
|
||||||
: 0;
|
|
||||||
const effectiveMeasuredInset = Math.max(measuredInset, androidResizeInset);
|
|
||||||
|
|
||||||
if (ignoreOpenUntilZero) {
|
|
||||||
if (effectiveMeasuredInset === 0) {
|
|
||||||
ignoreOpenUntilZero = false;
|
|
||||||
}
|
|
||||||
stickyKeyboardInset = 0;
|
|
||||||
} else if (stickyKeyboardInset === 0) {
|
|
||||||
if (effectiveMeasuredInset > 0 && isTextTarget) {
|
|
||||||
stickyKeyboardInset = effectiveMeasuredInset;
|
|
||||||
setKeyboardOpen(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const closingByHeight = !isTextTarget && height > previousHeight + 6;
|
|
||||||
|
|
||||||
if (effectiveMeasuredInset === 0) {
|
|
||||||
stickyKeyboardInset = 0;
|
|
||||||
setKeyboardOpen(false);
|
|
||||||
} else if (closingByHeight) {
|
|
||||||
forceKeyboardClosed();
|
|
||||||
} else if (effectiveMeasuredInset > 0 && isTextTarget) {
|
|
||||||
stickyKeyboardInset = effectiveMeasuredInset;
|
|
||||||
setKeyboardOpen(true);
|
|
||||||
} else if (effectiveMeasuredInset > stickyKeyboardInset) {
|
|
||||||
stickyKeyboardInset = effectiveMeasuredInset;
|
|
||||||
setKeyboardOpen(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
root.style.setProperty('--oc-keyboard-inset', `${stickyKeyboardInset}px`);
|
|
||||||
previousHeight = height;
|
|
||||||
|
|
||||||
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, effectiveMeasuredInset);
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const related = event.relatedTarget as HTMLElement | null;
|
|
||||||
if (isTextInputTarget(related)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ const setRootDeviceAttributes = (
|
|||||||
isTauriShellRuntime: boolean,
|
isTauriShellRuntime: boolean,
|
||||||
deviceType: DeviceType,
|
deviceType: DeviceType,
|
||||||
hasTouchInput: boolean,
|
hasTouchInput: boolean,
|
||||||
isAndroid: boolean,
|
|
||||||
) => {
|
) => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return;
|
return;
|
||||||
@@ -42,7 +41,7 @@ const setRootDeviceAttributes = (
|
|||||||
const isMobile = deviceType === 'mobile';
|
const isMobile = deviceType === 'mobile';
|
||||||
const isTablet = deviceType === 'tablet';
|
const isTablet = deviceType === 'tablet';
|
||||||
|
|
||||||
root.classList.remove('device-mobile', 'device-tablet', 'device-desktop', 'device-android');
|
root.classList.remove('device-mobile', 'device-tablet', 'device-desktop');
|
||||||
root.classList.add(
|
root.classList.add(
|
||||||
deviceType === 'mobile'
|
deviceType === 'mobile'
|
||||||
? 'device-mobile'
|
? 'device-mobile'
|
||||||
@@ -61,9 +60,6 @@ const setRootDeviceAttributes = (
|
|||||||
root.classList.remove('mobile-pointer');
|
root.classList.remove('mobile-pointer');
|
||||||
} else {
|
} else {
|
||||||
root.classList.remove('desktop-runtime');
|
root.classList.remove('desktop-runtime');
|
||||||
if (isAndroid) {
|
|
||||||
root.classList.add('device-android');
|
|
||||||
}
|
|
||||||
root.style.setProperty('--is-mobile', isMobile ? '1' : '0');
|
root.style.setProperty('--is-mobile', isMobile ? '1' : '0');
|
||||||
root.style.setProperty('--device-type', deviceType);
|
root.style.setProperty('--device-type', deviceType);
|
||||||
root.style.setProperty('--font-scale', isMobile ? '0.9' : isTablet ? '0.95' : '1');
|
root.style.setProperty('--font-scale', isMobile ? '0.9' : isTablet ? '0.95' : '1');
|
||||||
@@ -85,8 +81,6 @@ export function getDeviceInfo(): DeviceInfo {
|
|||||||
const prefersCoarsePointer = pointerQuery?.matches ?? false;
|
const prefersCoarsePointer = pointerQuery?.matches ?? false;
|
||||||
const noHover = hoverQuery?.matches ?? false;
|
const noHover = hoverQuery?.matches ?? false;
|
||||||
const maxTouchPoints = typeof navigator !== 'undefined' ? navigator.maxTouchPoints ?? 0 : 0;
|
const maxTouchPoints = typeof navigator !== 'undefined' ? navigator.maxTouchPoints ?? 0 : 0;
|
||||||
const isAndroid = typeof navigator !== 'undefined' && /Android/i.test(navigator.userAgent);
|
|
||||||
|
|
||||||
const isDesktopShellRuntime = isDesktopShell();
|
const isDesktopShellRuntime = isDesktopShell();
|
||||||
|
|
||||||
const hasTouchInput = prefersCoarsePointer || noHover || maxTouchPoints > 0;
|
const hasTouchInput = prefersCoarsePointer || noHover || maxTouchPoints > 0;
|
||||||
@@ -113,7 +107,7 @@ export function getDeviceInfo(): DeviceInfo {
|
|||||||
deviceType = 'desktop';
|
deviceType = 'desktop';
|
||||||
}
|
}
|
||||||
|
|
||||||
setRootDeviceAttributes(isDesktopShellRuntime, deviceType, hasTouchInput, !isDesktopShellRuntime && isAndroid);
|
setRootDeviceAttributes(isDesktopShellRuntime, deviceType, hasTouchInput);
|
||||||
|
|
||||||
let breakpoint: keyof typeof BREAKPOINTS = 'xs';
|
let breakpoint: keyof typeof BREAKPOINTS = 'xs';
|
||||||
for (const [key, value] of Object.entries(BREAKPOINTS)) {
|
for (const [key, value] of Object.entries(BREAKPOINTS)) {
|
||||||
@@ -225,14 +219,8 @@ export function useDeviceInfo(): DeviceInfo {
|
|||||||
const prefersCoarsePointer = pointerQuery?.matches ?? false;
|
const prefersCoarsePointer = pointerQuery?.matches ?? false;
|
||||||
const noHover = hoverQuery?.matches ?? false;
|
const noHover = hoverQuery?.matches ?? false;
|
||||||
const maxTouchPoints = typeof navigator !== 'undefined' ? navigator.maxTouchPoints ?? 0 : 0;
|
const maxTouchPoints = typeof navigator !== 'undefined' ? navigator.maxTouchPoints ?? 0 : 0;
|
||||||
const isAndroid = typeof navigator !== 'undefined' && /Android/i.test(navigator.userAgent);
|
|
||||||
const hasTouchInput = prefersCoarsePointer || noHover || maxTouchPoints > 0;
|
const hasTouchInput = prefersCoarsePointer || noHover || maxTouchPoints > 0;
|
||||||
setRootDeviceAttributes(
|
setRootDeviceAttributes(isDesktopShellRuntime, deviceInfo.deviceType, hasTouchInput);
|
||||||
isDesktopShellRuntime,
|
|
||||||
deviceInfo.deviceType,
|
|
||||||
hasTouchInput,
|
|
||||||
!isDesktopShellRuntime && isAndroid,
|
|
||||||
);
|
|
||||||
}, [deviceInfo.deviceType, deviceInfo.hasTouchInput]);
|
}, [deviceInfo.deviceType, deviceInfo.hasTouchInput]);
|
||||||
|
|
||||||
return deviceInfo;
|
return deviceInfo;
|
||||||
|
|||||||
@@ -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((state) => state.isKeyboardOpen === open ? state : { isKeyboardOpen: open });
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleFavoriteModel: (providerID, modelID) => {
|
toggleFavoriteModel: (providerID, modelID) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const exists = state.favoriteModels.some(
|
const exists = state.favoriteModels.some(
|
||||||
|
|||||||
@@ -9,9 +9,6 @@
|
|||||||
--oc-header-height: 56px;
|
--oc-header-height: 56px;
|
||||||
--oc-visual-viewport-offset-top: 0px;
|
--oc-visual-viewport-offset-top: 0px;
|
||||||
--oc-visual-viewport-height: 100dvh;
|
--oc-visual-viewport-height: 100dvh;
|
||||||
--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;
|
||||||
|
|||||||
@@ -164,28 +164,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.device-mobile:not(.desktop-runtime) body,
|
|
||||||
:root.device-mobile:not(.desktop-runtime) #root {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: var(--oc-visual-viewport-height, 100dvh);
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
overscroll-behavior-y: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.device-android:not(.desktop-runtime) body,
|
|
||||||
:root.device-android:not(.desktop-runtime) #root {
|
|
||||||
position: relative;
|
|
||||||
top: auto;
|
|
||||||
left: auto;
|
|
||||||
right: auto;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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;
|
||||||
@@ -288,10 +266,6 @@
|
|||||||
padding-bottom: var(--oc-safe-area-bottom-visual);
|
padding-bottom: var(--oc-safe-area-bottom-visual);
|
||||||
}
|
}
|
||||||
|
|
||||||
: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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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;
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
<html lang="en" class="h-full">
|
<html lang="en" class="h-full">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<!-- interactive-widget=resizes-content: tells supporting browsers (Chrome 108+, Safari 16.4+) that the
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
page handles keyboard-driven viewport resizing itself. Unsupported browsers ignore the token. -->
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" />
|
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
|||||||
Reference in New Issue
Block a user