@thelacanians/vue-native-cli 0.4.14 → 0.6.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 (123) hide show
  1. package/dist/cli.js +789 -200
  2. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +156 -5
  3. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VListFactory.kt +33 -13
  4. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VScrollViewFactory.kt +27 -6
  5. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSliderFactory.kt +9 -2
  6. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +178 -1
  7. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/EventThrottle.kt +57 -0
  8. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeneratedModuleRegistry.kt +28 -0
  9. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModuleRegistry.kt +3 -0
  10. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ComponentFactoryTest.kt +674 -0
  11. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ErrorOverlayViewTest.kt +183 -0
  12. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/EventThrottleTest.kt +203 -0
  13. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/HotReloadManagerTest.kt +162 -0
  14. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/JSPolyfillsTest.kt +153 -0
  15. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeBridgeTest.kt +6 -3
  16. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeModuleTest.kt +475 -0
  17. package/native/android/gradle.properties +1 -0
  18. package/native/android/gradlew +1 -1
  19. package/native/ios/VueNativeCore/Package.swift +1 -1
  20. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/EventThrottle.swift +80 -0
  21. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +244 -112
  22. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VListFactory.swift +19 -2
  23. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VScrollViewFactory.swift +9 -4
  24. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSliderFactory.swift +8 -3
  25. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VTextFactory.swift +43 -0
  26. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VViewFactory.swift +116 -4
  27. package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/GestureWrapper.swift +100 -0
  28. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/GeneratedModuleRegistry.swift +28 -0
  29. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModuleRegistry.swift +3 -0
  30. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/CertificatePinningTests.swift +190 -0
  31. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/ComponentFactoryTests.swift +585 -0
  32. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/EventThrottleTests.swift +161 -0
  33. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/HotReloadManagerTests.swift +88 -0
  34. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/JSPolyfillsTests.swift +319 -0
  35. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/NativeModuleTests.swift +400 -0
  36. package/native/macos/VueNativeMacOS/Package.swift +34 -0
  37. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/ErrorOverlayView.swift +112 -0
  38. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/EventThrottle.swift +58 -0
  39. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/HotReloadManager.swift +153 -0
  40. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSPolyfills.swift +696 -0
  41. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSRuntime.swift +347 -0
  42. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/NativeBridge.swift +877 -0
  43. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/VueNativeWindowController.swift +125 -0
  44. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/ComponentRegistry.swift +209 -0
  45. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActionSheetFactory.swift +155 -0
  46. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActivityIndicatorFactory.swift +85 -0
  47. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VAlertDialogFactory.swift +132 -0
  48. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VButtonFactory.swift +83 -0
  49. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VCheckboxFactory.swift +108 -0
  50. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VDropdownFactory.swift +155 -0
  51. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VImageFactory.swift +270 -0
  52. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VInputFactory.swift +257 -0
  53. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VKeyboardAvoidingFactory.swift +22 -0
  54. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VListFactory.swift +324 -0
  55. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VModalFactory.swift +231 -0
  56. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VOutlineViewFactory.swift +276 -0
  57. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPickerFactory.swift +134 -0
  58. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPressableFactory.swift +120 -0
  59. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VProgressBarFactory.swift +71 -0
  60. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRadioFactory.swift +193 -0
  61. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRefreshControlFactory.swift +25 -0
  62. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSafeAreaFactory.swift +46 -0
  63. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VScrollViewFactory.swift +190 -0
  64. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSectionListFactory.swift +374 -0
  65. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSegmentedControlFactory.swift +125 -0
  66. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSliderFactory.swift +131 -0
  67. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSplitViewFactory.swift +215 -0
  68. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VStatusBarFactory.swift +25 -0
  69. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSwitchFactory.swift +92 -0
  70. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VTextFactory.swift +336 -0
  71. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VToolbarFactory.swift +212 -0
  72. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VVideoFactory.swift +245 -0
  73. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VViewFactory.swift +314 -0
  74. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VWebViewFactory.swift +162 -0
  75. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/NativeComponentFactory.swift +54 -0
  76. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/ClickableView.swift +100 -0
  77. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/Extensions.swift +23 -0
  78. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/GestureWrapper.swift +183 -0
  79. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/NSColor+Hex.swift +78 -0
  80. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/FlippedView.swift +19 -0
  81. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/LayoutNode.swift +493 -0
  82. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AnimationModule.swift +354 -0
  83. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AppStateModule.swift +62 -0
  84. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/BiometryModule.swift +60 -0
  85. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/CameraModule.swift +167 -0
  86. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ClipboardModule.swift +34 -0
  87. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DeviceInfoModule.swift +49 -0
  88. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DragDropModule.swift +50 -0
  89. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/FileDialogModule.swift +86 -0
  90. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/HapticsModule.swift +42 -0
  91. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/KeyboardModule.swift +28 -0
  92. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/LinkingModule.swift +49 -0
  93. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/MenuModule.swift +95 -0
  94. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NativeModuleRegistry.swift +63 -0
  95. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NotificationsModule.swift +112 -0
  96. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/PermissionsModule.swift +149 -0
  97. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ShareModule.swift +37 -0
  98. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/WindowModule.swift +71 -0
  99. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Resources/vue-native-placeholder.js +2 -0
  100. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Styling/StyleEngine.swift +885 -0
  101. package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/ComponentFactoryTests.swift +80 -0
  102. package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/VueNativeMacOSTests.swift +149 -0
  103. package/native/shared/VueNativeShared/AGENTS.md +129 -0
  104. package/native/shared/VueNativeShared/Package.swift +14 -0
  105. package/native/shared/VueNativeShared/Sources/VueNativeShared/CertificatePinning.swift +134 -0
  106. package/native/shared/VueNativeShared/Sources/VueNativeShared/EventThrottle.swift +78 -0
  107. package/native/shared/VueNativeShared/Sources/VueNativeShared/HotReloadManager.swift +162 -0
  108. package/native/shared/VueNativeShared/Sources/VueNativeShared/JSRuntime.swift +412 -0
  109. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AsyncStorageModule.swift +68 -0
  110. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AudioModule.swift +359 -0
  111. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/DatabaseModule.swift +259 -0
  112. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/FileSystemModule.swift +233 -0
  113. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/GeolocationModule.swift +156 -0
  114. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/NetworkModule.swift +59 -0
  115. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/PerformanceModule.swift +113 -0
  116. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/SecureStorageModule.swift +119 -0
  117. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/WebSocketModule.swift +212 -0
  118. package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeEventDispatcher.swift +6 -0
  119. package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModule.swift +26 -0
  120. package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModuleRegistry.swift +37 -0
  121. package/native/shared/VueNativeShared/Sources/VueNativeShared/SharedJSPolyfills.swift +673 -0
  122. package/native/shared/VueNativeShared/Tests/VueNativeSharedTests/VueNativeSharedTests.swift +44 -0
  123. package/package.json +8 -2
