diff --git a/packages/app/src/components/headers/compact-header.tsx b/packages/app/src/components/headers/compact-header.tsx
index 69f73cc438..c87c6df956 100644
--- a/packages/app/src/components/headers/compact-header.tsx
+++ b/packages/app/src/components/headers/compact-header.tsx
@@ -1,6 +1,6 @@
-import { useCallback, useMemo, useState, type ReactNode } from "react";
+import { useCallback, useMemo, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
-import { Text, View, type LayoutChangeEvent } from "react-native";
+import { Text, View } from "react-native";
import { router } from "expo-router";
import { ArrowLeft } from "lucide-react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -12,7 +12,6 @@ import {
iconButtonChromeGlyphSize,
mutedIconColorMapping,
} from "@/components/ui/icon-button-chrome";
-import { TOUCH_TARGET } from "@/components/ui/control-sizing";
import { HEADER_INNER_HEIGHT_MOBILE, HEADER_TOP_PADDING_MOBILE } from "@/constants/layout";
import { SPACING } from "@/styles/theme";
import { useTopBandRow } from "@/window-layout";
@@ -22,9 +21,6 @@ const ThemedArrowLeft = withUnistyles(ArrowLeft);
const NO_SHORTCUT: ShortcutKey[] = [];
const ROW_PADDING = SPACING[3];
-const ROW_GAP = SPACING[2];
-// Below this the title is unreadable; the row starts below the system's occlusions instead.
-const MIN_TITLE_WIDTH = 96;
interface CompactHeaderProps {
/** The leading island: the sidebar menu, or a back button. */
@@ -58,21 +54,12 @@ export function CompactHeader({
actions,
}: CompactHeaderProps) {
const insets = useSafeAreaInsets();
- const [actionsWidth, setActionsWidth] = useState(0);
- const measureActions = useCallback(
- (event: LayoutChangeEvent) => setActionsWidth(event.nativeEvent.layout.width),
- [],
- );
// The trailing side gives way to the occlusion, so it needs no padding of its own.
const topBand = useTopBandRow({
offset: insets.top + HEADER_TOP_PADDING_MOBILE,
height: HEADER_INNER_HEIGHT_MOBILE,
- minimumWidth:
- ROW_PADDING +
- TOUCH_TARGET +
- ROW_GAP +
- MIN_TITLE_WIDTH +
- (actions ? ROW_GAP + actionsWidth : 0),
+ // The header stays in the band in every sidebar state; the title gives up width instead.
+ minimumWidth: 0,
});
const rootStyle = useMemo(
() => [
@@ -102,11 +89,7 @@ export function CompactHeader({
subtitle={subtitle}
/>
- {actions ? (
-
- {actions}
-
- ) : null}
+ {actions ? {actions} : null}
@@ -174,7 +157,7 @@ const styles = StyleSheet.create((theme) => ({
height: HEADER_INNER_HEIGHT_MOBILE,
flexDirection: "row",
alignItems: "center",
- gap: ROW_GAP,
+ gap: theme.spacing[2],
},
titleGroup: {
flex: 1,
diff --git a/packages/app/src/components/headers/screen-header.tsx b/packages/app/src/components/headers/screen-header.tsx
index 2af603ade4..feabfe1ecb 100644
--- a/packages/app/src/components/headers/screen-header.tsx
+++ b/packages/app/src/components/headers/screen-header.tsx
@@ -1,5 +1,5 @@
import { ComposerDockBackground } from "@/composer/dock";
-import { useCallback, useMemo, useState, type ReactNode } from "react";
+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";
@@ -12,12 +12,8 @@ import {
} from "@/constants/layout";
import { WindowChromeSafeArea } from "@/utils/desktop-window";
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
-import { TOUCH_TARGET } from "@/components/ui/control-sizing";
import { useTopBandRow } from "@/window-layout";
-// Leading content that must stay readable in the top band: a navigation button and a title.
-const MIN_LEADING_WIDTH = TOUCH_TARGET + 8 + 96;
-
interface ScreenHeaderProps {
left?: ReactNode;
right?: ReactNode;
@@ -46,15 +42,11 @@ export function ScreenHeader({
const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
const baseHorizontalPadding = isMobile ? theme.spacing[2] : theme.spacing[3];
- const [rightWidth, setRightWidth] = useState(0);
- const measureRight = useCallback(
- (event: LayoutChangeEvent) => setRightWidth(event.nativeEvent.layout.width),
- [],
- );
const topBand = useTopBandRow({
offset: insets.top + topPadding,
height: isMobile ? HEADER_INNER_HEIGHT_MOBILE : HEADER_INNER_HEIGHT,
- minimumWidth: baseHorizontalPadding + MIN_LEADING_WIDTH + rightWidth,
+ // The header stays in the band in every sidebar state; the title gives up width instead.
+ minimumWidth: 0,
});
const innerStyle = useMemo(
() => [styles.inner, { paddingTop: insets.top + topPadding + topBand.top }],
@@ -85,9 +77,7 @@ export function ScreenHeader({
>
{left}
-
- {right}
-
+ {right}
diff --git a/packages/app/src/window-layout/model.test.ts b/packages/app/src/window-layout/model.test.ts
index 275c38e816..10065b8bf9 100644
--- a/packages/app/src/window-layout/model.test.ts
+++ b/packages/app/src/window-layout/model.test.ts
@@ -129,9 +129,9 @@ describe("resolveTopBand", () => {
describe("resolveTopBandClearance", () => {
// The full workspace header with the sidebar closed spans the window.
const fullHeader = { x: 0, y: duo.topBar.y, width: 669, height: 44 };
- // The compact header beside a 320pt sidebar: 8pt top padding, 56pt row.
- const compactHeader = { x: 320, y: duo.topBar.y + 8, width: 349, height: 56 };
- const compactMinimum = 12 + 44 + 8 + 96 + 8 + 132;
+ // A row beside a 320pt sidebar whose controls need more width than the status region leaves.
+ const narrowRow = { x: 320, y: duo.topBar.y + 8, width: 349, height: 56 };
+ const narrowMinimum = 300;
it("trims the trailing side to the status region", () => {
expect(
@@ -156,9 +156,9 @@ describe("resolveTopBandClearance", () => {
it("starts below the status region when the controls no longer fit beside it", () => {
expect(
resolveTopBandClearance({
- row: compactHeader,
+ row: narrowRow,
occlusions: [duo.statusRegion],
- minimumWidth: compactMinimum,
+ minimumWidth: narrowMinimum,
}),
).toEqual({ trailing: 0, top: 48 });
});
@@ -166,9 +166,9 @@ describe("resolveTopBandClearance", () => {
it("starts below the lowest of several overlapping occlusions", () => {
expect(
resolveTopBandClearance({
- row: compactHeader,
+ row: narrowRow,
occlusions: [duo.statusRegion, { x: 600, y: 0, width: 69, height: 96 }],
- minimumWidth: compactMinimum,
+ minimumWidth: narrowMinimum,
}),
).toEqual({ trailing: 0, top: 62 });
});