diff --git a/packages/app/src/components/headers/compact-header.tsx b/packages/app/src/components/headers/compact-header.tsx index c87c6df956..e150a4a85b 100644 --- a/packages/app/src/components/headers/compact-header.tsx +++ b/packages/app/src/components/headers/compact-header.tsx @@ -12,15 +12,21 @@ import { iconButtonChromeGlyphSize, mutedIconColorMapping, } from "@/components/ui/icon-button-chrome"; -import { HEADER_INNER_HEIGHT_MOBILE, HEADER_TOP_PADDING_MOBILE } from "@/constants/layout"; -import { SPACING } from "@/styles/theme"; -import { useTopBandRow } from "@/window-layout"; +import { + HEADER_INNER_HEIGHT, + HEADER_INNER_HEIGHT_MOBILE, + HEADER_TOP_PADDING_MOBILE, +} from "@/constants/layout"; +import { WindowChromeSafeArea } from "@/utils/desktop-window"; import type { ShortcutKey } from "@/utils/format-shortcut"; +import { useTopBand } from "@/window-layout"; const ThemedArrowLeft = withUnistyles(ArrowLeft); +const HeaderSafeArea = withUnistyles(WindowChromeSafeArea, (theme) => ({ + horizontalPadding: theme.spacing[3], +})); const NO_SHORTCUT: ShortcutKey[] = []; -const ROW_PADDING = SPACING[3]; interface CompactHeaderProps { /** The leading island: the sidebar menu, or a back button. */ @@ -42,7 +48,8 @@ interface CompactHeaderProps { * left-aligned between them. Neither the header nor its islands draw a background, so it reads the * same in flow and floating over content (see `FloatingHeaderLayout`, whose fade gives the icons * contrast). Its height matches the old flat header, so anything - * offset by the header height still lines up. + * offset by the header height still lines up. In a window's top band it takes the top bar's + * place, like the full header, and keeps clear of the status region as it does of window controls. */ export function CompactHeader({ navigation, @@ -54,30 +61,20 @@ export function CompactHeader({ actions, }: CompactHeaderProps) { const insets = useSafeAreaInsets(); - // 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, - // The header stays in the band in every sidebar state; the title gives up width instead. - minimumWidth: 0, - }); + const inTopBand = useTopBand() !== null; const rootStyle = useMemo( - () => [ - styles.root, - { - paddingTop: insets.top + HEADER_TOP_PADDING_MOBILE + topBand.top, - paddingRight: Math.max(ROW_PADDING, topBand.trailing), - marginLeft: insets.left, - marginRight: insets.right, - }, - ], - [insets.top, insets.left, insets.right, topBand.top, topBand.trailing], + () => ({ + paddingTop: insets.top + (inTopBand ? 0 : HEADER_TOP_PADDING_MOBILE), + marginLeft: insets.left, + marginRight: insets.right, + }), + [inTopBand, insets.top, insets.left, insets.right], ); return ( - - + + {navigation === "menu" ? : } @@ -91,7 +88,7 @@ export function CompactHeader({ {actions ? {actions} : null} - + ); } @@ -150,15 +147,18 @@ function BackButton({ onBack }: { onBack?: () => void }) { } const styles = StyleSheet.create((theme) => ({ - root: { - paddingLeft: ROW_PADDING, - }, row: { height: HEADER_INNER_HEIGHT_MOBILE, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, + topBandRow: { + height: HEADER_INNER_HEIGHT, + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + }, titleGroup: { flex: 1, minWidth: 0, diff --git a/packages/app/src/components/headers/screen-header.tsx b/packages/app/src/components/headers/screen-header.tsx index feabfe1ecb..6492658389 100644 --- a/packages/app/src/components/headers/screen-header.tsx +++ b/packages/app/src/components/headers/screen-header.tsx @@ -12,7 +12,6 @@ import { } from "@/constants/layout"; import { WindowChromeSafeArea } from "@/utils/desktop-window"; import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region"; -import { useTopBandRow } from "@/window-layout"; interface ScreenHeaderProps { left?: ReactNode; @@ -42,33 +41,17 @@ export function ScreenHeader({ const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0; const baseHorizontalPadding = isMobile ? theme.spacing[2] : theme.spacing[3]; - const topBand = useTopBandRow({ - offset: insets.top + topPadding, - height: isMobile ? HEADER_INNER_HEIGHT_MOBILE : HEADER_INNER_HEIGHT, - // 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 }], - [insets.top, topPadding, topBand.top], + () => [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]); - // The occlusion replaces the row's trailing padding rather than adding to it. - const rightCombinedStyle = useMemo( - () => [ - styles.right, - rightStyle, - topBand.trailing > baseHorizontalPadding && { - marginRight: topBand.trailing - baseHorizontalPadding, - }, - ], - [rightStyle, topBand.trailing, baseHorizontalPadding], - ); + const rightCombinedStyle = useMemo(() => [styles.right, rightStyle], [rightStyle]); return ( - + ({ paddingTop: navBand.top }), [navBand.top]); const closeButton = useMemo( () => ( ), - [labels.closeSidebar, closeSidebar, insets.right, navBand.trailing], + [labels.closeSidebar, closeSidebar, insets.right], ); const mobileSidebarInsetStyle = useMemo( @@ -559,13 +550,11 @@ function MobileSidebar({ ); const navRows = ( - - - + ); return ( @@ -759,9 +748,11 @@ function DesktopSidebar({ ], [insetsTop, insets.bottom, insets.left, overlay, divisionLayout], ); + // Below the window controls' row, or in the top band's place, the first row starts flush. + const inTopBand = useTopBand() !== null; const sidebarHeaderGroupStyle = useMemo( - () => [styles.sidebarHeaderGroup, ownsTopLeft && styles.sidebarHeaderGroupBelowChrome], - [ownsTopLeft], + () => [styles.sidebarHeaderGroup, (ownsTopLeft || inTopBand) && styles.sidebarHeaderGroupFlush], + [ownsTopLeft, inTopBand], ); return ( ({ borderBottomWidth: 1, borderBottomColor: theme.colors.border, }, - sidebarHeaderGroupBelowChrome: { + sidebarHeaderGroupFlush: { paddingTop: 0, }, // The nav block caps at half the sidebar; a long item list scrolls inside the @@ -905,8 +896,8 @@ const styles = StyleSheet.create((theme) => ({ flex: 1, minHeight: 0, }, - mobileCloseButton: (rightInset: number, topBandTrailing: number) => ({ - marginRight: Math.max(theme.spacing[2] + rightInset, topBandTrailing), + mobileCloseButton: (rightInset: number) => ({ + marginRight: theme.spacing[2] + rightInset, }), desktopSidebarBorder: (hasDivision: boolean) => ({ borderRightWidth: hasDivision ? 0 : 1, diff --git a/packages/app/src/components/toast-host.tsx b/packages/app/src/components/toast-host.tsx index d5d3eff1b8..62a15bad04 100644 --- a/packages/app/src/components/toast-host.tsx +++ b/packages/app/src/components/toast-host.tsx @@ -19,8 +19,6 @@ import type { Theme } from "@/styles/theme"; import { useTranslation } from "react-i18next"; import { useIsCompactFormFactor } from "@/constants/layout"; import { isWeb } from "@/constants/platform"; -import { TOUCH_TARGET } from "@/components/ui/control-sizing"; -import { useTopBandRow } from "@/window-layout"; import { AlertTriangle, CheckCircle2, Info } from "lucide-react-native"; import { getOverlayRoot, OVERLAY_Z } from "@/lib/overlay-root"; import { @@ -202,30 +200,13 @@ export function ToastViewport({ const topOffset = placement === "app-shell" ? insets.top + headerTopPadding + headerHeight : panelClearance; - // A centred toast cannot trim one side, so it always moves below an occlusion it meets. - const topBand = useTopBandRow({ - offset: 0, - height: TOUCH_TARGET, - minimumWidth: Number.POSITIVE_INFINITY, - }); - const widthBoundaryStyle = useMemo( - () => [styles.widthBoundary, { paddingTop: topBand.top }], - [topBand.top], - ); - const content = ( - + {toast ? ( [styles.enterButton, { right: -6 + railOffset }], [railOffset]); const exitStyle = useMemo(() => [styles.iconButton, { marginRight: railOffset }], [railOffset]); - const exitSize = TOOLBAR_CONTROL_SIZES[controlSizing]; - const topBand = useTopBandRow({ offset: 0, height: exitSize, minimumWidth: exitSize }); - const exitRowStyle = useMemo( - () => [styles.exitRow, { paddingTop: topBand.top, paddingRight: topBand.trailing }], - [topBand.top, topBand.trailing], - ); return ( {fullscreen ? ( - +