@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,132 @@
1
+ import AppKit
2
+ import ObjectiveC
3
+
4
+ /// Factory for VAlertDialog — presents an NSAlert when `visible=true`.
5
+ /// The view itself is a zero-size, hidden placeholder in the native tree.
6
+ final class VAlertDialogFactory: NativeComponentFactory {
7
+
8
+ // MARK: - Associated-object keys
9
+
10
+ private static var titleKey: UInt8 = 0
11
+ private static var messageKey: UInt8 = 1
12
+ private static var buttonsKey: UInt8 = 2
13
+ private static var onActionKey: UInt8 = 3
14
+
15
+ // MARK: - NativeComponentFactory
16
+
17
+ func createView() -> NSView {
18
+ let view = FlippedView()
19
+ view.isHidden = true
20
+ let node = view.ensureLayoutNode()
21
+ node.width = .points(0)
22
+ node.height = .points(0)
23
+ return view
24
+ }
25
+
26
+ func updateProp(view: NSView, key: String, value: Any?) {
27
+ switch key {
28
+ case "visible":
29
+ let visible = (value as? Bool) ?? ((value as? Int).map { $0 != 0 } ?? false)
30
+ if visible {
31
+ presentAlert(for: view)
32
+ }
33
+
34
+ case "title":
35
+ objc_setAssociatedObject(
36
+ view, &VAlertDialogFactory.titleKey,
37
+ value as? String,
38
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
39
+ )
40
+
41
+ case "message":
42
+ objc_setAssociatedObject(
43
+ view, &VAlertDialogFactory.messageKey,
44
+ value as? String,
45
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
46
+ )
47
+
48
+ case "buttons":
49
+ objc_setAssociatedObject(
50
+ view, &VAlertDialogFactory.buttonsKey,
51
+ value,
52
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
53
+ )
54
+
55
+ default:
56
+ break
57
+ }
58
+ }
59
+
60
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
61
+ switch event {
62
+ case "action":
63
+ objc_setAssociatedObject(
64
+ view, &VAlertDialogFactory.onActionKey,
65
+ handler as AnyObject,
66
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
67
+ )
68
+ default:
69
+ break
70
+ }
71
+ }
72
+
73
+ func removeEventListener(view: NSView, event: String) {
74
+ switch event {
75
+ case "action":
76
+ objc_setAssociatedObject(view, &VAlertDialogFactory.onActionKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
77
+ default:
78
+ break
79
+ }
80
+ }
81
+
82
+ // MARK: - Alert Presentation
83
+
84
+ private func presentAlert(for view: NSView) {
85
+ let title = objc_getAssociatedObject(view, &VAlertDialogFactory.titleKey) as? String
86
+ let message = objc_getAssociatedObject(view, &VAlertDialogFactory.messageKey) as? String
87
+
88
+ let alert = NSAlert()
89
+ alert.messageText = title ?? ""
90
+ alert.informativeText = message ?? ""
91
+ alert.alertStyle = .informational
92
+
93
+ // Parse buttons — supports both [String] and [[String: Any]] formats
94
+ var buttonLabels: [String] = []
95
+ if let buttonsArray = objc_getAssociatedObject(view, &VAlertDialogFactory.buttonsKey) as? [String] {
96
+ buttonLabels = buttonsArray
97
+ } else if let buttonsArray = objc_getAssociatedObject(view, &VAlertDialogFactory.buttonsKey) as? [[String: Any]] {
98
+ buttonLabels = buttonsArray.compactMap { $0["label"] as? String }
99
+ }
100
+
101
+ // Add buttons (first is default, last is cancel style if > 1)
102
+ if buttonLabels.isEmpty {
103
+ alert.addButton(withTitle: "OK")
104
+ buttonLabels = ["OK"]
105
+ } else {
106
+ for label in buttonLabels {
107
+ alert.addButton(withTitle: label)
108
+ }
109
+ }
110
+
111
+ // Present as sheet if we have a key window, otherwise run modal
112
+ if let window = NSApplication.shared.keyWindow {
113
+ alert.beginSheetModal(for: window) { [weak view] response in
114
+ guard let view = view else { return }
115
+ let index = response.rawValue - NSApplication.ModalResponse.alertFirstButtonReturn.rawValue
116
+ let label = index < buttonLabels.count ? buttonLabels[index] : ""
117
+ self.fireAction(for: view, buttonIndex: index, buttonLabel: label)
118
+ }
119
+ } else {
120
+ let response = alert.runModal()
121
+ let index = response.rawValue - NSApplication.ModalResponse.alertFirstButtonReturn.rawValue
122
+ let label = index < buttonLabels.count ? buttonLabels[index] : ""
123
+ fireAction(for: view, buttonIndex: index, buttonLabel: label)
124
+ }
125
+ }
126
+
127
+ private func fireAction(for view: NSView, buttonIndex: Int, buttonLabel: String) {
128
+ if let handler = objc_getAssociatedObject(view, &VAlertDialogFactory.onActionKey) as? ((Any?) -> Void) {
129
+ handler(["buttonIndex": buttonIndex, "buttonLabel": buttonLabel])
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,83 @@
1
+ import AppKit
2
+ import ObjectiveC
3
+
4
+ /// Factory for VButton — the clickable/pressable component.
5
+ /// Maps to a ClickableView (custom NSView subclass) with a LayoutNode.
6
+ /// Supports press and long press events with configurable active opacity.
7
+ final class VButtonFactory: NativeComponentFactory {
8
+
9
+ // MARK: - NativeComponentFactory
10
+
11
+ func createView() -> NSView {
12
+ let clickable = ClickableView()
13
+ // Ensure layout node is attached
14
+ clickable.ensureLayoutNode()
15
+ return clickable
16
+ }
17
+
18
+ func updateProp(view: NSView, key: String, value: Any?) {
19
+ guard let clickable = view as? ClickableView else {
20
+ // Fallback to StyleEngine for non-ClickableView
21
+ StyleEngine.apply(key: key, value: value, to: view)
22
+ return
23
+ }
24
+
25
+ switch key {
26
+ case "disabled":
27
+ if let disabled = value as? Bool {
28
+ clickable.isDisabled = disabled
29
+ } else if let disabled = value as? Int {
30
+ clickable.isDisabled = disabled != 0
31
+ } else {
32
+ clickable.isDisabled = false
33
+ }
34
+
35
+ case "activeOpacity":
36
+ if let opacity = value as? Double {
37
+ clickable.activeOpacity = CGFloat(opacity)
38
+ } else if let opacity = value as? Int {
39
+ clickable.activeOpacity = CGFloat(opacity)
40
+ } else {
41
+ clickable.activeOpacity = 0.7
42
+ }
43
+
44
+ default:
45
+ // Delegate to StyleEngine for layout/visual styling
46
+ StyleEngine.apply(key: key, value: value, to: view)
47
+ }
48
+ }
49
+
50
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
51
+ guard let clickable = view as? ClickableView else { return }
52
+
53
+ switch event {
54
+ case "press":
55
+ clickable.onPress = {
56
+ handler(nil)
57
+ }
58
+
59
+ case "longpress":
60
+ clickable.onLongPress = {
61
+ handler(nil)
62
+ }
63
+
64
+ default:
65
+ break
66
+ }
67
+ }
68
+
69
+ func removeEventListener(view: NSView, event: String) {
70
+ guard let clickable = view as? ClickableView else { return }
71
+
72
+ switch event {
73
+ case "press":
74
+ clickable.onPress = nil
75
+
76
+ case "longpress":
77
+ clickable.onLongPress = nil
78
+
79
+ default:
80
+ break
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,108 @@
1
+ import AppKit
2
+ import ObjectiveC
3
+
4
+ /// Factory for VCheckbox — a checkbox toggle control.
5
+ /// Maps to NSButton with .checkbox style.
6
+ final class VCheckboxFactory: NativeComponentFactory {
7
+
8
+ private static var changeHandlerKey: UInt8 = 0
9
+
10
+ // MARK: - NativeComponentFactory
11
+
12
+ func createView() -> NSView {
13
+ let button = NSButton()
14
+ button.setButtonType(.switch) // .switch is the checkbox type in AppKit
15
+ button.title = ""
16
+ button.state = .off
17
+ button.wantsLayer = true
18
+ button.ensureLayoutNode()
19
+ return button
20
+ }
21
+
22
+ func updateProp(view: NSView, key: String, value: Any?) {
23
+ guard let button = view as? NSButton else {
24
+ StyleEngine.apply(key: key, value: value, to: view)
25
+ return
26
+ }
27
+
28
+ switch key {
29
+ case "checked", "value":
30
+ let checked: Bool
31
+ if let val = value as? Bool {
32
+ checked = val
33
+ } else if let val = value as? Int {
34
+ checked = val != 0
35
+ } else {
36
+ checked = false
37
+ }
38
+ let newState: NSControl.StateValue = checked ? .on : .off
39
+ if button.state != newState {
40
+ button.state = newState
41
+ }
42
+
43
+ case "label", "title":
44
+ if let text = value as? String {
45
+ button.title = text
46
+ } else {
47
+ button.title = ""
48
+ }
49
+
50
+ case "disabled":
51
+ if let disabled = value as? Bool {
52
+ button.isEnabled = !disabled
53
+ } else if let disabled = value as? Int {
54
+ button.isEnabled = disabled == 0
55
+ } else {
56
+ button.isEnabled = true
57
+ }
58
+
59
+ case "tintColor":
60
+ if let colorStr = value as? String {
61
+ button.contentTintColor = NSColor.fromHex(colorStr)
62
+ } else {
63
+ button.contentTintColor = nil
64
+ }
65
+
66
+ default:
67
+ StyleEngine.apply(key: key, value: value, to: view)
68
+ }
69
+ }
70
+
71
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
72
+ guard let button = view as? NSButton, event == "change" else { return }
73
+
74
+ let proxy = CheckboxActionProxy(handler: handler)
75
+ button.target = proxy
76
+ button.action = #selector(CheckboxActionProxy.toggled(_:))
77
+ objc_setAssociatedObject(
78
+ button,
79
+ &VCheckboxFactory.changeHandlerKey,
80
+ proxy,
81
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
82
+ )
83
+ }
84
+
85
+ func removeEventListener(view: NSView, event: String) {
86
+ guard let button = view as? NSButton, event == "change" else { return }
87
+
88
+ button.target = nil
89
+ button.action = nil
90
+ objc_setAssociatedObject(button, &VCheckboxFactory.changeHandlerKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
91
+ }
92
+ }
93
+
94
+ // MARK: - CheckboxActionProxy
95
+
96
+ /// Target-action proxy that routes NSButton checkbox state changes to a closure handler.
97
+ final class CheckboxActionProxy: NSObject {
98
+
99
+ let handler: (Any?) -> Void
100
+
101
+ init(handler: @escaping (Any?) -> Void) {
102
+ self.handler = handler
103
+ }
104
+
105
+ @objc func toggled(_ sender: NSButton) {
106
+ handler(["checked": sender.state == .on])
107
+ }
108
+ }
@@ -0,0 +1,155 @@
1
+ import AppKit
2
+ import ObjectiveC
3
+
4
+ /// Factory for VDropdown — a dropdown selection control.
5
+ /// Maps to NSPopUpButton configured for dropdown selection.
6
+ final class VDropdownFactory: NativeComponentFactory {
7
+
8
+ private static var changeHandlerKey: UInt8 = 0
9
+ nonisolated(unsafe) static var optionsKey: UInt8 = 0
10
+ nonisolated(unsafe) static var placeholderKey: UInt8 = 0
11
+
12
+ // MARK: - NativeComponentFactory
13
+
14
+ func createView() -> NSView {
15
+ let popup = NSPopUpButton()
16
+ popup.pullsDown = false
17
+ popup.wantsLayer = true
18
+ popup.ensureLayoutNode()
19
+ return popup
20
+ }
21
+
22
+ func updateProp(view: NSView, key: String, value: Any?) {
23
+ guard let popup = view as? NSPopUpButton else {
24
+ StyleEngine.apply(key: key, value: value, to: view)
25
+ return
26
+ }
27
+
28
+ switch key {
29
+ case "options":
30
+ popup.removeAllItems()
31
+ var storedOptions: [[String: Any]] = []
32
+
33
+ // Add placeholder as first disabled item if set
34
+ let placeholder = objc_getAssociatedObject(popup, &VDropdownFactory.placeholderKey) as? String
35
+
36
+ if let options = value as? [[String: Any]] {
37
+ if let placeholder = placeholder {
38
+ popup.addItem(withTitle: placeholder)
39
+ popup.item(at: 0)?.isEnabled = false
40
+ }
41
+ for option in options {
42
+ let label = option["label"] as? String ?? option["value"] as? String ?? ""
43
+ popup.addItem(withTitle: label)
44
+ storedOptions.append(option)
45
+ }
46
+ } else if let strings = value as? [String] {
47
+ if let placeholder = placeholder {
48
+ popup.addItem(withTitle: placeholder)
49
+ popup.item(at: 0)?.isEnabled = false
50
+ }
51
+ for str in strings {
52
+ popup.addItem(withTitle: str)
53
+ storedOptions.append(["label": str, "value": str])
54
+ }
55
+ }
56
+ objc_setAssociatedObject(popup, &VDropdownFactory.optionsKey, storedOptions, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
57
+
58
+ case "selectedValue":
59
+ if let val = value as? String,
60
+ let options = objc_getAssociatedObject(popup, &VDropdownFactory.optionsKey) as? [[String: Any]] {
61
+ let offset = hasPlaceholder(popup) ? 1 : 0
62
+ if let idx = options.firstIndex(where: { ($0["value"] as? String) == val }) {
63
+ popup.selectItem(at: idx + offset)
64
+ }
65
+ }
66
+
67
+ case "placeholder":
68
+ objc_setAssociatedObject(popup, &VDropdownFactory.placeholderKey, value as? String, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
69
+ // Rebuild items if options are already set
70
+ if let options = objc_getAssociatedObject(popup, &VDropdownFactory.optionsKey) as? [[String: Any]], !options.isEmpty {
71
+ updateProp(view: view, key: "options", value: options)
72
+ }
73
+
74
+ case "disabled":
75
+ if let disabled = value as? Bool {
76
+ popup.isEnabled = !disabled
77
+ } else if let disabled = value as? Int {
78
+ popup.isEnabled = disabled == 0
79
+ } else {
80
+ popup.isEnabled = true
81
+ }
82
+
83
+ case "tintColor":
84
+ if let colorStr = value as? String {
85
+ popup.contentTintColor = NSColor.fromHex(colorStr)
86
+ } else {
87
+ popup.contentTintColor = nil
88
+ }
89
+
90
+ default:
91
+ StyleEngine.apply(key: key, value: value, to: view)
92
+ }
93
+ }
94
+
95
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
96
+ guard let popup = view as? NSPopUpButton, event == "change" else { return }
97
+
98
+ let proxy = DropdownActionProxy(popup: popup, handler: handler)
99
+ popup.target = proxy
100
+ popup.action = #selector(DropdownActionProxy.selectionChanged(_:))
101
+ objc_setAssociatedObject(
102
+ popup,
103
+ &VDropdownFactory.changeHandlerKey,
104
+ proxy,
105
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
106
+ )
107
+ }
108
+
109
+ func removeEventListener(view: NSView, event: String) {
110
+ guard let popup = view as? NSPopUpButton, event == "change" else { return }
111
+
112
+ popup.target = nil
113
+ popup.action = nil
114
+ objc_setAssociatedObject(popup, &VDropdownFactory.changeHandlerKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
115
+ }
116
+
117
+ // MARK: - Private
118
+
119
+ private func hasPlaceholder(_ popup: NSPopUpButton) -> Bool {
120
+ return objc_getAssociatedObject(popup, &VDropdownFactory.placeholderKey) as? String != nil
121
+ }
122
+ }
123
+
124
+ // MARK: - DropdownActionProxy
125
+
126
+ /// Target-action proxy that routes NSPopUpButton selection changes to a closure handler.
127
+ final class DropdownActionProxy: NSObject {
128
+
129
+ private weak var popup: NSPopUpButton?
130
+ private let handler: (Any?) -> Void
131
+
132
+ init(popup: NSPopUpButton, handler: @escaping (Any?) -> Void) {
133
+ self.popup = popup
134
+ self.handler = handler
135
+ }
136
+
137
+ @objc func selectionChanged(_ sender: NSPopUpButton) {
138
+ let options = objc_getAssociatedObject(sender, &VDropdownFactory.optionsKey) as? [[String: Any]] ?? []
139
+ let hasPlaceholder = objc_getAssociatedObject(sender, &VDropdownFactory.placeholderKey) as? String != nil
140
+ let rawIndex = sender.indexOfSelectedItem
141
+ let offset = hasPlaceholder ? 1 : 0
142
+ let optionIndex = rawIndex - offset
143
+
144
+ guard optionIndex >= 0 && optionIndex < options.count else { return }
145
+
146
+ let option = options[optionIndex]
147
+ let label = option["label"] as? String ?? sender.titleOfSelectedItem ?? ""
148
+ let value = option["value"] as? String ?? label
149
+
150
+ handler([
151
+ "value": value,
152
+ "label": label
153
+ ])
154
+ }
155
+ }