feat: Introduce Alerts, Clips, and Settings tabs, update theme colors, and enhance ThemedView, ThemedText, and IconSymbol components.

This commit is contained in:
2025-12-06 11:42:00 +00:00
parent 3c31ead3ef
commit ed0faaa4d6
10 changed files with 315 additions and 207 deletions

View File

@@ -1,5 +1,6 @@
import { Tabs } from 'expo-router'; import { Tabs } from 'expo-router';
import React from 'react'; import React from 'react';
import { Platform } from 'react-native';
import { HapticTab } from '@/components/haptic-tab'; import { HapticTab } from '@/components/haptic-tab';
import { IconSymbol } from '@/components/ui/icon-symbol'; import { IconSymbol } from '@/components/ui/icon-symbol';
@@ -13,8 +14,18 @@ export default function TabLayout() {
<Tabs <Tabs
screenOptions={{ screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
tabBarInactiveTintColor: Colors[colorScheme ?? 'light'].tabIconDefault,
headerShown: false, headerShown: false,
tabBarButton: HapticTab, tabBarButton: HapticTab,
tabBarStyle: Platform.select({
default: {
backgroundColor: Colors[colorScheme ?? 'light'].background,
borderTopColor: Colors[colorScheme ?? 'light'].border,
borderTopWidth: 1,
elevation: 0,
paddingTop: 8,
},
}),
}}> }}>
<Tabs.Screen <Tabs.Screen
name="index" name="index"
@@ -24,10 +35,24 @@ export default function TabLayout() {
}} }}
/> />
<Tabs.Screen <Tabs.Screen
name="explore" name="alerts"
options={{ options={{
title: 'Explore', title: 'Alerts',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />, tabBarIcon: ({ color }) => <IconSymbol size={28} name="bell.fill" color={color} />,
}}
/>
<Tabs.Screen
name="clips"
options={{
title: 'Clips',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="film.fill" color={color} />,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Settings',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="gearshape.fill" color={color} />,
}} }}
/> />
</Tabs> </Tabs>

View File

@@ -0,0 +1,65 @@
import { StyleSheet } from 'react-native';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
export default function AlertsScreen() {
const colorScheme = useColorScheme();
const theme = colorScheme ?? 'light';
return (
<ThemedView style={styles.container}>
<ThemedView style={styles.header}>
<ThemedText type="title">Alerts</ThemedText>
<ThemedText type="secondary">Recent security activity</ThemedText>
</ThemedView>
<ThemedView
style={[styles.card, {
shadowColor: Colors[theme].border,
borderColor: Colors[theme].border
}]}
variant="card"
>
<IconSymbol size={48} name="bell.fill" color={Colors[theme].icon} />
<ThemedText type="subtitle" style={styles.cardTitle}>No Alerts</ThemedText>
<ThemedText style={styles.cardDescription} type="secondary">
You have no new security alerts.
</ThemedText>
</ThemedView>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
paddingTop: 80,
},
header: {
marginBottom: 32,
},
card: {
padding: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 2,
},
cardTitle: {
marginTop: 16,
marginBottom: 8,
},
cardDescription: {
textAlign: 'center',
maxWidth: 240,
},
});

View File

@@ -0,0 +1,65 @@
import { StyleSheet } from 'react-native';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
export default function ClipsScreen() {
const colorScheme = useColorScheme();
const theme = colorScheme ?? 'light';
return (
<ThemedView style={styles.container}>
<ThemedView style={styles.header}>
<ThemedText type="title">Clips</ThemedText>
<ThemedText type="secondary">Recorded footage</ThemedText>
</ThemedView>
<ThemedView
style={[styles.card, {
shadowColor: Colors[theme].border,
borderColor: Colors[theme].border
}]}
variant="card"
>
<IconSymbol size={48} name="film.fill" color={Colors[theme].icon} />
<ThemedText type="subtitle" style={styles.cardTitle}>No Clips Available</ThemedText>
<ThemedText style={styles.cardDescription} type="secondary">
Recorded clips will appear here.
</ThemedText>
</ThemedView>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
paddingTop: 80,
},
header: {
marginBottom: 32,
},
card: {
padding: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 2,
},
cardTitle: {
marginTop: 16,
marginBottom: 8,
},
cardDescription: {
textAlign: 'center',
maxWidth: 240,
},
});

