feat(app): FightRateApp tab fade + brand LiveDot + tab live indicator

This commit is contained in:
fightrate-bot committed 2026-06-16 01:59:20 -03:00
1 parent 55fb37a34a
commit 1cb658f899
1 file changed
+62 -64
+62 -64
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { import {
Image, Image,
Pressable, Pressable,
@@ -16,12 +16,19 @@ import {
type LucideIcon, type LucideIcon,
} from 'lucide-react-native'; } from 'lucide-react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { useHeartRateMonitor } from '../ble/useHeartRateMonitor'; import { useHeartRateMonitor } from '../ble/useHeartRateMonitor';
import { LiveDot } from '../components/LiveDot';
import { CaptureScreen } from '../screens/CaptureScreen'; import { CaptureScreen } from '../screens/CaptureScreen';
import { HistoryScreen } from '../screens/HistoryScreen'; import { HistoryScreen } from '../screens/HistoryScreen';
import { InsightsScreen } from '../screens/InsightsScreen'; import { InsightsScreen } from '../screens/InsightsScreen';
import { SensorScreen } from '../screens/SensorScreen'; import { SensorScreen } from '../screens/SensorScreen';
import { colors, radius, spacing, typography } from '../theme'; import { colors, motion, radius, spacing, typography } from '../theme';
type TabKey = 'capture' | 'history' | 'sensor' | 'insights'; type TabKey = 'capture' | 'history' | 'sensor' | 'insights';
@@ -33,7 +40,7 @@ type Tab = {
const tabs: readonly Tab[] = [ const tabs: readonly Tab[] = [
{ key: 'capture', label: 'Captura', icon: Radio }, { key: 'capture', label: 'Captura', icon: Radio },
{ key: 'history', label: 'Histórico', icon: History }, { key: 'history', label: 'Hist\órico', icon: History },
{ key: 'sensor', label: 'Sensor', icon: Bluetooth }, { key: 'sensor', label: 'Sensor', icon: Bluetooth },
{ key: 'insights', label: 'Insights', icon: ChartNoAxesCombined }, { key: 'insights', label: 'Insights', icon: ChartNoAxesCombined },
]; ];
@@ -42,13 +49,23 @@ export function FightRateApp() {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const monitor = useHeartRateMonitor(); const monitor = useHeartRateMonitor();
const [activeTab, setActiveTab] = useState<TabKey>('capture'); const [activeTab, setActiveTab] = useState<TabKey>('capture');
const opacity = useSharedValue(1);
useEffect(() => {
opacity.value = 0;
opacity.value = withTiming(1, {
duration: motion.duration.fast,
easing: Easing.out(Easing.quad),
});
}, [activeTab, opacity]);
const fadeStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
const onCaptureAction = useCallback(async () => { const onCaptureAction = useCallback(async () => {
if (monitor.isCapturing) { if (monitor.isCapturing) {
await monitor.stopCapture(); await monitor.stopCapture();
return; return;
} }
await monitor.startCapture(); await monitor.startCapture();
}, [monitor]); }, [monitor]);
@@ -57,31 +74,22 @@ export function FightRateApp() {
await monitor.stopMonitoring(); await monitor.stopMonitoring();
return; return;
} }
await monitor.startMonitoring(); await monitor.startMonitoring();
}, [monitor]); }, [monitor]);
const activeTitle = useMemo(() => { const activeTitle = useMemo(() => {
if (activeTab === 'capture') { if (activeTab === 'capture') {
if (monitor.isCapturing) { if (monitor.isCapturing) return 'Janela de treino ativa';
return 'Janela de treino ativa'; if (monitor.isMonitoring) return 'Monitor cont\ínuo';
}
if (monitor.isMonitoring) {
return 'Monitor contínuo';
}
return 'Ativar sensor'; return 'Ativar sensor';
} }
if (activeTab === 'history') return 'Hist\órico real';
if (activeTab === 'history') { if (activeTab === 'sensor') return 'Sensor BLE';
return 'Histórico real'; return 'An\álise real';
}
if (activeTab === 'sensor') {
return 'Sensor BLE';
}
return 'Análise real';
}, [activeTab, monitor.isCapturing, monitor.isMonitoring]); }, [activeTab, monitor.isCapturing, monitor.isMonitoring]);
const isAnyLive = monitor.isMonitoring || monitor.isCapturing;
return ( return (
<View style={styles.root}> <View style={styles.root}>
<StatusBar barStyle="light-content" backgroundColor={colors.canvas} /> <StatusBar barStyle="light-content" backgroundColor={colors.canvas} />
@@ -101,11 +109,15 @@ export function FightRateApp() {
style={styles.logo} style={styles.logo}
/> />
<View style={styles.titleColumn}> <View style={styles.titleColumn}>
<View style={styles.kickerRow}>
{isAnyLive ? <LiveDot live size={6} /> : null}
<Text style={styles.kicker}>FightRate</Text> <Text style={styles.kicker}>FightRate</Text>
</View>
<Text style={styles.title}>{activeTitle}</Text> <Text style={styles.title}>{activeTitle}</Text>
</View> </View>
</View> </View>
<Animated.View style={fadeStyle} key={`tab-${activeTab}`}>
{activeTab === 'capture' ? ( {activeTab === 'capture' ? (
<CaptureScreen <CaptureScreen
monitor={monitor} monitor={monitor}
@@ -122,7 +134,10 @@ export function FightRateApp() {
/> />
) : null} ) : null}
{activeTab === 'sensor' ? ( {activeTab === 'sensor' ? (
<SensorScreen monitor={monitor} onMonitorAction={onMonitorAction} /> <SensorScreen
monitor={monitor}
onMonitorAction={onMonitorAction}
/>
) : null} ) : null}
{activeTab === 'insights' ? ( {activeTab === 'insights' ? (
<InsightsScreen <InsightsScreen
@@ -131,13 +146,17 @@ export function FightRateApp() {
getContinuousSamples={monitor.getContinuousSamples} getContinuousSamples={monitor.getContinuousSamples}
/> />
) : null} ) : null}
</Animated.View>
</ScrollView> </ScrollView>
<View style={[styles.navWrap, { bottom: insets.bottom + spacing.sm }]}> <View style={[styles.navWrap, { bottom: insets.bottom + spacing.sm }]}>
<View style={styles.navRail}> <View style={styles.navRail}>
{tabs.map(tab => { {tabs.map((tab) => {
const selected = tab.key === activeTab; const selected = tab.key === activeTab;
const Icon = tab.icon; const Icon = tab.icon;
const isTabLive =
(tab.key === 'capture' && monitor.isCapturing) ||
(tab.key === 'sensor' && monitor.isMonitoring);
return ( return (
<Pressable <Pressable
accessibilityRole="tab" accessibilityRole="tab"
@@ -146,11 +165,18 @@ export function FightRateApp() {
onPress={() => setActiveTab(tab.key)} onPress={() => setActiveTab(tab.key)}
style={[styles.navItem, selected ? styles.navItemActive : null]} style={[styles.navItem, selected ? styles.navItemActive : null]}
> >
<View style={styles.navIconRow}>
<Icon <Icon
color={selected ? colors.text : colors.textMuted} color={selected ? colors.text : colors.textMuted}
size={18} size={18}
strokeWidth={2.4} strokeWidth={2.4}
/> />
{isTabLive ? (
<View style={styles.tabLiveDot}>
<LiveDot live size={4} />
</View>
) : null}
</View>
<Text <Text
style={[ style={[
styles.navLabel, styles.navLabel,
@@ -169,31 +195,13 @@ export function FightRateApp() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
root: { root: { flex: 1, backgroundColor: colors.canvas },
flex: 1, content: { paddingHorizontal: spacing.md, gap: spacing.lg },
backgroundColor: colors.canvas, brandRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.md },
}, logo: { width: 54, height: 54, borderRadius: radius.sm },
content: { titleColumn: { flex: 1 },
paddingHorizontal: spacing.md, kickerRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
gap: spacing.lg, kicker: { ...typography.label, color: colors.action },
},
brandRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.md,
},
logo: {
width: 54,
height: 54,
borderRadius: radius.sm,
},
titleColumn: {
flex: 1,
},
kicker: {
...typography.label,
color: colors.action,
},
title: { title: {
marginTop: spacing.xs, marginTop: spacing.xs,
color: colors.text, color: colors.text,
@@ -202,11 +210,7 @@ const styles = StyleSheet.create({
fontWeight: '900', fontWeight: '900',
letterSpacing: -2.1, letterSpacing: -2.1,
}, },
navWrap: { navWrap: { position: 'absolute', left: spacing.md, right: spacing.md },
position: 'absolute',
left: spacing.md,
right: spacing.md,
},
navRail: { navRail: {
borderWidth: 1, borderWidth: 1,
borderColor: colors.line, borderColor: colors.line,
@@ -228,15 +232,9 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
gap: spacing.xxs, gap: spacing.xxs,
}, },
navItemActive: { navItemActive: { backgroundColor: colors.action },
backgroundColor: colors.action, navIconRow: { position: 'relative' },
}, tabLiveDot: { position: 'absolute', top: -2, right: -6 },
navLabel: { navLabel: { ...typography.label, color: colors.textMuted, fontSize: 9 },
...typography.label, navLabelActive: { color: colors.text },
color: colors.textMuted,
fontSize: 9,
},
navLabelActive: {
color: colors.text,
},
}); });