@thelacanians/vue-native-cli 0.4.2 → 0.4.4

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 (172) hide show
  1. package/dist/cli.js +43 -23
  2. package/native/android/README.md +205 -0
  3. package/native/android/VueNativeCore/build.gradle.kts +100 -0
  4. package/native/android/VueNativeCore/consumer-rules.pro +12 -0
  5. package/native/android/VueNativeCore/proguard-rules.pro +33 -0
  6. package/native/android/VueNativeCore/src/main/AndroidManifest.xml +17 -0
  7. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/ErrorOverlayView.kt +94 -0
  8. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/HotReloadManager.kt +105 -0
  9. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/JSPolyfills.kt +652 -0
  10. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/JSRuntime.kt +207 -0
  11. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +417 -0
  12. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/ComponentRegistry.kt +76 -0
  13. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VActionSheetFactory.kt +78 -0
  14. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VActivityIndicatorFactory.kt +46 -0
  15. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VAlertDialogFactory.kt +84 -0
  16. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VButtonFactory.kt +73 -0
  17. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VCheckboxFactory.kt +93 -0
  18. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VDropdownFactory.kt +125 -0
  19. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VImageFactory.kt +75 -0
  20. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VInputFactory.kt +210 -0
  21. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VKeyboardAvoidingFactory.kt +31 -0
  22. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VListFactory.kt +183 -0
  23. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VModalFactory.kt +105 -0
  24. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VPickerFactory.kt +57 -0
  25. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VPressableFactory.kt +109 -0
  26. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VProgressBarFactory.kt +43 -0
  27. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VRadioFactory.kt +103 -0
  28. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VRefreshControlFactory.kt +73 -0
  29. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VRootFactory.kt +39 -0
  30. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSafeAreaFactory.kt +48 -0
  31. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VScrollViewFactory.kt +105 -0
  32. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSectionListFactory.kt +144 -0
  33. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSegmentedControlFactory.kt +77 -0
  34. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSliderFactory.kt +74 -0
  35. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VStatusBarFactory.kt +52 -0
  36. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSwitchFactory.kt +62 -0
  37. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VTextFactory.kt +53 -0
  38. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VVideoFactory.kt +191 -0
  39. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +48 -0
  40. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VWebViewFactory.kt +90 -0
  41. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/NativeComponentFactory.kt +40 -0
  42. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/VTextNodeView.kt +23 -0
  43. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/GestureHelper.kt +16 -0
  44. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/TouchableView.kt +105 -0
  45. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AnimationModule.kt +292 -0
  46. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AppStateModule.kt +41 -0
  47. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AsyncStorageModule.kt +59 -0
  48. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AudioModule.kt +331 -0
  49. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BackgroundTaskModule.kt +166 -0
  50. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BiometryModule.kt +56 -0
  51. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BluetoothModule.kt +302 -0
  52. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/CalendarModule.kt +198 -0
  53. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/CameraModule.kt +64 -0
  54. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ClipboardModule.kt +36 -0
  55. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ContactsModule.kt +288 -0
  56. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/DatabaseModule.kt +229 -0
  57. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/DeviceInfoModule.kt +39 -0
  58. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/FileSystemModule.kt +193 -0
  59. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeolocationModule.kt +68 -0
  60. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/HapticsModule.kt +61 -0
  61. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/HttpModule.kt +111 -0
  62. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/IAPModule.kt +302 -0
  63. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/KeyboardModule.kt +26 -0
  64. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/LinkingModule.kt +43 -0
  65. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModule.kt +27 -0
  66. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModuleRegistry.kt +92 -0
  67. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NetworkModule.kt +75 -0
  68. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NotificationsModule.kt +181 -0
  69. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/OTAModule.kt +255 -0
  70. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/PerformanceModule.kt +147 -0
  71. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/PermissionsModule.kt +126 -0
  72. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SecureStorageModule.kt +51 -0
  73. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SensorsModule.kt +134 -0
  74. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ShareModule.kt +36 -0
  75. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SocialAuthModule.kt +160 -0
  76. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/WebSocketModule.kt +155 -0
  77. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Styling/StyleEngine.kt +802 -0
  78. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Tags.kt +43 -0
  79. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/VueNativeActivity.kt +169 -0
  80. package/native/android/VueNativeCore/src/main/res/values/ids.xml +8 -0
  81. package/native/android/app/build.gradle.kts +45 -0
  82. package/native/android/app/proguard-rules.pro +5 -0
  83. package/native/android/app/src/main/AndroidManifest.xml +25 -0
  84. package/native/android/app/src/main/assets/.gitkeep +0 -0
  85. package/native/android/app/src/main/kotlin/com/vuenative/example/counter/MainActivity.kt +14 -0
  86. package/native/android/app/src/main/res/layout/activity_main.xml +6 -0
  87. package/native/android/app/src/main/res/values/strings.xml +3 -0
  88. package/native/android/app/src/main/res/values/themes.xml +9 -0
  89. package/native/android/app/src/main/res/xml/network_security_config.xml +8 -0
  90. package/native/android/build.gradle.kts +6 -0
  91. package/native/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  92. package/native/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  93. package/native/android/gradle.properties +4 -0
  94. package/native/android/gradlew +87 -0
  95. package/native/android/gradlew.bat +48 -0
  96. package/native/android/settings.gradle.kts +20 -0
  97. package/native/ios/VueNativeCore/Package.resolved +23 -0
  98. package/native/ios/VueNativeCore/Package.swift +32 -0
  99. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/CertificatePinning.swift +132 -0
  100. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/ErrorOverlayView.swift +92 -0
  101. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/HotReloadManager.swift +147 -0
  102. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/JSPolyfills.swift +711 -0
  103. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/JSRuntime.swift +421 -0
  104. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +891 -0
  105. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/VueNativeViewController.swift +88 -0
  106. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/ComponentRegistry.swift +193 -0
  107. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VActionSheetFactory.swift +91 -0
  108. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VActivityIndicatorFactory.swift +74 -0
  109. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VAlertDialogFactory.swift +150 -0
  110. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VButtonFactory.swift +93 -0
  111. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VCheckboxFactory.swift +114 -0
  112. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VDropdownFactory.swift +112 -0
  113. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VImageFactory.swift +172 -0
  114. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VInputFactory.swift +357 -0
  115. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VKeyboardAvoidingFactory.swift +99 -0
  116. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VListFactory.swift +250 -0
  117. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VModalFactory.swift +112 -0
  118. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VPickerFactory.swift +96 -0
  119. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VPressableFactory.swift +168 -0
  120. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VProgressBarFactory.swift +39 -0
  121. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VRadioFactory.swift +167 -0
  122. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VRefreshControlFactory.swift +153 -0
  123. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSafeAreaFactory.swift +56 -0
  124. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VScrollViewFactory.swift +240 -0
  125. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSectionListFactory.swift +248 -0
  126. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSegmentedControlFactory.swift +73 -0
  127. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSliderFactory.swift +63 -0
  128. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VStatusBarFactory.swift +50 -0
  129. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSwitchFactory.swift +108 -0
  130. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VTextFactory.swift +290 -0
  131. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VVideoFactory.swift +246 -0
  132. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VViewFactory.swift +157 -0
  133. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VWebViewFactory.swift +172 -0
  134. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/NativeComponentFactory.swift +53 -0
  135. package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/GestureWrapper.swift +107 -0
  136. package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/TouchableView.swift +136 -0
  137. package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/UIColor+Hex.swift +80 -0
  138. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AnimationModule.swift +291 -0
  139. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AppStateModule.swift +65 -0
  140. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AsyncStorageModule.swift +68 -0
  141. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AudioModule.swift +366 -0
  142. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/BackgroundTaskModule.swift +135 -0
  143. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/BiometryModule.swift +61 -0
  144. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/BluetoothModule.swift +387 -0
  145. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/CalendarModule.swift +161 -0
  146. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/CameraModule.swift +318 -0
  147. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/ClipboardModule.swift +33 -0
  148. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/ContactsModule.swift +173 -0
  149. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/DatabaseModule.swift +259 -0
  150. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/DeviceInfoModule.swift +34 -0
  151. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/FileSystemModule.swift +233 -0
  152. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/GeolocationModule.swift +147 -0
  153. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/HapticsModule.swift +50 -0
  154. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/IAPModule.swift +194 -0
  155. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/KeyboardModule.swift +31 -0
  156. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/LinkingModule.swift +42 -0
  157. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModule.swift +28 -0
  158. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModuleRegistry.swift +78 -0
  159. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NetworkModule.swift +62 -0
  160. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NotificationsModule.swift +215 -0
  161. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/OTAModule.swift +281 -0
  162. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/PerformanceModule.swift +138 -0
  163. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/PermissionsModule.swift +190 -0
  164. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/SecureStorageModule.swift +118 -0
  165. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/SensorsModule.swift +103 -0
  166. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/ShareModule.swift +49 -0
  167. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/SocialAuthModule.swift +240 -0
  168. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/WebSocketModule.swift +213 -0
  169. package/native/ios/VueNativeCore/Sources/VueNativeCore/Resources/vue-native-placeholder.js +8 -0
  170. package/native/ios/VueNativeCore/Sources/VueNativeCore/Styling/StyleEngine.swift +885 -0
  171. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/JSRuntimeTests.swift +362 -0
  172. package/package.json +3 -2
