@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.
- package/build/commonjs/dynamic-widget/api.js +28 -1
- package/build/commonjs/dynamic-widget/api.js.map +1 -1
- package/build/commonjs/index.js +7 -0
- package/build/commonjs/index.js.map +1 -1
- package/build/commonjs/native/NativeVoltra.js.map +1 -1
- package/build/module/dynamic-widget/api.js +26 -0
- package/build/module/dynamic-widget/api.js.map +1 -1
- package/build/module/index.js +1 -1
- package/build/module/index.js.map +1 -1
- package/build/module/native/NativeVoltra.js.map +1 -1
- package/build/typescript/commonjs/dynamic-widget/api.d.ts +12 -0
- package/build/typescript/commonjs/dynamic-widget/api.d.ts.map +1 -1
- package/build/typescript/commonjs/index.d.ts +1 -1
- package/build/typescript/commonjs/index.d.ts.map +1 -1
- package/build/typescript/commonjs/native/NativeVoltra.d.ts +2 -0
- package/build/typescript/commonjs/native/NativeVoltra.d.ts.map +1 -1
- package/build/typescript/module/dynamic-widget/api.d.ts +12 -0
- package/build/typescript/module/dynamic-widget/api.d.ts.map +1 -1
- package/build/typescript/module/index.d.ts +1 -1
- package/build/typescript/module/index.d.ts.map +1 -1
- package/build/typescript/module/native/NativeVoltra.d.ts +2 -0
- package/build/typescript/module/native/NativeVoltra.d.ts.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js +4 -0
- package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js +9 -1
- package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/files/index.js +8 -1
- package/expo-plugin/build/cjs/ios-widget/files/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js +16 -4
- package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/files/localization.js +167 -0
- package/expo-plugin/build/cjs/ios-widget/files/localization.js.map +1 -0
- package/expo-plugin/build/cjs/ios-widget/files/swift.js +68 -7
- package/expo-plugin/build/cjs/ios-widget/files/swift.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/groups.js +4 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/groups.js.map +1 -1
- package/expo-plugin/build/cjs/utils/fileDiscovery.js +7 -4
- package/expo-plugin/build/cjs/utils/fileDiscovery.js.map +1 -1
- package/expo-plugin/build/cjs/validation.js +1 -0
- package/expo-plugin/build/cjs/validation.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js +4 -0
- package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js +9 -1
- package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/files/index.js +8 -1
- package/expo-plugin/build/esm/ios-widget/files/index.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/files/infoPlist.js +16 -4
- package/expo-plugin/build/esm/ios-widget/files/infoPlist.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/files/localization.js +128 -0
- package/expo-plugin/build/esm/ios-widget/files/localization.js.map +1 -0
- package/expo-plugin/build/esm/ios-widget/files/swift.js +69 -8
- package/expo-plugin/build/esm/ios-widget/files/swift.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/groups.js +5 -2
- package/expo-plugin/build/esm/ios-widget/xcode/groups.js.map +1 -1
- package/expo-plugin/build/esm/utils/fileDiscovery.js +6 -3
- package/expo-plugin/build/esm/utils/fileDiscovery.js.map +1 -1
- package/expo-plugin/build/esm/validation.js +2 -1
- package/expo-plugin/build/esm/validation.js.map +1 -1
- package/expo-plugin/build/types/ios-widget/clientRenderedPrerender.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/dynamic-live-activity/swift.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/files/index.d.ts +1 -0
- package/expo-plugin/build/types/ios-widget/files/index.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts +3 -1
- package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/files/localization.d.ts +46 -0
- package/expo-plugin/build/types/ios-widget/files/localization.d.ts.map +1 -0
- package/expo-plugin/build/types/ios-widget/files/swift.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts.map +1 -1
- package/expo-plugin/build/types/types.d.ts +13 -4
- package/expo-plugin/build/types/types.d.ts.map +1 -1
- package/expo-plugin/build/types/utils/fileDiscovery.d.ts +2 -0
- package/expo-plugin/build/types/utils/fileDiscovery.d.ts.map +1 -1
- package/expo-plugin/build/types/validation.d.ts.map +1 -1
- package/expo-plugin/src/ios-widget/clientRenderedPrerender.ts +4 -0
- package/expo-plugin/src/ios-widget/dynamic-live-activity/swift.ts +9 -1
- package/expo-plugin/src/ios-widget/files/index.ts +14 -1
- package/expo-plugin/src/ios-widget/files/infoPlist.ts +23 -4
- package/expo-plugin/src/ios-widget/files/localization.ts +176 -0
- package/expo-plugin/src/ios-widget/files/swift.ts +88 -10
- package/expo-plugin/src/ios-widget/xcode/groups.ts +5 -2
- package/expo-plugin/src/types.ts +13 -3
- package/expo-plugin/src/utils/fileDiscovery.ts +7 -3
- package/expo-plugin/src/validation.ts +2 -0
- package/ios/Package.swift +8 -1
- package/ios/VoltraWidget.podspec +1 -1
- package/ios/app/NativeVoltra.mm +15 -2
- package/ios/app/VoltraModule.swift +11 -0
- package/ios/app/VoltraModuleImpl.swift +14 -0
- package/ios/app/VoltraViewComponentView.mm +2 -2
- package/ios/app/VoltraViewRoot.swift +6 -12
- package/ios/shared/ShortNames.swift +1 -0
- package/ios/shared/VoltraConstants.swift +4 -0
- package/ios/shared/VoltraElement.swift +43 -2
- package/ios/shared/VoltraLocaleEnvironment.swift +185 -0
- package/ios/shared/VoltraModifierDescriptor.swift +33 -0
- package/ios/shared/VoltraWidgetDefaults.swift +24 -0
- package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderer.swift +50 -28
- package/ios/target/VoltraClientWidgetRuntime.swift +12 -1
- package/ios/target/VoltraHomeWidget.swift +10 -15
- package/ios/target/VoltraWidget.swift +26 -18
- package/ios/ui/Extensions/VoltraNode+View.swift +7 -0
- package/ios/ui/Helpers/VoltraDeepLinkResolver+URL.swift +37 -0
- package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +1 -32
- package/ios/ui/Modifiers/BuiltInModifiers.swift +613 -0
- package/ios/ui/Modifiers/VoltraElement+nativeModifiers.swift +53 -0
- package/ios/ui/Modifiers/VoltraModifierRegistry.swift +181 -0
- package/ios/ui/Modifiers/VoltraRootDefaults.swift +100 -0
- package/ios/ui/Style/TextStyle.swift +4 -2
- package/ios/ui/Style/View+applyStyle.swift +1 -1
- package/ios/ui/Views/VoltraText.swift +4 -8
- package/package.json +4 -4
- package/src/dynamic-widget/api.ts +28 -0
- package/src/index.ts +6 -1
- 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
|
+
}
|