voltra 1.4.0-rc.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # voltra
2
2
 
3
+ ## 1.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - a5a315b: Fix `maxLines` text truncation on Android widgets so line limits apply correctly.
8
+ - iOS home screen widgets now match Tinted and Clear system appearances: no more default opaque white card behind your widget, with colors and gradients adjusted so content stays readable.
9
+ - Updated dependencies [a5a315b]
10
+ - Updated dependencies
11
+ - @use-voltra/android@1.4.1
12
+ - @use-voltra/android-client@1.4.1
13
+ - @use-voltra/android-server@1.4.1
14
+ - @use-voltra/core@1.4.1
15
+ - @use-voltra/expo-plugin@1.4.1
16
+ - @use-voltra/ios@1.4.1
17
+ - @use-voltra/ios-client@1.4.1
18
+ - @use-voltra/ios-server@1.4.1
19
+ - @use-voltra/server@1.4.1
20
+
21
+ ## 1.4.0
22
+
23
+ ### Minor Changes
24
+
25
+ - Android home-screen widgets can use colors that follow the user’s theme and wallpaper (including Material You), so widgets feel native in light, dark, and dynamic setups. If you drive widgets from your own server, you can read the full request URL—including query parameters—when handling updates, which makes it easier to personalize or A/B content per link. Widget updates on iOS are a bit more forgiving when variant data is missing.
26
+ - 14d4fa5: Add Android ongoing notification support, including richer notification content, remote update flows, and server-side payload rendering APIs. This release also expands the Expo integration and documentation so apps can configure, send, and manage Android ongoing notifications more easily.
27
+
28
+ ### Patch Changes
29
+
30
+ - Android apps built for production (minified / release) are less likely to crash or mis-render widgets because of how widget payloads are processed on the device.
31
+ - 8cedb47: Fix iOS Live Activity naming so named activities can be reused more reliably across app launches.
32
+ - Work on decomposing Voltra into smaller packages continues, and more pieces have moved from the umbrella package into the respective `@use-voltra/*` packages. You should still use the `voltra` umbrella for your app.
33
+ - Updated dependencies
34
+ - Updated dependencies [14d4fa5]
35
+ - Updated dependencies
36
+ - @use-voltra/android@1.4.0
37
+ - @use-voltra/android-server@1.4.0
38
+ - @use-voltra/ios-server@1.3.2
39
+ - @use-voltra/server@1.4.0
40
+ - @use-voltra/expo-plugin@1.4.0
41
+ - @use-voltra/android-client@1.4.0
42
+ - @use-voltra/core@1.4.0
43
+ - @use-voltra/ios@1.4.0
44
+ - @use-voltra/ios-client@1.4.0
45
+
3
46
  ## 1.3.0
4
47
 
5
48
  ### Patch Changes
