feat(components): ActionButton stopAlert + typography type fixes

This commit is contained in:
fightrate-bot committed 2026-06-16 01:50:26 -03:00
1 parent 2c51fd2623
commit bbe2919c2c
89 files changed
+75993 -9961

No files matched your search

+423
View File
@@ -0,0 +1,423 @@
import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import {
Activity,
Battery,
Bluetooth,
RadioReceiver,
ShieldCheck,
Signal,
} from 'lucide-react-native';
import { type HeartRateMonitorState } from '../ble/useHeartRateMonitor';
import { formatRssi } from '../ble/rssi';
import { ActionButton } from '../components/ActionButton';
import { colors, radius, spacing, typography } from '../theme';
type Props = {
readonly monitor: HeartRateMonitorState;
readonly onMonitorAction: () => Promise<void>;
};
export function SensorScreen({ monitor, onMonitorAction }: Props) {
const device = monitor.connectedDevice;
const services = monitor.services.length
? monitor.services
: ['Heart Rate Service · 0x180D · aguardando conexão'];
const characteristics = monitor.characteristics.length
? monitor.characteristics
: ['Heart Rate Measurement · 0x2A37 · notify esperado'];
return (
<View style={styles.screen}>
<View style={styles.sensorConsole}>
<View style={styles.consoleTopline}>
<View>
<Text style={styles.eyebrow}>SENSOR REAL</Text>
<Text style={styles.title}>
{friendlyDeviceTitle(device?.name)}
</Text>
</View>
<View style={styles.phaseBadge}>
<Bluetooth color={colors.warning} size={16} strokeWidth={2.4} />
<Text style={styles.phaseText}>{phaseLabel(monitor.phase)}</Text>
</View>
</View>
<View style={styles.signalGrid}>
<SignalDatum
icon="signal"
label="RSSI"
value={formatRssi(device?.rssi)}
/>
<SignalDatum
icon="activity"
label="Leituras"
value={`${monitor.samples.length}`}
/>
<SignalDatum
icon="battery"
label="Bateria"
value={formatBattery(device?.batteryPercent)}
/>
<SignalDatum
icon="shield"
label="Fundo"
value={monitor.batteryOptimizationIgnored ? 'liberado' : 'restrito'}
/>
</View>
<Text style={styles.explainer}>
{monitor.errorMessage ?? sensorExplanation(monitor)}
</Text>
<View style={styles.actionRow}>
{!monitor.isMonitoring ? (
<ActionButton
label="Buscar sensor"
onPress={() => {
onMonitorAction().catch(() => undefined);
}}
showIcon={false}
variant="primary"
/>
) : (
<View style={styles.livePill}>
<Activity color={colors.success} size={15} strokeWidth={2.4} />
<Text style={styles.livePillText}>Monitor contínuo ativo</Text>
</View>
)}
{!monitor.batteryOptimizationIgnored ? (
<Pressable
onPress={() =>
monitor.openBatterySettings().catch(() => undefined)
}
style={styles.secondaryButton}
>
<Text style={styles.secondaryButtonText}>Bateria</Text>
</Pressable>
) : null}
</View>
</View>
{monitor.discoveredDevices.length > 0 ? (
<View style={styles.discoveryBlock}>
<Text style={styles.blockLabel}>ENCONTRADOS AGORA</Text>
{monitor.discoveredDevices.map(candidate => (
<Pressable
key={candidate.id}
onPress={() =>
monitor.connectToDevice(candidate.id).catch(() => undefined)
}
style={styles.deviceRow}
>
<View style={styles.deviceDot} />
<View style={styles.deviceInfo}>
<Text style={styles.deviceName}>
{friendlyDeviceTitle(candidate.name)}
</Text>
<Text style={styles.deviceMeta}>
{candidate.name} · {formatRssi(candidate.rssi)}
</Text>
</View>
<View style={styles.connectPill}>
<Text style={styles.connectPillText}>Conectar</Text>
</View>
</Pressable>
))}
</View>
) : null}
<BleInventoryBlock title="SERVIÇOS BLE" rows={services} />
<BleInventoryBlock title="CARACTERÍSTICAS" rows={characteristics} />
</View>
);
}
function SignalDatum({
icon,
label,
value,
}: {
readonly icon: 'signal' | 'battery' | 'shield' | 'activity';
readonly label: string;
readonly value: string;
}) {
const Icon =
icon === 'signal'
? Signal
: icon === 'battery'
? Battery
: icon === 'activity'
? Activity
: ShieldCheck;
return (
<View style={styles.signalDatum}>
<Icon color={colors.warning} size={17} strokeWidth={2.3} />
<Text style={styles.signalValue}>{value}</Text>
<Text style={styles.signalLabel}>{label}</Text>
</View>
);
}
function BleInventoryBlock({
title,
rows,
}: {
readonly title: string;
readonly rows: readonly string[];
}) {
return (
<View style={styles.diagnosticBlock}>
<Text style={styles.blockLabel}>{title}</Text>
{rows.slice(0, 8).map(row => {
const parsed = parseBleRow(row);
return (
<View key={row} style={styles.diagnosticRow}>
<RadioReceiver color={colors.textDim} size={14} strokeWidth={2.2} />
<View style={styles.diagnosticBody}>
<Text style={styles.diagnosticTitle}>{parsed.name}</Text>
<Text style={styles.diagnosticText}>
{parsed.uuid} · {parsed.detail}
</Text>
</View>
</View>
);
})}
</View>
);
}
function friendlyDeviceTitle(name?: string | null) {
if (!name) {
return 'Cinta cardíaca BLE';
}
if (/magene|h003/i.test(name)) {
return 'Magene H003';
}
if (/heart|hrm/i.test(name)) {
return 'Cinta cardíaca BLE';
}
return name;
}
function parseBleRow(row: string) {
const [name = row, uuid = 'BLE', ...rest] = row.split(' · ');
return {
name,
uuid,
detail: rest.join(' · ') || 'aguardando leitura',
};
}
function phaseLabel(phase: HeartRateMonitorState['phase']) {
const labels: Record<HeartRateMonitorState['phase'], string> = {
idle: 'parado',
'requesting-permission': 'permissões',
'bluetooth-off': 'bluetooth off',
scanning: 'buscando',
connecting: 'conectando',
connected: 'conectado',
receiving: 'recebendo',
'permission-denied': 'sem permissão',
'sensor-not-found': 'sem sensor',
error: 'erro',
};
return labels[phase];
}
function sensorExplanation(monitor: HeartRateMonitorState) {
if (monitor.phase === 'receiving' && monitor.samples.length > 0) {
return `Amostras reais chegando: ${monitor.samples.length}. Cada BPM reconhecido está sendo salvo em SQLite.`;
}
if (monitor.phase === 'receiving' || monitor.phase === 'connected') {
return 'Sensor reconhecido. O app está armando a leitura 2A37; se não vier BPM, reativa notify e reconecta sozinho.';
}
if (monitor.phase === 'scanning') {
return 'Busca nativa BLE ativa. Se o app for bloqueado, a notificação mantém o serviço vivo.';
}
return 'A tela traduz nomes técnicos: 180D é o serviço cardíaco; 2A37 é o dado de BPM.';
}
function formatBattery(percent?: number | null) {
if (typeof percent === 'number') {
return `${percent}%`;
}
return '--';
}
const styles = StyleSheet.create({
screen: {
gap: spacing.sm,
},
sensorConsole: {
borderTopWidth: 1,
borderColor: colors.line,
paddingVertical: spacing.md,
gap: spacing.md,
},
consoleTopline: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: spacing.md,
},
eyebrow: {
...typography.label,
color: colors.action,
},
title: {
color: colors.text,
fontSize: 34,
lineHeight: 36,
fontWeight: '900',
letterSpacing: -2,
marginTop: spacing.xs,
},
phaseBadge: {
paddingVertical: spacing.xs,
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
height: 36,
},
phaseText: {
...typography.label,
color: colors.text,
},
signalGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.xs,
},
signalDatum: {
width: '48.5%',
minHeight: 76,
borderWidth: 1,
borderColor: colors.line,
padding: spacing.sm,
gap: spacing.xxs,
justifyContent: 'center',
},
signalValue: {
color: colors.text,
fontSize: 18,
lineHeight: 22,
fontWeight: '900',
marginTop: spacing.xs,
},
signalLabel: {
...typography.label,
color: colors.textMuted,
},
explainer: {
...typography.body,
color: colors.textMuted,
},
actionRow: {
flexDirection: 'row',
gap: spacing.sm,
alignItems: 'center',
},
livePill: {
minHeight: 48,
flex: 1,
borderWidth: 1,
borderColor: colors.line,
paddingHorizontal: spacing.md,
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
livePillText: {
...typography.label,
color: colors.success,
},
secondaryButton: {
minHeight: 48,
borderWidth: 1,
borderColor: colors.line,
borderRadius: radius.xl,
paddingHorizontal: spacing.md,
justifyContent: 'center',
},
secondaryButtonText: {
...typography.label,
color: colors.warning,
},
discoveryBlock: {
borderTopWidth: 1,
borderTopColor: colors.line,
paddingTop: spacing.md,
gap: spacing.sm,
},
blockLabel: {
...typography.label,
color: colors.warning,
},
deviceRow: {
flexDirection: 'row',
gap: spacing.sm,
alignItems: 'center',
paddingVertical: spacing.sm,
borderBottomWidth: 1,
borderBottomColor: colors.line,
},
deviceDot: {
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: colors.success,
},
deviceInfo: {
flex: 1,
},
deviceName: {
color: colors.text,
fontSize: 18,
lineHeight: 22,
fontWeight: '900',
},
deviceMeta: {
...typography.body,
color: colors.textMuted,
marginTop: spacing.xxs,
},
connectPill: {
borderWidth: 1,
borderColor: colors.line,
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
},
connectPillText: {
...typography.label,
color: colors.warning,
},
diagnosticBlock: {
borderTopWidth: 1,
borderTopColor: colors.line,
paddingTop: spacing.sm,
gap: spacing.xs,
},
diagnosticRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
paddingVertical: spacing.xs,
borderBottomWidth: 1,
borderBottomColor: colors.line,
},
diagnosticBody: {
flex: 1,
gap: 2,
},
diagnosticTitle: {
...typography.label,
color: colors.text,
},
diagnosticText: {
flex: 1,
color: colors.textMuted,
fontSize: 12,
lineHeight: 16,
fontWeight: '700',
},
});