@webview-native-ui/engine 1.0.1 → 1.0.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 (230) hide show
  1. package/README.md +14 -0
  2. package/WebViewNativeUIControls.podspec +17 -0
  3. package/dist/controls/activation.d.ts +15 -0
  4. package/dist/controls/activation.js +370 -0
  5. package/dist/controls/components/index.d.ts +16 -0
  6. package/dist/controls/components/index.js +38 -0
  7. package/dist/controls/components/ion-back-button.d.ts +4 -0
  8. package/dist/controls/components/ion-back-button.js +16 -0
  9. package/dist/controls/components/ion-button.d.ts +4 -0
  10. package/dist/controls/components/ion-button.js +26 -0
  11. package/dist/controls/components/ion-buttons.d.ts +5 -0
  12. package/dist/controls/components/ion-buttons.js +34 -0
  13. package/dist/controls/components/ion-fab.d.ts +4 -0
  14. package/dist/controls/components/ion-fab.js +164 -0
  15. package/dist/controls/components/ion-menu-button.d.ts +7 -0
  16. package/dist/controls/components/ion-menu-button.js +41 -0
  17. package/dist/controls/components/ion-segment.d.ts +5 -0
  18. package/dist/controls/components/ion-segment.js +24 -0
  19. package/dist/controls/components/ion-tab-bar.d.ts +5 -0
  20. package/dist/controls/components/ion-tab-bar.js +49 -0
  21. package/dist/controls/components/searchable-tabs.d.ts +20 -0
  22. package/dist/controls/components/searchable-tabs.js +525 -0
  23. package/dist/controls/definitions.d.ts +360 -0
  24. package/dist/controls/definitions.js +1 -0
  25. package/dist/controls/extensions.d.ts +44 -0
  26. package/dist/controls/extensions.js +164 -0
  27. package/dist/controls/foldable/fold-state.d.ts +4 -0
  28. package/dist/controls/foldable/fold-state.js +8 -0
  29. package/dist/controls/foldable/types.d.ts +9 -0
  30. package/dist/controls/foldable/types.js +1 -0
  31. package/dist/controls/html.d.ts +8 -0
  32. package/dist/controls/html.js +260 -0
  33. package/dist/controls/index.d.ts +7 -0
  34. package/dist/controls/index.js +4 -0
  35. package/dist/controls/ionic.d.ts +1 -0
  36. package/dist/controls/ionic.js +1 -0
  37. package/dist/controls/legacy-layout.d.ts +7 -0
  38. package/dist/controls/legacy-layout.js +23 -0
  39. package/dist/controls/overlays/content.d.ts +2 -0
  40. package/dist/controls/overlays/content.js +55 -0
  41. package/dist/controls/overlays/controller.d.ts +7 -0
  42. package/dist/controls/overlays/controller.js +369 -0
  43. package/dist/controls/overlays/covered-overlay.d.ts +4 -0
  44. package/dist/controls/overlays/covered-overlay.js +9 -0
  45. package/dist/controls/overlays/dialog-animation.d.ts +7 -0
  46. package/dist/controls/overlays/dialog-animation.js +49 -0
  47. package/dist/controls/overlays/dismiss-animation.d.ts +2 -0
  48. package/dist/controls/overlays/dismiss-animation.js +14 -0
  49. package/dist/controls/overlays/focus.d.ts +2 -0
  50. package/dist/controls/overlays/focus.js +67 -0
  51. package/dist/controls/overlays/popover.d.ts +10 -0
  52. package/dist/controls/overlays/popover.js +117 -0
  53. package/dist/controls/overlays/styles.d.ts +9 -0
  54. package/dist/controls/overlays/styles.js +194 -0
  55. package/dist/controls/prehide.d.ts +6 -0
  56. package/dist/controls/prehide.js +280 -0
  57. package/dist/controls/runtime.d.ts +5 -0
  58. package/dist/controls/runtime.js +1685 -0
  59. package/dist/controls/shared/bridge.d.ts +4 -0
  60. package/dist/controls/shared/bridge.js +15 -0
  61. package/dist/controls/shared/candidate.d.ts +23 -0
  62. package/dist/controls/shared/candidate.js +128 -0
  63. package/dist/controls/shared/clipping.d.ts +1 -0
  64. package/dist/controls/shared/clipping.js +1 -0
  65. package/dist/controls/shared/connection.d.ts +21 -0
  66. package/dist/controls/shared/connection.js +112 -0
  67. package/dist/controls/shared/control-options.d.ts +4 -0
  68. package/dist/controls/shared/control-options.js +14 -0
  69. package/dist/controls/shared/covered-snapshot.d.ts +35 -0
  70. package/dist/controls/shared/covered-snapshot.js +41 -0
  71. package/dist/controls/shared/covered-source.d.ts +22 -0
  72. package/dist/controls/shared/covered-source.js +72 -0
  73. package/dist/controls/shared/crossfade.d.ts +11 -0
  74. package/dist/controls/shared/crossfade.js +71 -0
  75. package/dist/controls/shared/diagnostics.d.ts +2 -0
  76. package/dist/controls/shared/diagnostics.js +16 -0
  77. package/dist/controls/shared/dom.d.ts +45 -0
  78. package/dist/controls/shared/dom.js +258 -0
  79. package/dist/controls/shared/events.d.ts +12 -0
  80. package/dist/controls/shared/events.js +13 -0
  81. package/dist/controls/shared/horizontal-navigation.d.ts +36 -0
  82. package/dist/controls/shared/horizontal-navigation.js +90 -0
  83. package/dist/controls/shared/icons.d.ts +5 -0
  84. package/dist/controls/shared/icons.js +66 -0
  85. package/dist/controls/shared/modal-animation.d.ts +21 -0
  86. package/dist/controls/shared/modal-animation.js +97 -0
  87. package/dist/controls/shared/modal.d.ts +26 -0
  88. package/dist/controls/shared/modal.js +261 -0
  89. package/dist/controls/shared/projection-id.d.ts +2 -0
  90. package/dist/controls/shared/projection-id.js +2 -0
  91. package/dist/controls/shared/scroll-placement.d.ts +1 -0
  92. package/dist/controls/shared/scroll-placement.js +1 -0
  93. package/dist/controls/shared/tab-bar-width.d.ts +7 -0
  94. package/dist/controls/shared/tab-bar-width.js +72 -0
  95. package/dist/controls/shared/toolbar-options.d.ts +10 -0
  96. package/dist/controls/shared/toolbar-options.js +38 -0
  97. package/dist/controls/shared/transition-identity.d.ts +3 -0
  98. package/dist/controls/shared/transition-identity.js +64 -0
  99. package/dist/controls/shared/vertical-bars-geometry.d.ts +8 -0
  100. package/dist/controls/shared/vertical-bars-geometry.js +82 -0
  101. package/dist/controls/shared/vertical-navigation.d.ts +5 -0
  102. package/dist/controls/shared/vertical-navigation.js +39 -0
  103. package/dist/controls/startup.d.ts +7 -0
  104. package/dist/controls/startup.js +72 -0
  105. package/dist/controls/transport.d.ts +10 -0
  106. package/dist/controls/transport.js +1 -0
  107. package/dist/controls/vertical-bars-web-search.d.ts +8 -0
  108. package/dist/controls/vertical-bars-web-search.js +83 -0
  109. package/dist/controls/vertical-bars-web.d.ts +2 -0
  110. package/dist/controls/vertical-bars-web.js +609 -0
  111. package/dist/index.d.ts +1 -0
  112. package/dist/index.js +1 -0
  113. package/dist/integration/index.d.ts +36 -0
  114. package/dist/integration/index.js +25 -0
  115. package/dist/native/session.js +14 -8
  116. package/dist/projection/pause.d.ts +10 -0
  117. package/dist/projection/pause.js +58 -0
  118. package/dist/projection/styles.d.ts +1 -1
  119. package/dist/projection/styles.js +0 -1
  120. package/dist/projection/surfaces.js +2 -1
  121. package/dist/projection/web.js +39 -5
  122. package/dist/types.d.ts +5 -1
  123. package/docs/README.md +1 -0
  124. package/docs/configuration/README.md +3 -1
  125. package/docs/configuration/ionic-overlays.md +36 -2
  126. package/docs/configuration/pause-resume.md +51 -0
  127. package/docs/controls.md +39 -0
  128. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButton.swift +98 -0
  129. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButtons.swift +24 -0
  130. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIComponents.swift +19 -0
  131. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIFab.swift +80 -0
  132. package/ios/Sources/WebViewNativeUIControls/Components/NativeUISearchController.swift +449 -0
  133. package/ios/Sources/WebViewNativeUIControls/Components/NativeUISegment.swift +91 -0
  134. package/ios/Sources/WebViewNativeUIControls/Components/NativeUITabBar.swift +213 -0
  135. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIToolbarTransition.swift +186 -0
  136. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalBars.swift +696 -0
  137. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalSearch.swift +117 -0
  138. package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUI.swift +180 -0
  139. package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUIRendering.swift +38 -0
  140. package/ios/Sources/WebViewNativeUIControls/NativeUIController.swift +801 -0
  141. package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayController.swift +131 -0
  142. package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayHost.swift +85 -0
  143. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIControlRendering.swift +21 -0
  144. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICoveredSnapshots.swift +14 -0
  145. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICrossfade.swift +35 -0
  146. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIHost.swift +8 -0
  147. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIScrollTracker.swift +2 -0
  148. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUISnapshot.swift +206 -0
  149. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIVerticalBarsGeometry.swift +93 -0
  150. package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
  151. package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +1 -1
  152. package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -4
  153. package/package.json +30 -5
  154. package/src/controls/activation.ts +427 -0
  155. package/src/controls/components/index.ts +45 -0
  156. package/src/controls/components/ion-back-button.ts +22 -0
  157. package/src/controls/components/ion-button.ts +28 -0
  158. package/src/controls/components/ion-buttons.ts +37 -0
  159. package/src/controls/components/ion-fab.ts +151 -0
  160. package/src/controls/components/ion-menu-button.ts +37 -0
  161. package/src/controls/components/ion-segment.ts +27 -0
  162. package/src/controls/components/ion-tab-bar.ts +55 -0
  163. package/src/controls/components/searchable-tabs.ts +534 -0
  164. package/src/controls/definitions.ts +338 -0
  165. package/src/controls/extensions.ts +198 -0
  166. package/src/controls/foldable/fold-state.ts +10 -0
  167. package/src/controls/foldable/types.ts +4 -0
  168. package/src/controls/html.ts +239 -0
  169. package/src/controls/index.ts +7 -0
  170. package/src/controls/ionic.ts +1 -0
  171. package/src/controls/legacy-layout.ts +21 -0
  172. package/src/controls/overlays/content.ts +51 -0
  173. package/src/controls/overlays/controller.ts +394 -0
  174. package/src/controls/overlays/covered-overlay.ts +11 -0
  175. package/src/controls/overlays/dialog-animation.ts +53 -0
  176. package/src/controls/overlays/dismiss-animation.ts +15 -0
  177. package/src/controls/overlays/focus.ts +64 -0
  178. package/src/controls/overlays/popover.ts +116 -0
  179. package/src/controls/overlays/styles.ts +195 -0
  180. package/src/controls/prehide.ts +285 -0
  181. package/src/controls/runtime.ts +1731 -0
  182. package/src/controls/shared/bridge.ts +18 -0
  183. package/src/controls/shared/candidate.ts +163 -0
  184. package/src/controls/shared/clipping.ts +1 -0
  185. package/src/controls/shared/connection.ts +105 -0
  186. package/src/controls/shared/control-options.ts +20 -0
  187. package/src/controls/shared/covered-snapshot.ts +52 -0
  188. package/src/controls/shared/covered-source.ts +78 -0
  189. package/src/controls/shared/crossfade.ts +70 -0
  190. package/src/controls/shared/diagnostics.ts +13 -0
  191. package/src/controls/shared/dom.ts +310 -0
  192. package/src/controls/shared/events.ts +14 -0
  193. package/src/controls/shared/horizontal-navigation.ts +106 -0
  194. package/src/controls/shared/icons.ts +64 -0
  195. package/src/controls/shared/modal-animation.ts +109 -0
  196. package/src/controls/shared/modal.ts +286 -0
  197. package/src/controls/shared/projection-id.ts +2 -0
  198. package/src/controls/shared/scroll-placement.ts +1 -0
  199. package/src/controls/shared/tab-bar-width.ts +66 -0
  200. package/src/controls/shared/toolbar-options.ts +51 -0
  201. package/src/controls/shared/transition-identity.ts +67 -0
  202. package/src/controls/shared/vertical-bars-geometry.ts +92 -0
  203. package/src/controls/shared/vertical-navigation.ts +34 -0
  204. package/src/controls/startup.ts +74 -0
  205. package/src/controls/transport.ts +11 -0
  206. package/src/controls/vertical-bars-web-search.ts +86 -0
  207. package/src/controls/vertical-bars-web.ts +674 -0
  208. package/src/dom/geometry.ts +24 -0
  209. package/src/dom/overlays.ts +196 -0
  210. package/src/dom/scroll-bindings.ts +66 -0
  211. package/src/dom/scroll-placement.ts +92 -0
  212. package/src/dom/visibility.ts +29 -0
  213. package/src/index.ts +19 -0
  214. package/src/integration/index.ts +64 -0
  215. package/src/ionic.ts +34 -0
  216. package/src/native/bootstrap.ts +132 -0
  217. package/src/native/bridge.ts +18 -0
  218. package/src/native/resource-url.ts +11 -0
  219. package/src/native/session.ts +708 -0
  220. package/src/native/updates.ts +103 -0
  221. package/src/native/values.ts +29 -0
  222. package/src/projection/events.ts +58 -0
  223. package/src/projection/pause.ts +58 -0
  224. package/src/projection/placement.ts +34 -0
  225. package/src/projection/scope.ts +13 -0
  226. package/src/projection/styles.ts +26 -0
  227. package/src/projection/surface.ts +88 -0
  228. package/src/projection/surfaces.ts +119 -0
  229. package/src/projection/web.ts +1013 -0
  230. package/src/types.ts +58 -0