@@ -0,0 +1,80 @@
1
+ #if canImport(UIKit)
2
+ import Foundation
3
+ import QuartzCore
4
+
5
+ /// Throttles high-frequency event handlers to avoid flooding the JS bridge.
6
+ ///
7
+ /// When a high-frequency event (scroll, slider drag, text input) fires many
8
+ /// times per frame, each invocation becomes a bridge round-trip. This utility
9
+ /// ensures at most one call per `interval` seconds, with a trailing call
10
+ /// to deliver the latest value.
11
+ ///
12
+ /// Default interval: 16ms (~60 FPS). Callers can customize via `interval`.
13
+ final class EventThrottle {
14
+
15
+ /// Minimum time between handler invocations (seconds).
16
+ let interval: TimeInterval
17
+
18
+ /// The throttled handler to call.
19
+ private let handler: (Any?) -> Void
20
+
21
+ /// Timestamp of the last invocation.
22
+ private var lastFireTime: CFTimeInterval = 0
23
+
24
+ /// Whether a trailing call is pending.
25
+ private var pendingTrailing = false
26
+
27
+ /// The most recent payload, used for the trailing call.
28
+ private var latestPayload: Any?
29
+
30
+ /// Timer for delivering the trailing call.
31
+ private var trailingTimer: DispatchSourceTimer?
32
+
33
+ /// Create a throttled event handler.
34
+ /// - Parameters:
35
+ /// - interval: Minimum seconds between invocations. Default 0.016 (~60fps).
36
+ /// - handler: The closure to invoke with the event payload.
37
+ init(interval: TimeInterval = 0.016, handler: @escaping (Any?) -> Void) {
38
+ self.interval = interval
39
+ self.handler = handler
40
+ }
41
+
42
+ deinit {
43
+ trailingTimer?.cancel()
44
+ }
45
+
46
+ /// Call this from the native event callback instead of the original handler.
47
+ /// Fires immediately if enough time has elapsed, otherwise schedules a trailing call.
48
+ func fire(_ payload: Any?) {
49
+ let now = CACurrentMediaTime()
50
+ let elapsed = now - lastFireTime
51
+
52
+ latestPayload = payload
53
+
54
+ if elapsed >= interval {
55
+ // Enough time has passed — fire immediately
56
+ lastFireTime = now
57
+ pendingTrailing = false
58
+ trailingTimer?.cancel()
59
+ trailingTimer = nil
60
+ handler(payload)
61
+ } else if !pendingTrailing {
62
+ // Schedule a trailing call for the remaining time
63
+ pendingTrailing = true
64
+ let remaining = interval - elapsed
65
+ let timer = DispatchSource.makeTimerSource(queue: .main)
66
+ timer.schedule(deadline: .now() + remaining)
67
+ timer.setEventHandler { [weak self] in
68
+ guard let self = self else { return }
69
+ self.lastFireTime = CACurrentMediaTime()
70
+ self.pendingTrailing = false
71
+ self.trailingTimer = nil
72
+ self.handler(self.latestPayload)
73
+ }
74
+ trailingTimer = timer
75
+ timer.resume()
76
+ }
77
+ // If a trailing call is already pending, just update latestPayload (done above)
78
+ }
79
+ }
80
+ #endif
@@ -61,6 +61,23 @@ public final class NativeBridge {
61
61
  /// Reference to the JS runtime.
62
62
  private let runtime = JSRuntime.shared
63
63
 
64
+ // MARK: - Teleport Support
65
+
66
+ /// Maps teleport marker IDs (start, end) for cleanup
67
+ private var teleportMarkers: [Int: (start: Int, end: Int)] = [:]
68
+
69
+ /// Maps parent node IDs to their teleport containers
70
+ private var teleportContainers: [Int: UIView] = [:]
71
+
72
+ /// Modal container for teleporting modals
73
+ private lazy var modalContainer: UIView = {
74
+ let container = UIView()
75
+ container.backgroundColor = .clear
76
+ container.isUserInteractionEnabled = true
77
+ container.translatesAutoresizingMaskIntoConstraints = false
78
+ return container
79
+ }()
80
+
64
81
  // MARK: - Initialization
65
82
 
66
83
  private init() {}
@@ -69,6 +86,59 @@ public final class NativeBridge {
69
86
 
70
87
  private static var traitObserverKey: UInt8 = 99
71
88
 
89
+ // MARK: - Bridge Function Registration
90
+
91
+ /// Register `__VN_flushOperations`, `__VN_teardown`, `__VN_log`, and `__VN_handleError`
92
+ /// on the given JSContext. Called from both `initialize()` and `reloadWithBundle()`.
93
+ private func registerBridgeFunctions(on context: JSContext) {
94
+ let flushOps: @convention(block) (JSValue) -> Void = { [weak self] opsValue in
95
+ guard let self = self else { return }
96
+ guard let jsonString = opsValue.toString(), !jsonString.isEmpty else {
97
+ NSLog("[VueNative Bridge] Warning: Empty operations batch")
98
+ return
99
+ }
100
+
101
+ guard let data = jsonString.data(using: .utf8),
102
+ let operations = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
103
+ NSLog("[VueNative Bridge] Error: Failed to parse operations JSON")
104
+ return
105
+ }
106
+
107
+ DispatchQueue.main.async { [weak self] in
108
+ self?.processOperations(operations)
109
+ }
110
+ }
111
+ context.setObject(flushOps, forKeyedSubscript: "__VN_flushOperations" as NSString)
112
+
113
+ let teardown: @convention(block) () -> Void = { [weak self] in
114
+ NSLog("[VueNative] Teardown called from JS")
115
+ _ = self
116
+ }
117
+ context.setObject(teardown, forKeyedSubscript: "__VN_teardown" as NSString)
118
+
119
+ let log: @convention(block) (JSValue) -> Void = { message in
120
+ NSLog("[VueNative JS] \(message.toString() ?? "")")
121
+ }
122
+ context.setObject(log, forKeyedSubscript: "__VN_log" as NSString)
123
+
124
+ let handleError: @convention(block) (JSValue) -> Void = { errorInfoValue in
125
+ let jsonString = errorInfoValue.toString() ?? "{}"
126
+ NSLog("[VueNative Error] %@", jsonString)
127
+
128
+ if let data = jsonString.data(using: .utf8),
129
+ let info = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
130
+ let message = info["message"] as? String ?? "Unknown error"
131
+ let stack = info["stack"] as? String ?? ""
132
+ let componentName = info["componentName"] as? String ?? "unknown"
133
+ NSLog("[VueNative Error] Component: %@, Message: %@", componentName, message)
134
+ if !stack.isEmpty {
135
+ NSLog("[VueNative Error] Stack: %@", stack)
136
+ }
137
+ }
138
+ }
139
+ context.setObject(handleError, forKeyedSubscript: "__VN_handleError" as NSString)
140
+ }
141
+
72
142
  // MARK: - Setup
