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 +43 -0
- package/android/src/main/java/voltra/glance/renderers/TextAndImageRenderers.kt +6 -1
- package/ios/shared/VoltraNode.swift +0 -2
- package/ios/target/VoltraHomeWidget.swift +29 -3
- package/ios/ui/Layout/FlexContainerHelper.swift +1 -1
- package/ios/ui/Style/CompositeStyle.swift +2 -2
- package/ios/ui/Style/DecorationStyle.swift +38 -2
- package/ios/ui/Style/JSColorParser.swift +102 -10
- package/ios/ui/Style/StyleConverter.swift +1 -0
- package/ios/ui/Style/TextStyle.swift +14 -1
- package/ios/ui/Views/VoltraGlassContainer.swift +4 -3
- package/ios/ui/Views/VoltraLinearGradient.swift +37 -8
- package/ios/ui/Views/VoltraText.swift +13 -1
- package/ios/ui/Voltra.swift +44 -3
- package/package.json +10 -10
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
|
|
@@ -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(
|
|
292
|
-
|
|
293
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
186
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
105
|
+
if let children = element.children {
|
|
106
|
+
return AnyView(
|
|
107
|
+
children.applyStyle((layout, decoration, rendering, text))
|
|
108
|
+
)
|
|
83
109
|
}
|
|
84
|
-
|
|
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(
|
|
79
|
+
.foregroundColor(resolvedColor)
|
|
68
80
|
.multilineTextAlignment(alignment)
|
|
69
81
|
.lineSpacing(textStyle.lineSpacing)
|
|
70
82
|
.voltraIfLet(params.numberOfLines) { view, numberOfLines in
|
package/ios/ui/Voltra.swift
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
91
|
-
"@use-voltra/android-client": "1.4.
|
|
92
|
-
"@use-voltra/android-server": "1.4.
|
|
93
|
-
"@use-voltra/core": "1.4.
|
|
94
|
-
"@use-voltra/expo-plugin": "1.4.
|
|
95
|
-
"@use-voltra/ios": "1.4.
|
|
96
|
-
"@use-voltra/ios-client": "1.4.
|
|
97
|
-
"@use-voltra/ios-server": "1.4.
|
|
98
|
-
"@use-voltra/server": "1.4.
|
|
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": {
|