Add translucent headers (#771)

This commit is contained in:
Leendert de Borst
2025-05-06 14:22:22 +02:00
parent e714d8563c
commit 82423fffcb
13 changed files with 148 additions and 85 deletions

View File

@@ -1,14 +1,12 @@
import { Stack } from 'expo-router';
import { Platform } from 'react-native';
import { useColors } from '@/hooks/useColorScheme';
import { defaultHeaderOptions } from '@/components/themed/ThemedHeader';
/**
* Credentials layout.
*/
export default function CredentialsLayout() : React.ReactNode {
const colors = useColors();
export default function CredentialsLayout(): React.ReactNode {
return (
<Stack>
<Stack.Screen
@@ -23,10 +21,8 @@ export default function CredentialsLayout() : React.ReactNode {
title: 'Add Credential',
presentation: Platform.OS === 'ios' ? 'modal' : 'card',
headerShown: true,
headerStyle: {
backgroundColor: colors.headerBackground,
},
gestureEnabled: true,
...defaultHeaderOptions,
}}
/>
<Stack.Screen
@@ -35,9 +31,7 @@ export default function CredentialsLayout() : React.ReactNode {
title: 'Credential Created',
presentation: Platform.OS === 'ios' ? 'modal' : 'card',
headerShown: true,
headerStyle: {
backgroundColor: colors.headerBackground,
},
...defaultHeaderOptions,
}}
/>
<Stack.Screen
@@ -45,9 +39,7 @@ export default function CredentialsLayout() : React.ReactNode {
options={{
title: 'Credential Details',
headerShown: true,
headerStyle: {
backgroundColor: colors.headerBackground,
},
...defaultHeaderOptions,
}}
/>
<Stack.Screen
@@ -55,9 +47,6 @@ export default function CredentialsLayout() : React.ReactNode {
options={{
title: 'Email Preview',
headerShown: true,
headerStyle: {
backgroundColor: colors.headerBackground,
},
}}
/>
</Stack>

View File

@@ -1,14 +1,14 @@
import { StyleSheet, View, TouchableOpacity, Alert, Keyboard, ScrollView } from 'react-native';
import { StyleSheet, View, TouchableOpacity, Alert, Keyboard, KeyboardAvoidingView, Platform } from 'react-native';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import Toast from 'react-native-toast-message';
import { Resolver, useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { ThemedText } from '@/components/themed/ThemedText';
import { ThemedView } from '@/components/themed/ThemedView';
import { ThemedSafeAreaView } from '@/components/themed/ThemedSafeAreaView';
import { useColors } from '@/hooks/useColorScheme';
import { useDb } from '@/context/DbContext';
import { useWebApi } from '@/context/WebApiContext';
@@ -404,10 +404,13 @@ export default function AddEditCredentialScreen() : React.ReactNode {
},
content: {
flex: 1,
marginTop: 36,
padding: 16,
paddingTop: 0,
},
contentContainer: {
paddingBottom: 40,
paddingTop: Platform.OS === 'ios' ? 76 : 56,
},
deleteButton: {
alignItems: 'center',
backgroundColor: colors.errorBackground,
@@ -441,7 +444,6 @@ export default function AddEditCredentialScreen() : React.ReactNode {
},
headerLeftButtonText: {
color: colors.primary,
fontSize: 20,
},
headerRightButton: {
padding: 10,
@@ -518,9 +520,17 @@ export default function AddEditCredentialScreen() : React.ReactNode {
{(isLoading) && (
<LoadingOverlay status={syncStatus} />
)}
<ThemedSafeAreaView style={styles.container}>
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 140 : 0} // adjust offset as needed
>
<ThemedView style={styles.content}>
<ScrollView
<KeyboardAwareScrollView
enableOnAndroid={true}
contentContainerStyle={styles.contentContainer}
keyboardShouldPersistTaps="handled"
extraScrollHeight={0}
>
{!isEditMode && (
<View style={styles.modeSelector}>
@@ -658,10 +668,10 @@ export default function AddEditCredentialScreen() : React.ReactNode {
)}
</>
)}
</ScrollView>
</KeyboardAwareScrollView>
</ThemedView>
<AliasVaultToast />
</ThemedSafeAreaView>
</KeyboardAvoidingView>
</>
);
}

View File

@@ -14,6 +14,7 @@ import EncryptionUtility from '@/utils/EncryptionUtility';
import { useColors } from '@/hooks/useColorScheme';
import { IconSymbol } from '@/components/ui/IconSymbol';
import emitter from '@/utils/EventEmitter';
import { ThemedView } from '@/components/themed/ThemedView';
/**
* Email details screen.
@@ -297,12 +298,6 @@ export default function EmailDetailsScreen() : React.ReactNode {
flexDirection: 'row',
padding: 2,
},
viewDark: {
backgroundColor: colors.background,
},
viewLight: {
backgroundColor: colors.background,
},
webView: {
flex: 1,
},
@@ -323,7 +318,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
<Ionicons
name={isHtmlView ? 'text-outline' : 'document-outline'}
size={22}
color="#FFA500"
color={colors.primary}
/>
</TouchableOpacity>
<TouchableOpacity
@@ -335,30 +330,30 @@ export default function EmailDetailsScreen() : React.ReactNode {
</View>
),
});
}, [isHtmlView, navigation, handleDelete, styles.headerRightButton, styles.headerRightContainer]);
}, [isHtmlView, navigation, handleDelete, colors.primary, styles.headerRightButton, styles.headerRightContainer]);
if (isLoading) {
return (
<View style={[styles.centerContainer, isDarkMode ? styles.viewDark : styles.viewLight]}>
<ThemedView style={styles.centerContainer}>
<Stack.Screen options={{ title: 'Email Details' }} />
<ActivityIndicator size="large" />
</View>
</ThemedView>
);
}
if (error) {
return (
<View style={[styles.centerContainer, isDarkMode ? styles.viewDark : styles.viewLight]}>
<ThemedView style={styles.centerContainer}>
<ThemedText style={styles.errorText}>Error: {error}</ThemedText>
</View>
</ThemedView>
);
}
if (!email) {
return (
<View style={[styles.centerContainer, isDarkMode ? styles.viewDark : styles.viewLight]}>
<ThemedView style={styles.centerContainer}>
<ThemedText style={styles.emptyText}>Email not found</ThemedText>
</View>
</ThemedView>
);
}
@@ -471,7 +466,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
}
return (
<View style={[styles.container, isDarkMode ? styles.viewDark : styles.viewLight]}>
<ThemedView style={styles.container}>
<Stack.Screen options={{ title: 'Email Details' }} />
{metadataView}
{emailView}
@@ -492,6 +487,6 @@ export default function EmailDetailsScreen() : React.ReactNode {
))}
</View>
)}
</View>
</ThemedView>
);
}

View File

@@ -1,13 +1,9 @@
import { Stack } from 'expo-router';
import { useColors } from '@/hooks/useColorScheme';
/**
* Emails layout.
*/
export default function EmailsLayout() : React.ReactNode {
const colors = useColors();
export default function EmailsLayout(): React.ReactNode {
return (
<Stack>
<Stack.Screen
@@ -21,9 +17,6 @@ export default function EmailsLayout() : React.ReactNode {
options={{
title: 'Email',
headerShown: true,
headerStyle: {
backgroundColor: colors.headerBackground,
},
}}
/>
</Stack>

View File

@@ -1,13 +1,11 @@
import { Stack } from 'expo-router';
import { useColors } from '@/hooks/useColorScheme';
import { defaultHeaderOptions } from '@/components/themed/ThemedHeader';
/**
* Settings layout.
*/
export default function SettingsLayout() : React.ReactNode {
const colors = useColors();
export default function SettingsLayout(): React.ReactNode {
return (
<Stack>
<Stack.Screen
@@ -21,9 +19,8 @@ export default function SettingsLayout() : React.ReactNode {
options={{
title: 'iOS Autofill',
headerBackTitle: 'Settings',
headerStyle: {
backgroundColor: colors.headerBackground,
},
headerShown: true,
...defaultHeaderOptions,
}}
/>
<Stack.Screen
@@ -31,9 +28,8 @@ export default function SettingsLayout() : React.ReactNode {
options={{
title: 'Vault Unlock Method',
headerBackTitle: 'Settings',
headerStyle: {
backgroundColor: colors.headerBackground,
},
headerShown: true,
...defaultHeaderOptions,
}}
/>
<Stack.Screen
@@ -41,9 +37,8 @@ export default function SettingsLayout() : React.ReactNode {
options={{
title: 'Auto-lock Settings',
headerBackTitle: 'Settings',
headerStyle: {
backgroundColor: colors.headerBackground,
},
headerShown: true,
...defaultHeaderOptions,
}}
/>
</Stack>

View File

@@ -4,10 +4,10 @@ import { useState, useEffect, useCallback } from 'react';
import Toast from 'react-native-toast-message';
import { ThemedText } from '@/components/themed/ThemedText';
import { ThemedSafeAreaView } from '@/components/themed/ThemedSafeAreaView';
import { useColors } from '@/hooks/useColorScheme';
import { AuthMethod, useAuth } from '@/context/AuthContext';
import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
import { ThemedView } from '@/components/themed/ThemedView';
/**
* Vault unlock settings screen.
@@ -166,7 +166,7 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
});
return (
<ThemedSafeAreaView style={styles.container}>
<ThemedView style={styles.container}>
<ThemedScrollView>
<View style={styles.header}>
<ThemedText style={styles.headerText}>
@@ -214,6 +214,6 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
</View>
</View>
</ThemedScrollView>
</ThemedSafeAreaView>
</ThemedView>
);
}

View File

@@ -53,6 +53,7 @@ function RootLayoutNav() : React.ReactNode {
<Stack screenOptions={{
headerShown: true,
animation: 'none',
headerTransparent: true,
headerStyle: {
backgroundColor: colors.accentBackground,
},

View File

@@ -0,0 +1,57 @@
import { Platform, StyleSheet, useColorScheme, View } from 'react-native';
import { BlurView } from 'expo-blur';
import { useColors } from '@/hooks/useColorScheme';
/**
* ThemedHeader component that provides consistent header styling across the app.
* This component is used as a headerBackground in Stack.Screen options.
* @returns {React.ReactNode} The themed header component
*/
export function ThemedHeader(): React.ReactNode {
const colorScheme = useColorScheme();
const colors = useColors();
const styles = StyleSheet.create({
header: {
flex: 1,
},
headerBorder: {
backgroundColor: colors.headerBorder,
bottom: 0,
height: StyleSheet.hairlineWidth,
left: 0,
position: 'absolute',
right: 0,
},
});
if (Platform.OS === 'ios') {
return (
<View style={styles.header}>
<BlurView
tint={colorScheme === 'dark' ? 'dark' : 'light'}
intensity={colorScheme === 'dark' ? 90 : 100}
style={[StyleSheet.absoluteFill, { backgroundColor: colors.headerBackground }]}
/>
<View style={[styles.headerBorder, { backgroundColor: colors.headerBorder }]} />
</View>
);
}
return null;
}
/**
* Default header options for Stack.Screen components.
* This provides consistent header styling across the app.
* @returns {Object} The default header options
*/
export const defaultHeaderOptions = {
headerTransparent: true,
/**
* Header background component that provides consistent styling.
* @returns {React.ReactNode} The themed header background component
*/
headerBackground: (): React.ReactNode => <ThemedHeader />,
};

View File

@@ -1,4 +1,4 @@
import { ScrollView, StyleProp, StyleSheet, ViewStyle } from 'react-native';
import { Platform, ScrollView, StyleProp, StyleSheet, ViewStyle } from 'react-native';
type ThemedScrollViewProps = {
style?: StyleProp<ViewStyle>;
@@ -23,10 +23,13 @@ export function ThemedScrollView({ style, lightColor, darkColor, ...otherProps }
);
}
const HEADER_HEIGHT = Platform.OS === 'ios' ? 96 : 56;
const styles = StyleSheet.create({
container: {
flex: 1,
marginBottom: 80,
paddingTop: HEADER_HEIGHT,
},
contentContainer: {
paddingBottom: 40,

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { StyleSheet, Platform, Animated, TouchableOpacity, useColorScheme } from 'react-native';
import { StyleSheet, Platform, Animated, TouchableOpacity, useColorScheme, View } from 'react-native';
import { Stack } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { BlurView } from 'expo-blur';
@@ -89,7 +89,7 @@ export function CollapsibleHeader({
flex: 1,
},
headerBorder: {
backgroundColor: colors.accentBorder,
backgroundColor: colors.headerBorder,
bottom: 0,
height: 1,
left: 0,
@@ -124,23 +124,20 @@ export function CollapsibleHeader({
]}
>
{Platform.OS === 'ios' ? (
colorScheme === 'dark' ? (
<Animated.View style={[StyleSheet.absoluteFill, { opacity: headerOpacity }]}>
<AnimatedBlurView
tint="dark"
intensity={80}
style={[StyleSheet.absoluteFill, { opacity: headerOpacity }]}
tint={colorScheme === 'dark' ? 'dark' : 'light'}
intensity={colorScheme === 'dark' ? 80 : 100}
style={[StyleSheet.absoluteFill, { backgroundColor: colors.headerBackground }]}
/>
) : (
<AnimatedBlurView
tint="light"
intensity={100}
style={[StyleSheet.absoluteFill, { opacity: headerOpacity }]}
/>
)
<View style={styles.headerBorder} />
</Animated.View>
) : (
<Animated.View
style={[StyleSheet.absoluteFill, { backgroundColor: headerBackground }]}
/>
>
<View style={styles.headerBorder} />
</Animated.View>
)}
<Animated.View
@@ -166,10 +163,7 @@ export function CollapsibleHeader({
))}
<Animated.View
style={[
styles.headerBorder,
{ opacity: headerOpacity },
]}
style={{ opacity: headerOpacity }}
/>
</Animated.View>
</>

View File

@@ -17,7 +17,8 @@ export const Colors = {
icon: '#687076',
tabIconDefault: '#687076',
tabIconSelected: '#f49541',
headerBackground: '#fff',
headerBackground: 'rgba(255, 255, 255, 0.7)',
headerBorder: '#eae9eb',
tabBarBackground: '#fff',
primary: '#f49541',
primarySurfaceText: '#ffffff',
@@ -40,7 +41,8 @@ export const Colors = {
icon: '#9BA1A6',
tabIconDefault: '#9BA1A6',
tabIconSelected: '#f49541',
headerBackground: '#202020',
headerBackground: 'rgba(0, 0, 0, 0.3)',
headerBorder: '#2f2e30',
tabBarBackground: '#202020',
primary: '#f49541',
primarySurfaceText: '#ffffff',

View File

@@ -42,6 +42,7 @@
"react-native-argon2": "^2.0.1",
"react-native-gesture-handler": "~2.20.2",
"react-native-get-random-values": "^1.11.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-quick-crypto": "^0.7.13",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
@@ -16268,6 +16269,15 @@
"react": "^16.6.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-native-iphone-x-helper": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz",
"integrity": "sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.42.0"
}
},
"node_modules/react-native-is-edge-to-edge": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz",
@@ -16278,6 +16288,19 @@
"react-native": "*"
}
},
"node_modules/react-native-keyboard-aware-scroll-view": {
"version": "0.9.5",
"resolved": "https://registry.npmjs.org/react-native-keyboard-aware-scroll-view/-/react-native-keyboard-aware-scroll-view-0.9.5.tgz",
"integrity": "sha512-XwfRn+T/qBH9WjTWIBiJD2hPWg0yJvtaEw6RtPCa5/PYHabzBaWxYBOl0usXN/368BL1XktnZPh8C2lmTpOREA==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.6.2",
"react-native-iphone-x-helper": "^1.0.3"
},
"peerDependencies": {
"react-native": ">=0.48.4"
}
},
"node_modules/react-native-quick-base64": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-native-quick-base64/-/react-native-quick-base64-2.1.2.tgz",

View File

@@ -63,6 +63,7 @@
"react-native-argon2": "^2.0.1",
"react-native-gesture-handler": "~2.20.2",
"react-native-get-random-values": "^1.11.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-quick-crypto": "^0.7.13",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",