frontend: add shadows, cards, sections and some other layout helpers
Parents:
aba81d94 file(s) changed
- frontend/components/ui/Cards.tsx +63 -0
- frontend/components/ui/LayoutHelpers.tsx +112 -0
- frontend/components/ui/Sections.tsx +95 -0
- frontend/constants/theme.ts +25 -0
frontend/components/ui/Cards.tsx
@@ -0,0 +1,63 @@
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 + };
frontend/components/ui/LayoutHelpers.tsx
@@ -0,0 +1,112 @@
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 + );
frontend/components/ui/Sections.tsx
@@ -0,0 +1,95 @@
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
@@ -133,6 +133,7 @@ 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 = {
@@ -142,6 +143,30 @@ 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({