feat: add glass UI component library
GlassCard, GlassButton (with pulse animation), GlassSlider (gesture-based), GlassBottomSheet (3-state snap), GlassPill selector, and GlassTabBar with blur backing.
This commit is contained in:
116
src/components/glass/GlassBottomSheet.tsx
Normal file
116
src/components/glass/GlassBottomSheet.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React from "react";
|
||||
import { Dimensions, StyleSheet, View } from "react-native";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withSpring,
|
||||
clamp,
|
||||
} from "react-native-reanimated";
|
||||
import { colors, glassRadius, layout } from "@/theme";
|
||||
|
||||
const SCREEN_HEIGHT = Dimensions.get("window").height;
|
||||
|
||||
type SheetState = "peek" | "half" | "full";
|
||||
|
||||
const SNAP_POINTS: Record<SheetState, number> = {
|
||||
peek: SCREEN_HEIGHT - layout.bottomSheetPeek,
|
||||
half: SCREEN_HEIGHT - layout.bottomSheetHalf,
|
||||
full: 80,
|
||||
};
|
||||
|
||||
interface GlassBottomSheetProps {
|
||||
children: React.ReactNode;
|
||||
initialState?: SheetState;
|
||||
}
|
||||
|
||||
export function GlassBottomSheet({
|
||||
children,
|
||||
initialState = "peek",
|
||||
}: GlassBottomSheetProps) {
|
||||
const translateY = useSharedValue(SNAP_POINTS[initialState]);
|
||||
const startY = useSharedValue(0);
|
||||
|
||||
const findNearestSnap = (y: number): number => {
|
||||
const points = Object.values(SNAP_POINTS);
|
||||
return points.reduce((prev, curr) =>
|
||||
Math.abs(curr - y) < Math.abs(prev - y) ? curr : prev
|
||||
);
|
||||
};
|
||||
|
||||
const pan = Gesture.Pan()
|
||||
.onStart(() => {
|
||||
startY.value = translateY.value;
|
||||
})
|
||||
.onChange((e) => {
|
||||
translateY.value = clamp(
|
||||
startY.value + e.translationY,
|
||||
SNAP_POINTS.full,
|
||||
SNAP_POINTS.peek
|
||||
);
|
||||
})
|
||||
.onEnd((e) => {
|
||||
const projected = translateY.value + e.velocityY * 0.1;
|
||||
const snap = findNearestSnap(projected);
|
||||
translateY.value = withSpring(snap, {
|
||||
damping: 25,
|
||||
stiffness: 200,
|
||||
velocity: e.velocityY,
|
||||
});
|
||||
});
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateY: translateY.value }],
|
||||
}));
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={pan}>
|
||||
<Animated.View style={[styles.container, animatedStyle]}>
|
||||
<BlurView intensity={40} tint="dark" style={StyleSheet.absoluteFill} />
|
||||
<View style={styles.overlay} />
|
||||
{/* Handle bar */}
|
||||
<View style={styles.handleContainer}>
|
||||
<View style={styles.handle} />
|
||||
</View>
|
||||
<View style={styles.content}>{children}</View>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: SCREEN_HEIGHT,
|
||||
borderTopLeftRadius: glassRadius.xl,
|
||||
borderTopRightRadius: glassRadius.xl,
|
||||
overflow: "hidden",
|
||||
borderWidth: 1,
|
||||
borderBottomWidth: 0,
|
||||
borderColor: colors.glass.border,
|
||||
},
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: "rgba(12, 12, 20, 0.60)",
|
||||
},
|
||||
handleContainer: {
|
||||
alignItems: "center",
|
||||
paddingTop: 12,
|
||||
paddingBottom: 8,
|
||||
zIndex: 1,
|
||||
},
|
||||
handle: {
|
||||
width: 36,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.25)",
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
zIndex: 1,
|
||||
},
|
||||
});
|
||||
139
src/components/glass/GlassButton.tsx
Normal file
139
src/components/glass/GlassButton.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, Text, ViewStyle, ActivityIndicator } from "react-native";
|
||||
import { BlurView } from "expo-blur";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withSpring,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
import { colors, glassRadius, typography } from "@/theme";
|
||||
|
||||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
||||
|
||||
interface GlassButtonProps {
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
variant?: "primary" | "secondary" | "ghost";
|
||||
size?: "sm" | "md" | "lg";
|
||||
icon?: React.ReactNode;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
pulse?: boolean;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
export function GlassButton({
|
||||
label,
|
||||
onPress,
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
icon,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
pulse = false,
|
||||
style,
|
||||
}: GlassButtonProps) {
|
||||
const scale = useSharedValue(1);
|
||||
const opacity = useSharedValue(1);
|
||||
|
||||
// Subtle pulse animation for CTA
|
||||
React.useEffect(() => {
|
||||
if (pulse && !disabled) {
|
||||
opacity.value = withRepeat(
|
||||
withSequence(
|
||||
withTiming(0.85, { duration: 1500 }),
|
||||
withTiming(1, { duration: 1500 })
|
||||
),
|
||||
-1,
|
||||
true
|
||||
);
|
||||
}
|
||||
}, [pulse, disabled]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
opacity: disabled ? 0.4 : opacity.value,
|
||||
}));
|
||||
|
||||
const handlePressIn = () => {
|
||||
scale.value = withSpring(0.96, { damping: 15 });
|
||||
};
|
||||
|
||||
const handlePressOut = () => {
|
||||
scale.value = withSpring(1, { damping: 15 });
|
||||
};
|
||||
|
||||
const handlePress = () => {
|
||||
if (disabled || loading) return;
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
onPress();
|
||||
};
|
||||
|
||||
const heights = { sm: 36, md: 48, lg: 56 };
|
||||
const fontSizes = { sm: 13, md: 15, lg: 17 };
|
||||
|
||||
return (
|
||||
<AnimatedPressable
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
onPress={handlePress}
|
||||
disabled={disabled || loading}
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
height: heights[size],
|
||||
borderRadius: glassRadius.pill,
|
||||
borderColor:
|
||||
variant === "primary"
|
||||
? colors.glass.borderLight
|
||||
: colors.glass.border,
|
||||
backgroundColor:
|
||||
variant === "primary"
|
||||
? "rgba(255, 255, 255, 0.12)"
|
||||
: variant === "secondary"
|
||||
? "rgba(255, 255, 255, 0.06)"
|
||||
: "transparent",
|
||||
},
|
||||
style,
|
||||
animatedStyle,
|
||||
]}
|
||||
>
|
||||
<BlurView
|
||||
intensity={variant === "ghost" ? 0 : 20}
|
||||
tint="dark"
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={colors.text.primary} size="small" />
|
||||
) : (
|
||||
<>
|
||||
{icon}
|
||||
<Text
|
||||
style={[
|
||||
typography.button,
|
||||
{ fontSize: fontSizes[size] },
|
||||
icon != null && { marginLeft: 8 },
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</AnimatedPressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 24,
|
||||
borderWidth: 1,
|
||||
overflow: "hidden",
|
||||
},
|
||||
});
|
||||
67
src/components/glass/GlassCard.tsx
Normal file
67
src/components/glass/GlassCard.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, ViewStyle } from "react-native";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { glassSurface, glassIntensity, glassRadius } from "@/theme";
|
||||
import type { GlassVariant } from "@/theme";
|
||||
|
||||
interface GlassCardProps {
|
||||
children: React.ReactNode;
|
||||
variant?: GlassVariant;
|
||||
radius?: keyof typeof glassRadius;
|
||||
padding?: number;
|
||||
style?: ViewStyle;
|
||||
noPadding?: boolean;
|
||||
}
|
||||
|
||||
export function GlassCard({
|
||||
children,
|
||||
variant = "medium",
|
||||
radius = "lg",
|
||||
padding = 16,
|
||||
style,
|
||||
noPadding = false,
|
||||
}: GlassCardProps) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
glassSurface(variant),
|
||||
{ borderRadius: glassRadius[radius] },
|
||||
style,
|
||||
]}
|
||||
>
|
||||
<BlurView
|
||||
intensity={glassIntensity[variant]}
|
||||
tint="dark"
|
||||
style={[StyleSheet.absoluteFill, { borderRadius: glassRadius[radius] }]}
|
||||
/>
|
||||
{/* Top edge highlight for depth illusion */}
|
||||
<View
|
||||
style={[
|
||||
styles.highlight,
|
||||
{ borderTopLeftRadius: glassRadius[radius], borderTopRightRadius: glassRadius[radius] },
|
||||
]}
|
||||
/>
|
||||
<View style={[styles.content, !noPadding && { padding }]}>{children}</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
},
|
||||
highlight: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 1,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||
},
|
||||
content: {
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
},
|
||||
});
|
||||
89
src/components/glass/GlassPill.tsx
Normal file
89
src/components/glass/GlassPill.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, Text, ViewStyle } from "react-native";
|
||||
import { BlurView } from "expo-blur";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withSpring,
|
||||
} from "react-native-reanimated";
|
||||
import { colors, glassRadius, typography } from "@/theme";
|
||||
|
||||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
||||
|
||||
interface GlassPillProps {
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
onPress: () => void;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
export function GlassPill({ label, selected = false, onPress, style }: GlassPillProps) {
|
||||
const scale = useSharedValue(1);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
}));
|
||||
|
||||
const handlePress = () => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
scale.value = withSpring(0.92, { damping: 12 });
|
||||
setTimeout(() => {
|
||||
scale.value = withSpring(1, { damping: 12 });
|
||||
}, 100);
|
||||
onPress();
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatedPressable
|
||||
onPress={handlePress}
|
||||
style={[
|
||||
styles.container,
|
||||
selected && styles.selected,
|
||||
style,
|
||||
animatedStyle,
|
||||
]}
|
||||
>
|
||||
<BlurView
|
||||
intensity={selected ? 30 : 15}
|
||||
tint="dark"
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.label,
|
||||
selected && styles.labelSelected,
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</AnimatedPressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: glassRadius.pill,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.glass.border,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.04)",
|
||||
overflow: "hidden",
|
||||
marginRight: 8,
|
||||
},
|
||||
selected: {
|
||||
borderColor: colors.glass.borderLight,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.12)",
|
||||
},
|
||||
label: {
|
||||
...typography.bodySecondary,
|
||||
fontSize: 13,
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
},
|
||||
labelSelected: {
|
||||
color: colors.text.primary,
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
149
src/components/glass/GlassSlider.tsx
Normal file
149
src/components/glass/GlassSlider.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
runOnJS,
|
||||
withSpring,
|
||||
clamp,
|
||||
} from "react-native-reanimated";
|
||||
import { colors, typography } from "@/theme";
|
||||
|
||||
interface GlassSliderProps {
|
||||
label: string;
|
||||
value: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
onValueChange: (value: number) => void;
|
||||
formatValue?: (value: number) => string;
|
||||
}
|
||||
|
||||
export function GlassSlider({
|
||||
label,
|
||||
value,
|
||||
min = 0,
|
||||
max = 1,
|
||||
step = 0.01,
|
||||
onValueChange,
|
||||
formatValue,
|
||||
}: GlassSliderProps) {
|
||||
const trackWidth = useSharedValue(0);
|
||||
const thumbScale = useSharedValue(1);
|
||||
const progress = (value - min) / (max - min);
|
||||
|
||||
const snap = (v: number) => {
|
||||
const stepped = Math.round(v / step) * step;
|
||||
return Math.round(stepped * 100) / 100;
|
||||
};
|
||||
|
||||
const pan = Gesture.Pan()
|
||||
.onBegin(() => {
|
||||
thumbScale.value = withSpring(1.3, { damping: 12 });
|
||||
})
|
||||
.onChange((e) => {
|
||||
const normalized = clamp(
|
||||
(value - min) / (max - min) + e.changeX / trackWidth.value,
|
||||
0,
|
||||
1
|
||||
);
|
||||
const newValue = snap(min + normalized * (max - min));
|
||||
runOnJS(onValueChange)(newValue);
|
||||
})
|
||||
.onEnd(() => {
|
||||
thumbScale.value = withSpring(1, { damping: 12 });
|
||||
});
|
||||
|
||||
const thumbAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: thumbScale.value }],
|
||||
}));
|
||||
|
||||
const displayValue = formatValue ? formatValue(value) : `${Math.round(value * 100)}%`;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.labelRow}>
|
||||
<Text style={typography.bodySecondary}>{label}</Text>
|
||||
<Text style={typography.bodySecondary}>{displayValue}</Text>
|
||||
</View>
|
||||
<View
|
||||
style={styles.track}
|
||||
onLayout={(e) => {
|
||||
trackWidth.value = e.nativeEvent.layout.width;
|
||||
}}
|
||||
>
|
||||
{/* Track background */}
|
||||
<View style={styles.trackBg} />
|
||||
{/* Filled portion */}
|
||||
<View
|
||||
style={[
|
||||
styles.trackFill,
|
||||
{ width: `${progress * 100}%` },
|
||||
]}
|
||||
/>
|
||||
{/* Thumb */}
|
||||
<GestureDetector gesture={pan}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.thumb,
|
||||
{ left: `${progress * 100}%` },
|
||||
thumbAnimatedStyle,
|
||||
]}
|
||||
>
|
||||
<View style={styles.thumbInner} />
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginVertical: 8,
|
||||
},
|
||||
labelRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 10,
|
||||
},
|
||||
track: {
|
||||
height: 32,
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
},
|
||||
trackBg: {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||
},
|
||||
trackFill: {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.30)",
|
||||
},
|
||||
thumb: {
|
||||
position: "absolute",
|
||||
width: 24,
|
||||
height: 24,
|
||||
marginLeft: -12,
|
||||
borderRadius: 12,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
borderWidth: 1.5,
|
||||
borderColor: "rgba(255, 255, 255, 0.40)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
thumbInner: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.text.primary,
|
||||
},
|
||||
});
|
||||
143
src/components/glass/GlassTabBar.tsx
Normal file
143
src/components/glass/GlassTabBar.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, Text, View, Pressable } from "react-native";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { BottomTabBarProps } from "@react-navigation/bottom-tabs";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withSpring,
|
||||
} from "react-native-reanimated";
|
||||
import { colors, glassRadius, layout } from "@/theme";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
||||
|
||||
export function GlassTabBar({ state, descriptors, navigation }: BottomTabBarProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingBottom: insets.bottom || 12 }]}>
|
||||
<BlurView intensity={40} tint="dark" style={StyleSheet.absoluteFill} />
|
||||
<View style={styles.overlay} />
|
||||
<View style={styles.tabs}>
|
||||
{state.routes.map((route: typeof state.routes[number], index: number) => {
|
||||
const { options } = descriptors[route.key];
|
||||
const label = (options.tabBarLabel ?? options.title ?? route.name) as string;
|
||||
const isFocused = state.index === index;
|
||||
|
||||
const onPress = () => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
const event = navigation.emit({
|
||||
type: "tabPress",
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
if (!isFocused && !event.defaultPrevented) {
|
||||
navigation.navigate(route.name);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TabItem
|
||||
key={route.key}
|
||||
label={label}
|
||||
focused={isFocused}
|
||||
icon={options.tabBarIcon}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function TabItem({
|
||||
label,
|
||||
focused,
|
||||
icon,
|
||||
onPress,
|
||||
}: {
|
||||
label: string;
|
||||
focused: boolean;
|
||||
icon: any;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const scale = useSharedValue(1);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
}));
|
||||
|
||||
return (
|
||||
<AnimatedPressable
|
||||
onPress={onPress}
|
||||
onPressIn={() => {
|
||||
scale.value = withSpring(0.9, { damping: 12 });
|
||||
}}
|
||||
onPressOut={() => {
|
||||
scale.value = withSpring(1, { damping: 12 });
|
||||
}}
|
||||
style={[styles.tab, animatedStyle]}
|
||||
>
|
||||
{icon?.({
|
||||
color: focused ? colors.text.primary : colors.text.tertiary,
|
||||
size: 22,
|
||||
focused,
|
||||
})}
|
||||
<Text
|
||||
style={[
|
||||
styles.tabLabel,
|
||||
{ color: focused ? colors.text.primary : colors.text.tertiary },
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{focused && <View style={styles.indicator} />}
|
||||
</AnimatedPressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
borderTopLeftRadius: glassRadius.xl,
|
||||
borderTopRightRadius: glassRadius.xl,
|
||||
borderWidth: 1,
|
||||
borderBottomWidth: 0,
|
||||
borderColor: colors.glass.border,
|
||||
overflow: "hidden",
|
||||
},
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: "rgba(8, 8, 16, 0.65)",
|
||||
},
|
||||
tabs: {
|
||||
flexDirection: "row",
|
||||
paddingTop: 10,
|
||||
paddingHorizontal: 16,
|
||||
zIndex: 1,
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
paddingVertical: 6,
|
||||
},
|
||||
tabLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: "500",
|
||||
marginTop: 4,
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
indicator: {
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: colors.text.primary,
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
6
src/components/glass/index.ts
Normal file
6
src/components/glass/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { GlassCard } from "./GlassCard";
|
||||
export { GlassButton } from "./GlassButton";
|
||||
export { GlassSlider } from "./GlassSlider";
|
||||
export { GlassBottomSheet } from "./GlassBottomSheet";
|
||||
export { GlassPill } from "./GlassPill";
|
||||
export { GlassTabBar } from "./GlassTabBar";
|
||||
Reference in New Issue
Block a user