Workout detail (workout/[id]): - Dynamic data via useLocalSearchParams + getWorkoutById - VideoPlayer hero with trainer color gradient fallback - Exercise list, equipment, music vibe, "Start Workout" CTA Player (player/[id]): - useTimer hook drives phase transitions (PREP/WORK/REST/COMPLETE) - useHaptics for phase changes and countdown ticks - useAudio for sound effects (beeps, dings, completion chime) - Real calorie tracking, progress ring, exercise display - Saves WorkoutResult to activityStore on completion Complete (complete/[id]): - Reads real stats from activityStore history - Burn bar, streak counter, calories/duration/completion stats - Recommended workouts, share via expo-sharing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
757 lines
20 KiB
TypeScript
757 lines
20 KiB
TypeScript
/**
|
|
* TabataFit Player Screen
|
|
* Full-screen workout player with timer overlay
|
|
* Wired to shared data + useTimer hook
|
|
*/
|
|
|
|
import React, { useRef, useEffect, useCallback, useState } from 'react'
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
Pressable,
|
|
Animated,
|
|
Dimensions,
|
|
StatusBar,
|
|
} from 'react-native'
|
|
import { useRouter, useLocalSearchParams } from 'expo-router'
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
import { LinearGradient } from 'expo-linear-gradient'
|
|
import { BlurView } from 'expo-blur'
|
|
import { useKeepAwake } from 'expo-keep-awake'
|
|
import Ionicons from '@expo/vector-icons/Ionicons'
|
|
|
|
import { useTimer } from '@/src/shared/hooks/useTimer'
|
|
import { useHaptics } from '@/src/shared/hooks/useHaptics'
|
|
import { useAudio } from '@/src/shared/hooks/useAudio'
|
|
import { useActivityStore } from '@/src/shared/stores'
|
|
import { getWorkoutById, getTrainerById } from '@/src/shared/data'
|
|
|
|
import {
|
|
BRAND,
|
|
DARK,
|
|
TEXT,
|
|
GLASS,
|
|
SHADOW,
|
|
PHASE_COLORS,
|
|
GRADIENTS,
|
|
} from '@/src/shared/constants/colors'
|
|
import { TYPOGRAPHY } from '@/src/shared/constants/typography'
|
|
import { SPACING } from '@/src/shared/constants/spacing'
|
|
import { RADIUS } from '@/src/shared/constants/borderRadius'
|
|
import { DURATION, EASE, SPRING } from '@/src/shared/constants/animations'
|
|
|
|
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window')
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// COMPONENTS
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
type TimerPhase = 'PREP' | 'WORK' | 'REST' | 'COMPLETE'
|
|
|
|
function TimerRing({
|
|
progress,
|
|
phase,
|
|
size = 280,
|
|
}: {
|
|
progress: number
|
|
phase: TimerPhase
|
|
size?: number
|
|
}) {
|
|
const strokeWidth = 12
|
|
const phaseColor = PHASE_COLORS[phase].fill
|
|
|
|
return (
|
|
<View style={[timerStyles.timerRingContainer, { width: size, height: size }]}>
|
|
<View
|
|
style={[
|
|
timerStyles.timerRingBg,
|
|
{
|
|
width: size,
|
|
height: size,
|
|
borderRadius: size / 2,
|
|
borderWidth: strokeWidth,
|
|
},
|
|
]}
|
|
/>
|
|
<View style={timerStyles.timerRingContent}>
|
|
<View
|
|
style={[
|
|
timerStyles.timerProgressRing,
|
|
{
|
|
width: size - strokeWidth * 2,
|
|
height: size - strokeWidth * 2,
|
|
borderRadius: (size - strokeWidth * 2) / 2,
|
|
borderColor: phaseColor,
|
|
borderTopWidth: strokeWidth,
|
|
opacity: progress,
|
|
},
|
|
]}
|
|
/>
|
|
</View>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function PhaseIndicator({ phase }: { phase: TimerPhase }) {
|
|
const phaseColor = PHASE_COLORS[phase].fill
|
|
const phaseLabels: Record<TimerPhase, string> = {
|
|
PREP: 'GET READY',
|
|
WORK: 'WORK',
|
|
REST: 'REST',
|
|
COMPLETE: 'COMPLETE',
|
|
}
|
|
|
|
return (
|
|
<View style={[timerStyles.phaseIndicator, { backgroundColor: `${phaseColor}20` }]}>
|
|
<Text style={[timerStyles.phaseText, { color: phaseColor }]}>{phaseLabels[phase]}</Text>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function ExerciseDisplay({
|
|
exercise,
|
|
nextExercise,
|
|
}: {
|
|
exercise: string
|
|
nextExercise?: string
|
|
}) {
|
|
return (
|
|
<View style={timerStyles.exerciseDisplay}>
|
|
<Text style={timerStyles.currentExerciseLabel}>Current</Text>
|
|
<Text style={timerStyles.currentExercise}>{exercise}</Text>
|
|
{nextExercise && (
|
|
<View style={timerStyles.nextExerciseContainer}>
|
|
<Text style={timerStyles.nextExerciseLabel}>Next: </Text>
|
|
<Text style={timerStyles.nextExercise}>{nextExercise}</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function RoundIndicator({ current, total }: { current: number; total: number }) {
|
|
return (
|
|
<View style={timerStyles.roundIndicator}>
|
|
<Text style={timerStyles.roundText}>
|
|
Round <Text style={timerStyles.roundCurrent}>{current}</Text>/{total}
|
|
</Text>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function ControlButton({
|
|
icon,
|
|
onPress,
|
|
size = 64,
|
|
variant = 'primary',
|
|
}: {
|
|
icon: keyof typeof Ionicons.glyphMap
|
|
onPress: () => void
|
|
size?: number
|
|
variant?: 'primary' | 'secondary' | 'danger'
|
|
}) {
|
|
const scaleAnim = useRef(new Animated.Value(1)).current
|
|
|
|
const handlePressIn = () => {
|
|
Animated.spring(scaleAnim, {
|
|
toValue: 0.9,
|
|
...SPRING.SNAPPY,
|
|
useNativeDriver: true,
|
|
}).start()
|
|
}
|
|
|
|
const handlePressOut = () => {
|
|
Animated.spring(scaleAnim, {
|
|
toValue: 1,
|
|
...SPRING.BOUNCY,
|
|
useNativeDriver: true,
|
|
}).start()
|
|
}
|
|
|
|
const backgroundColor =
|
|
variant === 'primary'
|
|
? BRAND.PRIMARY
|
|
: variant === 'danger'
|
|
? '#FF3B30'
|
|
: 'rgba(255, 255, 255, 0.1)'
|
|
|
|
return (
|
|
<Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
|
|
<Pressable
|
|
onPressIn={handlePressIn}
|
|
onPressOut={handlePressOut}
|
|
onPress={onPress}
|
|
style={[timerStyles.controlButton, { width: size, height: size, borderRadius: size / 2 }]}
|
|
>
|
|
<View style={[timerStyles.controlButtonBg, { backgroundColor }]} />
|
|
<Ionicons name={icon} size={size * 0.4} color={TEXT.PRIMARY} />
|
|
</Pressable>
|
|
</Animated.View>
|
|
)
|
|
}
|
|
|
|
function BurnBar({
|
|
currentCalories,
|
|
avgCalories,
|
|
}: {
|
|
currentCalories: number
|
|
avgCalories: number
|
|
}) {
|
|
const percentage = Math.min((currentCalories / avgCalories) * 100, 100)
|
|
|
|
return (
|
|
<View style={timerStyles.burnBar}>
|
|
<View style={timerStyles.burnBarHeader}>
|
|
<Text style={timerStyles.burnBarLabel}>Burn Bar</Text>
|
|
<Text style={timerStyles.burnBarValue}>{currentCalories} cal</Text>
|
|
</View>
|
|
<View style={timerStyles.burnBarTrack}>
|
|
<View style={[timerStyles.burnBarFill, { width: `${percentage}%` }]} />
|
|
<View style={[timerStyles.burnBarAvg, { left: '50%' }]} />
|
|
</View>
|
|
<Text style={timerStyles.burnBarAvgLabel}>Community avg: {avgCalories} cal</Text>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// MAIN SCREEN
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export default function PlayerScreen() {
|
|
useKeepAwake()
|
|
const router = useRouter()
|
|
const { id } = useLocalSearchParams<{ id: string }>()
|
|
const insets = useSafeAreaInsets()
|
|
const haptics = useHaptics()
|
|
const addWorkoutResult = useActivityStore((s) => s.addWorkoutResult)
|
|
|
|
const workout = getWorkoutById(id ?? '1')
|
|
const trainer = workout ? getTrainerById(workout.trainerId) : null
|
|
|
|
const timer = useTimer(workout ?? null)
|
|
const audio = useAudio()
|
|
|
|
const [showControls, setShowControls] = useState(true)
|
|
|
|
// Animation refs
|
|
const timerScaleAnim = useRef(new Animated.Value(0.8)).current
|
|
const glowAnim = useRef(new Animated.Value(0)).current
|
|
|
|
const phaseColor = PHASE_COLORS[timer.phase].fill
|
|
|
|
// Format time
|
|
const formatTime = (seconds: number) => {
|
|
const mins = Math.floor(seconds / 60)
|
|
const secs = seconds % 60
|
|
return mins > 0 ? `${mins}:${secs.toString().padStart(2, '0')}` : `${secs}`
|
|
}
|
|
|
|
// Start timer
|
|
const startTimer = useCallback(() => {
|
|
timer.start()
|
|
haptics.buttonTap()
|
|
}, [timer, haptics])
|
|
|
|
// Pause/Resume
|
|
const togglePause = useCallback(() => {
|
|
if (timer.isPaused) {
|
|
timer.resume()
|
|
} else {
|
|
timer.pause()
|
|
}
|
|
haptics.selection()
|
|
}, [timer, haptics])
|
|
|
|
// Stop workout
|
|
const stopWorkout = useCallback(() => {
|
|
haptics.phaseChange()
|
|
timer.stop()
|
|
router.back()
|
|
}, [router, timer, haptics])
|
|
|
|
// Complete workout - go to celebration screen
|
|
const completeWorkout = useCallback(() => {
|
|
haptics.workoutComplete()
|
|
if (workout) {
|
|
addWorkoutResult({
|
|
id: Date.now().toString(),
|
|
workoutId: workout.id,
|
|
completedAt: Date.now(),
|
|
calories: timer.calories,
|
|
durationMinutes: workout.duration,
|
|
rounds: workout.rounds,
|
|
completionRate: 1,
|
|
})
|
|
}
|
|
router.replace(`/complete/${workout?.id ?? '1'}`)
|
|
}, [router, workout, timer.calories, haptics, addWorkoutResult])
|
|
|
|
// Skip
|
|
const handleSkip = useCallback(() => {
|
|
timer.skip()
|
|
haptics.selection()
|
|
}, [timer, haptics])
|
|
|
|
// Toggle controls visibility
|
|
const toggleControls = useCallback(() => {
|
|
setShowControls(s => !s)
|
|
}, [])
|
|
|
|
// Entrance animation
|
|
useEffect(() => {
|
|
Animated.parallel([
|
|
Animated.spring(timerScaleAnim, {
|
|
toValue: 1,
|
|
friction: 6,
|
|
tension: 100,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(glowAnim, {
|
|
toValue: 1,
|
|
duration: DURATION.BREATH,
|
|
easing: EASE.EASE_IN_OUT,
|
|
useNativeDriver: false,
|
|
}),
|
|
Animated.timing(glowAnim, {
|
|
toValue: 0,
|
|
duration: DURATION.BREATH,
|
|
easing: EASE.EASE_IN_OUT,
|
|
useNativeDriver: false,
|
|
}),
|
|
])
|
|
),
|
|
]).start()
|
|
}, [])
|
|
|
|
// Phase change animation + audio
|
|
useEffect(() => {
|
|
timerScaleAnim.setValue(0.9)
|
|
Animated.spring(timerScaleAnim, {
|
|
toValue: 1,
|
|
friction: 4,
|
|
tension: 150,
|
|
useNativeDriver: true,
|
|
}).start()
|
|
haptics.phaseChange()
|
|
if (timer.phase === 'COMPLETE') {
|
|
audio.workoutComplete()
|
|
} else if (timer.isRunning) {
|
|
audio.phaseStart()
|
|
}
|
|
}, [timer.phase])
|
|
|
|
// Countdown beep for last 3 seconds
|
|
useEffect(() => {
|
|
if (timer.isRunning && timer.timeRemaining <= 3 && timer.timeRemaining > 0) {
|
|
audio.countdownBeep()
|
|
}
|
|
}, [timer.timeRemaining])
|
|
|
|
const glowOpacity = glowAnim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [0.3, 0.6],
|
|
})
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<StatusBar hidden />
|
|
|
|
{/* Background gradient */}
|
|
<LinearGradient
|
|
colors={[DARK.BASE, DARK.SURFACE]}
|
|
style={StyleSheet.absoluteFill}
|
|
/>
|
|
|
|
{/* Phase glow */}
|
|
<Animated.View
|
|
style={[
|
|
styles.phaseGlow,
|
|
{ opacity: glowOpacity, backgroundColor: phaseColor },
|
|
]}
|
|
/>
|
|
|
|
{/* Main content */}
|
|
<Pressable style={styles.content} onPress={toggleControls}>
|
|
{/* Header */}
|
|
{showControls && (
|
|
<View style={[styles.header, { paddingTop: insets.top + SPACING[4] }]}>
|
|
<Pressable onPress={stopWorkout} style={styles.closeButton}>
|
|
<BlurView intensity={GLASS.BLUR_LIGHT} tint="dark" style={StyleSheet.absoluteFill} />
|
|
<Ionicons name="close" size={24} color={TEXT.PRIMARY} />
|
|
</Pressable>
|
|
<View style={styles.headerCenter}>
|
|
<Text style={styles.workoutTitle}>{workout?.title ?? 'Workout'}</Text>
|
|
<Text style={styles.workoutTrainer}>with {trainer?.name ?? 'Coach'}</Text>
|
|
</View>
|
|
<View style={styles.closeButton} />
|
|
</View>
|
|
)}
|
|
|
|
{/* Timer */}
|
|
<Animated.View
|
|
style={[
|
|
styles.timerContainer,
|
|
{ transform: [{ scale: timerScaleAnim }] },
|
|
]}
|
|
>
|
|
<TimerRing progress={timer.progress} phase={timer.phase} />
|
|
|
|
<View style={timerStyles.timerTextContainer}>
|
|
<PhaseIndicator phase={timer.phase} />
|
|
<Text style={timerStyles.timerTime}>{formatTime(timer.timeRemaining)}</Text>
|
|
<RoundIndicator current={timer.currentRound} total={timer.totalRounds} />
|
|
</View>
|
|
</Animated.View>
|
|
|
|
{/* Exercise */}
|
|
{!timer.isComplete && (
|
|
<ExerciseDisplay
|
|
exercise={timer.currentExercise}
|
|
nextExercise={timer.nextExercise}
|
|
/>
|
|
)}
|
|
|
|
{/* Complete state */}
|
|
{timer.isComplete && (
|
|
<View style={styles.completeContainer}>
|
|
<Text style={styles.completeTitle}>Workout Complete!</Text>
|
|
<Text style={styles.completeSubtitle}>Great job!</Text>
|
|
<View style={styles.completeStats}>
|
|
<View style={styles.completeStat}>
|
|
<Text style={styles.completeStatValue}>{timer.totalRounds}</Text>
|
|
<Text style={styles.completeStatLabel}>Rounds</Text>
|
|
</View>
|
|
<View style={styles.completeStat}>
|
|
<Text style={styles.completeStatValue}>{timer.calories}</Text>
|
|
<Text style={styles.completeStatLabel}>Calories</Text>
|
|
</View>
|
|
<View style={styles.completeStat}>
|
|
<Text style={styles.completeStatValue}>{workout?.duration ?? 4}</Text>
|
|
<Text style={styles.completeStatLabel}>Minutes</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Controls */}
|
|
{showControls && !timer.isComplete && (
|
|
<View style={[styles.controls, { paddingBottom: insets.bottom + SPACING[6] }]}>
|
|
{!timer.isRunning ? (
|
|
<ControlButton icon="play" onPress={startTimer} size={80} />
|
|
) : (
|
|
<View style={styles.controlsRow}>
|
|
<ControlButton
|
|
icon="stop"
|
|
onPress={stopWorkout}
|
|
size={56}
|
|
variant="danger"
|
|
/>
|
|
<ControlButton
|
|
icon={timer.isPaused ? 'play' : 'pause'}
|
|
onPress={togglePause}
|
|
size={80}
|
|
/>
|
|
<ControlButton
|
|
icon="play-skip-forward"
|
|
onPress={handleSkip}
|
|
size={56}
|
|
variant="secondary"
|
|
/>
|
|
</View>
|
|
)}
|
|
</View>
|
|
)}
|
|
|
|
{/* Complete button */}
|
|
{timer.isComplete && (
|
|
<View style={[styles.controls, { paddingBottom: insets.bottom + SPACING[6] }]}>
|
|
<Pressable style={styles.doneButton} onPress={completeWorkout}>
|
|
<LinearGradient
|
|
colors={GRADIENTS.CTA}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={StyleSheet.absoluteFill}
|
|
/>
|
|
<Text style={styles.doneButtonText}>Done</Text>
|
|
</Pressable>
|
|
</View>
|
|
)}
|
|
|
|
{/* Burn Bar */}
|
|
{showControls && timer.isRunning && !timer.isComplete && (
|
|
<View style={[styles.burnBarContainer, { bottom: insets.bottom + 140 }]}>
|
|
<BlurView intensity={GLASS.BLUR_MEDIUM} tint="dark" style={StyleSheet.absoluteFill} />
|
|
<BurnBar currentCalories={timer.calories} avgCalories={workout?.calories ?? 45} />
|
|
</View>
|
|
)}
|
|
</Pressable>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// STYLES
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
const timerStyles = StyleSheet.create({
|
|
timerRingContainer: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
timerRingBg: {
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
position: 'absolute',
|
|
},
|
|
timerRingContent: {
|
|
position: 'absolute',
|
|
},
|
|
timerProgressRing: {
|
|
position: 'absolute',
|
|
},
|
|
timerTextContainer: {
|
|
position: 'absolute',
|
|
alignItems: 'center',
|
|
},
|
|
phaseIndicator: {
|
|
paddingHorizontal: SPACING[4],
|
|
paddingVertical: SPACING[1],
|
|
borderRadius: RADIUS.FULL,
|
|
marginBottom: SPACING[2],
|
|
},
|
|
phaseText: {
|
|
...TYPOGRAPHY.CALLOUT,
|
|
fontWeight: '700',
|
|
letterSpacing: 1,
|
|
},
|
|
timerTime: {
|
|
...TYPOGRAPHY.TIMER_NUMBER,
|
|
color: TEXT.PRIMARY,
|
|
},
|
|
roundIndicator: {
|
|
marginTop: SPACING[2],
|
|
},
|
|
roundText: {
|
|
...TYPOGRAPHY.BODY,
|
|
color: TEXT.TERTIARY,
|
|
},
|
|
roundCurrent: {
|
|
color: TEXT.PRIMARY,
|
|
fontWeight: '700',
|
|
},
|
|
|
|
// Exercise
|
|
exerciseDisplay: {
|
|
alignItems: 'center',
|
|
marginTop: SPACING[6],
|
|
paddingHorizontal: SPACING[6],
|
|
},
|
|
currentExerciseLabel: {
|
|
...TYPOGRAPHY.CAPTION_1,
|
|
color: TEXT.TERTIARY,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 1,
|
|
},
|
|
currentExercise: {
|
|
...TYPOGRAPHY.TITLE_1,
|
|
color: TEXT.PRIMARY,
|
|
textAlign: 'center',
|
|
marginTop: SPACING[1],
|
|
},
|
|
nextExerciseContainer: {
|
|
flexDirection: 'row',
|
|
marginTop: SPACING[2],
|
|
},
|
|
nextExerciseLabel: {
|
|
...TYPOGRAPHY.BODY,
|
|
color: TEXT.TERTIARY,
|
|
},
|
|
nextExercise: {
|
|
...TYPOGRAPHY.BODY,
|
|
color: BRAND.PRIMARY,
|
|
},
|
|
|
|
// Controls
|
|
controlButton: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
controlButtonBg: {
|
|
position: 'absolute',
|
|
width: '100%',
|
|
height: '100%',
|
|
borderRadius: 100,
|
|
},
|
|
|
|
// Burn Bar
|
|
burnBar: {},
|
|
burnBarHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
marginBottom: SPACING[2],
|
|
},
|
|
burnBarLabel: {
|
|
...TYPOGRAPHY.CAPTION_1,
|
|
color: TEXT.TERTIARY,
|
|
},
|
|
burnBarValue: {
|
|
...TYPOGRAPHY.CALLOUT,
|
|
color: BRAND.PRIMARY,
|
|
fontWeight: '600',
|
|
},
|
|
burnBarTrack: {
|
|
height: 6,
|
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
borderRadius: 3,
|
|
overflow: 'hidden',
|
|
},
|
|
burnBarFill: {
|
|
height: '100%',
|
|
backgroundColor: BRAND.PRIMARY,
|
|
borderRadius: 3,
|
|
},
|
|
burnBarAvg: {
|
|
position: 'absolute',
|
|
top: -2,
|
|
width: 2,
|
|
height: 10,
|
|
backgroundColor: TEXT.TERTIARY,
|
|
},
|
|
burnBarAvgLabel: {
|
|
...TYPOGRAPHY.CAPTION_2,
|
|
color: TEXT.TERTIARY,
|
|
marginTop: SPACING[1],
|
|
textAlign: 'right',
|
|
},
|
|
})
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: DARK.BASE,
|
|
},
|
|
phaseGlow: {
|
|
position: 'absolute',
|
|
top: -100,
|
|
left: -100,
|
|
right: -100,
|
|
bottom: -100,
|
|
borderRadius: 500,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
},
|
|
|
|
// Header
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: SPACING[4],
|
|
},
|
|
closeButton: {
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 22,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
overflow: 'hidden',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
},
|
|
headerCenter: {
|
|
alignItems: 'center',
|
|
},
|
|
workoutTitle: {
|
|
...TYPOGRAPHY.HEADLINE,
|
|
color: TEXT.PRIMARY,
|
|
},
|
|
workoutTrainer: {
|
|
...TYPOGRAPHY.CAPTION_1,
|
|
color: TEXT.TERTIARY,
|
|
},
|
|
|
|
// Timer
|
|
timerContainer: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginTop: SPACING[8],
|
|
},
|
|
|
|
// Controls
|
|
controls: {
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
alignItems: 'center',
|
|
},
|
|
controlsRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: SPACING[6],
|
|
},
|
|
|
|
// Burn Bar
|
|
burnBarContainer: {
|
|
position: 'absolute',
|
|
left: SPACING[4],
|
|
right: SPACING[4],
|
|
height: 72,
|
|
borderRadius: RADIUS.LG,
|
|
overflow: 'hidden',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
padding: SPACING[3],
|
|
},
|
|
|
|
// Complete
|
|
completeContainer: {
|
|
alignItems: 'center',
|
|
marginTop: SPACING[8],
|
|
},
|
|
completeTitle: {
|
|
...TYPOGRAPHY.LARGE_TITLE,
|
|
color: TEXT.PRIMARY,
|
|
},
|
|
completeSubtitle: {
|
|
...TYPOGRAPHY.TITLE_3,
|
|
color: BRAND.PRIMARY,
|
|
marginTop: SPACING[1],
|
|
},
|
|
completeStats: {
|
|
flexDirection: 'row',
|
|
marginTop: SPACING[6],
|
|
gap: SPACING[8],
|
|
},
|
|
completeStat: {
|
|
alignItems: 'center',
|
|
},
|
|
completeStatValue: {
|
|
...TYPOGRAPHY.TITLE_1,
|
|
color: TEXT.PRIMARY,
|
|
},
|
|
completeStatLabel: {
|
|
...TYPOGRAPHY.CAPTION_1,
|
|
color: TEXT.TERTIARY,
|
|
marginTop: SPACING[1],
|
|
},
|
|
doneButton: {
|
|
width: 200,
|
|
height: 56,
|
|
borderRadius: RADIUS.GLASS_BUTTON,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
overflow: 'hidden',
|
|
...SHADOW.BRAND_GLOW,
|
|
},
|
|
doneButtonText: {
|
|
...TYPOGRAPHY.BUTTON_MEDIUM,
|
|
color: TEXT.PRIMARY,
|
|
letterSpacing: 1,
|
|
},
|
|
})
|