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