@thelacanians/vue-native-cli 0.4.15 → 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 (116) hide show
  1. package/dist/cli.js +329 -15
  2. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +118 -0
  3. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +178 -1
  4. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeneratedModuleRegistry.kt +28 -0
  5. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModuleRegistry.kt +3 -0
  6. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ComponentFactoryTest.kt +674 -0
  7. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ErrorOverlayViewTest.kt +183 -0
  8. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/EventThrottleTest.kt +203 -0
  9. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/HotReloadManagerTest.kt +162 -0
  10. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/JSPolyfillsTest.kt +153 -0
  11. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeBridgeTest.kt +6 -3
  12. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeModuleTest.kt +475 -0
  13. package/native/android/gradle.properties +1 -0
  14. package/native/android/gradlew +1 -1
  15. package/native/ios/VueNativeCore/Package.swift +1 -1
  16. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/EventThrottle.swift +1 -0
  17. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +143 -5
  18. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VTextFactory.swift +43 -0
  19. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VViewFactory.swift +116 -4
  20. package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/GestureWrapper.swift +100 -0
  21. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/GeneratedModuleRegistry.swift +28 -0
  22. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModuleRegistry.swift +3 -0
  23. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/CertificatePinningTests.swift +190 -0
  24. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/ComponentFactoryTests.swift +585 -0
  25. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/EventThrottleTests.swift +161 -0
  26. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/HotReloadManagerTests.swift +88 -0
  27. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/JSPolyfillsTests.swift +319 -0
  28. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/NativeModuleTests.swift +400 -0
  29. package/native/macos/VueNativeMacOS/Package.swift +34 -0
  30. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/ErrorOverlayView.swift +112 -0
  31. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/EventThrottle.swift +58 -0
  32. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/HotReloadManager.swift +153 -0
  33. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSPolyfills.swift +696 -0
  34. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSRuntime.swift +347 -0
  35. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/NativeBridge.swift +877 -0
  36. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/VueNativeWindowController.swift +125 -0
  37. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/ComponentRegistry.swift +209 -0
  38. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActionSheetFactory.swift +155 -0
  39. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActivityIndicatorFactory.swift +85 -0
  40. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VAlertDialogFactory.swift +132 -0
  41. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VButtonFactory.swift +83 -0
  42. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VCheckboxFactory.swift +108 -0
  43. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VDropdownFactory.swift +155 -0
  44. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VImageFactory.swift +270 -0
  45. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VInputFactory.swift +257 -0
  46. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VKeyboardAvoidingFactory.swift +22 -0
  47. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VListFactory.swift +324 -0
  48. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VModalFactory.swift +231 -0
  49. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VOutlineViewFactory.swift +276 -0
  50. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPickerFactory.swift +134 -0
  51. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPressableFactory.swift +120 -0
  52. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VProgressBarFactory.swift +71 -0
  53. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRadioFactory.swift +193 -0
  54. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRefreshControlFactory.swift +25 -0
  55. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSafeAreaFactory.swift +46 -0
  56. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VScrollViewFactory.swift +190 -0
  57. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSectionListFactory.swift +374 -0
  58. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSegmentedControlFactory.swift +125 -0
  59. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSliderFactory.swift +131 -0
  60. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSplitViewFactory.swift +215 -0
  61. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VStatusBarFactory.swift +25 -0
  62. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSwitchFactory.swift +92 -0
  63. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VTextFactory.swift +336 -0
  64. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VToolbarFactory.swift +212 -0
  65. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VVideoFactory.swift +245 -0
  66. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VViewFactory.swift +314 -0
  67. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VWebViewFactory.swift +162 -0
  68. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/NativeComponentFactory.swift +54 -0
  69. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/ClickableView.swift +100 -0
  70. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/Extensions.swift +23 -0
  71. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/GestureWrapper.swift +183 -0
  72. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/NSColor+Hex.swift +78 -0
  73. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/FlippedView.swift +19 -0
  74. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/LayoutNode.swift +493 -0
  75. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AnimationModule.swift +354 -0
  76. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AppStateModule.swift +62 -0
  77. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/BiometryModule.swift +60 -0
  78. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/CameraModule.swift +167 -0
  79. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ClipboardModule.swift +34 -0
  80. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DeviceInfoModule.swift +49 -0
  81. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DragDropModule.swift +50 -0
  82. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/FileDialogModule.swift +86 -0
  83. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/HapticsModule.swift +42 -0
  84. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/KeyboardModule.swift +28 -0
  85. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/LinkingModule.swift +49 -0
  86. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/MenuModule.swift +95 -0
  87. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NativeModuleRegistry.swift +63 -0
  88. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NotificationsModule.swift +112 -0
  89. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/PermissionsModule.swift +149 -0
  90. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ShareModule.swift +37 -0
  91. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/WindowModule.swift +71 -0
  92. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Resources/vue-native-placeholder.js +2 -0
  93. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Styling/StyleEngine.swift +885 -0
  94. package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/ComponentFactoryTests.swift +80 -0
  95. package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/VueNativeMacOSTests.swift +149 -0
  96. package/native/shared/VueNativeShared/AGENTS.md +129 -0
  97. package/native/shared/VueNativeShared/Package.swift +14 -0
  98. package/native/shared/VueNativeShared/Sources/VueNativeShared/CertificatePinning.swift +134 -0
  99. package/native/shared/VueNativeShared/Sources/VueNativeShared/EventThrottle.swift +78 -0
  100. package/native/shared/VueNativeShared/Sources/VueNativeShared/HotReloadManager.swift +162 -0
  101. package/native/shared/VueNativeShared/Sources/VueNativeShared/JSRuntime.swift +412 -0
  102. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AsyncStorageModule.swift +68 -0
  103. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AudioModule.swift +359 -0
  104. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/DatabaseModule.swift +259 -0
  105. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/FileSystemModule.swift +233 -0
  106. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/GeolocationModule.swift +156 -0
  107. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/NetworkModule.swift +59 -0
  108. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/PerformanceModule.swift +113 -0
  109. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/SecureStorageModule.swift +119 -0
  110. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/WebSocketModule.swift +212 -0
  111. package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeEventDispatcher.swift +6 -0
  112. package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModule.swift +26 -0
  113. package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModuleRegistry.swift +37 -0
  114. package/native/shared/VueNativeShared/Sources/VueNativeShared/SharedJSPolyfills.swift +673 -0
  115. package/native/shared/VueNativeShared/Tests/VueNativeSharedTests/VueNativeSharedTests.swift +44 -0
  116. package/package.json +8 -2
