Skip to content

Commit 117b016

Browse files
feat(mobile): implement minimalist brutalist redesign and manual theme toggle
- Added themeStore with persistence for light/dark/system modes. - Updated color palette to high-contrast brutalist tokens. - Redesigned Button, Card, and FAB components with bold borders and hard shadows. - Updated HomeScreen and SettingsScreen with the new aesthetic and theme toggle.
1 parent c6abf15 commit 117b016

9 files changed

Lines changed: 442 additions & 256 deletions

File tree

apps/mobile/App.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { AppNavigator } from './app/navigation/AppNavigator';
1313
import { navigationRef } from './app/navigation/navigationRef';
1414
import { lightTheme, darkTheme } from './app/theme/theme';
1515
import { useAuthStore } from './app/store/authStore';
16+
import { useThemeStore } from './app/store/themeStore';
1617
import { userService } from './app/services/user.service';
1718
import { apiBaseUrl } from './app/services/api';
1819
import { realtimeService } from './app/services/realtime.service';
@@ -37,8 +38,14 @@ Notifications.setNotificationHandler({
3738

3839
export default function App() {
3940
const accessToken = useAuthStore((state) => state.accessToken);
40-
const colorScheme = useColorScheme();
41-
const isDarkMode = colorScheme === 'dark';
41+
const systemColorScheme = useColorScheme();
42+
const themeMode = useThemeStore((state) => state.themeMode);
43+
44+
const isDarkMode = React.useMemo(() => {
45+
if (themeMode === 'system') return systemColorScheme === 'dark';
46+
return themeMode === 'dark';
47+
}, [themeMode, systemColorScheme]);
48+
4249
const theme = isDarkMode ? darkTheme : lightTheme;
4350

4451
React.useEffect(() => {
Lines changed: 60 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,90 @@
11
import React from 'react';
22
import { StyleSheet, TouchableOpacity, View } from 'react-native';
33
import { MaterialCommunityIcons } from '@expo/vector-icons';
4-
import Animated, {
5-
useAnimatedStyle,
6-
useSharedValue,
7-
withSpring,
8-
} from 'react-native-reanimated';
4+
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
95
import { useAppTheme } from '../theme/useAppTheme';
106

117
interface FloatingActionButtonProps {
128
onPress: () => void;
139
icon?: keyof typeof MaterialCommunityIcons.glyphMap;
1410
}
1511

16-
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
17-
1812
export function FloatingActionButton({
1913
onPress,
2014
icon = 'plus',
2115
}: FloatingActionButtonProps) {
2216
const { colors } = useAppTheme();
23-
const scale = useSharedValue(1);
17+
const shadowOffset = useSharedValue(6);
2418

2519
const animatedStyle = useAnimatedStyle(() => ({
26-
transform: [{ scale: scale.value }],
20+
transform: [
21+
{ translateX: 6 - shadowOffset.value },
22+
{ translateY: 6 - shadowOffset.value },
23+
],
2724
}));
2825

29-
const handlePressIn = () => {
30-
scale.value = withSpring(0.9, { damping: 15, stiffness: 400 });
31-
};
32-
33-
const handlePressOut = () => {
34-
scale.value = withSpring(1, { damping: 15, stiffness: 400 });
35-
};
36-
3726
return (
38-
<AnimatedTouchable
39-
style={[
40-
styles.fab,
41-
{
42-
backgroundColor: colors.primary,
43-
shadowColor: colors.primary,
44-
},
45-
animatedStyle,
46-
]}
47-
onPress={onPress}
48-
onPressIn={handlePressIn}
49-
onPressOut={handlePressOut}
50-
activeOpacity={0.85}
51-
accessibilityLabel="Create expense"
52-
accessibilityRole="button"
53-
>
54-
<View style={styles.innerGlow}>
55-
<MaterialCommunityIcons name={icon} size={28} color="#FFFFFF" />
56-
</View>
57-
</AnimatedTouchable>
27+
<View style={styles.container}>
28+
{/* Hard Shadow */}
29+
<View style={[styles.shadow, { backgroundColor: colors.border }]} />
30+
31+
<Animated.View style={[styles.animatedWrapper, animatedStyle]}>
32+
<TouchableOpacity
33+
style={[
34+
styles.fab,
35+
{
36+
backgroundColor: colors.primary,
37+
borderColor: colors.border,
38+
},
39+
]}
40+
onPress={onPress}
41+
onPressIn={() => {
42+
shadowOffset.value = withSpring(0, { damping: 20, stiffness: 300 });
43+
}}
44+
onPressOut={() => {
45+
shadowOffset.value = withSpring(6, { damping: 20, stiffness: 300 });
46+
}}
47+
activeOpacity={1}
48+
accessibilityLabel="Create expense"
49+
accessibilityRole="button"
50+
>
51+
<MaterialCommunityIcons
52+
name={icon}
53+
size={32}
54+
color={colors.background}
55+
/>
56+
</TouchableOpacity>
57+
</Animated.View>
58+
</View>
5859
);
5960
}
6061

6162
const styles = StyleSheet.create({
62-
fab: {
63+
container: {
6364
position: 'absolute',
64-
bottom: 28,
65-
right: 20,
66-
width: 60,
67-
height: 60,
68-
borderRadius: 30,
69-
alignItems: 'center',
70-
justifyContent: 'center',
71-
elevation: 8,
72-
shadowOffset: { width: 0, height: 4 },
73-
shadowOpacity: 0.35,
74-
shadowRadius: 8,
65+
bottom: 32,
66+
right: 32,
67+
width: 64,
68+
height: 64,
7569
},
76-
innerGlow: {
77-
width: 60,
78-
height: 60,
79-
borderRadius: 30,
70+
fab: {
71+
width: 64,
72+
height: 64,
8073
alignItems: 'center',
8174
justifyContent: 'center',
82-
backgroundColor: 'rgba(255,255,255,0.1)',
75+
borderWidth: 3,
76+
borderRadius: 4,
77+
},
78+
shadow: {
79+
position: 'absolute',
80+
top: 6,
81+
left: 6,
82+
width: 64,
83+
height: 64,
84+
borderRadius: 4,
85+
},
86+
animatedWrapper: {
87+
width: 64,
88+
height: 64,
8389
},
8490
});
Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { StyleSheet, ViewProps } from 'react-native';
2+
import { StyleSheet, View } from 'react-native';
33
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
44
import { Button as PaperButton, ButtonProps } from 'react-native-paper';
55
import { useAppTheme } from '../../theme/useAppTheme';
@@ -11,41 +11,87 @@ type Props = ButtonProps & {
1111
};
1212

1313
export function Button({ variant = 'primary', style, ...props }: Props) {
14-
const { colors } = useAppTheme();
15-
const scale = useSharedValue(1);
14+
const { colors, isDark } = useAppTheme();
15+
const shadowOffset = useSharedValue(4);
1616

1717
const animatedStyle = useAnimatedStyle(() => ({
18-
transform: [{ scale: scale.value }],
18+
transform: [{ translateX: 4 - shadowOffset.value }, { translateY: 4 - shadowOffset.value }],
1919
}));
2020

21-
const variantStyles: Record<ButtonVariant, { mode: ButtonProps['mode']; buttonColor?: string; textColor?: string }> = {
22-
primary: { mode: 'contained', buttonColor: colors.primary, textColor: '#FFFFFF' },
23-
secondary: { mode: 'outlined', textColor: colors.primary },
24-
danger: { mode: 'contained', buttonColor: colors.danger, textColor: '#FFFFFF' },
25-
ghost: { mode: 'text', textColor: colors.primary },
21+
const variantStyles: Record<ButtonVariant, { mode: ButtonProps['mode']; buttonColor?: string; textColor?: string; borderColor?: string }> = {
22+
primary: { mode: 'contained', buttonColor: colors.primary, textColor: isDark ? '#000000' : '#FFFFFF', borderColor: colors.border },
23+
secondary: { mode: 'outlined', buttonColor: colors.surface, textColor: colors.text_primary, borderColor: colors.border },
24+
danger: { mode: 'contained', buttonColor: colors.danger, textColor: '#FFFFFF', borderColor: colors.border },
25+
ghost: { mode: 'text', textColor: colors.text_primary },
2626
};
2727

2828
const vs = variantStyles[variant];
2929

30-
return (
31-
<Animated.View style={animatedStyle}>
30+
if (variant === 'ghost') {
31+
return (
3232
<PaperButton
3333
{...props}
34-
mode={vs.mode}
35-
buttonColor={props.buttonColor ?? vs.buttonColor}
34+
mode="text"
3635
textColor={props.textColor ?? vs.textColor}
37-
style={[{ borderRadius: 14 }, style as any]}
38-
contentStyle={{ paddingVertical: 4 }}
39-
labelStyle={{ fontWeight: '700', fontSize: 14 }}
40-
onPressIn={(event) => {
41-
scale.value = withSpring(0.96, { damping: 15, stiffness: 300 });
42-
props.onPressIn?.(event);
43-
}}
44-
onPressOut={(event) => {
45-
scale.value = withSpring(1, { damping: 15, stiffness: 300 });
46-
props.onPressOut?.(event);
47-
}}
36+
style={[{ borderRadius: 0 }, style as any]}
37+
labelStyle={{ fontWeight: '800', fontSize: 14, textTransform: 'uppercase' }}
4838
/>
49-
</Animated.View>
39+
);
40+
}
41+
42+
return (
43+
<View style={styles.buttonContainer}>
44+
{/* Hard Shadow Layer */}
45+
<View style={[styles.shadow, { backgroundColor: colors.border, borderRadius: 2 }]} />
46+
47+
<Animated.View style={[styles.animatedWrapper, animatedStyle]}>
48+
<PaperButton
49+
{...props}
50+
mode={vs.mode}
51+
buttonColor={props.buttonColor ?? vs.buttonColor}
52+
textColor={props.textColor ?? vs.textColor}
53+
style={[
54+
styles.button,
55+
{
56+
borderRadius: 2,
57+
borderWidth: 2,
58+
borderColor: vs.borderColor ?? colors.border
59+
},
60+
style as any
61+
]}
62+
contentStyle={{ paddingVertical: 6 }}
63+
labelStyle={{ fontWeight: '900', fontSize: 14, textTransform: 'uppercase' }}
64+
onPressIn={(event) => {
65+
shadowOffset.value = withSpring(0, { damping: 20, stiffness: 300 });
66+
props.onPressIn?.(event);
67+
}}
68+
onPressOut={(event) => {
69+
shadowOffset.value = withSpring(4, { damping: 20, stiffness: 300 });
70+
props.onPressOut?.(event);
71+
}}
72+
/>
73+
</Animated.View>
74+
</View>
5075
);
5176
}
77+
78+
const styles = StyleSheet.create({
79+
buttonContainer: {
80+
position: 'relative',
81+
marginBottom: 4,
82+
marginRight: 4,
83+
},
84+
shadow: {
85+
position: 'absolute',
86+
top: 4,
87+
left: 4,
88+
right: -4,
89+
bottom: -4,
90+
},
91+
animatedWrapper: {
92+
width: '100%',
93+
},
94+
button: {
95+
borderWidth: 2,
96+
},
97+
});

apps/mobile/app/components/ui/Card.tsx

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,49 @@ import { spacing } from '../../theme/spacing';
66
interface CardProps {
77
children: React.ReactNode;
88
style?: ViewStyle;
9-
variant?: 'default' | 'glass';
109
}
1110

12-
export function Card({ children, style, variant = 'default' }: CardProps) {
13-
const { colors, isDark } = useAppTheme();
11+
export function Card({ children, style }: CardProps) {
12+
const { colors } = useAppTheme();
1413

1514
return (
16-
<View
17-
style={[
18-
styles.card,
19-
{
20-
backgroundColor: isDark ? colors.card : colors.surface,
21-
borderColor: colors.border,
22-
...(variant === 'glass'
23-
? {
24-
backgroundColor: isDark
25-
? 'rgba(31, 41, 55, 0.8)'
26-
: 'rgba(255, 255, 255, 0.8)',
27-
}
28-
: {}),
29-
},
30-
style,
31-
]}
32-
>
33-
{children}
15+
<View style={styles.container}>
16+
{/* Hard Shadow */}
17+
<View style={[styles.shadow, { backgroundColor: colors.border }]} />
18+
19+
<View
20+
style={[
21+
styles.card,
22+
{
23+
backgroundColor: colors.surface,
24+
borderColor: colors.border,
25+
},
26+
style,
27+
]}
28+
>
29+
{children}
30+
</View>
3431
</View>
3532
);
3633
}
3734

3835
const styles = StyleSheet.create({
36+
container: {
37+
position: 'relative',
38+
marginBottom: 8,
39+
marginRight: 8,
40+
},
3941
card: {
40-
borderRadius: 16,
42+
borderRadius: 4,
4143
padding: spacing.lg,
42-
borderWidth: 1,
43-
shadowColor: '#000',
44-
shadowOffset: { width: 0, height: 2 },
45-
shadowOpacity: 0.06,
46-
shadowRadius: 8,
47-
elevation: 3,
44+
borderWidth: 2,
45+
},
46+
shadow: {
47+
position: 'absolute',
48+
top: 6,
49+
left: 6,
50+
right: -6,
51+
bottom: -6,
52+
borderRadius: 4,
4853
},
4954
});

0 commit comments

Comments
 (0)