fix: align iPad PWA window controls spacing
Adds tablet standalone PWA detection Aligns header and sidebar action padding Extends safe-area handling for tablet PWAs
This commit is contained in:
@@ -1401,14 +1401,14 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paddingLeft: isTabletStandalonePwa
|
paddingLeft: isTabletStandalonePwa && !isSidebarOpen
|
||||||
? 'max(calc(0.75rem + var(--oc-wco-left-inset, 0px)), 5.5rem)'
|
? 'max(calc(0.75rem + var(--oc-wco-left-inset, 0px)), 5.5rem)'
|
||||||
: 'calc(0.75rem + var(--oc-wco-left-inset, 0px))',
|
: 'calc(0.75rem + var(--oc-wco-left-inset, 0px))',
|
||||||
paddingRight: 'calc(0.75rem + var(--oc-wco-right-inset, 0px))',
|
paddingRight: 'calc(0.75rem + var(--oc-wco-right-inset, 0px))',
|
||||||
minHeight: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
|
minHeight: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
|
||||||
height: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
|
height: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
|
||||||
};
|
};
|
||||||
}, [isDesktopApp, isTabletStandalonePwa, isVSCode]);
|
}, [isDesktopApp, isSidebarOpen, isTabletStandalonePwa, isVSCode]);
|
||||||
|
|
||||||
const updateHeaderHeight = React.useCallback(() => {
|
const updateHeaderHeight = React.useCallback(() => {
|
||||||
if (typeof document === 'undefined') {
|
if (typeof document === 'undefined') {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { cn } from '@/lib/utils';
|
|||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { isDesktopShell, isVSCodeRuntime, startDesktopWindowDrag } from '@/lib/desktop';
|
import { isDesktopShell, isVSCodeRuntime, startDesktopWindowDrag } from '@/lib/desktop';
|
||||||
|
import { useTabletStandalonePwaRuntime } from '@/lib/device';
|
||||||
|
|
||||||
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;
|
||||||
@@ -21,6 +22,7 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ isOpen, children, cl
|
|||||||
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 isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
|
const isTabletStandalonePwa = useTabletStandalonePwaRuntime();
|
||||||
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);
|
||||||
@@ -137,8 +139,9 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ isOpen, children, cl
|
|||||||
return {
|
return {
|
||||||
paddingLeft: 'calc(0.75rem + var(--oc-wco-left-inset, 0px))',
|
paddingLeft: 'calc(0.75rem + var(--oc-wco-left-inset, 0px))',
|
||||||
paddingRight: 'calc(0.75rem + var(--oc-wco-right-inset, 0px))',
|
paddingRight: 'calc(0.75rem + var(--oc-wco-right-inset, 0px))',
|
||||||
|
...(isTabletStandalonePwa ? { paddingTop: 'var(--oc-safe-area-top, 0px)' } : null),
|
||||||
};
|
};
|
||||||
}, [isDesktopApp, isVSCode]);
|
}, [isDesktopApp, isTabletStandalonePwa, isVSCode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
@@ -162,7 +165,10 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ isOpen, children, cl
|
|||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<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={cn(
|
||||||
|
'app-region-drag absolute inset-x-0 top-0 z-20 flex items-center justify-end px-3',
|
||||||
|
'h-[var(--oc-header-height,56px)]',
|
||||||
|
)}
|
||||||
style={webWindowControlsOverlayStyle}
|
style={webWindowControlsOverlayStyle}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -94,12 +94,17 @@ export function SidebarHeader(props: Props): React.ReactNode {
|
|||||||
'select-none flex-shrink-0',
|
'select-none flex-shrink-0',
|
||||||
showSidebarToggle ? (avoidWindowControlsOverlay ? 'pl-[5.5rem] pr-3' : 'pl-3 pr-3') : 'px-2.5 py-1',
|
showSidebarToggle ? (avoidWindowControlsOverlay ? 'pl-[5.5rem] pr-3' : 'pl-3 pr-3') : 'px-2.5 py-1',
|
||||||
)}
|
)}
|
||||||
|
style={showSidebarToggle && avoidWindowControlsOverlay ? { paddingTop: 'var(--oc-safe-area-top, 0px)' } : undefined}
|
||||||
>
|
>
|
||||||
{reserveHeaderActionsSpace ? (
|
{reserveHeaderActionsSpace ? (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-auto flex-col gap-1',
|
'flex h-auto flex-col gap-1',
|
||||||
showSidebarToggle ? 'min-h-[var(--oc-header-height,56px)] justify-center' : 'min-h-8',
|
showSidebarToggle
|
||||||
|
? avoidWindowControlsOverlay
|
||||||
|
? 'min-h-[calc(var(--oc-header-height,56px)-var(--oc-safe-area-top,0px))] justify-center'
|
||||||
|
: 'min-h-[var(--oc-header-height,56px)] justify-center'
|
||||||
|
: 'min-h-8',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex h-8 items-center justify-between gap-2">
|
<div className="flex h-8 items-center justify-between gap-2">
|
||||||
|
|||||||
@@ -28,6 +28,21 @@ export const BREAKPOINTS = {
|
|||||||
'2xl': 1536,
|
'2xl': 1536,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const getNavigatorDeviceHints = (maxTouchPoints: number) => {
|
||||||
|
if (typeof navigator === 'undefined') {
|
||||||
|
return { isExplicitTablet: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAgent = navigator.userAgent || '';
|
||||||
|
const platform = navigator.platform || '';
|
||||||
|
const isIPad = /iPad/i.test(userAgent)
|
||||||
|
|| ((/Macintosh|MacIntel/i.test(userAgent) || /MacIntel/i.test(platform)) && maxTouchPoints > 1);
|
||||||
|
const isAndroidTablet = /Android/i.test(userAgent) && !/Mobile/i.test(userAgent);
|
||||||
|
const isGenericTablet = /Tablet/i.test(userAgent);
|
||||||
|
|
||||||
|
return { isExplicitTablet: isIPad || isAndroidTablet || isGenericTablet };
|
||||||
|
};
|
||||||
|
|
||||||
const setRootDeviceAttributes = (
|
const setRootDeviceAttributes = (
|
||||||
isTauriShellRuntime: boolean,
|
isTauriShellRuntime: boolean,
|
||||||
deviceType: DeviceType,
|
deviceType: DeviceType,
|
||||||
@@ -82,6 +97,7 @@ export function getDeviceInfo(): DeviceInfo {
|
|||||||
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 isDesktopShellRuntime = isDesktopShell();
|
const isDesktopShellRuntime = isDesktopShell();
|
||||||
|
const { isExplicitTablet } = getNavigatorDeviceHints(maxTouchPoints);
|
||||||
|
|
||||||
const hasTouchInput = prefersCoarsePointer || noHover || maxTouchPoints > 0;
|
const hasTouchInput = prefersCoarsePointer || noHover || maxTouchPoints > 0;
|
||||||
|
|
||||||
@@ -89,8 +105,8 @@ export function getDeviceInfo(): DeviceInfo {
|
|||||||
const isMobileWidth = width <= BREAKPOINTS.md;
|
const isMobileWidth = width <= BREAKPOINTS.md;
|
||||||
|
|
||||||
let isMobile = hasTouchInput && isMobileWidth;
|
let isMobile = hasTouchInput && isMobileWidth;
|
||||||
let isTablet = hasTouchInput && !isMobile && isTabletWidth;
|
let isTablet = hasTouchInput && !isMobile && (isTabletWidth || isExplicitTablet);
|
||||||
let isDesktop = !hasTouchInput || width > BREAKPOINTS.lg;
|
let isDesktop = !hasTouchInput || (!isTablet && width > BREAKPOINTS.lg);
|
||||||
let deviceType: DeviceType = 'desktop';
|
let deviceType: DeviceType = 'desktop';
|
||||||
|
|
||||||
if (isDesktopShellRuntime) {
|
if (isDesktopShellRuntime) {
|
||||||
|
|||||||
@@ -223,7 +223,9 @@
|
|||||||
|
|
||||||
/* iOS safe area defaults (applies to PWA + Safari) */
|
/* iOS safe area defaults (applies to PWA + Safari) */
|
||||||
@supports (-webkit-touch-callout: none) {
|
@supports (-webkit-touch-callout: none) {
|
||||||
:root.device-mobile:not(.desktop-runtime) {
|
:root.device-mobile:not(.desktop-runtime),
|
||||||
|
:root.device-tablet:not(.desktop-runtime),
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) {
|
||||||
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
||||||
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
||||||
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
||||||
@@ -231,7 +233,9 @@
|
|||||||
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.device-mobile:not(.desktop-runtime) .header-safe-area {
|
:root.device-mobile:not(.desktop-runtime) .header-safe-area,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) .header-safe-area,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||||
padding-top: var(--oc-safe-area-top);
|
padding-top: var(--oc-safe-area-top);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,7 +245,9 @@
|
|||||||
/* iOS-specific safe area handling with CSS variable support */
|
/* iOS-specific safe area handling with CSS variable support */
|
||||||
@supports (-webkit-touch-callout: none) {
|
@supports (-webkit-touch-callout: none) {
|
||||||
/* Safe area handling for fixed positioned elements */
|
/* Safe area handling for fixed positioned elements */
|
||||||
:root.device-mobile:not(.desktop-runtime) {
|
:root.device-mobile:not(.desktop-runtime),
|
||||||
|
:root.device-tablet:not(.desktop-runtime),
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) {
|
||||||
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
||||||
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
||||||
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
||||||
@@ -249,11 +255,15 @@
|
|||||||
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.device-mobile:not(.desktop-runtime) .header-safe-area {
|
:root.device-mobile:not(.desktop-runtime) .header-safe-area,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) .header-safe-area,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||||
padding-top: var(--oc-safe-area-top);
|
padding-top: var(--oc-safe-area-top);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.device-mobile:not(.desktop-runtime) .main-content-safe-area {
|
:root.device-mobile:not(.desktop-runtime) .main-content-safe-area,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) .main-content-safe-area,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .main-content-safe-area {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
padding-left: var(--oc-safe-area-left);
|
padding-left: var(--oc-safe-area-left);
|
||||||
@@ -261,35 +271,47 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Safe area for bottom fixed/bottom-0 elements */
|
/* Safe area for bottom fixed/bottom-0 elements */
|
||||||
:root.device-mobile:not(.desktop-runtime) .bottom-safe-area {
|
:root.device-mobile:not(.desktop-runtime) .bottom-safe-area,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) .bottom-safe-area,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .bottom-safe-area {
|
||||||
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Drawer safe area - top already offset by header height */
|
/* Drawer safe area - top already offset by header height */
|
||||||
:root.device-mobile:not(.desktop-runtime) .drawer-safe-area {
|
:root.device-mobile:not(.desktop-runtime) .drawer-safe-area,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) .drawer-safe-area,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .drawer-safe-area {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: var(--oc-safe-area-bottom-visual);
|
padding-bottom: var(--oc-safe-area-bottom-visual);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) .flex.flex-col.h-screen,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: -webkit-fill-available;
|
min-height: -webkit-fill-available;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent content overlap in iOS */
|
/* Prevent content overlap in iOS */
|
||||||
:root.device-mobile:not(.desktop-runtime) .flex-1.overflow-hidden {
|
:root.device-mobile:not(.desktop-runtime) .flex-1.overflow-hidden,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) .flex-1.overflow-hidden,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .flex-1.overflow-hidden {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Update header height for iOS */
|
/* Update header height for iOS */
|
||||||
:root.device-mobile:not(.desktop-runtime) .header-safe-area {
|
:root.device-mobile:not(.desktop-runtime) .header-safe-area,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) .header-safe-area,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||||
--header-height: 4.5rem;
|
--header-height: 4.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.device-mobile:not(.desktop-runtime) .mobile-sidebar-top {
|
:root.device-mobile:not(.desktop-runtime) .mobile-sidebar-top,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) .mobile-sidebar-top,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .mobile-sidebar-top {
|
||||||
--header-height: 4.5rem;
|
--header-height: 4.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +348,9 @@
|
|||||||
|
|
||||||
/* Fallback for non-iOS standalone mode */
|
/* Fallback for non-iOS standalone mode */
|
||||||
@supports not (-webkit-touch-callout: none) {
|
@supports not (-webkit-touch-callout: none) {
|
||||||
:root.device-mobile:not(.desktop-runtime) {
|
:root.device-mobile:not(.desktop-runtime),
|
||||||
|
:root.device-tablet:not(.desktop-runtime),
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) {
|
||||||
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
||||||
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
||||||
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
||||||
@@ -334,18 +358,24 @@
|
|||||||
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.device-mobile:not(.desktop-runtime) .header-safe-area {
|
:root.device-mobile:not(.desktop-runtime) .header-safe-area,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) .header-safe-area,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||||
padding-top: var(--oc-safe-area-top);
|
padding-top: var(--oc-safe-area-top);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.device-mobile:not(.desktop-runtime) .main-content-safe-area {
|
:root.device-mobile:not(.desktop-runtime) .main-content-safe-area,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) .main-content-safe-area,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .main-content-safe-area {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
padding-left: var(--oc-safe-area-left);
|
padding-left: var(--oc-safe-area-left);
|
||||||
padding-right: var(--oc-safe-area-right);
|
padding-right: var(--oc-safe-area-right);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.device-mobile:not(.desktop-runtime) .bottom-safe-area {
|
:root.device-mobile:not(.desktop-runtime) .bottom-safe-area,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) .bottom-safe-area,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .bottom-safe-area {
|
||||||
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,7 +437,9 @@
|
|||||||
|
|
||||||
@media (display-mode: standalone) {
|
@media (display-mode: standalone) {
|
||||||
/* Home indicator overlay */
|
/* Home indicator overlay */
|
||||||
:root.device-mobile:not(.desktop-runtime) body::after {
|
:root.device-mobile:not(.desktop-runtime) body::after,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) body::after,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) body::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -428,7 +460,9 @@
|
|||||||
|
|
||||||
/* Fallback for browsers without rgba(from ...) support */
|
/* Fallback for browsers without rgba(from ...) support */
|
||||||
@supports not (color: rgba(from white r g b / 0.5)) {
|
@supports not (color: rgba(from white r g b / 0.5)) {
|
||||||
:root.device-mobile:not(.desktop-runtime) body::after {
|
:root.device-mobile:not(.desktop-runtime) body::after,
|
||||||
|
:root.device-tablet:not(.desktop-runtime) body::after,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) body::after {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to top,
|
to top,
|
||||||
var(--background) 0%,
|
var(--background) 0%,
|
||||||
|
|||||||
Reference in New Issue
Block a user