From 4775917db05a8c8122bcaf2794eafec84097f706 Mon Sep 17 00:00:00 2001 From: gibbyb Date: Tue, 4 Mar 2025 16:55:45 -0600 Subject: [PATCH] Added theme components. Reorganized them. Prepping for Tech Tracker Rewrite --- app/(tabs)/index.tsx | 2 +- app/(tabs)/settings.tsx | 2 +- app/+not-found.tsx | 5 +- components/default/Collapsible.tsx | 2 +- components/default/ParallaxScrollView.tsx | 2 +- components/theme/buttons/TextButton.tsx | 33 -------- .../buttons/{Button.tsx => ThemedButton.tsx} | 34 +++++--- components/theme/buttons/ThemedTextButton.tsx | 55 ++++++++++++ .../{Theme.tsx => default/ThemedText.tsx} | 10 +-- components/theme/default/ThemedView.tsx | 13 +++ components/theme/index.tsx | 25 ++++++ components/theme/inputs/ThemedSearchBar.tsx | 62 ++++++++++++++ components/theme/inputs/ThemedSwitch.tsx | 26 ++++++ components/theme/inputs/ThemedTextInput.tsx | 63 ++++++++++++++ components/theme/ui/ThemedAvatar.tsx | 84 +++++++++++++++++++ components/theme/ui/ThemedBadge.tsx | 62 ++++++++++++++ components/theme/ui/ThemedCard.tsx | 64 ++++++++++++++ components/theme/ui/ThemedDivider.tsx | 37 ++++++++ components/theme/ui/ThemedIcon.tsx | 34 ++++++++ constants/Colors.ts | 52 ++++++++---- 20 files changed, 595 insertions(+), 72 deletions(-) delete mode 100644 components/theme/buttons/TextButton.tsx rename components/theme/buttons/{Button.tsx => ThemedButton.tsx} (55%) create mode 100644 components/theme/buttons/ThemedTextButton.tsx rename components/theme/{Theme.tsx => default/ThemedText.tsx} (79%) create mode 100644 components/theme/default/ThemedView.tsx create mode 100644 components/theme/index.tsx create mode 100644 components/theme/inputs/ThemedSearchBar.tsx create mode 100644 components/theme/inputs/ThemedSwitch.tsx create mode 100644 components/theme/inputs/ThemedTextInput.tsx create mode 100644 components/theme/ui/ThemedAvatar.tsx create mode 100644 components/theme/ui/ThemedBadge.tsx create mode 100644 components/theme/ui/ThemedCard.tsx create mode 100644 components/theme/ui/ThemedDivider.tsx create mode 100644 components/theme/ui/ThemedIcon.tsx diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 11ace04..8f91958 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,6 +1,6 @@ import { Image, StyleSheet, Platform } from 'react-native'; import ParallaxScrollView from '@/components/default/ParallaxScrollView'; -import { ThemedText, ThemedView } from '@/components/theme/Theme'; +import { ThemedText, ThemedView } from '@/components/theme'; const HomeScreen = () => { return ( diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index 406376e..c4545fc 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -2,7 +2,7 @@ import { StyleSheet, Image, Platform } from 'react-native'; import { Collapsible } from '@/components/default/Collapsible'; import { ExternalLink } from '@/components/default/ExternalLink'; import ParallaxScrollView from '@/components/default/ParallaxScrollView'; -import { ThemedText, ThemedView } from '@/components/theme/Theme'; +import { ThemedText, ThemedView } from '@/components/theme'; import { IconSymbol } from '@/components/ui/IconSymbol'; const TabTwoScreen = () => { diff --git a/app/+not-found.tsx b/app/+not-found.tsx index 59a6497..09d8c08 100644 --- a/app/+not-found.tsx +++ b/app/+not-found.tsx @@ -1,7 +1,6 @@ import { Link, Stack } from 'expo-router'; import { StyleSheet } from 'react-native'; -import { ThemedText, ThemedView } from '@/components/theme/Theme'; -import TextButton from '@/components/theme/buttons/TextButton'; +import { ThemedText, ThemedTextButton, ThemedView } from '@/components/theme'; const NotFoundScreen = () => { return ( @@ -10,7 +9,7 @@ const NotFoundScreen = () => { This screen doesn't exist. - + diff --git a/components/default/Collapsible.tsx b/components/default/Collapsible.tsx index 90219fb..47a2b8d 100644 --- a/components/default/Collapsible.tsx +++ b/components/default/Collapsible.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren, useState } from 'react'; import { StyleSheet, TouchableOpacity } from 'react-native'; -import { ThemedText, ThemedView } from '@/components/theme/Theme'; +import { ThemedText, ThemedView } from '@/components/theme'; import { IconSymbol } from '@/components/ui/IconSymbol'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; diff --git a/components/default/ParallaxScrollView.tsx b/components/default/ParallaxScrollView.tsx index 2af4f0a..bbc8acf 100644 --- a/components/default/ParallaxScrollView.tsx +++ b/components/default/ParallaxScrollView.tsx @@ -6,7 +6,7 @@ import Animated, { useAnimatedStyle, useScrollViewOffset, } from 'react-native-reanimated'; -import { ThemedView } from '@/components/theme/Theme'; +import { ThemedView } from '@/components/theme'; import { useBottomTabOverflow } from '@/components/ui/TabBarBackground'; import { useColorScheme } from '@/hooks/useColorScheme'; diff --git a/components/theme/buttons/TextButton.tsx b/components/theme/buttons/TextButton.tsx deleted file mode 100644 index 65490f5..0000000 --- a/components/theme/buttons/TextButton.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import Button from '@/components/theme/buttons/Button'; -import { ThemedText } from '@/components/theme/Theme'; -import { Colors } from '@/constants/Colors'; -import { useColorScheme } from '@/hooks/useColorScheme'; - -const DEFAULT_FONT_SIZE = 16; - -type Props = { - width?: number; - height?: number; - text: string; - fontSize?: number; - onPress?: () => void; -}; - -const TextButton = ({ width, height, text, fontSize, onPress }: Props) => { - const scheme = useColorScheme() ?? 'dark'; - return ( - - ); -}; -export default TextButton; diff --git a/components/theme/buttons/Button.tsx b/components/theme/buttons/ThemedButton.tsx similarity index 55% rename from components/theme/buttons/Button.tsx rename to components/theme/buttons/ThemedButton.tsx index 46bf696..fc72cbf 100644 --- a/components/theme/buttons/Button.tsx +++ b/components/theme/buttons/ThemedButton.tsx @@ -1,25 +1,30 @@ -import { StyleSheet, Pressable } from 'react-native'; -import { ThemedView } from '@/components/theme/Theme'; +import React from 'react'; +import { StyleSheet, Pressable, PressableProps } from 'react-native'; +import { ThemedView } from '@/components/theme'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; -import { StyleProp, ViewStyle } from 'react-native'; const DEFAULT_WIDTH = 320; const DEFAULT_HEIGHT = 68; -type Props = { +type ThemedButtonProps = PressableProps & { width?: number; height?: number; - onPress?: () => void; + containerStyle?: object; + buttonStyle?: object; }; -const Button = ({ +const ThemedButton: React.FC = ({ width, height, children, - onPress, -}: Props & React.ComponentProps) => { + containerStyle, + buttonStyle, + style, + ...restProps // This now includes onPress automatically +}) => { const scheme = useColorScheme() ?? 'dark'; + return ( {children} ); }; -export default Button; + +export default ThemedButton; const styles = StyleSheet.create({ buttonContainer: { diff --git a/components/theme/buttons/ThemedTextButton.tsx b/components/theme/buttons/ThemedTextButton.tsx new file mode 100644 index 0000000..8c1e1cb --- /dev/null +++ b/components/theme/buttons/ThemedTextButton.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { TextStyle, PressableProps } from 'react-native'; +import { ThemedButton, ThemedText } from '@/components/theme'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +const DEFAULT_FONT_SIZE = 16; + +// Extend ThemedButton props (which already extends PressableProps) +type ThemedTextButtonProps = Omit & { + width?: number; + height?: number; + text: string; + fontSize?: number; + textStyle?: TextStyle; + containerStyle?: object; + buttonStyle?: object; +}; + +const ThemedTextButton: React.FC = ({ + width, + height, + text, + fontSize, + textStyle, + containerStyle, + buttonStyle, + ...restProps // This includes onPress and all other Pressable props +}) => { + const scheme = useColorScheme() ?? 'dark'; + + return ( + + + {text} + + + ); +}; + +export default ThemedTextButton; diff --git a/components/theme/Theme.tsx b/components/theme/default/ThemedText.tsx similarity index 79% rename from components/theme/Theme.tsx rename to components/theme/default/ThemedText.tsx index 0defec1..7aefcc1 100644 --- a/components/theme/Theme.tsx +++ b/components/theme/default/ThemedText.tsx @@ -1,4 +1,4 @@ -import { View, type ViewProps } from 'react-native'; +import { type ViewProps } from 'react-native'; import { Text, type TextProps, StyleSheet } from 'react-native'; import { useThemeColor } from '@/hooks/useThemeColor'; @@ -12,12 +12,7 @@ export type ThemedTextProps = TextProps & { type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'; }; -export const ThemedView = ({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) => { - const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); - return ; -}; - -export const ThemedText = ({ +const ThemedText = ({ style, lightColor, darkColor, @@ -40,6 +35,7 @@ export const ThemedText = ({ /> ); }; +export default ThemedText; const styles = StyleSheet.create({ default: { diff --git a/components/theme/default/ThemedView.tsx b/components/theme/default/ThemedView.tsx new file mode 100644 index 0000000..411c581 --- /dev/null +++ b/components/theme/default/ThemedView.tsx @@ -0,0 +1,13 @@ +import { View, type ViewProps } from 'react-native'; +import { useThemeColor } from '@/hooks/useThemeColor'; + +export type ThemedViewProps = ViewProps & { + lightColor?: string; + darkColor?: string; +}; + +const ThemedView = ({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) => { + const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); + return ; +}; +export default ThemedView; diff --git a/components/theme/index.tsx b/components/theme/index.tsx new file mode 100644 index 0000000..526ecbd --- /dev/null +++ b/components/theme/index.tsx @@ -0,0 +1,25 @@ +import ThemedText from '@/components/theme/default/ThemedText'; +import ThemedView from '@/components/theme/default/ThemedView'; +import ThemedButton from '@/components/theme/buttons/ThemedButton'; +import ThemedTextButton from '@/components/theme/buttons/ThemedTextButton'; +import ThemedTextInput from '@/components/theme/inputs/ThemedTextInput'; +import ThemedCard from '@/components/theme/ui/ThemedCard'; +import ThemedBadge from '@/components/theme/ui/ThemedBadge'; +import ThemedIcon from '@/components/theme/ui/ThemedIcon'; +import ThemedSwitch from '@/components/theme/inputs/ThemedSwitch'; +import ThemedAvatar from '@/components/theme/ui/ThemedAvatar'; +import ThemedSearchBar from '@/components/theme/inputs/ThemedSearchBar'; + +export { + ThemedText, + ThemedView, + ThemedButton, + ThemedTextButton, + ThemedTextInput, + ThemedCard, + ThemedBadge, + ThemedIcon, + ThemedSwitch, + ThemedAvatar, + ThemedSearchBar, +}; diff --git a/components/theme/inputs/ThemedSearchBar.tsx b/components/theme/inputs/ThemedSearchBar.tsx new file mode 100644 index 0000000..22cf8c8 --- /dev/null +++ b/components/theme/inputs/ThemedSearchBar.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { StyleSheet, ViewProps, View, DimensionValue } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { ThemedTextInput } from '@/components/theme'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +type ThemedSearchBarProps = ViewProps & { + placeholder?: string; + value: string; + onChangeText: (text: string) => void; + width?: DimensionValue; + height?: number; +}; + +const ThemedSearchBar: React.FC = ({ + placeholder = 'Search', + value, + onChangeText, + width = '100%', + height = 40, + style, + ...restProps +}) => { + const scheme = useColorScheme() ?? 'dark'; + + return ( + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + }, + icon: { + position: 'absolute', + zIndex: 1, + left: 15, + }, + input: { + paddingLeft: 45, + }, +}); + +export default ThemedSearchBar; diff --git a/components/theme/inputs/ThemedSwitch.tsx b/components/theme/inputs/ThemedSwitch.tsx new file mode 100644 index 0000000..90ed403 --- /dev/null +++ b/components/theme/inputs/ThemedSwitch.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Switch, SwitchProps } from 'react-native'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +type ThemedSwitchProps = SwitchProps; + +const ThemedSwitch: React.FC = ({ + ...props +}) => { + const scheme = useColorScheme() ?? 'dark'; + + return ( + + ); +}; + +export default ThemedSwitch; diff --git a/components/theme/inputs/ThemedTextInput.tsx b/components/theme/inputs/ThemedTextInput.tsx new file mode 100644 index 0000000..69dc6a7 --- /dev/null +++ b/components/theme/inputs/ThemedTextInput.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { StyleSheet, TextInput, TextInputProps, DimensionValue } from 'react-native'; +import { ThemedView } from '@/components/theme'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +const DEFAULT_WIDTH = 320; +const DEFAULT_HEIGHT = 50; + +type ThemedTextInputProps = TextInputProps & { + width?: DimensionValue; + height?: DimensionValue + containerStyle?: object; +}; + +const ThemedTextInput: React.FC = ({ + width = DEFAULT_WIDTH, + height = DEFAULT_HEIGHT, + containerStyle, + style, + ...restProps +}) => { + const scheme = useColorScheme() ?? 'dark'; + + return ( + + + + ); +}; + +export default ThemedTextInput; + +const styles = StyleSheet.create({ + inputContainer: { + padding: 3, + borderRadius: 10, + }, + input: { + width: '100%', + height: '100%', + borderRadius: 8, + paddingHorizontal: 15, + fontSize: 16, + }, +}); diff --git a/components/theme/ui/ThemedAvatar.tsx b/components/theme/ui/ThemedAvatar.tsx new file mode 100644 index 0000000..86b2299 --- /dev/null +++ b/components/theme/ui/ThemedAvatar.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { StyleSheet, Image, ImageProps, View } from 'react-native'; +import { ThemedText } from '@/components/theme'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +type ThemedAvatarProps = Omit & { + size?: number; + source?: ImageProps['source']; + name?: string; + borderWidth?: number; + borderColor?: string; +}; + +const ThemedAvatar: React.FC = ({ + size = 50, + source, + name, + borderWidth = 0, + borderColor, + style, + ...restProps +}) => { + const scheme = useColorScheme() ?? 'dark'; + const border = borderColor || Colors[scheme].tint; + + // Get initials from name + const getInitials = (name?: string) => { + if (!name) return ''; + return name + .split(' ') + .map(part => part[0]) + .join('') + .toUpperCase() + .substring(0, 2); + }; + + const initials = getInitials(name); + + return ( + + {source ? ( + + ) : ( + + {initials} + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + }, +}); + +export default ThemedAvatar; diff --git a/components/theme/ui/ThemedBadge.tsx b/components/theme/ui/ThemedBadge.tsx new file mode 100644 index 0000000..167b2b7 --- /dev/null +++ b/components/theme/ui/ThemedBadge.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { StyleSheet, ViewProps } from 'react-native'; +import { ThemedView, ThemedText } from '@/components/theme'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +type ThemedBadgeProps = ViewProps & { + value: number | string; + size?: number; + color?: string; + textColor?: string; +}; + +const ThemedBadge: React.FC = ({ + value, + size = 24, + color, + textColor, + style, + ...restProps +}) => { + const scheme = useColorScheme() ?? 'dark'; + const badgeColor = color || Colors[scheme].tint; + const badgeTextColor = textColor || Colors[scheme].background; + + return ( + + + {value} + + + ); +}; + +const styles = StyleSheet.create({ + badge: { + alignItems: 'center', + justifyContent: 'center', + }, + text: { + fontWeight: 'bold', + }, +}); + +export default ThemedBadge; diff --git a/components/theme/ui/ThemedCard.tsx b/components/theme/ui/ThemedCard.tsx new file mode 100644 index 0000000..06e733d --- /dev/null +++ b/components/theme/ui/ThemedCard.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { StyleSheet, ViewProps, Platform, DimensionValue } from 'react-native'; +import { ThemedView } from '@/components/theme'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +type ThemedCardProps = ViewProps & { + width?: DimensionValue; + height?: DimensionValue; + padding?: number; + elevation?: number; + borderRadius?: number; +}; + +const ThemedCard: React.FC = ({ + width = '100%', + height, + padding = 16, + elevation = 3, + borderRadius = 12, + style, + children, + ...restProps +}) => { + const scheme = useColorScheme() ?? 'dark'; + + return ( + + {children} + + ); +}; + +const styles = StyleSheet.create({ + card: { + overflow: 'hidden', + }, +}); + +export default ThemedCard; diff --git a/components/theme/ui/ThemedDivider.tsx b/components/theme/ui/ThemedDivider.tsx new file mode 100644 index 0000000..8d30ce4 --- /dev/null +++ b/components/theme/ui/ThemedDivider.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { StyleSheet, ViewProps, View, DimensionValue } from 'react-native'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +type ThemedDividerProps = ViewProps & { + orientation?: 'horizontal' | 'vertical'; + thickness?: DimensionValue + length?: DimensionValue + color?: string; +}; + +const ThemedDivider: React.FC = ({ + orientation = 'horizontal', + thickness = 1, + length = '100%', + style, + ...restProps +}) => { + const scheme = useColorScheme() ?? 'dark'; + const color = restProps.color || Colors[scheme].border; + + return ( + + ); +}; + +export default ThemedDivider; diff --git a/components/theme/ui/ThemedIcon.tsx b/components/theme/ui/ThemedIcon.tsx new file mode 100644 index 0000000..bf698f6 --- /dev/null +++ b/components/theme/ui/ThemedIcon.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { StyleSheet, ViewProps } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; // Or your preferred icon library +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +type ThemedIconProps = ViewProps & { + name: string; + size?: number; + color?: string; +}; + +const ThemedIcon: React.FC = ({ + name, + size = 24, + color, + style, + ...restProps +}) => { + const scheme = useColorScheme() ?? 'dark'; + const iconColor = color || Colors[scheme].text; + + return ( + + ); +}; + +export default ThemedIcon; diff --git a/constants/Colors.ts b/constants/Colors.ts index 14e6784..2c09e6d 100644 --- a/constants/Colors.ts +++ b/constants/Colors.ts @@ -1,26 +1,50 @@ -/** - * Below are the colors that are used in the app. The colors are defined in the light and dark mode. - * 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. - */ - -const tintColorLight = '#0a7ea4'; +// Dark mode colors +const dark = '#2e2f3d'; const tintColorDark = '#fff'; +const iconColorDark = '#9BA1A6'; + +// Light mode colors +const light = '#ECEDEE'; +const tintColorLight = '#0a7ea4'; +const iconColorLight = '#687076'; export const Colors = { light: { - text: '#11181C', - background: '#fff', + text: dark, + background: light, tint: tintColorLight, - icon: '#687076', - tabIconDefault: '#687076', + icon: iconColorLight, + tabIconDefault: iconColorLight, tabIconSelected: tintColorLight, + + // New colors + card: '#ffffff', + border: '#d0d7de', + notification: '#f85149', + placeholder: '#8b949e', + inactive: '#afb8c1', + subtle: '#f6f8fa', + error: '#e5484d', + success: '#46954a', + warning: '#daaa3f' }, dark: { - text: '#ECEDEE', - background: '#151718', + text: light, + background: dark, tint: tintColorDark, - icon: '#9BA1A6', - tabIconDefault: '#9BA1A6', + icon: iconColorDark, + tabIconDefault: iconColorDark, tabIconSelected: tintColorDark, + + // New colors + card: '#3a3b4a', + border: '#444c56', + notification: '#ff6a69', + placeholder: '#636e7b', + inactive: '#4d5560', + subtle: '#272934', + error: '#ff6369', + success: '#3fb950', + warning: '#d29922' }, };