fix: keep screens clear of the camera cutout and side navigation bar in landscape

This commit is contained in:
Linus Rath
2026-10-15 03:29:59 +02:00
parent d7cee7ca78
commit e19229a5c6
10 changed files with 20 additions and 20 deletions
+5 -1
View File
@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Animated, Easing, Pressable, StyleSheet, Text, View } from 'react-native'; import { Animated, Easing, Pressable, StyleSheet, Text, View } from 'react-native';
import { CalendarPlus, CircleCheck, Plus } from 'lucide-react-native'; import { CalendarPlus, CircleCheck, Plus } from 'lucide-react-native';
import { componentSizes, radius, spacing, typography, type ThemePalette } from '../../theme/tokens'; import { componentSizes, radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
@@ -39,6 +40,8 @@ export function CalendarFab({ onNewEvent, onNewTask }: CalendarFabProps) {
action(); action();
}; };
const insets = useSafeAreaInsets();
const onMainPress = () => { const onMainPress = () => {
if (!onNewTask) { if (!onNewTask) {
onNewEvent(); onNewEvent();
@@ -63,7 +66,8 @@ export function CalendarFab({ onNewEvent, onNewTask }: CalendarFabProps) {
/> />
</Animated.View> </Animated.View>
)} )}
<View style={styles.anchor} pointerEvents="box-none"> {/* Clear of a side inset (camera cutout, navigation bar) in landscape. */}
<View style={[styles.anchor, { right: spacing.lg + insets.right }]} pointerEvents="box-none">
{open && onNewTask && ( {open && onNewTask && (
<Animated.View style={[styles.option, { opacity: progress, transform: [{ translateY: lift }] }]}> <Animated.View style={[styles.option, { opacity: progress, transform: [{ translateY: lift }] }]}>
<Text style={styles.optionLabel}>{t('calendar.fab.task', 'Task')}</Text> <Text style={styles.optionLabel}>{t('calendar.fab.task', 'Task')}</Text>
+1 -1
View File
@@ -988,7 +988,7 @@ export default function CalendarScreen() {
const titleDate = (monthPanelOpen && panelMonth) || visibleDate || focus.date; const titleDate = (monthPanelOpen && panelMonth) || visibleDate || focus.date;
return ( return (
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top', 'left', 'right']}>
<View style={styles.header}> <View style={styles.header}>
<Pressable <Pressable
onPress={() => setSidebarVisible(true)} onPress={() => setSidebarVisible(true)}
+1 -1
View File
@@ -361,7 +361,7 @@ export default function ContactDetailScreen() {
].filter(Boolean) as MoreItem[]; ].filter(Boolean) as MoreItem[];
return ( return (
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top', 'left', 'right']}>
<View style={[styles.header, inPane && styles.headerInPane]}> <View style={[styles.header, inPane && styles.headerInPane]}>
{/* In a split view's pane the list is right next to it: no back. */} {/* In a split view's pane the list is right next to it: no back. */}
{!inPane && ( {!inPane && (
+2 -2
View File
@@ -1070,7 +1070,7 @@ export default function ContactFormScreen() {
if (asGroup) { if (asGroup) {
// Groups are a name plus a member list - none of the person fields apply. // Groups are a name plus a member list - none of the person fields apply.
return ( return (
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top', 'left', 'right']}>
{header} {header}
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined} behavior={Platform.OS === 'ios' ? 'padding' : undefined}
@@ -1131,7 +1131,7 @@ export default function ContactFormScreen() {
} }
return ( return (
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top', 'left', 'right']}>
{header} {header}
<KeyboardAvoidingView <KeyboardAvoidingView
+1 -1
View File
@@ -381,7 +381,7 @@ export default function ContactsScreen({ onOpenContact, activeContactId = null }
}, [addressBooks, groups, selectedCategory, t]); }, [addressBooks, groups, selectedCategory, t]);
return ( return (
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top', 'left', 'right']}>
<ContactsSidebarDrawer visible={drawerOpen} onClose={() => setDrawerOpen(false)} /> <ContactsSidebarDrawer visible={drawerOpen} onClose={() => setDrawerOpen(false)} />
<View style={styles.header}> <View style={styles.header}>
+1 -1
View File
@@ -1151,7 +1151,7 @@ export default function EmailListScreen({
); );
return ( return (
<View style={[styles.container, { paddingTop: insets.top }]}> <View style={[styles.container, { paddingTop: insets.top, paddingLeft: insets.left, paddingRight: insets.right }]}>
{/* Header */} {/* Header */}
{selectionMode ? ( {selectionMode ? (
<View style={styles.header}> <View style={styles.header}>
+1 -1
View File
@@ -65,7 +65,7 @@ export default function EmailSourceScreen({ route, navigation }: Props) {
}; };
return ( return (
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top', 'left', 'right']}>
<View style={styles.header}> <View style={styles.header}>
<Pressable <Pressable
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
+2 -2
View File
@@ -689,7 +689,7 @@ function EmailViewer({ route, navigation, embedded = false, onActiveChange }: Pr
: false; : false;
return ( return (
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top', 'left', 'right']}>
{/* Toolbar */} {/* Toolbar */}
<View style={[styles.toolbar, embedded && styles.toolbarEmbedded]}> <View style={[styles.toolbar, embedded && styles.toolbarEmbedded]}>
{!embedded && ( {!embedded && (
@@ -827,7 +827,7 @@ function EmailViewer({ route, navigation, embedded = false, onActiveChange }: Pr
{/* Bottom action bar (hidden while keyboard is up so quick reply stays visible) */} {/* Bottom action bar (hidden while keyboard is up so quick reply stays visible) */}
{kbObstruction === 0 && ( {kbObstruction === 0 && (
<View style={[styles.bottomBar, { paddingBottom: Math.max(insets.bottom, 4) }]}> <View style={[styles.bottomBar, { paddingBottom: Math.max(insets.bottom, 4), paddingLeft: insets.left, paddingRight: insets.right }]}>
<BottomBarButton <BottomBarButton
icon={<ChevronLeft size={20} color={c.textMuted} />} icon={<ChevronLeft size={20} color={c.textMuted} />}
label={t('email_viewer.previous', 'Prev')} label={t('email_viewer.previous', 'Prev')}
+4 -8
View File
@@ -168,14 +168,10 @@ export default function FilesScreen() {
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
const t = useLocaleStore((s) => s.t); const t = useLocaleStore((s) => s.t);
const formatModified = useFormatModified(); const formatModified = useFormatModified();
// Clear of the status bar and, in landscape, the side insets; the header // Clear of the status bar (the header used a fixed 60 px top padding); the
// used a fixed 60 px top padding instead. // whole screen keeps clear of the side insets in landscape.
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const headerInsets = { const headerInsets = { paddingTop: insets.top + spacing.sm, paddingHorizontal: spacing.md };
paddingTop: insets.top + spacing.sm,
paddingLeft: spacing.md + insets.left,
paddingRight: spacing.md + insets.right,
};
// Folder navigation stack. Hierarchy is real parentId nesting (#379) — each // Folder navigation stack. Hierarchy is real parentId nesting (#379) — each
// entry is the folder's node id (namespaced "accountId:nodeId" inside a // entry is the folder's node id (namespaced "accountId:nodeId" inside a
@@ -1035,7 +1031,7 @@ export default function FilesScreen() {
// On a wide window an open file shows beside the list instead of covering it. // On a wide window an open file shows beside the list instead of covering it.
const previewDocked = isSplit && previewTarget != null; const previewDocked = isSplit && previewTarget != null;
return ( return (
<View style={[styles.container, previewDocked && styles.splitRow]}> <View style={[styles.container, previewDocked && styles.splitRow, { paddingLeft: insets.left, paddingRight: insets.right }]}>
<View style={styles.listColumn}> <View style={styles.listColumn}>
{renderHeader()} {renderHeader()}
{body} {body}
+2 -2
View File
@@ -62,7 +62,7 @@ export default function GroupDetailScreen() {
if (!group) { if (!group) {
return ( return (
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top', 'left', 'right']}>
<View style={styles.header}> <View style={styles.header}>
<Pressable <Pressable
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
@@ -133,7 +133,7 @@ export default function GroupDetailScreen() {
}; };
return ( return (
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top', 'left', 'right']}>
<View style={[styles.header, inPane && styles.headerInPane]}> <View style={[styles.header, inPane && styles.headerInPane]}>
{/* In a split view's pane the list is right next to it: no back. */} {/* In a split view's pane the list is right next to it: no back. */}
{!inPane && ( {!inPane && (