Skip to content

Commit

Permalink
Add theming and dark mode support (#64)
Browse files Browse the repository at this point in the history
Introduces a thin theming layer to support dark mode across the app using built-in theming capabilities of react-navigation. It leverages existing light and dark mode defaults from react navigation.
  • Loading branch information
katsuroo authored Oct 25, 2024
1 parent 6d824c9 commit 77a9408
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 37 deletions.
18 changes: 17 additions & 1 deletion templates/boilerplate/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import { useColorScheme } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { QueryClientProvider } from '@tanstack/react-query';
import Providers, { Provider } from 'src/components/Providers';
import RootNavigator from 'src/navigators/RootNavigator';
import queryClient from 'src/util/api/queryClient';
import { lightThemeColors, darkThemeColors } from 'src/theme/colors';

// Add providers to this array. They will be wrapped around the app, with the
// first items in the array wrapping the last items in the array.
const providers: Provider[] = [
(children) => <NavigationContainer>{children}</NavigationContainer>,
(children) => {
const colorScheme = useColorScheme();
const colors = colorScheme === 'dark' ? darkThemeColors : lightThemeColors;

return (
<NavigationContainer
theme={{
dark: colorScheme === 'dark',
colors,
}}
>
{children}
</NavigationContainer>
);
},
(children) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
Expand Down
2 changes: 1 addition & 1 deletion templates/boilerplate/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
Expand Down
11 changes: 11 additions & 0 deletions templates/boilerplate/src/components/Text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { Text as RNText, TextProps as RNTextProps } from 'react-native';
import useTheme from 'src/theme/useTheme';

type TextProps = RNTextProps;

export default function Text({ style, ...props }: TextProps) {
const { colors } = useTheme();

return <RNText style={[{ color: colors.text }, style]} {...props} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import {
TouchableOpacity,
TouchableOpacityProps,
} from 'react-native';
import useTheme from 'src/theme/useTheme';

type ButtonProps = TouchableOpacityProps;

export default function PrimaryButton({ style, ...props }: ButtonProps) {
const { colors } = useTheme();

return (
<TouchableOpacity
activeOpacity={0.6}
style={[styles.button, style]}
style={[styles.button, { backgroundColor: colors.button }, style]}
{...props}
/>
);
Expand All @@ -20,7 +23,6 @@ const styles = StyleSheet.create({
button: {
paddingVertical: 16,
paddingHorizontal: 14,
backgroundColor: '#08f',
borderRadius: 12,
justifyContent: 'center',
},
Expand Down
9 changes: 2 additions & 7 deletions templates/boilerplate/src/screens/AboutScreen/AboutScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import Feather from '@expo/vector-icons/Feather';
import { useQuery } from '@tanstack/react-query';
import {
ActivityIndicator,
FlatList,
StyleSheet,
Text,
View,
} from 'react-native';
import { ActivityIndicator, FlatList, StyleSheet, View } from 'react-native';
import Screen from 'src/components/Screen';
import Text from 'src/components/Text';
import api, { GithubProject } from 'src/util/api/api';

export default function AboutScreen() {
Expand Down
49 changes: 26 additions & 23 deletions templates/boilerplate/src/screens/HomeScreen/HomeScreenContent.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { ReactNode } from 'react';
import {
Image,
Platform,
StyleSheet,
TextProps,
Text as UnStyledText,
View,
} from 'react-native';
import { Image, Platform, StyleSheet, TextProps, View } from 'react-native';
import UnStyledText from 'src/components/Text';
import useTheme from 'src/theme/useTheme';

// TODO: sample, remove
export default function HomeScreenContent() {
Expand All @@ -32,14 +27,14 @@ export default function HomeScreenContent() {
started.
</Text>

<View style={styles.card}>
<Card>
<Text>
<Text style={styles.bold}>We set some things up for you:</Text> Expo,
TanStack Query, Testing Library, React Navigation, and more!
</Text>
</View>
</Card>

<View style={styles.card}>
<Card>
<Text style={styles.bold}>Some notes about organization</Text>
<View style={styles.orgBullets} accessibilityRole="list">
<BulletListItem>
Expand Down Expand Up @@ -77,50 +72,50 @@ export default function HomeScreenContent() {
</UnStyledText>
</BulletListItem>
</View>
</View>
</Card>

<View style={styles.card}>
<Card>
<Text>
<Text style={styles.bold}>To add a new bottom tab</Text> head to{' '}
<Text style={styles.inlineCode}>TabNavigator.tsx</Text> and follow the
instructions commented there.
</Text>
</View>
</Card>

<View style={styles.card}>
<Card>
<Text>
<Text style={styles.bold}>To add a new screen</Text> head to the
screens directory and mount it in the appropriate stack.
</Text>
</View>
</Card>

<View style={styles.card}>
<Card>
<Text>
Check out an <Text style={styles.bold}>example API call</Text> using
TanStack Query in{' '}
<Text style={styles.inlineCode}>AboutScreen.tsx</Text>.
</Text>
</View>
</Card>

<View style={styles.card}>
<Card>
<Text>
Check out <Text style={styles.bold}>a sample test</Text> in{' '}
<Text style={styles.inlineCode}>
src/__tests__/App.integration.test.tsx
</Text>
.
</Text>
</View>
</Card>

<View style={styles.card}>
<Card>
<Text>
Sample code is marked with a{' '}
<Text style={styles.inlineCode}>“TODO”</Text> comment.{' '}
<Text style={styles.bold}>Search the codebase</Text> for{' '}
<Text style={styles.inlineCode}>“TODO”</Text> and remove any desired
sample code.
</Text>
</View>
</Card>
</>
);
}
Expand All @@ -138,6 +133,15 @@ function BulletListItem({ children }: { children: ReactNode }) {
);
}

function Card({ children }: { children: ReactNode }) {
const { colors } = useTheme();
return (
<View style={[styles.card, { backgroundColor: colors.card }]}>
{children}
</View>
);
}

const styles = StyleSheet.create({
text: {
fontSize: 18,
Expand All @@ -151,7 +155,6 @@ const styles = StyleSheet.create({
},
card: {
marginBottom: 18,
backgroundColor: '#fefafa',
paddingVertical: 18,
paddingHorizontal: 12,
borderRadius: 10,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useRoute } from '@react-navigation/native';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import { StyleSheet, View } from 'react-native';
import { InformationScreenProp } from 'src/navigators/navigatorTypes';
import Text from 'src/components/Text';

export default function InformationScreen() {
const { params } = useRoute<InformationScreenProp['route']>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import { StyleSheet, View } from 'react-native';
import Text from 'src/components/Text';

export default function SettingsScreen() {
return (
Expand All @@ -13,7 +14,6 @@ export default function SettingsScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
Expand Down
16 changes: 16 additions & 0 deletions templates/boilerplate/src/theme/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DefaultTheme, DarkTheme } from '@react-navigation/native';
import type { Theme } from '@react-navigation/native';

export type CustomThemeColors = Theme['colors'] & {
button: string;
};

export const lightThemeColors: CustomThemeColors = {
...DefaultTheme.colors,
button: '#08f',
};

export const darkThemeColors: CustomThemeColors = {
...DarkTheme.colors,
button: '#08f',
};
16 changes: 16 additions & 0 deletions templates/boilerplate/src/theme/useTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
Theme,
useTheme as useNavigationTheme,
} from '@react-navigation/native';
import type { CustomThemeColors } from './colors';

export type CustomTheme = Theme & {
colors: CustomThemeColors;
};

const useTheme = () => {
const theme = useNavigationTheme();
return theme as CustomTheme;
};

export default useTheme;

0 comments on commit 77a9408

Please sign in to comment.