nicolium: window-controls-overlay improvements

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-02-26 14:49:03 +01:00
parent 531df09182
commit 267e644481
3 changed files with 66 additions and 4 deletions

View File

@ -54,6 +54,37 @@ const useMinWidth = (query: string) => {
return matches;
};
interface WindowControlsOverlay extends EventTarget {
visible: boolean;
getTitlebarAreaRect(): DOMRect;
}
declare global {
interface Navigator {
windowControlsOverlay?: WindowControlsOverlay;
}
}
const useWindowControlsOverlay = () => {
const getRect = (): DOMRect | null => {
const overlay = navigator.windowControlsOverlay;
return overlay?.visible ? overlay.getTitlebarAreaRect() : null;
};
const [rect, setRect] = useState<DOMRect | null>(getRect);
useEffect(() => {
const overlay = navigator.windowControlsOverlay;
if (!overlay) return;
const update = () => setRect(overlay.visible ? overlay.getTitlebarAreaRect() : null);
overlay.addEventListener('geometrychange', update);
return () => overlay.removeEventListener('geometrychange', update);
}, []);
return rect;
};
/** Layout container, to hold Sidebar, Main, and Aside. */
const Layout: LayoutComponent = ({ children, fullWidth }) => (
<div className='⁂-layout'>
@ -70,6 +101,8 @@ const Layout: LayoutComponent = ({ children, fullWidth }) => (
/** Left sidebar container in the UI. */
const Sidebar: React.FC<ISidebar> = ({ children, shrink }) => {
const isVisible = useMinWidth(`(min-width: ${breakpoints.lg})`);
const wcoRect = useWindowControlsOverlay();
const offsetTop = wcoRect && wcoRect.x > 0 ? 16 + wcoRect.y : 16;
if (!isVisible) {
return null;
@ -77,7 +110,7 @@ const Sidebar: React.FC<ISidebar> = ({ children, shrink }) => {
return (
<div className={clsx('⁂-layout__sidebar', { '⁂-layout__sidebar--shrink': shrink })}>
<StickyBox offsetTop={16} className='⁂-layout__sidebar__content'>
<StickyBox offsetTop={offsetTop} className='⁂-layout__sidebar__content'>
{children}
</StickyBox>
</div>
@ -106,6 +139,8 @@ const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, classN
/** Right sidebar container in the UI. */
const Aside: React.FC<IAside> = ({ children }) => {
const isVisible = useMinWidth(`(min-width: ${breakpoints.xl})`);
const wcoRect = useWindowControlsOverlay();
const offsetTop = wcoRect && wcoRect.x + wcoRect.width < window.innerWidth ? 16 + wcoRect.y : 16;
if (!isVisible) {
return null;
@ -113,7 +148,7 @@ const Aside: React.FC<IAside> = ({ children }) => {
return (
<aside className='⁂-layout__aside'>
<StickyBox offsetTop={16} className='⁂-layout__aside__content'>
<StickyBox offsetTop={offsetTop} className='⁂-layout__aside__content'>
<Suspense>{children}</Suspense>
</StickyBox>
</aside>

View File

@ -1,5 +1,5 @@
import clsx from 'clsx';
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import InlineStyle from '@/components/inline-style';
import { useFrontendConfig } from '@/hooks/use-frontend-config';
@ -25,6 +25,7 @@ const PlFeHead = () => {
} = useSettings();
const frontendConfig = useFrontendConfig();
const theme = useTheme();
const [wcoVisible, setWcoVisible] = React.useState(false);
const withModals = useHasModals();
@ -45,6 +46,22 @@ const PlFeHead = () => {
}
}, [dsn]);
useEffect(() => {
const overlay = navigator.windowControlsOverlay;
if (!overlay) return;
const update = () => setWcoVisible(overlay.visible);
overlay.addEventListener('geometrychange', update);
return () => overlay.removeEventListener('geometrychange', update);
}, []);
const color = useMemo(() => {
if (wcoVisible) {
return window.getComputedStyle(document.body, null).getPropertyValue('background-color');
}
return frontendConfig.brandColor;
}, [frontendConfig.brandColor, theme, wcoVisible]);
return (
<>
<Helmet>
@ -53,10 +70,11 @@ const PlFeHead = () => {
className={clsx(`text-${themeSettings?.interfaceSize ?? 'md'}`, {
dark: theme === 'dark',
'black dark': theme === 'black',
'window-controls-overlay': wcoVisible,
})}
/>
<body className={bodyClass} dir={direction} />
<meta name='theme-color' content={frontendConfig.brandColor} />
<meta name='theme-color' content={color} />
</Helmet>
<InlineStyle>{`:root { ${themeCss} }`}</InlineStyle>
{['dark', 'black'].includes(theme) && (

View File

@ -1,4 +1,5 @@
@use 'mixins';
@use 'variables';
html {
height: 100%;
@ -373,6 +374,14 @@ body {
&--chats {
@apply xl:pb-16;
}
&:is(.window-controls-overlay *) {
padding-top: env(titlebar-area-height);
@media (min-width: variables.$breakpoint-lg) {
padding-top: 0;
}
}
}
&__aside {