redesign take 1
This commit is contained in:
1 parent
1143f8ca02
commit
e3a3c3fccf
19 files changed
+2783
-1493
No files matched your search
@@ -0,0 +1,124 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, ViewStyle, StyleProp } from "react-native";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
import { fontSize, fontWeight } from "../styles/typography";
|
||||
|
||||
type BadgeVariant =
|
||||
| "neutral"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "danger"
|
||||
| "info"
|
||||
| "primary";
|
||||
type BadgeSize = "sm" | "md";
|
||||
|
||||
interface BadgeProps {
|
||||
label: string;
|
||||
variant?: BadgeVariant;
|
||||
size?: BadgeSize;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Badge - Pill-shaped status indicator
|
||||
*
|
||||
* Variants:
|
||||
* - neutral: Gray badge for general labels
|
||||
* - success: Green badge for positive status
|
||||
* - warning: Orange/yellow badge for warnings
|
||||
* - danger: Red badge for errors or critical status
|
||||
* - info: Blue badge for informational status
|
||||
* - primary: Primary color badge
|
||||
*
|
||||
* Sizes:
|
||||
* - sm: 5px vertical, 10px horizontal, 11px font
|
||||
* - md: 6px vertical, 12px horizontal, 13px font (default)
|
||||
*/
|
||||
export function Badge({
|
||||
label,
|
||||
variant = "neutral",
|
||||
size = "md",
|
||||
style,
|
||||
}: BadgeProps) {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const sizeStyles = {
|
||||
sm: {
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 10,
|
||||
fontSize: fontSize.xs,
|
||||
},
|
||||
md: {
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 12,
|
||||
fontSize: fontSize.sm,
|
||||
},
|
||||
};
|
||||
|
||||
const variantStyles: Record<
|
||||
BadgeVariant,
|
||||
{ backgroundColor: string; color: string }
|
||||
> = {
|
||||
neutral: {
|
||||
backgroundColor: colors.surfaceElevated,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
success: {
|
||||
backgroundColor: `${colors.success}20`, // 20% opacity
|
||||
color: colors.success,
|
||||
},
|
||||
warning: {
|
||||
backgroundColor: `${colors.warning}20`,
|
||||
color: colors.warning,
|
||||
},
|
||||
danger: {
|
||||
backgroundColor: `${colors.danger}20`,
|
||||
color: colors.danger,
|
||||
},
|
||||
info: {
|
||||
backgroundColor: `${colors.info}20`,
|
||||
color: colors.info,
|
||||
},
|
||||
primary: {
|
||||
backgroundColor: `${colors.primary}20`,
|
||||
color: colors.primary,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{
|
||||
backgroundColor: variantStyles[variant].backgroundColor,
|
||||
paddingVertical: sizeStyles[size].paddingVertical,
|
||||
paddingHorizontal: sizeStyles[size].paddingHorizontal,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.label,
|
||||
{
|
||||
color: variantStyles[variant].color,
|
||||
fontSize: sizeStyles[size].fontSize,
|
||||
fontWeight: fontWeight.medium,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
badge: {
|
||||
borderRadius: 999, // Full pill shape
|
||||
alignSelf: "flex-start",
|
||||
},
|
||||
label: {
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
@@ -1,143 +1,129 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, TouchableOpacity, Platform, Dimensions } from 'react-native';
|
||||
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { theme } from '../styles/theme';
|
||||
import { Animated } from 'react-native';
|
||||
import React from "react";
|
||||
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
||||
import { BottomTabBarProps } from "@react-navigation/bottom-tabs";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
/**
|
||||
* CustomTabBar - Minimal bottom navigation with pill indicator
|
||||
*
|
||||
* Design:
|
||||
* - Simple flat design (no floating, no glassmorphism)
|
||||
* - Clean icons with outline/filled states
|
||||
* - Small pill indicator below active tab
|
||||
* - 56px height (reduced from 70px)
|
||||
* - No animations (just opacity fade on press)
|
||||
* - Theme-aware colors
|
||||
*/
|
||||
export function CustomTabBar({
|
||||
state,
|
||||
descriptors,
|
||||
navigation,
|
||||
}: BottomTabBarProps) {
|
||||
const { colors } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
export function CustomTabBar({ state, descriptors, navigation }: BottomTabBarProps) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={['rgba(255, 255, 255, 0.9)', 'rgba(255, 255, 255, 0.7)']}
|
||||
style={[styles.tabBar, theme.shadows.medium]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
>
|
||||
{state.routes.map((route, index) => {
|
||||
const { options } = descriptors[route.key];
|
||||
const isFocused = state.index === index;
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: colors.surface,
|
||||
borderTopColor: colors.border,
|
||||
paddingBottom: insets.bottom,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{state.routes.map((route, index) => {
|
||||
const { options } = descriptors[route.key];
|
||||
const isFocused = state.index === index;
|
||||
|
||||
const onPress = () => {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
const onPress = () => {
|
||||
const event = navigation.emit({
|
||||
type: "tabPress",
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
||||
if (!isFocused && !event.defaultPrevented) {
|
||||
navigation.navigate(route.name);
|
||||
}
|
||||
};
|
||||
if (!isFocused && !event.defaultPrevented) {
|
||||
navigation.navigate(route.name);
|
||||
}
|
||||
};
|
||||
|
||||
const getIconName = (routeName: string, focused: boolean): keyof typeof Ionicons.glyphMap => {
|
||||
switch (routeName) {
|
||||
case 'index':
|
||||
return focused ? 'home' : 'home-outline';
|
||||
case 'goals':
|
||||
return focused ? 'trophy' : 'trophy-outline';
|
||||
case 'attendance':
|
||||
return focused ? 'calendar' : 'calendar-outline';
|
||||
case 'recommendations':
|
||||
return focused ? 'sparkles' : 'sparkles-outline';
|
||||
case 'profile':
|
||||
return focused ? 'person' : 'person-outline';
|
||||
default:
|
||||
return 'ellipse-outline';
|
||||
}
|
||||
};
|
||||
const getIconName = (
|
||||
routeName: string,
|
||||
focused: boolean,
|
||||
): keyof typeof Ionicons.glyphMap => {
|
||||
switch (routeName) {
|
||||
case "index":
|
||||
return focused ? "home" : "home-outline";
|
||||
case "goals":
|
||||
return focused ? "trophy" : "trophy-outline";
|
||||
case "attendance":
|
||||
return focused ? "calendar" : "calendar-outline";
|
||||
case "recommendations":
|
||||
return focused ? "sparkles" : "sparkles-outline";
|
||||
case "profile":
|
||||
return focused ? "person" : "person-outline";
|
||||
default:
|
||||
return "ellipse-outline";
|
||||
}
|
||||
};
|
||||
|
||||
// Animation for scale
|
||||
const scaleValue = React.useRef(new Animated.Value(1)).current;
|
||||
|
||||
React.useEffect(() => {
|
||||
Animated.spring(scaleValue, {
|
||||
toValue: isFocused ? 1.2 : 1,
|
||||
useNativeDriver: true,
|
||||
friction: 8,
|
||||
}).start();
|
||||
}, [isFocused]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={isFocused ? { selected: true } : {}}
|
||||
accessibilityLabel={options.tabBarAccessibilityLabel}
|
||||
testID={(options as any).tabBarTestID}
|
||||
onPress={onPress}
|
||||
style={styles.tabItem}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Animated.View style={{ transform: [{ scale: scaleValue }] }}>
|
||||
{isFocused ? (
|
||||
<LinearGradient
|
||||
colors={theme.gradients.primary}
|
||||
style={styles.iconContainer}
|
||||
>
|
||||
<Ionicons
|
||||
name={getIconName(route.name, true)}
|
||||
size={20}
|
||||
color="#fff"
|
||||
/>
|
||||
</LinearGradient>
|
||||
) : (
|
||||
<Ionicons
|
||||
name={getIconName(route.name, false)}
|
||||
size={24}
|
||||
color={theme.colors.gray500}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={isFocused ? { selected: true } : {}}
|
||||
accessibilityLabel={options.tabBarAccessibilityLabel}
|
||||
testID={(options as any).tabBarTestID}
|
||||
onPress={onPress}
|
||||
style={styles.tabItem}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.iconWrapper}>
|
||||
<Ionicons
|
||||
name={getIconName(route.name, isFocused)}
|
||||
size={24}
|
||||
color={isFocused ? colors.primary : colors.textTertiary}
|
||||
/>
|
||||
{isFocused && (
|
||||
<View
|
||||
style={[
|
||||
styles.indicator,
|
||||
{ backgroundColor: colors.primary },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
paddingBottom: Platform.OS === 'ios' ? 30 : 20,
|
||||
pointerEvents: 'box-none',
|
||||
},
|
||||
tabBar: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
borderRadius: 35,
|
||||
height: 70,
|
||||
width: width - 40,
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
tabItem: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
iconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: theme.colors.primary,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
height: 56,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
tabItem: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
},
|
||||
iconWrapper: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
indicator: {
|
||||
width: 24,
|
||||
height: 3,
|
||||
borderRadius: 999,
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from "react";
|
||||
import { View, StyleSheet, ViewStyle, StyleProp } from "react-native";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
type IconContainerVariant = "plain" | "subtle" | "colored";
|
||||
type IconContainerSize = "sm" | "md" | "lg";
|
||||
|
||||
interface IconContainerProps {
|
||||
children: React.ReactNode;
|
||||
variant?: IconContainerVariant;
|
||||
size?: IconContainerSize;
|
||||
backgroundColor?: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
/**
|
||||
* IconContainer - Clean container for icons with subtle backgrounds
|
||||
*
|
||||
* Variants:
|
||||
* - plain: No background, just the icon
|
||||
* - subtle: Light background (surfaceSecondary)
|
||||
* - colored: Custom background color (pass backgroundColor prop)
|
||||
*
|
||||
* Sizes:
|
||||
* - sm: 32px circle
|
||||
* - md: 40px circle (default)
|
||||
* - lg: 48px circle
|
||||
*/
|
||||
export function IconContainer({
|
||||
children,
|
||||
variant = "subtle",
|
||||
size = "md",
|
||||
backgroundColor,
|
||||
style,
|
||||
}: IconContainerProps) {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const sizeStyles: Record<IconContainerSize, ViewStyle> = {
|
||||
sm: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
},
|
||||
md: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
},
|
||||
lg: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
},
|
||||
};
|
||||
|
||||
const getBackgroundColor = (): string | undefined => {
|
||||
if (variant === "colored" && backgroundColor) {
|
||||
return backgroundColor;
|
||||
}
|
||||
if (variant === "subtle") {
|
||||
return colors.surfaceElevated;
|
||||
}
|
||||
return "transparent";
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
sizeStyles[size],
|
||||
{ backgroundColor: getBackgroundColor() },
|
||||
style,
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
ViewStyle,
|
||||
StyleProp,
|
||||
} from "react-native";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
interface ListItemProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightElement?: React.ReactNode;
|
||||
onPress?: () => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
/**
|
||||
* ListItem - Clean list item with optional icon and trailing element
|
||||
*
|
||||
* Usage:
|
||||
* - Settings lists
|
||||
* - Navigation items
|
||||
* - Selectable options
|
||||
*
|
||||
* Features:
|
||||
* - Optional left icon (wrap in IconContainer)
|
||||
* - Optional subtitle for additional context
|
||||
* - Optional right element (arrow, badge, switch, etc.)
|
||||
* - Optional onPress for touchable items
|
||||
*/
|
||||
export function ListItem({
|
||||
title,
|
||||
subtitle,
|
||||
leftIcon,
|
||||
rightElement,
|
||||
onPress,
|
||||
style,
|
||||
}: ListItemProps) {
|
||||
const { colors, typography } = useTheme();
|
||||
|
||||
const content = (
|
||||
<View style={[styles.container, style]}>
|
||||
{leftIcon && <View style={styles.leftIcon}>{leftIcon}</View>}
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={[typography.h3, { color: colors.textPrimary }]}>
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle && (
|
||||
<Text
|
||||
style={[
|
||||
typography.caption,
|
||||
{ color: colors.textTertiary, marginTop: 2 },
|
||||
]}
|
||||
>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{rightElement && <View style={styles.rightElement}>{rightElement}</View>}
|
||||
</View>
|
||||
);
|
||||
|
||||
if (onPress) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
|
||||
{content}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
leftIcon: {
|
||||
marginRight: 12,
|
||||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
rightElement: {
|
||||
marginLeft: 12,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import React from "react";
|
||||
import {
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ActivityIndicator,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
StyleProp,
|
||||
} from "react-native";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
import { fontSize, fontWeight } from "../styles/typography";
|
||||
|
||||
type ButtonVariant = "primary" | "secondary" | "tertiary" | "danger";
|
||||
type ButtonSize = "sm" | "md" | "lg";
|
||||
|
||||
interface MinimalButtonProps {
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
/**
|
||||
* MinimalButton - Clean button component with solid colors
|
||||
*
|
||||
* Variants:
|
||||
* - primary: Solid primary background
|
||||
* - secondary: Outlined with primary color
|
||||
* - tertiary: Text only, no background
|
||||
* - danger: Solid danger background
|
||||
*
|
||||
* Sizes:
|
||||
* - sm: 12px vertical padding, 16px horizontal
|
||||
* - md: 14px vertical padding, 24px horizontal (default)
|
||||
* - lg: 16px vertical padding, 32px horizontal
|
||||
*/
|
||||
export function MinimalButton({
|
||||
title,
|
||||
onPress,
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
loading = false,
|
||||
disabled = false,
|
||||
style,
|
||||
textStyle,
|
||||
}: MinimalButtonProps) {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const isDisabled = disabled || loading;
|
||||
|
||||
// Get button styles based on variant
|
||||
const getButtonStyle = (): ViewStyle => {
|
||||
const baseStyle: ViewStyle = {
|
||||
borderRadius: 10,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
opacity: isDisabled ? 0.5 : 1,
|
||||
};
|
||||
|
||||
// Size-specific padding
|
||||
const sizeStyles: Record<
|
||||
ButtonSize,
|
||||
{ paddingVertical: number; paddingHorizontal: number }
|
||||
> = {
|
||||
sm: { paddingVertical: 12, paddingHorizontal: 16 },
|
||||
md: { paddingVertical: 14, paddingHorizontal: 24 },
|
||||
lg: { paddingVertical: 16, paddingHorizontal: 32 },
|
||||
};
|
||||
|
||||
const variantStyles: Record<ButtonVariant, ViewStyle> = {
|
||||
primary: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
secondary: {
|
||||
backgroundColor: "transparent",
|
||||
borderWidth: 1.5,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
tertiary: {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
danger: {
|
||||
backgroundColor: colors.danger,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...baseStyle,
|
||||
...sizeStyles[size],
|
||||
...variantStyles[variant],
|
||||
};
|
||||
};
|
||||
|
||||
// Get text styles based on variant
|
||||
const getTextStyle = (): TextStyle => {
|
||||
const baseTextStyle: TextStyle = {
|
||||
fontSize: fontSize.base,
|
||||
fontWeight: fontWeight.semibold,
|
||||
};
|
||||
|
||||
const variantTextStyles: Record<ButtonVariant, TextStyle> = {
|
||||
primary: {
|
||||
color: colors.white,
|
||||
},
|
||||
secondary: {
|
||||
color: colors.primary,
|
||||
},
|
||||
tertiary: {
|
||||
color: colors.primary,
|
||||
},
|
||||
danger: {
|
||||
color: colors.white,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...baseTextStyle,
|
||||
...variantTextStyles[variant],
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[getButtonStyle(), style]}
|
||||
onPress={onPress}
|
||||
disabled={isDisabled}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={
|
||||
variant === "secondary" || variant === "tertiary"
|
||||
? colors.primary
|
||||
: colors.white
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Text style={[getTextStyle(), textStyle]}>{title}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
// No static styles needed - all dynamic based on theme
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ViewStyle,
|
||||
StyleProp,
|
||||
} from "react-native";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
type CardVariant = "default" | "elevated" | "bordered";
|
||||
|
||||
interface MinimalCardProps {
|
||||
children: React.ReactNode;
|
||||
variant?: CardVariant;
|
||||
onPress?: () => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
/**
|
||||
* MinimalCard - Clean card component without gradients
|
||||
*
|
||||
* Variants:
|
||||
* - default: Subtle shadow on surface background
|
||||
* - elevated: More prominent shadow
|
||||
* - bordered: Border instead of shadow
|
||||
*/
|
||||
export function MinimalCard({
|
||||
children,
|
||||
variant = "default",
|
||||
onPress,
|
||||
style,
|
||||
}: MinimalCardProps) {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const cardStyles = [
|
||||
styles.base,
|
||||
{
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
variant === "default" && styles.default,
|
||||
variant === "elevated" && styles.elevated,
|
||||
variant === "bordered" && {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
style,
|
||||
];
|
||||
|
||||
if (onPress) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={cardStyles}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return <View style={cardStyles}>{children}</View>;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
},
|
||||
default: {
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 3,
|
||||
elevation: 1,
|
||||
},
|
||||
elevated: {
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 8,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from "react";
|
||||
import { View, StyleSheet, ViewStyle, StyleProp } from "react-native";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
interface ProgressBarProps {
|
||||
progress: number; // 0-1 (e.g., 0.75 for 75%)
|
||||
color?: string;
|
||||
backgroundColor?: string;
|
||||
height?: number;
|
||||
borderRadius?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
/**
|
||||
* ProgressBar - Simple linear progress indicator
|
||||
*
|
||||
* Usage:
|
||||
* - Goal progress tracking
|
||||
* - Loading states
|
||||
* - Completion indicators
|
||||
*
|
||||
* Props:
|
||||
* - progress: Value between 0 and 1 (e.g., 0.75 for 75%)
|
||||
* - color: Custom fill color (defaults to theme primary)
|
||||
* - backgroundColor: Custom track color (defaults to theme border)
|
||||
* - height: Bar height in pixels (defaults to 8)
|
||||
* - borderRadius: Corner radius (defaults to 999 for full pill shape)
|
||||
*/
|
||||
export function ProgressBar({
|
||||
progress,
|
||||
color,
|
||||
backgroundColor,
|
||||
height = 8,
|
||||
borderRadius = 999,
|
||||
style,
|
||||
}: ProgressBarProps) {
|
||||
const { colors } = useTheme();
|
||||
|
||||
// Clamp progress between 0 and 1
|
||||
const clampedProgress = Math.min(Math.max(progress, 0), 1);
|
||||
|
||||
const trackColor = backgroundColor || colors.border;
|
||||
const fillColor = color || colors.primary;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.track,
|
||||
{
|
||||
height,
|
||||
borderRadius,
|
||||
backgroundColor: trackColor,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.fill,
|
||||
{
|
||||
width: `${clampedProgress * 100}%`,
|
||||
height,
|
||||
borderRadius,
|
||||
backgroundColor: fillColor,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
track: {
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
},
|
||||
fill: {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
ViewStyle,
|
||||
StyleProp,
|
||||
} from "react-native";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
interface SectionHeaderProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
actionLabel?: string;
|
||||
onActionPress?: () => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
/**
|
||||
* SectionHeader - Clean section header with optional action button
|
||||
*
|
||||
* Usage:
|
||||
* - Divides content into logical sections
|
||||
* - Optional subtitle for additional context
|
||||
* - Optional action button (e.g., "See All", "Add New")
|
||||
*/
|
||||
export function SectionHeader({
|
||||
title,
|
||||
subtitle,
|
||||
actionLabel,
|
||||
onActionPress,
|
||||
style,
|
||||
}: SectionHeaderProps) {
|
||||
const { colors, typography } = useTheme();
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={[typography.h2, { color: colors.textPrimary }]}>
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle && (
|
||||
<Text
|
||||
style={[
|
||||
typography.caption,
|
||||
{ color: colors.textTertiary, marginTop: 2 },
|
||||
]}
|
||||
>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{actionLabel && onActionPress && (
|
||||
<TouchableOpacity onPress={onActionPress} activeOpacity={0.7}>
|
||||
<Text
|
||||
style={[
|
||||
typography.body,
|
||||
{ color: colors.primary, fontWeight: "600" },
|
||||
]}
|
||||
>
|
||||
{actionLabel}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 12,
|
||||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user