Skip to content
Open
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
5 changes: 5 additions & 0 deletions example/app/testing-grounds/gradient-playground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import GradientPlaygroundScreen from '~/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen'

export default function GradientPlaygroundIndex() {
return <GradientPlaygroundScreen />
}
7 changes: 7 additions & 0 deletions example/screens/testing-grounds/TestingGroundsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ const TESTING_GROUNDS_SECTIONS = [
'Interactive playground for experimenting with flex layout properties. Test alignItems, justifyContent, flexDirection, spacing, and padding with live visual feedback.',
route: '/testing-grounds/flex-playground',
},
{
id: 'gradient-playground',
title: 'Gradient Playground',
description:
'Test CSS gradient strings as backgroundColor. Experiment with linear, radial, and conic gradients, direction/angle controls, color presets, stop positions, and borderRadius clipping.',
route: '/testing-grounds/gradient-playground',
},
{
id: 'image-preloading',
title: 'Image Preloading',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import { Link } from 'expo-router'
import React, { useState } from 'react'
import { ScrollView, StyleSheet, Text, View } from 'react-native'
import { Voltra } from 'voltra'
import { VoltraView } from 'voltra/client'

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

type GradientType = 'linear' | 'radial' | 'conic'
type Direction = 'to right' | 'to bottom' | 'to bottom right' | 'to top right'

const GRADIENT_TYPES: GradientType[] = ['linear', 'radial', 'conic']
const DIRECTIONS: Direction[] = ['to right', 'to bottom', 'to bottom right', 'to top right']
const DIRECTION_LABELS: Record<Direction, string> = {
'to right': 'to right',
'to bottom': 'to bottom',
'to bottom right': 'to bottom right',
'to top right': 'to top right',
}

const PRESETS: Array<{ label: string; colors: [string, string, ...string[]] }> = [
{ label: 'Sunset', colors: ['#FF6B6B', '#FFD93D'] },
{ label: 'Ocean', colors: ['#0093E9', '#80D0C7'] },
{ label: 'Purple', colors: ['#8B5CF6', '#EC4899'] },
{ label: 'Tri-color', colors: ['#EF4444', '#10B981', '#3B82F6'] },
]

const ANGLE_OPTIONS = [0, 45, 90, 135, 180]

export default function GradientPlaygroundScreen() {
const [gradientType, setGradientType] = useState<GradientType>('linear')
const [direction, setDirection] = useState<Direction>('to right')
const [angle, setAngle] = useState(90)
const [useAngle, setUseAngle] = useState(false)
const [preset, setPreset] = useState(0)
const [borderRadius, setBorderRadius] = useState(12)

const colors = PRESETS[preset].colors
const positionedStops = colors
.map((color, idx) => {
const pct = colors.length === 1 ? 0 : Math.round((idx / (colors.length - 1)) * 100)
return `${color} ${pct}%`
})
.join(', ')

const buildGradient = (): string => {
if (gradientType === 'radial') {
return `radial-gradient(circle at center, ${positionedStops})`
}
if (gradientType === 'conic') {
return `conic-gradient(from ${angle}deg at center, ${positionedStops})`
}
// linear
const dir = useAngle ? `${angle}deg` : direction
return `linear-gradient(${dir}, ${positionedStops})`
}

const gradient = buildGradient()

const cycleGradientType = () => {
const i = GRADIENT_TYPES.indexOf(gradientType)
setGradientType(GRADIENT_TYPES[(i + 1) % GRADIENT_TYPES.length])
}

const cycleDirection = () => {
const i = DIRECTIONS.indexOf(direction)
setDirection(DIRECTIONS[(i + 1) % DIRECTIONS.length])
}

const cycleAngle = () => {
const i = ANGLE_OPTIONS.indexOf(angle)
setAngle(ANGLE_OPTIONS[(i + 1) % ANGLE_OPTIONS.length])
}

const cyclePreset = () => {
setPreset((prev) => (prev + 1) % PRESETS.length)
}

const increaseBorderRadius = () => setBorderRadius((prev) => Math.min(prev + 8, 80))
const decreaseBorderRadius = () => setBorderRadius((prev) => Math.max(prev - 8, 0))

return (
<View style={styles.container}>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.content}>
<Text style={styles.heading}>Gradient Playground</Text>
<Text style={styles.subheading}>Test CSS gradient strings as backgroundColor on Voltra views.</Text>
<Text style={styles.warningText}>
Playground uses parser-compatible CSS syntax only. If a preview is blank, this indicates a gradient parser bug
in iOS rendering.
</Text>

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

{/* Gradient Type */}
<View style={styles.controlRow}>
<Text style={styles.controlLabel}>Type:</Text>
<Button title={gradientType} onPress={cycleGradientType} variant="secondary" />
</View>

{/* Direction (linear only) */}
{gradientType === 'linear' && (
<>
<View style={styles.controlRow}>
<Text style={styles.controlLabel}>Mode:</Text>
<Button
title={useAngle ? 'angle' : 'direction'}
onPress={() => setUseAngle((v) => !v)}
variant="secondary"
/>
</View>
{useAngle ? (
<View style={styles.controlRow}>
<Text style={styles.controlLabel}>Angle: {angle}deg</Text>
<Button title="cycle" onPress={cycleAngle} variant="secondary" />
</View>
) : (
<View style={styles.controlRow}>
<Text style={styles.controlLabel}>Direction:</Text>
<Button title={DIRECTION_LABELS[direction]} onPress={cycleDirection} variant="secondary" />
</View>
)}
</>
)}

{/* Angle for conic */}
{gradientType === 'conic' && (
<View style={styles.controlRow}>
<Text style={styles.controlLabel}>Start angle: {angle}deg</Text>
<Button title="cycle" onPress={cycleAngle} variant="secondary" />
</View>
)}

{/* Color Preset */}
<View style={styles.controlRow}>
<Text style={styles.controlLabel}>Colors: {PRESETS[preset].label}</Text>
<Button title="cycle" onPress={cyclePreset} variant="secondary" />
</View>

{/* Border Radius */}
<View style={styles.controlRow}>
<Text style={styles.controlLabel}>borderRadius: {borderRadius}px</Text>
<View style={styles.buttonGroup}>
<Button title="-" onPress={decreaseBorderRadius} variant="secondary" />
<Button title="+" onPress={increaseBorderRadius} variant="secondary" />
</View>
</View>
</Card>

{/* Live Preview */}
<Card>
<Card.Title>Live Preview</Card.Title>

<VoltraView style={{ width: '100%', height: 220, backgroundColor: '#0F172A', padding: 16, marginTop: 12 }}>
<Voltra.View
style={{
height: '100%',
backgroundColor: gradient,
borderRadius,
width: '100%',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Voltra.Text style={{ color: '#FFFFFF', fontSize: 16, fontWeight: 'bold' }}>Gradient View</Voltra.Text>
<Voltra.Text style={{ color: 'rgba(255,255,255,0.75)', fontSize: 11 }}>{gradient}</Voltra.Text>
</Voltra.View>
</VoltraView>
</Card>

{/* Explicit stop positions */}
<Card>
<Card.Title>Color Stop Positions</Card.Title>
<Text style={styles.previewSubtext}>Explicit percentage stops: red 10%, yellow 50%, blue 90%</Text>

<VoltraView style={{ width: '100%', height: 80, backgroundColor: '#0F172A', padding: 16, marginTop: 12 }}>
<Voltra.View
style={{
height: '100%',
backgroundColor: 'linear-gradient(to right, red 10%, yellow 50%, blue 90%)',
borderRadius: 8,
width: '100%',
}}
/>
</VoltraView>
</Card>

{/* rgba inside gradient */}
<Card>
<Card.Title>RGBA Inside Gradient</Card.Title>
<Text style={styles.previewSubtext}>linear-gradient(to right, rgba(255,0,0,0.8) 0%, rgba(0,0,255,0.3) 100%)</Text>

<VoltraView style={{ width: '100%', height: 80, backgroundColor: '#0F172A', padding: 16, marginTop: 12 }}>
<Voltra.View
style={{
height: '100%',
backgroundColor: 'linear-gradient(to right, rgba(255,0,0,0.8) 0%, rgba(0,0,255,0.3) 100%)',
borderRadius: 8,
width: '100%',
}}
/>
</VoltraView>
</Card>

{/* Solid color still works */}
<Card>
<Card.Title>Solid Color (Unchanged)</Card.Title>
<Text style={styles.previewSubtext}>backgroundColor: "#3B82F6" — plain colors still work</Text>

<VoltraView style={{ width: '100%', height: 80, backgroundColor: '#0F172A', padding: 16, marginTop: 12 }}>
<Voltra.View
style={{
height: '100%',
backgroundColor: '#3B82F6',
borderRadius: 8,
width: '100%',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Voltra.Text style={{ color: '#FFFFFF', fontSize: 12 }}>Solid #3B82F6</Voltra.Text>
</Voltra.View>
</VoltraView>
</Card>

<View style={styles.footer}>
<Link href="/testing-grounds" asChild>
<Button title="Back to Testing Grounds" variant="ghost" />
</Link>
</View>
</ScrollView>
</View>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollView: {
flex: 1,
},
content: {
paddingHorizontal: 20,
paddingVertical: 24,
},
heading: {
fontSize: 24,
fontWeight: '700',
color: '#FFFFFF',
marginBottom: 8,
},
subheading: {
fontSize: 14,
lineHeight: 20,
color: '#CBD5F5',
marginBottom: 8,
},
warningText: {
fontSize: 12,
lineHeight: 18,
color: '#FCA5A5',
marginBottom: 20,
},
controlRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
controlLabel: {
fontSize: 14,
color: '#CBD5F5',
flex: 1,
},
buttonGroup: {
flexDirection: 'row',
gap: 8,
},
previewSubtext: {
fontSize: 12,
color: '#94A3B8',
marginTop: 4,
},
footer: {
marginTop: 24,
alignItems: 'center',
},
})
19 changes: 19 additions & 0 deletions ios/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ let package = Package(
name: "VoltraSharedCore",
targets: ["VoltraSharedCore"]
),
.library(
name: "VoltraStyleCore",
targets: ["VoltraStyleCore"]
),
],
targets: [
.target(
Expand Down Expand Up @@ -45,5 +49,20 @@ let package = Package(
dependencies: ["VoltraSharedCore"],
path: "Tests/VoltraSharedTests"
),
.target(
name: "VoltraStyleCore",
path: "ui",
sources: [
"Style/BackgroundValue.swift",
"Style/JSColorParser.swift",
"Style/JSGradientParser.swift",
]
),
.testTarget(
name: "VoltraStyleTests",
dependencies: ["VoltraStyleCore"],
path: "tests",
sources: ["JSGradientParserTests.swift"]
),
]
)
Loading
Loading