frontend: add shadows, cards, sections and some other layout helpers
Pedro Lucas Porcellis porcellis@eletrotupi.com · 5 months ago
87bfdbe89f107ee4cfd523c6b559754da54cdd4d Browse filesParents
aba81d9Changed files
4
Additions
+295
Deletions
0
/dev/null → frontend/components/ui/Cards.tsx
1 +
import React from 'react';
2 +
import {
3 +
View,
4 +
StyleSheet,
5 +
Pressable,
6 +
ViewStyle,
7 +
} from 'react-native';
8 +
9 +
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '@/constants/theme';
10 +
import { useThemeColor } from '@/hooks/use-theme-color';
11 +
import { ThemedText } from '@/components/misc/themed-text'
12 +
13 +
interface CardProps {
14 +
children: React.ReactNode;
15 +
onPress?: () => void;
16 +
variant?: 'default' | 'highlighted';
17 +
style?: ViewStyle;
18 +
}
19 +
20 +
export const Card: React.FC<CardProps> = ({
21 +
children,
22 +
onPress,
23 +
variant = 'default',
24 +
style,
25 +
}) => {
26 +
const isHighlighted = variant === 'highlighted';
27 +
28 +
const surfaceColor = useThemeColor({}, 'surface');
29 +
const cardBorderColor = useThemeColor({}, 'cardBorder');
30 +
const accentBlueColor = useThemeColor({}, 'accentBlue');
31 +
32 +
const styles = StyleSheet.create({
33 +
card: {
34 +
backgroundColor: surfaceColor,
35 +
borderRadius: BorderRadius.lg,
36 +
borderWidth: 1,
37 +
borderColor: cardBorderColor,
38 +
padding: Spacing.internalPadding,
39 +
marginBottom: Spacing.cardGap,
40 +
...(isHighlighted && {
41 +
backgroundColor: accentBlueColor,
42 +
borderColor: 'transparent',
43 +
}),
44 +
...Shadows.sm,
45 +
},
46 +
});
47 +
48 +
return (
49 +
<Pressable
50 +
onPress={onPress}
51 +
disabled={!onPress}
52 +
style={({ pressed }) => [
53 +
styles.card,
54 +
pressed && onPress && {
55 +
opacity: 0.8,
56 +
},
57 +
style,
58 +
]}
59 +
>
60 +
{children}
61 +
</Pressable>
62 +
);
63 +
};
/dev/null → frontend/components/ui/LayoutHelpers.tsx
1 +
import React from 'react';
2 +
import { View, ViewStyle } from 'react-native';
3 +
import { Spacing } from '@/constants/theme';
4 +
5 +
interface FlexLayoutProps {
6 +
children: React.ReactNode;
7 +
gap?: number;
8 +
style?: ViewStyle;
9 +
}
10 +
11 +
export const Row: React.FC<FlexLayoutProps> = ({
12 +
children,
13 +
gap = Spacing.inlineGapSm,
14 +
style,
15 +
}) => (
16 +
<View
17 +
style={[
18 +
{
19 +
flexDirection: 'row',
20 +
gap,
21 +
},
22 +
style,
23 +
]}
24 +
>
25 +
{children}
26 +
</View>
27 +
);
28 +
29 +
export const Col: React.FC<FlexLayoutProps> = ({
30 +
children,
31 +
gap = Spacing.inlineGapSm,
32 +
style,
33 +
}) => (
34 +
<View
35 +
style={[
36 +
{
37 +
flexDirection: 'column',
38 +
gap,
39 +
},
40 +
style,
41 +
]}
42 +
>
43 +
{children}
44 +
</View>
45 +
);
46 +
47 +
export const Grid: React.FC<FlexLayoutProps & { columns?: number }> = ({
48 +
children,
49 +
gap = Spacing.cardGap,
50 +
columns = 2,
51 +
style,
52 +
}) => (
53 +
<View
54 +
style={[
55 +
{
56 +
flexDirection: 'row',
57 +
flexWrap: 'wrap',
58 +
gap,
59 +
justifyContent: 'space-between',
60 +
},
61 +
style,
62 +
]}
63 +
>
64 +
{React.Children.map(children, (child) =>
65 +
React.cloneElement(child as React.ReactElement, {
66 +
style: [
67 +
{
68 +
width: `${100 / columns - (gap * (columns - 1)) / columns}%`,
69 +
},
70 +
(child as React.ReactElement).props.style,
71 +
],
72 +
})
73 +
)}
74 +
</View>
75 +
);
76 +
77 +
export const Center: React.FC<{ children: React.ReactNode; style?: ViewStyle }> = ({
78 +
children,
79 +
style,
80 +
}) => (
81 +
<View
82 +
style={[
83 +
{
84 +
justifyContent: 'center',
85 +
alignItems: 'center',
86 +
},
87 +
style,
88 +
]}
89 +
>
90 +
{children}
91 +
</View>
92 +
);
93 +
94 +
export const Between: React.FC<FlexLayoutProps> = ({
95 +
children,
96 +
gap = 0,
97 +
style,
98 +
}) => (
99 +
<View
100 +
style={[
101 +
{
102 +
flexDirection: 'row',
103 +
justifyContent: 'space-between',
104 +
alignItems: 'center',
105 +
gap,
106 +
},
107 +
style,
108 +
]}
109 +
>
110 +
{children}
111 +
</View>
112 +
);
/dev/null → frontend/components/ui/Sections.tsx
1 +
import React from 'react';
2 +
import {
3 +
View,
4 +
StyleSheet,
5 +
Pressable,
6 +
ViewStyle,
7 +
} from 'react-native';
8 +
import { ThemedText } from '@/components/misc/themed-text'
9 +
10 +
import { Colors, Typography, Spacing, BorderRadius } from '@/constants/theme';
11 +
import { useThemeColor } from '@/hooks/use-theme-color';
12 +
13 +
interface SectionProps {
14 +
children: React.ReactNode;
15 +
style?: ViewStyle;
16 +
}
17 +
18 +
export const Section: React.FC<SectionProps> = ({ children, style }) => {
19 +
const styles = StyleSheet.create({
20 +
section: {
21 +
marginBottom: Spacing.sectionGap,
22 +
},
23 +
});
24 +
return <View style={[styles.section, style]}>{children}</View>;
25 +
};
26 +
27 +
interface SectionHeaderProps {
28 +
title: string;
29 +
actionLabel?: string;
30 +
onActionPress?: () => void;
31 +
style?: ViewStyle;
32 +
}
33 +
34 +
// XXX: A Section must either have a label as a pill/informative thing
35 +
// or it should be a link
36 +
export const SectionHeader: React.FC<SectionHeaderProps> = ({
37 +
title,
38 +
info,
39 +
actionLabel,
40 +
onActionPress,
41 +
style,
42 +
}) => {
43 +
const textColor = useThemeColor({}, 'text');
44 +
const tintColor = useThemeColor({}, 'tint');
45 +
46 +
const styles = StyleSheet.create({
47 +
headerContainer: {
48 +
flexDirection: 'row',
49 +
justifyContent: 'space-between',
50 +
alignItems: 'center',
51 +
marginBottom: (Spacing.cardGap * 2),
52 +
},
53 +
title: {
54 +
fontSize: Typography.headlineMd.fontSize,
55 +
fontWeight: Typography.headlineMd.fontWeight,
56 +
color: textColor,
57 +
},
58 +
actionButton: {
59 +
padding: 4,
60 +
},
61 +
actionText: {
62 +
fontSize: Typography.bodyMd.fontSize,
63 +
fontWeight: '600',
64 +
color: tintColor,
65 +
},
66 +
});
67 +
68 +
// TODO: Rework these when we finally have chips/badges
69 +
return (
70 +
<View style={[styles.headerContainer, style]}>
71 +
<ThemedText style={styles.title}>
72 +
{title}
73 +
</ThemedText>
74 +
75 +
{info && !actionLabel && (
76 +
<ThemedText>
77 +
{info}
78 +
</ThemedText>
79 +
)}
80 +
81 +
{actionLabel && !info && onActionPress && (
82 +
<Pressable
83 +
style={styles.actionButton}
84 +
onPress={onActionPress}
85 +
hitSlop={8}
86 +
>
87 +
<ThemedText style={styles.actionText}>
88 +
{actionLabel}
89 +
</ThemedText>
90 +
</Pressable>
91 +
)}
92 +
</View>
93 +
);
94 +
};
95 +
frontend/constants/theme.ts
1 1
/**
2 2
* Below are the colors that are used in the app. The colors are defined in the light and dark mode.
3 3
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
4 4
*/
5 5
131 131
132 132
export const Spacing = {
133 133
containerPadding: 16, // 1rem
134 134
sectionGap: 32, // 2rem
135 135
inlineGapSm: 8, // 0.5rem
136 +
cardGap: 12 // 0.75rem
136 137
};
137 138
138 139
export const BorderRadius = {
139 140
sm: 4, // 0.25rem
140 141
default: 8, // 0.5rem
141 142
md: 12, // 0.75rem
142 143
lg: 16, // 1rem
143 144
xl: 24, // 1.5rem
144 145
full: 9999, // pill-shaped
146 +
};
147 +
148 +
export const Shadows = {
149 +
sm: {
150 +
shadowColor: '#000000',
151 +
shadowOffset: { width: 0, height: 1 },
152 +
shadowOpacity: 0.05,
153 +
shadowRadius: 2,
154 +
elevation: 2,
155 +
},
156 +
md: {
157 +
shadowColor: '#000000',
158 +
shadowOffset: { width: 0, height: 4 },
159 +
shadowOpacity: 0.1,
160 +
shadowRadius: 8,
161 +
elevation: 4,
162 +
},
163 +
lg: {
164 +
shadowColor: NEON_GREEN,
165 +
shadowOffset: { width: 0, height: 8 },
166 +
shadowOpacity: 0.15,
167 +
shadowRadius: 16,
168 +
elevation: 8,
169 +
},
145 170
};
146 171
147 172
export const Fonts = Platform.select({
148 173
default: {
149 174
sans: 'Inter',
150 175
serif: 'serif',
151 176
rounded: 'Inter',
152 177
mono: 'monospace',
153 178
},
154 179
});