* fix(app): preserve desktop layout in half-screen windows Treat native window controls as physical corner obstructions instead of deriving clearance from compact layout state. Keep 751px windows in desktop mode while preserving usable content when persisted sidebars are wide. * fix(app): retain sidebar width across window resizes * fix(app): yield app sidebar to narrow settings * test(app): wait for mobile sidebar motion to settle * fix(app): yield navigation to narrow workspace explorer * fix(app): mark sidebar sizing helpers as worklets * fix(app): hide app navigation when space is constrained
104 lines
3.0 KiB
TypeScript
104 lines
3.0 KiB
TypeScript
import { useMemo, type ReactNode } from "react";
|
|
import type { LayoutChangeEvent } from "react-native";
|
|
import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
import {
|
|
HEADER_INNER_HEIGHT,
|
|
HEADER_INNER_HEIGHT_MOBILE,
|
|
HEADER_TOP_PADDING_MOBILE,
|
|
useIsCompactFormFactor,
|
|
} from "@/constants/layout";
|
|
import { WindowChromeSafeArea } from "@/utils/desktop-window";
|
|
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
|
|
|
interface ScreenHeaderProps {
|
|
left?: ReactNode;
|
|
right?: ReactNode;
|
|
leftStyle?: StyleProp<ViewStyle>;
|
|
rightStyle?: StyleProp<ViewStyle>;
|
|
borderless?: boolean;
|
|
onRowLayout?: (event: LayoutChangeEvent) => void;
|
|
}
|
|
|
|
/**
|
|
* Shared frame for the home/back headers so we only maintain padding, border,
|
|
* and safe-area logic in one place.
|
|
*/
|
|
export function ScreenHeader({
|
|
left,
|
|
right,
|
|
leftStyle,
|
|
rightStyle,
|
|
borderless,
|
|
onRowLayout,
|
|
}: ScreenHeaderProps) {
|
|
const { theme } = useUnistyles();
|
|
const insets = useSafeAreaInsets();
|
|
const isMobile = useIsCompactFormFactor();
|
|
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets
|
|
const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
|
|
const baseHorizontalPadding = theme.spacing[2];
|
|
|
|
const innerStyle = useMemo(
|
|
() => [styles.inner, { paddingTop: insets.top + topPadding }],
|
|
[insets.top, topPadding],
|
|
);
|
|
const rowStyle = useMemo(() => [styles.row, borderless && styles.borderless], [borderless]);
|
|
const leftCombinedStyle = useMemo(() => [styles.left, leftStyle], [leftStyle]);
|
|
const rightCombinedStyle = useMemo(() => [styles.right, rightStyle], [rightStyle]);
|
|
|
|
return (
|
|
<View style={styles.header}>
|
|
<View style={innerStyle}>
|
|
<WindowChromeSafeArea
|
|
placement="inline"
|
|
horizontalPadding={baseHorizontalPadding}
|
|
onLayout={onRowLayout}
|
|
style={rowStyle}
|
|
>
|
|
<TitlebarDragRegion />
|
|
<View style={leftCombinedStyle}>{left}</View>
|
|
<View style={rightCombinedStyle}>{right}</View>
|
|
</WindowChromeSafeArea>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
header: {
|
|
backgroundColor: theme.colors.surface0,
|
|
},
|
|
inner: {},
|
|
row: {
|
|
position: "relative",
|
|
height: {
|
|
xs: HEADER_INNER_HEIGHT_MOBILE,
|
|
md: HEADER_INNER_HEIGHT,
|
|
},
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
borderBottomWidth: theme.borderWidth[1],
|
|
borderBottomColor: theme.colors.border,
|
|
userSelect: "none",
|
|
},
|
|
left: {
|
|
flex: 1,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
minWidth: 0,
|
|
},
|
|
right: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
flexShrink: 0,
|
|
},
|
|
borderless: {
|
|
borderBottomColor: "transparent",
|
|
},
|
|
}));
|