diff --git a/example/screens/testing-grounds/weather/WeatherTestingScreen.tsx b/example/screens/testing-grounds/weather/WeatherTestingScreen.tsx
index 983bb3f0..7828455a 100644
--- a/example/screens/testing-grounds/weather/WeatherTestingScreen.tsx
+++ b/example/screens/testing-grounds/weather/WeatherTestingScreen.tsx
@@ -6,7 +6,7 @@ import { reloadWidgets, scheduleWidget, updateWidget, VoltraWidgetPreview, Widge
import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
-import { WeatherWidget } from '~/widgets/ios/IosWeatherWidget'
+import { IosWeatherWidget } from '~/widgets/ios/IosWeatherWidget'
import { SAMPLE_WEATHER_DATA, type WeatherCondition, type WeatherData } from '~/widgets/weather-types'
const WIDGET_FAMILIES: { id: WidgetFamily; title: string; description: string }[] = [
@@ -74,9 +74,9 @@ export default function WeatherTestingScreen() {
setIsUpdating(true)
try {
await updateWidget('weather', {
- systemSmall: ,
- systemMedium: ,
- systemLarge: ,
+ systemSmall: ,
+ systemMedium: ,
+ systemLarge: ,
})
await reloadWidgets(['weather'])
} catch (error) {
@@ -107,9 +107,9 @@ export default function WeatherTestingScreen() {
setIsUpdating(true)
try {
await updateWidget('weather', {
- systemSmall: ,
- systemMedium: ,
- systemLarge: ,
+ systemSmall: ,
+ systemMedium: ,
+ systemLarge: ,
})
await reloadWidgets(['weather'])
} catch (error) {
@@ -237,9 +237,9 @@ export default function WeatherTestingScreen() {
try {
await updateWidget('weather', {
- systemSmall: ,
- systemMedium: ,
- systemLarge: ,
+ systemSmall: ,
+ systemMedium: ,
+ systemLarge: ,
})
// Don't call reloadWidgets here to avoid resetting scheduled timelines
} catch (error) {
@@ -348,7 +348,7 @@ export default function WeatherTestingScreen() {
-
+
diff --git a/packages/voltra/ios/target/VoltraHomeWidget.swift b/packages/voltra/ios/target/VoltraHomeWidget.swift
index 8cb6cf78..91acd1f5 100644
--- a/packages/voltra/ios/target/VoltraHomeWidget.swift
+++ b/packages/voltra/ios/target/VoltraHomeWidget.swift
@@ -276,6 +276,9 @@ public struct VoltraHomeWidgetProvider: TimelineProvider {
public struct VoltraHomeWidgetView: View {
public var entry: VoltraHomeWidgetEntry
+ @Environment(\.showsWidgetContainerBackground) private var showsWidgetContainerBackground
+ @Environment(\.widgetRenderingMode) private var widgetRenderingMode
+
public init(entry: VoltraHomeWidgetEntry) {
self.entry = entry
}
@@ -285,12 +288,22 @@ public struct VoltraHomeWidgetView: View {
}
public var body: some View {
+ let mappedRenderingMode = mapWidgetRenderingMode(widgetRenderingMode)
+
Group {
if let root = entry.rootNode {
// No parsing here - just render the pre-parsed AST
- let content = Voltra(root: root, activityId: "widget")
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
- .widgetURL(resolveDeepLinkURL(entry))
+ let content = Voltra(
+ root: root,
+ activityId: "widget",
+ widget: VoltraWidgetEnvironment(
+ isHomeScreenWidget: true,
+ renderingMode: mappedRenderingMode,
+ showsContainerBackground: showsWidgetContainerBackground
+ )
+ )
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+ .widgetURL(resolveDeepLinkURL(entry))
if showRefreshButton {
content.overlay(alignment: .topTrailing) {
@@ -306,6 +319,19 @@ public struct VoltraHomeWidgetView: View {
.disableWidgetMarginsIfAvailable()
}
+ private func mapWidgetRenderingMode(_ mode: WidgetRenderingMode) -> VoltraWidgetRenderingMode {
+ switch mode {
+ case .fullColor:
+ return .fullColor
+ case .accented:
+ return .accented
+ case .vibrant:
+ return .vibrant
+ default:
+ return .unknown
+ }
+ }
+
@ViewBuilder
private var refreshButton: some View {
if #available(iOSApplicationExtension 17.0, *) {
diff --git a/packages/voltra/ios/ui/Layout/FlexContainerHelper.swift b/packages/voltra/ios/ui/Layout/FlexContainerHelper.swift
index 258ea2d9..7882b7c1 100644
--- a/packages/voltra/ios/ui/Layout/FlexContainerHelper.swift
+++ b/packages/voltra/ios/ui/Layout/FlexContainerHelper.swift
@@ -81,7 +81,7 @@ struct FlexContainerStyleModifier: ViewModifier {
content
.modifier(LayoutModifier(style: layoutWithoutPadding))
- .modifier(DecorationModifier(style: values.decoration))
+ .modifier(DecorationModifier(style: values.decoration, layout: layout))
.modifier(RenderingModifier(style: values.rendering))
.voltraIfLet(layout.margin) { c, margin in
c.background(.clear).padding(margin)
diff --git a/packages/voltra/ios/ui/Style/CompositeStyle.swift b/packages/voltra/ios/ui/Style/CompositeStyle.swift
index bc329919..fcd0e966 100644
--- a/packages/voltra/ios/ui/Style/CompositeStyle.swift
+++ b/packages/voltra/ios/ui/Style/CompositeStyle.swift
@@ -20,7 +20,7 @@ struct CompositeStyleModifier: ViewModifier {
content
.voltraIfLet(layout.padding) { c, p in c.padding(p) }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: contentAlignment)
- .modifier(DecorationModifier(style: decoration))
+ .modifier(DecorationModifier(style: decoration, layout: layout))
.modifier(RenderingModifier(style: rendering))
.layoutValue(key: FlexItemLayoutKey.self, value: FlexItemValues(
flexGrow: layout.flexGrow,
@@ -53,7 +53,7 @@ struct CompositeStyleModifier: ViewModifier {
alignment: alignment
)
}
- .modifier(DecorationModifier(style: decoration))
+ .modifier(DecorationModifier(style: decoration, layout: layout))
.modifier(RenderingModifier(style: rendering))
}
}
diff --git a/packages/voltra/ios/ui/Style/DecorationStyle.swift b/packages/voltra/ios/ui/Style/DecorationStyle.swift
index e7e345dd..6b06b8d1 100644
--- a/packages/voltra/ios/ui/Style/DecorationStyle.swift
+++ b/packages/voltra/ios/ui/Style/DecorationStyle.swift
@@ -11,6 +11,8 @@ struct DecorationStyle {
struct DecorationModifier: ViewModifier {
let style: DecorationStyle
+ let layout: LayoutStyle?
+ @Environment(\.voltraEnvironment) private var voltraEnvironment
private func point(from unitPoint: UnitPoint, in size: CGSize) -> CGPoint {
CGPoint(x: unitPoint.x * size.width, y: unitPoint.y * size.height)
@@ -105,9 +107,43 @@ struct DecorationModifier: ViewModifier {
.allowsHitTesting(false)
}
+ private var suppressesDecorativeContainerEffects: Bool {
+ voltraEnvironment.widget?.suppressesDecorativeContainerEffects == true
+ }
+
+ private var isFullBleedBackgroundCandidate: Bool {
+ guard let layout else { return false }
+
+ if let flex = layout.flex, flex > 0 {
+ return true
+ }
+
+ if layout.flexGrow > 0 {
+ return true
+ }
+
+ return layout.width == .fill && layout.height == .fill
+ }
+
+ private var resolvedBackgroundColor: BackgroundValue? {
+ guard suppressesDecorativeContainerEffects, isFullBleedBackgroundCandidate else {
+ return style.backgroundColor
+ }
+
+ return nil
+ }
+
+ private var resolvedGlassEffect: GlassEffect? {
+ guard suppressesDecorativeContainerEffects else {
+ return style.glassEffect
+ }
+
+ return nil
+ }
+
func body(content: Content) -> some View {
content
- .voltraIfLet(style.backgroundColor) { content, bg in
+ .voltraIfLet(resolvedBackgroundColor) { content, bg in
switch bg {
case let .color(color):
content.background(color)
@@ -151,7 +187,7 @@ struct DecorationModifier: ViewModifier {
y: shadow.offset.height
)
}
- .voltraIfLet(style.glassEffect) { content, glassEffect in
+ .voltraIfLet(resolvedGlassEffect) { content, glassEffect in
if #available(iOS 26.0, *) {
switch glassEffect {
case .clear:
diff --git a/packages/voltra/ios/ui/Style/TextStyle.swift b/packages/voltra/ios/ui/Style/TextStyle.swift
index 2bf3389d..c168fd02 100644
--- a/packages/voltra/ios/ui/Style/TextStyle.swift
+++ b/packages/voltra/ios/ui/Style/TextStyle.swift
@@ -16,6 +16,15 @@ struct TextStyle {
struct TextStyleModifier: ViewModifier {
let style: TextStyle
+ @Environment(\.voltraEnvironment) private var voltraEnvironment
+
+ private var resolvedColor: Color {
+ if let widget = voltraEnvironment.widget, widget.usesReducedBackgroundPresentation {
+ return .primary
+ }
+
+ return style.color
+ }
func body(content: Content) -> some View {
content
@@ -28,7 +37,7 @@ struct TextStyleModifier: ViewModifier {
: .system(size: style.fontSize, weight: style.fontWeight)
)
// 2. Color
- .foregroundColor(style.color)
+ .foregroundColor(resolvedColor)
// 3. Layout / Spacing
.multilineTextAlignment(style.alignment)
.lineLimit(style.lineLimit)
diff --git a/packages/voltra/ios/ui/Views/VoltraGlassContainer.swift b/packages/voltra/ios/ui/Views/VoltraGlassContainer.swift
index 2e45380c..852952b5 100644
--- a/packages/voltra/ios/ui/Views/VoltraGlassContainer.swift
+++ b/packages/voltra/ios/ui/Views/VoltraGlassContainer.swift
@@ -4,6 +4,7 @@ public struct VoltraGlassContainer: VoltraView {
public typealias Parameters = GlassContainerParameters
public let element: VoltraElement
+ @Environment(\.voltraEnvironment) private var voltraEnvironment
public init(_ element: VoltraElement) {
self.element = element
@@ -11,7 +12,9 @@ public struct VoltraGlassContainer: VoltraView {
public var body: some View {
if let children = element.children {
- if #available(iOS 26.0, *) {
+ if voltraEnvironment.widget?.suppressesDecorativeContainerEffects == true {
+ children.applyStyle(element.style)
+ } else if #available(iOS 26.0, *) {
let spacing = params.spacing ?? 0.0
GlassEffectContainer(spacing: CGFloat(spacing)) {
children
diff --git a/packages/voltra/ios/ui/Views/VoltraLinearGradient.swift b/packages/voltra/ios/ui/Views/VoltraLinearGradient.swift
index 1ae8fd05..53edcdd4 100644
--- a/packages/voltra/ios/ui/Views/VoltraLinearGradient.swift
+++ b/packages/voltra/ios/ui/Views/VoltraLinearGradient.swift
@@ -4,6 +4,7 @@ public struct VoltraLinearGradient: VoltraView {
public typealias Parameters = LinearGradientParameters
public let element: VoltraElement
+ @Environment(\.voltraEnvironment) private var voltraEnvironment
public init(_ element: VoltraElement) {
self.element = element
@@ -67,20 +68,48 @@ public struct VoltraLinearGradient: VoltraView {
return Gradient(colors: [Color.black.opacity(0.25), Color.black.opacity(0.05)])
}
+ private func isFullBleedWidgetBackgroundCandidate() -> Bool {
+ guard let style = element.style, element.children != nil else {
+ return false
+ }
+ if let flex = style["flex"]?.doubleValue, flex > 0 {
+ return true
+ }
+ if let flexGrow = style["flexGrow"]?.doubleValue, flexGrow > 0 {
+ return true
+ }
+ let width = style["width"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
+ let height = style["height"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
+ return width == "100%" && height == "100%"
+ }
+
public var body: some View {
let gradient = buildGradient(params: params)
let start = parsePoint(params.startPoint)
let end = parsePoint(params.endPoint)
+ let anyStyle = element.style?.mapValues { $0.toAny() } ?? [:]
+ let (layout, baseDecoration, rendering, text) = StyleConverter.convert(anyStyle)
+
+ var decoration = baseDecoration
+ decoration.backgroundColor = .linearGradient(gradient: gradient, startPoint: start, endPoint: end)
- // Note: dither parameter is available in node.parameters["dither"] but SwiftUI's LinearGradient
- // doesn't expose dithering control directly. This is handled automatically by the system.
- let lg = LinearGradient(gradient: gradient, startPoint: start, endPoint: end)
+ if let widget = voltraEnvironment.widget,
+ widget.isHomeScreenWidget,
+ widget.usesReducedBackgroundPresentation,
+ isFullBleedWidgetBackgroundCandidate(),
+ let children = element.children
+ {
+ return AnyView(children.applyStyle(element.style))
+ }
- // Use ZStack with a Rectangle that fills and is tinted by the gradient, then overlay children.
- return ZStack {
- Rectangle().fill(lg)
- element.children ?? .empty
+ if let children = element.children {
+ return AnyView(
+ children.applyStyle((layout, decoration, rendering, text))
+ )
}
- .applyStyle(element.style)
+
+ return AnyView(
+ Color.clear.applyStyle((layout, decoration, rendering, text))
+ )
}
}
diff --git a/packages/voltra/ios/ui/Views/VoltraText.swift b/packages/voltra/ios/ui/Views/VoltraText.swift
index fb3aad76..416050c7 100644
--- a/packages/voltra/ios/ui/Views/VoltraText.swift
+++ b/packages/voltra/ios/ui/Views/VoltraText.swift
@@ -4,6 +4,7 @@ public struct VoltraText: VoltraView {
public typealias Parameters = TextParameters
public let element: VoltraElement
+ @Environment(\.voltraEnvironment) private var voltraEnvironment
public init(_ element: VoltraElement) {
self.element = element
@@ -58,13 +59,21 @@ public struct VoltraText: VoltraView {
return textStyle.alignment
}()
+ let resolvedColor: Color = {
+ if let widget = voltraEnvironment.widget, widget.usesReducedBackgroundPresentation {
+ return .primary
+ }
+
+ return textStyle.color
+ }()
+
Text(.init(textContent))
.kerning(textStyle.letterSpacing)
.underline(textStyle.decoration == .underline || textStyle.decoration == .underlineLineThrough)
.strikethrough(textStyle.decoration == .lineThrough || textStyle.decoration == .underlineLineThrough)
// These technically work on View, but good to keep close
.font(font)
- .foregroundColor(textStyle.color)
+ .foregroundColor(resolvedColor)
.multilineTextAlignment(alignment)
.lineSpacing(textStyle.lineSpacing)
.voltraIfLet(params.numberOfLines) { view, numberOfLines in
diff --git a/packages/voltra/ios/ui/Voltra.swift b/packages/voltra/ios/ui/Voltra.swift
index 22672831..cf063a80 100644
--- a/packages/voltra/ios/ui/Voltra.swift
+++ b/packages/voltra/ios/ui/Voltra.swift
@@ -1,8 +1,42 @@
import SwiftUI
+public enum VoltraWidgetRenderingMode {
+ case fullColor
+ case accented
+ case vibrant
+ case unknown
+}
+
+public struct VoltraWidgetEnvironment {
+ public let isHomeScreenWidget: Bool
+ public let renderingMode: VoltraWidgetRenderingMode
+ public let showsContainerBackground: Bool
+
+ var usesReducedBackgroundPresentation: Bool {
+ renderingMode != .fullColor || !showsContainerBackground
+ }
+
+ var suppressesDecorativeContainerEffects: Bool {
+ isHomeScreenWidget && usesReducedBackgroundPresentation
+ }
+
+ public init(
+ isHomeScreenWidget: Bool,
+ renderingMode: VoltraWidgetRenderingMode,
+ showsContainerBackground: Bool
+ ) {
+ self.isHomeScreenWidget = isHomeScreenWidget
+ self.renderingMode = renderingMode
+ self.showsContainerBackground = showsContainerBackground
+ }
+}
+
struct VoltraEnvironment {
/// Activity ID for Live Activity interactions
let activityId: String
+
+ /// Widget-specific presentation context, when rendering inside WidgetKit.
+ let widget: VoltraWidgetEnvironment?
}
public struct Voltra: View {
@@ -12,28 +46,35 @@ public struct Voltra: View {
/// Activity ID for Live Activity interactions
public var activityId: String
+ /// Widget-specific presentation context, when rendering inside WidgetKit.
+ var widget: VoltraWidgetEnvironment?
+
/// Initialize Voltra
///
/// - Parameter root: Pre-parsed root VoltraNode
/// - Parameter callback: Handler for element interactions
/// - Parameter activityId: Activity ID for Live Activity interactions
- public init(root: VoltraNode, activityId: String) {
+ /// - Parameter widget: Widget rendering context used to adapt Voltra output for WidgetKit surfaces
+ public init(root: VoltraNode, activityId: String, widget: VoltraWidgetEnvironment? = nil) {
self.root = root
self.activityId = activityId
+ self.widget = widget
}
/// Generated body for SwiftUI
public var body: some View {
root
.environment(\.voltraEnvironment, VoltraEnvironment(
- activityId: activityId
+ activityId: activityId,
+ widget: widget
))
}
}
private struct VoltraEnvironmentKey: EnvironmentKey {
static let defaultValue: VoltraEnvironment = .init(
- activityId: ""
+ activityId: "",
+ widget: nil
)
}