@@ -0,0 +1,885 @@
1
+ #if canImport(UIKit)
2
+ import UIKit
3
+ import FlexLayout
4
+ import ObjectiveC
5
+
6
+ /// Static class that applies style properties to UIViews via FlexLayout (Yoga).
7
+ /// Handles both Yoga layout properties (flex, padding, margin, etc.) and
8
+ /// UIView visual properties (backgroundColor, borderRadius, etc.).
9
+ ///
10
+ /// Supports point values, percentage values, and auto for dimensions.
11
+ @MainActor
12
+ enum StyleEngine {
13
+
14
+ // MARK: - Public API
15
+
16
+ /// Apply a batch of style properties to a view.
17
+ static func applyStyles(_ styles: [String: Any], to view: UIView) {
18
+ for (key, value) in styles {
19
+ apply(key: key, value: value, to: view)
20
+ }
21
+ }
22
+
23
+ /// Apply a single style property to a view.
24
+ /// Routes to the appropriate handler based on the property key.
25
+ static func apply(key: String, value: Any?, to view: UIView) {
26
+ // Store internal props (prefixed with "__") as associated objects
27
+ // so parent factories can inspect them (e.g. VSectionListFactory).
28
+ if key.hasPrefix("__") {
29
+ setInternalProp(key, value: value, on: view)
30
+ return
31
+ }
32
+
33
+ // First try layout properties (FlexLayout / Yoga)
34
+ if applyLayoutProp(key: key, value: value, to: view) {
35
+ return
36
+ }
37
+
38
+ // Then try visual properties (UIView)
39
+ if applyVisualProp(key: key, value: value, to: view) {
40
+ return
41
+ }
42
+
43
+ // Text properties are handled by VTextFactory directly,
44
+ // but we handle them here as a fallback for convenience
45
+ if applyTextProp(key: key, value: value, to: view) {
46
+ return
47
+ }
48
+ }
49
+
50
+ // MARK: - Internal Props
51
+
52
+ private static var internalPropsKey: UInt8 = 0
53
+
54
+ /// Store an internal prop (prefixed with "__") on a view as an associated object.
55
+ private static func setInternalProp(_ key: String, value: Any?, on view: UIView) {
56
+ var props = objc_getAssociatedObject(view, &internalPropsKey) as? [String: Any] ?? [:]
57
+ if let value = value {
58
+ props[key] = value
59
+ } else {
60
+ props.removeValue(forKey: key)
61
+ }
62
+ objc_setAssociatedObject(view, &internalPropsKey, props, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
63
+ }
64
+
65
+ /// Retrieve an internal prop from a view.
66
+ static func getInternalProp(_ key: String, from view: UIView) -> Any? {
67
+ let props = objc_getAssociatedObject(view, &internalPropsKey) as? [String: Any]
68
+ return props?[key]
69
+ }
70
+
71
+ // MARK: - Yoga Value Helpers
72
+
73
+ /// Convert a value to CGFloat points. Supports Double and Int.
74
+ /// Returns nil for non-numeric values (strings like "50%", "auto").
75
+ static func yogaValue(_ value: Any?) -> CGFloat? {
76
+ if let num = value as? Double { return CGFloat(num) }
77
+ if let num = value as? Int { return CGFloat(num) }
78
+ if let num = value as? CGFloat { return num }
79
+ if let str = value as? String, let num = Double(str) { return CGFloat(num) }
80
+ return nil
81
+ }
82
+
83
+ /// Check if a value represents "auto" (for dimensions that support it).
84
+ static func isAuto(_ value: Any?) -> Bool {
85
+ if let str = value as? String, str.lowercased() == "auto" {
86
+ return true
87
+ }
88
+ return false
89
+ }
90
+
91
+ /// Extract percentage value from strings like "50%". Returns 50.0 for "50%".
92
+ static func asPercent(_ value: Any?) -> CGFloat? {
93
+ guard let str = value as? String, str.hasSuffix("%"),
94
+ let num = Double(str.dropLast()) else { return nil }
95
+ return CGFloat(num)
96
+ }
97
+
98
+ // MARK: - Layout Properties (FlexLayout / Yoga)
99
+
100
+ /// Apply a layout property via FlexLayout. Returns true if the key was recognized.
101
+ @discardableResult
102
+ private static func applyLayoutProp(key: String, value: Any?, to view: UIView) -> Bool {
103
+ let flex = view.flex
104
+
105
+ switch key {
106
+
107
+ // MARK: Flex container properties
108
+
109
+ case "flexDirection":
110
+ if let str = value as? String {
111
+ switch str {
112
+ case "row": flex.direction(.row)
113
+ case "row-reverse", "rowReverse": flex.direction(.rowReverse)
114
+ case "column-reverse", "columnReverse": flex.direction(.columnReverse)
115
+ default: flex.direction(.column)
116
+ }
117
+ }
118
+ return true
119
+
120
+ case "justifyContent":
121
+ if let str = value as? String {
122
+ switch str {
123
+ case "flex-start", "flexStart", "start": flex.justifyContent(.start)
124
+ case "flex-end", "flexEnd", "end": flex.justifyContent(.end)
125
+ case "center": flex.justifyContent(.center)
126
+ case "space-between", "spaceBetween": flex.justifyContent(.spaceBetween)
127
+ case "space-around", "spaceAround": flex.justifyContent(.spaceAround)
128
+ case "space-evenly", "spaceEvenly": flex.justifyContent(.spaceEvenly)
129
+ default: flex.justifyContent(.start)
130
+ }
131
+ }
132
+ return true
133
+
134
+ case "alignItems":
135
+ if let str = value as? String {
136
+ switch str {
137
+ case "flex-start", "flexStart", "start": flex.alignItems(.start)
138
+ case "flex-end", "flexEnd", "end": flex.alignItems(.end)
139
+ case "center": flex.alignItems(.center)
140
+ case "stretch": flex.alignItems(.stretch)
141
+ case "baseline": flex.alignItems(.baseline)
142
+ default: flex.alignItems(.stretch)
143
+ }
144
+ }
145
+ return true
146
+
147
+ case "alignSelf":
148
+ if let str = value as? String {
149
+ switch str {
150
+ case "auto": flex.alignSelf(.auto)
151
+ case "flex-start", "flexStart", "start": flex.alignSelf(.start)
152
+ case "flex-end", "flexEnd", "end": flex.alignSelf(.end)
153
+ case "center": flex.alignSelf(.center)
154
+ case "stretch": flex.alignSelf(.stretch)
155
+ case "baseline": flex.alignSelf(.baseline)
156
+ default: flex.alignSelf(.auto)
157
+ }
158
+ }
159
+ return true
160
+
161
+ case "alignContent":
162
+ if let str = value as? String {
163
+ switch str {
164
+ case "flex-start", "flexStart", "start": flex.alignContent(.start)
165
+ case "flex-end", "flexEnd", "end": flex.alignContent(.end)
166
+ case "center": flex.alignContent(.center)
167
+ case "stretch": flex.alignContent(.stretch)
168
+ case "space-between", "spaceBetween": flex.alignContent(.spaceBetween)
169
+ case "space-around", "spaceAround": flex.alignContent(.spaceAround)
170
+ default: flex.alignContent(.stretch)
171
+ }
172
+ }
173
+ return true
174
+
175
+ case "flexWrap":
176
+ if let str = value as? String {
177
+ switch str {
178
+ case "wrap": flex.wrap(.wrap)
179
+ case "wrap-reverse", "wrapReverse": flex.wrap(.wrapReverse)
180
+ default: flex.wrap(.noWrap)
181
+ }
182
+ }
183
+ return true
184
+
185
+ // MARK: Flex item properties
186
+
187
+ case "flex":
188
+ if let num = yogaValue(value) {
189
+ // CSS "flex" shorthand: when a single number, it sets flexGrow.
190
+ // flex: 1 => grow(1), shrink(1), basis(0)
191
+ flex.grow(num)
192
+ if num > 0 {
193
+ flex.shrink(1)
194
+ flex.basis(0)
195
+ }
196
+ }
197
+ return true
198
+
199
+ case "flexGrow":
200
+ if let num = yogaValue(value) {
201
+ flex.grow(num)
202
+ }
203
+ return true
204
+
205
+ case "flexShrink":
206
+ if let num = yogaValue(value) {
207
+ flex.shrink(num)
208
+ }
209
+ return true
210
+
211
+ case "flexBasis":
212
+ if isAuto(value) {
213
+ flex.basis(nil) // nil means auto in FlexLayout
214
+ } else if let num = yogaValue(value) {
215
+ flex.basis(num)
216
+ }
217
+ return true
218
+
219
+ // MARK: Dimensions
220
+
221
+ case "width":
222
+ if isAuto(value) {
223
+ flex.width(nil)
224
+ } else if let pct = asPercent(value) {
225
+ flex.width(pct%)
226
+ } else if let num = yogaValue(value) {
227
+ flex.width(num)
228
+ }
229
+ return true
230
+
231
+ case "height":
232
+ if isAuto(value) {
233
+ flex.height(nil)
234
+ } else if let pct = asPercent(value) {
235
+ flex.height(pct%)
236
+ } else if let num = yogaValue(value) {
237
+ flex.height(num)
238
+ }
239
+ return true
240
+
241
+ case "minWidth":
242
+ if let pct = asPercent(value) {
243
+ flex.minWidth(pct%)
244
+ } else if let num = yogaValue(value) {
245
+ flex.minWidth(num)
246
+ }
247
+ return true
248
+
249
+ case "minHeight":
250
+ if let pct = asPercent(value) {
251
+ flex.minHeight(pct%)
252
+ } else if let num = yogaValue(value) {
253
+ flex.minHeight(num)
254
+ }
255
+ return true
256
+
257
+ case "maxWidth":
258
+ if let pct = asPercent(value) {
259
+ flex.maxWidth(pct%)
260
+ } else if let num = yogaValue(value) {
261
+ flex.maxWidth(num)
262
+ }
263
+ return true
264
+
265
+ case "maxHeight":
266
+ if let pct = asPercent(value) {
267
+ flex.maxHeight(pct%)
268
+ } else if let num = yogaValue(value) {
269
+ flex.maxHeight(num)
270
+ }
271
+ return true
272
+
273
+ case "aspectRatio":
274
+ if let num = yogaValue(value) {
275
+ flex.aspectRatio(num)
276
+ }
277
+ return true
278
+
279
+ // MARK: Padding
280
+
281
+ case "padding":
282
+ if let num = yogaValue(value) {
283
+ flex.padding(num)
284
+ }
285
+ return true
286
+
287
+ case "paddingTop":
288
+ if let num = yogaValue(value) {
289
+ flex.paddingTop(num)
290
+ }
291
+ return true
292
+
293
+ case "paddingRight":
294
+ if let num = yogaValue(value) {
295
+ flex.paddingRight(num)
296
+ }
297
+ return true
298
+
299
+ case "paddingBottom":
300
+ if let num = yogaValue(value) {
301
+ flex.paddingBottom(num)
302
+ }
303
+ return true
304
+
305
+ case "paddingLeft":
306
+ if let num = yogaValue(value) {
307
+ flex.paddingLeft(num)
308
+ }
309
+ return true
310
+
311
+ case "paddingHorizontal":
312
+ if let num = yogaValue(value) {
313
+ flex.paddingHorizontal(num)
314
+ }
315
+ return true
316
+
317
+ case "paddingVertical":
318
+ if let num = yogaValue(value) {
319
+ flex.paddingVertical(num)
320
+ }
321
+ return true
322
+
323
+ case "paddingStart":
324
+ if let num = yogaValue(value) {
325
+ flex.paddingStart(num)
326
+ }
327
+ return true
328
+
329
+ case "paddingEnd":
330
+ if let num = yogaValue(value) {
331
+ flex.paddingEnd(num)
332
+ }
333
+ return true
334
+
335
+ // MARK: Margin
336
+
337
+ case "margin":
338
+ // Note: FlexLayout does not expose auto margins via its Swift API.
339
+ // Point values are supported; auto margins are not.
340
+ if isAuto(value) {
341
+ // Auto margins not supported by FlexLayout — skip gracefully
342
+ } else if let num = yogaValue(value) {
343
+ flex.margin(num)
344
+ }
345
+ return true
346
+
347
+ case "marginTop":
348
+ if let num = yogaValue(value) {
349
+ flex.marginTop(num)
350
+ }
351
+ return true
352
+
353
+ case "marginRight":
354
+ if let num = yogaValue(value) {
355
+ flex.marginRight(num)
356
+ }
357
+ return true
358
+
359
+ case "marginBottom":
360
+ if let num = yogaValue(value) {
361
+ flex.marginBottom(num)
362
+ }
363
+ return true
364
+
365
+ case "marginLeft":
366
+ if let num = yogaValue(value) {
367
+ flex.marginLeft(num)
368
+ }
369
+ return true
370
+
371
+ case "marginHorizontal":
372
+ if let num = yogaValue(value) {
373
+ flex.marginHorizontal(num)
374
+ }
375
+ return true
376
+
377
+ case "marginVertical":
378
+ if let num = yogaValue(value) {
379
+ flex.marginVertical(num)
380
+ }
381
+ return true
382
+
383
+ case "marginStart":
384
+ if let num = yogaValue(value) {
385
+ flex.marginStart(num)
386
+ }
387
+ return true
388
+
389
+ case "marginEnd":
390
+ if let num = yogaValue(value) {
391
+ flex.marginEnd(num)
392
+ }
393
+ return true
394
+
395
+ // MARK: Gap
396
+
397
+ case "gap":
398
+ if let num = yogaValue(value) {
399
+ flex.gap(num)
400
+ }
401
+ return true
402
+
403
+ case "rowGap":
404
+ if let num = yogaValue(value) {
405
+ flex.rowGap(num)
406
+ }
407
+ return true
408
+
409
+ case "columnGap":
410
+ if let num = yogaValue(value) {
411
+ flex.columnGap(num)
412
+ }
413
+ return true
414
+
415
+ // MARK: Position
416
+
417
+ case "position":
418
+ if let str = value as? String {
419
+ switch str {
420
+ case "absolute": flex.position(.absolute)
421
+ case "relative": flex.position(.relative)
422
+ default: flex.position(.relative)
423
+ }
424
+ }
425
+ return true
426
+
427
+ case "top":
428
+ if let num = yogaValue(value) {
429
+ flex.top(num)
430
+ }
431
+ return true
432
+
433
+ case "right":
434
+ if let num = yogaValue(value) {
435
+ flex.right(num)
436
+ }
437
+ return true
438
+
439
+ case "bottom":
440
+ if let num = yogaValue(value) {
441
+ flex.bottom(num)
442
+ }
443
+ return true
444
+
445
+ case "left":
446
+ if let num = yogaValue(value) {
447
+ flex.left(num)
448
+ }
449
+ return true
450
+
451
+ case "start":
452
+ if let num = yogaValue(value) {
453
+ flex.start(num)
454
+ }
455
+ return true
456
+
457
+ case "end":
458
+ if let num = yogaValue(value) {
459
+ flex.end(num)
460
+ }
461
+ return true
462
+
463
+ // MARK: Overflow
464
+
465
+ case "overflow":
466
+ // Note: FlexLayout's Flex.overflow() is not exposed in the public API.
467
+ // We handle overflow purely via UIView.clipsToBounds.
468
+ if let str = value as? String {
469
+ switch str {
470
+ case "hidden":
471
+ view.clipsToBounds = true
472
+ default:
473
+ view.clipsToBounds = false
474
+ }
475
+ }
476
+ return true
477
+
478
+ // MARK: Display
479
+
480
+ case "display":
481
+ if let str = value as? String {
482
+ switch str {
483
+ case "none":
484
+ flex.display(.none)
485
+ view.isHidden = true
486
+ default:
487
+ flex.display(.flex)
488
+ view.isHidden = false
489
+ }
490
+ }
491
+ return true
492
+
493
+ // MARK: Direction (RTL/LTR)
494
+
495
+ case "direction":
496
+ if let str = value as? String {
497
+ switch str {
498
+ case "ltr": flex.direction(.LTR)
499
+ case "rtl": flex.direction(.RTL)
500
+ case "inherit": flex.direction(.inherit)
501
+ default: break
502
+ }
503
+ }
504
+ return true
505
+
506
+ default:
507
+ return false
508
+ }
509
+ }
510
+
511
+ // MARK: - Visual Properties (UIView)
512
+
513
+ /// Apply a visual property directly on the UIView. Returns true if recognized.
514
+ @discardableResult
515
+ private static func applyVisualProp(key: String, value: Any?, to view: UIView) -> Bool {
516
+ switch key {
517
+
518
+ case "backgroundColor":
519
+ if let colorStr = value as? String {
520
+ view.backgroundColor = UIColor.fromHex(colorStr)
521
+ } else {
522
+ view.backgroundColor = nil
523
+ }
524
+ return true
525
+
526
+ case "opacity":
527
+ if let num = yogaValue(value) {
528
+ view.alpha = num
529
+ } else {
530
+ view.alpha = 1.0
531
+ }
532
+ return true
533
+
534
+ case "borderRadius":
535
+ if let num = yogaValue(value) {
536
+ view.layer.cornerRadius = num
537
+ // Automatically enable clipping when border radius is set
538
+ if num > 0 {
539
+ view.clipsToBounds = true
540
+ }
541
+ } else {
542
+ view.layer.cornerRadius = 0
543
+ }
544
+ return true
545
+
546
+ case "borderTopLeftRadius":
547
+ if let num = yogaValue(value) {
548
+ applyCornerRadius(view: view, corner: .layerMinXMinYCorner, radius: num)
549
+ }
550
+ return true
551
+
552
+ case "borderTopRightRadius":
553
+ if let num = yogaValue(value) {
554
+ applyCornerRadius(view: view, corner: .layerMaxXMinYCorner, radius: num)
555
+ }
556
+ return true
557
+
558
+ case "borderBottomLeftRadius":
559
+ if let num = yogaValue(value) {
560
+ applyCornerRadius(view: view, corner: .layerMinXMaxYCorner, radius: num)
561
+ }
562
+ return true
563
+
564
+ case "borderBottomRightRadius":
565
+ if let num = yogaValue(value) {
566
+ applyCornerRadius(view: view, corner: .layerMaxXMaxYCorner, radius: num)
567
+ }
568
+ return true
569
+
570
+ case "borderWidth":
571
+ if let num = yogaValue(value) {
572
+ view.layer.borderWidth = num
573
+ } else {
574
+ view.layer.borderWidth = 0
575
+ }
576
+ return true
577
+
578
+ case "borderColor":
579
+ if let colorStr = value as? String {
580
+ view.layer.borderColor = UIColor.fromHex(colorStr).cgColor
581
+ } else {
582
+ view.layer.borderColor = nil
583
+ }
584
+ return true
585
+
586
+ case "shadowColor":
587
+ if let colorStr = value as? String {
588
+ view.layer.shadowColor = UIColor.fromHex(colorStr).cgColor
589
+ }
590
+ return true
591
+
592
+ case "shadowOpacity":
593
+ if let num = yogaValue(value) {
594
+ view.layer.shadowOpacity = Float(num)
595
+ }
596
+ return true
597
+
598
+ case "shadowRadius":
599
+ if let num = yogaValue(value) {
600
+ view.layer.shadowRadius = num
601
+ }
602
+ return true
603
+
604
+ case "shadowOffsetX":
605
+ if let num = yogaValue(value) {
606
+ view.layer.shadowOffset = CGSize(
607
+ width: num,
608
+ height: view.layer.shadowOffset.height
609
+ )
610
+ }
611
+ return true
612
+
613
+ case "shadowOffsetY":
614
+ if let num = yogaValue(value) {
615
+ view.layer.shadowOffset = CGSize(
616
+ width: view.layer.shadowOffset.width,
617
+ height: num
618
+ )
619
+ }
620
+ return true
621
+
622
+ case "shadowOffset":
623
+ if let dict = value as? [String: Any] {
624
+ let w = (dict["width"] as? Double).map { CGFloat($0) } ?? view.layer.shadowOffset.width
625
+ let h = (dict["height"] as? Double).map { CGFloat($0) } ?? view.layer.shadowOffset.height
626
+ view.layer.shadowOffset = CGSize(width: w, height: h)
627
+ }
628
+ return true
629
+
630
+ case "transform":
631
+ if let transforms = value as? [[String: Any]] {
632
+ var result = CGAffineTransform.identity
633
+ for dict in transforms {
634
+ if let rotateStr = dict["rotate"] as? String {
635
+ let angle = parseAngle(rotateStr)
636
+ result = result.rotated(by: angle)
637
+ }
638
+ if let scale = dict["scale"] as? Double {
639
+ result = result.scaledBy(x: CGFloat(scale), y: CGFloat(scale))
640
+ }
641
+ if let scaleX = dict["scaleX"] as? Double {
642
+ result = result.scaledBy(x: CGFloat(scaleX), y: 1)
643
+ }
644
+ if let scaleY = dict["scaleY"] as? Double {
645
+ result = result.scaledBy(x: 1, y: CGFloat(scaleY))
646
+ }
647
+ if let tx = dict["translateX"] as? Double {
648
+ result = result.translatedBy(x: CGFloat(tx), y: 0)
649
+ }
650
+ if let ty = dict["translateY"] as? Double {
651
+ result = result.translatedBy(x: 0, y: CGFloat(ty))
652
+ }
653
+ }
654
+ view.transform = result
655
+ } else {
656
+ view.transform = .identity
657
+ }
658
+ return true
659
+
660
+ case "hidden":
661
+ view.isHidden = (value as? Bool) ?? false
662
+ return true
663
+
664
+ case "isHidden":
665
+ if let hidden = value as? Bool {
666
+ view.isHidden = hidden
667
+ } else if let hidden = value as? Int {
668
+ view.isHidden = hidden != 0
669
+ }
670
+ return true
671
+
672
+ case "zIndex":
673
+ if let num = yogaValue(value) {
674
+ view.layer.zPosition = num
675
+ }
676
+ return true
677
+
678
+
679
+ case "accessibilityLabel":
680
+ view.accessibilityLabel = value as? String
681
+ view.isAccessibilityElement = true
682
+ return true
683
+
684
+ case "accessibilityHint":
685
+ view.accessibilityHint = value as? String
686
+ view.isAccessibilityElement = true
687
+ return true
688
+
689
+ case "accessibilityValue":
690
+ view.accessibilityValue = value as? String
691
+ view.isAccessibilityElement = true
692
+ return true
693
+
694
+ case "accessibilityRole":
695
+ if let role = value as? String {
696
+ switch role {
697
+ case "button": view.accessibilityTraits = .button
698
+ case "link": view.accessibilityTraits = .link
699
+ case "header": view.accessibilityTraits = .header
700
+ case "image": view.accessibilityTraits = .image
701
+ case "selected": view.accessibilityTraits = .selected
702
+ case "text": view.accessibilityTraits = .staticText
703
+ case "adjustable": view.accessibilityTraits = .adjustable
704
+ case "search": view.accessibilityTraits = .searchField
705
+ case "tab": view.accessibilityTraits = .tabBar
706
+ case "none": view.accessibilityTraits = .none
707
+ default: break
708
+ }
709
+ view.isAccessibilityElement = true
710
+ }
711
+ return true
712
+
713
+ case "accessibilityState":
714
+ if let state = value as? [String: Any] {
715
+ var traits = view.accessibilityTraits
716
+ if let disabled = state["disabled"] as? Bool, disabled { traits.insert(.notEnabled) }
717
+ if let selected = state["selected"] as? Bool, selected { traits.insert(.selected) }
718
+ if let checked = state["checked"] as? Bool, checked { traits.insert(.selected) }
719
+ view.accessibilityTraits = traits
720
+ view.isAccessibilityElement = true
721
+ }
722
+ return true
723
+
724
+ case "accessible":
725
+ let acc = (value as? Bool) ?? (value as? NSNumber)?.boolValue ?? false
726
+ view.isAccessibilityElement = acc
727
+ return true
728
+
729
+ case "importantForAccessibility":
730
+ if let val = value as? String {
731
+ view.accessibilityElementsHidden = (val == "no-hide-descendants")
732
+ }
733
+ return true
734
+
735
+ default:
736
+ return false
737
+ }
738
+ }
739
+
740
+ // MARK: - Text Properties
741
+
742
+ /// Apply text-specific properties. Returns true if recognized.
743
+ /// This is a fallback — VTextFactory handles these directly when it can.
744
+ @discardableResult
745
+ private static func applyTextProp(key: String, value: Any?, to view: UIView) -> Bool {
746
+ guard let label = view as? UILabel else { return false }
747
+
748
+ switch key {
749
+ case "fontSize":
750
+ if let num = yogaValue(value) {
751
+ label.font = label.font.withSize(num)
752
+ label.flex.markDirty()
753
+ }
754
+ return true
755
+
756
+ case "fontWeight":
757
+ if let str = value as? String {
758
+ let weight = VTextFactory.fontWeightMap[str] ?? .regular
759
+ label.font = UIFont.systemFont(ofSize: label.font.pointSize, weight: weight)
760
+ label.flex.markDirty()
761
+ }
762
+ return true
763
+
764
+ case "color":
765
+ if let colorStr = value as? String {
766
+ label.textColor = UIColor.fromHex(colorStr)
767
+ }
768
+ return true
769
+
770
+ case "textAlign":
771
+ if let alignStr = value as? String {
772
+ label.textAlignment = VTextFactory.textAlignMap[alignStr] ?? .natural
773
+ }
774
+ return true
775
+
776
+ case "fontStyle":
777
+ if let str = value as? String, str == "italic" {
778
+ let descriptor = label.font.fontDescriptor.withSymbolicTraits(.traitItalic) ?? label.font.fontDescriptor
779
+ label.font = UIFont(descriptor: descriptor, size: label.font.pointSize)
780
+ label.flex.markDirty()
781
+ } else {
782
+ // Remove italic if "normal"
783
+ var traits = label.font.fontDescriptor.symbolicTraits
784
+ traits.remove(.traitItalic)
785
+ if let descriptor = label.font.fontDescriptor.withSymbolicTraits(traits) {
786
+ label.font = UIFont(descriptor: descriptor, size: label.font.pointSize)
787
+ label.flex.markDirty()
788
+ }
789
+ }
790
+ return true
791
+
792
+ case "lineHeight":
793
+ if let num = yogaValue(value) {
794
+ let paragraphStyle = NSMutableParagraphStyle()
795
+ paragraphStyle.minimumLineHeight = num
796
+ paragraphStyle.maximumLineHeight = num
797
+ paragraphStyle.alignment = label.textAlignment
798
+ let text = label.text ?? ""
799
+ let attrs: [NSAttributedString.Key: Any] = [
800
+ .paragraphStyle: paragraphStyle,
801
+ .font: label.font as Any
802
+ ]
803
+ label.attributedText = NSAttributedString(string: text, attributes: attrs)
804
+ label.flex.markDirty()
805
+ }
806
+ return true
807
+
808
+ case "letterSpacing":
809
+ if let num = yogaValue(value) {
810
+ let text = label.text ?? ""
811
+ let attrs: [NSAttributedString.Key: Any] = [
812
+ .kern: num,
813
+ .font: label.font as Any
814
+ ]
815
+ label.attributedText = NSAttributedString(string: text, attributes: attrs)
816
+ label.flex.markDirty()
817
+ }
818
+ return true
819
+
820
+ case "textDecorationLine":
821
+ if let str = value as? String {
822
+ let text = label.text ?? ""
823
+ var attrs: [NSAttributedString.Key: Any] = [.font: label.font as Any]
824
+ switch str {
825
+ case "underline":
826
+ attrs[.underlineStyle] = NSUnderlineStyle.single.rawValue
827
+ case "line-through", "lineThrough":
828
+ attrs[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
829
+ case "underline line-through":
830
+ attrs[.underlineStyle] = NSUnderlineStyle.single.rawValue
831
+ attrs[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
832
+ default:
833
+ break
834
+ }
835
+ label.attributedText = NSAttributedString(string: text, attributes: attrs)
836
+ label.flex.markDirty()
837
+ }
838
+ return true
839
+
840
+ case "textTransform":
841
+ if let str = value as? String, let text = label.text {
842
+ switch str {
843
+ case "uppercase": label.text = text.uppercased()
844
+ case "lowercase": label.text = text.lowercased()
845
+ case "capitalize": label.text = text.capitalized
846
+ default: break
847
+ }
848
+ label.flex.markDirty()
849
+ }
850
+ return true
851
+
852
+ default:
853
+ return false
854
+ }
855
+ }
856
+
857
+ // MARK: - Corner radius helpers
858
+
859
+ /// Apply a corner radius to a specific corner of the view.
860
+ private static func applyCornerRadius(view: UIView, corner: CACornerMask, radius: CGFloat) {
861
+ view.clipsToBounds = true
862
+ view.layer.maskedCorners.insert(corner)
863
+ view.layer.cornerRadius = max(view.layer.cornerRadius, radius)
864
+ }
865
+
866
+ // MARK: - Helpers
867
+
868
+ /// Parse an angle string into radians.
869
+ /// Supports "45deg" (degrees) and "1.5rad" (radians).
870
+ private static func parseAngle(_ str: String) -> CGFloat {
871
+ let s = str.trimmingCharacters(in: .whitespaces).lowercased()
872
+ if s.hasSuffix("deg"), let num = Double(s.dropLast(3)) {
873
+ return CGFloat(num * .pi / 180)
874
+ }
875
+ if s.hasSuffix("rad"), let num = Double(s.dropLast(3)) {
876
+ return CGFloat(num)
877
+ }
878
+ // Fallback: try to parse as raw number (radians)
879
+ if let num = Double(s) {
880
+ return CGFloat(num)
881
+ }
882
+ return 0
883
+ }
884
+ }
885
+ #endif