Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion android/src/main/java/voltra/generated/ShortNames.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ object ShortNames {
"al" to "alignment",
"ar" to "aspectRatio",
"an" to "assetName",
"ahe" to "autoHideOnEnd",
"bkg" to "background",
"bg" to "backgroundColor",
"bgs" to "backgroundStyle",
Expand Down
6 changes: 0 additions & 6 deletions data/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"shortNames": {
"style": "s",
"alignment": "al",
"autoHideOnEnd": "ahe",
"buttonStyle": "bs",
"colors": "cls",
"cornerRadius": "cr",
Expand Down Expand Up @@ -685,11 +684,6 @@
"default": "down",
"description": "Count direction"
},
"autoHideOnEnd": {
"type": "boolean",
"optional": true,
"description": "Hide timer when complete"
},
"textStyle": {
"type": "string",
"optional": true,
Expand Down
3 changes: 3 additions & 0 deletions example/app/testing-grounds/timer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import TimerTestingScreen from '../../screens/testing-grounds/timer/TimerTestingScreen'

export default TimerTestingScreen
7 changes: 7 additions & 0 deletions example/screens/testing-grounds/TestingGroundsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ const TESTING_GROUNDS_SECTIONS = [
'Test the weather widget with different conditions, gradients, and real-time updates. Change weather conditions and see the widget update instantly.',
route: '/testing-grounds/weather',
},
{
id: 'timer',
title: 'Timer',
description:
'Test the VoltraTimer component with different styles (Timer/Relative), count directions, and templates. Verifies native Live Activity behavior.',
route: '/testing-grounds/timer',
},
{
id: 'styling',
title: 'Styling',
Expand Down
238 changes: 238 additions & 0 deletions example/screens/testing-grounds/timer/TimerTestingScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { useRouter } from 'expo-router'
import React, { useState } from 'react'
import { ScrollView, StyleSheet, Text, TextInput, useColorScheme, View } from 'react-native'
import { Voltra } from 'voltra'
import { VoltraWidgetPreview } from 'voltra/client'

import { Button } from '~/components/Button'
import { Card } from '~/components/Card'

const DEFAULT_TEMPLATES = {
running: 'Time remaining: {time}',
completed: 'Timer finished!',
}

export default function TimerTestingScreen() {
const router = useRouter()
const colorScheme = useColorScheme()

// Timer State
const [mode, setMode] = useState<'timer' | 'stopwatch'>('timer')
const [direction, setDirection] = useState<'up' | 'down'>('down')
const [textStyle, setTextStyle] = useState<'timer' | 'relative'>('timer')
const [showHours, setShowHours] = useState(false)
const [durationSec, setDurationSec] = useState('300') // 5 minutes default
const [templateJson, setTemplateJson] = useState(JSON.stringify(DEFAULT_TEMPLATES, null, 2))

// Timestamps
const [timerState, setTimerState] = useState<{ startAtMs?: number; endAtMs?: number; durationMs?: number }>({
endAtMs: Date.now() + 300000,
})

const widgetPreviewStyle = {
borderRadius: 16,
backgroundColor: colorScheme === 'light' ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.8)',
}

const resetTimer = () => {
const duration = (parseInt(durationSec) || 0) * 1000
const now = Date.now()

if (mode === 'stopwatch') {
setTimerState({
startAtMs: now,
endAtMs: undefined,
durationMs: undefined,
})
setDirection('up')
} else {
if (direction === 'down') {
setTimerState({
startAtMs: now,
endAtMs: now + duration,
durationMs: duration,
})
} else {
setTimerState({
startAtMs: now,
endAtMs: now + duration,
durationMs: duration,
})
}
}
}

// Timer Component for Preview
const renderTimerWidget = () => (
<Voltra.ZStack style={{ flex: 1, backgroundColor: '#1a1a2e', padding: 16 }}>
<Voltra.VStack spacing={8} alignment="center">
<Voltra.Text style={{ color: '#aaa', fontSize: 14 }}>Voltra Timer</Voltra.Text>
<Voltra.Timer
style={{
color: '#ffffff',
fontSize: 24,
fontWeight: 'bold',
}}
startAtMs={timerState.startAtMs}
endAtMs={timerState.endAtMs}
durationMs={timerState.durationMs}
direction={mode === 'stopwatch' ? 'up' : direction}
textStyle={textStyle}
showHours={showHours}
textTemplates={templateJson}
/>
</Voltra.VStack>
</Voltra.ZStack>
)

return (
<View style={styles.container}>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.content}>
<Text style={styles.heading}>Timer Testing</Text>
<Text style={styles.subheading}>
Test the VoltraTimer component behaviors, including native text updates for Live Activities.
</Text>

{/* Preview */}
<Card>
<Card.Title>Live Preview</Card.Title>
<View style={styles.previewContainer}>
<VoltraWidgetPreview family="systemMedium" style={widgetPreviewStyle}>
{renderTimerWidget()}
</VoltraWidgetPreview>
</View>
<Button title="Reset / Start Timer" variant="primary" onPress={resetTimer} style={{ marginTop: 16 }} />
</Card>

{/* Configuration */}
<Card>
<Card.Title>Configuration</Card.Title>

<View style={styles.row}>
<Text style={styles.label}>Mode</Text>
<View style={styles.toggleGroup}>
<Button
title="Timer"
variant={mode === 'timer' ? 'primary' : 'secondary'}
onPress={() => setMode('timer')}
style={styles.smButton}
/>
<Button
title="Stopwatch"
variant={mode === 'stopwatch' ? 'primary' : 'secondary'}
onPress={() => setMode('stopwatch')}
style={styles.smButton}
/>
</View>
</View>

{mode === 'timer' && (
<>
<View style={styles.row}>
<Text style={styles.label}>Direction</Text>
<View style={styles.toggleGroup}>
<Button
title="Down"
variant={direction === 'down' ? 'primary' : 'secondary'}
onPress={() => setDirection('down')}
style={styles.smButton}
/>
<Button
title="Up"
variant={direction === 'up' ? 'primary' : 'secondary'}
onPress={() => setDirection('up')}
style={styles.smButton}
/>
</View>
</View>

<View style={styles.row}>
<Text style={styles.label}>Duration (seconds)</Text>
<TextInput
style={styles.input}
value={durationSec}
onChangeText={setDurationSec}
keyboardType="numeric"
/>
</View>
</>
)}

<View style={styles.row}>
<Text style={styles.label}>Style</Text>
<View style={styles.toggleGroup}>
<Button
title="Timer"
variant={textStyle === 'timer' ? 'primary' : 'secondary'}
onPress={() => setTextStyle('timer')}
style={styles.smButton}
/>
<Button
title="Relative"
variant={textStyle === 'relative' ? 'primary' : 'secondary'}
onPress={() => setTextStyle('relative')}
style={styles.smButton}
/>
</View>
</View>

<View style={styles.row}>
<Text style={styles.label}>Show Hours</Text>
<View style={styles.toggleGroup}>
<Button
title="Off"
variant={!showHours ? 'primary' : 'secondary'}
onPress={() => setShowHours(false)}
style={styles.smButton}
/>
<Button
title="On"
variant={showHours ? 'primary' : 'secondary'}
onPress={() => setShowHours(true)}
style={styles.smButton}
/>
</View>
</View>

<View style={styles.col}>
<Text style={styles.label}>Templates (JSON)</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={templateJson}
onChangeText={setTemplateJson}
multiline
/>
</View>
</Card>

<View style={styles.footer}>
<Button title="Back" variant="ghost" onPress={() => router.back()} />
</View>
</ScrollView>
</View>
)
}

const styles = StyleSheet.create({
container: { flex: 1 },
scrollView: { flex: 1 },
content: { padding: 20 },
heading: { fontSize: 24, fontWeight: '700', color: '#FFFFFF', marginBottom: 8 },
subheading: { fontSize: 14, color: '#CBD5F5', marginBottom: 24 },
previewContainer: { alignItems: 'center', padding: 10 },
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 },
col: { marginBottom: 16 },
label: { color: '#fff', fontSize: 16 },
input: {
backgroundColor: '#rgba(255,255,255,0.1)',
color: '#fff',
padding: 8,
borderRadius: 8,
minWidth: 100,
textAlign: 'right',
},
textArea: { textAlign: 'left', height: 100, textAlignVertical: 'top' },
toggleGroup: { flexDirection: 'row', gap: 8 },
smButton: { paddingVertical: 6, paddingHorizontal: 12 },
footer: { marginTop: 24, alignItems: 'center' },
})
1 change: 0 additions & 1 deletion ios/shared/ShortNames.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public enum ShortNames {
"al": "alignment",
"ar": "aspectRatio",
"an": "assetName",
"ahe": "autoHideOnEnd",
"bkg": "background",
"bg": "backgroundColor",
"bgs": "backgroundStyle",
Expand Down
5 changes: 0 additions & 5 deletions ios/ui/Generated/Parameters/TimerParameters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ public struct TimerParameters: ComponentParameters {
/// Count direction
public let direction: String

/// Hide timer when complete
public let autoHideOnEnd: Bool?

/// Text formatting style
public let textStyle: String

Expand All @@ -39,7 +36,6 @@ public struct TimerParameters: ComponentParameters {
case startAtMs
case durationMs
case direction
case autoHideOnEnd
case textStyle
case textTemplates
case showHours
Expand All @@ -51,7 +47,6 @@ public struct TimerParameters: ComponentParameters {
startAtMs = try container.decodeIfPresent(Double.self, forKey: .startAtMs)
durationMs = try container.decodeIfPresent(Double.self, forKey: .durationMs)
direction = try container.decodeIfPresent(String.self, forKey: .direction) ?? "down"
autoHideOnEnd = try container.decodeIfPresent(Bool.self, forKey: .autoHideOnEnd)
textStyle = try container.decodeIfPresent(String.self, forKey: .textStyle) ?? "timer"
textTemplates = try container.decodeIfPresent(String.self, forKey: .textTemplates)
showHours = try container.decodeIfPresent(Bool.self, forKey: .showHours) ?? false
Expand Down
Loading
Loading