- Create BLEScanner.tsx with multiple selection modes (single/multiple/none) - Support device filtering, disabled devices, and auto-scan on mount - Include RSSI signal strength visualization with color-coded indicators - Add "Select All" functionality for batch sensor setup - Create comprehensive test suite with mocked BLEContext - Export component via components/ble/index.ts The component integrates with existing BLEContext and follows the established patterns from add-sensor.tsx screen. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
823 lines
22 KiB
TypeScript
823 lines
22 KiB
TypeScript
import React, { useState, useCallback, useEffect } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
ActivityIndicator,
|
|
FlatList,
|
|
Linking,
|
|
} from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { useBLE } from '@/contexts/BLEContext';
|
|
import { WPDevice } from '@/services/ble/types';
|
|
import {
|
|
WiFiSignalIndicator,
|
|
getSignalStrengthLabel,
|
|
getSignalStrengthColor,
|
|
} from '@/components/WiFiSignalIndicator';
|
|
import {
|
|
AppColors,
|
|
BorderRadius,
|
|
FontSizes,
|
|
FontWeights,
|
|
Spacing,
|
|
Shadows,
|
|
} from '@/constants/theme';
|
|
|
|
export type SelectionMode = 'single' | 'multiple' | 'none';
|
|
|
|
export interface BLEScannerProps {
|
|
/** Callback when device(s) are selected */
|
|
onDevicesSelected?: (devices: WPDevice[]) => void;
|
|
/** Callback when a single device is selected (convenience for single mode) */
|
|
onDeviceSelected?: (device: WPDevice) => void;
|
|
/** Selection mode: 'single', 'multiple', or 'none' */
|
|
selectionMode?: SelectionMode;
|
|
/** Whether to auto-start scanning when component mounts */
|
|
autoScan?: boolean;
|
|
/** Custom filter function for devices */
|
|
deviceFilter?: (device: WPDevice) => boolean;
|
|
/** Device IDs that are already connected/added (will show as disabled) */
|
|
disabledDeviceIds?: Set<string>;
|
|
/** Show instructions header */
|
|
showInstructions?: boolean;
|
|
/** Custom empty state message */
|
|
emptyStateMessage?: string;
|
|
/** Compact mode with smaller styling */
|
|
compact?: boolean;
|
|
/** Test ID for testing */
|
|
testID?: string;
|
|
}
|
|
|
|
interface DeviceItemProps {
|
|
device: WPDevice;
|
|
isSelected: boolean;
|
|
isDisabled: boolean;
|
|
selectionMode: SelectionMode;
|
|
onPress: () => void;
|
|
compact: boolean;
|
|
}
|
|
|
|
function DeviceItem({
|
|
device,
|
|
isSelected,
|
|
isDisabled,
|
|
selectionMode,
|
|
onPress,
|
|
compact,
|
|
}: DeviceItemProps) {
|
|
return (
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.deviceCard,
|
|
compact && styles.deviceCardCompact,
|
|
isSelected && styles.deviceCardSelected,
|
|
isDisabled && styles.deviceCardDisabled,
|
|
]}
|
|
onPress={onPress}
|
|
disabled={isDisabled || selectionMode === 'none'}
|
|
activeOpacity={0.7}
|
|
testID={`device-item-${device.id}`}
|
|
>
|
|
{selectionMode !== 'none' && (
|
|
<View style={styles.checkboxContainer}>
|
|
<View
|
|
style={[
|
|
styles.checkbox,
|
|
compact && styles.checkboxCompact,
|
|
isSelected && styles.checkboxSelected,
|
|
isDisabled && styles.checkboxDisabled,
|
|
]}
|
|
>
|
|
{isSelected && (
|
|
<Ionicons
|
|
name="checkmark"
|
|
size={compact ? 12 : 16}
|
|
color={AppColors.white}
|
|
/>
|
|
)}
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
<View style={styles.deviceInfo}>
|
|
<View style={[styles.deviceIcon, compact && styles.deviceIconCompact]}>
|
|
<Ionicons
|
|
name="bluetooth"
|
|
size={compact ? 18 : 24}
|
|
color={isDisabled ? AppColors.textMuted : AppColors.primary}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.deviceDetails}>
|
|
<Text
|
|
style={[
|
|
styles.deviceName,
|
|
compact && styles.deviceNameCompact,
|
|
isDisabled && styles.deviceNameDisabled,
|
|
]}
|
|
numberOfLines={1}
|
|
>
|
|
{device.name}
|
|
</Text>
|
|
|
|
{device.wellId && (
|
|
<Text style={[styles.deviceMeta, compact && styles.deviceMetaCompact]}>
|
|
Well ID: {device.wellId}
|
|
</Text>
|
|
)}
|
|
|
|
<View style={styles.signalRow}>
|
|
<WiFiSignalIndicator
|
|
rssi={device.rssi}
|
|
size={compact ? 'small' : 'small'}
|
|
/>
|
|
<Text
|
|
style={[
|
|
styles.signalLabel,
|
|
{ color: getSignalStrengthColor(device.rssi) },
|
|
]}
|
|
>
|
|
{getSignalStrengthLabel(device.rssi)}
|
|
</Text>
|
|
<Text style={styles.signalDbm}>({device.rssi} dBm)</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{isDisabled && (
|
|
<View style={styles.disabledBadge}>
|
|
<Text style={styles.disabledBadgeText}>Added</Text>
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
}
|
|
|
|
export function BLEScanner({
|
|
onDevicesSelected,
|
|
onDeviceSelected,
|
|
selectionMode = 'multiple',
|
|
autoScan = false,
|
|
deviceFilter,
|
|
disabledDeviceIds = new Set(),
|
|
showInstructions = false,
|
|
emptyStateMessage = 'No sensors found. Make sure your WP sensor is powered on and nearby.',
|
|
compact = false,
|
|
testID,
|
|
}: BLEScannerProps) {
|
|
const {
|
|
foundDevices,
|
|
isScanning,
|
|
isBLEAvailable,
|
|
error,
|
|
permissionError,
|
|
scanDevices,
|
|
stopScan,
|
|
clearError,
|
|
} = useBLE();
|
|
|
|
const [selectedDevices, setSelectedDevices] = useState<Set<string>>(new Set());
|
|
const [hasScanned, setHasScanned] = useState(false);
|
|
|
|
// Filter devices if filter function provided
|
|
const filteredDevices = deviceFilter
|
|
? foundDevices.filter(deviceFilter)
|
|
: foundDevices;
|
|
|
|
// Auto-scan on mount if enabled
|
|
useEffect(() => {
|
|
if (autoScan && !hasScanned) {
|
|
handleScan();
|
|
}
|
|
}, [autoScan, hasScanned]);
|
|
|
|
// Select all devices by default in multiple selection mode when scan completes
|
|
useEffect(() => {
|
|
if (
|
|
selectionMode === 'multiple' &&
|
|
filteredDevices.length > 0 &&
|
|
!isScanning &&
|
|
hasScanned
|
|
) {
|
|
const selectableIds = filteredDevices
|
|
.filter((d) => !disabledDeviceIds.has(d.id))
|
|
.map((d) => d.id);
|
|
setSelectedDevices(new Set(selectableIds));
|
|
}
|
|
}, [filteredDevices, isScanning, hasScanned, selectionMode, disabledDeviceIds]);
|
|
|
|
// Notify parent of selection changes
|
|
useEffect(() => {
|
|
const selected = filteredDevices.filter((d) => selectedDevices.has(d.id));
|
|
|
|
if (selectionMode === 'single' && selected.length === 1 && onDeviceSelected) {
|
|
onDeviceSelected(selected[0]);
|
|
}
|
|
|
|
if (onDevicesSelected) {
|
|
onDevicesSelected(selected);
|
|
}
|
|
}, [selectedDevices, filteredDevices, selectionMode, onDeviceSelected, onDevicesSelected]);
|
|
|
|
const handleScan = useCallback(async () => {
|
|
try {
|
|
clearError();
|
|
setHasScanned(true);
|
|
setSelectedDevices(new Set());
|
|
await scanDevices();
|
|
} catch {
|
|
// Error is handled by BLE context
|
|
}
|
|
}, [clearError, scanDevices]);
|
|
|
|
const handleDevicePress = useCallback(
|
|
(deviceId: string) => {
|
|
if (selectionMode === 'none') return;
|
|
|
|
if (selectionMode === 'single') {
|
|
setSelectedDevices(new Set([deviceId]));
|
|
} else {
|
|
setSelectedDevices((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(deviceId)) {
|
|
next.delete(deviceId);
|
|
} else {
|
|
next.add(deviceId);
|
|
}
|
|
return next;
|
|
});
|
|
}
|
|
},
|
|
[selectionMode]
|
|
);
|
|
|
|
const toggleSelectAll = useCallback(() => {
|
|
const selectableDevices = filteredDevices.filter(
|
|
(d) => !disabledDeviceIds.has(d.id)
|
|
);
|
|
if (selectedDevices.size === selectableDevices.length) {
|
|
setSelectedDevices(new Set());
|
|
} else {
|
|
setSelectedDevices(new Set(selectableDevices.map((d) => d.id)));
|
|
}
|
|
}, [filteredDevices, disabledDeviceIds, selectedDevices]);
|
|
|
|
const handleOpenSettings = useCallback(() => {
|
|
Linking.openSettings();
|
|
}, []);
|
|
|
|
const renderDevice = useCallback(
|
|
({ item }: { item: WPDevice }) => (
|
|
<DeviceItem
|
|
device={item}
|
|
isSelected={selectedDevices.has(item.id)}
|
|
isDisabled={disabledDeviceIds.has(item.id)}
|
|
selectionMode={selectionMode}
|
|
onPress={() => handleDevicePress(item.id)}
|
|
compact={compact}
|
|
/>
|
|
),
|
|
[selectedDevices, disabledDeviceIds, selectionMode, handleDevicePress, compact]
|
|
);
|
|
|
|
const keyExtractor = useCallback((item: WPDevice) => item.id, []);
|
|
|
|
const selectableCount = filteredDevices.filter(
|
|
(d) => !disabledDeviceIds.has(d.id)
|
|
).length;
|
|
const allSelected = selectedDevices.size === selectableCount && selectableCount > 0;
|
|
|
|
return (
|
|
<View style={[styles.container, compact && styles.containerCompact]} testID={testID}>
|
|
{/* Simulator Warning */}
|
|
{!isBLEAvailable && (
|
|
<View style={styles.simulatorWarning}>
|
|
<Ionicons name="information-circle" size={16} color={AppColors.warning} />
|
|
<Text style={styles.simulatorWarningText}>
|
|
Simulator mode - showing mock sensors
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Permission Error */}
|
|
{permissionError && error && (
|
|
<View style={styles.permissionError}>
|
|
<View style={styles.permissionErrorContent}>
|
|
<Ionicons name="warning" size={18} color={AppColors.error} />
|
|
<Text style={styles.permissionErrorText}>{error}</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
style={styles.settingsButton}
|
|
onPress={handleOpenSettings}
|
|
>
|
|
<Text style={styles.settingsButtonText}>Open Settings</Text>
|
|
<Ionicons name="settings-outline" size={14} color={AppColors.error} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
|
|
{/* Instructions */}
|
|
{showInstructions && !isScanning && filteredDevices.length === 0 && (
|
|
<View style={[styles.instructionsCard, compact && styles.instructionsCardCompact]}>
|
|
<Text style={styles.instructionsTitle}>Scanning for Sensors</Text>
|
|
<View style={styles.instruction}>
|
|
<Ionicons name="power" size={16} color={AppColors.textMuted} />
|
|
<Text style={styles.instructionText}>
|
|
Make sure sensors are powered on
|
|
</Text>
|
|
</View>
|
|
<View style={styles.instruction}>
|
|
<Ionicons name="locate" size={16} color={AppColors.textMuted} />
|
|
<Text style={styles.instructionText}>
|
|
Stay within 10 meters of sensors
|
|
</Text>
|
|
</View>
|
|
<View style={styles.instruction}>
|
|
<Ionicons name="bluetooth" size={16} color={AppColors.textMuted} />
|
|
<Text style={styles.instructionText}>
|
|
Bluetooth must be enabled
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Scan Button (when not scanning and no devices) */}
|
|
{!isScanning && filteredDevices.length === 0 && (
|
|
<TouchableOpacity
|
|
style={[styles.scanButton, compact && styles.scanButtonCompact]}
|
|
onPress={handleScan}
|
|
testID="scan-button"
|
|
>
|
|
<Ionicons name="bluetooth" size={compact ? 20 : 24} color={AppColors.white} />
|
|
<Text style={[styles.scanButtonText, compact && styles.scanButtonTextCompact]}>
|
|
Scan for Sensors
|
|
</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
|
|
{/* Scanning State */}
|
|
{isScanning && (
|
|
<View style={[styles.scanningCard, compact && styles.scanningCardCompact]}>
|
|
<ActivityIndicator
|
|
size={compact ? 'small' : 'large'}
|
|
color={AppColors.primary}
|
|
/>
|
|
<Text style={[styles.scanningText, compact && styles.scanningTextCompact]}>
|
|
Scanning for WP sensors...
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={styles.stopScanButton}
|
|
onPress={stopScan}
|
|
testID="stop-scan-button"
|
|
>
|
|
<Text style={styles.stopScanText}>Stop Scan</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
|
|
{/* Device List Header */}
|
|
{!isScanning && filteredDevices.length > 0 && (
|
|
<View style={styles.listHeader}>
|
|
<Text style={styles.listHeaderTitle}>
|
|
Found Sensors ({filteredDevices.length})
|
|
</Text>
|
|
<View style={styles.listHeaderActions}>
|
|
{selectionMode === 'multiple' && selectableCount > 0 && (
|
|
<TouchableOpacity
|
|
style={styles.selectAllButton}
|
|
onPress={toggleSelectAll}
|
|
testID="select-all-button"
|
|
>
|
|
<Ionicons
|
|
name={allSelected ? 'checkbox' : 'square-outline'}
|
|
size={16}
|
|
color={AppColors.primary}
|
|
/>
|
|
<Text style={styles.selectAllText}>
|
|
{allSelected ? 'Deselect All' : 'Select All'}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
<TouchableOpacity
|
|
style={styles.rescanButton}
|
|
onPress={handleScan}
|
|
testID="rescan-button"
|
|
>
|
|
<Ionicons name="refresh" size={16} color={AppColors.primary} />
|
|
<Text style={styles.rescanText}>Rescan</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Device List */}
|
|
{!isScanning && filteredDevices.length > 0 && (
|
|
<FlatList
|
|
data={filteredDevices}
|
|
renderItem={renderDevice}
|
|
keyExtractor={keyExtractor}
|
|
contentContainerStyle={styles.deviceList}
|
|
showsVerticalScrollIndicator={false}
|
|
testID="device-list"
|
|
/>
|
|
)}
|
|
|
|
{/* Empty State (after scan, no devices) */}
|
|
{!isScanning && hasScanned && filteredDevices.length === 0 && !error && (
|
|
<View style={[styles.emptyState, compact && styles.emptyStateCompact]}>
|
|
<View style={styles.emptyIconContainer}>
|
|
<Ionicons
|
|
name="bluetooth-outline"
|
|
size={compact ? 32 : 48}
|
|
color={AppColors.textMuted}
|
|
/>
|
|
</View>
|
|
<Text style={[styles.emptyTitle, compact && styles.emptyTitleCompact]}>
|
|
No Sensors Found
|
|
</Text>
|
|
<Text style={[styles.emptyText, compact && styles.emptyTextCompact]}>
|
|
{emptyStateMessage}
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={[styles.retryButton, compact && styles.retryButtonCompact]}
|
|
onPress={handleScan}
|
|
testID="retry-scan-button"
|
|
>
|
|
<Ionicons name="refresh" size={18} color={AppColors.primary} />
|
|
<Text style={styles.retryButtonText}>Try Again</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
containerCompact: {
|
|
flex: 0,
|
|
},
|
|
|
|
// Simulator Warning
|
|
simulatorWarning: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: AppColors.warningLight,
|
|
paddingVertical: Spacing.xs,
|
|
paddingHorizontal: Spacing.md,
|
|
gap: Spacing.xs,
|
|
borderRadius: BorderRadius.sm,
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
simulatorWarningText: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.warning,
|
|
fontWeight: FontWeights.medium,
|
|
},
|
|
|
|
// Permission Error
|
|
permissionError: {
|
|
backgroundColor: AppColors.errorLight,
|
|
padding: Spacing.md,
|
|
borderRadius: BorderRadius.md,
|
|
marginBottom: Spacing.md,
|
|
gap: Spacing.sm,
|
|
},
|
|
permissionErrorContent: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
gap: Spacing.xs,
|
|
},
|
|
permissionErrorText: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.error,
|
|
fontWeight: FontWeights.medium,
|
|
flex: 1,
|
|
lineHeight: 18,
|
|
},
|
|
settingsButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: AppColors.white,
|
|
paddingVertical: Spacing.xs,
|
|
paddingHorizontal: Spacing.sm,
|
|
borderRadius: BorderRadius.sm,
|
|
gap: Spacing.xs,
|
|
alignSelf: 'flex-start',
|
|
},
|
|
settingsButtonText: {
|
|
fontSize: FontSizes.xs,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.error,
|
|
},
|
|
|
|
// Instructions
|
|
instructionsCard: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.md,
|
|
marginBottom: Spacing.md,
|
|
gap: Spacing.sm,
|
|
...Shadows.xs,
|
|
},
|
|
instructionsCardCompact: {
|
|
padding: Spacing.sm,
|
|
gap: Spacing.xs,
|
|
},
|
|
instructionsTitle: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
instruction: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.sm,
|
|
},
|
|
instructionText: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textSecondary,
|
|
},
|
|
|
|
// Scan Button
|
|
scanButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: AppColors.primary,
|
|
paddingVertical: Spacing.md,
|
|
borderRadius: BorderRadius.lg,
|
|
gap: Spacing.sm,
|
|
...Shadows.md,
|
|
},
|
|
scanButtonCompact: {
|
|
paddingVertical: Spacing.sm,
|
|
},
|
|
scanButtonText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.white,
|
|
},
|
|
scanButtonTextCompact: {
|
|
fontSize: FontSizes.sm,
|
|
},
|
|
|
|
// Scanning State
|
|
scanningCard: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.xl,
|
|
alignItems: 'center',
|
|
...Shadows.sm,
|
|
},
|
|
scanningCardCompact: {
|
|
padding: Spacing.md,
|
|
},
|
|
scanningText: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
marginTop: Spacing.md,
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
scanningTextCompact: {
|
|
fontSize: FontSizes.sm,
|
|
marginTop: Spacing.sm,
|
|
},
|
|
stopScanButton: {
|
|
paddingVertical: Spacing.xs,
|
|
paddingHorizontal: Spacing.md,
|
|
},
|
|
stopScanText: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.error,
|
|
},
|
|
|
|
// List Header
|
|
listHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: Spacing.sm,
|
|
},
|
|
listHeaderTitle: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textSecondary,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.5,
|
|
},
|
|
listHeaderActions: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.md,
|
|
},
|
|
selectAllButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
},
|
|
selectAllText: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.primary,
|
|
},
|
|
rescanButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
},
|
|
rescanText: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.primary,
|
|
},
|
|
|
|
// Device List
|
|
deviceList: {
|
|
gap: Spacing.sm,
|
|
},
|
|
|
|
// Device Card
|
|
deviceCard: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.md,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
...Shadows.xs,
|
|
},
|
|
deviceCardCompact: {
|
|
padding: Spacing.sm,
|
|
},
|
|
deviceCardSelected: {
|
|
borderWidth: 2,
|
|
borderColor: AppColors.primary,
|
|
},
|
|
deviceCardDisabled: {
|
|
opacity: 0.6,
|
|
},
|
|
|
|
// Checkbox
|
|
checkboxContainer: {
|
|
marginRight: Spacing.sm,
|
|
},
|
|
checkbox: {
|
|
width: 24,
|
|
height: 24,
|
|
borderRadius: BorderRadius.sm,
|
|
borderWidth: 2,
|
|
borderColor: AppColors.border,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: AppColors.white,
|
|
},
|
|
checkboxCompact: {
|
|
width: 20,
|
|
height: 20,
|
|
},
|
|
checkboxSelected: {
|
|
backgroundColor: AppColors.primary,
|
|
borderColor: AppColors.primary,
|
|
},
|
|
checkboxDisabled: {
|
|
backgroundColor: AppColors.surfaceSecondary,
|
|
borderColor: AppColors.border,
|
|
},
|
|
|
|
// Device Info
|
|
deviceInfo: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.md,
|
|
},
|
|
deviceIcon: {
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: BorderRadius.lg,
|
|
backgroundColor: AppColors.primaryLighter,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
deviceIconCompact: {
|
|
width: 36,
|
|
height: 36,
|
|
},
|
|
deviceDetails: {
|
|
flex: 1,
|
|
},
|
|
deviceName: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: 2,
|
|
},
|
|
deviceNameCompact: {
|
|
fontSize: FontSizes.sm,
|
|
},
|
|
deviceNameDisabled: {
|
|
color: AppColors.textMuted,
|
|
},
|
|
deviceMeta: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textMuted,
|
|
marginBottom: 4,
|
|
},
|
|
deviceMetaCompact: {
|
|
marginBottom: 2,
|
|
},
|
|
signalRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
},
|
|
signalLabel: {
|
|
fontSize: FontSizes.xs,
|
|
fontWeight: FontWeights.semibold,
|
|
},
|
|
signalDbm: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textMuted,
|
|
},
|
|
|
|
// Disabled Badge
|
|
disabledBadge: {
|
|
backgroundColor: AppColors.successLight,
|
|
paddingVertical: Spacing.xs,
|
|
paddingHorizontal: Spacing.sm,
|
|
borderRadius: BorderRadius.sm,
|
|
},
|
|
disabledBadgeText: {
|
|
fontSize: FontSizes.xs,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.success,
|
|
},
|
|
|
|
// Empty State
|
|
emptyState: {
|
|
alignItems: 'center',
|
|
padding: Spacing.xl,
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.lg,
|
|
...Shadows.sm,
|
|
},
|
|
emptyStateCompact: {
|
|
padding: Spacing.md,
|
|
},
|
|
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,
|
|
},
|
|
emptyTitleCompact: {
|
|
fontSize: FontSizes.base,
|
|
},
|
|
emptyText: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textMuted,
|
|
textAlign: 'center',
|
|
lineHeight: 20,
|
|
marginBottom: Spacing.md,
|
|
},
|
|
emptyTextCompact: {
|
|
fontSize: FontSizes.xs,
|
|
},
|
|
retryButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.xs,
|
|
paddingVertical: Spacing.sm,
|
|
paddingHorizontal: Spacing.md,
|
|
borderRadius: BorderRadius.md,
|
|
borderWidth: 1,
|
|
borderColor: AppColors.primary,
|
|
},
|
|
retryButtonCompact: {
|
|
paddingVertical: Spacing.xs,
|
|
paddingHorizontal: Spacing.sm,
|
|
},
|
|
retryButtonText: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.primary,
|
|
},
|
|
});
|
|
|
|
export default BLEScanner;
|