View File

@@ -1,112 +0,0 @@
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
import { Collapsible } from '@/components/ui/collapsible';
import { ExternalLink } from '@/components/external-link';
import ParallaxScrollView from '@/components/parallax-scroll-view';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Fonts } from '@/constants/theme';
export default function TabTwoScreen() {
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
headerImage={
<IconSymbol
size={310}
color="#808080"
name="chevron.left.forwardslash.chevron.right"
style={styles.headerImage}
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText
type="title"
style={{
fontFamily: Fonts.rounded,
}}>
Explore
</ThemedText>
</ThemedView>
<ThemedText>This app includes example code to help you get started.</ThemedText>
<Collapsible title="File-based routing">
<ThemedText>
This app has two screens:{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
</ThemedText>
<ThemedText>
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
sets up the tab navigator.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/router/introduction">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Android, iOS, and web support">
<ThemedText>
You can open this project on Android, iOS, and the web. To open the web version, press{' '}
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
</ThemedText>
</Collapsible>
<Collapsible title="Images">
<ThemedText>
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
different screen densities
</ThemedText>
<Image
source={require('@/assets/images/react-logo.png')}
style={{ width: 100, height: 100, alignSelf: 'center' }}
/>
<ExternalLink href="https://reactnative.dev/docs/images">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Light and dark mode components">
<ThemedText>
This template has light and dark mode support. The{' '}
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
what the user&apos;s current color scheme is, and so you can adjust UI colors accordingly.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Animations">
<ThemedText>
This template includes an example of an animated component. The{' '}
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
the powerful{' '}
<ThemedText type="defaultSemiBold" style={{ fontFamily: Fonts.mono }}>
react-native-reanimated
</ThemedText>{' '}
library to create a waving hand animation.
</ThemedText>
{Platform.select({
ios: (
<ThemedText>
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
component provides a parallax effect for the header image.
</ThemedText>
),
})}
</Collapsible>
</ParallaxScrollView>
);
}
const styles = StyleSheet.create({
headerImage: {
color: '#808080',
bottom: -90,
left: -35,
position: 'absolute',
},
titleContainer: {
flexDirection: 'row',
gap: 8,
},
});

View File

@@ -1,98 +1,67 @@
import { Image } from 'expo-image'; import { StyleSheet } from 'react-native';
import { Platform, StyleSheet } from 'react-native';
import { HelloWave } from '@/components/hello-wave';
import ParallaxScrollView from '@/components/parallax-scroll-view';
import { ThemedText } from '@/components/themed-text'; import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view'; import { ThemedView } from '@/components/themed-view';
import { Link } from 'expo-router'; import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
export default function HomeScreen() { export default function HomeScreen() {
return ( const colorScheme = useColorScheme();
<ParallaxScrollView const theme = colorScheme ?? 'light';
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
headerImage={
<Image
source={require('@/assets/images/partial-react-logo.png')}
style={styles.reactLogo}
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Welcome!</ThemedText>
<HelloWave />
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
<ThemedText>
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
Press{' '}
<ThemedText type="defaultSemiBold">
{Platform.select({
ios: 'cmd + d',
android: 'cmd + m',
web: 'F12',
})}
</ThemedText>{' '}
to open developer tools.
</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<Link href="/modal">
<Link.Trigger>
<ThemedText type="subtitle">Step 2: Explore</ThemedText>
</Link.Trigger>
<Link.Preview />
<Link.Menu>
<Link.MenuAction title="Action" icon="cube" onPress={() => alert('Action pressed')} />
<Link.MenuAction
title="Share"
icon="square.and.arrow.up"
onPress={() => alert('Share pressed')}
/>
<Link.Menu title="More" icon="ellipsis">
<Link.MenuAction
title="Delete"
icon="trash"
destructive
onPress={() => alert('Delete pressed')}
/>
</Link.Menu>
</Link.Menu>
</Link>
<ThemedText> return (
{`Tap the Explore tab to learn more about what's included in this starter app.`} <ThemedView style={styles.container}>
<ThemedView style={styles.header}>
<ThemedText type="title">Your Cameras</ThemedText>
<ThemedText type="secondary">Manage your indoor security devices</ThemedText>
</ThemedView>
<ThemedView
style={[styles.card, {
shadowColor: Colors[theme].border,
borderColor: Colors[theme].border
}]}
variant="card"
>
<IconSymbol size={48} name="house.fill" color={Colors[theme].icon} />
<ThemedText type="subtitle" style={styles.cardTitle}>No Cameras Added</ThemedText>
<ThemedText style={styles.cardDescription} type="secondary">
Add a camera to start monitoring your home.
</ThemedText> </ThemedText>
</ThemedView> </ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
<ThemedText>
{`When you're ready, run `}
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
<ThemedText type="defaultSemiBold">app-example</ThemedText>.
</ThemedText>
</ThemedView> </ThemedView>
</ParallaxScrollView>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
titleContainer: { container: {
flexDirection: 'row', flex: 1,
alignItems: 'center', padding: 20,
gap: 8, paddingTop: 80,
}, },
stepContainer: { header: {
gap: 8, marginBottom: 32,
},
card: {
padding: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
// iOS Shadow
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
// Android Shadow
elevation: 2,
},
cardTitle: {
marginTop: 16,
marginBottom: 8, marginBottom: 8,
}, },
reactLogo: { cardDescription: {
height: 178, textAlign: 'center',
width: 290, maxWidth: 240,
bottom: 0,
left: 0,
position: 'absolute',
}, },
}); });

View File

@@ -0,0 +1,78 @@
import { StyleSheet } from 'react-native';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
export default function SettingsScreen() {
const colorScheme = useColorScheme();
const theme = colorScheme ?? 'light';
const cardStyle = [
styles.card,
{
shadowColor: Colors[theme].border,
borderColor: Colors[theme].border
}
];
return (
<ThemedView style={styles.container}>
<ThemedView style={styles.header}>
<ThemedText type="title">Settings</ThemedText>
<ThemedText type="secondary">App preferences</ThemedText>
</ThemedView>
<ThemedView style={styles.sectionHeader}>
<ThemedText type="subtitle">Account</ThemedText>
</ThemedView>
<ThemedView style={cardStyle} variant="card">
<ThemedText style={styles.item}>Profile</ThemedText>
<ThemedText style={styles.item}>Notifications</ThemedText>
<ThemedText style={[styles.item, styles.lastItem]}>Privacy</ThemedText>
</ThemedView>
<ThemedView style={styles.sectionHeader}>
<ThemedText type="subtitle">General</ThemedText>
</ThemedView>
<ThemedView style={cardStyle} variant="card">
<ThemedText style={styles.item}>About</ThemedText>
<ThemedText style={styles.item}>Help & Support</ThemedText>
<ThemedText style={[styles.item, styles.lastItem]}>Sign Out</ThemedText>
</ThemedView>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
paddingTop: 80,
},
header: {
marginBottom: 32,
},
sectionHeader: {
marginBottom: 12,
marginTop: 24,
},
card: {
borderRadius: 16,
borderWidth: 1,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 2,
overflow: 'hidden',
},
item: {
padding: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0,0,0,0.05)',
},
lastItem: {
borderBottomWidth: 0,
},
});

