@use-voltra/ios-client 2.3.2 → 2.4.0

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.
Files changed (114) hide show
  1. package/build/commonjs/dynamic-widget/api.js +28 -1
  2. package/build/commonjs/dynamic-widget/api.js.map +1 -1
  3. package/build/commonjs/index.js +7 -0
  4. package/build/commonjs/index.js.map +1 -1
  5. package/build/commonjs/native/NativeVoltra.js.map +1 -1
  6. package/build/module/dynamic-widget/api.js +26 -0
  7. package/build/module/dynamic-widget/api.js.map +1 -1
  8. package/build/module/index.js +1 -1
  9. package/build/module/index.js.map +1 -1
  10. package/build/module/native/NativeVoltra.js.map +1 -1
  11. package/build/typescript/commonjs/dynamic-widget/api.d.ts +12 -0
  12. package/build/typescript/commonjs/dynamic-widget/api.d.ts.map +1 -1
  13. package/build/typescript/commonjs/index.d.ts +1 -1
  14. package/build/typescript/commonjs/index.d.ts.map +1 -1
  15. package/build/typescript/commonjs/native/NativeVoltra.d.ts +2 -0
  16. package/build/typescript/commonjs/native/NativeVoltra.d.ts.map +1 -1
  17. package/build/typescript/module/dynamic-widget/api.d.ts +12 -0
  18. package/build/typescript/module/dynamic-widget/api.d.ts.map +1 -1
  19. package/build/typescript/module/index.d.ts +1 -1
  20. package/build/typescript/module/index.d.ts.map +1 -1
  21. package/build/typescript/module/native/NativeVoltra.d.ts +2 -0
  22. package/build/typescript/module/native/NativeVoltra.d.ts.map +1 -1
  23. package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js +4 -0
  24. package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js.map +1 -1
  25. package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js +9 -1
  26. package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js.map +1 -1
  27. package/expo-plugin/build/cjs/ios-widget/files/index.js +8 -1
  28. package/expo-plugin/build/cjs/ios-widget/files/index.js.map +1 -1
  29. package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js +16 -4
  30. package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js.map +1 -1
  31. package/expo-plugin/build/cjs/ios-widget/files/localization.js +167 -0
  32. package/expo-plugin/build/cjs/ios-widget/files/localization.js.map +1 -0
  33. package/expo-plugin/build/cjs/ios-widget/files/swift.js +68 -7
  34. package/expo-plugin/build/cjs/ios-widget/files/swift.js.map +1 -1
  35. package/expo-plugin/build/cjs/ios-widget/xcode/groups.js +4 -1
  36. package/expo-plugin/build/cjs/ios-widget/xcode/groups.js.map +1 -1
  37. package/expo-plugin/build/cjs/utils/fileDiscovery.js +7 -4
  38. package/expo-plugin/build/cjs/utils/fileDiscovery.js.map +1 -1
  39. package/expo-plugin/build/cjs/validation.js +1 -0
  40. package/expo-plugin/build/cjs/validation.js.map +1 -1
  41. package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js +4 -0
  42. package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js.map +1 -1
  43. package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js +9 -1
  44. package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js.map +1 -1
  45. package/expo-plugin/build/esm/ios-widget/files/index.js +8 -1
  46. package/expo-plugin/build/esm/ios-widget/files/index.js.map +1 -1
  47. package/expo-plugin/build/esm/ios-widget/files/infoPlist.js +16 -4
  48. package/expo-plugin/build/esm/ios-widget/files/infoPlist.js.map +1 -1
  49. package/expo-plugin/build/esm/ios-widget/files/localization.js +128 -0
  50. package/expo-plugin/build/esm/ios-widget/files/localization.js.map +1 -0
  51. package/expo-plugin/build/esm/ios-widget/files/swift.js +69 -8
  52. package/expo-plugin/build/esm/ios-widget/files/swift.js.map +1 -1
  53. package/expo-plugin/build/esm/ios-widget/xcode/groups.js +5 -2
  54. package/expo-plugin/build/esm/ios-widget/xcode/groups.js.map +1 -1
  55. package/expo-plugin/build/esm/utils/fileDiscovery.js +6 -3
  56. package/expo-plugin/build/esm/utils/fileDiscovery.js.map +1 -1
  57. package/expo-plugin/build/esm/validation.js +2 -1
  58. package/expo-plugin/build/esm/validation.js.map +1 -1
  59. package/expo-plugin/build/types/ios-widget/clientRenderedPrerender.d.ts.map +1 -1
  60. package/expo-plugin/build/types/ios-widget/dynamic-live-activity/swift.d.ts.map +1 -1
  61. package/expo-plugin/build/types/ios-widget/files/index.d.ts +1 -0
  62. package/expo-plugin/build/types/ios-widget/files/index.d.ts.map +1 -1
  63. package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts +3 -1
  64. package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts.map +1 -1
  65. package/expo-plugin/build/types/ios-widget/files/localization.d.ts +46 -0
  66. package/expo-plugin/build/types/ios-widget/files/localization.d.ts.map +1 -0
  67. package/expo-plugin/build/types/ios-widget/files/swift.d.ts.map +1 -1
  68. package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts.map +1 -1
  69. package/expo-plugin/build/types/types.d.ts +13 -4
  70. package/expo-plugin/build/types/types.d.ts.map +1 -1
  71. package/expo-plugin/build/types/utils/fileDiscovery.d.ts +2 -0
  72. package/expo-plugin/build/types/utils/fileDiscovery.d.ts.map +1 -1
  73. package/expo-plugin/build/types/validation.d.ts.map +1 -1
  74. package/expo-plugin/src/ios-widget/clientRenderedPrerender.ts +4 -0
  75. package/expo-plugin/src/ios-widget/dynamic-live-activity/swift.ts +9 -1
  76. package/expo-plugin/src/ios-widget/files/index.ts +14 -1
  77. package/expo-plugin/src/ios-widget/files/infoPlist.ts +23 -4
  78. package/expo-plugin/src/ios-widget/files/localization.ts +176 -0
  79. package/expo-plugin/src/ios-widget/files/swift.ts +88 -10
  80. package/expo-plugin/src/ios-widget/xcode/groups.ts +5 -2
  81. package/expo-plugin/src/types.ts +13 -3
  82. package/expo-plugin/src/utils/fileDiscovery.ts +7 -3
  83. package/expo-plugin/src/validation.ts +2 -0
  84. package/ios/Package.swift +8 -1
  85. package/ios/VoltraWidget.podspec +1 -1
  86. package/ios/app/NativeVoltra.mm +15 -2
  87. package/ios/app/VoltraModule.swift +11 -0
  88. package/ios/app/VoltraModuleImpl.swift +14 -0
  89. package/ios/app/VoltraViewComponentView.mm +2 -2
  90. package/ios/app/VoltraViewRoot.swift +6 -12
  91. package/ios/shared/ShortNames.swift +1 -0
  92. package/ios/shared/VoltraConstants.swift +4 -0
  93. package/ios/shared/VoltraElement.swift +43 -2
  94. package/ios/shared/VoltraLocaleEnvironment.swift +185 -0
  95. package/ios/shared/VoltraModifierDescriptor.swift +33 -0
  96. package/ios/shared/VoltraWidgetDefaults.swift +24 -0
  97. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderer.swift +50 -28
  98. package/ios/target/VoltraClientWidgetRuntime.swift +12 -1
  99. package/ios/target/VoltraHomeWidget.swift +10 -15
  100. package/ios/target/VoltraWidget.swift +26 -18
  101. package/ios/ui/Extensions/VoltraNode+View.swift +7 -0
  102. package/ios/ui/Helpers/VoltraDeepLinkResolver+URL.swift +37 -0
  103. package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +1 -32
  104. package/ios/ui/Modifiers/BuiltInModifiers.swift +613 -0
  105. package/ios/ui/Modifiers/VoltraElement+nativeModifiers.swift +53 -0
  106. package/ios/ui/Modifiers/VoltraModifierRegistry.swift +181 -0
  107. package/ios/ui/Modifiers/VoltraRootDefaults.swift +100 -0
  108. package/ios/ui/Style/TextStyle.swift +4 -2
  109. package/ios/ui/Style/View+applyStyle.swift +1 -1
  110. package/ios/ui/Views/VoltraText.swift +4 -8
  111. package/package.json +4 -4
  112. package/src/dynamic-widget/api.ts +28 -0
  113. package/src/index.ts +6 -1
  114. package/src/native/NativeVoltra.ts +2 -0