@@ -53,6 +53,10 @@ fun RenderText(
53
53
  val text = extractTextFromNode(element.c)
54
54
  val renderAsBitmap = element.p?.get("renderAsBitmap") as? Boolean ?: false
55
55
  val textStyle = resolvedStyle?.text ?: voltra.styling.TextStyle.Default
56
+ val maxLines =
57
+ (element.p?.get("maxLines") as? Number)?.toInt()
58
+ ?: textStyle.lineLimit
59
+ ?: Int.MAX_VALUE
56
60
 
57
61
  if (renderAsBitmap && textStyle.fontFamily != null) {
58
62
  val context = LocalContext.current
@@ -67,6 +71,7 @@ fun RenderText(
67
71
  val bitmapTextStyle =
68
72
  textStyle.copy(
69
73
  color = textStyle.color?.let { VoltraColorValue.Static(it.resolveColor(context)) },
74
+ lineLimit = maxLines,
70
75
  )
71
76
  // Use screen width as max constraint
72
77
  val maxWidthPx = (context.resources.displayMetrics.widthPixels * 0.9f).toInt()
@@ -95,7 +100,7 @@ fun RenderText(
95
100
  }
96
101
 
97
102
  val glanceTextStyle = textStyle.toGlanceTextStyle()
98
- Text(text = text, modifier = finalModifier, style = glanceTextStyle)
103
+ Text(text = text, modifier = finalModifier, style = glanceTextStyle, maxLines = maxLines)
99
104
  }
100
105
 
101
106
  @Composable
@@ -185,8 +185,6 @@ struct VoltraElementView: View {
185
185
  case "Chart":
186
186
  if #available(iOS 16.0, macOS 13.0, *) {
187
187
  VoltraChart(element)
188
- } else {
189
- EmptyView()
190
188
  }
191
189
 
192
190
  default:
@@ -276,6 +276,9 @@ public struct VoltraHomeWidgetProvider: TimelineProvider {
276
276
  public struct VoltraHomeWidgetView: View {
277
277
  public var entry: VoltraHomeWidgetEntry
278
278
 
279
+ @Environment(\.showsWidgetContainerBackground) private var showsWidgetContainerBackground
280
+ @Environment(\.widgetRenderingMode) private var widgetRenderingMode
281
+
279
282
  public init(entry: VoltraHomeWidgetEntry) {
280
283
  self.entry = entry
281
284
  }
@@ -285,12 +288,22 @@ public struct VoltraHomeWidgetView: View {
285
288
  }
286
289
 
287
290
  public var body: some View {
291
+ let mappedRenderingMode = mapWidgetRenderingMode(widgetRenderingMode)
292
+
288
293
  Group {
289
294
  if let root = entry.rootNode {
290
295
  // No parsing here - just render the pre-parsed AST
291
- let content = Voltra(root: root, activityId: "widget")
292
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
293
- .widgetURL(resolveDeepLinkURL(entry))
296
+ let content = Voltra(
297
+ root: root,
298
+ activityId: "widget",
299
+ widget: VoltraWidgetEnvironment(
300
+ isHomeScreenWidget: true,
301
+ renderingMode: mappedRenderingMode,
302
+ showsContainerBackground: showsWidgetContainerBackground
303
+ )
304
+ )
305
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
306
+ .widgetURL(resolveDeepLinkURL(entry))
294
307
 
295
308
  if showRefreshButton {
296
309
  content.overlay(alignment: .topTrailing) {
@@ -306,6 +319,19 @@ public struct VoltraHomeWidgetView: View {
306
319
  .disableWidgetMarginsIfAvailable()
307
320
  }
308
321
 
322
+ private func mapWidgetRenderingMode(_ mode: WidgetRenderingMode) -> VoltraWidgetRenderingMode {
323
+ switch mode {
324
+ case .fullColor:
325
+ return .fullColor
326
+ case .accented:
327
+ return .accented
328
+ case .vibrant:
329
+ return .vibrant
330
+ default:
331
+ return .unknown
332
+ }
333
+ }
334
+
309
335
  @ViewBuilder
310
336
  private var refreshButton: some View {
311
337
  if #available(iOSApplicationExtension 17.0, *) {
@@ -81,7 +81,7 @@ struct FlexContainerStyleModifier: ViewModifier {
81
81
 
82
82
  content
83
83
  .modifier(LayoutModifier(style: layoutWithoutPadding))
84
- .modifier(DecorationModifier(style: values.decoration))
84
+ .modifier(DecorationModifier(style: values.decoration, layout: layout))
85
85
  .modifier(RenderingModifier(style: values.rendering))
86
86
  .voltraIfLet(layout.margin) { c, margin in
87
87
  c.background(.clear).padding(margin)
@@ -20,7 +20,7 @@ struct CompositeStyleModifier: ViewModifier {
20
20
  content
21
21
  .voltraIfLet(layout.padding) { c, p in c.padding(p) }
22
22
  .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: contentAlignment)
23
- .modifier(DecorationModifier(style: decoration))
23
+ .modifier(DecorationModifier(style: decoration, layout: layout))
24
24
  .modifier(RenderingModifier(style: rendering))
25
25
  .layoutValue(key: FlexItemLayoutKey.self, value: FlexItemValues(
26
26
  flexGrow: layout.flexGrow,
@@ -53,7 +53,7 @@ struct CompositeStyleModifier: ViewModifier {
53
53
  alignment: alignment
54
54
  )
55
55
  }
56
- .modifier(DecorationModifier(style: decoration))
56
+ .modifier(DecorationModifier(style: decoration, layout: layout))
57
57
  .modifier(RenderingModifier(style: rendering))
58
58
  }
59
59
  }
@@ -11,6 +11,8 @@ struct DecorationStyle {
11
11
 
12
12
  struct DecorationModifier: ViewModifier {
13
13
  let style: DecorationStyle
14
+ let layout: LayoutStyle?
15
+ @Environment(\.voltraEnvironment) private var voltraEnvironment
14
16
 
15
17
  private func point(from unitPoint: UnitPoint, in size: CGSize) -> CGPoint {
16
18
  CGPoint(x: unitPoint.x * size.width, y: unitPoint.y * size.height)
@@ -105,9 +107,43 @@ struct DecorationModifier: ViewModifier {
105
107
  .allowsHitTesting(false)
106
108
  }
107
109
 
110
+ private var suppressesDecorativeContainerEffects: Bool {
111
+ voltraEnvironment.widget?.suppressesDecorativeContainerEffects == true
112
+ }
113
+
114
+ private var isFullBleedBackgroundCandidate: Bool {
115
+ guard let layout else { return false }
116
+
117
+ if let flex = layout.flex, flex > 0 {
118
+ return true
119
+ }
120
+
121
+ if layout.flexGrow > 0 {
122
+ return true
123
+ }
124
+
125
+ return layout.width == .fill && layout.height == .fill
126
+ }
127
+
128
+ private var resolvedBackgroundColor: BackgroundValue? {
129
+ guard suppressesDecorativeContainerEffects, isFullBleedBackgroundCandidate else {
130
+ return style.backgroundColor
131
+ }
132
+
133
+ return nil
134
+ }
135
+
136
+ private var resolvedGlassEffect: GlassEffect? {
137
+ guard suppressesDecorativeContainerEffects else {
138
+ return style.glassEffect
139
+ }
140
+
141
+ return nil
142
+ }
143
+
108
144
  func body(content: Content) -> some View {
109
145
  content
110
- .voltraIfLet(style.backgroundColor) { content, bg in
146
+ .voltraIfLet(resolvedBackgroundColor) { content, bg in
111
147
  switch bg {
112
148
  case let .color(color):
113
149
  content.background(color)
@@ -151,7 +187,7 @@ struct DecorationModifier: ViewModifier {
151
187
  y: shadow.offset.height
152
188
  )
153
189
  }
154
- .voltraIfLet(style.glassEffect) { content, glassEffect in
190
+ .voltraIfLet(resolvedGlassEffect) { content, glassEffect in
155
191
  if #available(iOS 26.0, *) {
156
192
  switch glassEffect {
157
193
  case .clear:
@@ -38,6 +38,13 @@ enum JSColorParser {
38
38
  return nil
39
39
  }
40
40
 
41
+ /// Returns true for neutral foreground colors that should follow WidgetKit's
42
+ /// reduced-presentation text color instead of preserving a hard-coded shade.
43
+ static func shouldUsePrimaryColorInReducedPresentation(_ value: Any?) -> Bool {
44
+ guard let components = parseColorComponents(value) else { return false }
45
+ return isNeutralColor(components.red, components.green, components.blue)
46
+ }
47
+
41
48
  /// Check if a string is a valid hex color (6 or 8 hex digits)
42
49
  private static func isHexColor(_ string: String) -> Bool {
43
50
  guard string.count == 6 || string.count == 8 else { return false }
@@ -46,6 +53,40 @@ enum JSColorParser {
46
53
  return string.unicodeScalars.allSatisfy { hexChars.contains($0) }
47
54
  }
48
55
 
56
+ private static func parseColorComponents(_ value: Any?) -> (red: Double, green: Double, blue: Double, alpha: Double)? {
57
+ guard let string = value as? String else { return nil }
58
+
59
+ let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
60
+ if trimmed.isEmpty { return nil }
61
+
62
+ if trimmed.hasPrefix("#") {
63
+ return parseHexComponents(trimmed)
64
+ }
65
+
66
+ if isHexColor(trimmed) {
67
+ return parseHexComponents("#" + trimmed)
68
+ }
69
+
70
+ if trimmed.hasPrefix("rgb") {
71
+ return parseRGBComponents(trimmed)
72
+ }
73
+
74
+ if trimmed.hasPrefix("hsl") {
75
+ return parseHSLComponents(trimmed)
76
+ }
77
+
78
+ return parseNamedColorComponents(trimmed)
79
+ }
80
+
81
+ private static func isNeutralColor(_ red: Double, _ green: Double, _ blue: Double) -> Bool {
82
+ let maxComponent = max(red, green, blue)
83
+ let minComponent = min(red, green, blue)
84
+ guard maxComponent > 0 else { return true }
85
+
86
+ let saturation = (maxComponent - minComponent) / maxComponent
87
+ return saturation <= 0.2
88
+ }
89
+
49
90
  /// Parse named color strings
50
91
  private static func parseNamedColor(_ name: String) -> Color? {
51
92
  switch name {
@@ -105,10 +146,56 @@ enum JSColorParser {
105
146
  }
106
147
  }
107
148
 
149
+ private static func parseNamedColorComponents(_ name: String) -> (red: Double, green: Double, blue: Double, alpha: Double)? {
150
+ switch name {
151
+ case "red":
152
+ return (1, 0, 0, 1)
153
+ case "orange":
154
+ return (1, 0.5, 0, 1)
155
+ case "yellow":
156
+ return (1, 1, 0, 1)
157
+ case "green":
158
+ return (0, 1, 0, 1)
159
+ case "mint":
160
+ return (0.62, 0.98, 0.84, 1)
161
+ case "teal":
162
+ return (0, 0.5, 0.5, 1)
163
+ case "cyan":
164
+ return (0, 1, 1, 1)
165
+ case "blue":
166
+ return (0, 0, 1, 1)
167
+ case "indigo":
168
+ return (0.29, 0, 0.51, 1)
169
+ case "purple":
170
+ return (0.5, 0, 0.5, 1)
171
+ case "pink":
172
+ return (1, 0.75, 0.8, 1)
173
+ case "brown":
174
+ return (0.6, 0.4, 0.2, 1)
175
+ case "white":
176
+ return (1, 1, 1, 1)
177
+ case "gray":
178
+ return (0.5, 0.5, 0.5, 1)
179
+ case "black":
180
+ return (0, 0, 0, 1)
181
+ case "clear", "transparent":
182
+ return (0, 0, 0, 0)
183
+ case "primary", "secondary":
184
+ return (0.5, 0.5, 0.5, 1)
185
+ default:
186
+ return nil
187
+ }
188
+ }
189
+
108
190
  // MARK: - Hex Parser
109
191
 
110
192
  /// Supports #RGB, #RGBA, #RRGGBB, #RRGGBBAA
111
193
  private static func parseHex(_ hex: String) -> Color? {
194
+ guard let parsed = parseHexComponents(hex) else { return nil }
195
+ return Color(.sRGB, red: parsed.red, green: parsed.green, blue: parsed.blue, opacity: parsed.alpha)
196
+ }
197
+
198
+ private static func parseHexComponents(_ hex: String) -> (red: Double, green: Double, blue: Double, alpha: Double)? {
112
199
  let hexSanitized = hex.replacingOccurrences(of: "#", with: "")
113
200
 
114
201
  var rgb: UInt64 = 0
@@ -142,7 +229,7 @@ enum JSColorParser {
142
229
  return nil
143
230
  }
144
231
 
145
- return Color(.sRGB, red: r, green: g, blue: b, opacity: a)
232
+ return (r, g, b, a)
146
233
  }
147
234
 
148
235
  // MARK: - RGB Parser
@@ -151,6 +238,12 @@ enum JSColorParser {
151
238
  /// - rgb(255, 0, 0), rgba(255, 0, 0, 0.5)
152
239
  /// - rgb(255 0 0 / 80%), rgba(255 0 0 / 0.8)
153
240
  private static func parseRGB(_ string: String) -> Color? {
241
+ guard let parsed = parseRGBComponents(string) else { return nil }
242
+
243
+ return Color(.sRGB, red: parsed.red, green: parsed.green, blue: parsed.blue, opacity: parsed.alpha)
244
+ }
245
+
246
+ private static func parseRGBComponents(_ string: String) -> (red: Double, green: Double, blue: Double, alpha: Double)? {
154
247
  guard let function = parseFunctionCall(string, allowedNames: ["rgb", "rgba"]) else { return nil }
155
248
 
156
249
  let isRgba = function.name == "rgba"
@@ -162,7 +255,7 @@ enum JSColorParser {
162
255
  }
163
256
  guard let parsed else { return nil }
164
257
 
165
- return Color(.sRGB, red: parsed.r, green: parsed.g, blue: parsed.b, opacity: parsed.a)
258
+ return (parsed.r, parsed.g, parsed.b, parsed.a)
166
259
  }
167
260
 
168
261
  // MARK: - HSL Parser
@@ -171,6 +264,11 @@ enum JSColorParser {
171
264
  /// - hsl(120, 100%, 50%), hsla(120, 100%, 50%, 0.5)
172
265
  /// - hsl(120 100% 50% / 30%), hsla(120 100% 50% / 0.3)
173
266
  private static func parseHSL(_ string: String) -> Color? {
267
+ guard let parsed = parseHSLComponents(string) else { return nil }
268
+ return Color(.sRGB, red: parsed.red, green: parsed.green, blue: parsed.blue, opacity: parsed.alpha)
269
+ }
270
+
271
+ private static func parseHSLComponents(_ string: String) -> (red: Double, green: Double, blue: Double, alpha: Double)? {
174
272
  guard let function = parseFunctionCall(string, allowedNames: ["hsl", "hsla"]) else { return nil }
175
273
 
176
274
  let isHsla = function.name == "hsla"
@@ -182,14 +280,8 @@ enum JSColorParser {
182
280
  }
183
281
  guard let parsed else { return nil }
184
282
 
185
- let h = parsed.h
186
- let s = parsed.s
187
- let l = parsed.l
188
- let a = parsed.a
189
-
190
- // Convert HSL to RGB (HSL != HSB/HSV)
191
- let (r, g, b) = hslToRgb(h: h, s: s, l: l)
192
- return Color(.sRGB, red: r, green: g, blue: b, opacity: a)
283
+ let (r, g, b) = hslToRgb(h: parsed.h, s: parsed.s, l: parsed.l)
284
+ return (r, g, b, parsed.a)
193
285
  }
194
286
 
195
287
  private struct FunctionCall {
@@ -155,6 +155,7 @@ enum StyleConverter {
155
155
 
156
156
  if let color = JSColorParser.parse(js["color"]) {
157
157
  style.color = color
158
+ style.usesPrimaryColorInReducedPresentation = JSColorParser.shouldUsePrimaryColorInReducedPresentation(js["color"])
158
159
  }
159
160
 
160
161
  if let size = JSStyleParser.number(js["fontSize"]) {
@@ -2,6 +2,7 @@ import SwiftUI
2
2
 
3
3
  struct TextStyle {
4
4
  var color: Color = .primary
5
+ var usesPrimaryColorInReducedPresentation = false
5
6
  var fontSize: CGFloat = 17
6
7
  var fontWeight: Font.Weight = .regular
7
8
  var fontFamily: String?
@@ -16,6 +17,18 @@ struct TextStyle {
16
17
 
17
18
  struct TextStyleModifier: ViewModifier {
18
19
  let style: TextStyle
20
+ @Environment(\.voltraEnvironment) private var voltraEnvironment
21
+
22
+ private var resolvedColor: Color {
23
+ if let widget = voltraEnvironment.widget,
24
+ widget.usesReducedBackgroundPresentation,
25
+ style.usesPrimaryColorInReducedPresentation
26
+ {
27
+ return .primary
28
+ }
29
+
30
+ return style.color
31
+ }
19
32
 
20
33
  func body(content: Content) -> some View {
21
34
  content
@@ -28,7 +41,7 @@ struct TextStyleModifier: ViewModifier {
28
41
  : .system(size: style.fontSize, weight: style.fontWeight)
29
42
  )
30
43
  // 2. Color
31
- .foregroundColor(style.color)
44
+ .foregroundColor(resolvedColor)
32
45
  // 3. Layout / Spacing
33
46
  .multilineTextAlignment(style.alignment)
34
47
  .lineLimit(style.lineLimit)
@@ -4,6 +4,7 @@ public struct VoltraGlassContainer: VoltraView {
4
4
  public typealias Parameters = GlassContainerParameters
5
5
 
6
6
  public let element: VoltraElement
7
+ @Environment(\.voltraEnvironment) private var voltraEnvironment
7
8
 
8
9
  public init(_ element: VoltraElement) {
9
10
  self.element = element
@@ -11,7 +12,9 @@ public struct VoltraGlassContainer: VoltraView {
11
12
 
12
13
  public var body: some View {
13
14
  if let children = element.children {
14
- if #available(iOS 26.0, *) {
15
+ if voltraEnvironment.widget?.suppressesDecorativeContainerEffects == true {
16
+ children.applyStyle(element.style)
17
+ } else if #available(iOS 26.0, *) {
15
18
  let spacing = params.spacing ?? 0.0
16
19
  GlassEffectContainer(spacing: CGFloat(spacing)) {
17
20
  children
@@ -21,8 +24,6 @@ public struct VoltraGlassContainer: VoltraView {
21
24
  children
22
25
  }.applyStyle(element.style)
23
26
  }
24
- } else {
25
- EmptyView()
26
27
  }
27
28
  }
28
29
  }
@@ -4,6 +4,7 @@ public struct VoltraLinearGradient: VoltraView {
4
4
  public typealias Parameters = LinearGradientParameters
5
5
 
6
6
  public let element: VoltraElement
7
+ @Environment(\.voltraEnvironment) private var voltraEnvironment
7
8
 
8
9
  public init(_ element: VoltraElement) {
9
10
  self.element = element
@@ -67,20 +68,48 @@ public struct VoltraLinearGradient: VoltraView {
67
68
  return Gradient(colors: [Color.black.opacity(0.25), Color.black.opacity(0.05)])
68
69
  }
69
70
 
71
+ private func isFullBleedWidgetBackgroundCandidate() -> Bool {
72
+ guard let style = element.style, element.children != nil else {
73
+ return false
74
+ }
75
+ if let flex = style["flex"]?.doubleValue, flex > 0 {
76
+ return true
77
+ }
78
+ if let flexGrow = style["flexGrow"]?.doubleValue, flexGrow > 0 {
79
+ return true
80
+ }
81
+ let width = style["width"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
82
+ let height = style["height"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
83
+ return width == "100%" && height == "100%"
84
+ }
85
+
70
86
  public var body: some View {
71
87
  let gradient = buildGradient(params: params)
72
88
  let start = parsePoint(params.startPoint)
73
89
  let end = parsePoint(params.endPoint)
90
+ let anyStyle = element.style?.mapValues { $0.toAny() } ?? [:]
91
+ let (layout, baseDecoration, rendering, text) = StyleConverter.convert(anyStyle)
92
+
93
+ var decoration = baseDecoration
94
+ decoration.backgroundColor = .linearGradient(gradient: gradient, startPoint: start, endPoint: end)
74
95
 
75
- // Note: dither parameter is available in node.parameters["dither"] but SwiftUI's LinearGradient
76
- // doesn't expose dithering control directly. This is handled automatically by the system.
77
- let lg = LinearGradient(gradient: gradient, startPoint: start, endPoint: end)
96
+ if let widget = voltraEnvironment.widget,
97
+ widget.isHomeScreenWidget,
98
+ widget.usesReducedBackgroundPresentation,
99
+ isFullBleedWidgetBackgroundCandidate(),
100
+ let children = element.children
101
+ {
102
+ return AnyView(children.applyStyle(element.style))
103
+ }
78
104
 
79
- // Use ZStack with a Rectangle that fills and is tinted by the gradient, then overlay children.
80
- return ZStack {
81
- Rectangle().fill(lg)
82
- element.children ?? .empty
105
+ if let children = element.children {
106
+ return AnyView(
107
+ children.applyStyle((layout, decoration, rendering, text))
108
+ )
83
109
  }
84
- .applyStyle(element.style)
110
+
111
+ return AnyView(
112
+ Color.clear.applyStyle((layout, decoration, rendering, text))
113
+ )
85
114
  }
86
115
  }
@@ -4,6 +4,7 @@ public struct VoltraText: VoltraView {
4
4
  public typealias Parameters = TextParameters
5
5
 
6
6
  public let element: VoltraElement
7
+ @Environment(\.voltraEnvironment) private var voltraEnvironment
7
8
 
8
9
  public init(_ element: VoltraElement) {
9
10
  self.element = element
@@ -58,13 +59,24 @@ public struct VoltraText: VoltraView {
58
59
  return textStyle.alignment
59
60
  }()
60
61
 
62
+ let resolvedColor: Color = {
63
+ if let widget = voltraEnvironment.widget,
64
+ widget.usesReducedBackgroundPresentation,
65
+ textStyle.usesPrimaryColorInReducedPresentation
66
+ {
67
+ return .primary
68
+ }
69
+
70
+ return textStyle.color
71
+ }()
72
+
61
73
  Text(.init(textContent))
62
74
  .kerning(textStyle.letterSpacing)
63
75
  .underline(textStyle.decoration == .underline || textStyle.decoration == .underlineLineThrough)
64
76
  .strikethrough(textStyle.decoration == .lineThrough || textStyle.decoration == .underlineLineThrough)
65
77
  // These technically work on View, but good to keep close
66
78
  .font(font)
67
- .foregroundColor(textStyle.color)
79
+ .foregroundColor(resolvedColor)
68
80
  .multilineTextAlignment(alignment)
69
81
  .lineSpacing(textStyle.lineSpacing)
70
82
  .voltraIfLet(params.numberOfLines) { view, numberOfLines in
@@ -1,8 +1,42 @@
1
1
  import SwiftUI
2
2
 
3
+ public enum VoltraWidgetRenderingMode {
4
+ case fullColor
5
+ case accented
6
+ case vibrant
7
+ case unknown
8
+ }
9
+
10
+ public struct VoltraWidgetEnvironment {
11
+ public let isHomeScreenWidget: Bool
12
+ public let renderingMode: VoltraWidgetRenderingMode
13
+ public let showsContainerBackground: Bool
14
+
15
+ var usesReducedBackgroundPresentation: Bool {
16
+ renderingMode != .fullColor || !showsContainerBackground
17
+ }
18
+
19
+ var suppressesDecorativeContainerEffects: Bool {
20
+ isHomeScreenWidget && usesReducedBackgroundPresentation
21
+ }
22
+
23
+ public init(
24
+ isHomeScreenWidget: Bool,
25
+ renderingMode: VoltraWidgetRenderingMode,
26
+ showsContainerBackground: Bool
27
+ ) {
28
+ self.isHomeScreenWidget = isHomeScreenWidget
29
+ self.renderingMode = renderingMode
30
+ self.showsContainerBackground = showsContainerBackground
31
+ }
32
+ }
33
+
3
34
  struct VoltraEnvironment {
4
35
  /// Activity ID for Live Activity interactions
5
36
  let activityId: String
37
+
38
+ /// Widget-specific presentation context, when rendering inside WidgetKit.
39
+ let widget: VoltraWidgetEnvironment?
6
40
  }
7
41
 
8
42
  public struct Voltra: View {
@@ -12,28 +46,35 @@ public struct Voltra: View {
12
46
  /// Activity ID for Live Activity interactions
13
47
  public var activityId: String
14
48
 
49
+ /// Widget-specific presentation context, when rendering inside WidgetKit.
50
+ var widget: VoltraWidgetEnvironment?
51
+
15
52
  /// Initialize Voltra
16
53
  ///
17
54
  /// - Parameter root: Pre-parsed root VoltraNode
18
55
  /// - Parameter callback: Handler for element interactions
19
56
  /// - Parameter activityId: Activity ID for Live Activity interactions
20
- public init(root: VoltraNode, activityId: String) {
57
+ /// - Parameter widget: Widget rendering context used to adapt Voltra output for WidgetKit surfaces
58
+ public init(root: VoltraNode, activityId: String, widget: VoltraWidgetEnvironment? = nil) {
21
59
  self.root = root
22
60
  self.activityId = activityId
61
+ self.widget = widget
23
62
  }
24
63
 
25
64
  /// Generated body for SwiftUI
26
65
  public var body: some View {
27
66
  root
28
67
  .environment(\.voltraEnvironment, VoltraEnvironment(
29
- activityId: activityId
68
+ activityId: activityId,
69
+ widget: widget
30
70
  ))
31
71
  }
32
72
  }
33
73
 
34
74
  private struct VoltraEnvironmentKey: EnvironmentKey {
35
75
  static let defaultValue: VoltraEnvironment = .init(
36
- activityId: ""
76
+ activityId: "",
77
+ widget: nil
37
78
  )
38
79
  }
39
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voltra",
3
- "version": "1.4.0-rc.0",
3
+ "version": "1.4.1",
4
4
  "description": "Build Live Activities with JSX in React Native",
5
5
  "main": "build/cjs/index.js",
6
6
  "module": "build/esm/index.js",
@@ -87,15 +87,15 @@
87
87
  "license": "MIT",
88
88
  "homepage": "https://use-voltra.dev",
89
89
  "dependencies": {
90
- "@use-voltra/android": "1.4.0-rc.0",
91
- "@use-voltra/android-client": "1.4.0-rc.0",
92
- "@use-voltra/android-server": "1.4.0-rc.0",
93
- "@use-voltra/core": "1.4.0-rc.0",
94
- "@use-voltra/expo-plugin": "1.4.0-rc.0",
95
- "@use-voltra/ios": "1.4.0-rc.0",
96
- "@use-voltra/ios-client": "1.4.0-rc.0",
97
- "@use-voltra/ios-server": "1.4.0-rc.0",
98
- "@use-voltra/server": "1.4.0-rc.0",
90
+ "@use-voltra/android": "1.4.1",
91
+ "@use-voltra/android-client": "1.4.1",
92
+ "@use-voltra/android-server": "1.4.1",
93
+ "@use-voltra/core": "1.4.1",
94
+ "@use-voltra/expo-plugin": "1.4.1",
95
+ "@use-voltra/ios": "1.4.1",
96
+ "@use-voltra/ios-client": "1.4.1",
97
+ "@use-voltra/ios-server": "1.4.1",
98
+ "@use-voltra/server": "1.4.1",
99
99
  "react-is": "^19.2.0"
100
100
  },
101
101
  "peerDependencies": {