Sergei 1301c6e093 Make sensor location tappable to navigate to Device Settings
Added TouchableOpacity wrapper around the location text in the equipment
list so users can tap on a sensor's location to go directly to its
Device Settings screen.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-19 23:17:00 -08:00

911 lines
28 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
ActivityIndicator,
RefreshControl,
Platform,
ActionSheetIOS,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router, useLocalSearchParams } from 'expo-router';
import * as Device from 'expo-device';
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
import { useBLE } from '@/contexts/BLEContext';
import { api } from '@/services/api';
import type { WPSensor } from '@/types';
import {
AppColors,
BorderRadius,
FontSizes,
FontWeights,
Spacing,
Shadows,
} from '@/constants/theme';
import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu';
const sensorConfig = {
icon: 'water' as const,
label: 'WP Sensor',
color: AppColors.primary,
bgColor: AppColors.primaryLighter,
};
export default function EquipmentScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const { currentBeneficiary } = useBeneficiary();
const { isBLEAvailable, scanDevices, stopScan, foundDevices, isScanning: isBLEScanning } = useBLE();
// Separate state for API sensors (attached) and BLE sensors (nearby)
const [apiSensors, setApiSensors] = useState<WPSensor[]>([]);
const [bleSensors, setBleSensors] = useState<WPSensor[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isDetaching, setIsDetaching] = useState<string | null>(null);
const beneficiaryName = currentBeneficiary?.name || 'this person';
useEffect(() => {
loadSensors();
}, [id]);
const loadSensors = async () => {
if (!id) return;
try {
setIsLoading(true);
// Get WP sensors from API (attached to beneficiary)
const response = await api.getDevicesForBeneficiary(id);
if (!response.ok) {
// If error is "Not authenticated with Legacy API" or network error,
// just show empty state without Alert
console.warn('[Equipment] Could not load sensors:', response.error);
setApiSensors([]);
return;
}
setApiSensors(response.data || []);
} catch (error) {
console.error('[Equipment] Failed to load sensors:', error);
// Show empty state instead of Alert
setApiSensors([]);
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
};
const handleRefresh = useCallback(() => {
setIsRefreshing(true);
loadSensors();
}, [id]);
// BLE Scan for nearby sensors
const handleScanNearby = async () => {
if (isBLEScanning) {
// Stop scan
stopScan();
return;
}
setBleSensors([]); // Clear previous results
try {
await scanDevices();
// foundDevices will be updated by BLEContext
} catch (error) {
console.error('[Equipment] BLE scan failed:', error);
Alert.alert('Scan Failed', 'Could not scan for nearby sensors. Make sure Bluetooth is enabled.');
}
};
// Effect to convert BLE foundDevices to WPSensor format
useEffect(() => {
if (foundDevices.length === 0) return;
// Convert BLE devices to WPSensor format
const nearbyWPSensors: WPSensor[] = foundDevices
.filter((d: { name?: string }) => d.name?.startsWith('WP_')) // Only WP sensors
.map((d: { id: string; name?: string }) => {
// Parse WP_<wellId>_<mac> format
const parts = d.name!.split('_');
const wellId = parseInt(parts[1], 10) || 0;
const mac = parts[2] || d.id.slice(-6);
return {
deviceId: d.id,
wellId: wellId,
mac: mac,
name: d.name!,
status: 'offline' as const, // Nearby but not attached
lastSeen: new Date(),
beneficiaryId: id!,
deploymentId: 0, // Not attached yet
source: 'ble' as const, // From BLE scan
};
});
// Filter out sensors that are already in API list
const apiDeviceIds = new Set(apiSensors.map(s => s.mac));
const uniqueBleSensors = nearbyWPSensors.filter(s => !apiDeviceIds.has(s.mac));
setBleSensors(uniqueBleSensors);
}, [foundDevices, apiSensors, id]);
// Handle sensor click - show action sheet for offline, navigate to settings for online
const handleSensorPress = (sensor: WPSensor) => {
// For offline API sensors - show reconnect options
if (sensor.source === 'api' && sensor.status === 'offline') {
if (Platform.OS === 'ios') {
ActionSheetIOS.showActionSheetWithOptions(
{
title: `${sensor.name} is Offline`,
message: `Last seen: ${formatLastSeen(sensor.lastSeen)}`,
options: ['Cancel', 'Reconnect via Bluetooth', 'Remove from this home'],
destructiveButtonIndex: 2,
cancelButtonIndex: 0,
},
buttonIndex => {
if (buttonIndex === 1) {
// Reconnect - go to setup-wifi flow
router.push(`/(tabs)/beneficiaries/${id}/setup-wifi?deviceId=${sensor.deviceId}&deviceName=${sensor.name}&wellId=${sensor.wellId}` as any);
} else if (buttonIndex === 2) {
// Remove
handleDetachDevice(sensor);
}
}
);
} else {
// Android fallback
Alert.alert(
`${sensor.name} is Offline`,
`Last seen: ${formatLastSeen(sensor.lastSeen)}\n\nWhat would you like to do?`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Reconnect',
onPress: () => router.push(`/(tabs)/beneficiaries/${id}/setup-wifi?deviceId=${sensor.deviceId}&deviceName=${sensor.name}&wellId=${sensor.wellId}` as any)
},
{
text: 'Remove',
style: 'destructive',
onPress: () => handleDetachDevice(sensor)
},
]
);
}
}
// For BLE nearby sensors - go directly to setup
else if (sensor.source === 'ble') {
router.push(`/(tabs)/beneficiaries/${id}/setup-wifi?deviceId=${sensor.deviceId}&deviceName=${sensor.name}&wellId=${sensor.wellId}` as any);
}
// For online API sensors - navigate to settings
else {
handleDeviceSettings(sensor);
}
};
const handleDetachDevice = (sensor: WPSensor) => {
Alert.alert(
'Detach Sensor',
`Are you sure you want to detach "${sensor.name}" from ${beneficiaryName}?\n\nThe sensor will become available for use with another person.`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Detach',
style: 'destructive',
onPress: async () => {
setIsDetaching(sensor.deviceId);
try {
const response = await api.detachDeviceFromBeneficiary(id!, sensor.deviceId);
if (!response.ok) {
throw new Error('Failed to detach sensor');
}
// Remove from local state
setApiSensors(prev => prev.filter(s => s.deviceId !== sensor.deviceId));
Alert.alert('Success', `${sensor.name} has been detached.`);
} catch (error) {
console.error('[Equipment] Failed to detach sensor:', error);
Alert.alert('Error', 'Failed to detach sensor. Please try again.');
} finally {
setIsDetaching(null);
}
},
},
]
);
};
const handleDetachAll = () => {
if (apiSensors.length === 0) {
Alert.alert('No Sensors', 'There are no sensors to detach.');
return;
}
Alert.alert(
'Detach All Sensors',
`Are you sure you want to detach all ${apiSensors.length} sensors from ${beneficiaryName}?\n\nThis action cannot be undone.`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Detach All',
style: 'destructive',
onPress: async () => {
setIsLoading(true);
try {
// Detach all sensors sequentially
for (const sensor of apiSensors) {
await api.detachDeviceFromBeneficiary(id!, sensor.deviceId);
}
setApiSensors([]);
Alert.alert('Success', 'All sensors have been detached.');
} catch (error) {
console.error('[Equipment] Failed to detach all sensors:', error);
Alert.alert('Error', 'Failed to detach sensors. Please try again.');
} finally {
setIsLoading(false);
}
},
},
]
);
};
const handleAddSensor = () => {
// Navigate to Add Sensor screen
router.push(`/(tabs)/beneficiaries/${id}/add-sensor` as any);
};
const handleDeviceSettings = (sensor: WPSensor) => {
// Navigate to Device Settings screen
router.push(`/(tabs)/beneficiaries/${id}/device-settings/${sensor.deviceId}` as any);
};
const formatLastSeen = (lastSeen: Date): string => {
const now = new Date();
const diffMs = now.getTime() - lastSeen.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins} min ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
};
const getStatusColor = (status: 'online' | 'warning' | 'offline') => {
switch (status) {
case 'online':
return AppColors.success;
case 'warning':
return AppColors.warning;
case 'offline':
return AppColors.error;
}
};
const getStatusLabel = (status: 'online' | 'warning' | 'offline') => {
switch (status) {
case 'online':
return 'Online';
case 'warning':
return 'Warning';
case 'offline':
return 'Offline';
}
};
const getSignalStrength = (rssi: number): string => {
if (rssi >= -50) return 'Excellent';
if (rssi >= -60) return 'Good';
if (rssi >= -70) return 'Fair';
return 'Weak';
};
if (isLoading) {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Sensors</Text>
<View style={styles.placeholder} />
</View>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={AppColors.primary} />
<Text style={styles.loadingText}>Loading sensors...</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Sensors</Text>
<View style={styles.headerRight}>
<TouchableOpacity style={styles.addButton} onPress={handleAddSensor}>
<Ionicons name="add" size={24} color={AppColors.primary} />
</TouchableOpacity>
<BeneficiaryMenu
beneficiaryId={id || ''}
userRole={currentBeneficiary?.role}
currentPage="sensors"
/>
</View>
</View>
{/* Simulator Warning */}
{!isBLEAvailable && (
<View style={styles.simulatorWarning}>
<Ionicons name="information-circle" size={18} color={AppColors.warning} />
<Text style={styles.simulatorWarningText}>
Running in Simulator - BLE features use mock data
</Text>
</View>
)}
<ScrollView
style={styles.content}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} />
}
>
{/* Summary Card */}
<View style={styles.summaryCard}>
<View style={styles.summaryRow}>
<View style={styles.summaryItem}>
<Text style={styles.summaryValue}>{apiSensors.length}</Text>
<Text style={styles.summaryLabel}>Total</Text>
</View>
<View style={styles.summaryDivider} />
<View style={styles.summaryItem}>
<Text style={[styles.summaryValue, { color: AppColors.success }]}>
{apiSensors.filter(s => s.status === 'online').length}
</Text>
<Text style={styles.summaryLabel}>Online</Text>
</View>
<View style={styles.summaryDivider} />
<View style={styles.summaryItem}>
<Text style={[styles.summaryValue, { color: AppColors.warning }]}>
{apiSensors.filter(s => s.status === 'warning').length}
</Text>
<Text style={styles.summaryLabel}>Warning</Text>
</View>
<View style={styles.summaryDivider} />
<View style={styles.summaryItem}>
<Text style={[styles.summaryValue, { color: AppColors.error }]}>
{apiSensors.filter(s => s.status === 'offline').length}
</Text>
<Text style={styles.summaryLabel}>Offline</Text>
</View>
</View>
</View>
{/* Connected Sensors Section */}
{apiSensors.length === 0 ? (
<View style={styles.emptyState}>
<View style={styles.emptyIconContainer}>
<Ionicons name="water-outline" size={48} color={AppColors.textMuted} />
</View>
<Text style={styles.emptyTitle}>No Sensors Connected</Text>
<Text style={styles.emptyText}>
Add WP sensors to start monitoring {beneficiaryName}'s wellness.
</Text>
<TouchableOpacity style={styles.addDeviceButton} onPress={handleAddSensor}>
<Ionicons name="add" size={20} color={AppColors.white} />
<Text style={styles.addDeviceButtonText}>Add Sensor</Text>
</TouchableOpacity>
</View>
) : (
<>
<Text style={styles.sectionTitle}>Connected Sensors ({apiSensors.length})</Text>
<View style={styles.devicesList}>
{apiSensors.map((sensor) => {
const isDetachingThis = isDetaching === sensor.deviceId;
const sensorConfig = {
icon: 'water' as const,
color: AppColors.primary,
bgColor: AppColors.primaryLighter,
};
return (
<TouchableOpacity
key={sensor.deviceId}
style={styles.deviceCard}
onPress={() => handleSensorPress(sensor)}
activeOpacity={0.7}
>
<View style={styles.deviceInfo}>
<View style={[styles.deviceIcon, { backgroundColor: sensorConfig.bgColor }]}>
<Ionicons name={sensorConfig.icon} size={22} color={sensorConfig.color} />
</View>
<View style={styles.deviceDetails}>
<Text style={styles.deviceName}>{sensor.name}</Text>
<View style={styles.deviceMeta}>
<View style={[
styles.statusDot,
{ backgroundColor: getStatusColor(sensor.status) }
]} />
<Text style={[styles.deviceStatus, { color: getStatusColor(sensor.status) }]}>
{getStatusLabel(sensor.status)}
</Text>
<Text style={styles.deviceMetaSeparator}>•</Text>
<Text style={styles.deviceRoom}>{formatLastSeen(sensor.lastSeen)}</Text>
</View>
<TouchableOpacity
onPress={(e) => {
e.stopPropagation();
handleDeviceSettings(sensor);
}}
activeOpacity={0.7}
>
<Text style={[
styles.deviceLocation,
!sensor.location && styles.deviceLocationPlaceholder
]}>
{sensor.location || 'No location set'}
</Text>
</TouchableOpacity>
</View>
</View>
<View style={styles.deviceActions}>
<TouchableOpacity
style={styles.settingsButton}
onPress={(e) => {
e.stopPropagation();
handleDeviceSettings(sensor);
}}
>
<Ionicons name="settings-outline" size={20} color={AppColors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={styles.detachButton}
onPress={(e) => {
e.stopPropagation();
handleDetachDevice(sensor);
}}
disabled={isDetachingThis}
>
{isDetachingThis ? (
<ActivityIndicator size="small" color={AppColors.error} />
) : (
<Ionicons name="unlink-outline" size={20} color={AppColors.error} />
)}
</TouchableOpacity>
</View>
</TouchableOpacity>
);
})}
</View>
{/* Detach All Button */}
{apiSensors.length > 1 && (
<TouchableOpacity style={styles.detachAllButton} onPress={handleDetachAll}>
<Ionicons name="trash-outline" size={20} color={AppColors.error} />
<Text style={styles.detachAllText}>Detach All Sensors</Text>
</TouchableOpacity>
)}
</>
)}
{/* Scan Nearby Button */}
<TouchableOpacity
style={[styles.scanButton, isBLEScanning && styles.scanButtonActive]}
onPress={handleScanNearby}
disabled={!isBLEAvailable}
>
{isBLEScanning ? (
<>
<ActivityIndicator size="small" color={AppColors.white} />
<Text style={styles.scanButtonText}>Scanning... ({bleSensors.length} found)</Text>
</>
) : (
<>
<Ionicons name="bluetooth" size={20} color={AppColors.white} />
<Text style={styles.scanButtonText}>
{bleSensors.length > 0 ? 'Scan Again' : 'Scan for Nearby Sensors'}
</Text>
</>
)}
</TouchableOpacity>
{/* Available Nearby Section */}
{bleSensors.length > 0 && (
<>
<Text style={styles.sectionTitle}>Available Nearby ({bleSensors.length})</Text>
<View style={styles.devicesList}>
{bleSensors.map((sensor) => {
const sensorConfig = {
icon: 'water-outline' as const,
color: AppColors.textMuted,
bgColor: AppColors.surface,
};
return (
<TouchableOpacity
key={sensor.deviceId}
style={[styles.deviceCard, styles.nearbyDeviceCard]}
onPress={() => handleSensorPress(sensor)}
activeOpacity={0.7}
>
<View style={styles.deviceInfo}>
<View style={[styles.deviceIcon, { backgroundColor: sensorConfig.bgColor }]}>
<Ionicons name={sensorConfig.icon} size={22} color={sensorConfig.color} />
</View>
<View style={styles.deviceDetails}>
<Text style={styles.deviceName}>{sensor.name}</Text>
<Text style={styles.deviceMeta}>
<Text style={styles.deviceStatus}>Not Connected</Text>
<Text style={styles.deviceMetaSeparator}> • </Text>
<Text style={styles.deviceRoom}>Tap to connect</Text>
</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
</TouchableOpacity>
);
})}
</View>
</>
)}
{/* Info Section */}
<View style={styles.infoCard}>
<View style={styles.infoHeader}>
<Ionicons name="information-circle" size={20} color={AppColors.info} />
<Text style={styles.infoTitle}>About Sensors</Text>
</View>
<Text style={styles.infoText}>
WP sensors monitor wellness metrics via WiFi. Tap a sensor to configure WiFi settings, view detailed status, or troubleshoot connectivity issues.
{'\n\n'}
Detaching a sensor removes it from {beneficiaryName}'s monitoring setup. You can then attach it to another person or re-attach it later.
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
backButton: {
padding: Spacing.xs,
},
headerTitle: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
},
placeholder: {
width: 32,
},
headerRight: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.xs,
},
addButton: {
width: 40,
height: 40,
borderRadius: BorderRadius.md,
backgroundColor: AppColors.primaryLighter,
justifyContent: 'center',
alignItems: 'center',
},
content: {
flex: 1,
},
scrollContent: {
padding: Spacing.lg,
paddingBottom: Spacing.xxl,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: Spacing.md,
},
loadingText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
// Summary Card
summaryCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.xl,
padding: Spacing.lg,
marginBottom: Spacing.lg,
...Shadows.sm,
},
summaryRow: {
flexDirection: 'row',
alignItems: 'center',
},
summaryItem: {
flex: 1,
alignItems: 'center',
},
summaryValue: {
fontSize: FontSizes['2xl'],
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
},
summaryLabel: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
summaryDivider: {
width: 1,
height: 32,
backgroundColor: AppColors.border,
},
// Section Title
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.semibold,
color: AppColors.textSecondary,
marginBottom: Spacing.md,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
// Devices List
devicesList: {
gap: Spacing.md,
},
deviceCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
...Shadows.xs,
},
deviceInfo: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
},
deviceIcon: {
width: 48,
height: 48,
borderRadius: BorderRadius.lg,
justifyContent: 'center',
alignItems: 'center',
},
deviceDetails: {
flex: 1,
},
deviceName: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
},
deviceMeta: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 2,
gap: 4,
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
},
deviceStatus: {
fontSize: FontSizes.xs,
color: AppColors.textSecondary,
},
deviceMetaSeparator: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
},
deviceRoom: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
},
deviceActions: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.xs,
},
settingsButton: {
width: 40,
height: 40,
borderRadius: BorderRadius.md,
backgroundColor: AppColors.primaryLighter,
justifyContent: 'center',
alignItems: 'center',
},
detachButton: {
width: 40,
height: 40,
borderRadius: BorderRadius.md,
backgroundColor: AppColors.errorLight,
justifyContent: 'center',
alignItems: 'center',
},
// Empty State
emptyState: {
alignItems: 'center',
padding: Spacing.xl,
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.xl,
...Shadows.sm,
},
emptyIconContainer: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: AppColors.surfaceSecondary,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.md,
},
emptyTitle: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
emptyText: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
textAlign: 'center',
marginBottom: Spacing.lg,
},
addDeviceButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.primary,
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.lg,
borderRadius: BorderRadius.lg,
gap: Spacing.xs,
},
addDeviceButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
// Detach All Button
detachAllButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: AppColors.errorLight,
paddingVertical: Spacing.md,
borderRadius: BorderRadius.lg,
marginTop: Spacing.lg,
gap: Spacing.sm,
},
detachAllText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.error,
},
// Info Card
infoCard: {
backgroundColor: AppColors.infoLight,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
marginTop: Spacing.xl,
},
infoHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.sm,
marginBottom: Spacing.xs,
},
infoTitle: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.semibold,
color: AppColors.info,
},
infoText: {
fontSize: FontSizes.sm,
color: AppColors.info,
lineHeight: 20,
},
// WiFi Info
wifiInfo: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
marginTop: 4,
},
wifiText: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
},
// Simulator Warning
simulatorWarning: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.warningLight,
paddingVertical: Spacing.xs,
paddingHorizontal: Spacing.md,
gap: Spacing.xs,
borderBottomWidth: 1,
borderBottomColor: AppColors.warning,
},
simulatorWarningText: {
fontSize: FontSizes.xs,
color: AppColors.warning,
fontWeight: FontWeights.medium,
flex: 1,
},
// Scan Button
scanButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: AppColors.primary,
paddingVertical: Spacing.md,
borderRadius: BorderRadius.lg,
marginTop: Spacing.lg,
marginBottom: Spacing.lg,
gap: Spacing.sm,
...Shadows.md,
},
scanButtonActive: {
backgroundColor: AppColors.primaryDark,
},
scanButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
// Nearby Device Card
nearbyDeviceCard: {
borderWidth: 1,
borderColor: AppColors.border,
borderStyle: 'dashed',
},
deviceLocation: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
deviceLocationPlaceholder: {
fontStyle: 'italic',
opacity: 0.6,
},
});