mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Update mobile app UI with premium design system
- Add design guidelines to CLAUDE.md - Redesign navigation with boutique styling - Add video support to chat with compression - Update matches/dares/quiz screens with gold/rose accents - Improve packs browsing with category tabs - Add SwipeCardPremium component - Update theme with premium color palette - Add video cleanup edge function and migration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
feb7657562
commit
cac7a2a31d
31 files changed
+6638
-1827
No files matched your search
@@ -0,0 +1,711 @@
|
||||
import { View, Text, StyleSheet, RefreshControl, Platform, Alert, Pressable } from "react-native";
|
||||
import { usePacksStore, useAuthStore, useSubscriptionStore } from "../../../src/store";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
import { useMemo, useState, useCallback } from "react";
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
useAnimatedScrollHandler,
|
||||
interpolate,
|
||||
Extrapolation,
|
||||
} from "react-native-reanimated";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { GradientBackground } from "../../../src/components/ui";
|
||||
import { Paywall } from "../../../src/components/Paywall";
|
||||
import { PackTeaser } from "../../../src/components/PackTeaser";
|
||||
import { colors, spacing, radius, typography } from "../../../src/theme";
|
||||
import type { QuestionPack, Category } from "../../../src/types";
|
||||
|
||||
const HEADER_MAX_HEIGHT = 180;
|
||||
const HEADER_MIN_HEIGHT = 100;
|
||||
const HEADER_SCROLL_DISTANCE = HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT;
|
||||
|
||||
interface PackRowProps {
|
||||
pack: QuestionPack;
|
||||
isEnabled: boolean;
|
||||
isPremiumLocked: boolean;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
onPress: () => void;
|
||||
onInfoPress: () => void;
|
||||
}
|
||||
|
||||
function PackRow({ pack, isEnabled, isPremiumLocked, isExpanded, onToggle, onPress, onInfoPress }: PackRowProps) {
|
||||
const questionCount = pack.questions?.[0]?.count || 0;
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress} style={styles.packRow}>
|
||||
<View style={styles.packRowMain}>
|
||||
<View style={[styles.packIcon, isEnabled && styles.packIconEnabled]}>
|
||||
<Text style={styles.packIconText}>{pack.icon || "📦"}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.packContent}>
|
||||
<View style={styles.packTitleRow}>
|
||||
<Text style={styles.packName} numberOfLines={1}>{pack.name}</Text>
|
||||
{isPremiumLocked && (
|
||||
<Ionicons name="lock-closed" size={14} color={colors.premium.gold} />
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.packMeta}>
|
||||
{questionCount} {questionCount === 1 ? "question" : "questions"}
|
||||
{isEnabled && <Text style={styles.packMetaEnabled}> · Enabled</Text>}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Ionicons
|
||||
name={isExpanded ? "chevron-up" : "chevron-down"}
|
||||
size={20}
|
||||
color={colors.textTertiary}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{isExpanded && (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200)}
|
||||
exiting={FadeOut.duration(150)}
|
||||
style={styles.packExpanded}
|
||||
>
|
||||
{pack.description && (
|
||||
<Text style={styles.packDescription}>{pack.description}</Text>
|
||||
)}
|
||||
<View style={styles.packActions}>
|
||||
<Pressable style={styles.packInfoButton} onPress={onInfoPress}>
|
||||
<Ionicons name="eye-outline" size={18} color={colors.textSecondary} />
|
||||
<Text style={styles.packInfoButtonText}>Preview</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={[
|
||||
styles.packToggleButton,
|
||||
isEnabled && styles.packToggleButtonEnabled,
|
||||
isPremiumLocked && styles.packToggleButtonLocked,
|
||||
]}
|
||||
onPress={onToggle}
|
||||
>
|
||||
{isPremiumLocked ? (
|
||||
<>
|
||||
<Ionicons name="lock-closed" size={16} color={colors.premium.gold} />
|
||||
<Text style={[styles.packToggleButtonText, styles.packToggleButtonTextLocked]}>
|
||||
Unlock
|
||||
</Text>
|
||||
</>
|
||||
) : isEnabled ? (
|
||||
<>
|
||||
<Ionicons name="checkmark" size={16} color={colors.primary} />
|
||||
<Text style={[styles.packToggleButtonText, styles.packToggleButtonTextEnabled]}>
|
||||
Enabled
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="add" size={16} color={colors.text} />
|
||||
<Text style={styles.packToggleButtonText}>Enable</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
interface CategorySectionProps {
|
||||
category: Category;
|
||||
packs: QuestionPack[];
|
||||
enabledPackIds: string[];
|
||||
hasPremiumAccess: boolean;
|
||||
expandedPackId: string | null;
|
||||
onPackPress: (packId: string) => void;
|
||||
onPackToggle: (pack: QuestionPack) => void;
|
||||
onPackInfo: (pack: QuestionPack) => void;
|
||||
}
|
||||
|
||||
function CategorySection({
|
||||
category,
|
||||
packs,
|
||||
enabledPackIds,
|
||||
hasPremiumAccess,
|
||||
expandedPackId,
|
||||
onPackPress,
|
||||
onPackToggle,
|
||||
onPackInfo,
|
||||
}: CategorySectionProps) {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const enabledCount = packs.filter(p => enabledPackIds.includes(p.id)).length;
|
||||
|
||||
const rotateAnim = useSharedValue(0);
|
||||
|
||||
const chevronStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ rotate: `${rotateAnim.value}deg` }],
|
||||
}));
|
||||
|
||||
const handleToggleCollapse = () => {
|
||||
rotateAnim.value = withTiming(isCollapsed ? 0 : -90, { duration: 200 });
|
||||
setIsCollapsed(!isCollapsed);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.categorySection}>
|
||||
<Pressable style={styles.categoryHeader} onPress={handleToggleCollapse}>
|
||||
<View style={styles.categoryHeaderLeft}>
|
||||
{category.icon && (
|
||||
<Text style={styles.categoryIcon}>{category.icon}</Text>
|
||||
)}
|
||||
<Text style={styles.categoryName}>{category.name}</Text>
|
||||
{enabledCount > 0 && (
|
||||
<View style={styles.categoryBadge}>
|
||||
<Text style={styles.categoryBadgeText}>{enabledCount}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Animated.View style={chevronStyle}>
|
||||
<Ionicons name="chevron-down" size={20} color={colors.textSecondary} />
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
|
||||
{!isCollapsed && (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200)}
|
||||
exiting={FadeOut.duration(150)}
|
||||
style={styles.packsList}
|
||||
>
|
||||
{packs.map((pack) => (
|
||||
<PackRow
|
||||
key={pack.id}
|
||||
pack={pack}
|
||||
isEnabled={enabledPackIds.includes(pack.id)}
|
||||
isPremiumLocked={pack.is_premium && !hasPremiumAccess}
|
||||
isExpanded={expandedPackId === pack.id}
|
||||
onPress={() => onPackPress(pack.id)}
|
||||
onToggle={() => onPackToggle(pack)}
|
||||
onInfoPress={() => onPackInfo(pack)}
|
||||
/>
|
||||
))}
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function QuestionPacksScreen() {
|
||||
const { packs, categories, enabledPackIds, togglePack, isLoading, fetchPacks } = usePacksStore();
|
||||
const { user, partner } = useAuthStore();
|
||||
const { subscription } = useSubscriptionStore();
|
||||
|
||||
const [expandedPackId, setExpandedPackId] = useState<string | null>(null);
|
||||
const [showPaywall, setShowPaywall] = useState(false);
|
||||
const [showTeaser, setShowTeaser] = useState(false);
|
||||
const [teaserPack, setTeaserPack] = useState<QuestionPack | null>(null);
|
||||
|
||||
const scrollY = useSharedValue(0);
|
||||
|
||||
const scrollHandler = useAnimatedScrollHandler({
|
||||
onScroll: (event) => {
|
||||
scrollY.value = event.contentOffset.y;
|
||||
},
|
||||
});
|
||||
|
||||
const hasPremiumAccess = useMemo(() => {
|
||||
return user?.is_premium || partner?.is_premium || subscription.isProUser;
|
||||
}, [user?.is_premium, partner?.is_premium, subscription.isProUser]);
|
||||
|
||||
const packsByCategory = useMemo(() => {
|
||||
const grouped = new Map<string, QuestionPack[]>();
|
||||
categories.forEach(cat => grouped.set(cat.id, []));
|
||||
grouped.set("uncategorized", []);
|
||||
packs.forEach(pack => {
|
||||
const catId = pack.category_id || "uncategorized";
|
||||
const existing = grouped.get(catId) || [];
|
||||
existing.push(pack);
|
||||
grouped.set(catId, existing);
|
||||
});
|
||||
return grouped;
|
||||
}, [packs, categories]);
|
||||
|
||||
const handlePackPress = useCallback((packId: string) => {
|
||||
setExpandedPackId(current => current === packId ? null : packId);
|
||||
}, []);
|
||||
|
||||
const handlePackToggle = useCallback(async (pack: QuestionPack) => {
|
||||
if (pack.is_premium && !hasPremiumAccess) {
|
||||
setTeaserPack(pack);
|
||||
setShowTeaser(true);
|
||||
return;
|
||||
}
|
||||
const result = await togglePack(pack.id);
|
||||
if (!result.success && result.reason === "no_couple") {
|
||||
Alert.alert(
|
||||
"Pair with Partner",
|
||||
"You need to pair with your partner before you can customise question packs.",
|
||||
[
|
||||
{ text: "Later", style: "cancel" },
|
||||
{ text: "Pair Now", onPress: () => router.push("/pairing") }
|
||||
]
|
||||
);
|
||||
}
|
||||
}, [hasPremiumAccess, togglePack]);
|
||||
|
||||
const handlePackInfo = useCallback((pack: QuestionPack) => {
|
||||
const isPremiumLocked = pack.is_premium && !hasPremiumAccess;
|
||||
if (isPremiumLocked) {
|
||||
setTeaserPack(pack);
|
||||
setShowTeaser(true);
|
||||
} else {
|
||||
router.push(`/pack/${pack.id}`);
|
||||
}
|
||||
}, [hasPremiumAccess]);
|
||||
|
||||
const handleTeaserUnlock = useCallback(() => {
|
||||
setShowTeaser(false);
|
||||
setShowPaywall(true);
|
||||
}, []);
|
||||
|
||||
const handleTeaserClose = useCallback(() => {
|
||||
setShowTeaser(false);
|
||||
setTeaserPack(null);
|
||||
}, []);
|
||||
|
||||
const enabledPackCount = enabledPackIds.length;
|
||||
const totalQuestions = packs
|
||||
.filter(p => enabledPackIds.includes(p.id))
|
||||
.reduce((sum, p) => sum + (p.questions?.[0]?.count || 0), 0);
|
||||
|
||||
// Animated styles for collapsing header
|
||||
const largeHeaderStyle = useAnimatedStyle(() => {
|
||||
const opacity = interpolate(
|
||||
scrollY.value,
|
||||
[0, HEADER_SCROLL_DISTANCE * 0.5],
|
||||
[1, 0],
|
||||
Extrapolation.CLAMP
|
||||
);
|
||||
const translateY = interpolate(
|
||||
scrollY.value,
|
||||
[0, HEADER_SCROLL_DISTANCE],
|
||||
[0, -20],
|
||||
Extrapolation.CLAMP
|
||||
);
|
||||
return { opacity, transform: [{ translateY }] };
|
||||
});
|
||||
|
||||
const compactHeaderStyle = useAnimatedStyle(() => {
|
||||
const opacity = interpolate(
|
||||
scrollY.value,
|
||||
[HEADER_SCROLL_DISTANCE * 0.5, HEADER_SCROLL_DISTANCE],
|
||||
[0, 1],
|
||||
Extrapolation.CLAMP
|
||||
);
|
||||
return { opacity };
|
||||
});
|
||||
|
||||
const navBarBackgroundStyle = useAnimatedStyle(() => {
|
||||
const opacity = interpolate(
|
||||
scrollY.value,
|
||||
[0, HEADER_SCROLL_DISTANCE * 0.8],
|
||||
[0, 1],
|
||||
Extrapolation.CLAMP
|
||||
);
|
||||
return { opacity };
|
||||
});
|
||||
|
||||
return (
|
||||
<GradientBackground>
|
||||
<View style={styles.container}>
|
||||
{/* Fixed Nav Bar */}
|
||||
<View style={styles.navBar}>
|
||||
{/* Nav bar background with blur */}
|
||||
<Animated.View style={[styles.navBarBackground, navBarBackgroundStyle]}>
|
||||
{Platform.OS === "ios" ? (
|
||||
<BlurView intensity={80} tint="dark" style={StyleSheet.absoluteFill} />
|
||||
) : (
|
||||
<View style={[StyleSheet.absoluteFill, { backgroundColor: "rgba(13, 13, 26, 0.95)" }]} />
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
{/* Back button */}
|
||||
<Pressable
|
||||
style={styles.backButton}
|
||||
onPress={() => router.back()}
|
||||
hitSlop={8}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||
</Pressable>
|
||||
|
||||
{/* Compact title (fades in on scroll) */}
|
||||
<Animated.Text style={[styles.navBarTitle, compactHeaderStyle]}>
|
||||
Question Packs
|
||||
</Animated.Text>
|
||||
|
||||
{/* Spacer for alignment */}
|
||||
<View style={styles.navBarSpacer} />
|
||||
</View>
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<Animated.ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
onScroll={scrollHandler}
|
||||
scrollEventThrottle={16}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isLoading}
|
||||
onRefresh={fetchPacks}
|
||||
tintColor={colors.primary}
|
||||
colors={[colors.primary]}
|
||||
progressViewOffset={HEADER_MAX_HEIGHT}
|
||||
/>
|
||||
}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
{/* Large Header (fades out on scroll) */}
|
||||
<Animated.View style={[styles.largeHeader, largeHeaderStyle]}>
|
||||
<Text style={styles.label}>COLLECTION</Text>
|
||||
<Text style={styles.title}>Question Packs</Text>
|
||||
|
||||
<View style={styles.separator}>
|
||||
<View style={styles.separatorLine} />
|
||||
<View style={styles.separatorDiamond} />
|
||||
<View style={styles.separatorLine} />
|
||||
</View>
|
||||
|
||||
<View style={styles.statsBadge}>
|
||||
<Ionicons name="checkmark-circle" size={14} color={colors.primary} />
|
||||
<Text style={styles.statsBadgeText}>
|
||||
{totalQuestions} {totalQuestions === 1 ? "question" : "questions"} from {enabledPackCount} {enabledPackCount === 1 ? "pack" : "packs"}
|
||||
</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
{/* Categories */}
|
||||
<View style={styles.categoriesContainer}>
|
||||
{categories.map(category => {
|
||||
const categoryPacks = packsByCategory.get(category.id) || [];
|
||||
if (categoryPacks.length === 0) return null;
|
||||
|
||||
return (
|
||||
<CategorySection
|
||||
key={category.id}
|
||||
category={category}
|
||||
packs={categoryPacks}
|
||||
enabledPackIds={enabledPackIds}
|
||||
hasPremiumAccess={hasPremiumAccess}
|
||||
expandedPackId={expandedPackId}
|
||||
onPackPress={handlePackPress}
|
||||
onPackToggle={handlePackToggle}
|
||||
onPackInfo={handlePackInfo}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{(packsByCategory.get("uncategorized")?.length || 0) > 0 && (
|
||||
<CategorySection
|
||||
category={{ id: "uncategorized", name: "Other", description: null, icon: "📁", sort_order: 999, created_at: "" }}
|
||||
packs={packsByCategory.get("uncategorized") || []}
|
||||
enabledPackIds={enabledPackIds}
|
||||
hasPremiumAccess={hasPremiumAccess}
|
||||
expandedPackId={expandedPackId}
|
||||
onPackPress={handlePackPress}
|
||||
onPackToggle={handlePackToggle}
|
||||
onPackInfo={handlePackInfo}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Animated.ScrollView>
|
||||
|
||||
{/* Modals */}
|
||||
{teaserPack && (
|
||||
<PackTeaser
|
||||
visible={showTeaser}
|
||||
packId={teaserPack.id}
|
||||
packName={teaserPack.name}
|
||||
packIcon={teaserPack.icon}
|
||||
onClose={handleTeaserClose}
|
||||
onUnlock={handleTeaserUnlock}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Paywall
|
||||
visible={showPaywall}
|
||||
onClose={() => setShowPaywall(false)}
|
||||
onSuccess={() => fetchPacks()}
|
||||
/>
|
||||
</View>
|
||||
</GradientBackground>
|
||||
);
|
||||
}
|
||||
|
||||
const NAV_BAR_HEIGHT = 44;
|
||||
const STATUS_BAR_HEIGHT = 60; // Match other pages
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
// Fixed Nav Bar
|
||||
navBar: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: STATUS_BAR_HEIGHT + NAV_BAR_HEIGHT,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingTop: STATUS_BAR_HEIGHT - 10,
|
||||
paddingHorizontal: spacing.md,
|
||||
zIndex: 100,
|
||||
},
|
||||
navBarBackground: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "rgba(255, 255, 255, 0.1)",
|
||||
overflow: "hidden",
|
||||
},
|
||||
backButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
navBarTitle: {
|
||||
flex: 1,
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
textAlign: "center",
|
||||
marginRight: 40, // Balance with back button
|
||||
},
|
||||
navBarSpacer: {
|
||||
width: 0,
|
||||
},
|
||||
|
||||
// Large Header
|
||||
largeHeader: {
|
||||
alignItems: "center",
|
||||
paddingBottom: spacing.lg,
|
||||
},
|
||||
label: {
|
||||
...typography.caption1,
|
||||
fontWeight: "600",
|
||||
letterSpacing: 3,
|
||||
color: colors.primary,
|
||||
textAlign: "center",
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
title: {
|
||||
...typography.title1,
|
||||
color: colors.text,
|
||||
textAlign: "center",
|
||||
},
|
||||
separator: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginVertical: spacing.md,
|
||||
width: 140,
|
||||
},
|
||||
separatorLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: "rgba(233, 69, 96, 0.3)",
|
||||
},
|
||||
separatorDiamond: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
backgroundColor: colors.primary,
|
||||
transform: [{ rotate: "45deg" }],
|
||||
marginHorizontal: spacing.md,
|
||||
opacity: 0.6,
|
||||
},
|
||||
statsBadge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "rgba(233, 69, 96, 0.1)",
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: radius.full,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(233, 69, 96, 0.2)",
|
||||
gap: spacing.sm,
|
||||
},
|
||||
statsBadgeText: {
|
||||
...typography.caption1,
|
||||
color: colors.textSecondary,
|
||||
fontWeight: "500",
|
||||
},
|
||||
|
||||
// Scroll Content
|
||||
scrollContent: {
|
||||
paddingTop: 60, // Match other pages
|
||||
paddingBottom: Platform.OS === "ios" ? 120 : 100,
|
||||
},
|
||||
categoriesContainer: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
|
||||
// Category Section
|
||||
categorySection: {
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
categoryHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: spacing.sm,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
categoryHeaderLeft: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.sm,
|
||||
},
|
||||
categoryIcon: {
|
||||
fontSize: 18,
|
||||
},
|
||||
categoryName: {
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
fontSize: 13,
|
||||
},
|
||||
categoryBadge: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 10,
|
||||
minWidth: 20,
|
||||
alignItems: "center",
|
||||
},
|
||||
categoryBadgeText: {
|
||||
...typography.caption2,
|
||||
color: colors.text,
|
||||
fontWeight: "600",
|
||||
},
|
||||
packsList: {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.03)",
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.glass.border,
|
||||
overflow: "hidden",
|
||||
},
|
||||
|
||||
// Pack Row
|
||||
packRow: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.glass.border,
|
||||
},
|
||||
packRowMain: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: spacing.md,
|
||||
gap: spacing.md,
|
||||
},
|
||||
packIcon: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 12,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.08)",
|
||||
},
|
||||
packIconEnabled: {
|
||||
backgroundColor: "rgba(233, 69, 96, 0.1)",
|
||||
borderColor: "rgba(233, 69, 96, 0.2)",
|
||||
},
|
||||
packIconText: {
|
||||
fontSize: 20,
|
||||
},
|
||||
packContent: {
|
||||
flex: 1,
|
||||
},
|
||||
packTitleRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.xs,
|
||||
},
|
||||
packName: {
|
||||
...typography.body,
|
||||
color: colors.text,
|
||||
fontWeight: "500",
|
||||
},
|
||||
packMeta: {
|
||||
...typography.caption1,
|
||||
color: colors.textTertiary,
|
||||
marginTop: 2,
|
||||
},
|
||||
packMetaEnabled: {
|
||||
color: colors.primary,
|
||||
},
|
||||
|
||||
// Expanded Pack
|
||||
packExpanded: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingBottom: spacing.md,
|
||||
paddingTop: 0,
|
||||
},
|
||||
packDescription: {
|
||||
...typography.subhead,
|
||||
color: colors.textSecondary,
|
||||
lineHeight: 20,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
packActions: {
|
||||
flexDirection: "row",
|
||||
gap: spacing.sm,
|
||||
},
|
||||
packInfoButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.xs,
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
||||
borderWidth: 1,
|
||||
borderColor: colors.glass.border,
|
||||
},
|
||||
packInfoButtonText: {
|
||||
...typography.subhead,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
packToggleButton: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: spacing.xs,
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.12)",
|
||||
},
|
||||
packToggleButtonEnabled: {
|
||||
backgroundColor: "rgba(233, 69, 96, 0.1)",
|
||||
borderColor: "rgba(233, 69, 96, 0.3)",
|
||||
},
|
||||
packToggleButtonLocked: {
|
||||
backgroundColor: "rgba(212, 175, 55, 0.1)",
|
||||
borderColor: "rgba(212, 175, 55, 0.3)",
|
||||
},
|
||||
packToggleButtonText: {
|
||||
...typography.subhead,
|
||||
color: colors.text,
|
||||
fontWeight: "500",
|
||||
},
|
||||
packToggleButtonTextEnabled: {
|
||||
color: colors.primary,
|
||||
},
|
||||
packToggleButtonTextLocked: {
|
||||
color: colors.premium.gold,
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user