73
143
 
74
144
  /// Initialize the bridge. Must be called after JSRuntime.initialize().
@@ -83,61 +153,7 @@ public final class NativeBridge {
83
153
  runtime.jsQueue.async { [weak self] in
84
154
  guard let self = self, let context = runtime.context else { return }
85
155
 
86
- let flushOps: @convention(block) (JSValue) -> Void = { [weak self] opsValue in
87
- guard let self = self else { return }
88
- guard let jsonString = opsValue.toString(), !jsonString.isEmpty else {
89
- NSLog("[VueNative Bridge] Warning: Empty operations batch")
90
- return
91
- }
92
-
93
- // Parse JSON on the JS queue (avoid main thread work)
94
- guard let data = jsonString.data(using: .utf8),
95
- let operations = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
96
- NSLog("[VueNative Bridge] Error: Failed to parse operations JSON")
97
- return
98
- }
99
-
100
- // Process operations on the main thread
101
- DispatchQueue.main.async { [weak self] in
102
- self?.processOperations(operations)
103
- }
104
- }
105
- context.setObject(flushOps, forKeyedSubscript: "__VN_flushOperations" as NSString)
106
-
107
- // Register __VN_teardown for graceful JS-side shutdown on hot reload
108
- let teardown: @convention(block) () -> Void = { [weak self] in
109
- // Graceful shutdown: bridge will be re-initialized on reload
110
- NSLog("[VueNative] Teardown called from JS")
111
- _ = self
112
- }
113
- context.setObject(teardown, forKeyedSubscript: "__VN_teardown" as NSString)
114
-
115
- // Register __VN_log for debug logging from JS
116
- let log: @convention(block) (JSValue) -> Void = { message in
117
- NSLog("[VueNative JS] \(message.toString() ?? "")")
118
- }
119
- context.setObject(log, forKeyedSubscript: "__VN_log" as NSString)
120
-
121
- // Register __VN_handleError for JS error reporting
122
- // Called from JS with a JSON-encoded string containing error info
123
- // (message, stack, componentName).
124
- let handleError: @convention(block) (JSValue) -> Void = { errorInfoValue in
125
- let jsonString = errorInfoValue.toString() ?? "{}"
126
- NSLog("[VueNative Error] %@", jsonString)
127
-
128
- // Try to extract structured fields for clearer logging
129
- if let data = jsonString.data(using: .utf8),
130
- let info = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
131
- let message = info["message"] as? String ?? "Unknown error"
132
- let stack = info["stack"] as? String ?? ""
133
- let componentName = info["componentName"] as? String ?? "unknown"
134
- NSLog("[VueNative Error] Component: %@, Message: %@", componentName, message)
135
- if !stack.isEmpty {
136
- NSLog("[VueNative Error] Stack: %@", stack)
137
- }
138
- }
139
- }
140
- context.setObject(handleError, forKeyedSubscript: "__VN_handleError" as NSString)
156
+ self.registerBridgeFunctions(on: context)
141
157
  }