@@ -0,0 +1,270 @@
1
+ import AppKit
2
+ import ObjectiveC
3
+
4
+ /// Factory for VImage — async image loading component.
5
+ /// Maps to NSImageView with URL-based async loading via URLSession,
6
+ /// NSCache for in-memory caching, and various resize modes.
7
+ final class VImageFactory: NativeComponentFactory {
8
+
9
+ // MARK: - Associated object keys
10
+
11
+ private static var loadTaskKey: UInt8 = 0
12
+ private static var loadHandlerKey: UInt8 = 0
13
+ private static var errorHandlerKey: UInt8 = 0
14
+
15
+ // MARK: - Shared image cache
16
+
17
+ private static let imageCache = NSCache<NSString, NSImage>()
18
+
19
+ // MARK: - NativeComponentFactory
20
+
21
+ func createView() -> NSView {
22
+ let imageView = NSImageView()
23
+ imageView.imageScaling = .scaleProportionallyUpOrDown
24
+ imageView.wantsLayer = true
25
+ imageView.ensureLayoutNode()
26
+ return imageView
27
+ }
28
+
29
+ func updateProp(view: NSView, key: String, value: Any?) {
30
+ guard let imageView = view as? NSImageView else {
31
+ StyleEngine.apply(key: key, value: value, to: view)
32
+ return
33
+ }
34
+
35
+ switch key {
36
+ case "source", "src":
37
+ // Cancel any in-flight load
38
+ cancelLoad(on: view)
39
+
40
+ guard let source = value as? String, !source.isEmpty else {
41
+ imageView.image = nil
42
+ return
43
+ }
44
+
45
+ // Try loading as URL
46
+ if let url = URL(string: source), url.scheme == "https" || url.scheme == "http" {
47
+ loadImageFromURL(url, into: imageView)
48
+ } else {
49
+ // Try loading as local resource name
50
+ if let image = NSImage(named: source) {
51
+ imageView.image = image
52
+ fireLoadEvent(for: imageView, image: image)
53
+ } else if let image = loadFromBundle(source) {
54
+ imageView.image = image
55
+ fireLoadEvent(for: imageView, image: image)
56
+ } else {
57
+ fireErrorEvent(for: imageView, message: "Image not found: \(source)")
58
+ }
59
+ }
60
+
61
+ case "resizeMode":
62
+ guard let mode = value as? String else { return }
63
+ switch mode {
64
+ case "cover":
65
+ imageView.imageScaling = .scaleProportionallyUpOrDown
66
+ imageView.layer?.contentsGravity = .resizeAspectFill
67
+ imageView.layer?.masksToBounds = true
68
+ case "contain":
69
+ imageView.imageScaling = .scaleProportionallyDown
70
+ imageView.layer?.contentsGravity = .resizeAspect
71
+ case "stretch":
72
+ imageView.imageScaling = .scaleAxesIndependently
73
+ imageView.layer?.contentsGravity = .resize
74
+ case "center":
75
+ imageView.imageScaling = .scaleNone
76
+ imageView.layer?.contentsGravity = .center
77
+ default:
78
+ imageView.imageScaling = .scaleProportionallyUpOrDown
79
+ }
80
+
81
+ case "tintColor":
82
+ if let colorStr = value as? String {
83
+ imageView.contentTintColor = NSColor.fromHex(colorStr)
84
+ // Set image as template so tint applies
85
+ imageView.image?.isTemplate = true
86
+ } else {
87
+ imageView.contentTintColor = nil
88
+ }
89
+
90
+ case "blurRadius":
91
+ if let radius = value as? Double, radius > 0 {
92
+ applyBlur(to: imageView, radius: radius)
93
+ } else {
94
+ // Remove blur
95
+ imageView.layer?.filters = nil
96
+ }
97
+
98
+ case "defaultSource":
99
+ // Set placeholder image
100
+ if let source = value as? String {
101
+ if let image = NSImage(named: source) {
102
+ // Only set if no image is currently loaded
103
+ if imageView.image == nil {
104
+ imageView.image = image
105
+ }
106
+ } else if let image = loadFromBundle(source) {
107
+ if imageView.image == nil {
108
+ imageView.image = image
109
+ }
110
+ }
111
+ }
112
+
113
+ default:
114
+ StyleEngine.apply(key: key, value: value, to: view)
115
+ }
116
+ }
117
+
118
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
119
+ switch event {
120
+ case "load":
121
+ objc_setAssociatedObject(
122
+ view, &VImageFactory.loadHandlerKey,
123
+ handler as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
124
+ )
125
+
126
+ case "error":
127
+ objc_setAssociatedObject(
128
+ view, &VImageFactory.errorHandlerKey,
129
+ handler as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
130
+ )
131
+
132
+ default:
133
+ break
134
+ }
135
+ }
136
+
137
+ func removeEventListener(view: NSView, event: String) {
138
+ switch event {
139
+ case "load":
140
+ objc_setAssociatedObject(
141
+ view, &VImageFactory.loadHandlerKey,
142
+ nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
143
+ )
144
+
145
+ case "error":
146
+ objc_setAssociatedObject(
147
+ view, &VImageFactory.errorHandlerKey,
148
+ nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
149
+ )
150
+
151
+ default:
152
+ break
153
+ }
154
+ }
155
+
156
+ // MARK: - Async image loading
157
+
158
+ private func loadImageFromURL(_ url: URL, into imageView: NSImageView) {
159
+ let cacheKey = url.absoluteString as NSString
160
+
161
+ // Check cache first
162
+ if let cached = VImageFactory.imageCache.object(forKey: cacheKey) {
163
+ imageView.image = cached
164
+ fireLoadEvent(for: imageView, image: cached)
165
+ return
166
+ }
167
+
168
+ let task = URLSession.shared.dataTask(with: url) { [weak imageView] data, response, error in
169
+ DispatchQueue.main.async {
170
+ guard let imageView = imageView else { return }
171
+
172
+ if let error = error {
173
+ self.fireErrorEvent(for: imageView, message: error.localizedDescription)
174
+ return
175
+ }
176
+
177
+ guard let data = data, let image = NSImage(data: data) else {
178
+ self.fireErrorEvent(for: imageView, message: "Failed to decode image data")
179
+ return
180
+ }
181
+
182
+ // Cache the image
183
+ VImageFactory.imageCache.setObject(image, forKey: cacheKey)
184
+
185
+ imageView.image = image
186
+ self.fireLoadEvent(for: imageView, image: image)
187
+ }
188
+ }
189
+ task.resume()
190
+
191
+ // Store the task so we can cancel it if source changes
192
+ objc_setAssociatedObject(
193
+ imageView, &VImageFactory.loadTaskKey,
194
+ task, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
195
+ )
196
+ }
197
+
198
+ private func cancelLoad(on view: NSView) {
199
+ if let task = objc_getAssociatedObject(view, &VImageFactory.loadTaskKey) as? URLSessionDataTask {
200
+ task.cancel()
201
+ objc_setAssociatedObject(
202
+ view, &VImageFactory.loadTaskKey,
203
+ nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
204
+ )
205
+ }
206
+ }
207
+
208
+ private func loadFromBundle(_ name: String) -> NSImage? {
209
+ // Try loading from main bundle by path
210
+ if let path = Bundle.main.path(forResource: name, ofType: nil) {
211
+ return NSImage(contentsOfFile: path)
212
+ }
213
+ // Try without extension
214
+ let components = name.split(separator: ".")
215
+ if components.count >= 2 {
216
+ let baseName = String(components.dropLast().joined(separator: "."))
217
+ let ext = String(components.last!)
218
+ if let path = Bundle.main.path(forResource: baseName, ofType: ext) {
219
+ return NSImage(contentsOfFile: path)
220
+ }
221
+ }
222
+ return nil
223
+ }
224
+
225
+ // MARK: - Event helpers
226
+
227
+ private func fireLoadEvent(for imageView: NSImageView, image: NSImage) {
228
+ guard let handler = objc_getAssociatedObject(
229
+ imageView, &VImageFactory.loadHandlerKey
230
+ ) as? (Any?) -> Void else { return }
231
+
232
+ let payload: [String: Any] = [
233
+ "width": image.size.width,
234
+ "height": image.size.height
235
+ ]
236
+ handler(payload)
237
+ }
238
+
239
+ private func fireErrorEvent(for imageView: NSImageView, message: String) {
240
+ guard let handler = objc_getAssociatedObject(
241
+ imageView, &VImageFactory.errorHandlerKey
242
+ ) as? (Any?) -> Void else { return }
243
+
244
+ let payload: [String: Any] = [
245
+ "error": message
246
+ ]
247
+ handler(payload)
248
+ }
249
+
250
+ // MARK: - Blur effect
251
+
252
+ private func applyBlur(to imageView: NSImageView, radius: Double) {
253
+ guard let image = imageView.image,
254
+ let tiffData = image.tiffRepresentation,
255
+ let ciImage = CIImage(data: tiffData) else { return }
256
+
257
+ let filter = CIFilter(name: "CIGaussianBlur")
258
+ filter?.setValue(ciImage, forKey: kCIInputImageKey)
259
+ filter?.setValue(radius, forKey: kCIInputRadiusKey)
260
+
261
+ guard let outputImage = filter?.outputImage else { return }
262
+
263
+ let context = CIContext()
264
+ let extent = ciImage.extent
265
+ guard let cgImage = context.createCGImage(outputImage, from: extent) else { return }
266
+
267
+ let blurredImage = NSImage(cgImage: cgImage, size: image.size)
268
+ imageView.image = blurredImage
269
+ }
270
+ }
@@ -0,0 +1,257 @@
1
+ import AppKit
2
+ import ObjectiveC
3
+
4
+ /// Factory for VInput — the text input component.
5
+ /// Maps to an NSTextField (editable) with a LayoutNode.
6
+ /// Supports v-model via text prop and changetext event.
7
+ /// For secure text entry, swaps to an NSSecureTextField.
8
+ final class VInputFactory: NativeComponentFactory {
9
+
10
+ // MARK: - Associated object keys
11
+
12
+ private static var delegateKey: UInt8 = 0
13
+ private static var changeTextHandlerKey: UInt8 = 0
14
+ private static var focusHandlerKey: UInt8 = 0
15
+ private static var blurHandlerKey: UInt8 = 0
16
+ private static var submitHandlerKey: UInt8 = 0
17
+ private static var maxLengthKey: UInt8 = 0
18
+
19
+ // MARK: - NativeComponentFactory
20
+
21
+ func createView() -> NSView {
22
+ let textField = NSTextField()
23
+ textField.isBordered = true
24
+ textField.isEditable = true
25
+ textField.isSelectable = true
26
+ textField.wantsLayer = true
27
+ // Set a sensible default height so the text field is not collapsed
28
+ let node = textField.ensureLayoutNode()
29
+ node.height = .points(28)
30
+ return textField
31
+ }
32
+
33
+ func updateProp(view: NSView, key: String, value: Any?) {
34
+ guard let textField = view as? NSTextField else {
35
+ StyleEngine.apply(key: key, value: value, to: view)
36
+ return
37
+ }
38
+
39
+ switch key {
40
+ case "text", "value":
41
+ if let text = value as? String {
42
+ // Only update if different to avoid cursor jump
43
+ if textField.stringValue != text {
44
+ textField.stringValue = text
45
+ }
46
+ } else {
47
+ textField.stringValue = ""
48
+ }
49
+
50
+ case "placeholder":
51
+ if let placeholder = value as? String {
52
+ textField.placeholderString = placeholder
53
+ } else {
54
+ textField.placeholderString = nil
55
+ }
56
+
57
+ case "placeholderColor", "placeholderTextColor":
58
+ if let colorStr = value as? String, let placeholder = textField.placeholderString {
59
+ textField.placeholderAttributedString = NSAttributedString(
60
+ string: placeholder,
61
+ attributes: [.foregroundColor: NSColor.fromHex(colorStr)]
62
+ )
63
+ }
64
+
65
+ case "secureTextEntry":
66
+ // Note: NSSecureTextField is a subclass, not a property toggle.
67
+ // Full secure text entry requires view replacement at the bridge level.
68
+ // Here we just store the prop for reference.
69
+ let secure: Bool
70
+ if let val = value as? Bool {
71
+ secure = val
72
+ } else if let val = value as? Int {
73
+ secure = val != 0
74
+ } else {
75
+ secure = false
76
+ }
77
+ StyleEngine.setInternalPropDirect("__secureTextEntry", value: secure, on: view)
78
+
79
+ case "editable":
80
+ if let editable = value as? Bool {
81
+ textField.isEditable = editable
82
+ } else {
83
+ textField.isEditable = true
84
+ }
85
+
86
+ case "maxLength":
87
+ if let maxLen = value as? Int {
88
+ storeMaxLength(maxLen, on: textField)
89
+ } else if let maxLen = value as? Double {
90
+ storeMaxLength(Int(maxLen), on: textField)
91
+ }
92
+
93
+ case "color":
94
+ if let colorStr = value as? String {
95
+ textField.textColor = NSColor.fromHex(colorStr)
96
+ } else {
97
+ textField.textColor = .labelColor
98
+ }
99
+
100
+ case "fontSize":
101
+ if let size = value as? Double {
102
+ textField.font = NSFont.systemFont(ofSize: CGFloat(size))
103
+ } else if let size = value as? Int {
104
+ textField.font = NSFont.systemFont(ofSize: CGFloat(size))
105
+ }
106
+
107
+ case "textAlign":
108
+ if let alignStr = value as? String {
109
+ switch alignStr {
110
+ case "left": textField.alignment = .left
111
+ case "center": textField.alignment = .center
112
+ case "right": textField.alignment = .right
113
+ default: textField.alignment = .natural
114
+ }
115
+ }
116
+
117
+ default:
118
+ StyleEngine.apply(key: key, value: value, to: view)
119
+ }
120
+ }
121
+
122
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
123
+ guard let textField = view as? NSTextField else { return }
124
+
125
+ // Ensure we have a delegate proxy set up
126
+ let delegate = ensureDelegate(for: textField)
127
+
128
+ switch event {
129
+ case "changetext":
130
+ delegate.onChangeText = handler
131
+ objc_setAssociatedObject(
132
+ view,
133
+ &VInputFactory.changeTextHandlerKey,
134
+ handler as AnyObject,
135
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
136
+ )
137
+
138
+ case "focus":
139
+ delegate.onFocus = handler
140
+ objc_setAssociatedObject(
141
+ view,
142
+ &VInputFactory.focusHandlerKey,
143
+ handler as AnyObject,
144
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
145
+ )
146
+
147
+ case "blur":
148
+ delegate.onBlur = handler
149
+ objc_setAssociatedObject(
150
+ view,
151
+ &VInputFactory.blurHandlerKey,
152
+ handler as AnyObject,
153
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
154
+ )
155
+
156
+ case "submit":
157
+ delegate.onSubmit = handler
158
+ objc_setAssociatedObject(
159
+ view,
160
+ &VInputFactory.submitHandlerKey,
161
+ handler as AnyObject,
162
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
163
+ )
164
+
165
+ default:
166
+ break
167
+ }
168
+ }
169
+
170
+ func removeEventListener(view: NSView, event: String) {
171
+ guard let textField = view as? NSTextField else { return }
172
+ guard let delegate = objc_getAssociatedObject(textField, &VInputFactory.delegateKey) as? InputDelegateProxy else { return }
173
+
174
+ switch event {
175
+ case "changetext":
176
+ delegate.onChangeText = nil
177
+ case "focus":
178
+ delegate.onFocus = nil
179
+ case "blur":
180
+ delegate.onBlur = nil
181
+ case "submit":
182
+ delegate.onSubmit = nil
183
+ default:
184
+ break
185
+ }
186
+ }
187
+
188
+ // MARK: - Private helpers
189
+
190
+ private func ensureDelegate(for textField: NSTextField) -> InputDelegateProxy {
191
+ if let existing = objc_getAssociatedObject(textField, &VInputFactory.delegateKey) as? InputDelegateProxy {
192
+ return existing
193
+ }
194
+
195
+ let delegate = InputDelegateProxy()
196
+ delegate.maxLengthProvider = { [weak textField] in
197
+ guard let tf = textField else { return nil }
198
+ return self.storedMaxLength(on: tf)
199
+ }
200
+ textField.delegate = delegate
201
+ objc_setAssociatedObject(
202
+ textField,
203
+ &VInputFactory.delegateKey,
204
+ delegate,
205
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
206
+ )
207
+ return delegate
208
+ }
209
+
210
+ // MARK: - Max length storage
211
+
212
+ private func storeMaxLength(_ length: Int, on view: NSView) {
213
+ objc_setAssociatedObject(view, &VInputFactory.maxLengthKey, length, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
214
+ }
215
+
216
+ private func storedMaxLength(on view: NSView) -> Int? {
217
+ return objc_getAssociatedObject(view, &VInputFactory.maxLengthKey) as? Int
218
+ }
219
+ }
220
+
221
+ // MARK: - InputDelegateProxy
222
+
223
+ /// NSTextFieldDelegate proxy that routes text field events to closure-based handlers.
224
+ /// Stored as an associated object on the NSTextField.
225
+ final class InputDelegateProxy: NSObject, NSTextFieldDelegate {
226
+
227
+ var onChangeText: ((Any?) -> Void)?
228
+ var onFocus: ((Any?) -> Void)?
229
+ var onBlur: ((Any?) -> Void)?
230
+ var onSubmit: ((Any?) -> Void)?
231
+ var maxLengthProvider: (() -> Int?)?
232
+
233
+ // MARK: - NSTextFieldDelegate
234
+
235
+ func controlTextDidChange(_ obj: Notification) {
236
+ guard let textField = obj.object as? NSTextField else { return }
237
+ // Enforce max length
238
+ if let maxLength = maxLengthProvider?(), textField.stringValue.count > maxLength {
239
+ textField.stringValue = String(textField.stringValue.prefix(maxLength))
240
+ }
241
+ onChangeText?(textField.stringValue)
242
+ }
243
+
244
+ func controlTextDidBeginEditing(_ obj: Notification) {
245
+ onFocus?(nil)
246
+ }
247
+
248
+ func controlTextDidEndEditing(_ obj: Notification) {
249
+ onBlur?(nil)
250
+ // Check if editing ended due to Return key
251
+ if let textField = obj.object as? NSTextField,
252
+ let movement = obj.userInfo?["NSTextMovement"] as? Int,
253
+ movement == NSReturnTextMovement {
254
+ onSubmit?(textField.stringValue)
255
+ }
256
+ }
257
+ }
@@ -0,0 +1,22 @@
1
+ import AppKit
2
+
3
+ /// Factory for VKeyboardAvoiding — pass-through container on macOS.
4
+ /// macOS keyboards do not cover content (unlike iOS on-screen keyboards),
5
+ /// so this component acts as a plain FlippedView container for API compatibility.
6
+ final class VKeyboardAvoidingFactory: NativeComponentFactory {
7
+
8
+ func createView() -> NSView {
9
+ let view = FlippedView()
10
+ view.ensureLayoutNode()
11
+ return view
12
+ }
13
+
14
+ func updateProp(view: NSView, key: String, value: Any?) {
15
+ // Delegate all props to StyleEngine — acts as a plain container
16
+ StyleEngine.apply(key: key, value: value, to: view)
17
+ }
18
+
19
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
20
+ // No-op — no keyboard avoidance on macOS
21
+ }
22
+ }