diff --git a/example/screens/ios/tabs/sections.ts b/example/screens/ios/tabs/sections.ts index 9b3b8d1d..ad3506b6 100644 --- a/example/screens/ios/tabs/sections.ts +++ b/example/screens/ios/tabs/sections.ts @@ -92,7 +92,7 @@ export const IOS_OTHER_SECTIONS: ExampleSection[] = [ id: 'gradient-playground', title: 'Gradient Playground', description: - 'Test CSS gradient strings as backgroundColor. Experiment with linear, radial, and conic gradients, direction and angle controls, color presets, stop positions, and borderRadius clipping.', + 'Test CSS gradient strings as backgroundImage. Experiment with linear, radial, and conic gradients, composed backgrounds, stop positions, and borderRadius clipping.', route: '/testing-grounds/gradient-playground', }, { diff --git a/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx b/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx index 35bd5b1f..e7c031e8 100644 --- a/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx +++ b/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx @@ -85,7 +85,7 @@ export default function GradientPlaygroundScreen() { return ( Playground uses parser-compatible CSS syntax only. If a preview is blank, this indicates a gradient parser bug @@ -159,7 +159,7 @@ export default function GradientPlaygroundScreen() { + {/* backgroundColor + backgroundImage composition */} + + Composed Background + + backgroundColor: "#101828" with backgroundImage: "linear-gradient(... transparent ...)" + + + + + + Solid base + image layer + + + Transparent pixels reveal the backgroundColor + + + + + {/* Solid color still works */} Solid Color (Unchanged) diff --git a/packages/ios-client/ios/ui/Style/DecorationStyle.swift b/packages/ios-client/ios/ui/Style/DecorationStyle.swift index 6b06b8d1..6c14ada7 100644 --- a/packages/ios-client/ios/ui/Style/DecorationStyle.swift +++ b/packages/ios-client/ios/ui/Style/DecorationStyle.swift @@ -2,6 +2,7 @@ import SwiftUI struct DecorationStyle { var backgroundColor: BackgroundValue? + var backgroundImage: BackgroundValue? var cornerRadius: CGFloat? var border: (width: CGFloat, color: Color)? var shadow: (radius: CGFloat, color: Color, opacity: Double, offset: CGSize)? @@ -133,6 +134,14 @@ struct DecorationModifier: ViewModifier { return nil } + private var resolvedBackgroundImage: BackgroundValue? { + guard suppressesDecorativeContainerEffects, isFullBleedBackgroundCandidate else { + return style.backgroundImage + } + + return nil + } + private var resolvedGlassEffect: GlassEffect? { guard suppressesDecorativeContainerEffects else { return style.glassEffect @@ -143,18 +152,14 @@ struct DecorationModifier: ViewModifier { func body(content: Content) -> some View { content + .voltraIfLet(resolvedBackgroundImage) { content, bg in + content.background { + backgroundView(for: bg) + } + } .voltraIfLet(resolvedBackgroundColor) { content, bg in - switch bg { - case let .color(color): - content.background(color) - case let .linearGradient(gradient, start, end): - content.background(LinearGradient(gradient: gradient, startPoint: start, endPoint: end)) - case let .radialGradient(spec): - content.background { - radialGradientBackground(spec) - } - case let .angularGradient(gradient, center, angle): - content.background(AngularGradient(gradient: gradient, center: center, angle: angle)) + content.background { + backgroundView(for: bg) } } // If we have a corner radius, we must handle the border specifically here @@ -204,4 +209,18 @@ struct DecorationModifier: ViewModifier { } } } + + @ViewBuilder + private func backgroundView(for background: BackgroundValue) -> some View { + switch background { + case let .color(color): + color + case let .linearGradient(gradient, start, end): + LinearGradient(gradient: gradient, startPoint: start, endPoint: end) + case let .radialGradient(spec): + radialGradientBackground(spec) + case let .angularGradient(gradient, center, angle): + AngularGradient(gradient: gradient, center: center, angle: angle) + } + } } diff --git a/packages/ios-client/ios/ui/Style/StyleConverter.swift b/packages/ios-client/ios/ui/Style/StyleConverter.swift index cabacce6..e7698f30 100644 --- a/packages/ios-client/ios/ui/Style/StyleConverter.swift +++ b/packages/ios-client/ios/ui/Style/StyleConverter.swift @@ -135,6 +135,7 @@ enum StyleConverter { return DecorationStyle( backgroundColor: JSStyleParser.background(js["backgroundColor"]), + backgroundImage: JSStyleParser.background(js["backgroundImage"]), cornerRadius: JSStyleParser.number(js["borderRadius"]), border: border, shadow: shadow, diff --git a/packages/ios-client/ios/ui/Views/VoltraChart.swift b/packages/ios-client/ios/ui/Views/VoltraChart.swift index 95529d3b..6359f205 100644 --- a/packages/ios-client/ios/ui/Views/VoltraChart.swift +++ b/packages/ios-client/ios/ui/Views/VoltraChart.swift @@ -658,6 +658,7 @@ private extension View { let decorationForLayout: DecorationStyle = hasClipping ? DecorationStyle( backgroundColor: decoration.backgroundColor, + backgroundImage: decoration.backgroundImage, cornerRadius: nil, border: decoration.border, shadow: decoration.shadow, diff --git a/packages/ios-client/ios/ui/Views/VoltraLinearGradient.swift b/packages/ios-client/ios/ui/Views/VoltraLinearGradient.swift index 53edcdd4..458c8498 100644 --- a/packages/ios-client/ios/ui/Views/VoltraLinearGradient.swift +++ b/packages/ios-client/ios/ui/Views/VoltraLinearGradient.swift @@ -91,7 +91,7 @@ public struct VoltraLinearGradient: VoltraView { let (layout, baseDecoration, rendering, text) = StyleConverter.convert(anyStyle) var decoration = baseDecoration - decoration.backgroundColor = .linearGradient(gradient: gradient, startPoint: start, endPoint: end) + decoration.backgroundImage = .linearGradient(gradient: gradient, startPoint: start, endPoint: end) if let widget = voltraEnvironment.widget, widget.isHomeScreenWidget, diff --git a/packages/ios/src/styles/types.ts b/packages/ios/src/styles/types.ts index 2e0257b6..2cf8c4aa 100644 --- a/packages/ios/src/styles/types.ts +++ b/packages/ios/src/styles/types.ts @@ -44,6 +44,7 @@ export type VoltraViewStyle = { marginHorizontal?: number | string marginVertical?: number | string backgroundColor?: string + backgroundImage?: string opacity?: number borderRadius?: number | string borderWidth?: number diff --git a/website/docs/ios/development/styling.md b/website/docs/ios/development/styling.md index 473a3a26..d90f1ad0 100644 --- a/website/docs/ios/development/styling.md +++ b/website/docs/ios/development/styling.md @@ -32,6 +32,7 @@ The following React Native style properties are supported: **Style:** - `backgroundColor` - Background color (hex strings, color names, or CSS gradient strings — see [Gradients](#gradients)) +- `backgroundImage` - Gradient layer rendered above `backgroundColor` when both are present - `opacity` - Opacity value between 0 and 1 - `borderRadius` - Corner radius value - `borderWidth` - Border width @@ -135,7 +136,9 @@ const element = ( ## Gradients -The `backgroundColor` style property accepts CSS gradient strings in addition to solid colors. Gradients are rendered natively using SwiftUI gradient modifiers and are automatically clipped by `borderRadius`. +The `backgroundImage` style property is the preferred way to render CSS gradients. It is painted above `backgroundColor`, so you can combine a solid base color with a semi-transparent gradient overlay. + +For backward compatibility, `backgroundColor` still accepts CSS gradient strings in addition to solid colors. Gradients are rendered natively using SwiftUI gradient modifiers and are automatically clipped by `borderRadius`. Invalid or unsupported gradient syntax is parsed in **strict mode** and results in **no gradient background** (instead of silent best-effort fallback). @@ -143,14 +146,14 @@ Invalid or unsupported gradient syntax is parsed in **strict mode** and results ```tsx // Named direction - + // Diagonal - + // Angle in degrees/radians/turns - - + + ``` Supported directions: `to right`, `to left`, `to top`, `to bottom`, `to top right`, `to top left`, `to bottom right`, `to bottom left`. @@ -162,7 +165,7 @@ Explicit percentage positions are supported: ```tsx ``` @@ -177,7 +180,8 @@ When positions are omitted, Voltra applies CSS-like stop fix-up: ```tsx ``` @@ -185,9 +189,9 @@ When positions are omitted, Voltra applies CSS-like stop fix-up: ### Radial gradients ```tsx - - - + + + ``` :::note @@ -199,8 +203,8 @@ For `ellipse`, SwiftUI does not provide native elliptical radial gradients. Volt ### Conic gradients ```tsx - - + + ``` ### With border radius @@ -210,7 +214,7 @@ Gradients are clipped by `borderRadius` automatically — no extra configuration ```tsx ``` +For backward compatibility, `backgroundColor` can still receive gradient strings, but new code should use `backgroundImage`. + ### Comparison with `` component -| Feature | `backgroundColor` gradient | `` component | +| Feature | `backgroundImage` / `backgroundColor` gradient | `` component | |---|---|---| | CSS string syntax | ✓ | — | | Named directions (`to right`) | ✓ (physical direction) | ✓ | @@ -245,7 +251,7 @@ Passing a plain color string to `backgroundColor` continues to work exactly as b | Dithering | — | ✓ | | Children layered on top | ✓ (as background) | ✓ (as container) | -Use `backgroundColor` gradient strings for convenience and web-style syntax. Use `` when you need precise `{x, y}` coordinate control or dithering. +Use `backgroundImage` for web-style gradient layering. Keep `backgroundColor` gradients if you need backward compatibility, and use `` when you need precise `{x, y}` coordinate control or dithering. ### Scope and exclusions