eletrotupi / tcc/ commit / 87bfdbe

frontend: add shadows, cards, sections and some other layout helpers

Pedro Lucas Porcellis porcellis@eletrotupi.com · 5 months ago 87bfdbe89f107ee4cfd523c6b559754da54cdd4d Browse files
Parents aba81d9
Changed 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
});