From 2567c9fb4ebb0754344c4ca3617ed0426a66e757 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 13 Feb 2026 22:40:57 +0100 Subject: [PATCH 1/3] feat: support gradients for backgrounds --- .../testing-grounds/gradient-playground.tsx | 5 + .../testing-grounds/TestingGroundsScreen.tsx | 7 + .../GradientPlaygroundScreen.tsx | 275 ++++++++++++++++++ ios/ui/Style/DecorationStyle.swift | 22 +- ios/ui/Style/JSGradientParser.swift | 200 +++++++++++++ ios/ui/Style/JSStyleParser.swift | 12 + ios/ui/Style/StyleConverter.swift | 2 +- website/docs/ios/development/styling.md | 103 ++++++- 8 files changed, 621 insertions(+), 5 deletions(-) create mode 100644 example/app/testing-grounds/gradient-playground.tsx create mode 100644 example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx create mode 100644 ios/ui/Style/JSGradientParser.swift diff --git a/example/app/testing-grounds/gradient-playground.tsx b/example/app/testing-grounds/gradient-playground.tsx new file mode 100644 index 00000000..6800ab31 --- /dev/null +++ b/example/app/testing-grounds/gradient-playground.tsx @@ -0,0 +1,5 @@ +import GradientPlaygroundScreen from '~/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen' + +export default function GradientPlaygroundIndex() { + return +} diff --git a/example/screens/testing-grounds/TestingGroundsScreen.tsx b/example/screens/testing-grounds/TestingGroundsScreen.tsx index 9e9ecba7..4e4b3691 100644 --- a/example/screens/testing-grounds/TestingGroundsScreen.tsx +++ b/example/screens/testing-grounds/TestingGroundsScreen.tsx @@ -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', diff --git a/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx b/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx new file mode 100644 index 00000000..2c6bb5d1 --- /dev/null +++ b/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx @@ -0,0 +1,275 @@ +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 = { + '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('linear') + const [direction, setDirection] = useState('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 buildGradient = (): string => { + if (gradientType === 'radial') { + return `radial-gradient(${colors.join(', ')})` + } + if (gradientType === 'conic') { + return `conic-gradient(from ${angle}deg, ${colors.join(', ')})` + } + // linear + const dir = useAngle ? `${angle}deg` : direction + return `linear-gradient(${dir}, ${colors.join(', ')})` + } + + 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 ( + + + Gradient Playground + Test CSS gradient strings as backgroundColor on Voltra views. + + {/* Controls */} + + Controls + + {/* Gradient Type */} + + Type: +