@@ -0,0 +1,613 @@
1
+ import SwiftUI
2
+ import WidgetKit
3
+
4
+ /// The built-in catalog. Each entry mirrors one factory in `packages/ios/src/modifiers`, and the
5
+ /// shared fixture test fails when the two sides drift apart. Modifiers newer than the pod's
6
+ /// minimum iOS version check availability in `body` and leave the content unchanged when missing.
7
+ let builtInModifierDefinitions: [String: VoltraModifierDefinition] = [
8
+ // MARK: Widgets and Live Activities
9
+
10
+ "widgetURL": VoltraModifierDefinition(parameters: ["url"]) { params in
11
+ // Same forms as `deepLinkUrl`: an absolute URL, or a path that gets the app's URL scheme.
12
+ guard let url = try VoltraDeepLinkResolver.resolveUrl(params.requiredString("url")) else {
13
+ throw VoltraModifierError.invalidParameter("url")
14
+ }
15
+ return WidgetURLModifier(url: url)
16
+ },
17
+ "containerBackground": VoltraModifierDefinition(parameters: ["color"]) { params in
18
+ try ContainerBackgroundModifier(color: params.requiredColor("color"))
19
+ },
20
+ "widgetAccentable": VoltraModifierDefinition(parameters: ["accentable"]) { params in
21
+ try WidgetAccentableModifier(accentable: params.optionalBool("accentable") ?? true)
22
+ },
23
+ "privacySensitive": VoltraModifierDefinition(parameters: ["sensitive"]) { params in
24
+ try PrivacySensitiveModifier(sensitive: params.optionalBool("sensitive") ?? true)
25
+ },
26
+ "redacted": VoltraModifierDefinition(parameters: ["reason"]) { params in
27
+ try RedactedModifier(reason: params.requiredEnum("reason"))
28
+ },
29
+ "unredacted": VoltraModifierDefinition(parameters: []) { _ in
30
+ UnredactedModifier()
31
+ },
32
+ "invalidatableContent": VoltraModifierDefinition(parameters: ["invalidatable"]) { params in
33
+ try InvalidatableContentModifier(invalidatable: params.optionalBool("invalidatable") ?? true)
34
+ },
35
+ "activityBackgroundTint": VoltraModifierDefinition(parameters: ["color"]) { params in
36
+ try ActivityBackgroundTintModifier(color: params.optionalColor("color"))
37
+ },
38
+ "activitySystemActionForegroundColor": VoltraModifierDefinition(parameters: ["color"]) { params in
39
+ try ActivitySystemActionForegroundColorModifier(color: params.optionalColor("color"))
40
+ },
41
+
42
+ // MARK: Transitions and animation
43
+
44
+ "contentTransition": VoltraModifierDefinition(parameters: ["transition", "countsDown"]) { params in
45
+ try ContentTransitionModifier(kind: params.requiredEnum("transition"), countsDown: params.optionalBool("countsDown") ?? false)
46
+ },
47
+ "transition": VoltraModifierDefinition(parameters: ["transition", "edge"]) { params in
48
+ try TransitionModifier(kind: params.requiredEnum("transition"), edge: params.optionalEnum("edge") ?? .bottom)
49
+ },
50
+ "animation": VoltraModifierDefinition(parameters: ["curve", "value", "duration"]) { params in
51
+ guard let value = params["value"], value is String || value is NSNumber else {
52
+ throw VoltraModifierError.missingParameter("value")
53
+ }
54
+ return try AnimationModifier(
55
+ curve: params.requiredEnum("curve"),
56
+ duration: params.optionalNumber("duration").map(Double.init),
57
+ value: String(describing: value)
58
+ )
59
+ },
60
+ "symbolEffect": VoltraModifierDefinition(parameters: ["effect"]) { params in
61
+ try SymbolEffectModifier(effect: params.requiredEnum("effect"))
62
+ },
63
+
64
+ // MARK: Visual effects
65
+
66
+ "clipShape": VoltraModifierDefinition(parameters: ["shape", "cornerRadius", "cornerStyle"]) { params in
67
+ try ClipShapeModifier(params)
68
+ },
69
+ "blur": VoltraModifierDefinition(parameters: ["radius", "opaque"]) { params in
70
+ try BlurModifier(radius: params.requiredNumber("radius"), opaque: params.optionalBool("opaque") ?? false)
71
+ },
72
+ "grayscale": VoltraModifierDefinition(parameters: ["amount"]) { params in
73
+ try ColorAdjustmentModifier(kind: .grayscale, amount: Double(params.requiredNumber("amount")))
74
+ },
75
+ "saturation": VoltraModifierDefinition(parameters: ["amount"]) { params in
76
+ try ColorAdjustmentModifier(kind: .saturation, amount: Double(params.requiredNumber("amount")))
77
+ },
78
+ "brightness": VoltraModifierDefinition(parameters: ["amount"]) { params in
79
+ try ColorAdjustmentModifier(kind: .brightness, amount: Double(params.requiredNumber("amount")))
80
+ },
81
+ "contrast": VoltraModifierDefinition(parameters: ["amount"]) { params in
82
+ try ColorAdjustmentModifier(kind: .contrast, amount: Double(params.requiredNumber("amount")))
83
+ },
84
+ "blendMode": VoltraModifierDefinition(parameters: ["mode"]) { params in
85
+ try BlendModeModifier(mode: params.requiredEnum("mode", as: BlendModeName.self).blendMode)
86
+ },
87
+
88
+ // MARK: Geometry and layout
89
+
90
+ "rotationEffect": VoltraModifierDefinition(parameters: ["degrees"]) { params in
91
+ try RotationEffectModifier(degrees: Double(params.requiredNumber("degrees")))
92
+ },
93
+ "scaleEffect": VoltraModifierDefinition(parameters: ["x", "y"]) { params in
94
+ try ScaleEffectModifier(x: params.optionalNumber("x") ?? 1, y: params.optionalNumber("y") ?? 1)
95
+ },
96
+ "offset": VoltraModifierDefinition(parameters: ["x", "y"]) { params in
97
+ try OffsetModifier(x: params.optionalNumber("x") ?? 0, y: params.optionalNumber("y") ?? 0)
98
+ },
99
+ "fixedSize": VoltraModifierDefinition(parameters: ["horizontal", "vertical"]) { params in
100
+ try FixedSizeModifier(horizontal: params.optionalBool("horizontal") ?? true, vertical: params.optionalBool("vertical") ?? true)
101
+ },
102
+ "layoutPriority": VoltraModifierDefinition(parameters: ["priority"]) { params in
103
+ try LayoutPriorityModifier(priority: Double(params.requiredNumber("priority")))
104
+ },
105
+ "containerRelativeFrame": VoltraModifierDefinition(parameters: ["axes"]) { params in
106
+ try ContainerRelativeFrameModifier(axes: params.requiredEnum("axes"))
107
+ },
108
+ "dynamicTypeSize": VoltraModifierDefinition(parameters: ["size"]) { params in
109
+ try DynamicTypeSizeModifier(size: params.requiredEnum("size", as: DynamicTypeSizeName.self).size)
110
+ },
111
+
112
+ // MARK: Text
113
+
114
+ "minimumScaleFactor": VoltraModifierDefinition(parameters: ["factor"]) { params in
115
+ try MinimumScaleFactorModifier(factor: params.requiredNumber("factor"))
116
+ },
117
+ "truncationMode": VoltraModifierDefinition(parameters: ["mode"]) { params in
118
+ try TruncationModeModifier(mode: params.requiredEnum("mode", as: TruncationModeName.self).mode)
119
+ },
120
+ "multilineTextAlignment": VoltraModifierDefinition(parameters: ["alignment"]) { params in
121
+ try MultilineTextAlignmentModifier(alignment: params.requiredEnum("alignment", as: TextAlignmentName.self).alignment)
122
+ },
123
+ "monospacedDigit": VoltraModifierDefinition(parameters: []) { _ in
124
+ MonospacedDigitModifier()
125
+ },
126
+ ]
127
+
128
+ // MARK: - Widgets and Live Activities
129
+
130
+ struct WidgetURLModifier: ViewModifier {
131
+ let url: URL
132
+
133
+ func body(content: Content) -> some View {
134
+ content.widgetURL(url)
135
+ }
136
+ }
137
+
138
+ struct ContainerBackgroundModifier: ViewModifier {
139
+ let color: Color
140
+
141
+ func body(content: Content) -> some View {
142
+ if #available(iOS 17.0, macOS 14.0, *) {
143
+ content.containerBackground(color, for: .widget)
144
+ } else {
145
+ content
146
+ }
147
+ }
148
+ }
149
+
150
+ struct WidgetAccentableModifier: ViewModifier {
151
+ let accentable: Bool
152
+
153
+ func body(content: Content) -> some View {
154
+ content.widgetAccentable(accentable)
155
+ }
156
+ }
157
+
158
+ struct PrivacySensitiveModifier: ViewModifier {
159
+ let sensitive: Bool
160
+
161
+ func body(content: Content) -> some View {
162
+ content.privacySensitive(sensitive)
163
+ }
164
+ }
165
+
166
+ struct RedactedModifier: ViewModifier {
167
+ enum Reason: String {
168
+ case placeholder, privacy, invalidated
169
+ }
170
+
171
+ let reason: Reason
172
+
173
+ func body(content: Content) -> some View {
174
+ switch reason {
175
+ case .placeholder:
176
+ content.redacted(reason: .placeholder)
177
+ case .privacy:
178
+ content.redacted(reason: .privacy)
179
+ case .invalidated:
180
+ if #available(iOS 17.0, macOS 14.0, *) {
181
+ content.redacted(reason: .invalidated)
182
+ } else {
183
+ content
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ struct UnredactedModifier: ViewModifier {
190
+ func body(content: Content) -> some View {
191
+ content.unredacted()
192
+ }
193
+ }
194
+
195
+ struct InvalidatableContentModifier: ViewModifier {
196
+ let invalidatable: Bool
197
+
198
+ func body(content: Content) -> some View {
199
+ if #available(iOS 17.0, macOS 14.0, *) {
200
+ content.invalidatableContent(invalidatable)
201
+ } else {
202
+ content
203
+ }
204
+ }
205
+ }
206
+
207
+ struct ActivityBackgroundTintModifier: ViewModifier {
208
+ let color: Color?
209
+
210
+ func body(content: Content) -> some View {
211
+ #if os(iOS)
212
+ content.activityBackgroundTint(color)
213
+ #else
214
+ content
215
+ #endif
216
+ }
217
+ }
218
+
219
+ struct ActivitySystemActionForegroundColorModifier: ViewModifier {
220
+ let color: Color?
221
+
222
+ func body(content: Content) -> some View {
223
+ #if os(iOS)
224
+ content.activitySystemActionForegroundColor(color)
225
+ #else
226
+ content
227
+ #endif
228
+ }
229
+ }
230
+
231
+ // MARK: - Transitions and animation
232
+
233
+ struct ContentTransitionModifier: ViewModifier {
234
+ enum Kind: String {
235
+ case identity, opacity, interpolate, numericText, symbolEffect
236
+ }
237
+
238
+ let kind: Kind
239
+ let countsDown: Bool
240
+
241
+ func body(content: Content) -> some View {
242
+ switch kind {
243
+ case .identity:
244
+ content.contentTransition(.identity)
245
+ case .opacity:
246
+ content.contentTransition(.opacity)
247
+ case .interpolate:
248
+ content.contentTransition(.interpolate)
249
+ case .numericText:
250
+ content.contentTransition(.numericText(countsDown: countsDown))
251
+ case .symbolEffect:
252
+ if #available(iOS 17.0, macOS 14.0, *) {
253
+ content.contentTransition(.symbolEffect)
254
+ } else {
255
+ content
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ struct TransitionModifier: ViewModifier {
262
+ enum Kind: String {
263
+ case identity, opacity, scale, slide, push, move
264
+ }
265
+
266
+ enum EdgeName: String {
267
+ case top, bottom, leading, trailing
268
+
269
+ var edge: Edge {
270
+ switch self {
271
+ case .top: .top
272
+ case .bottom: .bottom
273
+ case .leading: .leading
274
+ case .trailing: .trailing
275
+ }
276
+ }
277
+ }
278
+
279
+ let kind: Kind
280
+ let edge: EdgeName
281
+
282
+ private var transition: AnyTransition {
283
+ switch kind {
284
+ case .identity: .identity
285
+ case .opacity: .opacity
286
+ case .scale: .scale
287
+ case .slide: .slide
288
+ case .push: .push(from: edge.edge)
289
+ case .move: .move(edge: edge.edge)
290
+ }
291
+ }
292
+
293
+ func body(content: Content) -> some View {
294
+ content.transition(transition)
295
+ }
296
+ }
297
+
298
+ struct AnimationModifier: ViewModifier {
299
+ enum Curve: String {
300
+ case `default`, linear, easeIn, easeOut, easeInOut, spring, bouncy, smooth, snappy
301
+ }
302
+
303
+ let curve: Curve
304
+ let duration: Double?
305
+ /// The JSON value the animation keys on, compared as text.
306
+ let value: String
307
+
308
+ private var animation: Animation {
309
+ let duration = duration ?? 0.35
310
+ switch curve {
311
+ case .default: return .default
312
+ case .linear: return .linear(duration: duration)
313
+ case .easeIn: return .easeIn(duration: duration)
314
+ case .easeOut: return .easeOut(duration: duration)
315
+ case .easeInOut: return .easeInOut(duration: duration)
316
+ case .spring: return .spring()
317
+ case .bouncy, .smooth, .snappy:
318
+ guard #available(iOS 17.0, macOS 14.0, *) else { return .default }
319
+ switch curve {
320
+ case .bouncy: return .bouncy(duration: duration)
321
+ case .smooth: return .smooth(duration: duration)
322
+ default: return .snappy(duration: duration)
323
+ }
324
+ }
325
+ }
326
+
327
+ func body(content: Content) -> some View {
328
+ content.animation(animation, value: value)
329
+ }
330
+ }
331
+
332
+ struct SymbolEffectModifier: ViewModifier {
333
+ enum Effect: String {
334
+ case pulse, variableColor, breathe, rotate, wiggle
335
+ }
336
+
337
+ let effect: Effect
338
+
339
+ func body(content: Content) -> some View {
340
+ if #available(iOS 18.0, macOS 15.0, *) {
341
+ switch effect {
342
+ case .pulse: content.symbolEffect(.pulse)
343
+ case .variableColor: content.symbolEffect(.variableColor)
344
+ case .breathe: content.symbolEffect(.breathe)
345
+ case .rotate: content.symbolEffect(.rotate)
346
+ case .wiggle: content.symbolEffect(.wiggle)
347
+ }
348
+ } else if #available(iOS 17.0, macOS 14.0, *) {
349
+ switch effect {
350
+ case .pulse: content.symbolEffect(.pulse)
351
+ case .variableColor: content.symbolEffect(.variableColor)
352
+ case .breathe, .rotate, .wiggle: content
353
+ }
354
+ } else {
355
+ content
356
+ }
357
+ }
358
+ }
359
+
360
+ // MARK: - Visual effects
361
+
362
+ struct ClipShapeModifier: ViewModifier {
363
+ enum Shape: String {
364
+ case rectangle, roundedRectangle, circle, capsule, ellipse
365
+ }
366
+
367
+ let shape: Shape
368
+ let cornerRadius: CGFloat
369
+ let cornerStyle: RoundedCornerStyle
370
+
371
+ init(_ params: [String: Any]) throws {
372
+ shape = try params.requiredEnum("shape")
373
+ cornerRadius = try params.optionalNumber("cornerRadius") ?? 0
374
+ switch try params.optionalString("cornerStyle") {
375
+ case nil, "circular": cornerStyle = .circular
376
+ case "continuous": cornerStyle = .continuous
377
+ default: throw VoltraModifierError.invalidParameter("cornerStyle")
378
+ }
379
+ }
380
+
381
+ func body(content: Content) -> some View {
382
+ switch shape {
383
+ case .rectangle:
384
+ content.clipShape(Rectangle())
385
+ case .roundedRectangle:
386
+ content.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: cornerStyle))
387
+ case .circle:
388
+ content.clipShape(Circle())
389
+ case .capsule:
390
+ content.clipShape(Capsule(style: cornerStyle))
391
+ case .ellipse:
392
+ content.clipShape(Ellipse())
393
+ }
394
+ }
395
+ }
396
+
397
+ struct BlurModifier: ViewModifier {
398
+ let radius: CGFloat
399
+ let opaque: Bool
400
+
401
+ func body(content: Content) -> some View {
402
+ content.blur(radius: radius, opaque: opaque)
403
+ }
404
+ }
405
+
406
+ struct ColorAdjustmentModifier: ViewModifier {
407
+ enum Kind {
408
+ case grayscale, saturation, brightness, contrast
409
+ }
410
+
411
+ let kind: Kind
412
+ let amount: Double
413
+
414
+ func body(content: Content) -> some View {
415
+ switch kind {
416
+ case .grayscale: content.grayscale(amount)
417
+ case .saturation: content.saturation(amount)
418
+ case .brightness: content.brightness(amount)
419
+ case .contrast: content.contrast(amount)
420
+ }
421
+ }
422
+ }
423
+
424
+ enum BlendModeName: String {
425
+ case normal, multiply, screen, overlay, darken, lighten, colorDodge, colorBurn, softLight, hardLight
426
+ case difference, exclusion, hue, saturation, color, luminosity, sourceAtop, destinationOver
427
+ case destinationOut, plusDarker, plusLighter
428
+
429
+ var blendMode: BlendMode {
430
+ switch self {
431
+ case .normal: .normal
432
+ case .multiply: .multiply
433
+ case .screen: .screen
434
+ case .overlay: .overlay
435
+ case .darken: .darken
436
+ case .lighten: .lighten
437
+ case .colorDodge: .colorDodge
438
+ case .colorBurn: .colorBurn
439
+ case .softLight: .softLight
440
+ case .hardLight: .hardLight
441
+ case .difference: .difference
442
+ case .exclusion: .exclusion
443
+ case .hue: .hue
444
+ case .saturation: .saturation
445
+ case .color: .color
446
+ case .luminosity: .luminosity
447
+ case .sourceAtop: .sourceAtop
448
+ case .destinationOver: .destinationOver
449
+ case .destinationOut: .destinationOut
450
+ case .plusDarker: .plusDarker
451
+ case .plusLighter: .plusLighter
452
+ }
453
+ }
454
+ }
455
+
456
+ struct BlendModeModifier: ViewModifier {
457
+ let mode: BlendMode
458
+
459
+ func body(content: Content) -> some View {
460
+ content.blendMode(mode)
461
+ }
462
+ }
463
+
464
+ // MARK: - Geometry and layout
465
+
466
+ struct RotationEffectModifier: ViewModifier {
467
+ let degrees: Double
468
+
469
+ func body(content: Content) -> some View {
470
+ content.rotationEffect(.degrees(degrees))
471
+ }
472
+ }
473
+
474
+ struct ScaleEffectModifier: ViewModifier {
475
+ let x: CGFloat
476
+ let y: CGFloat
477
+
478
+ func body(content: Content) -> some View {
479
+ content.scaleEffect(x: x, y: y)
480
+ }
481
+ }
482
+
483
+ struct OffsetModifier: ViewModifier {
484
+ let x: CGFloat
485
+ let y: CGFloat
486
+
487
+ func body(content: Content) -> some View {
488
+ content.offset(x: x, y: y)
489
+ }
490
+ }
491
+
492
+ struct FixedSizeModifier: ViewModifier {
493
+ let horizontal: Bool
494
+ let vertical: Bool
495
+
496
+ func body(content: Content) -> some View {
497
+ content.fixedSize(horizontal: horizontal, vertical: vertical)
498
+ }
499
+ }
500
+
501
+ struct LayoutPriorityModifier: ViewModifier {
502
+ let priority: Double
503
+
504
+ func body(content: Content) -> some View {
505
+ content.layoutPriority(priority)
506
+ }
507
+ }
508
+
509
+ struct ContainerRelativeFrameModifier: ViewModifier {
510
+ enum Axes: String {
511
+ case horizontal, vertical, both
512
+ }
513
+
514
+ let axes: Axes
515
+
516
+ func body(content: Content) -> some View {
517
+ if #available(iOS 17.0, macOS 14.0, *) {
518
+ switch axes {
519
+ case .horizontal: content.containerRelativeFrame(.horizontal)
520
+ case .vertical: content.containerRelativeFrame(.vertical)
521
+ case .both: content.containerRelativeFrame([.horizontal, .vertical])
522
+ }
523
+ } else {
524
+ content
525
+ }
526
+ }
527
+ }
528
+
529
+ enum DynamicTypeSizeName: String {
530
+ case xSmall, small, medium, large, xLarge, xxLarge, xxxLarge
531
+ case accessibility1, accessibility2, accessibility3, accessibility4, accessibility5
532
+
533
+ var size: DynamicTypeSize {
534
+ switch self {
535
+ case .xSmall: .xSmall
536
+ case .small: .small
537
+ case .medium: .medium
538
+ case .large: .large
539
+ case .xLarge: .xLarge
540
+ case .xxLarge: .xxLarge
541
+ case .xxxLarge: .xxxLarge
542
+ case .accessibility1: .accessibility1
543
+ case .accessibility2: .accessibility2
544
+ case .accessibility3: .accessibility3
545
+ case .accessibility4: .accessibility4
546
+ case .accessibility5: .accessibility5
547
+ }
548
+ }
549
+ }
550
+
551
+ struct DynamicTypeSizeModifier: ViewModifier {
552
+ let size: DynamicTypeSize
553
+
554
+ func body(content: Content) -> some View {
555
+ content.dynamicTypeSize(size)
556
+ }
557
+ }
558
+
559
+ // MARK: - Text
560
+
561
+ struct MinimumScaleFactorModifier: ViewModifier {
562
+ let factor: CGFloat
563
+
564
+ func body(content: Content) -> some View {
565
+ content.minimumScaleFactor(factor)
566
+ }
567
+ }
568
+
569
+ enum TruncationModeName: String {
570
+ case head, middle, tail
571
+
572
+ var mode: Text.TruncationMode {
573
+ switch self {
574
+ case .head: .head
575
+ case .middle: .middle
576
+ case .tail: .tail
577
+ }
578
+ }
579
+ }
580
+
581
+ struct TruncationModeModifier: ViewModifier {
582
+ let mode: Text.TruncationMode
583
+
584
+ func body(content: Content) -> some View {
585
+ content.truncationMode(mode)
586
+ }
587
+ }
588
+
589
+ enum TextAlignmentName: String {
590
+ case leading, center, trailing
591
+
592
+ var alignment: TextAlignment {
593
+ switch self {
594
+ case .leading: .leading
595
+ case .center: .center
596
+ case .trailing: .trailing
597
+ }
598
+ }
599
+ }
600
+
601
+ struct MultilineTextAlignmentModifier: ViewModifier {
602
+ let alignment: TextAlignment
603
+
604
+ func body(content: Content) -> some View {
605
+ content.multilineTextAlignment(alignment)
606
+ }
607
+ }
608
+
609
+ struct MonospacedDigitModifier: ViewModifier {
610
+ func body(content: Content) -> some View {
611
+ content.monospacedDigit()
612
+ }
613
+ }
@@ -0,0 +1,53 @@
1
+ #if canImport(VoltraSharedCore)
2
+ import VoltraSharedCore
3
+ #endif
4
+
5
+ extension VoltraNode {
6
+ /// Which of the given native modifier types this node or anything it renders carries in a
7
+ /// descriptor that decodes, found in one walk that stops once all are found.
8
+ func nativeModifierTypes(among types: Set<String>) -> Set<String> {
9
+ var found: Set<String> = []
10
+ collectNativeModifierTypes(among: types, into: &found)
11
+ return found
12
+ }
13
+
14
+ private func collectNativeModifierTypes(among types: Set<String>, into found: inout Set<String>) {
15
+ guard found.count < types.count else { return }
16
+ switch self {
17
+ case let .element(element):
18
+ for descriptor in element.nativeModifiers
19
+ where types.contains(descriptor.type) && !found.contains(descriptor.type) && VoltraModifierRegistry.canApply(descriptor)
20
+ {
21
+ found.insert(descriptor.type)
22
+ }
23
+ element.children?.collectNativeModifierTypes(among: types, into: &found)
24
+ for (name, node) in element.propNodes where !element.skipsPropNode(name) {
25
+ node.collectNativeModifierTypes(among: types, into: &found)
26
+ }
27
+ case let .array(nodes):
28
+ for node in nodes {
29
+ node.collectNativeModifierTypes(among: types, into: &found)
30
+ }
31
+ case .text, .empty:
32
+ return
33
+ }
34
+ }
35
+
36
+ /// Whether this node or anything it renders carries a native modifier of the given type that
37
+ /// decodes. It looks through children and through nodes stored in component props, such as a
38
+ /// Gauge or Button label. A descriptor that is skipped at render time, such as
39
+ /// `containerBackground` with an unparsable color, must not make the host drop the default it
40
+ /// replaces.
41
+ func containsNativeModifier(_ type: String) -> Bool {
42
+ !nativeModifierTypes(among: [type]).isEmpty
43
+ }
44
+ }
45
+
46
+ private extension VoltraElement {
47
+ /// An Image draws its `fallback` only when its `source` fails to load. When there is a source,
48
+ /// the fallback is assumed not to be drawn, so a modifier inside it does not make the host drop a
49
+ /// default the widget then lacks.
50
+ func skipsPropNode(_ name: String) -> Bool {
51
+ type == "Image" && name == "fallback" && props?["source"] != nil
52
+ }
53
+ }