@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,215 @@
1
+ import AppKit
2
+ import ObjectiveC
3
+
4
+ /// Factory for VSplitView — macOS-specific split pane component.
5
+ /// Maps to NSSplitView for side-by-side or top-and-bottom pane arrangements.
6
+ ///
7
+ /// Props:
8
+ /// - direction: "horizontal" | "vertical" (horizontal = side-by-side, vertical = top-bottom)
9
+ /// - dividerStyle: "thin" | "thick" | "paneSplitter"
10
+ /// - dividerColor: hex color string
11
+ /// - dividerPosition: CGFloat (position of the first divider in points)
12
+ ///
13
+ /// Events:
14
+ /// - resize -> { positions: [CGFloat] }
15
+ final class VSplitViewFactory: NativeComponentFactory {
16
+
17
+ // MARK: - Associated object keys
18
+
19
+ private static var delegateKey: UInt8 = 0
20
+ private static var resizeHandlerKey: UInt8 = 0
21
+ nonisolated(unsafe) static var dividerColorKey: UInt8 = 0
22
+
23
+ // MARK: - NativeComponentFactory
24
+
25
+ func createView() -> NSView {
26
+ let splitView = VNSplitView()
27
+ splitView.isVertical = true // side-by-side by default
28
+ splitView.dividerStyle = .thin
29
+ splitView.wantsLayer = true
30
+ splitView.ensureLayoutNode()
31
+
32
+ let delegate = SplitViewDelegate(factory: self, view: splitView)
33
+ splitView.delegate = delegate
34
+ objc_setAssociatedObject(
35
+ splitView, &VSplitViewFactory.delegateKey,
36
+ delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
37
+ )
38
+
39
+ return splitView
40
+ }
41
+
42
+ func updateProp(view: NSView, key: String, value: Any?) {
43
+ guard let splitView = view as? NSSplitView else {
44
+ StyleEngine.apply(key: key, value: value, to: view)
45
+ return
46
+ }
47
+
48
+ switch key {
49
+ case "direction":
50
+ if let direction = value as? String {
51
+ // "horizontal" means side-by-side (isVertical = true in NSSplitView terminology)
52
+ // "vertical" means top-and-bottom (isVertical = false)
53
+ splitView.isVertical = (direction == "horizontal")
54
+ }
55
+
56
+ case "dividerStyle":
57
+ if let style = value as? String {
58
+ switch style {
59
+ case "thin":
60
+ splitView.dividerStyle = .thin
61
+ case "thick":
62
+ splitView.dividerStyle = .thick
63
+ case "paneSplitter":
64
+ splitView.dividerStyle = .paneSplitter
65
+ default:
66
+ splitView.dividerStyle = .thin
67
+ }
68
+ }
69
+
70
+ case "dividerColor":
71
+ if let hex = value as? String {
72
+ let color = NSColor.fromHex(hex)
73
+ objc_setAssociatedObject(
74
+ view, &VSplitViewFactory.dividerColorKey,
75
+ color, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
76
+ )
77
+ splitView.needsDisplay = true
78
+ }
79
+
80
+ case "dividerPosition":
81
+ let position: CGFloat
82
+ if let p = value as? Double {
83
+ position = CGFloat(p)
84
+ } else if let p = value as? Int {
85
+ position = CGFloat(p)
86
+ } else {
87
+ return
88
+ }
89
+ if splitView.arrangedSubviews.count >= 2 {
90
+ splitView.setPosition(position, ofDividerAt: 0)
91
+ }
92
+
93
+ default:
94
+ StyleEngine.apply(key: key, value: value, to: view)
95
+ }
96
+ }
97
+
98
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
99
+ switch event {
100
+ case "resize":
101
+ objc_setAssociatedObject(
102
+ view, &VSplitViewFactory.resizeHandlerKey,
103
+ handler as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
104
+ )
105
+
106
+ default:
107
+ break
108
+ }
109
+ }
110
+
111
+ func removeEventListener(view: NSView, event: String) {
112
+ switch event {
113
+ case "resize":
114
+ objc_setAssociatedObject(
115
+ view, &VSplitViewFactory.resizeHandlerKey,
116
+ nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
117
+ )
118
+
119
+ default:
120
+ break
121
+ }
122
+ }
123
+
124
+ // MARK: - Child management
125
+
126
+ func insertChild(_ child: NSView, into parent: NSView, before anchor: NSView?) {
127
+ guard let splitView = parent as? NSSplitView else {
128
+ if let anchor = anchor, parent.subviews.contains(anchor) {
129
+ parent.addSubview(child, positioned: .below, relativeTo: anchor)
130
+ } else {
131
+ parent.addSubview(child)
132
+ }
133
+ child.ensureLayoutNode()
134
+ return
135
+ }
136
+
137
+ if let anchor = anchor, let idx = splitView.arrangedSubviews.firstIndex(of: anchor) {
138
+ splitView.insertArrangedSubview(child, at: idx)
139
+ } else {
140
+ splitView.addArrangedSubview(child)
141
+ }
142
+ child.ensureLayoutNode()
143
+ }
144
+
145
+ func removeChild(_ child: NSView, from parent: NSView) {
146
+ guard let splitView = parent as? NSSplitView else {
147
+ child.removeFromSuperview()
148
+ return
149
+ }
150
+ splitView.removeArrangedSubview(child)
151
+ child.removeFromSuperview()
152
+ }
153
+
154
+ // MARK: - Internal resize notification
155
+
156
+ fileprivate func notifyResize(view: NSSplitView) {
157
+ guard let handler = objc_getAssociatedObject(
158
+ view, &VSplitViewFactory.resizeHandlerKey
159
+ ) as? (Any?) -> Void else { return }
160
+
161
+ var positions: [CGFloat] = []
162
+ let count = max(0, view.arrangedSubviews.count - 1)
163
+ for i in 0..<count {
164
+ if view.isVertical {
165
+ var pos: CGFloat = 0
166
+ for j in 0...i {
167
+ pos += view.arrangedSubviews[j].frame.width
168
+ if j < i { pos += view.dividerThickness }
169
+ }
170
+ positions.append(pos)
171
+ } else {
172
+ var pos: CGFloat = 0
173
+ for j in 0...i {
174
+ pos += view.arrangedSubviews[j].frame.height
175
+ if j < i { pos += view.dividerThickness }
176
+ }
177
+ positions.append(pos)
178
+ }
179
+ }
180
+
181
+ handler(["positions": positions])
182
+ }
183
+ }
184
+
185
+ // MARK: - VNSplitView (custom divider color support)
186
+
187
+ private class VNSplitView: NSSplitView {
188
+ override var dividerColor: NSColor {
189
+ if let custom = objc_getAssociatedObject(
190
+ self, &VSplitViewFactory.dividerColorKey
191
+ ) as? NSColor {
192
+ return custom
193
+ }
194
+ return super.dividerColor
195
+ }
196
+ }
197
+
198
+ // MARK: - SplitViewDelegate
199
+
200
+ private final class SplitViewDelegate: NSObject, NSSplitViewDelegate {
201
+
202
+ private weak var factory: VSplitViewFactory?
203
+ private weak var view: NSSplitView?
204
+
205
+ init(factory: VSplitViewFactory, view: NSSplitView) {
206
+ self.factory = factory
207
+ self.view = view
208
+ super.init()
209
+ }
210
+
211
+ func splitViewDidResizeSubviews(_ notification: Notification) {
212
+ guard let view = view, let factory = factory else { return }
213
+ factory.notifyResize(view: view)
214
+ }
215
+ }
@@ -0,0 +1,25 @@
1
+ import AppKit
2
+
3
+ /// Factory for VStatusBar — no-op on macOS.
4
+ /// macOS does not have an app status bar like iOS. This factory exists
5
+ /// for API compatibility so that cross-platform code using VStatusBar
6
+ /// does not break on macOS.
7
+ final class VStatusBarFactory: NativeComponentFactory {
8
+
9
+ func createView() -> NSView {
10
+ let view = FlippedView()
11
+ view.isHidden = true
12
+ let node = view.ensureLayoutNode()
13
+ node.width = .points(0)
14
+ node.height = .points(0)
15
+ return view
16
+ }
17
+
18
+ func updateProp(view: NSView, key: String, value: Any?) {
19
+ // No-op — macOS has no app status bar
20
+ }
21
+
22
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
23
+ // No-op
24
+ }
25
+ }
@@ -0,0 +1,92 @@
1
+ import AppKit
2
+ import ObjectiveC
3
+
4
+ /// Factory for VSwitch — the boolean toggle switch component.
5
+ /// Maps to NSSwitch (macOS 10.15+) with a LayoutNode.
6
+ /// Supports v-model via the `value` prop and `change` event.
7
+ final class VSwitchFactory: NativeComponentFactory {
8
+
9
+ private static var changeHandlerKey: UInt8 = 0
10
+
11
+ // MARK: - NativeComponentFactory
12
+
13
+ func createView() -> NSView {
14
+ let sw = NSSwitch()
15
+ sw.wantsLayer = true
16
+ sw.ensureLayoutNode()
17
+ return sw
18
+ }
19
+
20
+ func updateProp(view: NSView, key: String, value: Any?) {
21
+ guard let sw = view as? NSSwitch else {
22
+ StyleEngine.apply(key: key, value: value, to: view)
23
+ return
24
+ }
25
+
26
+ switch key {
27
+ case "value":
28
+ let newValue: NSControl.StateValue
29
+ if let val = value as? Bool {
30
+ newValue = val ? .on : .off
31
+ } else if let val = value as? Int {
32
+ newValue = val != 0 ? .on : .off
33
+ } else {
34
+ newValue = .off
35
+ }
36
+ if sw.state != newValue {
37
+ sw.state = newValue
38
+ }
39
+
40
+ case "disabled":
41
+ if let disabled = value as? Bool {
42
+ sw.isEnabled = !disabled
43
+ } else if let disabled = value as? Int {
44
+ sw.isEnabled = disabled == 0
45
+ } else {
46
+ sw.isEnabled = true
47
+ }
48
+
49
+ default:
50
+ StyleEngine.apply(key: key, value: value, to: view)
51
+ }
52
+ }
53
+
54
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
55
+ guard let sw = view as? NSSwitch, event == "change" else { return }
56
+
57
+ let proxy = SwitchActionProxy(handler: handler)
58
+ sw.target = proxy
59
+ sw.action = #selector(SwitchActionProxy.valueChanged(_:))
60
+ objc_setAssociatedObject(
61
+ sw,
62
+ &VSwitchFactory.changeHandlerKey,
63
+ proxy,
64
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
65
+ )
66
+ }
67
+
68
+ func removeEventListener(view: NSView, event: String) {
69
+ guard let sw = view as? NSSwitch, event == "change" else { return }
70
+
71
+ sw.target = nil
72
+ sw.action = nil
73
+ objc_setAssociatedObject(sw, &VSwitchFactory.changeHandlerKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
74
+ }
75
+ }
76
+
77
+ // MARK: - SwitchActionProxy
78
+
79
+ /// Target-action proxy that routes NSSwitch value changes to a closure handler.
80
+ /// Stored as an associated object on the NSSwitch.
81
+ final class SwitchActionProxy: NSObject {
82
+
83
+ let handler: (Any?) -> Void
84
+
85
+ init(handler: @escaping (Any?) -> Void) {
86
+ self.handler = handler
87
+ }
88
+
89
+ @objc func valueChanged(_ sender: NSSwitch) {
90
+ handler(sender.state == .on)
91
+ }
92
+ }
@@ -0,0 +1,336 @@
1
+ import AppKit
2
+ import ObjectiveC
3
+
4
+ /// Factory for VText — the text display component.
5
+ /// Maps to an NSTextField configured as a non-editable label.
6
+ /// After any text or font change, calls layoutNode.markDirty() to trigger remeasurement.
7
+ final class VTextFactory: NativeComponentFactory {
8
+
9
+ // MARK: - Font weight mapping
10
+
11
+ /// Maps CSS-like font weight strings to NSFont.Weight values.
12
+ static let fontWeightMap: [String: NSFont.Weight] = [
13
+ "100": .ultraLight,
14
+ "200": .thin,
15
+ "300": .light,
16
+ "400": .regular,
17
+ "normal": .regular,
18
+ "500": .medium,
19
+ "600": .semibold,
20
+ "semibold": .semibold,
21
+ "700": .bold,
22
+ "bold": .bold,
23
+ "800": .heavy,
24
+ "900": .black,
25
+ ]
26
+
27
+ // MARK: - Text alignment mapping
28
+
29
+ static let textAlignMap: [String: NSTextAlignment] = [
30
+ "left": .left,
31
+ "center": .center,
32
+ "right": .right,
33
+ "justify": .justified,
34
+ "auto": .natural,
35
+ ]
36
+
37
+ // MARK: - Associated object keys for stored state
38
+
39
+ private static var fontSizeKey: UInt8 = 0
40
+ private static var fontWeightKey: UInt8 = 0
41
+ private static var fontFamilyKey: UInt8 = 0
42
+ private static var textChildrenKey: UInt8 = 0
43
+
44
+ // MARK: - NativeComponentFactory
45
+
46
+ func createView() -> NSView {
47
+ let label = NSTextField(labelWithString: "")
48
+ label.isEditable = false
49
+ label.isBordered = false
50
+ label.drawsBackground = false
51
+ label.lineBreakMode = .byWordWrapping
52
+ label.maximumNumberOfLines = 0
53
+ label.wantsLayer = true
54
+ // Ensure layout node is attached
55
+ label.ensureLayoutNode()
56
+ return label
57
+ }
58
+
59
+ func updateProp(view: NSView, key: String, value: Any?) {
60
+ guard let label = view as? NSTextField else { return }
61
+
62
+ switch key {
63
+ case "text":
64
+ storeTextChildren([], on: label)
65
+ if let text = value as? String {
66
+ label.stringValue = text
67
+ } else {
68
+ label.stringValue = ""
69
+ }
70
+ label.layoutNode?.markDirty()
71
+
72
+ case "numberOfLines":
73
+ if let lines = value as? Int {
74
+ label.maximumNumberOfLines = lines
75
+ } else if let lines = value as? Double {
76
+ label.maximumNumberOfLines = Int(lines)
77
+ } else {
78
+ label.maximumNumberOfLines = 0
79
+ }
80
+ label.layoutNode?.markDirty()
81
+
82
+ case "color":
83
+ if let colorStr = value as? String {
84
+ label.textColor = NSColor.fromHex(colorStr)
85
+ } else {
86
+ label.textColor = .labelColor
87
+ }
88
+
89
+ case "fontSize":
90
+ let size: CGFloat
91
+ if let num = value as? Double {
92
+ size = CGFloat(num)
93
+ } else if let num = value as? Int {
94
+ size = CGFloat(num)
95
+ } else {
96
+ size = 13.0 // macOS system default
97
+ }
98
+ storeFontSize(size, on: label)
99
+ rebuildFont(for: label)
100
+ label.layoutNode?.markDirty()
101
+
102
+ case "fontWeight":
103
+ if let str = value as? String {
104
+ storeFontWeight(str, on: label)
105
+ } else {
106
+ storeFontWeight(nil, on: label)
107
+ }
108
+ rebuildFont(for: label)
109
+ label.layoutNode?.markDirty()
110
+
111
+ case "fontFamily":
112
+ if let family = value as? String {
113
+ storeFontFamily(family, on: label)
114
+ } else {
115
+ storeFontFamily(nil, on: label)
116
+ }
117
+ rebuildFont(for: label)
118
+ label.layoutNode?.markDirty()
119
+
120
+ case "textAlign":
121
+ if let alignStr = value as? String {
122
+ label.alignment = VTextFactory.textAlignMap[alignStr] ?? .natural
123
+ } else {
124
+ label.alignment = .natural
125
+ }
126
+
127
+ case "lineBreakMode":
128
+ if let mode = value as? String {
129
+ switch mode {
130
+ case "clip": label.lineBreakMode = .byClipping
131
+ case "head": label.lineBreakMode = .byTruncatingHead
132
+ case "middle": label.lineBreakMode = .byTruncatingMiddle
133
+ case "tail": label.lineBreakMode = .byTruncatingTail
134
+ case "wordwrap": label.lineBreakMode = .byWordWrapping
135
+ default: label.lineBreakMode = .byTruncatingTail
136
+ }
137
+ }
138
+
139
+ case "fontStyle":
140
+ if let str = value as? String {
141
+ let currentFont = label.font ?? NSFont.systemFont(ofSize: 13)
142
+ let currentSize = currentFont.pointSize
143
+ if str == "italic" {
144
+ let descriptor = currentFont.fontDescriptor.withSymbolicTraits(.italic)
145
+ label.font = NSFont(descriptor: descriptor, size: currentSize)
146
+ } else {
147
+ var traits = currentFont.fontDescriptor.symbolicTraits
148
+ traits.remove(.italic)
149
+ let descriptor = currentFont.fontDescriptor.withSymbolicTraits(traits)
150
+ label.font = NSFont(descriptor: descriptor, size: currentSize)
151
+ }
152
+ }
153
+ label.layoutNode?.markDirty()
154
+
155
+ case "lineHeight":
156
+ if let num = value as? Double {
157
+ let paragraphStyle = NSMutableParagraphStyle()
158
+ paragraphStyle.minimumLineHeight = CGFloat(num)
159
+ paragraphStyle.maximumLineHeight = CGFloat(num)
160
+ paragraphStyle.alignment = label.alignment
161
+ let text = label.stringValue
162
+ let font = label.font ?? NSFont.systemFont(ofSize: 13)
163
+ let attrs: [NSAttributedString.Key: Any] = [
164
+ .paragraphStyle: paragraphStyle,
165
+ .font: font
166
+ ]
167
+ label.attributedStringValue = NSAttributedString(string: text, attributes: attrs)
168
+ label.layoutNode?.markDirty()
169
+ }
170
+
171
+ case "letterSpacing":
172
+ if let num = value as? Double {
173
+ let text = label.stringValue
174
+ let font = label.font ?? NSFont.systemFont(ofSize: 13)
175
+ let attrs: [NSAttributedString.Key: Any] = [
176
+ .kern: CGFloat(num),
177
+ .font: font
178
+ ]
179
+ label.attributedStringValue = NSAttributedString(string: text, attributes: attrs)
180
+ label.layoutNode?.markDirty()
181
+ }
182
+
183
+ case "textDecorationLine":
184
+ if let str = value as? String {
185
+ let text = label.stringValue
186
+ let font = label.font ?? NSFont.systemFont(ofSize: 13)
187
+ var attrs: [NSAttributedString.Key: Any] = [.font: font]
188
+ switch str {
189
+ case "underline":
190
+ attrs[.underlineStyle] = NSUnderlineStyle.single.rawValue
191
+ case "line-through", "lineThrough":
192
+ attrs[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
193
+ case "underline line-through":
194
+ attrs[.underlineStyle] = NSUnderlineStyle.single.rawValue
195
+ attrs[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
196
+ default:
197
+ break
198
+ }
199
+ label.attributedStringValue = NSAttributedString(string: text, attributes: attrs)
200
+ label.layoutNode?.markDirty()
201
+ }
202
+
203
+ case "textTransform":
204
+ if let str = value as? String {
205
+ let original = label.stringValue
206
+ switch str {
207
+ case "uppercase": label.stringValue = original.uppercased()
208
+ case "lowercase": label.stringValue = original.lowercased()
209
+ case "capitalize": label.stringValue = original.capitalized
210
+ default: break
211
+ }
212
+ label.layoutNode?.markDirty()
213
+ }
214
+
215
+ default:
216
+ // Delegate unknown props to StyleEngine for layout/visual styling
217
+ StyleEngine.apply(key: key, value: value, to: view)
218
+ }
219
+ }
220
+
221
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
222
+ // Click/press support via gesture recognizer.
223
+ if event == "press" {
224
+ let wrapper = ClickGestureWrapper(handler: handler)
225
+ let click = NSClickGestureRecognizer(
226
+ target: wrapper,
227
+ action: #selector(ClickGestureWrapper.handleGesture(_:))
228
+ )
229
+ view.addGestureRecognizer(click)
230
+ GestureStorage.store(wrapper, for: view, event: event)
231
+ }
232
+ }
233
+
234
+ func removeEventListener(view: NSView, event: String) {
235
+ if event == "press" {
236
+ GestureStorage.remove(for: view, event: event)
237
+ view.gestureRecognizers.forEach { recognizer in
238
+ if recognizer is NSClickGestureRecognizer {
239
+ view.removeGestureRecognizer(recognizer)
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ func insertChild(_ child: NSView, into parent: NSView, before anchor: NSView?) {
246
+ guard let label = parent as? NSTextField else {
247
+ child.removeFromSuperview()
248
+ return
249
+ }
250
+
251
+ var children = storedTextChildren(on: label)
252
+ if let anchor = anchor, let index = children.firstIndex(where: { $0 === anchor }) {
253
+ children.insert(child, at: index)
254
+ } else {
255
+ children.append(child)
256
+ }
257
+
258
+ storeTextChildren(children, on: label)
259
+ rebuildText(from: children, on: label)
260
+ }
261
+
262
+ func removeChild(_ child: NSView, from parent: NSView) {
263
+ guard let label = parent as? NSTextField else { return }
264
+ var children = storedTextChildren(on: label)
265
+ children.removeAll { $0 === child }
266
+ storeTextChildren(children, on: label)
267
+ rebuildText(from: children, on: label)
268
+ }
269
+
270
+ // MARK: - Font rebuilding
271
+
272
+ /// Rebuild the NSFont from stored fontSize, fontWeight, and fontFamily.
273
+ private func rebuildFont(for label: NSTextField) {
274
+ let size = storedFontSize(on: label) ?? 13.0
275
+ let weightStr = storedFontWeight(on: label)
276
+ let family = storedFontFamily(on: label)
277
+
278
+ let weight = weightStr.flatMap { VTextFactory.fontWeightMap[$0] } ?? .regular
279
+
280
+ if let family = family, !family.isEmpty {
281
+ // Try to create a font with the specified family
282
+ if let customFont = NSFont(name: family, size: size) {
283
+ label.font = customFont
284
+ } else {
285
+ // Fallback: try as a font descriptor family
286
+ let descriptor = NSFontDescriptor()
287
+ .withFamily(family)
288
+ label.font = NSFont(descriptor: descriptor, size: size)
289
+ }
290
+ } else {
291
+ label.font = NSFont.systemFont(ofSize: size, weight: weight)
292
+ }
293
+ }
294
+
295
+ // MARK: - Font state storage via associated objects
296
+
297
+ private func storeFontSize(_ size: CGFloat, on view: NSView) {
298
+ objc_setAssociatedObject(view, &VTextFactory.fontSizeKey, size, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
299
+ }
300
+
301
+ private func storedFontSize(on view: NSView) -> CGFloat? {
302
+ return objc_getAssociatedObject(view, &VTextFactory.fontSizeKey) as? CGFloat
303
+ }
304
+
305
+ private func storeFontWeight(_ weight: String?, on view: NSView) {
306
+ objc_setAssociatedObject(view, &VTextFactory.fontWeightKey, weight, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
307
+ }
308
+
309
+ private func storedFontWeight(on view: NSView) -> String? {
310
+ return objc_getAssociatedObject(view, &VTextFactory.fontWeightKey) as? String
311
+ }
312
+
313
+ private func storeFontFamily(_ family: String?, on view: NSView) {
314
+ objc_setAssociatedObject(view, &VTextFactory.fontFamilyKey, family, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
315
+ }
316
+
317
+ private func storedFontFamily(on view: NSView) -> String? {
318
+ return objc_getAssociatedObject(view, &VTextFactory.fontFamilyKey) as? String
319
+ }
320
+
321
+ private func storeTextChildren(_ children: [NSView], on view: NSView) {
322
+ objc_setAssociatedObject(view, &VTextFactory.textChildrenKey, children, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
323
+ }
324
+
325
+ private func storedTextChildren(on view: NSView) -> [NSView] {
326
+ return objc_getAssociatedObject(view, &VTextFactory.textChildrenKey) as? [NSView] ?? []
327
+ }
328
+
329
+ private func rebuildText(from children: [NSView], on label: NSTextField) {
330
+ let text = children.compactMap { child -> String? in
331
+ (child as? NSTextField)?.stringValue
332
+ }.joined()
333
+ label.stringValue = text
334
+ label.layoutNode?.markDirty()
335
+ }
336
+ }