@thelacanians/vue-native-cli 0.4.15 → 0.6.2

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