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/shared/VoltraNode.swift b/packages/voltra/ios/shared/VoltraNode.swift
index 93d5fa1d..e69e2422 100644
--- a/packages/voltra/ios/shared/VoltraNode.swift
+++ b/packages/voltra/ios/shared/VoltraNode.swift
@@ -185,8 +185,6 @@ struct VoltraElementView: View {
case "Chart":
if #available(iOS 16.0, macOS 13.0, *) {
VoltraChart(element)
- } else {
- EmptyView()
}
default:
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/tests/JSGradientParserTests.swift b/packages/voltra/ios/tests/JSGradientParserTests.swift
index 3b8b3fd5..1d1b1549 100644
--- a/packages/voltra/ios/tests/JSGradientParserTests.swift
+++ b/packages/voltra/ios/tests/JSGradientParserTests.swift
@@ -186,4 +186,16 @@ final class JSColorParserTests: XCTestCase {
XCTAssertNil(JSColorParser.parse("rgb(255,0)"))
XCTAssertNil(JSColorParser.parse("hsl(120, 100, 50%)"))
}
+
+ func testReducedPresentationPrimaryColorDetectionTreatsNeutralColorsAsAdaptive() {
+ XCTAssertTrue(JSColorParser.shouldUsePrimaryColorInReducedPresentation("#F9FAFB"))
+ XCTAssertTrue(JSColorParser.shouldUsePrimaryColorInReducedPresentation("#6B7280"))
+ XCTAssertTrue(JSColorParser.shouldUsePrimaryColorInReducedPresentation("white"))
+ }
+
+ func testReducedPresentationPrimaryColorDetectionPreservesSemanticAccents() {
+ XCTAssertFalse(JSColorParser.shouldUsePrimaryColorInReducedPresentation("#34D399"))
+ XCTAssertFalse(JSColorParser.shouldUsePrimaryColorInReducedPresentation("#F87171"))
+ XCTAssertFalse(JSColorParser.shouldUsePrimaryColorInReducedPresentation("green"))
+ }
}
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/JSColorParser.swift b/packages/voltra/ios/ui/Style/JSColorParser.swift
index 7a62c439..7407d769 100644
--- a/packages/voltra/ios/ui/Style/JSColorParser.swift
+++ b/packages/voltra/ios/ui/Style/JSColorParser.swift
@@ -38,6 +38,13 @@ enum JSColorParser {
return nil
}
+ /// Returns true for neutral foreground colors that should follow WidgetKit's
+ /// reduced-presentation text color instead of preserving a hard-coded shade.
+ static func shouldUsePrimaryColorInReducedPresentation(_ value: Any?) -> Bool {
+ guard let components = parseColorComponents(value) else { return false }
+ return isNeutralColor(components.red, components.green, components.blue)
+ }
+
/// Check if a string is a valid hex color (6 or 8 hex digits)
private static func isHexColor(_ string: String) -> Bool {
guard string.count == 6 || string.count == 8 else { return false }
@@ -46,6 +53,40 @@ enum JSColorParser {
return string.unicodeScalars.allSatisfy { hexChars.contains($0) }
}
+ private static func parseColorComponents(_ value: Any?) -> (red: Double, green: Double, blue: Double, alpha: Double)? {
+ guard let string = value as? String else { return nil }
+
+ let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+ if trimmed.isEmpty { return nil }
+
+ if trimmed.hasPrefix("#") {
+ return parseHexComponents(trimmed)
+ }
+
+ if isHexColor(trimmed) {
+ return parseHexComponents("#" + trimmed)
+ }
+
+ if trimmed.hasPrefix("rgb") {
+ return parseRGBComponents(trimmed)
+ }
+
+ if trimmed.hasPrefix("hsl") {
+ return parseHSLComponents(trimmed)
+ }
+
+ return parseNamedColorComponents(trimmed)
+ }
+
+ private static func isNeutralColor(_ red: Double, _ green: Double, _ blue: Double) -> Bool {
+ let maxComponent = max(red, green, blue)
+ let minComponent = min(red, green, blue)
+ guard maxComponent > 0 else { return true }
+
+ let saturation = (maxComponent - minComponent) / maxComponent
+ return saturation <= 0.2
+ }
+
/// Parse named color strings
private static func parseNamedColor(_ name: String) -> Color? {
switch name {
@@ -105,10 +146,56 @@ enum JSColorParser {
}
}
+ private static func parseNamedColorComponents(_ name: String) -> (red: Double, green: Double, blue: Double, alpha: Double)? {
+ switch name {
+ case "red":
+ return (1, 0, 0, 1)
+ case "orange":
+ return (1, 0.5, 0, 1)
+ case "yellow":
+ return (1, 1, 0, 1)
+ case "green":
+ return (0, 1, 0, 1)
+ case "mint":
+ return (0.62, 0.98, 0.84, 1)
+ case "teal":
+ return (0, 0.5, 0.5, 1)
+ case "cyan":
+ return (0, 1, 1, 1)
+ case "blue":
+ return (0, 0, 1, 1)
+ case "indigo":
+ return (0.29, 0, 0.51, 1)
+ case "purple":
+ return (0.5, 0, 0.5, 1)
+ case "pink":
+ return (1, 0.75, 0.8, 1)
+ case "brown":
+ return (0.6, 0.4, 0.2, 1)
+ case "white":
+ return (1, 1, 1, 1)
+ case "gray":
+ return (0.5, 0.5, 0.5, 1)
+ case "black":
+ return (0, 0, 0, 1)
+ case "clear", "transparent":
+ return (0, 0, 0, 0)
+ case "primary", "secondary":
+ return (0.5, 0.5, 0.5, 1)
+ default:
+ return nil
+ }
+ }
+
// MARK: - Hex Parser
/// Supports #RGB, #RGBA, #RRGGBB, #RRGGBBAA
private static func parseHex(_ hex: String) -> Color? {
+ guard let parsed = parseHexComponents(hex) else { return nil }
+ return Color(.sRGB, red: parsed.red, green: parsed.green, blue: parsed.blue, opacity: parsed.alpha)
+ }
+
+ private static func parseHexComponents(_ hex: String) -> (red: Double, green: Double, blue: Double, alpha: Double)? {
let hexSanitized = hex.replacingOccurrences(of: "#", with: "")
var rgb: UInt64 = 0
@@ -142,7 +229,7 @@ enum JSColorParser {
return nil
}
- return Color(.sRGB, red: r, green: g, blue: b, opacity: a)
+ return (r, g, b, a)
}
// MARK: - RGB Parser
@@ -151,6 +238,12 @@ enum JSColorParser {
/// - rgb(255, 0, 0), rgba(255, 0, 0, 0.5)
/// - rgb(255 0 0 / 80%), rgba(255 0 0 / 0.8)
private static func parseRGB(_ string: String) -> Color? {
+ guard let parsed = parseRGBComponents(string) else { return nil }
+
+ return Color(.sRGB, red: parsed.red, green: parsed.green, blue: parsed.blue, opacity: parsed.alpha)
+ }
+
+ private static func parseRGBComponents(_ string: String) -> (red: Double, green: Double, blue: Double, alpha: Double)? {
guard let function = parseFunctionCall(string, allowedNames: ["rgb", "rgba"]) else { return nil }
let isRgba = function.name == "rgba"
@@ -162,7 +255,7 @@ enum JSColorParser {
}
guard let parsed else { return nil }
- return Color(.sRGB, red: parsed.r, green: parsed.g, blue: parsed.b, opacity: parsed.a)
+ return (parsed.r, parsed.g, parsed.b, parsed.a)
}
// MARK: - HSL Parser
@@ -171,6 +264,11 @@ enum JSColorParser {
/// - hsl(120, 100%, 50%), hsla(120, 100%, 50%, 0.5)
/// - hsl(120 100% 50% / 30%), hsla(120 100% 50% / 0.3)
private static func parseHSL(_ string: String) -> Color? {
+ guard let parsed = parseHSLComponents(string) else { return nil }
+ return Color(.sRGB, red: parsed.red, green: parsed.green, blue: parsed.blue, opacity: parsed.alpha)
+ }
+
+ private static func parseHSLComponents(_ string: String) -> (red: Double, green: Double, blue: Double, alpha: Double)? {
guard let function = parseFunctionCall(string, allowedNames: ["hsl", "hsla"]) else { return nil }
let isHsla = function.name == "hsla"
@@ -182,14 +280,8 @@ enum JSColorParser {
}
guard let parsed else { return nil }
- let h = parsed.h
- let s = parsed.s
- let l = parsed.l
- let a = parsed.a
-
- // Convert HSL to RGB (HSL != HSB/HSV)
- let (r, g, b) = hslToRgb(h: h, s: s, l: l)
- return Color(.sRGB, red: r, green: g, blue: b, opacity: a)
+ let (r, g, b) = hslToRgb(h: parsed.h, s: parsed.s, l: parsed.l)
+ return (r, g, b, parsed.a)
}
private struct FunctionCall {
diff --git a/packages/voltra/ios/ui/Style/StyleConverter.swift b/packages/voltra/ios/ui/Style/StyleConverter.swift
index 9cd5538b..cabacce6 100644
--- a/packages/voltra/ios/ui/Style/StyleConverter.swift
+++ b/packages/voltra/ios/ui/Style/StyleConverter.swift
@@ -155,6 +155,7 @@ enum StyleConverter {
if let color = JSColorParser.parse(js["color"]) {
style.color = color
+ style.usesPrimaryColorInReducedPresentation = JSColorParser.shouldUsePrimaryColorInReducedPresentation(js["color"])
}
if let size = JSStyleParser.number(js["fontSize"]) {
diff --git a/packages/voltra/ios/ui/Style/TextStyle.swift b/packages/voltra/ios/ui/Style/TextStyle.swift
index 2bf3389d..c00091e7 100644
--- a/packages/voltra/ios/ui/Style/TextStyle.swift
+++ b/packages/voltra/ios/ui/Style/TextStyle.swift
@@ -2,6 +2,7 @@ import SwiftUI
struct TextStyle {
var color: Color = .primary
+ var usesPrimaryColorInReducedPresentation = false
var fontSize: CGFloat = 17
var fontWeight: Font.Weight = .regular
var fontFamily: String?
@@ -16,6 +17,18 @@ struct TextStyle {
struct TextStyleModifier: ViewModifier {
let style: TextStyle
+ @Environment(\.voltraEnvironment) private var voltraEnvironment
+
+ private var resolvedColor: Color {
+ if let widget = voltraEnvironment.widget,
+ widget.usesReducedBackgroundPresentation,
+ style.usesPrimaryColorInReducedPresentation
+ {
+ return .primary
+ }
+
+ return style.color
+ }
func body(content: Content) -> some View {
content
@@ -28,7 +41,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..2544de64 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
@@ -21,8 +24,6 @@ public struct VoltraGlassContainer: VoltraView {
children
}.applyStyle(element.style)
}
- } else {
- EmptyView()
}
}
}
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..a532b393 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,24 @@ public struct VoltraText: VoltraView {
return textStyle.alignment
}()
+ let resolvedColor: Color = {
+ if let widget = voltraEnvironment.widget,
+ widget.usesReducedBackgroundPresentation,
+ textStyle.usesPrimaryColorInReducedPresentation
+ {
+ 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
)
}