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 }); });