142
158
 
143
159
  // Register all native modules synchronously so they are available
@@ -153,19 +169,44 @@ public final class NativeBridge {
153
169
  "setRootView", "setText", "setElementText"
154
170
  ]
155
171
 
172
+ /// Style properties that affect Yoga layout and require a layout pass when changed.
173
+ /// When a batch contains only updateStyle ops for non-layout properties (e.g.
174
+ /// backgroundColor, opacity), we skip the expensive layout recalculation.
175
+ private static let layoutAffectingStyles: Set<String> = [
176
+ // Dimensions
177
+ "width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight",
178
+ // Flex
179
+ "flex", "flexGrow", "flexShrink", "flexBasis", "flexDirection",
180
+ "flexWrap", "alignItems", "alignSelf", "alignContent", "justifyContent",
181
+ // Spacing
182
+ "padding", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
183
+ "paddingHorizontal", "paddingVertical", "paddingStart", "paddingEnd",
184
+ "margin", "marginTop", "marginRight", "marginBottom", "marginLeft",
185
+ "marginHorizontal", "marginVertical", "marginStart", "marginEnd",
186
+ // Gap
187
+ "gap", "rowGap", "columnGap",
188
+ // Position
189
+ "position", "top", "right", "bottom", "left", "start", "end",
190
+ // Other layout
191
+ "aspectRatio", "display", "overflow", "direction",
192
+ ]
193
+
156
194
  /// Process a batch of operations on the main thread.
157
195
  /// Each operation has an "op" key and "args" array.
158
- /// Only triggers a Yoga layout pass when the batch contains tree mutations
159
- /// (create, appendChild, insertBefore, removeChild, etc.). Batches that
160
- /// only update props/styles/events skip the expensive layout recalculation.
196
+ /// Triggers a Yoga layout pass when the batch contains tree mutations
197
+ /// (create, appendChild, insertBefore, removeChild, etc.) OR style changes
198
+ /// that affect layout (width, height, padding, margin, flex, etc.).
199
+ /// Batches that only update visual styles/events skip the expensive layout.
161
200
  ///
