Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions components/Alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import { View, Text, StyleSheet, ViewStyle } from 'react-native';

export interface AlertProps {
variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
title?: string;
children?: React.ReactNode;
style?: ViewStyle;
}

const VariantStyles = {
default: { bg: '#ffffff', border: '#e5e7eb', text: '#111827', icon: '📝' },
success: { bg: '#f0fdf4', border: '#bbf7d0', text: '#14532d', icon: '✅' },
warning: { bg: '#fffbeb', border: '#fde68a', text: '#78350f', icon: '⚠️' },
error: { bg: '#fef2f2', border: '#fecaca', text: '#7f1d1d', icon: '❌' },
info: { bg: '#eff6ff', border: '#bfdbfe', text: '#1e3a8a', icon: 'ℹ️' },
};

export const Alert: React.FC<AlertProps> = ({ variant = 'default', title, children, style }) => {
const currentVariant = VariantStyles[variant] || VariantStyles.default;

return (
<View style={[styles.container, { backgroundColor: currentVariant.bg, borderColor: currentVariant.border }, style]}>
<Text style={styles.icon}>{currentVariant.icon}</Text>
<View style={styles.content}>{title ? <Text style={[styles.title, { color: currentVariant.text }]}>{title}</Text> : null}{typeof children === 'string' || Array.isArray(children) ? (<Text style={[styles.message, { color: currentVariant.text }]}>{children}</Text>) : children}</View>
</View>
);
};

const styles = StyleSheet.create({
container: {
flexDirection: 'row',
padding: 16,
borderRadius: 8,
borderWidth: 1,
width: '100%',
},
icon: {
fontSize: 18,
marginRight: 12,
marginTop: 2,
},
content: {
flex: 1,
},
title: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
message: {
fontSize: 14,
lineHeight: 20,
opacity: 0.9,
},
});
1 change: 1 addition & 0 deletions components/Alert/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Alert';
97 changes: 97 additions & 0 deletions components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { ReactNode } from 'react';
import { TouchableOpacity, Text, ActivityIndicator, StyleSheet, ViewStyle, TextStyle } from 'react-native';

export interface ButtonProps {
children?: ReactNode;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
fullWidth?: boolean;
isLoading?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
disabled?: boolean;
onPress?: () => void;
style?: ViewStyle;
}

export const BaseButton: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
fullWidth = false,
isLoading = false,
leftIcon,
rightIcon,
disabled = false,
onPress,
style,
}) => {
const containerStyle: ViewStyle[] = [styles.base];
const textStyle: TextStyle[] = [styles.textBase];

// Variants
if (variant === 'primary') {
containerStyle.push(styles.primaryBg);
textStyle.push(styles.primaryText);
} else if (variant === 'secondary') {
containerStyle.push(styles.secondaryBg);
textStyle.push(styles.secondaryText);
} else if (variant === 'outline') {
containerStyle.push(styles.outlineBg);
textStyle.push(styles.outlineText);
} else if (variant === 'danger') {
containerStyle.push(styles.dangerBg);
textStyle.push(styles.dangerText);
} else if (variant === 'ghost') {
containerStyle.push(styles.ghostBg);
textStyle.push(styles.ghostText);
}

// Sizes
if (size === 'sm') containerStyle.push(styles.sizeSm);
else if (size === 'md') containerStyle.push(styles.sizeMd);
else if (size === 'lg') containerStyle.push(styles.sizeLg);

// Layout
if (fullWidth) containerStyle.push(styles.fullWidth);
if (disabled || isLoading) containerStyle.push(styles.disabled);

return (
<TouchableOpacity
style={[containerStyle, style]}
disabled={disabled || isLoading}
onPress={onPress}
activeOpacity={0.7}
>{isLoading ? <ActivityIndicator color={textStyle[1]?.color || '#fff'} style={{ marginRight: 8 }} /> : null}{(!isLoading && leftIcon) ? leftIcon : null}{typeof children === 'string' || Array.isArray(children) ? (<Text style={textStyle}>{children}</Text>) : children}{(!isLoading && rightIcon) ? rightIcon : null}</TouchableOpacity>
);
};

const styles = StyleSheet.create({
base: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 999, // Pill shape from Figma
},
textBase: {
fontWeight: '700',
letterSpacing: -0.5,
},
primaryBg: { backgroundColor: '#10B981' }, // Teal from Figma
primaryText: { color: '#FFFFFF' },
secondaryBg: { backgroundColor: '#F3F4F6' },
secondaryText: { color: '#0F172A' },
outlineBg: { backgroundColor: 'transparent', borderWidth: 2, borderColor: '#10B981' },
outlineText: { color: '#10B981' },
dangerBg: { backgroundColor: '#EF4444' },
dangerText: { color: '#FFFFFF' },
ghostBg: { backgroundColor: 'transparent' },
ghostText: { color: '#64748B' },
sizeSm: { height: 40, paddingHorizontal: 16 },
sizeMd: { height: 56, paddingHorizontal: 24 },
sizeLg: { height: 64, paddingHorizontal: 32 },
fullWidth: { width: '100%' },
disabled: { opacity: 0.5 },
});