@@ -0,0 +1,117 @@
1
+ import Combine
2
+ import SwiftUI
3
+ import UIKit
4
+
5
+ @available(iOS 26.0, *)
6
+ final class NativeUIVerticalSearchModel: ObservableObject {
7
+ @Published private(set) var configuration: NativeUISearch
8
+ @Published var text: String
9
+ @Published var presented: Bool
10
+ @Published var focused: Bool
11
+ private weak var railView: UIView?
12
+ private let activate: (String) -> Void
13
+ private let changed: (String, NativeUISearchPhase, String, Bool, Int) -> Int
14
+ private let inputDelegate = NativeUISearchInputDelegate()
15
+ private var editingSequence = 0
16
+ private var valueVersion: Int
17
+
18
+ init(_ configuration: NativeUISearch, in railView: UIView,
19
+ activate: @escaping (String) -> Void,
20
+ changed: @escaping (String, NativeUISearchPhase, String, Bool, Int) -> Int) {
21
+ self.configuration = configuration
22
+ self.railView = railView
23
+ self.activate = activate
24
+ self.changed = changed
25
+ text = configuration.value
26
+ presented = configuration.available && configuration.active
27
+ focused = configuration.focused
28
+ valueVersion = configuration.valueVersion
29
+ inputDelegate.clear = { [weak self] in self?.emit(.clear) }
30
+ }
31
+
32
+ var ownsKeyboardChrome: Bool { presented && (focused || textField?.isFirstResponder == true) }
33
+
34
+ private var textField: UISearchTextField? {
35
+ func find(_ view: UIView) -> UISearchTextField? {
36
+ guard !view.isHidden, view.alpha > 0.01 else { return nil }
37
+ if let field = view as? UISearchTextField { return field }
38
+ for child in view.subviews {
39
+ if let field = find(child) { return field }
40
+ }
41
+ return nil
42
+ }
43
+ return railView.flatMap(find)
44
+ }
45
+
46
+ func apply(_ next: NativeUISearch) {
47
+ let previous = configuration
48
+ configuration = next
49
+ if next.valueVersion != valueVersion || next.editSequence >= editingSequence {
50
+ valueVersion = next.valueVersion
51
+ text = next.value
52
+ }
53
+ presented = next.available && next.active
54
+ // The first Web acknowledgement still carries the old focus state.
55
+ if !next.active { focused = false } else if previous.focused != next.focused { focused = next.focused }
56
+ }
57
+
58
+ func input(_ value: String) {
59
+ text = value
60
+ emit(.input)
61
+ }
62
+
63
+ func present(_ value: Bool) {
64
+ presented = value
65
+ if value != configuration.active {
66
+ activate(value ? configuration.trigger.id : configuration.closeId)
67
+ }
68
+ }
69
+
70
+ func focus(_ value: Bool) {
71
+ focused = value
72
+ emit(value ? .focus : .blur)
73
+ }
74
+
75
+ func commit() { emit(.commit) }
76
+
77
+ func configureField() {
78
+ guard let field = textField else { return }
79
+ field.accessibilityIdentifier = configuration.id
80
+ field.accessibilityLabel = configuration.field.accessibilityLabel
81
+ field.isEnabled = !configuration.disabled
82
+ field.autocapitalizationType = .none
83
+ field.autocorrectionType = .no
84
+ field.spellCheckingType = .no
85
+ field.clearButtonMode = .always
86
+ if field.delegate !== inputDelegate {
87
+ inputDelegate.original = field.delegate
88
+ field.delegate = inputDelegate
89
+ }
90
+ }
91
+
92
+ private func emit(_ phase: NativeUISearchPhase) {
93
+ editingSequence = changed(configuration.id, phase, text,
94
+ textField?.markedTextRange != nil, valueVersion)
95
+ }
96
+ }
97
+
98
+ @available(iOS 26.0, *)
99
+ struct NativeUIVerticalSearchModifier: ViewModifier {
100
+ @ObservedObject var model: NativeUIVerticalSearchModel
101
+ @FocusState private var focused: Bool
102
+
103
+ @ViewBuilder func body(content: Content) -> some View {
104
+ if model.configuration.available {
105
+ content
106
+ .searchable(text: Binding(get: { model.text }, set: model.input),
107
+ isPresented: Binding(get: { model.presented }, set: model.present),
108
+ placement: .toolbar, prompt: Text(model.configuration.placeholder))
109
+ .searchToolbarBehavior(.minimize)
110
+ .searchPresentationToolbarBehavior(.avoidHidingContent)
111
+ .searchFocused($focused)
112
+ .onSubmit(of: .search, model.commit)
113
+ .onChange(of: focused) { _, value in model.focus(value) }
114
+ .onChange(of: model.focused) { _, value in focused = value }
115
+ } else { content }
116
+ }
117
+ }
@@ -0,0 +1,180 @@
1
+ import WebViewNativeUIEngine
2
+ import SwiftUI
3
+ import UIKit
4
+
5
+ /// Experimental API: registration interfaces and behavior may change in future releases.
6
+ /// App-local SwiftUI renderers. Register once before enabling the shell from JavaScript.
7
+ @MainActor public enum NativeUI {
8
+ private static var renderers: [String: (String, NativeUIActions, NativeUIContext) throws -> AnyView] = [:]
9
+
10
+ /// The name must match defineNativeUI in the application's TypeScript source.
11
+ public static func register<Props: Decodable, Content: View>(
12
+ _ name: String, @ViewBuilder content: @escaping (Props, NativeUIActions) -> Content
13
+ ) throws {
14
+ try register(name) { (props: Props, actions: NativeUIActions, _: NativeUIContext) in
15
+ content(props, actions)
16
+ }
17
+ }
18
+
19
+ /// Receives source geometry and presentation state on every accepted snapshot.
20
+ public static func register<Props: Decodable, Content: View>(
21
+ _ name: String, @ViewBuilder content: @escaping (Props, NativeUIActions, NativeUIContext) -> Content
22
+ ) throws {
23
+ guard !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, renderers[name] == nil else {
24
+ throw NativeUIError.duplicateRegistration
25
+ }
26
+ renderers[name] = { json, actions, context in
27
+ let props = try JSONDecoder().decode(Props.self, from: Data(json.utf8))
28
+ return AnyView(content(props, actions, context))
29
+ }
30
+ }
31
+
32
+ static var names: [String] { renderers.keys.sorted() }
33
+
34
+ static func content(_ data: NativeUIExtensionContent, actions: NativeUIActions, context: NativeUIContext) throws -> AnyView {
35
+ guard let renderer = renderers[data.name] else { throw NativeUIError.unavailable }
36
+ return try renderer(data.props, actions, context)
37
+ }
38
+ }
39
+
40
+ /// Read-only metadata for a projected Web source. Shell still owns its placement.
41
+ public struct NativeUIContext {
42
+ public enum Placement: String { case element, tabAccessory = "tab-accessory" }
43
+
44
+ /// Opaque identity stable for this source's current lifetime; not its HTML id.
45
+ public let sourceID: String
46
+ /// getBoundingClientRect(), in CSS pixels relative to the Web viewport.
47
+ public let sourceFrame: CGRect
48
+ /// The same source rectangle in WKWebView-local UIKit points, not screen coordinates.
49
+ public let sourceFrameInWebView: CGRect
50
+ /// CSS viewport width used to convert the snapshot into native coordinates.
51
+ public let viewportWidth: CGFloat
52
+ /// UIKit points per CSS pixel. This is not the device's pixel density.
53
+ public let pointsPerCSSPixel: CGFloat
54
+ public let placement: Placement
55
+ public let colorScheme: ColorScheme
56
+ public let layoutDirection: LayoutDirection
57
+
58
+ init(node: NativeUIControl, viewportWidth: CGFloat, pointsPerCSSPixel: CGFloat) {
59
+ sourceID = node.id
60
+ sourceFrame = node.frame.rect
61
+ sourceFrameInWebView = node.frame.rect.applying(
62
+ CGAffineTransform(scaleX: pointsPerCSSPixel, y: pointsPerCSSPixel))
63
+ self.viewportWidth = viewportWidth
64
+ self.pointsPerCSSPixel = pointsPerCSSPixel
65
+ placement = node.extensionContent?.tabBarId == nil ? .element : .tabAccessory
66
+ colorScheme = node.dark ? .dark : .light
67
+ layoutDirection = node.rtl ? .rightToLeft : .leftToRight
68
+ }
69
+ }
70
+
71
+ public enum NativeUIError: Error {
72
+ case duplicateRegistration, unavailable, rejected, timedOut
73
+ }
74
+
75
+ /// Completion means the original Web element received its click, not that the
76
+ /// application's asynchronous work triggered by that click has finished.
77
+ @MainActor public struct NativeUIActions {
78
+ let dispatch: (String) async throws -> Void
79
+ /// Names of currently visible, enabled DOM targets supplied by the reader.
80
+ public let availableNames: Set<String>
81
+
82
+ init(availableNames: Set<String> = [], dispatch: @escaping (String) async throws -> Void) {
83
+ self.availableNames = availableNames
84
+ self.dispatch = dispatch
85
+ }
86
+
87
+ public func isAvailable(_ name: String) -> Bool { availableNames.contains(name) }
88
+
89
+ public func activate(_ name: String) async throws {
90
+ try Task.checkCancellation()
91
+ try await dispatch(name)
92
+ }
93
+ }
94
+
95
+ struct NativeUIExtensionContent: Decodable, Equatable {
96
+ let name: String
97
+ let props: String
98
+ let actions: [String: String]
99
+ let tabBarId: String?
100
+
101
+ var isValid: Bool {
102
+ !name.isEmpty && props.utf8.count <= 262144 && actions.count <= 30 &&
103
+ actions.allSatisfy { !$0.key.isEmpty && !$0.value.isEmpty } &&
104
+ (tabBarId.map { !$0.isEmpty } ?? true) &&
105
+ (try? JSONSerialization.jsonObject(with: Data(props.utf8))) is [String: Any]
106
+ }
107
+ }
108
+
109
+ /// Owns pending native -> Web calls independently of the View's lifetime.
110
+ @MainActor final class NativeUIExtensionActions {
111
+ private struct Pending {
112
+ let owner: String
113
+ let continuation: CheckedContinuation<Void, Error>
114
+ let deadline: DispatchWorkItem
115
+ }
116
+ private var pending: [String: Pending] = [:]
117
+
118
+ func perform(owner: String, timeout: TimeInterval = 4, send: (String) -> Void) async throws {
119
+ let request = UUID().uuidString
120
+ try await withTaskCancellationHandler {
121
+ try Task.checkCancellation()
122
+ try await withCheckedThrowingContinuation { continuation in
123
+ let deadline = DispatchWorkItem { [weak self] in self?.finish(request, error: NativeUIError.timedOut) }
124
+ pending[request] = Pending(owner: owner, continuation: continuation, deadline: deadline)
125
+ DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: deadline)
126
+ send(request)
127
+ }
128
+ } onCancel: {
129
+ Task { @MainActor [weak self] in self?.finish(request, error: CancellationError()) }
130
+ }
131
+ }
132
+
133
+ func finish(_ request: String, error: Error? = nil) {
134
+ guard let call = pending.removeValue(forKey: request) else { return }
135
+ call.deadline.cancel()
136
+ if let error { call.continuation.resume(throwing: error) } else { call.continuation.resume() }
137
+ }
138
+
139
+ func cancel(owner: String? = nil) {
140
+ for (request, call) in pending where owner == nil || call.owner == owner {
141
+ finish(request, error: NativeUIError.unavailable)
142
+ }
143
+ }
144
+ }
145
+
146
+ /// Normal UIViewController containment keeps SwiftUI appearance and teardown intact.
147
+ @MainActor final class NativeUIExtensionController {
148
+ let controller: UIHostingController<AnyView>
149
+ let view: NativeUIClippedView
150
+
151
+ init(content: AnyView, owner: UIViewController) {
152
+ controller = UIHostingController(rootView: content)
153
+ view = NativeUIClippedView(content: controller.view)
154
+ controller.view.backgroundColor = .clear
155
+ if #available(iOS 16.4, *) { controller.safeAreaRegions = [] }
156
+ owner.addChild(controller)
157
+ // Capacitor can use WKWebView as its controller's root view. A sibling
158
+ // shell surface is outside that controller's containment hierarchy.
159
+ owner.view.addSubview(view)
160
+ controller.didMove(toParent: owner)
161
+ }
162
+
163
+ func update(_ content: AnyView) { controller.rootView = content }
164
+ func place(_ frame: CGRect, clip: CGRect? = nil) { view.place(frame, clip: clip) }
165
+ func detach(duration: TimeInterval) {
166
+ controller.willMove(toParent: nil)
167
+ NativeUICrossfade.retire(view, duration: duration)
168
+ controller.removeFromParent()
169
+ }
170
+ }
171
+
172
+ @available(iOS 26.0, *)
173
+ struct NativeUIExtensionAccessory: ViewModifier {
174
+ let content: AnyView?
175
+ func body(content base: Content) -> some View {
176
+ if #available(iOS 26.1, *) {
177
+ base.tabViewBottomAccessory(isEnabled: content != nil) { content }
178
+ } else { base }
179
+ }
180
+ }
@@ -0,0 +1,38 @@
1
+ import UIKit
2
+
3
+ /// Image returned by the TypeScript read context's image() helper.
4
+ public struct NativeUIImage: Decodable {
5
+ public let data: String
6
+ public let width: Double
7
+ public let height: Double
8
+ public let template: Bool
9
+
10
+ public init(data: String, width: Double, height: Double, template: Bool = false) {
11
+ self.data = data
12
+ self.width = width
13
+ self.height = height
14
+ self.template = template
15
+ }
16
+ }
17
+
18
+ /// Conversion primitives shared by standard controls and app-local extensions.
19
+ public enum NativeUIRendering {
20
+ /// Decodes base64 PNG artwork at its declared CSS size. Returns nil for invalid data.
21
+ public static func image(_ image: NativeUIImage) -> UIImage? {
22
+ guard image.width.isFinite, image.height.isFinite, image.width > 0, image.height > 0,
23
+ let bytes = Data(base64Encoded: image.data), let raw = UIImage(data: bytes), let cg = raw.cgImage else { return nil }
24
+ return UIImage(cgImage: cg, scale: CGFloat(cg.width) / image.width, orientation: .up)
25
+ .withRenderingMode(image.template ? .alwaysTemplate : .alwaysOriginal)
26
+ }
27
+
28
+ /// Converts computed rgb()/rgba() colors used by standard readers. Other CSS formats use the fallback.
29
+ public static func color(_ css: String?, fallback: UIColor = .label) -> UIColor {
30
+ guard let css, css.hasPrefix("rgb(") || css.hasPrefix("rgba("), !css.contains("%") else { return fallback }
31
+ let numbers = css.components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).compactMap(Double.init)
32
+ guard numbers.count == 3 || numbers.count == 4, numbers.allSatisfy(\.isFinite) else { return fallback }
33
+ return UIColor(red: min(255, max(0, numbers[0])) / 255,
34
+ green: min(255, max(0, numbers[1])) / 255,
35
+ blue: min(255, max(0, numbers[2])) / 255,
36
+ alpha: numbers.count == 4 ? min(1, max(0, numbers[3])) : 1)
37
+ }
38
+ }