diff --git a/data/components.json b/data/components.json
index 328a014..9e8e858 100644
--- a/data/components.json
+++ b/data/components.json
@@ -539,6 +539,16 @@
"type": "component",
"optional": true,
"description": "Custom thumb component to display at progress position"
+ },
+ "label": {
+ "type": "component",
+ "optional": true,
+ "description": "Label content for the progress indicator"
+ },
+ "currentValueLabel": {
+ "type": "component",
+ "optional": true,
+ "description": "Custom text for current value label"
}
}
},
@@ -587,6 +597,16 @@
"type": "number",
"optional": true,
"description": "Width of the stroke line"
+ },
+ "label": {
+ "type": "component",
+ "optional": true,
+ "description": "Label content for the progress indicator"
+ },
+ "currentValueLabel": {
+ "type": "component",
+ "optional": true,
+ "description": "Custom text for current value label"
}
}
},
diff --git a/example/app/testing-grounds/progress.tsx b/example/app/testing-grounds/progress.tsx
new file mode 100644
index 0000000..50ee0cc
--- /dev/null
+++ b/example/app/testing-grounds/progress.tsx
@@ -0,0 +1,7 @@
+import React from 'react'
+
+import ProgressTestingScreen from '~/screens/testing-grounds/progress/ProgressTestingScreen'
+
+export default function ProgressPage() {
+ return
+}
diff --git a/example/screens/testing-grounds/TestingGroundsScreen.tsx b/example/screens/testing-grounds/TestingGroundsScreen.tsx
index 95faf98..c546097 100644
--- a/example/screens/testing-grounds/TestingGroundsScreen.tsx
+++ b/example/screens/testing-grounds/TestingGroundsScreen.tsx
@@ -34,6 +34,13 @@ const TESTING_GROUNDS_SECTIONS = [
'Learn about static, relative, and absolute positioning modes. See how left, top, and zIndex properties work with visual examples.',
route: '/testing-grounds/positioning',
},
+ {
+ id: 'progress',
+ title: 'Progress Indicators',
+ description:
+ 'Explore linear and circular progress indicators. Test determinate, indeterminate, and timer-based modes with custom labels and styling.',
+ route: '/testing-grounds/progress',
+ },
{
id: 'components',
title: 'Components',
diff --git a/example/screens/testing-grounds/components/ComponentsScreen.tsx b/example/screens/testing-grounds/components/ComponentsScreen.tsx
index 40b3861..6eb06fd 100644
--- a/example/screens/testing-grounds/components/ComponentsScreen.tsx
+++ b/example/screens/testing-grounds/components/ComponentsScreen.tsx
@@ -150,10 +150,21 @@ const COMPONENTS_DATA = [
title: 'Progress Components',
description: 'Linear and circular progress indicators for showing completion states.',
renderExample: () => (
-
-
-
-
+
+
+ Downloading...}
+ currentValueLabel={75%}
+ />
+ Uptime}
+ currentValueLabel={45%}
+ style={{ width: 60, height: 60 }}
+ />
),
diff --git a/example/screens/testing-grounds/progress/ProgressTestingScreen.tsx b/example/screens/testing-grounds/progress/ProgressTestingScreen.tsx
new file mode 100644
index 0000000..42957cc
--- /dev/null
+++ b/example/screens/testing-grounds/progress/ProgressTestingScreen.tsx
@@ -0,0 +1,347 @@
+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'
+
+export default function ProgressTestingScreen() {
+ const router = useRouter()
+ const colorScheme = useColorScheme()
+
+ // State for interactivity
+ const [type, setType] = useState<'linear' | 'circular'>('linear')
+ const [mode, setMode] = useState<'determinate' | 'timer' | 'indeterminate'>('determinate')
+ const [progressValue, setProgressValue] = useState(65)
+ const [durationSec, setDurationSec] = useState('60')
+ const [timerState, setTimerState] = useState<{ startAtMs?: number; endAtMs?: number }>({
+ startAtMs: Date.now(),
+ endAtMs: Date.now() + 60000,
+ })
+
+ // Styling state
+ const [trackColor, setTrackColor] = useState('#333344')
+ const [progressColor, setProgressColor] = useState('#007AFF')
+ const [cornerRadius, setCornerRadius] = useState(4)
+ const [height, setHeight] = useState(8)
+ const [lineWidth, setLineWidth] = useState(6)
+ const [useThumb, setUseThumb] = useState(false)
+ const [countDown, setCountDown] = useState(false)
+
+ const resetTimer = () => {
+ const duration = (parseInt(durationSec) || 0) * 1000
+ const now = Date.now()
+ setTimerState({
+ startAtMs: now,
+ endAtMs: now + duration,
+ })
+ }
+
+ const widgetPreviewStyle = {
+ borderRadius: 16,
+ backgroundColor: colorScheme === 'light' ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.8)',
+ }
+
+ // Effect to handle unsupported modes
+ React.useEffect(() => {
+ if (type === 'circular' && mode === 'timer') {
+ setMode('determinate')
+ }
+ if (type === 'linear' && mode === 'indeterminate') {
+ setMode('determinate')
+ }
+ }, [type, mode])
+
+ const renderProgressWidget = () => {
+ const commonProps: any = {
+ label: (
+
+ {type === 'linear' ? 'Linear' : 'Circular'} Progress
+
+ ),
+ currentValueLabel:
+ mode === 'determinate' ? (
+ {progressValue}%
+ ) : mode === 'timer' ? (
+
+ ) : null,
+ trackColor,
+ progressColor,
+ }
+
+ const modeProps =
+ mode === 'determinate'
+ ? { value: progressValue, maximumValue: 100 }
+ : mode === 'timer'
+ ? { startAtMs: timerState.startAtMs, endAtMs: timerState.endAtMs, countDown }
+ : {}
+
+ return (
+
+
+ {type === 'linear' ? (
+ : undefined}
+ />
+ ) : (
+
+ )}
+
+
+ )
+ }
+
+ return (
+
+
+ Progress Testing
+
+ Test VoltraLinearProgressView and VoltraCircularProgressView with new label and styling support.
+
+
+ {/* 1. Live Preview */}
+
+ Live Preview
+
+
+ {renderProgressWidget()}
+
+
+ {mode === 'timer' && (
+
+ )}
+
+
+ {/* 2. Configuration */}
+
+ Base Configuration
+
+
+ Type
+
+
+
+
+
+ Mode
+
+ setMode('determinate')}
+ style={styles.smButton}
+ />
+ setMode('timer')}
+ style={styles.smButton}
+ />
+ setMode('indeterminate')}
+ style={styles.smButton}
+ />
+
+
+
+ {mode === 'determinate' && (
+
+ Progress: {progressValue}%
+
+ setProgressValue(Math.max(0, progressValue - 10))}
+ style={styles.smButton}
+ />
+ setProgressValue(Math.min(100, progressValue + 10))}
+ style={styles.smButton}
+ />
+
+
+ )}
+
+ {mode === 'timer' && (
+ <>
+
+ Duration (seconds)
+
+
+
+ Count Down
+ setCountDown(!countDown)}
+ style={styles.smButton}
+ />
+
+ Note: Custom styling is ignored for Timers to support realtime updates.
+ >
+ )}
+
+
+ {/* 3. Styling Configuration */}
+
+ Styling Configuration
+
+
+ Track Color
+
+
+
+
+ Progress Color
+
+
+
+ {type === 'linear' ? (
+ <>
+
+ Height: {height}
+
+ setHeight(4)}
+ style={styles.smButton}
+ />
+ setHeight(8)}
+ style={styles.smButton}
+ />
+ setHeight(16)}
+ style={styles.smButton}
+ />
+
+
+
+ Corner Radius: {cornerRadius}
+
+ setCornerRadius(0)}
+ style={styles.smButton}
+ />
+ setCornerRadius(4)}
+ style={styles.smButton}
+ />
+ setCornerRadius(20)}
+ style={styles.smButton}
+ />
+
+
+
+ Custom Thumb
+ setUseThumb(!useThumb)}
+ style={styles.smButton}
+ />
+
+ >
+ ) : (
+
+ Line Width: {lineWidth}
+
+ setLineWidth(2)}
+ style={styles.smButton}
+ />
+ setLineWidth(6)}
+ style={styles.smButton}
+ />
+ setLineWidth(12)}
+ style={styles.smButton}
+ />
+
+
+ )}
+
+
+
+ router.back()} />
+
+
+
+ )
+}
+
+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: { flexDirection: 'row', justifyContent: 'center', flexWrap: 'wrap', gap: 12, padding: 10 },
+ row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 },
+ label: { color: '#fff', fontSize: 16 },
+ toggleGroup: { flexDirection: 'row', gap: 8 },
+ smButton: { paddingVertical: 6, paddingHorizontal: 12 },
+ footer: { marginTop: 24, marginBottom: 40, alignItems: 'center' },
+ input: {
+ backgroundColor: 'rgba(255,255,255,0.1)',
+ color: '#fff',
+ padding: 8,
+ borderRadius: 8,
+ minWidth: 100,
+ textAlign: 'right',
+ },
+ info: { color: '#FFB800', fontSize: 12, marginTop: -8, marginBottom: 16 },
+})
diff --git a/generator/generators/kotlin-parameters.ts b/generator/generators/kotlin-parameters.ts
index f37c037..0f4d756 100644
--- a/generator/generators/kotlin-parameters.ts
+++ b/generator/generators/kotlin-parameters.ts
@@ -29,7 +29,9 @@ const toKotlinType = (param: ComponentParameter): string => {
}
const generateParameterClass = (component: ComponentDefinition, version: string): string => {
- const params = Object.entries(component.parameters)
+ const allParams = Object.entries(component.parameters)
+ // Filter out component type params as they are handled separately
+ const params = allParams.filter(([_, param]) => param.type !== 'component')
const header = `//
// ${component.name}Parameters.kt
diff --git a/generator/generators/swift-parameters.ts b/generator/generators/swift-parameters.ts
index b346189..c2349c0 100644
--- a/generator/generators/swift-parameters.ts
+++ b/generator/generators/swift-parameters.ts
@@ -31,7 +31,9 @@ const toSwiftType = (param: ComponentParameter): string => {
}
const generateParameterStruct = (component: ComponentDefinition, version: string): string => {
- const params = Object.entries(component.parameters)
+ const allParams = Object.entries(component.parameters)
+ // Filter out component type params as they are handled via element.componentProp()
+ const params = allParams.filter(([_, param]) => param.type !== 'component')
const paramsWithDefaults = params.filter(([_, param]) => param.default !== undefined)
const header = `//
diff --git a/ios/ui/Generated/Parameters/GaugeParameters.swift b/ios/ui/Generated/Parameters/GaugeParameters.swift
index 9641a37..4ef9234 100644
--- a/ios/ui/Generated/Parameters/GaugeParameters.swift
+++ b/ios/ui/Generated/Parameters/GaugeParameters.swift
@@ -31,15 +31,6 @@ public struct GaugeParameters: ComponentParameters {
/// Visual style of the gauge
public let gaugeStyle: String?
- /// Custom text for current value label
- public let currentValueLabel: String?
-
- /// Text for minimum value label
- public let minimumValueLabel: String?
-
- /// Text for maximum value label
- public let maximumValueLabel: String?
-
enum CodingKeys: String, CodingKey {
case value
case minimumValue
@@ -48,9 +39,6 @@ public struct GaugeParameters: ComponentParameters {
case startAtMs
case tintColor
case gaugeStyle
- case currentValueLabel
- case minimumValueLabel
- case maximumValueLabel
}
public init(from decoder: Decoder) throws {
@@ -62,8 +50,5 @@ public struct GaugeParameters: ComponentParameters {
startAtMs = try container.decodeIfPresent(Double.self, forKey: .startAtMs)
tintColor = try container.decodeIfPresent(String.self, forKey: .tintColor)
gaugeStyle = try container.decodeIfPresent(String.self, forKey: .gaugeStyle)
- currentValueLabel = try container.decodeIfPresent(String.self, forKey: .currentValueLabel)
- minimumValueLabel = try container.decodeIfPresent(String.self, forKey: .minimumValueLabel)
- maximumValueLabel = try container.decodeIfPresent(String.self, forKey: .maximumValueLabel)
}
}
diff --git a/ios/ui/Generated/Parameters/GroupBoxParameters.swift b/ios/ui/Generated/Parameters/GroupBoxParameters.swift
index ec63a89..e7416f7 100644
--- a/ios/ui/Generated/Parameters/GroupBoxParameters.swift
+++ b/ios/ui/Generated/Parameters/GroupBoxParameters.swift
@@ -9,7 +9,4 @@ import Foundation
/// Parameters for GroupBox component
/// Grouped content container
-public struct GroupBoxParameters: ComponentParameters {
- /// Label content for the group box
- public let label: String?
-}
+public struct GroupBoxParameters: ComponentParameters {}
diff --git a/ios/ui/Generated/Parameters/LinearProgressViewParameters.swift b/ios/ui/Generated/Parameters/LinearProgressViewParameters.swift
index 3bbe16c..9dd7692 100644
--- a/ios/ui/Generated/Parameters/LinearProgressViewParameters.swift
+++ b/ios/ui/Generated/Parameters/LinearProgressViewParameters.swift
@@ -37,9 +37,6 @@ public struct LinearProgressViewParameters: ComponentParameters {
/// Explicit height for the progress bar
public let height: Double?
- /// Custom thumb component to display at progress position
- public let thumb: String?
-
enum CodingKeys: String, CodingKey {
case value
case countDown
@@ -50,7 +47,6 @@ public struct LinearProgressViewParameters: ComponentParameters {
case progressColor
case cornerRadius
case height
- case thumb
}
public init(from decoder: Decoder) throws {
@@ -64,6 +60,5 @@ public struct LinearProgressViewParameters: ComponentParameters {
progressColor = try container.decodeIfPresent(String.self, forKey: .progressColor)
cornerRadius = try container.decodeIfPresent(Double.self, forKey: .cornerRadius)
height = try container.decodeIfPresent(Double.self, forKey: .height)
- thumb = try container.decodeIfPresent(String.self, forKey: .thumb)
}
}
diff --git a/ios/ui/Generated/Parameters/MaskParameters.swift b/ios/ui/Generated/Parameters/MaskParameters.swift
index 5bcd0b2..b1b27c0 100644
--- a/ios/ui/Generated/Parameters/MaskParameters.swift
+++ b/ios/ui/Generated/Parameters/MaskParameters.swift
@@ -9,7 +9,4 @@ import Foundation
/// Parameters for Mask component
/// Mask content using any Voltra element as the mask shape
-public struct MaskParameters: ComponentParameters {
- /// Voltra element used as the mask - alpha channel determines visibility
- public let maskElement: String?
-}
+public struct MaskParameters: ComponentParameters {}
diff --git a/ios/ui/Views/VoltraCircularProgressView.swift b/ios/ui/Views/VoltraCircularProgressView.swift
index 549f52e..fbc6404 100644
--- a/ios/ui/Views/VoltraCircularProgressView.swift
+++ b/ios/ui/Views/VoltraCircularProgressView.swift
@@ -11,52 +11,57 @@ public struct VoltraCircularProgressView: VoltraView {
@ViewBuilder
public var body: some View {
- let endAtMs = params.endAtMs
- let startAtMs = params.startAtMs
+ let p = params
+ let progressColor = p.progressColor.flatMap { JSColorParser.parse($0) }
+ let trackColor = p.trackColor.flatMap { JSColorParser.parse($0) }
+ let lineWidth = p.lineWidth.map { CGFloat($0) }
- // Extract colors and styling from direct props
- let trackColor = params.trackColor.flatMap { JSColorParser.parse($0) }
- let progressColor = params.progressColor.flatMap { JSColorParser.parse($0) }
- let lineWidth = params.lineWidth.map { CGFloat($0) }
+ // Circular variant does not support timer-based progress ring via built-in ProgressView on iOS.
+ // We remove the timer option as it would only display an indeterminate spinner.
+ let isDeterminate = p.value != nil
- // Determine if we need custom style
- let needsCustomStyle = trackColor != nil || lineWidth != nil || params.value != nil
+ // Custom style provides the "ring" appearance for determinate progress.
+ let hasCustomProps = trackColor != nil || lineWidth != nil
+ let useCustomStyle = hasCustomProps && isDeterminate
- // Group containing the ProgressView variations
- let progressContent = Group {
- if let endAtMs = endAtMs {
- // Timer-based progress
- let timeRange = Date.toTimerInterval(startAtMs: startAtMs, endAtMs: endAtMs)
-
- ProgressView(timerInterval: timeRange)
- } else if let value = params.value {
- // Determinate progress
- ProgressView(
- value: value,
- total: params.maximumValue ?? 100
- )
- } else {
- // Indeterminate progress (only supported for circular)
- ProgressView()
+ if let value = p.value {
+ // Determinate progress
+ ProgressView(value: value, total: p.maximumValue) {
+ element.componentProp("label")
+ } currentValueLabel: {
+ element.componentProp("currentValueLabel")
}
- }
-
- // Apply the style conditionally
- if needsCustomStyle {
- let customStyle = VoltraCircularProgressStyle(
- progressTint: progressColor,
- trackTint: trackColor ?? Color.gray.opacity(0.2),
- lineWidth: lineWidth
- )
-
- progressContent
- .progressViewStyle(customStyle)
- .applyStyle(element.style)
+ .tint(progressColor)
+ .voltraIf(useCustomStyle) {
+ $0.progressViewStyle(VoltraCircularProgressStyle(
+ progressTint: progressColor,
+ trackTint: trackColor ?? Color.gray.opacity(0.2),
+ lineWidth: lineWidth,
+ staticFraction: nil
+ ))
+ }
+ .voltraIf(!useCustomStyle) {
+ // Fallback to custom style for determinate even if no custom props,
+ // because built-in .circular is a spinner, not a ring.
+ $0.progressViewStyle(VoltraCircularProgressStyle(
+ progressTint: progressColor,
+ trackTint: Color.gray.opacity(0.2),
+ lineWidth: 4,
+ staticFraction: nil
+ ))
+ }
+ .applyStyle(element.style)
} else {
- progressContent
+ // Indeterminate progress (Built-in spinner)
+ // Wrapping in VStack to ensure stable geometry for the animation
+ VStack(spacing: 0) {
+ ProgressView {
+ element.componentProp("label")
+ }
.progressViewStyle(.circular)
.tint(progressColor)
- .applyStyle(element.style)
+ }
+ .applyStyle(element.style)
}
}
}
@@ -66,16 +71,17 @@ private struct VoltraCircularProgressStyle: ProgressViewStyle {
var progressTint: Color?
var trackTint: Color
var lineWidth: CGFloat?
+ var staticFraction: Double?
func makeBody(configuration: Configuration) -> some View {
- let fraction = max(0, min(configuration.fractionCompleted ?? 0, 1))
+ let fraction = max(0, min(staticFraction ?? configuration.fractionCompleted ?? 0, 1))
let strokeWidth = lineWidth ?? 4
- return GeometryReader { geometry in
- let size = min(geometry.size.width, geometry.size.height)
+ VStack(spacing: 8) {
+ configuration.label
ZStack {
- // Track (background circle)
+ // Track
Circle()
.stroke(trackTint, lineWidth: strokeWidth)
@@ -89,7 +95,9 @@ private struct VoltraCircularProgressStyle: ProgressViewStyle {
.rotationEffect(.degrees(-90))
.animation(.linear, value: fraction)
}
- .frame(width: size, height: size)
+ .aspectRatio(1, contentMode: .fit)
+
+ configuration.currentValueLabel
}
}
}
diff --git a/ios/ui/Views/VoltraLinearProgressView.swift b/ios/ui/Views/VoltraLinearProgressView.swift
index 35168f6..7bcffe3 100644
--- a/ios/ui/Views/VoltraLinearProgressView.swift
+++ b/ios/ui/Views/VoltraLinearProgressView.swift
@@ -5,67 +5,70 @@ public struct VoltraLinearProgressView: VoltraView {
public let element: VoltraElement
+ @State private var internalStartTime = Date()
+
public init(_ element: VoltraElement) {
self.element = element
}
@ViewBuilder
public var body: some View {
- let endAtMs = params.endAtMs
- let startAtMs = params.startAtMs
-
- // Extract colors and styling from direct props
- let trackColor = params.trackColor.flatMap { JSColorParser.parse($0) }
- let progressColor = params.progressColor.flatMap { JSColorParser.parse($0) }
- let cornerRadius = params.cornerRadius.map { CGFloat($0) }
- let height = params.height.map { CGFloat($0) }
-
- // Get thumb component prop
+ let p = params
+ let progressColor = p.progressColor.flatMap { JSColorParser.parse($0) }
+ let trackColor = p.trackColor.flatMap { JSColorParser.parse($0) }
+ let cornerRadius = p.cornerRadius.map { CGFloat($0) }
+ let height = p.height.map { CGFloat($0) }
let thumbComponent = element.componentProp("thumb")
- // Determine if we need custom style
- let needsCustomStyle = trackColor != nil || cornerRadius != nil || height != nil || thumbComponent != nil
-
- // Define the custom style builder (if needed)
- let customStyle = VoltraLinearProgressStyle(
- progressTint: progressColor,
- trackTint: trackColor ?? Color.gray.opacity(0.2),
- cornerRadius: cornerRadius,
- explicitHeight: height,
- thumbComponent: thumbComponent
- )
-
- // Group containing the ProgressView variations
- let progressContent = Group {
- if let endAtMs = endAtMs {
- // Timer-based progress
- let timeRange = Date.toTimerInterval(startAtMs: startAtMs, endAtMs: endAtMs)
-
- ProgressView(timerInterval: timeRange)
- .tint(progressColor)
- } else if let value = params.value {
- // Determinate progress
- ProgressView(
- value: value,
- total: params.maximumValue ?? 100
- )
- .tint(progressColor)
- } else {
- // Indeterminate progress
- ProgressView()
- .tint(progressColor)
+ let isTimer = p.endAtMs != nil
+
+ // Linear progress bar only supports Determinate and Timer modes.
+ // If no value or timer is provided, we fall back to a 0% determinate state
+ // as linear indeterminate progress is not supported.
+ let isDeterminate = p.value != nil
+ let effectiveValue = p.value ?? 0.0
+
+ // Built-in style is required for timers to animate in realtime (Live Activities).
+ let hasCustomProps = trackColor != nil || cornerRadius != nil || height != nil || thumbComponent != .empty
+ let useCustomStyle = hasCustomProps && isDeterminate
+
+ if isTimer, let endAtMs = p.endAtMs {
+ // Timer-based progress (Always uses built-in style for realtime updates)
+ ProgressView(
+ timerInterval: Date.toTimerInterval(
+ startAtMs: p.startAtMs ?? (internalStartTime.timeIntervalSince1970 * 1000),
+ endAtMs: endAtMs
+ ),
+ countsDown: p.countDown ?? false
+ ) {
+ element.componentProp("label")
+ } currentValueLabel: {
+ element.componentProp("currentValueLabel")
}
- }
-
- // Apply the style conditionally
- if needsCustomStyle {
- progressContent
- .progressViewStyle(customStyle)
- .applyStyle(element.style)
+ .progressViewStyle(.linear)
+ .tint(progressColor)
+ .applyStyle(element.style)
} else {
- progressContent
- .progressViewStyle(LinearProgressViewStyle())
- .applyStyle(element.style)
+ // Determinate progress (Default fallback if no value/timer)
+ ProgressView(value: effectiveValue, total: p.maximumValue) {
+ element.componentProp("label")
+ } currentValueLabel: {
+ element.componentProp("currentValueLabel")
+ }
+ .tint(progressColor)
+ .voltraIf(useCustomStyle) {
+ $0.progressViewStyle(VoltraLinearProgressStyle(
+ progressTint: progressColor,
+ trackTint: trackColor ?? Color.gray.opacity(0.2),
+ cornerRadius: cornerRadius,
+ explicitHeight: height,
+ thumbComponent: thumbComponent != .empty ? thumbComponent : nil
+ ))
+ }
+ .voltraIf(!useCustomStyle) {
+ $0.progressViewStyle(.linear)
+ }
+ .applyStyle(element.style)
}
}
}
@@ -80,56 +83,36 @@ private struct VoltraLinearProgressStyle: ProgressViewStyle {
func makeBody(configuration: Configuration) -> some View {
let fraction = max(0, min(configuration.fractionCompleted ?? 0, 1))
+ let baseHeight = explicitHeight ?? 4
- VoltraLinearProgressBar(
- fraction: fraction,
- progressTint: progressTint,
- trackTint: trackTint,
- cornerRadius: cornerRadius,
- explicitHeight: explicitHeight,
- thumbComponent: thumbComponent
- )
- }
-}
+ VStack(alignment: .leading, spacing: 4) {
+ configuration.label
-/// Shared progress bar view used by VoltraLinearProgressStyle
-@available(iOS 16.0, macOS 13.0, *)
-private struct VoltraLinearProgressBar: View {
- let fraction: Double
- var progressTint: Color?
- var trackTint: Color
- var cornerRadius: CGFloat?
- var explicitHeight: CGFloat?
- var thumbComponent: VoltraNode?
+ GeometryReader { geometry in
+ let totalWidth = geometry.size.width
+ let radius = cornerRadius ?? baseHeight / 2
- var body: some View {
- let baseHeight = explicitHeight ?? 4
+ ZStack(alignment: .leading) {
+ // Track
+ RoundedRectangle(cornerRadius: radius, style: .continuous)
+ .fill(trackTint)
+ .frame(width: totalWidth, height: baseHeight)
- GeometryReader { geometry in
- let totalWidth = geometry.size.width
- let radius = cornerRadius ?? baseHeight / 2
-
- // Progress bar
- ZStack(alignment: .leading) {
- // Track (background)
- RoundedRectangle(cornerRadius: radius, style: .continuous)
- .fill(trackTint)
- .frame(width: totalWidth, height: baseHeight)
-
- // Progress fill
- RoundedRectangle(cornerRadius: radius, style: .continuous)
- .fill(progressTint ?? Color.accentColor)
- .frame(width: totalWidth * CGFloat(fraction), height: baseHeight)
- }
- .overlay(alignment: .leading) {
- // Render thumb component at progress position
- if let thumbComponent = thumbComponent {
- let offsetX = (totalWidth * CGFloat(fraction))
- thumbComponent
- .offset(x: offsetX)
+ // Progress fill
+ RoundedRectangle(cornerRadius: radius, style: .continuous)
+ .fill(progressTint ?? Color.accentColor)
+ .frame(width: totalWidth * CGFloat(fraction), height: baseHeight)
+ }
+ .overlay(alignment: .leading) {
+ if let thumbComponent = thumbComponent {
+ thumbComponent
+ .offset(x: totalWidth * CGFloat(fraction))
+ }
}
}
- .frame(width: totalWidth, height: baseHeight)
+ .frame(height: baseHeight)
+
+ configuration.currentValueLabel
}
}
}
diff --git a/package.json b/package.json
index 9d40387..53230c2 100644
--- a/package.json
+++ b/package.json
@@ -36,8 +36,10 @@
"build": "expo-module build",
"clean": "expo-module clean",
"clean:plugin": "rm -rf plugin/build plugin/tsconfig.tsbuildinfo",
- "format:check": "prettier --check .",
- "format:fix": "prettier --write .",
+ "format:check": "npm run format:js:check && npm run format:kotlin:check && npm run format:swift:check",
+ "format:fix": "npm run format:js:fix && npm run format:kotlin:fix && npm run format:swift:fix",
+ "format:js:check": "prettier --check .",
+ "format:js:fix": "prettier --write .",
"format:kotlin:check": "ktlint \"android/**/*.kt\"",
"format:kotlin:fix": "ktlint -F \"android/**/*.kt\"",
"format:swift:check": "swiftformat --lint ios",
diff --git a/src/jsx/props/CircularProgressView.ts b/src/jsx/props/CircularProgressView.ts
index 8a8e3e4..e7fd940 100644
--- a/src/jsx/props/CircularProgressView.ts
+++ b/src/jsx/props/CircularProgressView.ts
@@ -2,6 +2,8 @@
// DO NOT EDIT MANUALLY - Changes will be overwritten
// Schema version: 1.0.0
+import type { ReactNode } from 'react'
+
import type { VoltraBaseProps } from '../baseProps'
export type CircularProgressViewProps = VoltraBaseProps & {
@@ -21,4 +23,8 @@ export type CircularProgressViewProps = VoltraBaseProps & {
progressColor?: string
/** Width of the stroke line */
lineWidth?: number
+ /** Label content for the progress indicator */
+ label?: ReactNode
+ /** Custom text for current value label */
+ currentValueLabel?: ReactNode
}
diff --git a/src/jsx/props/LinearProgressView.ts b/src/jsx/props/LinearProgressView.ts
index 758a692..b5f3ba4 100644
--- a/src/jsx/props/LinearProgressView.ts
+++ b/src/jsx/props/LinearProgressView.ts
@@ -27,4 +27,8 @@ export type LinearProgressViewProps = VoltraBaseProps & {
height?: number
/** Custom thumb component to display at progress position */
thumb?: ReactNode
+ /** Label content for the progress indicator */
+ label?: ReactNode
+ /** Custom text for current value label */
+ currentValueLabel?: ReactNode
}
diff --git a/website/docs/ios/components/status.md b/website/docs/ios/components/status.md
index 3eb64a7..0ca5d89 100644
--- a/website/docs/ios/components/status.md
+++ b/website/docs/ios/components/status.md
@@ -6,12 +6,20 @@ Components specifically designed to show dynamic values or states over time in L
A horizontal progress bar that displays determinate progress or timer-based progress.
+#### Limitations
+
+- When using `timerInterval` for smooth animations, custom styling properties such as `height`, `trackColor`, `cornerRadius`, and the `thumb` component are ignored. The component will use the default system appearance in this mode.
+
---
### CircularProgressView
A circular progress indicator that displays determinate progress or timer-based progress.
+#### Limitations
+
+- While `timerInterval` is supported, the progress ring will not animate continuously. It only updates its visual state when the component state is refreshed. For a smooth, live-animating progress bar, use `LinearProgressView`.
+
---
### Gauge
diff --git a/website/docs/ios/development/developing-live-activities.md b/website/docs/ios/development/developing-live-activities.md
index e94b5e7..c5f4091 100644
--- a/website/docs/ios/development/developing-live-activities.md
+++ b/website/docs/ios/development/developing-live-activities.md
@@ -74,6 +74,20 @@ const variants = {
If `supplementalActivityFamilies.small` is not provided, Voltra will automatically construct it from your Dynamic Island `compact` variant by combining the leading and trailing content in an HStack.
+## Limitations
+
+### Animations and Live Updates
+
+There are specific constraints on how content can animate or update:
+
+- **Continuous Animations**: Custom continuous animations, such as rotating icons or elements moving along a path, are not supported.
+- **Smooth Updates**: Per-second "live" updates are only supported by specific components designed for this purpose:
+ - `Timer`: For countdowns and stopwatches.
+ - `LinearProgressView`: When used with `timerInterval`.
+- **Styling Trade-offs**: To enable smooth, system-driven animations (like a progress bar filling up in real-time), certain components may ignore custom styling properties (e.g., custom heights or thumb components) and fallback to standard system appearances.
+
+All other components only update their visual state when a new activity state is pushed from your application.
+
## useLiveActivity
For React development, Voltra provides the `useLiveActivity` hook for integration with the component lifecycle and automatic updates during development.
diff --git a/website/docs/ios/development/styling.md b/website/docs/ios/development/styling.md
index 997266a..215619c 100644
--- a/website/docs/ios/development/styling.md
+++ b/website/docs/ios/development/styling.md
@@ -68,6 +68,7 @@ Properties not listed above are ignored during rendering. This includes common R
- Percentage-based widths and heights
- `right` and `bottom` positioning properties - Only `left` and `top` are supported
- Most text styling properties beyond `fontSize`, `fontWeight`, `fontFamily`, `color`, `letterSpacing`, and `fontVariant`
+- **Live Update Overrides**: Certain styling properties (like `height` or `borderRadius` on progress bars) may be ignored when using live-updating features like `timerInterval` to ensure compatibility with smooth system animations.
:::tip Positioning in Voltra