export default BaseButton;
36 changes: 36 additions & 0 deletions components/Button/buttonVariants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { cva, type VariantProps } from 'class-variance-authority';

export const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
{
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800 focus-visible:ring-blue-500',
outline: 'border border-gray-300 bg-transparent hover:bg-gray-50 active:bg-gray-100 text-gray-700 focus-visible:ring-gray-500',
ghost: 'bg-transparent hover:bg-gray-100 active:bg-gray-200 text-gray-700 focus-visible:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 active:bg-red-800 focus-visible:ring-red-500',
link: 'bg-transparent underline-offset-4 hover:underline text-blue-600 focus-visible:ring-blue-500',
},
size: {
sm: 'h-8 px-3 text-xs',
md: 'h-10 px-4 py-2',
lg: 'h-12 px-8 text-base',
},
weight: {
normal: 'font-normal',
medium: 'font-medium',
bold: 'font-bold',
},
fullWidth: {
true: 'w-full',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
weight: 'medium',
},
}
);

export type ButtonVariants = VariantProps<typeof buttonVariants>;
2 changes: 2 additions & 0 deletions components/Button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Button';
export { default } from './Button';
33 changes: 33 additions & 0 deletions components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { Text, StyleSheet, TextProps } from 'react-native';

export interface IconProps extends TextProps {
size?: 'sm' | 'md' | 'lg' | number;
}

const sizeMap = {
sm: 16,
md: 20,
lg: 24,
};

export const Icon: React.FC<IconProps> = ({ size = 'md', style, children, ...props }) => {
const computedSize = typeof size === 'number' ? size : sizeMap[size];

return (
<Text
style={[{ fontSize: computedSize }, styles.icon, style]}
{...props}
>
{children}
</Text>
);
};

Icon.displayName = 'Icon';

const styles = StyleSheet.create({
icon: {
// Basic styling for text-based icon representation
}
});
1 change: 1 addition & 0 deletions components/Icon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Icon';
93 changes: 93 additions & 0 deletions components/Input/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { forwardRef, useState } from 'react';
import { View, Text, TextInput as RNTextInput, StyleSheet, TextInputProps as RNTextInputProps, TouchableOpacity } from 'react-native';

export interface TextInputProps extends RNTextInputProps {
label?: string;
error?: string;
}

export const TextInput = forwardRef<RNTextInput, TextInputProps>(
({ label, error, style, secureTextEntry, ...props }, ref) => {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);

// If secureTextEntry is TRUE, we use our toggle logic.
// If it's undefined/false, it's a normal text input.
const isActuallySecure = secureTextEntry && !isPasswordVisible;

return (
<View style={styles.container}>{label && <Text style={styles.label}>{label}</Text>}<View style={styles.inputWrapper}><RNTextInput
ref={ref}
style={[
styles.input,
error ? styles.inputError : styles.inputNormal,
secureTextEntry && { paddingRight: 60 },
style
]}
secureTextEntry={!!isActuallySecure}
placeholderTextColor="#94A3B8"
{...props}
/>{secureTextEntry && (<TouchableOpacity
style={styles.eyeButton}
onPress={() => setIsPasswordVisible(!isPasswordVisible)}
activeOpacity={0.6}
><Text style={styles.eyeText}>{isPasswordVisible ? 'Hide' : 'Show'}</Text></TouchableOpacity>)}</View>{error && <Text style={styles.errorText}>{error}</Text>}</View>
);
}
);

TextInput.displayName = 'TextInput';

const styles = StyleSheet.create({
container: {
width: '100%',
marginBottom: 16,
},
label: {
fontSize: 13,
fontWeight: '700',
color: '#64748B',
marginBottom: 8,
marginLeft: 4,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
inputWrapper: {
width: '100%',
justifyContent: 'center',
},
input: {
height: 56,
width: '100%',
borderWidth: 1.5,
borderRadius: 16,
paddingHorizontal: 16,
fontSize: 15,
color: '#0F172A',
backgroundColor: '#F8FAFC',
},
inputNormal: {
borderColor: '#E2E8F0',
},
inputError: {
borderColor: '#EF4444',
},
eyeButton: {
position: 'absolute',
right: 16,
height: '100%',
justifyContent: 'center',
paddingHorizontal: 8,
},
eyeText: {
color: '#10B981', // Figma Teal
fontSize: 12,
fontWeight: '800',
textTransform: 'uppercase',
},
errorText: {
marginTop: 6,
fontSize: 12,
color: '#EF4444',
marginLeft: 4,
},
});
1 change: 1 addition & 0 deletions components/Input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TextInput';
Loading