fix(ui): stabilize mobile keyboard viewport handling (#1026)
* fix(ui): stabilize mobile keyboard viewport handling * fix(ui): tighten mobile keyboard viewport handling * Avoid redundant keyboard open store writes --------- Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
vhqtvn
Bohdan Triapitsyn
parent
33b1b67514
commit
87db2ea210
@@ -786,6 +786,7 @@ 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);
|
||||||
@@ -3291,10 +3292,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 && 'bottom-safe-area'
|
isMobile && (isKeyboardOpen ? 'ios-keyboard-safe-area' : '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 />
|
||||||
|
|||||||
@@ -351,6 +351,255 @@ 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 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) {
|
||||||
|
// 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
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 isTextInputTarget = (element: HTMLElement | null) => {
|
||||||
|
if (!element) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const tagName = element.tagName;
|
||||||
|
const isInput = tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT';
|
||||||
|
return isInput || element.isContentEditable;
|
||||||
|
};
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
const secondaryView = React.useMemo(() => {
|
const secondaryView = React.useMemo(() => {
|
||||||
switch (activeMainTab) {
|
switch (activeMainTab) {
|
||||||
case 'plan':
|
case 'plan':
|
||||||
@@ -382,7 +631,8 @@ export const MainLayout: React.FC = () => {
|
|||||||
<DiffWorkerProvider>
|
<DiffWorkerProvider>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'main-content-safe-area h-[100dvh]',
|
'main-content-safe-area',
|
||||||
|
isMobile ? 'h-full' : 'h-[100dvh]',
|
||||||
isMobile ? 'flex flex-col' : 'flex',
|
isMobile ? 'flex flex-col' : 'flex',
|
||||||
'bg-background'
|
'bg-background'
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -223,6 +223,10 @@ 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,6 +32,7 @@ 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;
|
||||||
@@ -41,7 +42,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');
|
root.classList.remove('device-mobile', 'device-tablet', 'device-desktop', 'device-android');
|
||||||
root.classList.add(
|
root.classList.add(
|
||||||
deviceType === 'mobile'
|
deviceType === 'mobile'
|
||||||
? 'device-mobile'
|
? 'device-mobile'
|
||||||
@@ -60,6 +61,9 @@ 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');
|
||||||
@@ -81,6 +85,7 @@ 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();
|
||||||
|
|
||||||
@@ -108,7 +113,7 @@ export function getDeviceInfo(): DeviceInfo {
|
|||||||
deviceType = 'desktop';
|
deviceType = 'desktop';
|
||||||
}
|
}
|
||||||
|
|
||||||
setRootDeviceAttributes(isDesktopShellRuntime, deviceType, hasTouchInput);
|
setRootDeviceAttributes(isDesktopShellRuntime, deviceType, hasTouchInput, !isDesktopShellRuntime && isAndroid);
|
||||||
|
|
||||||
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)) {
|
||||||
@@ -220,8 +225,14 @@ 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(isDesktopShellRuntime, deviceInfo.deviceType, hasTouchInput);
|
setRootDeviceAttributes(
|
||||||
|
isDesktopShellRuntime,
|
||||||
|
deviceInfo.deviceType,
|
||||||
|
hasTouchInput,
|
||||||
|
!isDesktopShellRuntime && isAndroid,
|
||||||
|
);
|
||||||
}, [deviceInfo.deviceType, deviceInfo.hasTouchInput]);
|
}, [deviceInfo.deviceType, deviceInfo.hasTouchInput]);
|
||||||
|
|
||||||
return deviceInfo;
|
return deviceInfo;
|
||||||
|
|||||||
@@ -482,6 +482,7 @@ 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;
|
||||||
@@ -636,6 +637,7 @@ 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;
|
||||||
@@ -722,6 +724,7 @@ 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,
|
||||||
@@ -1467,6 +1470,10 @@ 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(
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
--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-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;
|
||||||
|
|||||||
@@ -158,13 +158,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Fix mobile viewport height */
|
/* Fix mobile viewport height */
|
||||||
:root.mobile-pointer:not(.desktop-runtime),
|
:root.mobile-pointer:not(.desktop-runtime) {
|
||||||
:root.mobile-pointer:not(.desktop-runtime) body {
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
height: -webkit-fill-available;
|
height: -webkit-fill-available;
|
||||||
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;
|
||||||
@@ -267,6 +288,10 @@
|
|||||||
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,7 +2,7 @@
|
|||||||
<html lang="en" class="h-full">
|
<html lang="en" class="h-full">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
<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