View File

@@ -5,7 +5,7 @@ import { useThemeColor } from '@/hooks/use-theme-color';
export type ThemedTextProps = TextProps & { export type ThemedTextProps = TextProps & {
lightColor?: string; lightColor?: string;
darkColor?: string; darkColor?: string;
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'; type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' | 'secondary';
}; };
export function ThemedText({ export function ThemedText({
@@ -15,7 +15,10 @@ export function ThemedText({
type = 'default', type = 'default',
...rest ...rest
}: ThemedTextProps) { }: ThemedTextProps) {
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); const color = useThemeColor(
{ light: lightColor, dark: darkColor },
type === 'secondary' ? 'textSecondary' : 'text'
);
return ( return (
<Text <Text
@@ -26,6 +29,7 @@ export function ThemedText({
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined, type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
type === 'subtitle' ? styles.subtitle : undefined, type === 'subtitle' ? styles.subtitle : undefined,
type === 'link' ? styles.link : undefined, type === 'link' ? styles.link : undefined,
type === 'secondary' ? styles.secondary : undefined,
style, style,
]} ]}
{...rest} {...rest}
@@ -55,6 +59,10 @@ const styles = StyleSheet.create({
link: { link: {
lineHeight: 30, lineHeight: 30,
fontSize: 16, fontSize: 16,
color: '#0a7ea4', color: '#635bff', // Stripe blurple
},
secondary: {
fontSize: 14,
lineHeight: 22,
}, },
}); });

View File

@@ -5,10 +5,11 @@ import { useThemeColor } from '@/hooks/use-theme-color';
export type ThemedViewProps = ViewProps & { export type ThemedViewProps = ViewProps & {
lightColor?: string; lightColor?: string;
darkColor?: string; darkColor?: string;
variant?: 'default' | 'card';
}; };
export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) { export function ThemedView({ style, lightColor, darkColor, variant = 'default', ...otherProps }: ThemedViewProps) {
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, variant === 'card' ? 'card' : 'background');
return <View style={[{ backgroundColor }, style]} {...otherProps} />; return <View style={[{ backgroundColor }, style]} {...otherProps} />;
} }

