feat: implement window controls overlay layout and styles (#1001)
This commit is contained in:
committed by
GitHub
parent
a9830cfcf1
commit
bf110527a5
@@ -18,6 +18,7 @@ import { useRouter } from '@/hooks/useRouter';
|
|||||||
import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
|
import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
|
||||||
import { usePwaManifestSync } from '@/hooks/usePwaManifestSync';
|
import { usePwaManifestSync } from '@/hooks/usePwaManifestSync';
|
||||||
import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt';
|
import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt';
|
||||||
|
import { useWindowControlsOverlayLayout } from '@/hooks/useWindowControlsOverlayLayout';
|
||||||
import { useWindowTitle } from '@/hooks/useWindowTitle';
|
import { useWindowTitle } from '@/hooks/useWindowTitle';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
import { hasModifier } from '@/lib/utils';
|
import { hasModifier } from '@/lib/utils';
|
||||||
@@ -167,6 +168,7 @@ function SyncAppEffects({ embeddedBackgroundWorkEnabled }: {
|
|||||||
embeddedBackgroundWorkEnabled: boolean;
|
embeddedBackgroundWorkEnabled: boolean;
|
||||||
}) {
|
}) {
|
||||||
usePwaManifestSync();
|
usePwaManifestSync();
|
||||||
|
useWindowControlsOverlayLayout();
|
||||||
useSessionAutoCleanup(embeddedBackgroundWorkEnabled);
|
useSessionAutoCleanup(embeddedBackgroundWorkEnabled);
|
||||||
useQueuedMessageAutoSend(embeddedBackgroundWorkEnabled);
|
useQueuedMessageAutoSend(embeddedBackgroundWorkEnabled);
|
||||||
useKeyboardShortcuts();
|
useKeyboardShortcuts();
|
||||||
|
|||||||
@@ -1356,6 +1356,19 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
return '';
|
return '';
|
||||||
}, [isDesktopApp, isMacPlatform, macosMajorVersion]);
|
}, [isDesktopApp, isMacPlatform, macosMajorVersion]);
|
||||||
|
|
||||||
|
const webWindowControlsOverlayStyle = React.useMemo<React.CSSProperties | undefined>(() => {
|
||||||
|
if (isDesktopApp || isVSCode) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
paddingLeft: 'calc(0.75rem + var(--oc-wco-left-inset, 0px))',
|
||||||
|
paddingRight: 'calc(0.75rem + var(--oc-wco-right-inset, 0px))',
|
||||||
|
minHeight: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
|
||||||
|
height: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
|
||||||
|
};
|
||||||
|
}, [isDesktopApp, isVSCode]);
|
||||||
|
|
||||||
const updateHeaderHeight = React.useCallback(() => {
|
const updateHeaderHeight = React.useCallback(() => {
|
||||||
if (typeof document === 'undefined') {
|
if (typeof document === 'undefined') {
|
||||||
return;
|
return;
|
||||||
@@ -1694,6 +1707,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
desktopPaddingClass,
|
desktopPaddingClass,
|
||||||
macosHeaderSizeClass
|
macosHeaderSizeClass
|
||||||
)}
|
)}
|
||||||
|
style={webWindowControlsOverlayStyle}
|
||||||
role="tablist"
|
role="tablist"
|
||||||
aria-label="Main navigation"
|
aria-label="Main navigation"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { isDesktopShell, startDesktopWindowDrag } from '@/lib/desktop';
|
import { isDesktopShell, isVSCodeRuntime, startDesktopWindowDrag } from '@/lib/desktop';
|
||||||
|
|
||||||
export const RIGHT_SIDEBAR_CONTENT_WIDTH = 420;
|
export const RIGHT_SIDEBAR_CONTENT_WIDTH = 420;
|
||||||
const RIGHT_SIDEBAR_MIN_WIDTH = 400;
|
const RIGHT_SIDEBAR_MIN_WIDTH = 400;
|
||||||
@@ -18,6 +18,7 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ isOpen, children, cl
|
|||||||
const rightSidebarWidth = useUIStore((state) => state.rightSidebarWidth);
|
const rightSidebarWidth = useUIStore((state) => state.rightSidebarWidth);
|
||||||
const setRightSidebarWidth = useUIStore((state) => state.setRightSidebarWidth);
|
const setRightSidebarWidth = useUIStore((state) => state.setRightSidebarWidth);
|
||||||
const isDesktopApp = React.useMemo(() => isDesktopShell(), []);
|
const isDesktopApp = React.useMemo(() => isDesktopShell(), []);
|
||||||
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
const [isResizing, setIsResizing] = React.useState(false);
|
const [isResizing, setIsResizing] = React.useState(false);
|
||||||
const startXRef = React.useRef(0);
|
const startXRef = React.useRef(0);
|
||||||
const startWidthRef = React.useRef(rightSidebarWidth || 420);
|
const startWidthRef = React.useRef(rightSidebarWidth || 420);
|
||||||
@@ -126,6 +127,17 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ isOpen, children, cl
|
|||||||
await startDesktopWindowDrag();
|
await startDesktopWindowDrag();
|
||||||
}, [isDesktopApp]);
|
}, [isDesktopApp]);
|
||||||
|
|
||||||
|
const webWindowControlsOverlayStyle = React.useMemo<React.CSSProperties | undefined>(() => {
|
||||||
|
if (isDesktopApp || isVSCode) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
paddingLeft: 'calc(0.75rem + var(--oc-wco-left-inset, 0px))',
|
||||||
|
paddingRight: 'calc(0.75rem + var(--oc-wco-right-inset, 0px))',
|
||||||
|
};
|
||||||
|
}, [isDesktopApp, isVSCode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
@@ -149,6 +161,7 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ isOpen, children, cl
|
|||||||
<div
|
<div
|
||||||
onMouseDown={handleDragStart}
|
onMouseDown={handleDragStart}
|
||||||
className="app-region-drag absolute inset-x-0 top-0 z-20 flex h-[var(--oc-header-height,56px)] items-center justify-end px-3"
|
className="app-region-drag absolute inset-x-0 top-0 z-20 flex h-[var(--oc-header-height,56px)] items-center justify-end px-3"
|
||||||
|
style={webWindowControlsOverlayStyle}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { isWebRuntime } from '@/lib/desktop';
|
||||||
|
|
||||||
|
type WindowControlsOverlayArea = {
|
||||||
|
x: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WindowControlsOverlayLike = {
|
||||||
|
visible: boolean;
|
||||||
|
getTitlebarAreaRect: () => WindowControlsOverlayArea;
|
||||||
|
addEventListener?: (type: 'geometrychange', listener: () => void) => void;
|
||||||
|
removeEventListener?: (type: 'geometrychange', listener: () => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavigatorWithWindowControlsOverlay = Navigator & {
|
||||||
|
windowControlsOverlay?: WindowControlsOverlayLike;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clampPx = (value: number): string => {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return '0px';
|
||||||
|
}
|
||||||
|
return `${Math.max(0, Math.round(value))}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyOverlayInsets = (
|
||||||
|
root: HTMLElement,
|
||||||
|
leftInsetPx: number,
|
||||||
|
rightInsetPx: number,
|
||||||
|
titlebarHeightPx: number,
|
||||||
|
) => {
|
||||||
|
root.style.setProperty('--oc-wco-left-inset', clampPx(leftInsetPx));
|
||||||
|
root.style.setProperty('--oc-wco-right-inset', clampPx(rightInsetPx));
|
||||||
|
root.style.setProperty('--oc-wco-titlebar-height', clampPx(titlebarHeightPx));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWindowControlsOverlayLayout = () => {
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof window === 'undefined' || !isWebRuntime()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
const mediaQuery = typeof window.matchMedia === 'function'
|
||||||
|
? window.matchMedia('(display-mode: window-controls-overlay)')
|
||||||
|
: null;
|
||||||
|
const navigatorWithOverlay = window.navigator as NavigatorWithWindowControlsOverlay;
|
||||||
|
const overlay = navigatorWithOverlay.windowControlsOverlay;
|
||||||
|
|
||||||
|
const updateGeometry = () => {
|
||||||
|
if (!overlay || !mediaQuery?.matches || !overlay.visible) {
|
||||||
|
applyOverlayInsets(root, 0, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = overlay.getTitlebarAreaRect();
|
||||||
|
const leftInset = Math.max(0, Number(rect.x) || 0);
|
||||||
|
const width = Math.max(0, Number(rect.width) || 0);
|
||||||
|
const titlebarHeight = Math.max(0, Number(rect.height) || 0);
|
||||||
|
const rightInset = Math.max(0, window.innerWidth - (leftInset + width));
|
||||||
|
|
||||||
|
applyOverlayInsets(root, leftInset, rightInset, titlebarHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMediaQueryChange = () => {
|
||||||
|
updateGeometry();
|
||||||
|
};
|
||||||
|
|
||||||
|
updateGeometry();
|
||||||
|
|
||||||
|
window.addEventListener('resize', updateGeometry);
|
||||||
|
window.addEventListener('focus', updateGeometry);
|
||||||
|
|
||||||
|
if (mediaQuery) {
|
||||||
|
if (typeof mediaQuery.addEventListener === 'function') {
|
||||||
|
mediaQuery.addEventListener('change', handleMediaQueryChange);
|
||||||
|
} else if (typeof mediaQuery.addListener === 'function') {
|
||||||
|
mediaQuery.addListener(handleMediaQueryChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overlay && typeof overlay.addEventListener === 'function') {
|
||||||
|
overlay.addEventListener('geometrychange', updateGeometry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updateGeometry);
|
||||||
|
window.removeEventListener('focus', updateGeometry);
|
||||||
|
|
||||||
|
if (mediaQuery) {
|
||||||
|
if (typeof mediaQuery.removeEventListener === 'function') {
|
||||||
|
mediaQuery.removeEventListener('change', handleMediaQueryChange);
|
||||||
|
} else if (typeof mediaQuery.removeListener === 'function') {
|
||||||
|
mediaQuery.removeListener(handleMediaQueryChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overlay && typeof overlay.removeEventListener === 'function') {
|
||||||
|
overlay.removeEventListener('geometrychange', updateGeometry);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
--oc-keyboard-inset: 0px;
|
--oc-keyboard-inset: 0px;
|
||||||
--oc-keyboard-avoid-offset: 0px;
|
--oc-keyboard-avoid-offset: 0px;
|
||||||
--oc-keyboard-home-indicator: 0px;
|
--oc-keyboard-home-indicator: 0px;
|
||||||
|
--oc-wco-left-inset: 0px;
|
||||||
|
--oc-wco-right-inset: 0px;
|
||||||
|
--oc-wco-titlebar-height: 0px;
|
||||||
--ui-regular-font-weight: 400;
|
--ui-regular-font-weight: 400;
|
||||||
--oc-scrollbar-thumb: oklch(0.32 0.03 50 / 0.4);
|
--oc-scrollbar-thumb: oklch(0.32 0.03 50 / 0.4);
|
||||||
--oc-scrollbar-thumb-hover: oklch(0.32 0.03 50 / 0.6);
|
--oc-scrollbar-thumb-hover: oklch(0.32 0.03 50 / 0.6);
|
||||||
|
|||||||
@@ -200,6 +200,7 @@
|
|||||||
start_url: `${baseUrl}/`,
|
start_url: `${baseUrl}/`,
|
||||||
scope: `${baseUrl}/`,
|
scope: `${baseUrl}/`,
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
|
display_override: ['window-controls-overlay'],
|
||||||
...(manifestOrientation ? { orientation: manifestOrientation } : {}),
|
...(manifestOrientation ? { orientation: manifestOrientation } : {}),
|
||||||
background_color: '#151313',
|
background_color: '#151313',
|
||||||
theme_color: '#edb449',
|
theme_color: '#edb449',
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ export const registerPwaManifestRoute = (app, dependencies) => {
|
|||||||
start_url: '/',
|
start_url: '/',
|
||||||
scope: '/',
|
scope: '/',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
|
display_override: ['window-controls-overlay'],
|
||||||
background_color: '#151313',
|
background_color: '#151313',
|
||||||
theme_color: '#edb449',
|
theme_color: '#edb449',
|
||||||
...(manifestOrientation ? { orientation: manifestOrientation } : {}),
|
...(manifestOrientation ? { orientation: manifestOrientation } : {}),
|
||||||
|
|||||||
Reference in New Issue
Block a user