diff --git a/example/src/DrawerItems.tsx b/example/src/DrawerItems.tsx deleted file mode 100644 index 94afa3136c..0000000000 --- a/example/src/DrawerItems.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import * as React from 'react'; -import { Platform, StyleSheet, View } from 'react-native'; -import type { ColorValue } from 'react-native'; - -import { DrawerContentScrollView } from '@react-navigation/drawer'; -import Constants, { ExecutionEnvironment } from 'expo-constants'; -import { - Badge, - Button, - Dialog, - Drawer, - Palette, - Portal, - Switch, - Text, - TouchableRipple, - useTheme, -} from 'react-native-paper'; - -import { dynamicThemeSupported, isWeb } from '../utils'; -import { PreferencesContext } from './PreferencesContext'; - -const DrawerItemsData = [ - { - label: 'Inbox', - icon: 'inbox', - key: 0, - right: () => 44, - }, - { - label: 'Starred', - icon: 'star', - key: 1, - right: ({ color }: { color: ColorValue }) => ( - - ), - }, - { label: 'Sent mail', icon: 'send', key: 2 }, - { label: 'Colored label', icon: 'palette', key: 3 }, - { - label: 'A very long title that will be truncated', - icon: 'delete', - key: 4, - right: () => , - }, -]; - -const DrawerCollapsedItemsData = [ - { - label: 'Inbox', - focusedIcon: 'inbox', - unfocusedIcon: 'inbox-outline', - key: 0, - badge: 44, - }, - { - label: 'Starred', - focusedIcon: 'star', - unfocusedIcon: 'star-outline', - key: 1, - }, - { - label: 'Sent mail', - focusedIcon: 'send', - unfocusedIcon: 'send-outline', - key: 2, - }, - { - label: 'A very long title that will be truncated', - focusedIcon: 'delete', - unfocusedIcon: 'delete-outline', - key: 3, - }, - { - label: 'Full width', - focusedIcon: 'arrow-all', - key: 4, - }, - { - focusedIcon: 'bell', - unfocusedIcon: 'bell-outline', - key: 5, - badge: true, - }, -]; - -function DrawerItems() { - const [drawerItemIndex, setDrawerItemIndex] = React.useState(0); - const [showRTLDialog, setShowRTLDialog] = React.useState(false); - const preferences = React.useContext(PreferencesContext); - - const _setDrawerItem = (index: number) => setDrawerItemIndex(index); - - const { colors } = useTheme(); - const isIOS = Platform.OS === 'ios'; - const expoGoExecution = - Constants.executionEnvironment === ExecutionEnvironment.StoreClient; - - if (!preferences) throw new Error('PreferencesContext not provided'); - - const { - toggleShouldUseDynamicTheme, - toggleTheme, - toggleRtl: toggleRTL, - toggleCollapsed, - toggleCustomFont, - toggleRippleEffect, - customFontLoaded, - rippleEffectEnabled, - collapsed, - rtl: isRTL, - theme: { dark: isDarkTheme }, - shouldUseDynamicTheme, - } = preferences; - - const _handleToggleRTL = () => { - if (!isWeb && expoGoExecution) { - setShowRTLDialog(true); - return; - } - - toggleRTL(); - }; - - const _handleDismissRTLDialog = () => { - setShowRTLDialog(false); - }; - - const coloredLabelTheme = { - colors: { - secondaryContainer: Palette.tertiary80, - onSecondaryContainer: Palette.tertiary20, - }, - }; - - return ( - - {collapsed && ( - - {DrawerCollapsedItemsData.map((props, index) => ( - { - _setDrawerItem(index); - index === 4 && toggleCollapsed(); - }} - /> - ))} - - )} - {!collapsed && ( - <> - - {DrawerItemsData.map((props, index) => ( - _setDrawerItem(index)} - /> - ))} - - - - {dynamicThemeSupported ? ( - - - Use Dynamic Theme - - - - - - ) : null} - - - Dark Theme - - - - - - - - - RTL - - - - - - - - - Collapsed drawer * - - - - - - - - - Custom font * - - - - - - - - - - {isIOS ? 'Highlight' : 'Ripple'} effect * - - - - - - - - {!collapsed && ( - - * - optional example toggles - - )} - - React Native Paper Version{' '} - {require('react-native-paper/package.json').version} - - - )} - - - Changing to RTL - - - Due to Expo Go limitations it is impossible to change RTL - dynamically. To do so, you need to create a development build of - Example app or change it statically by setting{' '} - forcesRTL property to true in{' '} - app.json within{' '} - example directory. - - - - - - - - - ); -} - -const styles = StyleSheet.create({ - drawerContent: { - flex: 1, - }, - preference: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingVertical: 12, - paddingHorizontal: 16, - }, - v3Preference: { - height: 56, - paddingHorizontal: 28, - }, - badge: { - alignSelf: 'center', - }, - collapsedSection: { - marginTop: 16, - }, - annotation: { - marginHorizontal: 24, - marginVertical: 6, - }, -}); - -export default DrawerItems; diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx index 53132e299f..7891374962 100644 --- a/example/src/ExampleList.tsx +++ b/example/src/ExampleList.tsx @@ -1,9 +1,11 @@ -import { FlatList } from 'react-native'; +import { useMemo, useState } from 'react'; +import { FlatList, StyleSheet, View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; -import { Divider, List, useTheme } from 'react-native-paper'; +import { Divider, List, Text, useTheme } from 'react-native-paper'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import ExampleListHeader from './ExampleListHeader'; import ActivityIndicatorExample from './Examples/ActivityIndicatorExample'; import AppbarExample from './Examples/AppbarExample'; import AvatarExample from './Examples/AvatarExample'; @@ -116,32 +118,71 @@ export default function ExampleList() { const { colors } = useTheme(); const safeArea = useSafeAreaInsets(); + const [query, setQuery] = useState(''); + + const filteredData = useMemo(() => { + const search = query.trim().toLowerCase(); + + if (!search) { + return data; + } + + return data.filter( + ({ id, data: example }) => + example.title.toLowerCase().includes(search) || + id.toLowerCase().includes(search) + ); + }, [query]); + return ( - ( - { - // @ts-expect-error TypeScript can't call overloaded functions with union arguments. - // https://github.com/microsoft/TypeScript/issues/40803 - navigation.navigate(item.id); - }} - /> - )} - keyExtractor={({ id }) => id} - data={data} - /> + + + ( + { + // @ts-expect-error TypeScript can't call overloaded functions with union arguments. + // https://github.com/microsoft/TypeScript/issues/40803 + navigation.navigate(item.id); + }} + /> + )} + ListEmptyComponent={ + + {`No examples match "${query.trim()}"`} + + } + keyExtractor={({ id }) => id} + data={filteredData} + /> + ); } + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + empty: { + padding: 16, + textAlign: 'center', + }, +}); diff --git a/example/src/ExampleListHeader.tsx b/example/src/ExampleListHeader.tsx new file mode 100644 index 0000000000..e08deef858 --- /dev/null +++ b/example/src/ExampleListHeader.tsx @@ -0,0 +1,46 @@ +import { StyleSheet } from 'react-native'; + +import { useNavigation } from '@react-navigation/native'; +import { Appbar, Searchbar } from 'react-native-paper'; + +import { usePreferences } from './Preferences/usePreferences'; + +type Props = { + query: string; + onQueryChange: (query: string) => void; +}; + +export default function ExampleListHeader({ query, onQueryChange }: Props) { + const navigation = useNavigation('ExampleList'); + const { togglePreferences } = usePreferences(); + + const canGoBack = navigation.canGoBack(); + + return ( + + navigation.goBack() : undefined} + searchAccessibilityLabel={canGoBack ? 'go back' : 'search'} + traileringIcon="cog" + traileringIconAccessibilityLabel="preferences" + onTraileringIconPress={togglePreferences} + clearAccessibilityLabel="clear search" + autoCorrect={false} + autoCapitalize="none" + style={styles.searchbar} + /> + + ); +} + +const styles = StyleSheet.create({ + searchbar: { + flex: 1, + }, +}); diff --git a/example/src/Examples/CardExample.tsx b/example/src/Examples/CardExample.tsx index ad3451e8b0..ab451ca30e 100644 --- a/example/src/Examples/CardExample.tsx +++ b/example/src/Examples/CardExample.tsx @@ -12,7 +12,7 @@ import { } from 'react-native-paper'; import { isWeb } from '../../utils'; -import { PreferencesContext } from '../PreferencesContext'; +import { usePreferences } from '../Preferences/usePreferences'; import ScreenWrapper from '../ScreenWrapper'; type Mode = 'elevated' | 'outlined' | 'contained'; @@ -21,7 +21,7 @@ const CardExample = () => { const { colors } = useTheme(); const [selectedMode, setSelectedMode] = React.useState('elevated' as Mode); const [isSelected, setIsSelected] = React.useState(false); - const preferences = React.useContext(PreferencesContext); + const { toggleTheme } = usePreferences(); const modes: Mode[] = ['elevated', 'outlined', 'contained']; @@ -179,13 +179,7 @@ const CardExample = () => { - { - preferences?.toggleTheme(); - }} - mode={selectedMode} - > + } diff --git a/example/src/Examples/SearchbarExample.tsx b/example/src/Examples/SearchbarExample.tsx index 739002156b..cb8c0cc931 100644 --- a/example/src/Examples/SearchbarExample.tsx +++ b/example/src/Examples/SearchbarExample.tsx @@ -11,10 +11,12 @@ import { useTheme, } from 'react-native-paper'; +import { usePreferences } from '../Preferences/usePreferences'; import ScreenWrapper from '../ScreenWrapper'; const SearchExample = () => { const navigation = useNavigation('Searchbar'); + const { togglePreferences } = usePreferences(); const [isVisible, setIsVisible] = React.useState(false); const [searchQueries, setSearchQuery] = React.useState({ @@ -27,7 +29,7 @@ const SearchExample = () => { searchWithoutBottomLine: '', loadingViewMode: '', clickableBack: '', - clickableDrawer: '', + clickablePreferences: '', clickableLoading: '', }); @@ -185,15 +187,15 @@ const SearchExample = () => { onChangeText={(query) => setSearchQuery({ ...searchQueries, - clickableDrawer: query, + clickablePreferences: query, }) } - value={searchQueries.clickableDrawer} + value={searchQueries.clickablePreferences} onIconPress={() => { Keyboard.dismiss(); - navigation.openDrawer(); + togglePreferences(); }} - icon="menu" + icon="cog" style={styles.searchbar} /> { - const preferences = React.useContext(PreferencesContext); + const { toggleTheme } = usePreferences(); const [options, setOptions] = React.useState({ showSnackbar: false, @@ -31,9 +31,7 @@ const SnackbarExample = () => { const action = { label: showLongerAction ? 'Toggle Theme' : 'Action', - onPress: () => { - preferences?.toggleTheme(); - }, + onPress: toggleTheme, }; return ( diff --git a/example/src/PreferencesContext.tsx b/example/src/Preferences/PreferencesContext.tsx similarity index 64% rename from example/src/PreferencesContext.tsx rename to example/src/Preferences/PreferencesContext.tsx index b5e381ae05..2e1c9b97e1 100644 --- a/example/src/PreferencesContext.tsx +++ b/example/src/Preferences/PreferencesContext.tsx @@ -2,17 +2,20 @@ import * as React from 'react'; import type { Theme } from 'react-native-paper'; -export const PreferencesContext = React.createContext<{ +export type Preferences = { toggleTheme: () => void; toggleRtl: () => void; - toggleCollapsed: () => void; toggleCustomFont: () => void; + togglePreferences: () => void; toggleRippleEffect: () => void; toggleShouldUseDynamicTheme?: () => void; + resetPreferences: () => void; theme: Theme; rtl: boolean; - collapsed: boolean; customFontLoaded: boolean; + preferencesVisible: boolean; rippleEffectEnabled: boolean; shouldUseDynamicTheme?: boolean; -} | null>(null); +}; + +export const PreferencesContext = React.createContext(null); diff --git a/example/src/Preferences/PreferencesModal.tsx b/example/src/Preferences/PreferencesModal.tsx new file mode 100644 index 0000000000..fa2eb731a8 --- /dev/null +++ b/example/src/Preferences/PreferencesModal.tsx @@ -0,0 +1,199 @@ +import { useState } from 'react'; +import { + Platform, + ScrollView, + StyleSheet, + useWindowDimensions, + View, +} from 'react-native'; + +import Constants, { ExecutionEnvironment } from 'expo-constants'; +import { + Button, + Dialog, + Portal, + Switch, + Text, + TouchableRipple, + useTheme, +} from 'react-native-paper'; +import { Modal } from 'react-native-paper'; + +import { usePreferences } from './usePreferences'; +import { dynamicThemeSupported, isWeb } from '../../utils'; + +export default function PreferencesModal() { + const [showRTLDialog, setShowRTLDialog] = useState(false); + const theme = useTheme(); + const { height: windowHeight } = useWindowDimensions(); + + const isIOS = Platform.OS === 'ios'; + const expoGoExecution = + Constants.executionEnvironment === ExecutionEnvironment.StoreClient; + + const { + toggleShouldUseDynamicTheme, + toggleTheme, + toggleRtl: toggleRTL, + toggleCustomFont, + toggleRippleEffect, + togglePreferences, + resetPreferences, + preferencesVisible, + customFontLoaded, + rippleEffectEnabled, + rtl: isRTL, + theme: { dark: isDarkTheme }, + shouldUseDynamicTheme, + } = usePreferences(); + + const _handleToggleRTL = () => { + if (!isWeb && expoGoExecution) { + setShowRTLDialog(true); + return; + } + + toggleRTL(); + }; + + const _handleDismissRTLDialog = () => { + setShowRTLDialog(false); + }; + + return ( + <> + + + + {dynamicThemeSupported ? ( + + + Use Dynamic Theme + + + + + + ) : null} + + + Dark Theme + + + + + + + + + RTL + + + + + + + + + Custom font * + + + + + + + + + + {isIOS ? 'Highlight' : 'Ripple'} effect * + + + + + + + + + + + * - optional example toggles + + + React Native Paper Version{' '} + {require('react-native-paper/package.json').version} + + + + + + + Changing to RTL + + + Due to Expo Go limitations it is impossible to change RTL + dynamically. To do so, you need to create a development build of + Example app or change it statically by setting{' '} + forcesRTL property to true in{' '} + app.json within{' '} + example directory. + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + modalContent: { + marginHorizontal: 24, + borderRadius: 28, + }, + scrollView: { + flexGrow: 0, + flexShrink: 1, + }, + scrollViewContent: { + paddingVertical: 12, + }, + preference: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 16, + }, + v3Preference: { + height: 56, + paddingHorizontal: 28, + }, + resetButton: { + marginHorizontal: 28, + marginVertical: 12, + }, + annotation: { + marginHorizontal: 24, + marginVertical: 6, + }, +}); diff --git a/example/src/Preferences/usePreferences.tsx b/example/src/Preferences/usePreferences.tsx new file mode 100644 index 0000000000..85555f0223 --- /dev/null +++ b/example/src/Preferences/usePreferences.tsx @@ -0,0 +1,11 @@ +import { useContext } from 'react'; + +import { PreferencesContext } from './PreferencesContext'; + +export function usePreferences() { + const preferences = useContext(PreferencesContext); + + if (!preferences) throw new Error('PreferencesContext not provided'); + + return preferences; +} diff --git a/example/src/Preferences/useSetupPreferences.ts b/example/src/Preferences/useSetupPreferences.ts new file mode 100644 index 0000000000..5313f9853d --- /dev/null +++ b/example/src/Preferences/useSetupPreferences.ts @@ -0,0 +1,172 @@ +import * as React from 'react'; +import { I18nManager, Platform } from 'react-native'; + +import AsyncStorage from '@react-native-async-storage/async-storage'; +import * as Updates from 'expo-updates'; +import { + DarkTheme, + LightTheme, + DynamicLightTheme, + DynamicDarkTheme, +} from 'react-native-paper'; + +import type { Preferences } from './PreferencesContext'; +import { dynamicThemeSupported } from '../../utils'; + +const PERSISTENCE_KEY = 'NAVIGATION_STATE'; +const PREFERENCES_KEY = 'APP_PREFERENCES'; + +const getInitialRtl = () => { + if (Platform.OS === 'web' && typeof document !== 'undefined') { + return document.documentElement.dir === 'rtl'; + } + + return I18nManager.getConstants().isRTL; +}; + +export const navigationPersistor = { + async persist(state: unknown) { + await AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state)); + }, + async restore() { + const state = await AsyncStorage.getItem(PERSISTENCE_KEY); + + return state ? JSON.parse(state) : undefined; + }, +}; + +export function useSetupPreferences() { + const [isReady, setIsReady] = React.useState(false); + + const [initialRtl] = React.useState(getInitialRtl); + + const [shouldUseDynamicTheme, setShouldUseDynamicTheme] = + React.useState(true); + const [isDarkMode, setIsDarkMode] = React.useState(false); + const [rtl, setRtl] = React.useState(initialRtl); + const [customFontLoaded, setCustomFont] = React.useState(false); + const [rippleEffectEnabled, setRippleEffectEnabled] = React.useState(true); + const [preferencesVisible, setPreferencesVisible] = React.useState(false); + + const [navigationKey, setNavigationKey] = React.useState(0); + + const theme = + dynamicThemeSupported && shouldUseDynamicTheme + ? isDarkMode + ? DynamicDarkTheme + : DynamicLightTheme + : isDarkMode + ? DarkTheme + : LightTheme; + + const direction: 'rtl' | 'ltr' = rtl ? 'rtl' : 'ltr'; + + React.useEffect(() => { + const restorePrefs = async () => { + try { + const prefString = await AsyncStorage.getItem(PREFERENCES_KEY); + const preferences = JSON.parse(prefString || ''); + + if (preferences) { + setIsDarkMode(preferences.theme === 'dark'); + + if (typeof preferences.rtl === 'boolean') { + setRtl(preferences.rtl); + } + } + } catch (e) { + // ignore error + } finally { + setIsReady(true); + } + }; + + void restorePrefs(); + }, []); + + React.useEffect(() => { + const savePrefs = async () => { + if (!isReady) { + return; + } + + try { + await AsyncStorage.setItem( + PREFERENCES_KEY, + JSON.stringify({ + theme: isDarkMode ? 'dark' : 'light', + rtl, + }) + ); + } catch (e) { + // ignore error + } + + if (Platform.OS === 'web' && typeof document !== 'undefined') { + document.documentElement.dir = direction; + } + + if (I18nManager.getConstants().isRTL !== rtl) { + I18nManager.forceRTL(rtl); + + if (Platform.OS !== 'web') { + await Updates.reloadAsync(); + } + } + }; + + void savePrefs(); + }, [direction, isDarkMode, isReady, rtl]); + + const resetPreferences = React.useCallback(async () => { + setShouldUseDynamicTheme(true); + setIsDarkMode(false); + setCustomFont(false); + setRippleEffectEnabled(true); + setPreferencesVisible(false); + + try { + await AsyncStorage.multiRemove([PREFERENCES_KEY, PERSISTENCE_KEY]); + } catch (e) { + // ignore error + } + + setNavigationKey((oldValue) => oldValue + 1); + }, []); + + const preferences: Preferences = React.useMemo( + () => ({ + toggleShouldUseDynamicTheme: () => + setShouldUseDynamicTheme((oldValue) => !oldValue), + toggleTheme: () => setIsDarkMode((oldValue) => !oldValue), + toggleRtl: () => setRtl((oldValue) => !oldValue), + toggleCustomFont: () => setCustomFont((oldValue) => !oldValue), + toggleRippleEffect: () => setRippleEffectEnabled((oldValue) => !oldValue), + togglePreferences: () => setPreferencesVisible((oldValue) => !oldValue), + resetPreferences, + preferencesVisible, + customFontLoaded, + rippleEffectEnabled, + shouldUseDynamicTheme, + theme, + rtl, + }), + [ + rtl, + theme, + customFontLoaded, + preferencesVisible, + shouldUseDynamicTheme, + rippleEffectEnabled, + resetPreferences, + ] + ); + + return { + preferences, + isReady, + isDarkMode, + direction, + navigationKey, + }; +} diff --git a/example/src/RootNavigator.tsx b/example/src/RootNavigator.tsx index b64fd1d802..69df2388e1 100644 --- a/example/src/RootNavigator.tsx +++ b/example/src/RootNavigator.tsx @@ -1,6 +1,5 @@ import { Platform, StyleSheet, View } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; import { createNativeStackNavigator, createNativeStackScreen, @@ -9,46 +8,84 @@ import { import { Appbar } from 'react-native-paper'; import ExampleList, { examples } from './ExampleList'; +import PreferencesModal from './Preferences/PreferencesModal'; +import { usePreferences } from './Preferences/usePreferences'; +import SamplesList, { samples } from './SamplesList'; const { TeamDetails, ...examplesWithoutParams } = examples; type ExampleRouteName = keyof typeof examplesWithoutParams; +type SampleRouteName = keyof typeof samples; const fromEntries = ( entries: Array<[Key, Value]> ) => Object.fromEntries(entries) as Record; function Header({ navigation, route, options, back }: NativeStackHeaderProps) { - const drawerNavigation = useNavigation('Home'); + const { togglePreferences } = usePreferences(); + + const isIOS = Platform.OS === 'ios'; + + const backAction = navigation.goBack()} />; + const searchAction = ( + navigation.navigate('ExampleList')} + /> + ); return ( - - {back ? ( - navigation.goBack()} /> - ) : ( - drawerNavigation.openDrawer()} - /> - )} + + {back ? backAction : isIOS ? searchAction : null} + {!isIOS && !back && searchAction} + ); } const Root = createNativeStackNavigator({ - layout: ({ children }) => {children}, + initialRouteName: 'SamplesList', + layout: ({ children }) => ( + <> + {children} + + + ), screenOptions: { header: (props) =>
, }, screens: { + SamplesList: createNativeStackScreen({ + screen: SamplesList, + options: { + title: 'Samples', + }, + linking: '', + }), + ...fromEntries( + ( + Object.entries(samples) as [ + SampleRouteName, + (typeof samples)[SampleRouteName], + ][] + ).map(([id, sample]) => [ + id, + createNativeStackScreen({ + screen: sample.screen, + options: { + title: sample.title, + }, + }), + ]) + ), ExampleList: createNativeStackScreen({ screen: ExampleList, options: { title: 'Examples', + headerShown: false, }, - linking: '', + linking: 'examples', }), ...fromEntries( ( diff --git a/example/src/Samples/ArticleSample.tsx b/example/src/Samples/ArticleSample.tsx new file mode 100644 index 0000000000..8f70ed69af --- /dev/null +++ b/example/src/Samples/ArticleSample.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import { + Button, + Card, + Chip, + FAB, + IconButton, + Text, + Tooltip, +} from 'react-native-paper'; + +import type { SampleConfig } from './types'; +import ScreenWrapper from '../ScreenWrapper'; + +export const ArticleSampleConfig: SampleConfig = { + title: 'Article', + icon: 'newspaper-variant-outline', + components: [ + 'Button', + 'Card', + 'Chip', + 'FAB', + 'IconButton', + 'Text', + 'Tooltip', + ], +}; + +const TOPICS = ['Material 3', 'Design systems', 'React Native']; + +const ArticleSample = () => { + const [liked, setLiked] = React.useState(false); + const [bookmarked, setBookmarked] = React.useState(false); + const [menuExpanded, setMenuExpanded] = React.useState(false); + + return ( + <> + + + + + + + Material Design 3 leans on tonal color, larger corner radii and + motion to express hierarchy. Paper ships those decisions as + components, so a screen built from the defaults already follows + the spec. + + + {TOPICS.map((topic) => ( + + {topic} + + ))} + + + + + + setLiked(!liked)} + /> + + + setBookmarked(!bookmarked)} + /> + + + + + + + + setMenuExpanded(false)} + trigger={{ + icon: 'share-variant', + onPress: () => setMenuExpanded(true), + }} + items={[ + { icon: 'link', label: 'Copy link', onPress: () => {} }, + { icon: 'email', label: 'Send by email', onPress: () => {} }, + { icon: 'comment-outline', label: 'Comment', onPress: () => {} }, + ]} + /> + + ); +}; + +const styles = StyleSheet.create({ + content: { + padding: 16, + }, + cardContent: { + gap: 16, + }, + cardIcons: { + flexDirection: 'row', + alignItems: 'center', + }, + topics: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, + }, +}); + +export default ArticleSample; diff --git a/example/src/Samples/ContactsSample.tsx b/example/src/Samples/ContactsSample.tsx new file mode 100644 index 0000000000..641fef07ab --- /dev/null +++ b/example/src/Samples/ContactsSample.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; +import { FlatList, StyleSheet } from 'react-native'; + +import { + Avatar, + Badge, + Divider, + FAB, + List, + Searchbar, +} from 'react-native-paper'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import type { SampleConfig } from './types'; +import ScreenWrapper from '../ScreenWrapper'; + +export const ContactsSampleConfig: SampleConfig = { + title: 'Contacts', + icon: 'account-group-outline', + components: ['Avatar', 'Badge', 'Divider', 'FAB', 'List', 'Searchbar'], +}; + +const CONTACTS = [ + { id: '1', name: 'Ada Lovelace', role: 'Engineering', unread: 3 }, + { id: '2', name: 'Grace Hopper', role: 'Engineering', unread: 0 }, + { id: '3', name: 'Katherine Johnson', role: 'Research', unread: 12 }, + { id: '4', name: 'Mary Jackson', role: 'Research', unread: 0 }, + { id: '5', name: 'Radia Perlman', role: 'Networking', unread: 1 }, + { id: '6', name: 'Barbara Liskov', role: 'Architecture', unread: 0 }, +]; + +const getInitials = (name: string) => + name + .split(' ') + .map((part) => part[0]) + .join(''); + +const ContactsSample = () => { + const insets = useSafeAreaInsets(); + const [query, setQuery] = React.useState(''); + + const search = query.trim().toLowerCase(); + const contacts = search + ? CONTACTS.filter((contact) => contact.name.toLowerCase().includes(search)) + : CONTACTS; + + return ( + <> + + + contact.id} + keyboardShouldPersistTaps="handled" + ItemSeparatorComponent={Divider} + contentContainerStyle={{ paddingBottom: insets.bottom + 96 }} + renderItem={({ item }) => ( + {}} + left={({ style }) => ( + + )} + right={({ style }) => + item.unread ? ( + {item.unread} + ) : null + } + /> + )} + /> + + + {}} + style={[styles.fab, { bottom: insets.bottom + 16 }]} + /> + + ); +}; + +const styles = StyleSheet.create({ + searchbar: { + margin: 16, + }, + badge: { + alignSelf: 'center', + }, + fab: { + position: 'absolute', + right: 16, + }, +}); + +export default ContactsSample; diff --git a/example/src/Samples/HelpCenterSample.tsx b/example/src/Samples/HelpCenterSample.tsx new file mode 100644 index 0000000000..17293d2366 --- /dev/null +++ b/example/src/Samples/HelpCenterSample.tsx @@ -0,0 +1,111 @@ +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import { + Icon, + List, + Modal, + Portal, + Text, + TouchableRipple, + useTheme, +} from 'react-native-paper'; + +import type { SampleConfig } from './types'; +import ScreenWrapper from '../ScreenWrapper'; + +export const HelpCenterSampleConfig: SampleConfig = { + title: 'Help center', + icon: 'help-circle-outline', + components: ['Icon', 'List', 'Modal', 'Portal', 'Text', 'TouchableRipple'], +}; + +const TOPICS = [ + { + id: 'getting-started', + title: 'Getting started', + icon: 'rocket-launch-outline', + answers: ['Install the app', 'Create your first workspace'], + }, + { + id: 'billing', + title: 'Billing', + icon: 'credit-card-outline', + answers: ['Update your payment method', 'Download past invoices'], + }, + { + id: 'privacy', + title: 'Privacy', + icon: 'shield-lock-outline', + answers: ['Manage data sharing', 'Delete your account'], + }, +]; + +const HelpCenterSample = () => { + const { colors } = useTheme(); + const [contactVisible, setContactVisible] = React.useState(false); + + return ( + <> + + + + {TOPICS.map((topic) => ( + } + > + {topic.answers.map((answer) => ( + {}} /> + ))} + + ))} + + + + setContactVisible(true)}> + + + Still stuck? Contact support + + + + + + setContactVisible(false)} + contentContainerStyle={[ + styles.modal, + { backgroundColor: colors.surface }, + ]} + > + Contact support + + Write to support@example.com and we will get back to you within one + business day. + + + + + ); +}; + +const styles = StyleSheet.create({ + contact: { + flexDirection: 'row', + alignItems: 'center', + gap: 16, + padding: 16, + }, + modal: { + margin: 24, + padding: 24, + borderRadius: 28, + gap: 8, + }, +}); + +export default HelpCenterSample; diff --git a/example/src/Samples/OrdersSample.tsx b/example/src/Samples/OrdersSample.tsx new file mode 100644 index 0000000000..5212715192 --- /dev/null +++ b/example/src/Samples/OrdersSample.tsx @@ -0,0 +1,157 @@ +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import { + Banner, + Button, + Checkbox, + DataTable, + Menu, + Text, +} from 'react-native-paper'; + +import type { SampleConfig } from './types'; +import ScreenWrapper from '../ScreenWrapper'; + +export const OrdersSampleConfig: SampleConfig = { + title: 'Orders', + icon: 'table-large', + components: ['Banner', 'Button', 'Checkbox', 'DataTable', 'Menu', 'Text'], +}; + +const ORDERS = [ + { id: 'A-1041', customer: 'Ada Lovelace', total: 128 }, + { id: 'A-1042', customer: 'Grace Hopper', total: 64 }, + { id: 'A-1043', customer: 'Alan Turing', total: 512 }, + { id: 'A-1044', customer: 'Barbara Liskov', total: 96 }, + { id: 'A-1045', customer: 'Radia Perlman', total: 240 }, + { id: 'A-1046', customer: 'Mary Jackson', total: 32 }, +]; + +const ITEMS_PER_PAGE = 3; + +type SortKey = 'id' | 'customer' | 'total'; + +const SORT_LABELS: Record = { + id: 'Order number', + customer: 'Customer', + total: 'Total', +}; + +const OrdersSample = () => { + const [bannerVisible, setBannerVisible] = React.useState(true); + const [menuVisible, setMenuVisible] = React.useState(false); + const [sortBy, setSortBy] = React.useState('id'); + const [selected, setSelected] = React.useState([]); + const [page, setPage] = React.useState(0); + + const sorted = ORDERS.slice().sort((a, b) => + sortBy === 'total' + ? a.total - b.total + : String(a[sortBy]).localeCompare(String(b[sortBy])) + ); + + const from = page * ITEMS_PER_PAGE; + const to = Math.min(from + ITEMS_PER_PAGE, sorted.length); + + const toggleSelected = (id: string) => + setSelected((current) => + current.includes(id) + ? current.filter((item) => item !== id) + : [...current, id] + ); + + const selectSort = (key: SortKey) => { + setSortBy(key); + setMenuVisible(false); + setPage(0); + }; + + return ( + + setBannerVisible(false) }]} + > + Two orders are waiting for a payment confirmation. + + + + {selected.length} selected + setMenuVisible(false)} + anchor={ + + } + > + {(Object.keys(SORT_LABELS) as SortKey[]).map((key) => ( + selectSort(key)} + /> + ))} + + + + + + + Order + + Customer + + Total + + + {sorted.slice(from, to).map((order) => ( + + + toggleSelected(order.id)} + /> + + {order.id} + + {order.customer} + + {`$${order.total}`} + + ))} + + + + + ); +}; + +const styles = StyleSheet.create({ + toolbar: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: 16, + }, + selectColumn: { + flex: 0.5, + }, + customerColumn: { + flex: 2, + }, +}); + +export default OrdersSample; diff --git a/example/src/Samples/PlayerSample.tsx b/example/src/Samples/PlayerSample.tsx new file mode 100644 index 0000000000..d8f72c2638 --- /dev/null +++ b/example/src/Samples/PlayerSample.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import { + Icon, + ProgressBar, + SegmentedButtons, + Surface, + Text, + ToggleButton, +} from 'react-native-paper'; + +import type { SampleConfig } from './types'; +import ScreenWrapper from '../ScreenWrapper'; + +export const PlayerSampleConfig: SampleConfig = { + title: 'Now playing', + icon: 'play-circle-outline', + components: [ + 'Icon', + 'ProgressBar', + 'SegmentedButtons', + 'Surface', + 'Text', + 'ToggleButton', + ], +}; + +const PlayerSample = () => { + const [repeat, setRepeat] = React.useState('off'); + const [speed, setSpeed] = React.useState('1'); + + return ( + + + + + + + Nightfall + Aurora Skies · Long Way Home + + + + + value && setRepeat(value)} + > + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + content: { + padding: 16, + gap: 24, + }, + cover: { + height: 200, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 16, + }, +}); + +export default PlayerSample; diff --git a/example/src/Samples/SettingsSample.tsx b/example/src/Samples/SettingsSample.tsx new file mode 100644 index 0000000000..ea83866c99 --- /dev/null +++ b/example/src/Samples/SettingsSample.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import { StyleSheet } from 'react-native'; + +import { + Button, + Dialog, + Divider, + List, + Portal, + RadioButton, + Switch, + Text, +} from 'react-native-paper'; + +import type { SampleConfig } from './types'; +import ScreenWrapper from '../ScreenWrapper'; + +export const SettingsSampleConfig: SampleConfig = { + title: 'Settings', + icon: 'cog-outline', + components: [ + 'Button', + 'Dialog', + 'Divider', + 'List', + 'Portal', + 'RadioButton', + 'Switch', + 'Text', + ], +}; + +const SettingsSample = () => { + const [notifications, setNotifications] = React.useState(true); + const [backgroundSync, setBackgroundSync] = React.useState(false); + const [density, setDensity] = React.useState('comfortable'); + const [dialogVisible, setDialogVisible] = React.useState(false); + + const hideDialog = () => setDialogVisible(false); + + return ( + + + ( + + )} + /> + ( + + )} + /> + + + + + + + + + + + + + + + } + onPress={() => setDialogVisible(true)} + /> + + + + + + Sign out? + + + You will need to sign in again to access your workspace. + + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + dialogTitle: { + textAlign: 'center', + }, +}); + +export default SettingsSample; diff --git a/example/src/Samples/SignUpSample.tsx b/example/src/Samples/SignUpSample.tsx new file mode 100644 index 0000000000..ee85c84a61 --- /dev/null +++ b/example/src/Samples/SignUpSample.tsx @@ -0,0 +1,109 @@ +import * as React from 'react'; +import { StyleSheet } from 'react-native'; + +import { + Button, + Checkbox, + Divider, + Snackbar, + Text, + TextInput, +} from 'react-native-paper'; +import type { TextInputAccessoryProps } from 'react-native-paper'; + +import type { SampleConfig } from './types'; +import ScreenWrapper from '../ScreenWrapper'; + +export const SignUpSampleConfig: SampleConfig = { + title: 'Sign up', + icon: 'account-plus-outline', + components: [ + 'Button', + 'Checkbox', + 'Divider', + 'Snackbar', + 'Text', + 'TextInput', + ], +}; + +const SignUpSample = () => { + const [email, setEmail] = React.useState(''); + const [password, setPassword] = React.useState(''); + const [secure, setSecure] = React.useState(true); + const [accepted, setAccepted] = React.useState(false); + const [submitted, setSubmitted] = React.useState(false); + + const emailIcon = (props: TextInputAccessoryProps) => ( + + ); + + const passwordIcon = (props: TextInputAccessoryProps) => ( + setSecure(!secure)} + /> + ); + + return ( + <> + + Create your account + + Sign up to sync your projects across every device. + + + + + + setAccepted(!accepted)} + /> + + + + + + + + setSubmitted(false)}> + Account created + + + ); +}; + +const styles = StyleSheet.create({ + content: { + padding: 16, + gap: 16, + }, +}); + +export default SignUpSample; diff --git a/example/src/Samples/WorkspaceSample.tsx b/example/src/Samples/WorkspaceSample.tsx new file mode 100644 index 0000000000..199e252bde --- /dev/null +++ b/example/src/Samples/WorkspaceSample.tsx @@ -0,0 +1,140 @@ +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import { useNavigation } from '@react-navigation/native'; +import { + Appbar, + Avatar, + BottomNavigation, + Drawer, + Text, +} from 'react-native-paper'; +import type { BottomNavigationRoute } from 'react-native-paper'; + +import type { SampleConfig } from './types'; +import ScreenWrapper from '../ScreenWrapper'; + +export const WorkspaceSampleConfig: SampleConfig = { + title: 'Workspace', + icon: 'view-dashboard-outline', + components: ['Appbar', 'Avatar', 'BottomNavigation', 'Drawer', 'Text'], +}; + +const FOLDERS = [ + { key: 'primary', label: 'Primary', icon: 'inbox' }, + { key: 'starred', label: 'Starred', icon: 'star-outline' }, + { key: 'archive', label: 'Archive', icon: 'archive-outline' }, +]; + +const TEAM = ['Ada Lovelace', 'Grace Hopper', 'Alan Turing']; + +const InboxRoute = () => { + const [folder, setFolder] = React.useState('primary'); + + return ( + + + {FOLDERS.map((item) => ( + setFolder(item.key)} + /> + ))} + + + ); +}; + +const TasksRoute = () => ( + + Today + Review the release checklist + Prepare the design handoff + +); + +const TeamRoute = () => ( + + {TEAM.map((member) => ( + + + {member} + + ))} + +); + +const renderScene = BottomNavigation.SceneMap({ + inbox: InboxRoute, + tasks: TasksRoute, + team: TeamRoute, +}); + +const routes: BottomNavigationRoute[] = [ + { + key: 'inbox', + title: 'Inbox', + focusedIcon: 'inbox', + unfocusedIcon: 'inbox-outline', + badge: 4, + }, + { + key: 'tasks', + title: 'Tasks', + focusedIcon: 'check-circle', + unfocusedIcon: 'check-circle-outline', + }, + { + key: 'team', + title: 'Team', + focusedIcon: 'account-group', + unfocusedIcon: 'account-group-outline', + }, +]; + +const WorkspaceSample = () => { + const navigation = useNavigation('WorkspaceSample'); + const [index, setIndex] = React.useState(0); + + React.useLayoutEffect(() => { + navigation.setOptions({ headerShown: false }); + }, [navigation]); + + return ( + + + navigation.goBack()} /> + + {}} /> + + + + ); +}; + +const styles = StyleSheet.create({ + screen: { + flex: 1, + }, + content: { + padding: 16, + gap: 16, + }, + member: { + flexDirection: 'row', + alignItems: 'center', + gap: 16, + }, +}); + +export default WorkspaceSample; diff --git a/example/src/Samples/types.ts b/example/src/Samples/types.ts new file mode 100644 index 0000000000..7efa77a30d --- /dev/null +++ b/example/src/Samples/types.ts @@ -0,0 +1,5 @@ +export type SampleConfig = { + title: string; + icon: string; + components: string[]; +}; diff --git a/example/src/SamplesList.tsx b/example/src/SamplesList.tsx new file mode 100644 index 0000000000..21ee3a19d3 --- /dev/null +++ b/example/src/SamplesList.tsx @@ -0,0 +1,86 @@ +import { FlatList, StyleSheet, View } from 'react-native'; + +import { useNavigation } from '@react-navigation/native'; +import { Avatar, Card, Chip } from 'react-native-paper'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import ArticleSample, { ArticleSampleConfig } from './Samples/ArticleSample'; +import ContactsSample, { ContactsSampleConfig } from './Samples/ContactsSample'; +import HelpCenterSample, { + HelpCenterSampleConfig, +} from './Samples/HelpCenterSample'; +import OrdersSample, { OrdersSampleConfig } from './Samples/OrdersSample'; +import PlayerSample, { PlayerSampleConfig } from './Samples/PlayerSample'; +import SettingsSample, { SettingsSampleConfig } from './Samples/SettingsSample'; +import SignUpSample, { SignUpSampleConfig } from './Samples/SignUpSample'; +import WorkspaceSample, { + WorkspaceSampleConfig, +} from './Samples/WorkspaceSample'; + +export const samples = { + SignUpSample: { ...SignUpSampleConfig, screen: SignUpSample }, + ContactsSample: { ...ContactsSampleConfig, screen: ContactsSample }, + ArticleSample: { ...ArticleSampleConfig, screen: ArticleSample }, + SettingsSample: { ...SettingsSampleConfig, screen: SettingsSample }, + PlayerSample: { ...PlayerSampleConfig, screen: PlayerSample }, + OrdersSample: { ...OrdersSampleConfig, screen: OrdersSample }, + HelpCenterSample: { ...HelpCenterSampleConfig, screen: HelpCenterSample }, + WorkspaceSample: { ...WorkspaceSampleConfig, screen: WorkspaceSample }, +}; + +type SampleId = keyof typeof samples; + +const data = (Object.keys(samples) as SampleId[]).map((id) => ({ + id, + ...samples[id], +})); + +export default function SamplesList() { + const navigation = useNavigation('SamplesList'); + const safeArea = useSafeAreaInsets(); + + return ( + id} + contentContainerStyle={[ + styles.content, + { + paddingBottom: safeArea.bottom + 16, + paddingLeft: safeArea.left, + paddingRight: safeArea.right, + }, + ]} + showsVerticalScrollIndicator={false} + renderItem={({ item }) => ( + navigation.navigate(item.id)}> + } + /> + + + {item.components.map((component) => ( + + {component} + + ))} + + + + )} + /> + ); +} + +const styles = StyleSheet.create({ + content: { + padding: 16, + gap: 16, + }, + tags: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, + }, +}); diff --git a/example/src/index.tsx b/example/src/index.tsx index afa3941044..bfaf5e4c56 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -1,30 +1,18 @@ -import * as React from 'react'; -import { I18nManager, Platform } from 'react-native'; +import { Platform } from 'react-native'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { - createDrawerNavigator, - createDrawerScreen, -} from '@react-navigation/drawer'; import { createStaticNavigation } from '@react-navigation/native'; import { useFonts } from 'expo-font'; import { useKeepAwake } from 'expo-keep-awake'; import * as SplashScreen from 'expo-splash-screen'; import { StatusBar } from 'expo-status-bar'; -import * as Updates from 'expo-updates'; -import { - PaperProvider, - DarkTheme, - LightTheme, - DynamicLightTheme, - DynamicDarkTheme, -} from 'react-native-paper'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { PaperProvider } from 'react-native-paper'; -import DrawerItems from './DrawerItems'; -import { PreferencesContext } from './PreferencesContext'; +import { PreferencesContext } from './Preferences/PreferencesContext'; +import { + navigationPersistor, + useSetupPreferences, +} from './Preferences/useSetupPreferences'; import App from './RootNavigator'; -import { dynamicThemeSupported } from '../utils'; import { CombinedDarkTheme, CombinedDefaultTheme, @@ -32,54 +20,12 @@ import { createConfiguredFontTheme, } from '../utils/themes'; -const PERSISTENCE_KEY = 'NAVIGATION_STATE'; -const PREFERENCES_KEY = 'APP_PREFERENCES'; - -const getInitialRtl = () => { - if (Platform.OS === 'web' && typeof document !== 'undefined') { - return document.documentElement.dir === 'rtl'; - } - - return I18nManager.getConstants().isRTL; -}; +const Navigation = createStaticNavigation(App); -const Drawer = createDrawerNavigator({ - screens: { - Home: createDrawerScreen({ - screen: App, - options: { - headerShown: false, - }, - linking: { - path: '', - initialRouteName: 'ExampleList', - }, - }), - }, -}).with(({ Navigator }) => { - const preferences = React.useContext(PreferencesContext); - const insets = useSafeAreaInsets(); - const { left, right } = insets || { left: 0, right: 0 }; - const collapsedDrawerWidth = 100 + Math.max(left, right); - - return ( - } - /> - ); -}); - -const Navigation = createStaticNavigation(Drawer); - -type RootDrawerType = typeof Drawer; +type RootNavigationType = typeof App; declare module '@react-navigation/core' { - interface RootNavigator extends RootDrawerType {} + interface RootNavigator extends RootNavigationType {} } export default function PaperExample() { @@ -89,114 +35,15 @@ export default function PaperExample() { Abel: require('../assets/fonts/Abel-Regular.ttf'), }); - const [isReady, setIsReady] = React.useState(false); - - const [shouldUseDynamicTheme, setShouldUseDynamicTheme] = - React.useState(true); - const [isDarkMode, setIsDarkMode] = React.useState(false); - const [rtl, setRtl] = React.useState(getInitialRtl); - const [collapsed, setCollapsed] = React.useState(false); - const [customFontLoaded, setCustomFont] = React.useState(false); - const [rippleEffectEnabled, setRippleEffectEnabled] = React.useState(true); - - const theme = - dynamicThemeSupported && shouldUseDynamicTheme - ? isDarkMode - ? DynamicDarkTheme - : DynamicLightTheme - : isDarkMode - ? DarkTheme - : LightTheme; - - const direction = rtl ? 'rtl' : 'ltr'; - - React.useEffect(() => { - const restorePrefs = async () => { - try { - const prefString = await AsyncStorage.getItem(PREFERENCES_KEY); - const preferences = JSON.parse(prefString || ''); - - if (preferences) { - setIsDarkMode(preferences.theme === 'dark'); - - if (typeof preferences.rtl === 'boolean') { - setRtl(preferences.rtl); - } - } - } catch (e) { - // ignore error - } finally { - setIsReady(true); - } - }; - - void restorePrefs(); - }, []); - - React.useEffect(() => { - const savePrefs = async () => { - if (!isReady) { - return; - } - - try { - await AsyncStorage.setItem( - PREFERENCES_KEY, - JSON.stringify({ - theme: isDarkMode ? 'dark' : 'light', - rtl, - }) - ); - } catch (e) { - // ignore error - } - - if (Platform.OS === 'web' && typeof document !== 'undefined') { - document.documentElement.dir = direction; - } - - if (I18nManager.getConstants().isRTL !== rtl) { - I18nManager.forceRTL(rtl); - - if (Platform.OS !== 'web') { - await Updates.reloadAsync(); - } - } - }; - - void savePrefs(); - }, [direction, isDarkMode, isReady, rtl]); - - const preferences = React.useMemo( - () => ({ - toggleShouldUseDynamicTheme: () => - setShouldUseDynamicTheme((oldValue) => !oldValue), - toggleTheme: () => setIsDarkMode((oldValue) => !oldValue), - toggleRtl: () => setRtl((oldValue) => !oldValue), - toggleCollapsed: () => setCollapsed((oldValue) => !oldValue), - toggleCustomFont: () => setCustomFont((oldValue) => !oldValue), - toggleRippleEffect: () => setRippleEffectEnabled((oldValue) => !oldValue), - customFontLoaded, - rippleEffectEnabled, - shouldUseDynamicTheme, - theme, - collapsed, - rtl, - }), - [ - rtl, - theme, - collapsed, - customFontLoaded, - shouldUseDynamicTheme, - rippleEffectEnabled, - ] - ); + const { preferences, isReady, isDarkMode, direction, navigationKey } = + useSetupPreferences(); if (!isReady || !fontsLoaded) { return null; } + const { theme, customFontLoaded, rippleEffectEnabled } = preferences; + const combinedTheme = isDarkMode ? CombinedDarkTheme : CombinedDefaultTheme; const configuredFontTheme = createConfiguredFontTheme(combinedTheme); const configuredFontNavigationTheme = @@ -209,26 +56,16 @@ export default function PaperExample() { return ( { void SplashScreen.hideAsync(); }}