Revamp the bottom tab bar
CI / checks (push) Successful in 2m6s

This commit is contained in:
ponas committed 2026-09-07 01:53:49 +02:00
1 parent b7657d9138
commit cf77dd7bf1
15 files changed
+178 -91

No files matched your search

+16 -13
View File
@@ -5,6 +5,7 @@ import * as Notifications from 'expo-notifications';
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { initialWindowMetrics, SafeAreaProvider } from 'react-native-safe-area-context';
import 'expo-dev-client';
import { AuthProvider } from '@/auth/AuthContext';
@@ -27,19 +28,21 @@ export default function App() {
useDeepLinking();
return (
<GestureHandlerRootView>
<StatusBar style="light" />
<PersistQueryClientProvider client={appQueryClient} persistOptions={persistOptions}>
<AuthProvider>
{/* FIXME: Read urlScheme and merchantIdentifier from .Config */}
<StripeProvider
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY || ''}
urlScheme={Constants.expoConfig?.scheme as string}
merchantIdentifier="merchant.no.neuf.chateau" // https://developer.apple.com/account/resources/identifiers/list/merchant
>
<AppInner />
</StripeProvider>
</AuthProvider>
</PersistQueryClientProvider>
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<StatusBar style="light" />
<PersistQueryClientProvider client={appQueryClient} persistOptions={persistOptions}>
<AuthProvider>
{/* FIXME: Read urlScheme and merchantIdentifier from .Config */}
<StripeProvider
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY || ''}
urlScheme={Constants.expoConfig?.scheme as string}
merchantIdentifier="merchant.no.neuf.chateau" // https://developer.apple.com/account/resources/identifiers/list/merchant
>
<AppInner />
</StripeProvider>
</AuthProvider>
</PersistQueryClientProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
+63 -40
View File
@@ -2,8 +2,11 @@ import { BottomTabNavigationOptions, createBottomTabNavigator } from '@react-nav
import { DefaultTheme, NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator, NativeStackNavigationOptions } from '@react-navigation/native-stack';
import React from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useAuth } from '@/auth/AuthContext';
import TabButton from '@/components/TabButton';
import TabIcon, { PILL_SIZE } from '@/components/TabIcon';
import About from '@/modules/about/About';
import EventList from '@/modules/events/EventList';
import Login from '@/modules/login/Login';
@@ -14,9 +17,9 @@ import Settings from '@/modules/settings/Settings';
import SettingsHeaderButton from '@/modules/settings/SettingsHeaderButton';
import theme from '@/theme';
import type { EventStackParamList, HomeTabParamList, UserStackParamList } from '@/types/navigation';
import IconEvent from '@assets/icons/event.svg';
import IconInfo from '@assets/icons/information-circle.svg';
import IconPerson from '@assets/icons/person.svg';
import IconCalendar from '@assets/icons/calendar.svg';
import IconIdCard from '@assets/icons/id-card.svg';
import IconInfo from '@assets/icons/info.svg';
const defaultNavigationStyle = {
headerTintColor: theme.colors.white,
@@ -83,41 +86,61 @@ const UserStackScreen = () => {
type TabBarIconType = BottomTabNavigationOptions['tabBarIcon'];
const HomeTabs = createBottomTabNavigator<HomeTabParamList>();
const EventIcon: TabBarIconType = ({ color, size }) => <IconEvent width={size} height={size} fill={color} />;
const UserIcon: TabBarIconType = ({ color, size }) => <IconPerson width={size + 8} height={size + 8} fill={color} />;
const AboutIcon: TabBarIconType = ({ color, size }) => <IconInfo width={size} height={size} fill={color} />;
const Navigation = () => (
<NavigationContainer theme={navigationTheme}>
<HomeTabs.Navigator initialRouteName="EventStack">
<HomeTabs.Screen
name="EventStack"
component={EventStackScreen}
options={{
headerShown: false,
tabBarLabel: 'Program',
tabBarIcon: EventIcon,
}}
/>
<HomeTabs.Screen
name="UserStack"
component={UserStackScreen}
options={{
headerShown: false,
tabBarLabel: 'Medlemskap',
tabBarIcon: UserIcon,
}}
/>
<HomeTabs.Screen
name="About"
component={About}
options={{
title: 'Om oss',
tabBarLabel: 'Om oss',
tabBarIcon: AboutIcon,
...defaultNavigationStyle,
}}
/>
</HomeTabs.Navigator>
</NavigationContainer>
);
const EventIcon: TabBarIconType = (props) => <TabIcon icon={IconCalendar} {...props} />;
const UserIcon: TabBarIconType = (props) => <TabIcon icon={IconIdCard} {...props} />;
const AboutIcon: TabBarIconType = (props) => <TabIcon icon={IconInfo} {...props} />;
const tabBarOptions: BottomTabNavigationOptions = {
tabBarActiveTintColor: theme.tabBar.activeTint,
tabBarInactiveTintColor: theme.tabBar.inactiveTint,
tabBarLabelStyle: { fontSize: 11, fontWeight: '500' },
tabBarIconStyle: PILL_SIZE,
tabBarButton: TabButton,
};
const Navigation = () => {
const insets = useSafeAreaInsets();
// Setting a height replaces the navigator's own inset handling, so the inset is added back here
const tabBarStyle = {
height: theme.tabBar.height + insets.bottom,
backgroundColor: theme.tabBar.background,
borderTopWidth: 0,
elevation: 0,
};
return (
<NavigationContainer theme={navigationTheme}>
<HomeTabs.Navigator initialRouteName="EventStack" screenOptions={{ ...tabBarOptions, tabBarStyle }}>
<HomeTabs.Screen
name="EventStack"
component={EventStackScreen}
options={{
headerShown: false,
tabBarLabel: 'Program',
tabBarIcon: EventIcon,
}}
/>
<HomeTabs.Screen
name="UserStack"
component={UserStackScreen}
options={{
headerShown: false,
tabBarLabel: 'Medlemskap',
tabBarIcon: UserIcon,
}}
/>
<HomeTabs.Screen
name="About"
component={About}
options={{
title: 'Om oss',
tabBarLabel: 'Om oss',
tabBarIcon: AboutIcon,
...defaultNavigationStyle,
}}
/>
</HomeTabs.Navigator>
</NavigationContainer>
);
};
export default Navigation;
+15
View File
@@ -0,0 +1,15 @@
import type { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
import React from 'react';
import { Pressable } from 'react-native';
// The stock button's borderless Android ripple flashes a circle well past the tab; the pill is feedback enough
const TabButton = ({
android_ripple,
pressColor,
pressOpacity,
hoverEffect,
ref,
...props
}: BottomTabBarButtonProps) => <Pressable {...props} />;
export default TabButton;
+35
View File
@@ -0,0 +1,35 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import theme from '@/theme';
type SvgComponent = React.FunctionComponent<React.SVGAttributes<SVGElement>>;
export type TabIconProps = {
icon: SvgComponent;
focused: boolean;
color: string;
size: number;
};
export const PILL_SIZE = { width: 60, height: 32 };
const TabIcon = ({ icon: Icon, focused, color, size }: TabIconProps) => (
<View style={[styles.pill, focused && styles.pillActive]}>
<Icon width={size} height={size} color={focused ? theme.tabBar.pill.text : color} />
</View>
);
const styles = StyleSheet.create({
pill: {
...PILL_SIZE,
borderRadius: 999,
alignItems: 'center',
justifyContent: 'center',
},
pillActive: {
backgroundColor: theme.tabBar.pill.background,
},
});
export default TabIcon;
+10 -1
View File
@@ -38,6 +38,15 @@ const buttons = {
secondary: { background: colors.goldenBeige, text: colors.deepBrick },
};
// Bottom tab bar: the screen's own surface with no edge, the active icon in a pill of the secondary button colours
const tabBar = {
height: 56,
background: colors.betongGray,
activeTint: colors.chateauBlue,
inactiveTint: colors.chateauBlue05,
pill: buttons.secondary,
};
// neuf.no's type scale at phone widths: body 1.5 line height, headings 600 at 1.2
const typography = {
h1: { fontSize: 32, fontWeight: '600', lineHeight: 38 },
@@ -49,4 +58,4 @@ const typography = {
suphead: { fontSize: 14, fontWeight: '700', lineHeight: 21, textTransform: 'uppercase', letterSpacing: 0.7 },
} as const;
export default { card, colors, background, text, typography, buttons };
export default { card, colors, background, text, typography, buttons, tabBar };