Files
physical-expo/app/search.tsx
2026-01-15 15:03:31 +08:00

1186 lines
32 KiB
TypeScript

import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme";
import { useAppState } from "@/context/AppStateContext";
import { useTheme } from "@/context/ThemeContext";
import { fetchSearch } from "@/lib/api";
import { SearchLeague, SearchPlayer, SearchTeam } from "@/types/api";
import AsyncStorage from "@react-native-async-storage/async-storage";
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 {
ActivityIndicator,
Animated,
FlatList,
ScrollView,
StyleSheet,
TextInput,
TouchableOpacity,
View
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
type SearchType = "all" | "team" | "player" | "league";
interface SearchEntity {
id: string;
type: SearchType;
name: string;
meta: {
country?: string;
league?: string;
season?: string;
team?: string;
pos?: string;
};
}
// 将 API 数据转换为 SearchEntity
function convertLeagueToEntity(league: SearchLeague): SearchEntity {
return {
id: `league_${league.ID}`,
type: "league",
name: league.name,
meta: {
country: league.countryName,
season: "", // API 中没有 season 字段
},
};
}
function convertPlayerToEntity(player: SearchPlayer): SearchEntity {
return {
id: `player_${player.ID}`,
type: "player",
name: player.name,
meta: {
country: player.countryName,
team: player.teamName,
league: player.leagueName,
pos: player.position,
},
};
}
function convertTeamToEntity(team: SearchTeam): SearchEntity {
return {
id: `team_${team.ID}`,
type: "team",
name: team.name,
meta: {
country: team.countryName,
league: team.leagueName,
},
};
}
// 生成首字母
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;
const parts: string[] = [];
if (entity.type === "league") {
// 联赛:显示国家
if (meta.country) parts.push(meta.country);
} else if (entity.type === "player") {
// 球员:优先显示球队和位置,如果没有位置则显示联赛
if (meta.team) parts.push(meta.team);
if (meta.pos) {
parts.push(meta.pos);
} else if (meta.league) {
parts.push(meta.league);
}
} else if (entity.type === "team") {
// 球队:显示国家和联赛
if (meta.country) parts.push(meta.country);
if (meta.league) parts.push(meta.league);
}
return parts.filter(Boolean).join(" · ") || "";
}
const RECENT_SEARCHES_KEY = "recent_searches";
export default function SearchScreen() {
const router = useRouter();
const { theme } = useTheme();
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const isDark = theme === "dark";
const { state } = useAppState();
const [searchType, setSearchType] = useState<SearchType>("all");
const [query, setQuery] = useState("");
const [recentSearches, setRecentSearches] = useState<string[]>([]);
const [showDetailSheet, setShowDetailSheet] = useState(false);
const [selectedEntity, setSelectedEntity] = useState<SearchEntity | null>(null);
const [entities, setEntities] = useState<SearchEntity[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const sheetAnim = useRef(new Animated.Value(0)).current;
const searchTimeoutRef = useRef<number | null>(null);
useEffect(() => {
const loadRecentSearches = async () => {
try {
const stored = await AsyncStorage.getItem(RECENT_SEARCHES_KEY);
if (stored) {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed)) {
setRecentSearches(parsed);
}
}
} catch (err) {
console.error("Failed to load recent searches:", err);
}
};
loadRecentSearches();
}, []);
useEffect(() => {
if (showDetailSheet) {
Animated.spring(sheetAnim, {
toValue: 1,
useNativeDriver: true,
}).start();
} else {
Animated.timing(sheetAnim, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}).start();
}
}, [showDetailSheet]);
// 搜索 API 调用(带防抖)
useEffect(() => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
if (!query.trim()) {
setEntities([]);
setError(null);
return;
}
searchTimeoutRef.current = setTimeout(async () => {
try {
setLoading(true);
setError(null);
const result = await fetchSearch(
query.trim(),
state.selectedSportId || undefined
);
const allEntities: SearchEntity[] = [
...result.leagues.map(convertLeagueToEntity),
...result.players.map(convertPlayerToEntity),
...result.teams.map(convertTeamToEntity),
];
setEntities(allEntities);
if (allEntities.length > 0) {
const searchQuery = query.trim();
setRecentSearches((prev) => {
const filtered = prev.filter((x) => x !== searchQuery);
const newRecent = [searchQuery, ...filtered].slice(0, 8);
AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecent)).catch(
(err) => console.error("Failed to save recent searches:", err)
);
return newRecent;
});
}
} catch (err: any) {
console.error("Search error:", err);
setError(err?.message || t("search.error"));
setEntities([]);
} finally {
setLoading(false);
}
}, 500); // 500ms 防抖
return () => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
};
}, [query, state.selectedSportId]);
const filteredEntities = entities.filter((e) => {
if (searchType === "all") return true;
if (e.type !== searchType) return false;
return true;
});
const handleSearchTypeChange = (type: SearchType) => {
setSearchType(type);
// 不再清空输入框,保持搜索结果
};
const handleRecentClick = (text: string) => {
setQuery(text);
};
const handleEntityPress = (entity: SearchEntity) => {
setSelectedEntity(entity);
setShowDetailSheet(true);
};
const handleClearRecent = () => {
setRecentSearches([]);
AsyncStorage.removeItem(RECENT_SEARCHES_KEY).catch(
(err) => console.error("Failed to clear recent searches:", err)
);
};
const handleSaveToRecent = () => {
if (selectedEntity) {
const newRecent = [
selectedEntity.name,
...recentSearches.filter((x) => x !== selectedEntity.name),
].slice(0, 8);
setRecentSearches(newRecent);
AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecent)).catch(
(err) => console.error("Failed to save recent searches:", err)
);
}
};
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}
>
{(["all", "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" && (
<>
{selectedEntity.meta.country && (
<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.meta.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.league")}
</ThemedText>
<ThemedText style={styles.kvValue} numberOfLines={1}>
{selectedEntity.meta.league}
</ThemedText>
</View>
)}
</>
)}
{selectedEntity.type === "player" && (
<>
{selectedEntity.meta.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.team")}
</ThemedText>
<ThemedText style={styles.kvValue} numberOfLines={1}>
{selectedEntity.meta.team}
</ThemedText>
</View>
)}
{selectedEntity.meta.pos && (
<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>
)}
{selectedEntity.meta.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.league")}
</ThemedText>
<ThemedText style={styles.kvValue} numberOfLines={1}>
{selectedEntity.meta.league}
</ThemedText>
</View>
)}
{selectedEntity.meta.country && (
<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" && (
<>
{selectedEntity.meta.country && (
<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>
<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>
</BlurView>
<View style={styles.content}>
<View style={styles.contentContainer}>
{renderSearchBar()}
{renderChips()}
{renderRecentSearches()}
{query.trim() && (
<View style={styles.sectionHeader}>
<ThemedText style={styles.sectionTitle}>
{t("search.results")}
{searchType !== "all" && ` · ${t(`search.type.${searchType}`)}`}
</ThemedText>
<ThemedText style={styles.sectionAction}>
{loading
? t("search.loading")
: error
? t("search.error")
: `${filteredEntities.length} ${t("search.found")}`}
</ThemedText>
</View>
)}
</View>
{query.trim() ? (
loading ? (
<ThemedView style={styles.center}>
<ActivityIndicator size="large" color={Colors[theme].tint} />
</ThemedView>
) : error ? (
<View style={styles.emptyState}>
<ThemedText style={styles.emptyTitle}>{error}</ThemedText>
<ThemedText style={styles.emptySub}>
{t("search.error_hint")}
</ThemedText>
</View>
) : (
<FlatList
data={filteredEntities}
keyExtractor={(item) => item.id}
renderItem={renderEntityItem}
ListEmptyComponent={() => renderEmptyState()}
contentContainerStyle={[
styles.listContent,
{ paddingBottom: insets.bottom + 20 },
]}
showsVerticalScrollIndicator={false}
/>
)
) : null}
</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",
},
center: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
},
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,
},
});