This commit is contained in:
jjbrunton committed 2025-12-28 16:51:32 +00:00
1 parent 4d53d83769
commit 6ff5bc62f2
17 files changed
+2599 -1136

No files matched your search

@@ -0,0 +1,238 @@
import React from 'react';
import { StyleSheet, Text, Pressable, ViewStyle, TextStyle, ActivityIndicator, Platform } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
import { colors, gradients, radius, shadows, typography, animations } from '../../theme';
// Haptics is not supported on web
const triggerHaptic = async () => {
if (Platform.OS === 'web') return;
const Haptics = await import('expo-haptics');
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
type ButtonSize = 'sm' | 'md' | 'lg';
interface GlassButtonProps {
children: React.ReactNode;
onPress?: () => void;
variant?: ButtonVariant;
size?: ButtonSize;
disabled?: boolean;
loading?: boolean;
fullWidth?: boolean;
style?: ViewStyle;
textStyle?: TextStyle;
haptic?: boolean;
icon?: React.ReactNode;
}
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export function GlassButton({
children,
onPress,
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
fullWidth = false,
style,
textStyle,
haptic = true,
icon,
}: GlassButtonProps) {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const handlePressIn = () => {
scale.value = withSpring(0.96, animations.spring);
};
const handlePressOut = () => {
scale.value = withSpring(1, animations.spring);
};
const handlePress = async () => {
if (disabled || loading) return;
if (haptic) {
await triggerHaptic();
}
onPress?.();
};
const sizeStyles = sizes[size];
const variantStyles = variants[variant];
const content = (
<>
{loading ? (
<ActivityIndicator
color={variantStyles.textColor}
size="small"
style={styles.loader}
/>
) : (
<>
{icon && <Animated.View style={styles.icon}>{icon}</Animated.View>}
<Text
style={[
styles.text,
sizeStyles.text,
{ color: variantStyles.textColor },
textStyle,
]}
>
{children}
</Text>
</>
)}
</>
);
return (
<AnimatedPressable
onPress={handlePress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
disabled={disabled || loading}
style={[
animatedStyle,
fullWidth && styles.fullWidth,
disabled && styles.disabled,
]}
>
{variant === 'primary' || variant === 'danger' ? (
<LinearGradient
colors={variantStyles.gradient as [string, string]}
style={[
styles.button,
sizeStyles.button,
shadows.md,
style,
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
{content}
</LinearGradient>
) : (
<Animated.View
style={[
styles.button,
sizeStyles.button,
{
backgroundColor: variantStyles.background,
borderWidth: variantStyles.borderWidth,
borderColor: variantStyles.borderColor,
},
variant === 'secondary' && shadows.sm,
style,
]}
>
{content}
</Animated.View>
)}
</AnimatedPressable>
);
}
const variants = {
primary: {
gradient: gradients.primary,
textColor: colors.text,
background: undefined,
borderWidth: 0,
borderColor: undefined,
},
secondary: {
gradient: undefined,
textColor: colors.text,
background: colors.glass.background,
borderWidth: 1,
borderColor: colors.glass.border,
},
ghost: {
gradient: undefined,
textColor: colors.primary,
background: 'transparent',
borderWidth: 0,
borderColor: undefined,
},
danger: {
gradient: gradients.error,
textColor: colors.text,
background: undefined,
borderWidth: 0,
borderColor: undefined,
},
};
const sizes = {
sm: {
button: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: radius.md,
},
text: {
fontSize: 14,
fontWeight: '600' as const,
},
},
md: {
button: {
paddingHorizontal: 24,
paddingVertical: 14,
borderRadius: radius.md,
},
text: {
fontSize: 16,
fontWeight: '600' as const,
},
},
lg: {
button: {
paddingHorizontal: 32,
paddingVertical: 18,
borderRadius: radius.lg,
},
text: {
fontSize: 18,
fontWeight: '600' as const,
},
},
};
const styles = StyleSheet.create({
button: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
text: {
textAlign: 'center',
},
fullWidth: {
width: '100%',
},
disabled: {
opacity: 0.5,
},
loader: {
marginHorizontal: 8,
},
icon: {
marginRight: 8,
},
});
export default GlassButton;
@@ -0,0 +1,98 @@
import React from 'react';
import { StyleSheet, View, ViewStyle, Platform } from 'react-native';
import { BlurView } from 'expo-blur';
import { LinearGradient } from 'expo-linear-gradient';
import { colors, radius, shadows, blur } from '../../theme';
interface GlassCardProps {
children: React.ReactNode;
style?: ViewStyle;
intensity?: 'light' | 'medium' | 'heavy';
variant?: 'default' | 'elevated' | 'subtle';
noPadding?: boolean;
}
export function GlassCard({
children,
style,
intensity = 'medium',
variant = 'default',
noPadding = false,
}: GlassCardProps) {
const blurIntensity = blur[intensity];
const shadowStyle = variant === 'elevated' ? shadows.lg : variant === 'subtle' ? shadows.sm : shadows.md;
const borderColor = variant === 'elevated' ? colors.glass.borderLight : colors.glass.border;
// Use BlurView on iOS, fallback to solid background on Android for performance
const useBlur = Platform.OS === 'ios';
return (
<View style={[styles.container, shadowStyle, style]}>
{useBlur ? (
<BlurView
intensity={blurIntensity}
tint="dark"
style={[styles.blurContainer, { borderColor }]}
>
{/* Top highlight gradient */}
<LinearGradient
colors={[colors.glass.highlight, 'transparent']}
style={styles.highlight}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
/>
<View style={[styles.content, noPadding && styles.noPadding]}>
{children}
</View>
</BlurView>
) : (
// Android fallback with solid background
<View style={[styles.fallbackContainer, { borderColor }]}>
<LinearGradient
colors={[colors.glass.highlight, 'transparent']}
style={styles.highlight}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
/>
<View style={[styles.content, noPadding && styles.noPadding]}>
{children}
</View>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
borderRadius: radius.lg,
overflow: 'hidden',
},
blurContainer: {
overflow: 'hidden',
borderRadius: radius.lg,
borderWidth: 1,
backgroundColor: colors.glass.background,
},
fallbackContainer: {
overflow: 'hidden',
borderRadius: radius.lg,
borderWidth: 1,
backgroundColor: colors.glass.backgroundLight,
},
highlight: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 60,
},
content: {
padding: 16,
},
noPadding: {
padding: 0,
},
});
export default GlassCard;
@@ -0,0 +1,140 @@
import React, { useState } from 'react';
import { StyleSheet, TextInput, View, Text, TextInputProps, ViewStyle, Platform } from 'react-native';
import { BlurView } from 'expo-blur';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
interpolateColor,
} from 'react-native-reanimated';
import { colors, radius, blur, animations, typography } from '../../theme';
interface GlassInputProps extends Omit<TextInputProps, 'style'> {
label?: string;
error?: string;
containerStyle?: ViewStyle;
icon?: React.ReactNode;
}
const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
const AnimatedView = Animated.View;
export function GlassInput({
label,
error,
containerStyle,
icon,
...props
}: GlassInputProps) {
const [isFocused, setIsFocused] = useState(false);
const focusAnim = useSharedValue(0);
const handleFocus = (e: any) => {
setIsFocused(true);
focusAnim.value = withTiming(1, { duration: animations.timing.fast });
props.onFocus?.(e);
};
const handleBlur = (e: any) => {
setIsFocused(false);
focusAnim.value = withTiming(0, { duration: animations.timing.fast });
props.onBlur?.(e);
};
const animatedBorderStyle = useAnimatedStyle(() => ({
borderColor: interpolateColor(
focusAnim.value,
[0, 1],
[error ? colors.error : colors.glass.border, error ? colors.error : colors.primary]
),
}));
const useBlur = Platform.OS === 'ios';
const inputContent = (
<View style={styles.inputWrapper}>
{icon && <View style={styles.icon}>{icon}</View>}
<TextInput
{...props}
style={[styles.input, icon ? styles.inputWithIcon : undefined]}
placeholderTextColor={colors.textTertiary}
onFocus={handleFocus}
onBlur={handleBlur}
selectionColor={colors.primary}
/>
</View>
);
return (
<View style={[styles.container, containerStyle]}>
{label && <Text style={styles.label}>{label}</Text>}
{useBlur ? (
<AnimatedView style={[styles.blurWrapper, animatedBorderStyle]}>
<BlurView
intensity={blur.light}
tint="dark"
style={styles.blurContainer}
>
{inputContent}
</BlurView>
</AnimatedView>
) : (
<AnimatedView style={[styles.fallbackContainer, animatedBorderStyle]}>
{inputContent}
</AnimatedView>
)}
{error && <Text style={styles.error}>{error}</Text>}
</View>
);
}
const styles = StyleSheet.create({
container: {
marginBottom: 16,
},
label: {
...typography.subhead,
color: colors.textSecondary,
marginBottom: 8,
marginLeft: 4,
},
blurWrapper: {
borderRadius: radius.md,
borderWidth: 1,
overflow: 'hidden',
},
blurContainer: {
backgroundColor: colors.glass.background,
},
fallbackContainer: {
borderRadius: radius.md,
borderWidth: 1,
backgroundColor: colors.glass.backgroundLight,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
},
icon: {
paddingLeft: 16,
},
input: {
flex: 1,
padding: 16,
fontSize: 16,
color: colors.text,
},
inputWithIcon: {
paddingLeft: 12,
},
error: {
...typography.caption1,
color: colors.error,
marginTop: 6,
marginLeft: 4,
},
});
export default GlassInput;
@@ -0,0 +1,71 @@
import React from 'react';
import { StyleSheet, View, ViewStyle } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { colors, gradients } from '../../theme';
interface GradientBackgroundProps {
children: React.ReactNode;
variant?: 'default' | 'reverse' | 'subtle';
style?: ViewStyle;
showAccent?: boolean;
}
export function GradientBackground({
children,
variant = 'default',
style,
showAccent = false,
}: GradientBackgroundProps) {
const gradientColors = variant === 'reverse'
? gradients.backgroundReverse
: gradients.background;
return (
<View style={[styles.container, style]}>
<LinearGradient
colors={gradientColors as [string, string]}
style={StyleSheet.absoluteFill}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
/>
{showAccent && (
<>
{/* Top-right accent glow */}
<View style={styles.accentTopRight} />
{/* Bottom-left accent glow */}
<View style={styles.accentBottomLeft} />
</>
)}
{children}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
accentTopRight: {
position: 'absolute',
top: -100,
right: -100,
width: 300,
height: 300,
borderRadius: 150,
backgroundColor: colors.primaryGlow,
opacity: 0.15,
},
accentBottomLeft: {
position: 'absolute',
bottom: -50,
left: -100,
width: 250,
height: 250,
borderRadius: 125,
backgroundColor: colors.primaryGlow,
opacity: 0.1,
},
});
export default GradientBackground;
+4
View File
@@ -0,0 +1,4 @@
export { GradientBackground } from './GradientBackground';
export { GlassCard } from './GlassCard';
export { GlassButton } from './GlassButton';
export { GlassInput } from './GlassInput';