@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.
- package/package.json +32 -0
- package/src/components/AdaptiveGlassView.tsx +46 -0
- package/src/components/ErrorBoundary.tsx +92 -0
- package/src/components/FAB.tsx +209 -0
- package/src/components/GlassButton.tsx +279 -0
- package/src/components/GlassCard.tsx +59 -0
- package/src/components/GlassContainer.tsx +26 -0
- package/src/components/GlassHeaderBackground.tsx +32 -0
- package/src/components/GlassInput.tsx +166 -0
- package/src/components/GlassList.tsx +97 -0
- package/src/components/GlassStats.tsx +212 -0
- package/src/components/progress/CArcProgress.tsx +107 -0
- package/src/components/progress/CircularProgress.tsx +157 -0
- package/src/components/progress/MacroBar.tsx +119 -0
- package/src/hooks/useTheme.ts +11 -0
- package/src/index.ts +29 -0
- package/src/theme/colors.ts +78 -0
- package/src/theme/spacing.ts +13 -0
- package/src/theme/theme.ts +46 -0
- package/src/theme/typography.ts +56 -0
- package/src/utils/glass.ts +12 -0
|
@@ -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
|
+
});
|
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,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();
|