fix: improve desktop resize responsiveness

This commit is contained in:
Bohdan Triapitsyn
2026-06-05 13:36:39 +03:00
parent 4e1560a10e
commit f1675d27da
4 changed files with 169 additions and 103 deletions
+13 -2
View File
@@ -182,6 +182,7 @@ const state = {
windowCounter: 1,
focusedWindowIds: new Set(),
windowGeometryRevisions: new Map(),
windowGeometryTimers: new Map(),
miniChatWindowsBySession: new Map(),
sshStatuses: new Map(),
sshLogs: new Map(),
@@ -581,8 +582,15 @@ const debounceWindowStatePersist = (browserWindow, immediate = false) => {
const revision = (state.windowGeometryRevisions.get(key) || 0) + 1;
state.windowGeometryRevisions.set(key, revision);
const existingTimer = state.windowGeometryTimers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
state.windowGeometryTimers.delete(key);
}
const persist = async () => {
if (state.windowGeometryRevisions.get(key) !== revision) return;
state.windowGeometryTimers.delete(key);
await writeWindowState(browserWindow);
};
@@ -591,9 +599,10 @@ const debounceWindowStatePersist = (browserWindow, immediate = false) => {
return;
}
setTimeout(() => {
const timer = setTimeout(() => {
void persist();
}, 300);
state.windowGeometryTimers.set(key, timer);
};
const buildHealthUrl = (url) => {
@@ -1848,7 +1857,9 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
}
browserWindow.on('resize', () => {
emitToWindow(browserWindow, 'openchamber:window-resized');
if (process.platform === 'darwin') {
emitToWindow(browserWindow, 'openchamber:window-resized');
}
debounceWindowStatePersist(browserWindow, false);
});
browserWindow.on('maximize', () => {
@@ -271,24 +271,24 @@ export const MainLayout: React.FC = () => {
return;
}
let timeoutId: number | undefined;
let frameId: number | undefined;
const handleResize = () => {
if (timeoutId !== undefined) {
window.clearTimeout(timeoutId);
if (frameId !== undefined) {
return;
}
timeoutId = window.setTimeout(() => {
frameId = window.requestAnimationFrame(() => {
frameId = undefined;
useUIStore.getState().updateProportionalSidebarWidths();
}, 150);
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
if (timeoutId !== undefined) {
window.clearTimeout(timeoutId);
if (frameId !== undefined) {
window.cancelAnimationFrame(frameId);
}
};
}, []);
@@ -298,7 +298,7 @@ export const MainLayout: React.FC = () => {
return;
}
let timeoutId: number | undefined;
let frameId: number | undefined;
const handleResponsivePanels = () => {
const state = useUIStore.getState();
@@ -340,13 +340,13 @@ export const MainLayout: React.FC = () => {
};
const handleResize = () => {
if (timeoutId !== undefined) {
window.clearTimeout(timeoutId);
if (frameId !== undefined) {
return;
}
timeoutId = window.setTimeout(() => {
frameId = window.requestAnimationFrame(() => {
frameId = undefined;
handleResponsivePanels();
}, 100);
});
};
handleResponsivePanels();
@@ -354,8 +354,8 @@ export const MainLayout: React.FC = () => {
return () => {
window.removeEventListener('resize', handleResize);
if (timeoutId !== undefined) {
window.clearTimeout(timeoutId);
if (frameId !== undefined) {
window.cancelAnimationFrame(frameId);
}
};
}, [isMobile, isTablet, setBottomTerminalOpen, setRightSidebarOpen]);
+135 -83
View File
@@ -29,6 +29,17 @@ export const BREAKPOINTS = {
'2xl': 1536,
} as const;
const DEFAULT_DEVICE_INFO: DeviceInfo = {
isMobile: false,
isTablet: false,
isDesktop: true,
deviceType: 'desktop',
screenWidth: 1024,
breakpoint: 'lg',
hasTouchInput: false,
hasTouchOnlyPointer: false,
};
const getNavigatorDeviceHints = (maxTouchPoints: number) => {
if (typeof navigator === 'undefined') {
return { isExplicitTablet: false };
@@ -146,6 +157,125 @@ export function getDeviceInfo(): DeviceInfo {
};
}
const isSameDeviceInfo = (left: DeviceInfo, right: DeviceInfo): boolean => (
left.isMobile === right.isMobile
&& left.isTablet === right.isTablet
&& left.isDesktop === right.isDesktop
&& left.deviceType === right.deviceType
&& left.screenWidth === right.screenWidth
&& left.breakpoint === right.breakpoint
&& left.hasTouchInput === right.hasTouchInput
&& left.hasTouchOnlyPointer === right.hasTouchOnlyPointer
);
const deviceInfoSubscribers = new Set<() => void>();
let deviceInfoSnapshot: DeviceInfo | null = null;
let deviceInfoFrameId: number | undefined;
let cleanupDeviceInfoSource: (() => void) | null = null;
const readDeviceInfoSnapshot = (): DeviceInfo => {
if (typeof window === 'undefined') {
return DEFAULT_DEVICE_INFO;
}
if (!deviceInfoSnapshot) {
deviceInfoSnapshot = getDeviceInfo();
}
return deviceInfoSnapshot;
};
const notifyDeviceInfoSubscribers = () => {
for (const listener of deviceInfoSubscribers) {
listener();
}
};
const updateDeviceInfoSnapshot = () => {
deviceInfoFrameId = undefined;
const next = getDeviceInfo();
if (deviceInfoSnapshot && isSameDeviceInfo(deviceInfoSnapshot, next)) {
return;
}
deviceInfoSnapshot = next;
notifyDeviceInfoSubscribers();
};
const scheduleDeviceInfoUpdate = () => {
if (typeof window === 'undefined' || deviceInfoFrameId !== undefined) {
return;
}
deviceInfoFrameId = window.requestAnimationFrame(updateDeviceInfoSnapshot);
};
const attachMediaQueryListener = (query: MediaQueryList | null, listener: () => void): (() => void) => {
if (!query) {
return () => {};
}
if (typeof query.addEventListener === 'function') {
query.addEventListener('change', listener);
return () => query.removeEventListener('change', listener);
}
if (typeof query.addListener === 'function') {
query.addListener(listener);
return () => query.removeListener(listener);
}
return () => {};
};
const startDeviceInfoSource = (): (() => void) => {
if (typeof window === 'undefined') {
return () => {};
}
deviceInfoSnapshot = getDeviceInfo();
window.addEventListener('resize', scheduleDeviceInfoUpdate);
const pointerQuery = typeof window.matchMedia === 'function'
? window.matchMedia('(pointer: coarse)')
: null;
const hoverQuery = typeof window.matchMedia === 'function'
? window.matchMedia('(hover: none)')
: null;
const cleanupPointer = attachMediaQueryListener(pointerQuery, scheduleDeviceInfoUpdate);
const cleanupHover = attachMediaQueryListener(hoverQuery, scheduleDeviceInfoUpdate);
return () => {
window.removeEventListener('resize', scheduleDeviceInfoUpdate);
cleanupPointer();
cleanupHover();
if (deviceInfoFrameId !== undefined) {
window.cancelAnimationFrame(deviceInfoFrameId);
deviceInfoFrameId = undefined;
}
};
};
const subscribeDeviceInfo = (listener: () => void): (() => void) => {
if (typeof window === 'undefined') {
return () => {};
}
deviceInfoSubscribers.add(listener);
if (!cleanupDeviceInfoSource) {
cleanupDeviceInfoSource = startDeviceInfoSource();
}
return () => {
deviceInfoSubscribers.delete(listener);
if (deviceInfoSubscribers.size === 0 && cleanupDeviceInfoSource) {
cleanupDeviceInfoSource();
cleanupDeviceInfoSource = null;
deviceInfoSnapshot = null;
}
};
};
export function isMobileDeviceViaCSS(): boolean {
if (typeof window === 'undefined') return false;
@@ -224,87 +354,9 @@ export function useTabletStandalonePwaRuntime(): boolean {
}
export function useDeviceInfo(): DeviceInfo {
const [deviceInfo, setDeviceInfo] = React.useState<DeviceInfo>(() => {
if (typeof window === 'undefined') {
return {
isMobile: false,
isTablet: false,
isDesktop: true,
deviceType: 'desktop',
screenWidth: 1024,
breakpoint: 'lg',
hasTouchInput: false,
hasTouchOnlyPointer: false,
};
}
return getDeviceInfo();
});
React.useEffect(() => {
if (typeof window === 'undefined') return;
let debounceTimer: ReturnType<typeof setTimeout>;
const handleResize = () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
setDeviceInfo(getDeviceInfo());
}, 150);
};
window.addEventListener('resize', handleResize);
return () => {
clearTimeout(debounceTimer);
window.removeEventListener('resize', handleResize);
};
}, []);
React.useEffect(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
return;
}
const pointerQuery = window.matchMedia('(pointer: coarse)');
const hoverQuery = window.matchMedia('(hover: none)');
const handlePointerChange = () => {
setDeviceInfo(getDeviceInfo());
};
const cleanups: Array<() => void> = [];
const attachListener = (query: MediaQueryList | null) => {
if (!query) {
return;
}
if (typeof query.addEventListener === 'function') {
query.addEventListener('change', handlePointerChange);
cleanups.push(() => query.removeEventListener('change', handlePointerChange));
} else if (typeof query.addListener === 'function') {
query.addListener(handlePointerChange);
cleanups.push(() => query.removeListener(handlePointerChange));
}
};
attachListener(pointerQuery);
attachListener(hoverQuery);
return () => {
cleanups.forEach((cleanup) => cleanup());
};
}, []);
React.useEffect(() => {
if (typeof window === 'undefined') return;
const isDesktopShellRuntime = isDesktopShell();
const supportsMatchMedia = typeof window.matchMedia === 'function';
const pointerQuery = supportsMatchMedia ? window.matchMedia('(pointer: coarse)') : null;
const hoverQuery = supportsMatchMedia ? window.matchMedia('(hover: none)') : null;
const prefersCoarsePointer = pointerQuery?.matches ?? false;
const noHover = hoverQuery?.matches ?? false;
const maxTouchPoints = typeof navigator !== 'undefined' ? navigator.maxTouchPoints ?? 0 : 0;
const hasTouchInput = prefersCoarsePointer || noHover || maxTouchPoints > 0;
setRootDeviceAttributes(isDesktopShellRuntime, deviceInfo.deviceType, hasTouchInput);
}, [deviceInfo.deviceType, deviceInfo.hasTouchInput]);
return deviceInfo;
return React.useSyncExternalStore(
subscribeDeviceInfo,
readDeviceInfoSnapshot,
() => DEFAULT_DEVICE_INFO,
);
}
+5 -2
View File
@@ -1854,10 +1854,13 @@ export const useUIStore = create<UIStore>()(
const updates: Partial<UIStore> = {};
if (state.isBottomTerminalOpen && !state.hasManuallyResizedBottomTerminal) {
updates.bottomTerminalHeight = Math.floor(window.innerHeight * 0.32);
const nextHeight = Math.floor(window.innerHeight * 0.32);
if (state.bottomTerminalHeight !== nextHeight) {
updates.bottomTerminalHeight = nextHeight;
}
}
return updates;
return Object.keys(updates).length > 0 ? updates : state;
});
},