162
201
  /// Access: internal (not private) so that `@testable import` can exercise
163
202
  /// operation handling without going through JSContext.
164
203
  func processOperations(_ operations: [[String: Any]]) {
165
204
  dispatchPrecondition(condition: .onQueue(.main))
205
+ #if DEBUG
166
206
  NSLog("[VueNative Bridge] Processing %d operations", operations.count)
207
+ #endif
167
208
 
168
- var hasMutations = false
209
+ var needsLayout = false
169
210
 
170
211
  for operation in operations {
171
212
  guard let op = operation["op"] as? String,
@@ -174,8 +215,20 @@ public final class NativeBridge {
174
215
  continue
175
216
  }
176
217
 
177
- if !hasMutations && NativeBridge.treeMutationOps.contains(op) {
178
- hasMutations = true
218
+ if !needsLayout && NativeBridge.treeMutationOps.contains(op) {
219
+ needsLayout = true
220
+ }
221
+
222
+ // Check if updateStyle changes any layout-affecting property
223
+ if !needsLayout && op == "updateStyle",
224
+ args.count >= 2,
225
+ let styles = args[1] as? [String: Any] {
226
+ for key in styles.keys {
227
+ if NativeBridge.layoutAffectingStyles.contains(key) {
228
+ needsLayout = true
229
+ break
230
+ }
231
+ }
179
232
  }
180
233
 
181
234
  switch op {
@@ -203,6 +256,12 @@ public final class NativeBridge {
203
256
  handleRemoveEventListener(args: args)
204
257
  case "setRootView":
205
258
  handleSetRootView(args: args)
259
+ case "createTeleport":
260
+ handleCreateTeleport(args: args)
261
+ case "removeTeleport":
262
+ handleRemoveTeleport(args: args)
263
+ case "teleportTo":
264
+ handleTeleportTo(args: args)
206
265
  case "invokeNativeModule":
207
266
  handleInvokeNativeModule(args: args)
208
267
  case "invokeNativeModuleSync":
@@ -212,8 +271,8 @@ public final class NativeBridge {
212
271
  }
213
272
  }
214
273
 
215
- // Only trigger layout recalculation when the batch mutated the view tree
216
- if hasMutations {
274
+ // Trigger layout when tree was mutated or layout-affecting styles changed
275
+ if needsLayout {
217
276
  triggerLayout()
218
277
  }
219
278
  }
@@ -388,8 +447,9 @@ public final class NativeBridge {
388
447
  if let factory = ComponentRegistry.factory(for: parentView) {
389
448
  factory.insertChild(childView, into: container, before: beforeView)
390
449
  } else if let index = container.subviews.firstIndex(of: beforeView) {
391
- container.flex.addItem(childView)
392
450
  container.insertSubview(childView, at: index)
451
+ container.flex.markDirty()
452
+ container.setNeedsLayout()
393
453
  } else {
394
454
  container.flex.addItem(childView)
395
455
  }
@@ -559,6 +619,124 @@ public final class NativeBridge {
559
619
  vc.view.addSubview(traitObserver)
560
620
  // Retain the observer for the lifetime of the root view controller's view
561
621
  objc_setAssociatedObject(vc.view as UIView, &NativeBridge.traitObserverKey, traitObserver as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
622
+
623
+ // Add modal container to root view for teleport
624
+ if rootView != nil {
625
+ rootView?.addSubview(modalContainer)
626
+ modalContainer.topAnchor.constraint(equalTo: rootView!.topAnchor).isActive = true
627
+ modalContainer.leadingAnchor.constraint(equalTo: rootView!.leadingAnchor).isActive = true
628
+ modalContainer.trailingAnchor.constraint(equalTo: rootView!.trailingAnchor).isActive = true
629
+ modalContainer.bottomAnchor.constraint(equalTo: rootView!.bottomAnchor).isActive = true
630
+ }
631
+ }
632
+
633
+ // MARK: - Teleport Handlers
634
+
635
+ /// createTeleport: [parentId: Int, startId: Int, endId: Int]
636
+ private func handleCreateTeleport(args: [Any]) {
637
+ guard args.count >= 3,
638
+ let parentId = asInt(args[0]),
639
+ let startId = asInt(args[1]),
640
+ let endId = asInt(args[2]) else {
641
+ NSLog("[VueNative Bridge] Error: Invalid createTeleport args: \(args)")
642
+ return
643
+ }
644
+
645
+ guard let parentView = viewRegistry[parentId] else {
646
+ NSLog("[VueNative Bridge] Error: Parent view not found for teleport (id: \(parentId))")
647
+ return
648
+ }
649
+
650
+ // Store teleport marker IDs
651
+ teleportMarkers[parentId] = (start: startId, end: endId)
652
+
653
+ // Create container for teleported content
654
+ let container = UIView()
655
+ container.tag = -parentId // Negative tag to identify as teleport container
656
+ container.backgroundColor = .clear
657
+ container.isUserInteractionEnabled = true
658
+ container.translatesAutoresizingMaskIntoConstraints = false
659
+
660
+ parentView.addSubview(container)
661
+ teleportContainers[parentId] = container
662
+
663
+ #if DEBUG
664
+ NSLog("[VueNative Bridge] Created teleport container for parent \(parentId)")
665
+ #endif
666
+ }
667
+
668
+ /// removeTeleport: [parentId: Int, startId: Int, endId: Int]
669
+ private func handleRemoveTeleport(args: [Any]) {
670
+ guard args.count >= 1,
671
+ let parentId = asInt(args[0]) else {
672
+ NSLog("[VueNative Bridge] Error: Invalid removeTeleport args: \(args)")
673
+ return
674
+ }
675
+
676
+ // Remove teleport container
677
+ if let container = teleportContainers.removeValue(forKey: parentId) {
678
+ container.removeFromSuperview()
679
+ }
680
+
681
+ // Clean up markers
682
+ teleportMarkers.removeValue(forKey: parentId)
683
+
684
+ #if DEBUG
685
+ NSLog("[VueNative Bridge] Removed teleport container for parent \(parentId)")
686
+ #endif
687
+ }
688
+
689
+ /// teleportTo: [target: String, nodeId: Int]
690
+ private func handleTeleportTo(args: [Any]) {
691
+ guard args.count >= 2,
692
+ let target = args[0] as? String,
693
+ let nodeId = asInt(args[1]) else {
694
+ NSLog("[VueNative Bridge] Error: Invalid teleportTo args: \(args)")
695
+ return
696
+ }
697
+
698
+ guard let targetView = getTeleportTarget(target) else {
699
+ NSLog("[VueNative Bridge] Warning: Teleport target '\(target)' not found")
700
+ return
701
+ }
702
+
703
+ guard let childView = viewRegistry[nodeId] else {
704
+ NSLog("[VueNative Bridge] Warning: Node view not found for teleport (id: \(nodeId))")
705
+ return
706
+ }
707
+
708
+ // Move view to teleport target
709
+ childView.removeFromSuperview()
710
+ targetView.addSubview(childView)
711
+
712
+ // Set up full-size constraints
713
+ childView.translatesAutoresizingMaskIntoConstraints = false
714
+ NSLayoutConstraint.activate([
715
+ childView.topAnchor.constraint(equalTo: targetView.topAnchor),
716
+ childView.leadingAnchor.constraint(equalTo: targetView.leadingAnchor),
717
+ childView.trailingAnchor.constraint(equalTo: targetView.trailingAnchor),
718
+ childView.bottomAnchor.constraint(equalTo: targetView.bottomAnchor),
719
+ ])
720
+
721
+ #if DEBUG
722
+ NSLog("[VueNative Bridge] Teleported node \(nodeId) to target '\(target)'")
723
+ #endif
724
+ }
725
+
726
+ /// Get teleport target view by name
727
+ private func getTeleportTarget(_ target: String) -> UIView? {
728
+ switch target {
729
+ case "root":
730
+ return rootView
731
+ case "modal":
732
+ // Ensure modal container is added to root if not already
733
+ if modalContainer.superview == nil && rootView != nil {
734
+ rootView?.addSubview(modalContainer)
735
+ }
736
+ return modalContainer
737
+ default:
738
+ return nil
739
+ }
562
740
  }
563
741
 
564
742
  // MARK: - Native Module Handlers
@@ -781,55 +959,9 @@ public final class NativeBridge {
781
959
  return
782
960
  }
783
961
 
784
- // Re-register __VN_flushOperations, __VN_teardown, __VN_log on new context
962
+ // Re-register bridge functions on new context
785
963
  guard let context = self.runtime.context else { return }
786
-
787
- let flushOps: @convention(block) (JSValue) -> Void = { [weak self] opsValue in
788
- guard let self = self else { return }
789
- guard let jsonString = opsValue.toString(), !jsonString.isEmpty else {
790
- NSLog("[VueNative Bridge] Warning: Empty operations batch")
791
- return
792
- }
793
-
794
- guard let data = jsonString.data(using: .utf8),
795
- let operations = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
796
- NSLog("[VueNative Bridge] Error: Failed to parse operations JSON")
797
- return
798
- }
799
-
800
- DispatchQueue.main.async { [weak self] in
801
- self?.processOperations(operations)
802
- }
803
- }
804
- context.setObject(flushOps, forKeyedSubscript: "__VN_flushOperations" as NSString)
805
-
806
- let teardown: @convention(block) () -> Void = { [weak self] in
807
- NSLog("[VueNative] Teardown called from JS")
808
- _ = self
809
- }
810
- context.setObject(teardown, forKeyedSubscript: "__VN_teardown" as NSString)
811
-
812
- let log: @convention(block) (JSValue) -> Void = { message in
813
- NSLog("[VueNative JS] \(message.toString() ?? "")")
814
- }
815
- context.setObject(log, forKeyedSubscript: "__VN_log" as NSString)
816
-
817
- let handleError: @convention(block) (JSValue) -> Void = { errorInfoValue in
818
- let jsonString = errorInfoValue.toString() ?? "{}"
819
- NSLog("[VueNative Error] %@", jsonString)
820
-
821
- if let data = jsonString.data(using: .utf8),
822
- let info = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
823
- let message = info["message"] as? String ?? "Unknown error"
824
- let stack = info["stack"] as? String ?? ""
825
- let componentName = info["componentName"] as? String ?? "unknown"
826
- NSLog("[VueNative Error] Component: %@, Message: %@", componentName, message)
827
- if !stack.isEmpty {
828
- NSLog("[VueNative Error] Stack: %@", stack)
829
- }
830
- }
831
- }
832
- context.setObject(handleError, forKeyedSubscript: "__VN_handleError" as NSString)
964
+ self.registerBridgeFunctions(on: context)
833
965
 
834
966
  NSLog("[VueNative Bridge] reloadWithBundle: bridge re-registered on new context")
835
967
  }
@@ -37,6 +37,7 @@ final class VListFactory: NativeComponentFactory {
37
37
  switch event {
38
38
  case "scroll":
39
39
  container.onScroll = handler
40
+ container.scrollThrottle = EventThrottle(interval: 0.016, handler: handler)
40
41
  case "endReached":
41
42
  container.onEndReached = handler
42
43
  default:
@@ -47,7 +48,9 @@ final class VListFactory: NativeComponentFactory {
47
48
  func removeEventListener(view: UIView, event: String) {
48
49
  guard let container = view as? VListContainerView else { return }
49
50
  switch event {
50
- case "scroll": container.onScroll = nil
51
+ case "scroll":
52
+ container.onScroll = nil
53
+ container.scrollThrottle = nil
51
54
  case "endReached": container.onEndReached = nil
52
55
  default: break
53
56
  }
@@ -106,6 +109,7 @@ final class VListContainerView: UIView {
106
109
  var itemViews: [UIView] = []
107
110
  var estimatedItemHeight: CGFloat = 44
108
111
  var onScroll: ((Any?) -> Void)?
112
+ var scrollThrottle: EventThrottle?
109
113
  var onEndReached: ((Any?) -> Void)?
110
114
  fileprivate var firedEndReached = false
111
115
  private lazy var internalDelegate = VListInternalDelegate(container: self)
@@ -195,7 +199,20 @@ private final class VListInternalDelegate: NSObject,
195
199
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
196
200
  guard let container = container else { return }
197
201
  let offset = scrollView.contentOffset
198
- container.onScroll?(["x": Double(offset.x), "y": Double(offset.y)])
202
+ let payload: [String: Any] = [
203
+ "x": Double(offset.x),
204
+ "y": Double(offset.y),
205
+ "contentWidth": Double(scrollView.contentSize.width),
206
+ "contentHeight": Double(scrollView.contentSize.height),
207
+ "layoutWidth": Double(scrollView.frame.width),
208
+ "layoutHeight": Double(scrollView.frame.height),
209
+ ]
210
+ // Use throttle if available, otherwise fire directly
211
+ if let throttle = container.scrollThrottle {
212
+ throttle.fire(payload)
213
+ } else {
214
+ container.onScroll?(payload)
215
+ }
199
216
 
200
217
  // endReached detection (threshold = 20% from bottom)
201
218
  let contentH = scrollView.contentSize.height
@@ -221,20 +221,25 @@ private final class RefreshTarget: NSObject {
221
221
  // MARK: - ScrollDelegateProxy
222
222
 
223
223
  /// UIScrollViewDelegate proxy that forwards scroll events to a JS handler.
224
+ /// Uses EventThrottle to limit bridge round-trips to ~60/s during fast scrolling.
224
225
  private final class ScrollDelegateProxy: NSObject, UIScrollViewDelegate {
225
- private let handler: (Any?) -> Void
226
+ private let throttle: EventThrottle
226
227
 
227
228
  init(handler: @escaping (Any?) -> Void) {
228
- self.handler = handler
229
+ self.throttle = EventThrottle(interval: 0.016, handler: handler)
229
230
  super.init()
230
231
  }
231
232
 
232
233
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
233
234
  let payload: [String: Any] = [
234
235
  "x": scrollView.contentOffset.x,
235
- "y": scrollView.contentOffset.y
236
+ "y": scrollView.contentOffset.y,
237
+ "contentWidth": scrollView.contentSize.width,
238
+ "contentHeight": scrollView.contentSize.height,
239
+ "layoutWidth": scrollView.frame.width,
240
+ "layoutHeight": scrollView.frame.height,
236
241
  ]
237
- handler(payload)
242
+ throttle.fire(payload)
238
243
  }
239
244
  }
240
245
  #endif
@@ -53,11 +53,16 @@ final class VSliderFactory: NativeComponentFactory {
53
53
  }
54
54
  }
55
55
 
56
+ /// Throttled target for UISlider .valueChanged events.
57
+ /// Limits bridge round-trips to ~60/s during continuous slider dragging.
56
58
  private final class SliderTarget: NSObject {
57
- let handler: (Any?) -> Void
58
- init(handler: @escaping (Any?) -> Void) { self.handler = handler }
59
+ private let throttle: EventThrottle
60
+ init(handler: @escaping (Any?) -> Void) {
61
+ self.throttle = EventThrottle(interval: 0.016, handler: handler)
62
+ super.init()
63
+ }
59
64
  @objc func handleValueChanged(_ slider: UISlider) {
60
- handler(Double(slider.value))
65
+ throttle.fire(["value": Double(slider.value)])
61
66
  }
62
67
  }
63
68
  #endif
@@ -40,6 +40,7 @@ final class VTextFactory: NativeComponentFactory {
40
40
  private static var fontSizeKey: UInt8 = 0
41
41
  private static var fontWeightKey: UInt8 = 0
42
42
  private static var fontFamilyKey: UInt8 = 0
43
+ private static var textChildrenKey: UInt8 = 0
43
44
 
44
45
  // MARK: - NativeComponentFactory
45
46
 
@@ -57,6 +58,7 @@ final class VTextFactory: NativeComponentFactory {
57
58
 
58
59
  switch key {
59
60
  case "text":
61
+ storeTextChildren([], on: label)
60
62
  if let text = value as? String {
61
63
  label.text = text
62
64
  } else {
@@ -235,6 +237,31 @@ final class VTextFactory: NativeComponentFactory {
235
237
  }
236
238
  }
237
239
 
240
+ func insertChild(_ child: UIView, into parent: UIView, before anchor: UIView?) {
241
+ guard let label = parent as? UILabel else {
242
+ child.removeFromSuperview()
243
+ return
244
+ }
245
+
246
+ var children = storedTextChildren(on: label)
247
+ if let anchor = anchor, let index = children.firstIndex(where: { $0 === anchor }) {
248
+ children.insert(child, at: index)
249
+ } else {
250
+ children.append(child)
251
+ }
252
+
253
+ storeTextChildren(children, on: label)
254
+ rebuildText(from: children, on: label)
255
+ }
256
+
257
+ func removeChild(_ child: UIView, from parent: UIView) {
258
+ guard let label = parent as? UILabel else { return }
259
+ var children = storedTextChildren(on: label)
260
+ children.removeAll { $0 === child }
261
+ storeTextChildren(children, on: label)
262
+ rebuildText(from: children, on: label)
263
+ }
264
+
238
265
  // MARK: - Font rebuilding
239
266
 
240
267
  /// Rebuild the UIFont from stored fontSize, fontWeight, and fontFamily.
@@ -286,5 +313,21 @@ final class VTextFactory: NativeComponentFactory {
286
313
  private func storedFontFamily(on view: UIView) -> String? {
287
314
  return objc_getAssociatedObject(view, &VTextFactory.fontFamilyKey) as? String
288
315
  }
316
+
317
+ private func storeTextChildren(_ children: [UIView], on view: UIView) {
318
+ objc_setAssociatedObject(view, &VTextFactory.textChildrenKey, children, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
319
+ }
320
+
321
+ private func storedTextChildren(on view: UIView) -> [UIView] {
322
+ return objc_getAssociatedObject(view, &VTextFactory.textChildrenKey) as? [UIView] ?? []
323
+ }
324
+
325
+ private func rebuildText(from children: [UIView], on label: UILabel) {
326
+ let text = children.compactMap { child -> String? in
327
+ (child as? UILabel)?.text
328
+ }.joined()
329
+ label.text = text.isEmpty ? nil : text
330
+ label.flex.markDirty()
331
+ }
289
332
  }
290
333
  #endif