@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,157 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { View, Text, StyleSheet, Animated } from 'react-native';
3
+ import Svg, { Circle, G } from 'react-native-svg';
4
+ import { useTheme } from '../../hooks/useTheme';
5
+
6
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
7
+
8
+ interface CircularProgressProps {
9
+ size?: number;
10
+ strokeWidth?: number;
11
+ current: number;
12
+ target: number;
13
+ color?: string;
14
+ label?: string;
15
+ unit?: string;
16
+ showPercentage?: boolean;
17
+ showRemaining?: boolean;
18
+ subtitle?: string;
19
+ valueSize?: number;
20
+ subtitleSize?: number;
21
+ hideText?: boolean;
22
+ }
23
+
24
+ export const CircularProgress: React.FC<CircularProgressProps> = ({
25
+ size = 140,
26
+ strokeWidth = 12,
27
+ current,
28
+ target,
29
+ color,
30
+ label,
31
+ unit = '',
32
+ showPercentage = false,
33
+ showRemaining = false,
34
+ subtitle,
35
+ valueSize,
36
+ subtitleSize,
37
+ hideText = false,
38
+ }) => {
39
+ const { colors } = useTheme();
40
+ const styles = createStyles(colors);
41
+ const resolvedColor = color ?? colors.text.primary;
42
+
43
+ const animatedValue = useRef(new Animated.Value(0)).current;
44
+ const circleRef = useRef<any>(null);
45
+
46
+ const radius = (size - strokeWidth) / 2;
47
+ const circumference = 2 * Math.PI * radius;
48
+ const percentage = target > 0 ? Math.min((current / target) * 100, 100) : 0;
49
+
50
+ useEffect(() => {
51
+ Animated.timing(animatedValue, {
52
+ toValue: percentage,
53
+ duration: 1000,
54
+ useNativeDriver: true,
55
+ }).start();
56
+
57
+ animatedValue.addListener((v) => {
58
+ if (circleRef.current) {
59
+ const strokeDashoffset = circumference - (circumference * v.value) / 100;
60
+ circleRef.current.setNativeProps({
61
+ strokeDashoffset,
62
+ });
63
+ }
64
+ });
65
+
66
+ return () => {
67
+ animatedValue.removeAllListeners();
68
+ };
69
+ }, [percentage]);
70
+
71
+ return (
72
+ <View style={[styles.container, { width: size, height: size }]}>
73
+ <Svg width={size} height={size}>
74
+ <G rotation="-90" origin={`${size / 2}, ${size / 2}`}>
75
+ {/* Background circle */}
76
+ <Circle
77
+ cx={size / 2}
78
+ cy={size / 2}
79
+ r={radius}
80
+ stroke="rgba(0,0,0,0.08)"
81
+ strokeWidth={strokeWidth}
82
+ fill="transparent"
83
+ />
84
+ {/* Progress circle */}
85
+ <AnimatedCircle
86
+ ref={circleRef}
87
+ cx={size / 2}
88
+ cy={size / 2}
89
+ r={radius}
90
+ stroke={resolvedColor}
91
+ strokeWidth={strokeWidth}
92
+ strokeDasharray={circumference}
93
+ strokeDashoffset={circumference}
94
+ fill="transparent"
95
+ strokeLinecap="round"
96
+ />
97
+ </G>
98
+ </Svg>
99
+
100
+ {!hideText && (
101
+ <View style={styles.textContainer}>
102
+ {label && <Text style={styles.label}>{label}</Text>}
103
+ <Text style={[styles.value, valueSize ? { fontSize: valueSize } : undefined]}>
104
+ {showRemaining ? Math.max(0, Math.round(target - current)) : Math.round(current)}
105
+ {unit && <Text style={[styles.unit, valueSize ? { fontSize: valueSize * 0.58 } : undefined]}>{unit}</Text>}
106
+ </Text>
107
+ {subtitle ? (
108
+ <Text style={[styles.subtitle, subtitleSize ? { fontSize: subtitleSize } : undefined]}>{subtitle}</Text>
109
+ ) : target > 0 ? (
110
+ <Text style={styles.target}>
111
+ {showPercentage ? `${Math.round(percentage)}%` : `/ ${Math.round(target)}${unit}`}
112
+ </Text>
113
+ ) : null}
114
+ </View>
115
+ )}
116
+ </View>
117
+ );
118
+ };
119
+
120
+ const createStyles = (colors: any) => StyleSheet.create({
121
+ container: {
122
+ alignItems: 'center',
123
+ justifyContent: 'center',
124
+ },
125
+ textContainer: {
126
+ position: 'absolute',
127
+ alignItems: 'center',
128
+ },
129
+ label: {
130
+ fontSize: 12,
131
+ fontWeight: '600',
132
+ color: colors.text.secondary,
133
+ marginBottom: 4,
134
+ },
135
+ value: {
136
+ fontSize: 24,
137
+ fontWeight: '800',
138
+ color: colors.text.primary,
139
+ },
140
+ unit: {
141
+ fontSize: 14,
142
+ fontWeight: '600',
143
+ color: colors.text.secondary,
144
+ },
145
+ target: {
146
+ fontSize: 12,
147
+ fontWeight: '600',
148
+ color: colors.text.tertiary,
149
+ marginTop: 2,
150
+ },
151
+ subtitle: {
152
+ fontSize: 11,
153
+ fontWeight: '600',
154
+ color: colors.text.tertiary,
155
+ marginTop: 2,
156
+ },
157
+ });
@@ -0,0 +1,119 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { View, Text, StyleSheet, Animated } from 'react-native';
3
+ import { LinearGradient } from 'expo-linear-gradient';
4
+ import { useTheme } from '../../hooks/useTheme';
5
+
6
+ interface MacroBarProps {
7
+ label: string;
8
+ current: number;
9
+ target: number;
10
+ color: string[];
11
+ icon?: string;
12
+ unit?: string;
13
+ }
14
+
15
+ export const MacroBar: React.FC<MacroBarProps> = ({
16
+ label,
17
+ current,
18
+ target,
19
+ color,
20
+ unit = 'g',
21
+ }) => {
22
+ const { colors } = useTheme();
23
+ const styles = createStyles(colors);
24
+
25
+ const animatedWidth = useRef(new Animated.Value(0)).current;
26
+ const percentage = target > 0 ? Math.min((current / target) * 100, 100) : 0;
27
+
28
+ useEffect(() => {
29
+ Animated.timing(animatedWidth, {
30
+ toValue: percentage,
31
+ duration: 800,
32
+ useNativeDriver: false,
33
+ }).start();
34
+ }, [percentage]);
35
+
36
+ const widthInterpolation = animatedWidth.interpolate({
37
+ inputRange: [0, 100],
38
+ outputRange: ['0%', '100%'],
39
+ });
40
+
41
+ return (
42
+ <View style={styles.container}>
43
+ <View style={styles.header}>
44
+ <Text style={styles.label}>{label}</Text>
45
+ <Text style={styles.value}>
46
+ {Math.round(current)}{unit} / {Math.round(target)}{unit}
47
+ </Text>
48
+ </View>
49
+
50
+ <View style={styles.barContainer}>
51
+ <View style={styles.barBackground}>
52
+ <Animated.View style={[styles.barFill, { width: widthInterpolation }]}>
53
+ <LinearGradient
54
+ colors={color as [string, string, ...string[]]}
55
+ start={{ x: 0, y: 0 }}
56
+ end={{ x: 1, y: 0 }}
57
+ style={styles.gradient}
58
+ />
59
+ </Animated.View>
60
+ </View>
61
+ {percentage > 0 && (
62
+ <Text style={styles.percentage}>{Math.round(percentage)}%</Text>
63
+ )}
64
+ </View>
65
+ </View>
66
+ );
67
+ };
68
+
69
+ const createStyles = (colors: any) => StyleSheet.create({
70
+ container: {
71
+ marginVertical: 8,
72
+ },
73
+ header: {
74
+ flexDirection: 'row',
75
+ justifyContent: 'space-between',
76
+ alignItems: 'center',
77
+ marginBottom: 8,
78
+ },
79
+ label: {
80
+ fontSize: 15,
81
+ fontWeight: '700',
82
+ color: colors.text.primary,
83
+ },
84
+ value: {
85
+ fontSize: 14,
86
+ fontWeight: '600',
87
+ color: colors.text.secondary,
88
+ },
89
+ barContainer: {
90
+ position: 'relative',
91
+ },
92
+ barBackground: {
93
+ height: 28,
94
+ backgroundColor: 'rgba(0,0,0,0.06)',
95
+ borderRadius: 14,
96
+ overflow: 'hidden',
97
+ borderWidth: 1,
98
+ borderColor: 'rgba(0,0,0,0.04)',
99
+ },
100
+ barFill: {
101
+ height: '100%',
102
+ borderRadius: 14,
103
+ overflow: 'hidden',
104
+ },
105
+ gradient: {
106
+ flex: 1,
107
+ },
108
+ percentage: {
109
+ position: 'absolute',
110
+ right: 12,
111
+ top: 0,
112
+ bottom: 0,
113
+ fontSize: 13,
114
+ fontWeight: '700',
115
+ color: colors.text.primary,
116
+ textAlignVertical: 'center',
117
+ lineHeight: 28,
118
+ },
119
+ });
@@ -0,0 +1,11 @@
1
+ import { colors, shadows } from '../theme/colors';
2
+
3
+ export function useTheme() {
4
+ return {
5
+ colors,
6
+ shadows,
7
+ isDark: false as const,
8
+ mode: 'light' as const,
9
+ setMode: (_mode: string) => {}, // no-op stub for ProfileScreen
10
+ };
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ // Theme
2
+ export { colors, shadows } from './theme/colors';
3
+ export { typography, fonts, fontFamily } from './theme/typography';
4
+ export { spacing } from './theme/spacing';
5
+ export { theme } from './theme/theme';
6
+ export type { Theme } from './theme/theme';
7
+
8
+ // Hooks
9
+ export { useTheme } from './hooks/useTheme';
10
+
11
+ // Utils
12
+ export { GLASS_AVAILABLE } from './utils/glass';
13
+
14
+ // Components
15
+ export { AdaptiveGlassView } from './components/AdaptiveGlassView';
16
+ export { ErrorBoundary } from './components/ErrorBoundary';
17
+ export { FAB, FAB_SIZE } from './components/FAB';
18
+ export { GlassButton } from './components/GlassButton';
19
+ export { GlassCard } from './components/GlassCard';
20
+ export { GlassContainer } from './components/GlassContainer';
21
+ export { GlassHeaderBackground } from './components/GlassHeaderBackground';
22
+ export { GlassInput } from './components/GlassInput';
23
+ export { GlassListItem } from './components/GlassList';
24
+ export { GlassStatsCard, GlassMacroCard, GlassNutrientRow } from './components/GlassStats';
25
+
26
+ // Progress
27
+ export { CircularProgress } from './components/progress/CircularProgress';
28
+ export { CArcProgress } from './components/progress/CArcProgress';
29
+ export { MacroBar } from './components/progress/MacroBar';
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Cal AI-inspired clean, light-only color palette
3
+ * Warm off-white backgrounds, pure white cards, subtle shadows
4
+ */
5
+
6
+ export const colors = {
7
+ background: {
8
+ primary: '#FFFFFF', // Clean white background
9
+ card: '#FFFFFF',
10
+ secondary: '#F5F5F5',
11
+ tabBar: '#FFFFFF',
12
+ gradient: ['#FFFFFF', '#FFFFFF', '#FFFFFF'] as const, // backward compat
13
+ },
14
+ text: {
15
+ primary: '#1A1A1A',
16
+ secondary: '#6B6B6B',
17
+ tertiary: '#9B9B9B',
18
+ disabled: '#C5C5C5',
19
+ },
20
+ icon: {
21
+ primary: '#1A1A1A',
22
+ secondary: '#6B6B6B',
23
+ tertiary: '#9B9B9B',
24
+ },
25
+ border: {
26
+ light: 'rgba(0,0,0,0.06)',
27
+ medium: 'rgba(0,0,0,0.10)',
28
+ heavy: 'rgba(0,0,0,0.15)',
29
+ },
30
+ glass: { // backward compat aliases
31
+ background: '#FFFFFF',
32
+ backgroundLight: '#F7F7F4',
33
+ backgroundHeavy: '#FFFFFF',
34
+ tabBarFallback: '#FFFFFF',
35
+ fabFallback: '#1A1A1A',
36
+ headerFallback: 'rgba(255, 255, 255, 0.95)',
37
+ },
38
+ accent: {
39
+ blue: '#2196F3',
40
+ brand: '#1A1A1A', // Black primary buttons (Cal AI style)
41
+ brandLight: '#4A4A4A',
42
+ success: '#4CAF50',
43
+ warning: '#FF9800',
44
+ error: '#F44336',
45
+ green: '#4CAF50',
46
+ },
47
+ macro: {
48
+ protein: '#E91E63',
49
+ carbs: '#2196F3',
50
+ fat: '#FF9800',
51
+ calories: '#4CAF50',
52
+ },
53
+ shadow: {
54
+ text: 'transparent',
55
+ textLight: 'transparent',
56
+ textHeavy: 'transparent',
57
+ },
58
+ };
59
+
60
+ export const shadows = {
61
+ card: {
62
+ shadowColor: '#000',
63
+ shadowOffset: { width: 0, height: 2 },
64
+ shadowOpacity: 0.06,
65
+ shadowRadius: 8,
66
+ elevation: 3,
67
+ },
68
+ cardElevated: {
69
+ shadowColor: '#000',
70
+ shadowOffset: { width: 0, height: 4 },
71
+ shadowOpacity: 0.08,
72
+ shadowRadius: 12,
73
+ elevation: 5,
74
+ },
75
+ text: { color: 'transparent', offset: { width: 0, height: 0 }, radius: 0 },
76
+ textLight: { color: 'transparent', offset: { width: 0, height: 0 }, radius: 0 },
77
+ textHeavy: { color: 'transparent', offset: { width: 0, height: 0 }, radius: 0 },
78
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Spacing system for consistent margins and padding
3
+ */
4
+
5
+ export const spacing = {
6
+ xs: 4,
7
+ sm: 8,
8
+ md: 16,
9
+ lg: 24,
10
+ xl: 32,
11
+ '2xl': 48,
12
+ '3xl': 64,
13
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Main theme object combining all theme modules
3
+ */
4
+
5
+ import { colors, shadows } from './colors';
6
+ import { typography } from './typography';
7
+ import { spacing } from './spacing';
8
+
9
+ export const theme = {
10
+ colors,
11
+ shadows,
12
+ typography,
13
+ spacing,
14
+
15
+ // Border radius
16
+ borderRadius: {
17
+ sm: 8,
18
+ md: 12,
19
+ lg: 16,
20
+ xl: 20,
21
+ full: 9999,
22
+ },
23
+
24
+ // Common dimensions
25
+ dimensions: {
26
+ headerHeight: 60,
27
+ tabBarHeight: 60,
28
+ buttonHeight: 48,
29
+ inputHeight: 48,
30
+ iconSize: {
31
+ sm: 16,
32
+ md: 24,
33
+ lg: 32,
34
+ xl: 48,
35
+ },
36
+ },
37
+
38
+ // Animation durations
39
+ animation: {
40
+ fast: 150,
41
+ normal: 250,
42
+ slow: 400,
43
+ },
44
+ };
45
+
46
+ export type Theme = typeof theme;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Typography system — Plus Jakarta Sans
3
+ */
4
+
5
+ /** Font family names (must match the loaded .ttf file names) */
6
+ export const fonts = {
7
+ regular: 'PlusJakartaSans_400Regular',
8
+ medium: 'PlusJakartaSans_500Medium',
9
+ semiBold: 'PlusJakartaSans_600SemiBold',
10
+ bold: 'PlusJakartaSans_700Bold',
11
+ } as const;
12
+
13
+ /** Maps a numeric fontWeight string to the correct fontFamily */
14
+ export function fontFamily(weight?: string): string {
15
+ switch (weight) {
16
+ case '700':
17
+ case '800':
18
+ return fonts.bold;
19
+ case '600':
20
+ return fonts.semiBold;
21
+ case '500':
22
+ return fonts.medium;
23
+ default:
24
+ return fonts.regular;
25
+ }
26
+ }
27
+
28
+ export const typography = {
29
+ // Font sizes
30
+ fontSize: {
31
+ xs: 12,
32
+ sm: 14,
33
+ base: 16,
34
+ lg: 18,
35
+ xl: 20,
36
+ '2xl': 24,
37
+ '3xl': 30,
38
+ '4xl': 36,
39
+ },
40
+
41
+ // Font weights
42
+ fontWeight: {
43
+ regular: '400' as const,
44
+ medium: '500' as const,
45
+ semibold: '600' as const,
46
+ bold: '700' as const,
47
+ extrabold: '800' as const,
48
+ },
49
+
50
+ // Line heights
51
+ lineHeight: {
52
+ tight: 1.25,
53
+ normal: 1.5,
54
+ relaxed: 1.75,
55
+ },
56
+ };
@@ -0,0 +1,12 @@
1
+ import { Platform } from 'react-native';
2
+ import { isGlassEffectAPIAvailable } from 'expo-glass-effect';
3
+
4
+ const GLASS_DISABLED = process.env.EXPO_PUBLIC_DISABLE_GLASS === '1';
5
+
6
+ /**
7
+ * Whether the native glass effect API is available on this device.
8
+ * Computed once at module load — no per-render checks needed.
9
+ * True only on iOS 26+ where Liquid Glass is supported.
10
+ */
11
+ export const GLASS_AVAILABLE =
12
+ !GLASS_DISABLED && Platform.OS === 'ios' && isGlassEffectAPIAvailable();