Files
physical-expo/app/search.tsx
2026-01-13 11:10:07 +08:00

1087 lines
31 KiB
TypeScript

import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { useTheme } from "@/context/ThemeContext";
import { BlurView } from "expo-blur";
import { LinearGradient } from "expo-linear-gradient";
import { Stack, useRouter } from "expo-router";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Animated,
FlatList,
ScrollView,
StyleSheet,
TextInput,
TouchableOpacity,
View
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
type SearchType = "team" | "player" | "league";
interface SearchEntity {
id: string;
type: SearchType;
name: string;
meta: {
country?: string;
league?: string;
season?: string;
team?: string;
pos?: string;
};
}
// 模拟数据
const ENTITIES: SearchEntity[] = [
{ id: "t_rm", type: "team", name: "Real Madrid", meta: { country: "Spain", league: "LaLiga", season: "2025/26" } },
{ id: "t_fcb", type: "team", name: "FC Barcelona", meta: { country: "Spain", league: "LaLiga", season: "2025/26" } },
{ id: "t_mci", type: "team", name: "Manchester City", meta: { country: "England", league: "Premier League", season: "2025/26" } },
{ id: "t_liv", type: "team", name: "Liverpool", meta: { country: "England", league: "Premier League", season: "2025/26" } },
{ id: "t_ars", type: "team", name: "Arsenal", meta: { country: "England", league: "Premier League", season: "2025/26" } },
{ id: "p_mbappe", type: "player", name: "Kylian Mbappé", meta: { country: "France", team: "Real Madrid", league: "LaLiga", pos: "FW", season: "2025/26" } },
{ id: "p_haaland", type: "player", name: "Erling Haaland", meta: { country: "Norway", team: "Manchester City", league: "Premier League", pos: "FW", season: "2025/26" } },
{ id: "p_salah", type: "player", name: "Mohamed Salah", meta: { country: "Egypt", team: "Liverpool", league: "Premier League", pos: "FW", season: "2025/26" } },
{ id: "p_curry", type: "player", name: "Stephen Curry", meta: { country: "USA", team: "Golden State Warriors", league: "NBA", pos: "G", season: "2025/26" } },
{ id: "l_epl", type: "league", name: "Premier League", meta: { country: "England", season: "2025/26" } },
{ id: "l_laliga", type: "league", name: "LaLiga", meta: { country: "Spain", season: "2025/26" } },
{ id: "l_ucl", type: "league", name: "UEFA Champions League", meta: { country: "Europe", season: "2025/26" } },
{ id: "l_nba", type: "league", name: "NBA", meta: { country: "USA", season: "2025/26" } },
];
// 生成首字母
function getInitials(name: string): string {
const s = name.trim();
if (!s) return "SN";
const clean = s.replace(/[^a-zA-Z0-9]+/g, " ").trim();
const parts = clean ? clean.split(/\s+/) : [s];
const a = (parts[0] || s).charAt(0) || "S";
const b = (parts[1] || "").charAt(0) || ((parts[0] || s).charAt(1) || "N");
return (a + b).toUpperCase();
}
// 生成颜色哈希
function hashString(str: string): number {
let hash = 2166136261;
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash = (hash * 16777619) >>> 0;
}
return hash >>> 0;
}
// HSL 转 RGB
function hslToRgb(h: number, s: number, l: number): string {
s /= 100;
l /= 100;
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = l - c / 2;
let r = 0, g = 0, b = 0;
if (0 <= h && h < 60) {
r = c; g = x; b = 0;
} else if (60 <= h && h < 120) {
r = x; g = c; b = 0;
} else if (120 <= h && h < 180) {
r = 0; g = c; b = x;
} else if (180 <= h && h < 240) {
r = 0; g = x; b = c;
} else if (240 <= h && h < 300) {
r = x; g = 0; b = c;
} else if (300 <= h && h < 360) {
r = c; g = 0; b = x;
}
r = Math.round((r + m) * 255);
g = Math.round((g + m) * 255);
b = Math.round((b + m) * 255);
return `rgb(${r}, ${g}, ${b})`;
}
// 生成渐变颜色
function getLogoGradient(name: string): { color1: string; color2: string } {
const h = hashString(name);
const hue = h % 360;
const hue2 = (hue + 36 + (h % 24)) % 360;
return {
color1: hslToRgb(hue, 85, 58),
color2: hslToRgb(hue2, 85, 48),
};
}
// 获取副标题文本
function getSubText(entity: SearchEntity): string {
const { meta } = entity;
if (entity.type === "league") {
return `${meta.country || ""} · ${meta.season || ""}`;
}
if (entity.type === "player") {
return `${meta.team || ""} · ${meta.league || ""}`;
}
return `${meta.country || ""} · ${meta.league || ""}`;
}
export default function SearchScreen() {
const router = useRouter();
const { theme } = useTheme();
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const isDark = theme === "dark";
const [searchType, setSearchType] = useState<SearchType>("team");
const [query, setQuery] = useState("");
const [recentSearches, setRecentSearches] = useState<string[]>([
"Real Madrid",
"Premier League",
"Curry",
]);
const [showDetailSheet, setShowDetailSheet] = useState(false);
const [selectedEntity, setSelectedEntity] = useState<SearchEntity | null>(null);
const sheetAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (showDetailSheet) {
Animated.spring(sheetAnim, {
toValue: 1,
useNativeDriver: true,
}).start();
} else {
Animated.timing(sheetAnim, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}).start();
}
}, [showDetailSheet]);
const filteredEntities = ENTITIES.filter((e) => {
if (e.type !== searchType) return false;
if (!query.trim()) return true;
const q = query.toLowerCase();
return (
e.name.toLowerCase().includes(q) || getSubText(e).toLowerCase().includes(q)
);
});
const handleSearchTypeChange = (type: SearchType) => {
setSearchType(type);
setQuery("");
};
const handleRecentClick = (text: string) => {
setQuery(text);
};
const handleEntityPress = (entity: SearchEntity) => {
setSelectedEntity(entity);
setShowDetailSheet(true);
};
const handleClearRecent = () => {
setRecentSearches([]);
};
const handleSaveToRecent = () => {
if (selectedEntity) {
const newRecent = [
selectedEntity.name,
...recentSearches.filter((x) => x !== selectedEntity.name),
].slice(0, 10);
setRecentSearches(newRecent);
}
};
const handleOpenDetail = () => {
if (selectedEntity) {
handleSaveToRecent();
// TODO: Navigate to detail page
setShowDetailSheet(false);
}
};
const renderSearchBar = () => (
<View style={styles.searchWrap}>
<BlurView
intensity={80}
tint={isDark ? "dark" : "light"}
style={[
styles.search,
{
backgroundColor: isDark
? "rgba(0, 0, 0, 0.28)"
: "rgba(255, 255, 255, 0.8)",
borderColor: isDark
? "rgba(255, 255, 255, 0.12)"
: "rgba(0, 0, 0, 0.1)",
},
]}
>
<IconSymbol name="search" size={18} color={isDark ? "rgba(255,255,255,0.85)" : "rgba(0,0,0,0.6)"} />
<TextInput
style={[styles.searchInput, { color: isDark ? "#fff" : "#000" }]}
placeholder={t("search.placeholder")}
placeholderTextColor={isDark ? "rgba(255,255,255,0.4)" : "rgba(0,0,0,0.4)"}
value={query}
onChangeText={setQuery}
/>
{query.length > 0 && (
<TouchableOpacity onPress={() => setQuery("")}>
<IconSymbol name="close-circle" size={18} color={isDark ? "rgba(255,255,255,0.68)" : "rgba(0,0,0,0.5)"} />
</TouchableOpacity>
)}
</BlurView>
</View>
);
const renderChips = () => (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.chips}
contentContainerStyle={styles.chipsContent}
>
{(["team", "player", "league"] as SearchType[]).map((type) => (
<TouchableOpacity
key={type}
style={[
styles.chip,
searchType === type && styles.chipActive,
{
backgroundColor: isDark
? searchType === type
? "rgba(255, 255, 255, 0.12)"
: "rgba(255, 255, 255, 0.06)"
: searchType === type
? "rgba(0, 0, 0, 0.08)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? searchType === type
? "rgba(255, 255, 255, 0.14)"
: "rgba(255, 255, 255, 0.10)"
: searchType === type
? "rgba(0, 0, 0, 0.12)"
: "rgba(0, 0, 0, 0.08)",
},
]}
onPress={() => handleSearchTypeChange(type)}
>
<ThemedText
style={[
styles.chipText,
searchType === type && styles.chipTextActive,
]}
>
{t(`search.type.${type}`)}
</ThemedText>
</TouchableOpacity>
))}
</ScrollView>
);
const renderRecentSearches = () => {
if (recentSearches.length === 0 || query.trim()) return null;
return (
<>
<View style={styles.sectionHeader}>
<ThemedText style={styles.sectionTitle}>
{t("search.recent")}
</ThemedText>
<TouchableOpacity onPress={handleClearRecent}>
<ThemedText style={styles.sectionAction}>
{t("search.clear")}
</ThemedText>
</TouchableOpacity>
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.recentRow}
contentContainerStyle={styles.recentRowContent}
>
{recentSearches.map((item, index) => (
<TouchableOpacity
key={index}
style={[
styles.recentCard,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
},
]}
onPress={() => handleRecentClick(item)}
>
<ThemedText style={styles.recentCardTitle} numberOfLines={1}>
{item}
</ThemedText>
<ThemedText style={styles.recentCardSub} numberOfLines={1}>
{t(`search.type.${searchType}`)}
</ThemedText>
</TouchableOpacity>
))}
</ScrollView>
</>
);
};
const renderEntityItem = ({ item }: { item: SearchEntity }) => {
const gradient = getLogoGradient(item.name);
const initials = getInitials(item.name);
return (
<TouchableOpacity
style={[
styles.entityRow,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.06)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
},
]}
onPress={() => handleEntityPress(item)}
>
<LinearGradient
colors={[gradient.color1, gradient.color2]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={[
styles.logoDot,
{
borderColor: isDark
? "rgba(255, 255, 255, 0.14)"
: "rgba(255, 255, 255, 0.3)",
},
]}
>
<ThemedText style={styles.logoText}>{initials}</ThemedText>
</LinearGradient>
<View style={styles.entityMeta}>
<ThemedText style={styles.entityName} numberOfLines={1}>
{item.name}
</ThemedText>
<ThemedText style={styles.entitySub} numberOfLines={1}>
{getSubText(item)}
</ThemedText>
</View>
<ThemedText style={styles.entityBadge}>
{t(`search.type.${item.type}`)}
</ThemedText>
</TouchableOpacity>
);
};
const renderEmptyState = () => (
<View
style={[
styles.emptyState,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
},
]}
>
<ThemedText style={styles.emptyTitle}>
{t("search.no_results")}
</ThemedText>
<ThemedText style={styles.emptySub}>
{t("search.no_results_hint")}
</ThemedText>
</View>
);
const renderDetailSheet = () => {
if (!selectedEntity) return null;
const translateY = sheetAnim.interpolate({
inputRange: [0, 1],
outputRange: [600, 0],
});
return (
<>
<TouchableOpacity
style={styles.overlay}
activeOpacity={1}
onPress={() => setShowDetailSheet(false)}
>
<BlurView
intensity={20}
tint="dark"
style={StyleSheet.absoluteFill}
/>
</TouchableOpacity>
<Animated.View
style={[
styles.sheet,
{
transform: [{ translateY }],
backgroundColor: isDark
? "rgba(18, 20, 26, 0.96)"
: "rgba(255, 255, 255, 0.96)",
borderColor: isDark
? "rgba(255, 255, 255, 0.12)"
: "rgba(0, 0, 0, 0.1)",
},
{ paddingBottom: insets.bottom + 14 },
]}
>
<View style={styles.sheetHeader}>
<View style={styles.sheetTitleContainer}>
<ThemedText style={styles.sheetTitle}>
{selectedEntity.name}
</ThemedText>
<ThemedText style={styles.sheetSub}>
{t(`search.type.${selectedEntity.type}`)} ·{" "}
{getSubText(selectedEntity)}
</ThemedText>
</View>
<TouchableOpacity
style={[
styles.iconBtn,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.06)"
: "rgba(0, 0, 0, 0.06)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.10)",
},
]}
onPress={() => setShowDetailSheet(false)}
>
<IconSymbol
name="close"
size={18}
color={isDark ? "rgba(255,255,255,0.9)" : "rgba(0,0,0,0.9)"}
/>
</TouchableOpacity>
</View>
<View style={styles.kvContainer}>
{selectedEntity.type === "team" && (
<>
<View
style={[
styles.kvItem,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
},
]}
>
<ThemedText style={styles.kvLabel}>
{t("search.detail.country")}
</ThemedText>
<ThemedText style={styles.kvValue} numberOfLines={1}>
{selectedEntity.meta.country || "-"}
</ThemedText>
</View>
<View
style={[
styles.kvItem,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
},
]}
>
<ThemedText style={styles.kvLabel}>
{t("search.detail.league")}
</ThemedText>
<ThemedText style={styles.kvValue} numberOfLines={1}>
{selectedEntity.meta.league || "-"}
</ThemedText>
</View>
<View
style={[
styles.kvItem,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
},
]}
>
<ThemedText style={styles.kvLabel}>
{t("search.detail.season")}
</ThemedText>
<ThemedText style={styles.kvValue} numberOfLines={1}>
{selectedEntity.meta.season || "-"}
</ThemedText>
</View>
</>
)}
{selectedEntity.type === "player" && (
<>
<View
style={[
styles.kvItem,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
},
]}
>
<ThemedText style={styles.kvLabel}>
{t("search.detail.team")}
</ThemedText>
<ThemedText style={styles.kvValue} numberOfLines={1}>
{selectedEntity.meta.team || "-"}
</ThemedText>
</View>
<View
style={[
styles.kvItem,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
},
]}
>
<ThemedText style={styles.kvLabel}>
{t("search.detail.league")}
</ThemedText>
<ThemedText style={styles.kvValue} numberOfLines={1}>
{selectedEntity.meta.league || "-"}
</ThemedText>
</View>
<View
style={[
styles.kvItem,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
},
]}
>
<ThemedText style={styles.kvLabel}>
{t("search.detail.position")}
</ThemedText>
<ThemedText style={styles.kvValue} numberOfLines={1}>
{selectedEntity.meta.pos || "-"}
</ThemedText>
</View>
<View
style={[
styles.kvItem,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
},
]}
>
<ThemedText style={styles.kvLabel}>
{t("search.detail.country")}
</ThemedText>
<ThemedText style={styles.kvValue} numberOfLines={1}>
{selectedEntity.meta.country || "-"}
</ThemedText>
</View>
</>
)}
{selectedEntity.type === "league" && (
<>
<View
style={[
styles.kvItem,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
},
]}
>
<ThemedText style={styles.kvLabel}>
{t("search.detail.country")}
</ThemedText>
<ThemedText style={styles.kvValue} numberOfLines={1}>
{selectedEntity.meta.country || "-"}
</ThemedText>
</View>
<View
style={[
styles.kvItem,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
},
]}
>
<ThemedText style={styles.kvLabel}>
{t("search.detail.season")}
</ThemedText>
<ThemedText style={styles.kvValue} numberOfLines={1}>
{selectedEntity.meta.season || "-"}
</ThemedText>
</View>
</>
)}
</View>
<View style={styles.sheetActions}>
<TouchableOpacity
style={[
styles.sheetBtn,
{
backgroundColor: isDark
? "rgba(0, 0, 0, 0.14)"
: "rgba(0, 0, 0, 0.06)",
borderColor: isDark
? "rgba(255, 255, 255, 0.12)"
: "rgba(0, 0, 0, 0.1)",
},
]}
onPress={handleOpenDetail}
>
<ThemedText style={styles.sheetBtnText}>
{t("search.detail.open")}
</ThemedText>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.sheetBtn,
styles.sheetBtnPrimary,
{
backgroundColor: isDark
? "rgba(255, 210, 90, 0.12)"
: "rgba(255, 210, 90, 0.2)",
borderColor: isDark
? "rgba(255, 210, 90, 0.28)"
: "rgba(255, 210, 90, 0.4)",
},
]}
onPress={handleSaveToRecent}
>
<ThemedText
style={[
styles.sheetBtnText,
styles.sheetBtnTextPrimary,
]}
>
{t("search.detail.save")}
</ThemedText>
</TouchableOpacity>
</View>
</Animated.View>
</>
);
};
return (
<ThemedView style={styles.container}>
<Stack.Screen
options={{
headerShown: false,
animation: "slide_from_right",
}}
/>
<BlurView
intensity={80}
tint={isDark ? "dark" : "light"}
style={[
styles.nav,
{
backgroundColor: isDark
? "rgba(7, 8, 11, 0.95)"
: "rgba(255, 255, 255, 0.95)",
},
{ paddingTop: insets.top + 10 },
]}
>
<TouchableOpacity
style={[
styles.iconBtn,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.06)"
: "rgba(0, 0, 0, 0.06)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.10)",
},
]}
onPress={() => router.back()}
>
<IconSymbol
name="chevron-back"
size={20}
color={isDark ? "rgba(255,255,255,0.9)" : "rgba(0,0,0,0.9)"}
/>
</TouchableOpacity>
<View style={styles.brand}>
<ThemedText style={styles.brandTitle}>{t("search.title")}</ThemedText>
<ThemedText style={styles.brandSub}>
{t("search.subtitle")}
</ThemedText>
</View>
<TouchableOpacity
style={[
styles.iconBtn,
{
backgroundColor: isDark
? "rgba(255, 255, 255, 0.06)"
: "rgba(0, 0, 0, 0.06)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.10)",
},
]}
onPress={handleClearRecent}
>
<IconSymbol
name="refresh"
size={18}
color={isDark ? "rgba(255,255,255,0.9)" : "rgba(0,0,0,0.9)"}
/>
</TouchableOpacity>
</BlurView>
<View style={styles.content}>
<View style={styles.contentContainer}>
{renderSearchBar()}
{renderChips()}
{renderRecentSearches()}
<View style={styles.sectionHeader}>
<ThemedText style={styles.sectionTitle}>
{t("search.results")} · {t(`search.type.${searchType}`)}
</ThemedText>
<ThemedText style={styles.sectionAction}>
{query.trim()
? `${filteredEntities.length} ${t("search.found")}`
: t("search.tap_to_open")}
</ThemedText>
</View>
</View>
<FlatList
data={filteredEntities}
keyExtractor={(item) => item.id}
renderItem={renderEntityItem}
ListEmptyComponent={() => renderEmptyState()}
contentContainerStyle={[
styles.listContent,
{ paddingBottom: insets.bottom + 20 },
]}
showsVerticalScrollIndicator={false}
/>
</View>
{showDetailSheet && renderDetailSheet()}
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
nav: {
flexDirection: "row",
alignItems: "center",
gap: 10,
paddingHorizontal: 12,
paddingBottom: 12,
position: "sticky",
top: 0,
zIndex: 40,
},
iconBtn: {
width: 38,
height: 38,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
borderWidth: 1,
},
brand: {
flex: 1,
minWidth: 0,
gap: 2,
},
brandTitle: {
fontSize: 18,
fontWeight: "900",
letterSpacing: 0.2,
},
brandSub: {
fontSize: 12,
opacity: 0.42,
fontWeight: "800",
},
content: {
flex: 1,
},
contentContainer: {
paddingHorizontal: 12,
paddingTop: 8,
},
listContent: {
paddingHorizontal: 12,
},
searchWrap: {
marginBottom: 10,
},
search: {
flexDirection: "row",
alignItems: "center",
gap: 8,
height: 46,
paddingHorizontal: 12,
borderRadius: 18,
borderWidth: 1,
overflow: "hidden",
},
searchInput: {
flex: 1,
minWidth: 0,
fontSize: 15,
},
chips: {
marginBottom: 8,
},
chipsContent: {
gap: 10,
paddingBottom: 2,
},
chip: {
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 999,
borderWidth: 1,
},
chipActive: {},
chipText: {
fontSize: 14,
fontWeight: "900",
opacity: 0.78,
},
chipTextActive: {
opacity: 1,
},
sectionHeader: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginTop: 12,
marginBottom: 8,
},
sectionTitle: {
fontSize: 13,
opacity: 0.55,
fontWeight: "900",
},
sectionAction: {
fontSize: 12,
opacity: 0.42,
fontWeight: "800",
},
recentRow: {
marginBottom: 8,
},
recentRowContent: {
gap: 10,
paddingBottom: 2,
},
recentCard: {
minWidth: 190,
padding: 12,
borderRadius: 16,
borderWidth: 1,
},
recentCardTitle: {
fontWeight: "900",
},
recentCardSub: {
fontSize: 12,
opacity: 0.55,
marginTop: 4,
},
entityRow: {
flexDirection: "row",
alignItems: "center",
gap: 12,
padding: 12,
borderRadius: 18,
borderWidth: 1,
marginBottom: 10,
},
logoDot: {
width: 34,
height: 34,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
borderWidth: 1,
},
logoText: {
fontSize: 12,
fontWeight: "900",
color: "rgba(255, 255, 255, 0.92)",
},
entityMeta: {
flex: 1,
minWidth: 0,
},
entityName: {
fontWeight: "900",
},
entitySub: {
fontSize: 12,
opacity: 0.55,
marginTop: 3,
},
entityBadge: {
fontSize: 12,
opacity: 0.52,
fontWeight: "900",
},
emptyState: {
padding: 14,
borderRadius: 20,
borderWidth: 1,
},
emptyTitle: {
fontWeight: "900",
},
emptySub: {
marginTop: 6,
opacity: 0.6,
fontSize: 12,
fontWeight: "800",
lineHeight: 18,
},
overlay: {
...StyleSheet.absoluteFillObject,
zIndex: 80,
},
sheet: {
position: "absolute",
left: 0,
right: 0,
bottom: 0,
borderTopLeftRadius: 22,
borderTopRightRadius: 22,
borderWidth: 1,
padding: 12,
zIndex: 90,
shadowColor: "#000",
shadowOffset: { width: 0, height: -10 },
shadowOpacity: 0.3,
shadowRadius: 30,
elevation: 20,
},
sheetHeader: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: 10,
marginBottom: 12,
},
sheetTitleContainer: {
flex: 1,
minWidth: 0,
},
sheetTitle: {
fontWeight: "900",
},
sheetSub: {
fontSize: 12,
opacity: 0.52,
fontWeight: "800",
marginTop: 2,
},
kvContainer: {
flexDirection: "row",
flexWrap: "wrap",
gap: 10,
marginBottom: 12,
},
kvItem: {
flex: 1,
minWidth: "45%",
padding: 12,
borderRadius: 18,
borderWidth: 1,
},
kvLabel: {
fontSize: 12,
opacity: 0.52,
fontWeight: "900",
},
kvValue: {
marginTop: 6,
fontWeight: "900",
},
sheetActions: {
flexDirection: "row",
gap: 10,
},
sheetBtn: {
flex: 1,
height: 36,
paddingHorizontal: 12,
borderRadius: 14,
borderWidth: 1,
alignItems: "center",
justifyContent: "center",
},
sheetBtnPrimary: {},
sheetBtnText: {
fontSize: 14,
fontWeight: "900",
opacity: 0.86,
},
sheetBtnTextPrimary: {
opacity: 1,
},
});