View File

@@ -1,7 +1,7 @@
// Fallback for using MaterialIcons on Android and web. // Fallback for using MaterialIcons on Android and web.
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { SymbolWeight, SymbolViewProps } from 'expo-symbols'; import { SymbolViewProps, SymbolWeight } from 'expo-symbols';
import { ComponentProps } from 'react'; import { ComponentProps } from 'react';
import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native'; import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native';
@@ -18,6 +18,9 @@ const MAPPING = {
'paperplane.fill': 'send', 'paperplane.fill': 'send',
'chevron.left.forwardslash.chevron.right': 'code', 'chevron.left.forwardslash.chevron.right': 'code',
'chevron.right': 'chevron-right', 'chevron.right': 'chevron-right',
'bell.fill': 'notifications',
'film.fill': 'video-library',
'gearshape.fill': 'settings',
} as IconMapping; } as IconMapping;
/** /**

View File

@@ -5,25 +5,31 @@
import { Platform } from 'react-native'; import { Platform } from 'react-native';
const tintColorLight = '#0a7ea4'; const tintColorLight = '#635bff';
const tintColorDark = '#fff'; const tintColorDark = '#fff';
export const Colors = { export const Colors = {
light: { light: {
text: '#11181C', text: '#0a2540',
background: '#fff', textSecondary: '#425466',
background: '#f6f9fc',
tint: tintColorLight, tint: tintColorLight,
icon: '#687076', icon: '#8898aa',
tabIconDefault: '#687076', tabIconDefault: '#8898aa',
tabIconSelected: tintColorLight, tabIconSelected: tintColorLight,
card: '#ffffff',
border: '#e6ebf1',
}, },
dark: { dark: {
text: '#ECEDEE', text: '#ECEDEE',
textSecondary: '#9BA1A6',
background: '#151718', background: '#151718',
tint: tintColorDark, tint: tintColorDark,
icon: '#9BA1A6', icon: '#9BA1A6',
tabIconDefault: '#9BA1A6', tabIconDefault: '#9BA1A6',
tabIconSelected: tintColorDark, tabIconSelected: tintColorDark,
card: '#232526', // Fallback for dark mode
border: '#333333',
}, },
}; };