@klyftig/mobile-ui 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, ViewStyle } from 'react-native';
3
+ import { AdaptiveGlassView } from './AdaptiveGlassView';
4
+ import { useTheme } from '../hooks/useTheme';
5
+
6
+ interface GlassCardProps {
7
+ children: React.ReactNode;
8
+ style?: ViewStyle;
9
+ intensity?: number;
10
+ tint?: 'light' | 'dark' | 'default';
11
+ padding?: number;
12
+ useGlass?: boolean;
13
+ }
14
+
15
+ export const GlassCard: React.FC<GlassCardProps> = ({
16
+ children,
17
+ style,
18
+ padding = 16,
19
+ useGlass = false,
20
+ }) => {
21
+ const { colors } = useTheme();
22
+
23
+ if (useGlass) {
24
+ return (
25
+ <AdaptiveGlassView
26
+ variant="regular"
27
+ style={[styles.card, { padding }, style]}
28
+ fallbackStyle={{ backgroundColor: colors.background.card, borderColor: colors.border.light }}
29
+ >
30
+ {children}
31
+ </AdaptiveGlassView>
32
+ );
33
+ }
34
+
35
+ return (
36
+ <View
37
+ style={[
38
+ styles.card,
39
+ { backgroundColor: colors.background.card, borderColor: colors.border.light },
40
+ { padding },
41
+ style,
42
+ ]}
43
+ >
44
+ {children}
45
+ </View>
46
+ );
47
+ };
48
+
49
+ const styles = StyleSheet.create({
50
+ card: {
51
+ borderRadius: 20,
52
+ borderWidth: 1,
53
+ shadowColor: '#000',
54
+ shadowOffset: { width: 0, height: 2 },
55
+ shadowOpacity: 0.06,
56
+ shadowRadius: 8,
57
+ elevation: 3,
58
+ },
59
+ });
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, ViewStyle } from 'react-native';
3
+
4
+ interface GlassContainerProps {
5
+ children: React.ReactNode;
6
+ style?: ViewStyle;
7
+ gradientColors?: string[];
8
+ }
9
+
10
+ export const GlassContainer: React.FC<GlassContainerProps> = ({
11
+ children,
12
+ style,
13
+ }) => {
14
+ return (
15
+ <View style={[styles.container, style]}>
16
+ {children}
17
+ </View>
18
+ );
19
+ };
20
+
21
+ const styles = StyleSheet.create({
22
+ container: {
23
+ flex: 1,
24
+ backgroundColor: '#FFFFFF',
25
+ },
26
+ });
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import { AdaptiveGlassView } from './AdaptiveGlassView';
4
+ import { colors } from '../theme/colors';
5
+
6
+ interface GlassHeaderBackgroundProps {
7
+ /** Tint color for the glass material (e.g. 'rgba(0,0,0,0.3)' for dark headers) */
8
+ tintColor?: string;
9
+ }
10
+
11
+ /**
12
+ * Reusable header background using AdaptiveGlassView.
13
+ * Usage: `headerBackground: () => <GlassHeaderBackground />`
14
+ */
15
+ export const GlassHeaderBackground: React.FC<GlassHeaderBackgroundProps> = ({
16
+ tintColor,
17
+ }) => (
18
+ <AdaptiveGlassView
19
+ variant="regular"
20
+ tintColor={tintColor}
21
+ style={styles.fill}
22
+ fallbackStyle={{
23
+ backgroundColor: tintColor || colors.glass.headerFallback,
24
+ }}
25
+ />
26
+ );
27
+
28
+ const styles = StyleSheet.create({
29
+ fill: {
30
+ flex: 1,
31
+ },
32
+ });
@@ -0,0 +1,166 @@
1
+ import React, { useState, useCallback, memo } from 'react';
2
+ import { View, TextInput as RNTextInput, Text, TouchableOpacity, StyleSheet } from 'react-native';
3
+ import { MaterialCommunityIcons as Icon } from '@expo/vector-icons';
4
+ import { useTheme } from '../hooks/useTheme';
5
+
6
+ interface GlassInputProps {
7
+ label: string;
8
+ value: string;
9
+ onChangeText: (text: string) => void;
10
+ placeholder?: string;
11
+ secureTextEntry?: boolean;
12
+ keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad';
13
+ autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
14
+ disabled?: boolean;
15
+ icon?: string;
16
+ showPasswordToggle?: boolean;
17
+ rightText?: string;
18
+ textContentType?: 'none' | 'password' | 'newPassword' | 'emailAddress' | 'username' | 'oneTimeCode';
19
+ autoComplete?: string;
20
+ }
21
+
22
+ const GlassInputComponent: React.FC<GlassInputProps> = ({
23
+ label,
24
+ value,
25
+ onChangeText,
26
+ placeholder,
27
+ secureTextEntry = false,
28
+ keyboardType = 'default',
29
+ autoCapitalize = 'sentences',
30
+ disabled = false,
31
+ icon,
32
+ showPasswordToggle = false,
33
+ rightText,
34
+ textContentType,
35
+ autoComplete,
36
+ }) => {
37
+ const { colors } = useTheme();
38
+ const [isSecure, setIsSecure] = useState(secureTextEntry);
39
+ const [isFocused, setIsFocused] = useState(false);
40
+
41
+ const handleFocus = useCallback(() => {
42
+ setIsFocused(true);
43
+ }, []);
44
+
45
+ const handleBlur = useCallback(() => {
46
+ setIsFocused(false);
47
+ }, []);
48
+
49
+ const toggleSecure = useCallback(() => {
50
+ setIsSecure(prev => !prev);
51
+ }, []);
52
+
53
+ return (
54
+ <View style={styles.container}>
55
+ <Text style={[styles.label, { color: colors.text.primary }]}>{label}</Text>
56
+ <View
57
+ style={[
58
+ styles.inputContainer,
59
+ isFocused && styles.inputContainerFocused,
60
+ disabled && styles.inputContainerDisabled,
61
+ ]}
62
+ >
63
+ <View style={styles.inputWrapper}>
64
+ {icon && (
65
+ <Icon name={icon} size={20} color="#9B9B9B" style={styles.leftIcon} />
66
+ )}
67
+ <RNTextInput
68
+ value={value}
69
+ onChangeText={onChangeText}
70
+ placeholder={placeholder}
71
+ placeholderTextColor="#C5C5C5"
72
+ secureTextEntry={isSecure}
73
+ keyboardType={keyboardType}
74
+ autoCapitalize={autoCapitalize}
75
+ editable={!disabled}
76
+ onFocus={handleFocus}
77
+ onBlur={handleBlur}
78
+ textContentType={textContentType ?? (secureTextEntry ? 'oneTimeCode' : undefined)}
79
+ autoComplete={autoComplete as any}
80
+ accessibilityLabel={label}
81
+ accessibilityHint={placeholder ? `Enter ${label.toLowerCase()}` : undefined}
82
+ accessibilityState={{ disabled }}
83
+ style={[styles.input, icon && styles.inputWithLeftIcon, (rightText || showPasswordToggle) && styles.inputWithRightIcon]}
84
+ />
85
+ {rightText && (
86
+ <Text style={styles.rightText}>{rightText}</Text>
87
+ )}
88
+ {showPasswordToggle && (
89
+ <TouchableOpacity
90
+ onPress={toggleSecure}
91
+ style={styles.eyeIcon}
92
+ activeOpacity={0.7}
93
+ accessibilityRole="button"
94
+ accessibilityLabel={isSecure ? 'Show password' : 'Hide password'}
95
+ >
96
+ <Icon
97
+ name={isSecure ? 'eye-off' : 'eye'}
98
+ size={20}
99
+ color="#9B9B9B"
100
+ />
101
+ </TouchableOpacity>
102
+ )}
103
+ </View>
104
+ </View>
105
+ </View>
106
+ );
107
+ };
108
+
109
+ export const GlassInput = memo(GlassInputComponent);
110
+
111
+ const styles = StyleSheet.create({
112
+ container: {
113
+ marginBottom: 16,
114
+ },
115
+ label: {
116
+ fontSize: 14,
117
+ fontWeight: '600',
118
+ marginBottom: 8,
119
+ marginLeft: 4,
120
+ },
121
+ inputContainer: {
122
+ borderRadius: 16,
123
+ borderWidth: 1,
124
+ backgroundColor: '#F5F5F2',
125
+ borderColor: 'rgba(0,0,0,0.08)',
126
+ },
127
+ inputContainerFocused: {
128
+ borderColor: '#1A1A1A',
129
+ backgroundColor: '#FFFFFF',
130
+ },
131
+ inputContainerDisabled: {
132
+ opacity: 0.6,
133
+ },
134
+ inputWrapper: {
135
+ flexDirection: 'row',
136
+ alignItems: 'center',
137
+ paddingHorizontal: 16,
138
+ height: 56,
139
+ },
140
+ leftIcon: {
141
+ marginRight: 12,
142
+ },
143
+ input: {
144
+ flex: 1,
145
+ fontSize: 16,
146
+ height: '100%',
147
+ paddingVertical: 0,
148
+ color: '#1A1A1A',
149
+ },
150
+ inputWithLeftIcon: {
151
+ marginLeft: 0,
152
+ },
153
+ inputWithRightIcon: {
154
+ paddingRight: 8,
155
+ },
156
+ rightText: {
157
+ fontSize: 16,
158
+ fontWeight: '600',
159
+ marginLeft: 8,
160
+ color: '#6B6B6B',
161
+ },
162
+ eyeIcon: {
163
+ padding: 8,
164
+ marginRight: -8,
165
+ },
166
+ });
@@ -0,0 +1,97 @@
1
+ import React from 'react';
2
+ import { TouchableOpacity, Text, View, StyleSheet } from 'react-native';
3
+ import { MaterialCommunityIcons as Icon } from '@expo/vector-icons';
4
+ import { useTheme } from '../hooks/useTheme';
5
+
6
+ interface GlassListItemProps {
7
+ title: string;
8
+ description?: string;
9
+ leftIcon?: string;
10
+ rightIcon?: string;
11
+ onPress?: () => void;
12
+ style?: any;
13
+ }
14
+
15
+ export const GlassListItem: React.FC<GlassListItemProps> = ({
16
+ title,
17
+ description,
18
+ leftIcon,
19
+ rightIcon,
20
+ onPress,
21
+ style,
22
+ }) => {
23
+ const { colors } = useTheme();
24
+
25
+ const content = (
26
+ <View
27
+ style={[
28
+ styles.listItem,
29
+ { backgroundColor: colors.background.card, borderColor: colors.border.light },
30
+ style,
31
+ ]}
32
+ >
33
+ <View style={styles.listItemContent}>
34
+ {leftIcon && (
35
+ <Icon name={leftIcon} size={24} color={colors.icon.primary} style={styles.leftIcon} />
36
+ )}
37
+ <View style={styles.textContainer}>
38
+ <Text style={[styles.title, { color: colors.text.primary }]}>{title}</Text>
39
+ {description && <Text style={[styles.description, { color: colors.text.secondary }]}>{description}</Text>}
40
+ </View>
41
+ {rightIcon && (
42
+ <Icon name={rightIcon} size={20} color={colors.icon.tertiary} style={styles.rightIcon} />
43
+ )}
44
+ </View>
45
+ </View>
46
+ );
47
+
48
+ if (onPress) {
49
+ return (
50
+ <TouchableOpacity
51
+ onPress={onPress}
52
+ activeOpacity={0.8}
53
+ accessibilityRole="button"
54
+ accessibilityLabel={description ? `${title}, ${description}` : title}
55
+ >
56
+ {content}
57
+ </TouchableOpacity>
58
+ );
59
+ }
60
+
61
+ return content;
62
+ };
63
+
64
+ const styles = StyleSheet.create({
65
+ listItem: {
66
+ borderRadius: 16,
67
+ borderWidth: 1,
68
+ marginBottom: 12,
69
+ shadowColor: '#000',
70
+ shadowOffset: { width: 0, height: 2 },
71
+ shadowOpacity: 0.06,
72
+ shadowRadius: 8,
73
+ elevation: 3,
74
+ },
75
+ listItemContent: {
76
+ flexDirection: 'row',
77
+ alignItems: 'center',
78
+ padding: 16,
79
+ },
80
+ leftIcon: {
81
+ marginRight: 16,
82
+ },
83
+ textContainer: {
84
+ flex: 1,
85
+ },
86
+ title: {
87
+ fontSize: 16,
88
+ fontWeight: '600',
89
+ marginBottom: 4,
90
+ },
91
+ description: {
92
+ fontSize: 14,
93
+ },
94
+ rightIcon: {
95
+ marginLeft: 12,
96
+ },
97
+ });
@@ -0,0 +1,212 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+ import { LinearGradient } from 'expo-linear-gradient';
4
+ import { MaterialCommunityIcons as Icon } from '@expo/vector-icons';
5
+ import { AdaptiveGlassView } from './AdaptiveGlassView';
6
+ import { useTheme } from '../hooks/useTheme';
7
+
8
+ interface GlassStatsCardProps {
9
+ icon: string;
10
+ title: string;
11
+ value: string;
12
+ subtitle?: string;
13
+ gradientColors?: string[];
14
+ progress?: number; // 0-100
15
+ useGlass?: boolean;
16
+ }
17
+
18
+ export const GlassStatsCard: React.FC<GlassStatsCardProps> = ({
19
+ icon,
20
+ title,
21
+ value,
22
+ subtitle,
23
+ gradientColors = ['rgba(255,255,255,0.3)', 'rgba(255,255,255,0.1)'],
24
+ progress,
25
+ useGlass = false,
26
+ }) => {
27
+ const { colors } = useTheme();
28
+
29
+ const cardChildren = (
30
+ <>
31
+ <Icon name={icon} size={32} color={colors.icon.primary} style={styles.icon} />
32
+ <Text style={[styles.title, { color: colors.text.secondary }]}>{title}</Text>
33
+ <Text style={[styles.value, { color: colors.text.primary }]}>{value}</Text>
34
+ {subtitle && <Text style={[styles.subtitle, { color: colors.text.secondary }]}>{subtitle}</Text>}
35
+ {progress !== undefined && (
36
+ <View style={styles.progressContainer}>
37
+ <View style={[styles.progressBar, { width: `${Math.min(progress, 100)}%` }]} />
38
+ </View>
39
+ )}
40
+ </>
41
+ );
42
+
43
+ return (
44
+ <View style={styles.card}>
45
+ {useGlass ? (
46
+ <AdaptiveGlassView
47
+ variant="regular"
48
+ style={styles.gradient}
49
+ fallbackStyle={{ backgroundColor: '#FFFFFF' }}
50
+ >
51
+ {cardChildren}
52
+ </AdaptiveGlassView>
53
+ ) : (
54
+ <LinearGradient
55
+ colors={gradientColors as [string, string, ...string[]]}
56
+ start={{ x: 0, y: 0 }}
57
+ end={{ x: 1, y: 1 }}
58
+ style={styles.gradient}
59
+ >
60
+ {cardChildren}
61
+ </LinearGradient>
62
+ )}
63
+ </View>
64
+ );
65
+ };
66
+
67
+ interface GlassMacroCardProps {
68
+ icon: string;
69
+ label: string;
70
+ value: string;
71
+ gradientColors: string[];
72
+ }
73
+
74
+ export const GlassMacroCard: React.FC<GlassMacroCardProps> = ({
75
+ icon,
76
+ label,
77
+ value,
78
+ gradientColors,
79
+ }) => {
80
+ const { colors } = useTheme();
81
+
82
+ return (
83
+ <View style={[styles.macroCard, { borderColor: colors.border.light }]}>
84
+ <LinearGradient
85
+ colors={gradientColors as [string, string, ...string[]]}
86
+ start={{ x: 0, y: 0 }}
87
+ end={{ x: 1, y: 1 }}
88
+ style={styles.macroGradient}
89
+ >
90
+ <Icon name={icon} size={28} color={colors.icon.primary} />
91
+ <Text style={[styles.macroLabel, { color: colors.text.secondary }]}>{label}</Text>
92
+ <Text style={[styles.macroValue, { color: colors.text.primary }]}>{value}</Text>
93
+ </LinearGradient>
94
+ </View>
95
+ );
96
+ };
97
+
98
+ interface GlassNutrientRowProps {
99
+ icon: string;
100
+ label: string;
101
+ value: string;
102
+ iconColor?: string;
103
+ }
104
+
105
+ export const GlassNutrientRow: React.FC<GlassNutrientRowProps> = ({
106
+ icon,
107
+ label,
108
+ value,
109
+ iconColor = '#6B6B6B',
110
+ }) => {
111
+ const { colors } = useTheme();
112
+
113
+ return (
114
+ <View style={[styles.nutrientRow, { borderBottomColor: colors.border.light }]}>
115
+ <View style={styles.nutrientLeft}>
116
+ <Icon name={icon} size={22} color={iconColor} style={styles.nutrientIcon} />
117
+ <Text style={[styles.nutrientLabel, { color: colors.text.primary }]}>{label}</Text>
118
+ </View>
119
+ <Text style={[styles.nutrientValue, { color: colors.text.primary }]}>{value}</Text>
120
+ </View>
121
+ );
122
+ };
123
+
124
+ const styles = StyleSheet.create({
125
+ card: {
126
+ borderRadius: 20,
127
+ overflow: 'hidden',
128
+ backgroundColor: '#FFFFFF',
129
+ borderWidth: 1,
130
+ borderColor: 'rgba(0,0,0,0.06)',
131
+ shadowColor: '#000',
132
+ shadowOffset: { width: 0, height: 2 },
133
+ shadowOpacity: 0.06,
134
+ shadowRadius: 8,
135
+ elevation: 3,
136
+ marginBottom: 16,
137
+ },
138
+ gradient: {
139
+ padding: 20,
140
+ },
141
+ icon: {
142
+ marginBottom: 12,
143
+ },
144
+ title: {
145
+ fontSize: 16,
146
+ fontWeight: '600',
147
+ marginBottom: 8,
148
+ },
149
+ value: {
150
+ fontSize: 36,
151
+ fontWeight: '800',
152
+ marginBottom: 4,
153
+ },
154
+ subtitle: {
155
+ fontSize: 14,
156
+ marginBottom: 12,
157
+ },
158
+ progressContainer: {
159
+ height: 8,
160
+ borderRadius: 4,
161
+ overflow: 'hidden',
162
+ backgroundColor: 'rgba(0,0,0,0.06)',
163
+ },
164
+ progressBar: {
165
+ height: '100%',
166
+ borderRadius: 4,
167
+ backgroundColor: 'rgba(0,0,0,0.2)',
168
+ },
169
+ macroCard: {
170
+ flex: 1,
171
+ borderRadius: 16,
172
+ overflow: 'hidden',
173
+ borderWidth: 1,
174
+ backgroundColor: '#FFFFFF',
175
+ },
176
+ macroGradient: {
177
+ padding: 16,
178
+ alignItems: 'center',
179
+ },
180
+ macroLabel: {
181
+ fontSize: 12,
182
+ fontWeight: '600',
183
+ marginTop: 8,
184
+ },
185
+ macroValue: {
186
+ fontSize: 22,
187
+ fontWeight: '700',
188
+ marginTop: 4,
189
+ },
190
+ nutrientRow: {
191
+ flexDirection: 'row',
192
+ justifyContent: 'space-between',
193
+ alignItems: 'center',
194
+ paddingVertical: 12,
195
+ borderBottomWidth: 1,
196
+ },
197
+ nutrientLeft: {
198
+ flexDirection: 'row',
199
+ alignItems: 'center',
200
+ },
201
+ nutrientIcon: {
202
+ marginRight: 12,
203
+ },
204
+ nutrientLabel: {
205
+ fontSize: 15,
206
+ fontWeight: '500',
207
+ },
208
+ nutrientValue: {
209
+ fontSize: 16,
210
+ fontWeight: '700',
211
+ },
212
+ });
@@ -0,0 +1,107 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { View, StyleSheet, Animated } from 'react-native';
3
+ import Svg, { Circle, G } from 'react-native-svg';
4
+ import { MaterialCommunityIcons as Icon } from '@expo/vector-icons';
5
+
6
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
7
+
8
+ interface CArcProgressProps {
9
+ size?: number;
10
+ strokeWidth?: number;
11
+ current: number;
12
+ target: number;
13
+ color?: string;
14
+ }
15
+
16
+ /**
17
+ * C-shaped arc progress (270-degree arc with gap at bottom-left).
18
+ * Used as the main calorie ring on the dashboard.
19
+ */
20
+ export const CArcProgress: React.FC<CArcProgressProps> = ({
21
+ size = 120,
22
+ strokeWidth = 10,
23
+ current,
24
+ target,
25
+ color = '#1A1A1A',
26
+ }) => {
27
+ const animatedValue = useRef(new Animated.Value(0)).current;
28
+ const circleRef = useRef<any>(null);
29
+
30
+ const radius = (size - strokeWidth) / 2;
31
+ const circumference = 2 * Math.PI * radius;
32
+ // 270 degrees = 75% of full circle
33
+ const arcLength = circumference * 0.75;
34
+ const percentage = target > 0 ? Math.min((current / target) * 100, 100) : 0;
35
+
36
+ useEffect(() => {
37
+ Animated.timing(animatedValue, {
38
+ toValue: percentage,
39
+ duration: 1000,
40
+ useNativeDriver: true,
41
+ }).start();
42
+
43
+ const listener = animatedValue.addListener((v) => {
44
+ if (circleRef.current) {
45
+ const progressLength = (arcLength * v.value) / 100;
46
+ const dashOffset = arcLength - progressLength;
47
+ circleRef.current.setNativeProps({ strokeDashoffset: dashOffset });
48
+ }
49
+ });
50
+
51
+ return () => {
52
+ animatedValue.removeListener(listener);
53
+ };
54
+ }, [percentage]);
55
+
56
+ return (
57
+ <View style={[styles.container, { width: size, height: size }]}>
58
+ <Svg width={size} height={size}>
59
+ {/* Rotate so the gap is at bottom-left: start from 135deg (7.5 o'clock) */}
60
+ <G rotation="135" origin={`${size / 2}, ${size / 2}`}>
61
+ {/* Background arc */}
62
+ <Circle
63
+ cx={size / 2}
64
+ cy={size / 2}
65
+ r={radius}
66
+ stroke="rgba(0,0,0,0.08)"
67
+ strokeWidth={strokeWidth}
68
+ fill="transparent"
69
+ strokeDasharray={`${arcLength} ${circumference - arcLength}`}
70
+ strokeLinecap="round"
71
+ />
72
+ {/* Progress arc — hidden when current=0 to avoid round-cap artifact */}
73
+ {current > 0 && (
74
+ <AnimatedCircle
75
+ ref={circleRef}
76
+ cx={size / 2}
77
+ cy={size / 2}
78
+ r={radius}
79
+ stroke={color}
80
+ strokeWidth={strokeWidth}
81
+ fill="transparent"
82
+ strokeDasharray={`${arcLength} ${circumference}`}
83
+ strokeDashoffset={arcLength}
84
+ strokeLinecap="round"
85
+ />
86
+ )}
87
+ </G>
88
+ </Svg>
89
+ {/* Center icon */}
90
+ <View style={styles.centerIcon}>
91
+ <Icon name="fire" size={size * 0.22} color="#FF6B35" />
92
+ </View>
93
+ </View>
94
+ );
95
+ };
96
+
97
+ const styles = StyleSheet.create({
98
+ container: {
99
+ alignItems: 'center',
100
+ justifyContent: 'center',
101
+ },
102
+ centerIcon: {
103
+ position: 'absolute',
104
+ alignItems: 'center',
105
+ justifyContent: 'center',
106
+ },
107
+ });