@rdlabo/ionic-theme-ios27 1.2.1-beta.pr275.shad339e7688db7 → 2.0.0-1

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 (248) hide show
  1. package/README.md +24 -280
  2. package/dist/css/components/ion-toolbar.css +1 -1
  3. package/dist/css/ionic-theme-ios27.css +1 -1
  4. package/dist/css/vertical-bars.css +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/layout/index.d.ts +18 -0
  10. package/dist/layout/index.d.ts.map +1 -0
  11. package/dist/layout/index.js +26 -0
  12. package/dist/layout/index.js.map +1 -0
  13. package/dist/segment/index.js +3 -3
  14. package/dist/segment/index.js.map +1 -1
  15. package/dist/sheets-of-glass/index.js +7 -7
  16. package/dist/sheets-of-glass/index.js.map +1 -1
  17. package/dist/tab-bar-searchable/dismiss-button.js +53 -3
  18. package/dist/tab-bar-searchable/dismiss-button.js.map +1 -1
  19. package/dist/tab-bar-searchable/index.js +18 -3
  20. package/dist/tab-bar-searchable/index.js.map +1 -1
  21. package/dist/transition/ios.transition.d.ts.map +1 -1
  22. package/dist/transition/ios.transition.js +7 -4
  23. package/dist/transition/ios.transition.js.map +1 -1
  24. package/dist/transition/native-ui.d.ts +6 -0
  25. package/dist/transition/native-ui.d.ts.map +1 -0
  26. package/dist/{native-integration/index.js → transition/native-ui.js} +16 -30
  27. package/dist/transition/native-ui.js.map +1 -0
  28. package/docs/adaptive-themes.md +161 -0
  29. package/docs/add-native-ui.md +53 -0
  30. package/docs/animation.md +19 -3
  31. package/docs/eslint.md +3 -1
  32. package/docs/features.md +9 -5
  33. package/docs/iphone-duo.md +62 -35
  34. package/docs/migration.md +17 -26
  35. package/docs/setup.md +92 -0
  36. package/docs/special-markup.md +41 -10
  37. package/docs/using-ion-item-group.md +2 -2
  38. package/docs/vertical-bars-layout.md +67 -0
  39. package/package.json +12 -42
  40. package/src/index.ts +3 -1
  41. package/src/layout/index.ts +29 -0
  42. package/src/segment/index.ts +3 -3
  43. package/src/sheets-of-glass/index.ts +7 -7
  44. package/src/styles/components/ion-toolbar.scss +1 -1
  45. package/src/styles/vertical-bars.scss +15 -18
  46. package/src/tab-bar-searchable/dismiss-button.ts +73 -4
  47. package/src/tab-bar-searchable/index.ts +20 -3
  48. package/src/transition/ios.transition.ts +7 -4
  49. package/src/{native-integration/index.ts → transition/native-ui.ts} +17 -46
  50. package/Package.swift +0 -21
  51. package/dist/_virtual/_rolldown/runtime.js +0 -13
  52. package/dist/native/components/index.d.ts +0 -19
  53. package/dist/native/components/index.d.ts.map +0 -1
  54. package/dist/native/components/index.js +0 -40
  55. package/dist/native/components/index.js.map +0 -1
  56. package/dist/native/components/ion-back-button.d.ts +0 -9
  57. package/dist/native/components/ion-back-button.d.ts.map +0 -1
  58. package/dist/native/components/ion-back-button.js +0 -20
  59. package/dist/native/components/ion-back-button.js.map +0 -1
  60. package/dist/native/components/ion-button.d.ts +0 -10
  61. package/dist/native/components/ion-button.d.ts.map +0 -1
  62. package/dist/native/components/ion-button.js +0 -23
  63. package/dist/native/components/ion-button.js.map +0 -1
  64. package/dist/native/components/ion-buttons.d.ts +0 -11
  65. package/dist/native/components/ion-buttons.d.ts.map +0 -1
  66. package/dist/native/components/ion-buttons.js +0 -30
  67. package/dist/native/components/ion-buttons.js.map +0 -1
  68. package/dist/native/components/ion-fab.d.ts +0 -10
  69. package/dist/native/components/ion-fab.d.ts.map +0 -1
  70. package/dist/native/components/ion-fab.js +0 -132
  71. package/dist/native/components/ion-fab.js.map +0 -1
  72. package/dist/native/components/ion-menu-button.d.ts +0 -13
  73. package/dist/native/components/ion-menu-button.d.ts.map +0 -1
  74. package/dist/native/components/ion-menu-button.js +0 -31
  75. package/dist/native/components/ion-menu-button.js.map +0 -1
  76. package/dist/native/components/ion-segment.d.ts +0 -11
  77. package/dist/native/components/ion-segment.d.ts.map +0 -1
  78. package/dist/native/components/ion-segment.js +0 -30
  79. package/dist/native/components/ion-segment.js.map +0 -1
  80. package/dist/native/components/ion-tab-bar.d.ts +0 -11
  81. package/dist/native/components/ion-tab-bar.d.ts.map +0 -1
  82. package/dist/native/components/ion-tab-bar.js +0 -50
  83. package/dist/native/components/ion-tab-bar.js.map +0 -1
  84. package/dist/native/components/searchable-tabs.d.ts +0 -20
  85. package/dist/native/components/searchable-tabs.d.ts.map +0 -1
  86. package/dist/native/components/searchable-tabs.js +0 -383
  87. package/dist/native/components/searchable-tabs.js.map +0 -1
  88. package/dist/native/definitions.d.ts +0 -191
  89. package/dist/native/definitions.d.ts.map +0 -1
  90. package/dist/native/definitions.js +0 -0
  91. package/dist/native/foldable/fold-state.d.ts +0 -5
  92. package/dist/native/foldable/fold-state.d.ts.map +0 -1
  93. package/dist/native/foldable/fold-state.js +0 -14
  94. package/dist/native/foldable/fold-state.js.map +0 -1
  95. package/dist/native/foldable/types.d.ts +0 -12
  96. package/dist/native/foldable/types.d.ts.map +0 -1
  97. package/dist/native/foldable/types.js +0 -0
  98. package/dist/native/index.d.ts +0 -11
  99. package/dist/native/index.d.ts.map +0 -1
  100. package/dist/native/index.js +0 -267
  101. package/dist/native/index.js.map +0 -1
  102. package/dist/native/overlays/content.d.ts +0 -4
  103. package/dist/native/overlays/content.d.ts.map +0 -1
  104. package/dist/native/overlays/content.js +0 -51
  105. package/dist/native/overlays/content.js.map +0 -1
  106. package/dist/native/overlays/controller.d.ts +0 -8
  107. package/dist/native/overlays/controller.d.ts.map +0 -1
  108. package/dist/native/overlays/controller.js +0 -285
  109. package/dist/native/overlays/controller.js.map +0 -1
  110. package/dist/native/overlays/dialog-animation.d.ts +0 -9
  111. package/dist/native/overlays/dialog-animation.d.ts.map +0 -1
  112. package/dist/native/overlays/dialog-animation.js +0 -53
  113. package/dist/native/overlays/dialog-animation.js.map +0 -1
  114. package/dist/native/overlays/dismiss-animation.d.ts +0 -4
  115. package/dist/native/overlays/dismiss-animation.d.ts.map +0 -1
  116. package/dist/native/overlays/dismiss-animation.js +0 -18
  117. package/dist/native/overlays/dismiss-animation.js.map +0 -1
  118. package/dist/native/overlays/focus.d.ts +0 -5
  119. package/dist/native/overlays/focus.d.ts.map +0 -1
  120. package/dist/native/overlays/focus.js +0 -59
  121. package/dist/native/overlays/focus.js.map +0 -1
  122. package/dist/native/overlays/popover.d.ts +0 -12
  123. package/dist/native/overlays/popover.d.ts.map +0 -1
  124. package/dist/native/overlays/popover.js +0 -110
  125. package/dist/native/overlays/popover.js.map +0 -1
  126. package/dist/native/overlays/styles.d.ts +0 -11
  127. package/dist/native/overlays/styles.d.ts.map +0 -1
  128. package/dist/native/overlays/styles.js +0 -167
  129. package/dist/native/overlays/styles.js.map +0 -1
  130. package/dist/native/prehide.d.ts +0 -7
  131. package/dist/native/prehide.d.ts.map +0 -1
  132. package/dist/native/prehide.js +0 -230
  133. package/dist/native/prehide.js.map +0 -1
  134. package/dist/native/runtime.d.ts +0 -7
  135. package/dist/native/runtime.d.ts.map +0 -1
  136. package/dist/native/runtime.js +0 -710
  137. package/dist/native/runtime.js.map +0 -1
  138. package/dist/native/shared/bridge.d.ts +0 -6
  139. package/dist/native/shared/bridge.d.ts.map +0 -1
  140. package/dist/native/shared/bridge.js +0 -18
  141. package/dist/native/shared/bridge.js.map +0 -1
  142. package/dist/native/shared/candidate.d.ts +0 -21
  143. package/dist/native/shared/candidate.d.ts.map +0 -1
  144. package/dist/native/shared/candidate.js +0 -98
  145. package/dist/native/shared/candidate.js.map +0 -1
  146. package/dist/native/shared/crossfade.d.ts +0 -12
  147. package/dist/native/shared/crossfade.d.ts.map +0 -1
  148. package/dist/native/shared/crossfade.js +0 -67
  149. package/dist/native/shared/crossfade.js.map +0 -1
  150. package/dist/native/shared/dom.d.ts +0 -46
  151. package/dist/native/shared/dom.d.ts.map +0 -1
  152. package/dist/native/shared/dom.js +0 -172
  153. package/dist/native/shared/dom.js.map +0 -1
  154. package/dist/native/shared/events.d.ts +0 -11
  155. package/dist/native/shared/events.d.ts.map +0 -1
  156. package/dist/native/shared/events.js +0 -13
  157. package/dist/native/shared/events.js.map +0 -1
  158. package/dist/native/shared/icons.d.ts +0 -8
  159. package/dist/native/shared/icons.d.ts.map +0 -1
  160. package/dist/native/shared/icons.js +0 -61
  161. package/dist/native/shared/icons.js.map +0 -1
  162. package/dist/native/shared/modal.d.ts +0 -10
  163. package/dist/native/shared/modal.d.ts.map +0 -1
  164. package/dist/native/shared/modal.js +0 -160
  165. package/dist/native/shared/modal.js.map +0 -1
  166. package/dist/native/shared/projection-id.d.ts +0 -4
  167. package/dist/native/shared/projection-id.d.ts.map +0 -1
  168. package/dist/native/shared/projection-id.js +0 -7
  169. package/dist/native/shared/projection-id.js.map +0 -1
  170. package/dist/native/shared/vertical-bars-geometry.d.ts +0 -10
  171. package/dist/native/shared/vertical-bars-geometry.d.ts.map +0 -1
  172. package/dist/native/shared/vertical-bars-geometry.js +0 -85
  173. package/dist/native/shared/vertical-bars-geometry.js.map +0 -1
  174. package/dist/native/vertical-bars-web-search.d.ts +0 -10
  175. package/dist/native/vertical-bars-web-search.d.ts.map +0 -1
  176. package/dist/native/vertical-bars-web-search.js +0 -90
  177. package/dist/native/vertical-bars-web-search.js.map +0 -1
  178. package/dist/native/vertical-bars-web.d.ts +0 -5
  179. package/dist/native/vertical-bars-web.d.ts.map +0 -1
  180. package/dist/native/vertical-bars-web.js +0 -411
  181. package/dist/native/vertical-bars-web.js.map +0 -1
  182. package/dist/native-integration/index.d.ts +0 -23
  183. package/dist/native-integration/index.d.ts.map +0 -1
  184. package/dist/native-integration/index.js.map +0 -1
  185. package/dist/native-integration/transition.d.ts +0 -5
  186. package/dist/native-integration/transition.d.ts.map +0 -1
  187. package/dist/native-integration/transition.js +0 -16
  188. package/dist/native-integration/transition.js.map +0 -1
  189. package/dist/vertical-bars.d.ts +0 -5
  190. package/dist/vertical-bars.js +0 -4
  191. package/docs/e2e-testing.md +0 -51
  192. package/docs/feedback.md +0 -65
  193. package/docs/iphone-duo-with-original-theme.md +0 -158
  194. package/docs/native-ui-shell.md +0 -325
  195. package/docs/vertical-bars.md +0 -300
  196. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButton.swift +0 -71
  197. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButtons.swift +0 -24
  198. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellComponents.swift +0 -18
  199. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellFab.swift +0 -80
  200. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +0 -432
  201. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSegment.swift +0 -91
  202. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +0 -218
  203. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +0 -565
  204. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalSearch.swift +0 -118
  205. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +0 -502
  206. package/ios/Sources/IonicNativeUIShellPlugin/Overlays/ShellOverlayController.swift +0 -119
  207. package/ios/Sources/IonicNativeUIShellPlugin/Overlays/ShellOverlayHost.swift +0 -75
  208. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellCrossfade.swift +0 -35
  209. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellHost.swift +0 -8
  210. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellRendering.swift +0 -27
  211. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +0 -135
  212. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellVerticalBarsGeometry.swift +0 -52
  213. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +0 -788
  214. package/src/native/components/index.ts +0 -53
  215. package/src/native/components/ion-back-button.ts +0 -19
  216. package/src/native/components/ion-button.ts +0 -24
  217. package/src/native/components/ion-buttons.ts +0 -38
  218. package/src/native/components/ion-fab.ts +0 -151
  219. package/src/native/components/ion-menu-button.ts +0 -24
  220. package/src/native/components/ion-segment.ts +0 -27
  221. package/src/native/components/ion-tab-bar.ts +0 -50
  222. package/src/native/components/searchable-tabs.ts +0 -459
  223. package/src/native/definitions.ts +0 -218
  224. package/src/native/foldable/fold-state.ts +0 -9
  225. package/src/native/foldable/types.ts +0 -4
  226. package/src/native/index.ts +0 -315
  227. package/src/native/overlays/content.ts +0 -51
  228. package/src/native/overlays/controller.ts +0 -349
  229. package/src/native/overlays/dialog-animation.ts +0 -53
  230. package/src/native/overlays/dismiss-animation.ts +0 -15
  231. package/src/native/overlays/focus.ts +0 -64
  232. package/src/native/overlays/popover.ts +0 -120
  233. package/src/native/overlays/styles.ts +0 -181
  234. package/src/native/prehide.ts +0 -265
  235. package/src/native/runtime.ts +0 -885
  236. package/src/native/shared/bridge.ts +0 -18
  237. package/src/native/shared/candidate.ts +0 -150
  238. package/src/native/shared/crossfade.ts +0 -67
  239. package/src/native/shared/dom.ts +0 -286
  240. package/src/native/shared/events.ts +0 -8
  241. package/src/native/shared/icons.ts +0 -57
  242. package/src/native/shared/modal.ts +0 -157
  243. package/src/native/shared/projection-id.ts +0 -2
  244. package/src/native/shared/vertical-bars-geometry.ts +0 -80
  245. package/src/native/vertical-bars-web-search.ts +0 -86
  246. package/src/native/vertical-bars-web.ts +0 -485
  247. package/src/native-integration/transition.ts +0 -15
  248. package/src/vertical-bars.ts +0 -11
@@ -1,502 +0,0 @@
1
- import Capacitor
2
- import UIKit
3
- import WebKit
4
-
5
- @objc(IonicNativeUIShellPlugin)
6
- public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDelegate {
7
- public let identifier = "IonicNativeUIShellPlugin"
8
- public let jsName = "IonicNativeUIShell"
9
- public let pluginMethods: [CAPPluginMethod] = [
10
- CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise),
11
- CAPPluginMethod(name: "getWebViewMetrics", returnType: CAPPluginReturnPromise),
12
- CAPPluginMethod(name: "getVerticalBarsGeometry", returnType: CAPPluginReturnPromise),
13
- CAPPluginMethod(name: "update", returnType: CAPPluginReturnPromise),
14
- CAPPluginMethod(name: "clear", returnType: CAPPluginReturnPromise),
15
- CAPPluginMethod(name: "prepareOverlay", returnType: CAPPluginReturnPromise),
16
- CAPPluginMethod(name: "presentOverlay", returnType: CAPPluginReturnPromise),
17
- CAPPluginMethod(name: "dismissOverlay", returnType: CAPPluginReturnPromise),
18
- CAPPluginMethod(name: "closeOverlay", returnType: CAPPluginReturnPromise),
19
- CAPPluginMethod(name: "stopOverlays", returnType: CAPPluginReturnPromise)
20
- ]
21
- private var overlays: ShellOverlayController?
22
- private var host: ShellHost?
23
- private var verticalBars: ShellVerticalBarsControlling?
24
- private var controls: [String: UIView] = [:]
25
- private var searchControllers: [String: ShellSearchControlling] = [:]
26
- private var fingerprints: [String: ShellControl] = [:]
27
- private let rendering = ShellRendering()
28
- private var configurationGeneration = 0
29
- private var revision = 0
30
- private var sequence = 0
31
- private var keyboardVisible = false
32
- private var pendingTabSelections: [String: ShellTabBar.PendingSelection] = [:]
33
- private var pendingTabExpiryWorks: [String: DispatchWorkItem] = [:]
34
- private var restoreTopEdge: (() -> Void)?
35
- private var observers: [NSObjectProtocol] = []
36
- private var lastWebViewRadius: Double?
37
-
38
- public override func load() {
39
- for name in [UIApplication.didEnterBackgroundNotification, UIResponder.keyboardWillChangeFrameNotification, UIResponder.keyboardWillHideNotification,
40
- UIDevice.orientationDidChangeNotification] {
41
- observers.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] notification in
42
- guard let self else { return }
43
- let keyboard = name == UIResponder.keyboardWillChangeFrameNotification || name == UIResponder.keyboardWillHideNotification
44
- if keyboard {
45
- if name == UIResponder.keyboardWillHideNotification { self.keyboardVisible = false }
46
- else if let window = self.bridge?.webView?.window,
47
- let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
48
- let local = window.convert(frame, from: window.screen.coordinateSpace)
49
- let overlap = window.bounds.intersection(local)
50
- self.keyboardVisible = !overlap.isNull && overlap.width > 0 && overlap.height > 0
51
- }
52
- }
53
- if !keyboard {
54
- self.host?.isHidden = true
55
- self.verticalBars?.view.isHidden = true
56
- }
57
- var searchOwnsKeyboard = self.verticalBars?.ownsKeyboardChrome == true
58
- self.searchControllers.values.forEach { controller in
59
- if controller.ownsKeyboardChrome { searchOwnsKeyboard = true }
60
- if !keyboard { controller.surface.isHidden = true }
61
- }
62
- if keyboard {
63
- if !searchOwnsKeyboard {
64
- self.bridge?.triggerWindowJSEvent(eventName: "nativeUIShellRefresh")
65
- }
66
- } else if name == UIDevice.orientationDidChangeNotification {
67
- self.notifyWebViewMetricsChange()
68
- }
69
- })
70
- }
71
- for name in [UIApplication.didBecomeActiveNotification, UIResponder.keyboardDidHideNotification,
72
- UIDevice.orientationDidChangeNotification] {
73
- observers.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
74
- if name == UIResponder.keyboardDidHideNotification { self?.keyboardVisible = false }
75
- self?.bridge?.triggerWindowJSEvent(eventName: "nativeUIShellRefresh", data: name == UIApplication.didBecomeActiveNotification ? "{\"retireSearch\":true}" : "{}")
76
- if name == UIApplication.didBecomeActiveNotification { self?.notifyWebViewMetricsChange() }
77
- })
78
- }
79
- }
80
-
81
- deinit {
82
- observers.forEach(NotificationCenter.default.removeObserver)
83
- }
84
-
85
- @objc func prepareOverlay(_ call: CAPPluginCall) {
86
- DispatchQueue.main.async { [weak self] in
87
- guard let self, let id = call.getString("id"),
88
- let source = self.bridge?.webView, let owner = self.bridge?.viewController else {
89
- call.reject("Popover source unavailable"); return
90
- }
91
- if self.overlays == nil {
92
- self.overlays = ShellOverlayController(source: source, owner: owner) { [weak self] id, action in
93
- self?.notifyListeners("overlay", data: ["id": id, "action": action])
94
- }
95
- }
96
- guard self.overlays?.prepare(id) == true else { call.reject("Popover already pending"); return }
97
- call.resolve()
98
- }
99
- }
100
-
101
- @objc func presentOverlay(_ call: CAPPluginCall) {
102
- DispatchQueue.main.async { [weak self] in
103
- guard let self, let overlays = self.overlays, let id = call.getString("id") else {
104
- call.reject("Popover not prepared"); return
105
- }
106
- overlays.present(id, options: call.getObject("presentation"), anchored: { [weak self] anchor, host, completion in
107
- guard let self, let rail = self.verticalBars,
108
- rail.presentPopover(anchor: anchor, host: host, completion: completion) else { return false }
109
- let cancel = host.cancelAnchored
110
- host.cancelAnchored = { [weak self, weak rail] in
111
- cancel?()
112
- // Cancellation removed the rail; invalidate native ownership even
113
- // if the next DOM snapshot is identical to the previous one.
114
- if let self, let rail, self.verticalBars === rail { self.verticalBars = nil }
115
- self?.bridge?.triggerWindowJSEvent(eventName: "nativeUIShellRefresh")
116
- }
117
- return true
118
- }) { presented in
119
- if presented { call.resolve() } else { call.reject("Native Vertical Bars anchor unavailable") }
120
- }
121
- }
122
- }
123
-
124
- @objc func dismissOverlay(_ call: CAPPluginCall) {
125
- DispatchQueue.main.async { [weak self] in
126
- guard let overlays = self?.overlays, let id = call.getString("id") else { call.resolve(); return }
127
- overlays.dismiss(id, animated: call.getBool("animated") ?? false) { call.resolve() }
128
- }
129
- }
130
-
131
- @objc func closeOverlay(_ call: CAPPluginCall) {
132
- DispatchQueue.main.async { [weak self] in
133
- guard let overlays = self?.overlays, let id = call.getString("id") else { call.resolve(); return }
134
- overlays.close(id) { call.resolve() }
135
- }
136
- }
137
-
138
- @objc func stopOverlays(_ call: CAPPluginCall) {
139
- DispatchQueue.main.async { [weak self] in
140
- guard let self, let overlays = self.overlays else { call.resolve(); return }
141
- overlays.stop {
142
- if self.overlays === overlays { self.overlays = nil }
143
- call.resolve()
144
- }
145
- }
146
- }
147
-
148
- private func webViewMetrics() -> JSObject? {
149
- guard #available(iOS 26.0, *), let webView = bridge?.webView else { return nil }
150
- webView.layoutIfNeeded()
151
- return ["radius": Double(webView.effectiveRadius(corner: .topLeft))]
152
- }
153
-
154
- private func notifyWebViewMetricsChange() {
155
- let metrics = webViewMetrics() ?? ["radius": 0]
156
- let radius = metrics["radius"] as? Double ?? 0
157
- guard radius != lastWebViewRadius else { return }
158
- lastWebViewRadius = radius
159
- notifyListeners("webViewMetricsChange", data: metrics)
160
- }
161
-
162
- @objc func getWebViewMetrics(_ call: CAPPluginCall) {
163
- DispatchQueue.main.async { [weak self] in
164
- call.resolve(self?.webViewMetrics() ?? ["radius": 0])
165
- }
166
- }
167
-
168
- @objc func configure(_ call: CAPPluginCall) {
169
- DispatchQueue.main.async { [weak self] in
170
- guard let self else { call.resolve(["supported": false]); return }
171
- self.configurationGeneration += 1
172
- let generation = self.configurationGeneration
173
- let configure = { [self] in
174
- guard generation == self.configurationGeneration else {
175
- call.reject("Configuration superseded"); return
176
- }
177
- // A new JS context starts revision numbering again (live reload / navigation).
178
- self.restoreTopEdge?()
179
- self.restoreTopEdge = nil
180
- self.removeControls()
181
- self.revision = 0
182
- if #available(iOS 26.0, *) {
183
- self.bridge?.webView?.layoutIfNeeded()
184
- // Ionic already paints the header edge; a second native effect can
185
- // add a dark scrim when the OS and Web themes differ.
186
- if call.getBool("verticalBarsOnly") != true, let effect = self.bridge?.webView?.scrollView.topEdgeEffect {
187
- let hidden = effect.isHidden
188
- effect.isHidden = true
189
- self.restoreTopEdge = { [weak effect] in effect?.isHidden = hidden }
190
- }
191
- call.resolve(["supported": true])
192
- }
193
- else { call.resolve(["supported": false]) }
194
- }
195
- // A replacement JS context cannot close its predecessor's child window.
196
- // Finish dismissing it while its rail still owns the SwiftUI presentation.
197
- if let overlays = self.overlays {
198
- overlays.stop {
199
- if self.overlays === overlays { self.overlays = nil }
200
- configure()
201
- }
202
- } else { configure() }
203
- }
204
- }
205
-
206
- @objc func getVerticalBarsGeometry(_ call: CAPPluginCall) {
207
- DispatchQueue.main.async { [weak self] in
208
- guard let self, let webView = self.bridge?.webView, let rail = self.verticalBars,
209
- let width = call.getDouble("viewportWidth"), width.isFinite, width > 0,
210
- webView.bounds.width > 0 else { call.resolve(["items": [:]]); return }
211
- let scale = width / webView.bounds.width
212
- let ids = Set(call.getArray("ids", String.self) ?? [])
213
- let measured = ShellVerticalBarsGeometry.measure(in: rail.view, relativeTo: webView, ids: ids).merging(rail.measuredFrames(relativeTo: webView).filter { ids.contains($0.key) }) { _, swiftUI in swiftUI }
214
- let items = measured.mapValues { frame in
215
- ["x": frame.minX * scale, "y": frame.minY * scale,
216
- "width": frame.width * scale, "height": frame.height * scale]
217
- }
218
- call.resolve(["revision": self.revision, "items": items])
219
- }
220
- }
221
-
222
- @objc func clear(_ call: CAPPluginCall) {
223
- DispatchQueue.main.async { [weak self] in
224
- guard let self else { call.resolve(); return }
225
- let next = call.getInt("revision") ?? 0
226
- if next >= self.revision {
227
- self.revision = next
228
- self.restoreTopEdge?()
229
- self.restoreTopEdge = nil
230
- self.removeControls()
231
- }
232
- call.resolve()
233
- }
234
- }
235
-
236
- private func removeControl(_ id: String, duration: TimeInterval = 0) {
237
- searchControllers.removeValue(forKey: id)?.detach()
238
- if let control = controls.removeValue(forKey: id) { ShellCrossfade.retire(control, duration: duration) }
239
- fingerprints.removeValue(forKey: id)
240
- pendingTabSelections.removeValue(forKey: id)
241
- pendingTabExpiryWorks.removeValue(forKey: id)?.cancel()
242
- }
243
-
244
- private func removeControls(duration: TimeInterval = 0) {
245
- Array(controls.keys).forEach { removeControl($0, duration: duration) }
246
- host?.removeFromSuperview()
247
- host = nil
248
- verticalBars?.detach(duration: duration)
249
- verticalBars = nil
250
- rendering.clear()
251
- pendingTabSelections.removeAll()
252
- pendingTabExpiryWorks.values.forEach { $0.cancel() }
253
- pendingTabExpiryWorks.removeAll()
254
- }
255
-
256
- private func syncTabBar(_ tabBar: UITabBar, id: String, node: ShellControl) {
257
- var pending = pendingTabSelections[id]
258
- ShellTabBar.update(tabBar, node: node, rendering: rendering, pendingSelection: &pending)
259
- if let pending {
260
- pendingTabSelections[id] = pending
261
- } else {
262
- pendingTabSelections.removeValue(forKey: id)
263
- pendingTabExpiryWorks.removeValue(forKey: id)?.cancel()
264
- }
265
- }
266
-
267
- private func schedulePendingTabExpiry(_ id: String, until: CFAbsoluteTime) {
268
- pendingTabExpiryWorks[id]?.cancel()
269
- let delay = max(0, until - CFAbsoluteTimeGetCurrent()) + 0.02
270
- let work = DispatchWorkItem { [weak self] in
271
- guard let self else { return }
272
- self.pendingTabExpiryWorks.removeValue(forKey: id)
273
- guard let pending = self.pendingTabSelections[id],
274
- CFAbsoluteTimeGetCurrent() >= pending.until,
275
- let tabBar = self.controls[id] as? UITabBar,
276
- let node = self.fingerprints[id] else { return }
277
- self.syncTabBar(tabBar, id: id, node: node)
278
- }
279
- pendingTabExpiryWorks[id] = work
280
- DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: work)
281
- }
282
-
283
- @objc func update(_ call: CAPPluginCall) {
284
- DispatchQueue.main.async { [weak self] in
285
- guard let self else { call.reject("Plugin released"); return }
286
- guard #available(iOS 26.0, *) else { call.reject("Requires iOS 26"); return }
287
- let next = call.getInt("revision") ?? 0
288
- guard next > self.revision else { call.resolve(["revision": self.revision]); return }
289
- guard let webView = self.bridge?.webView, let parent = webView.superview else {
290
- call.reject("WebView unavailable"); return
291
- }
292
- // Decode and validate the entire batch before changing visible controls.
293
- guard let snapshot = try? call.decode(ShellSnapshot.self), snapshot.isValid else {
294
- call.reject("Invalid control snapshot"); return
295
- }
296
- self.notifyWebViewMetricsChange()
297
- let duration = ShellCrossfade.duration(snapshot.transitionDuration)
298
- let existing = Set(self.controls.keys)
299
- let verticalBars = snapshot.controls.filter { $0.placement == .verticalBars }
300
- let snapshots = snapshot.controls.filter { $0.placement != .verticalBars }
301
- let verticalSearchActive = verticalBars.contains { $0.search.map { $0.available && $0.active } ?? false }
302
- let width = snapshot.viewportWidth
303
- self.revision = next
304
- if snapshots.isEmpty && verticalBars.isEmpty {
305
- self.removeControls(duration: duration)
306
- call.resolve(["revision": next]); return
307
- }
308
- let scale = webView.bounds.width / width
309
- let retained = Set(snapshots.map(\.id))
310
- for id in Array(self.controls.keys) where !retained.contains(id) {
311
- self.removeControl(id, duration: duration)
312
- }
313
- var rejectedControls: [String] = []
314
- var fabs: [(ShellFab, ShellControl)] = []
315
- var searches: [(ShellSearchControlling, ShellControl, CGRect, CGRect, UIView?, Bool)] = []
316
- var rejectedSearches: [String] = []
317
- if verticalBars.isEmpty || (self.keyboardVisible && !verticalSearchActive) {
318
- self.verticalBars?.detach(duration: duration)
319
- self.verticalBars = nil
320
- if self.keyboardVisible { rejectedControls.append(contentsOf: verticalBars.map(\.id)) }
321
- } else if let owner = self.bridge?.viewController {
322
- let entering = self.verticalBars == nil
323
- let rail = self.verticalBars ?? ShellVerticalBarsController(
324
- activate: { [weak self] id in self?.activate(id) },
325
- changed: { [weak self] id, phase, value, composing, valueVersion in
326
- self?.searchChanged(id, phase: phase, value: value, composing: composing, valueVersion: valueVersion) ?? 0
327
- })
328
- self.verticalBars = rail
329
- rail.attach(to: owner, in: owner.view)
330
- if let frame = snapshot.verticalBarFrame {
331
- rail.view.frame = webView.convert(frame.rect.applying(CGAffineTransform(scaleX: scale, y: scale)), to: owner.view)
332
- } else {
333
- rail.view.frame = owner.view.bounds
334
- }
335
- // The shell owns the entry crossfade; later native updates keep SwiftUI motion.
336
- rail.apply(verticalBars, rendering: self.rendering, edge: snapshot.verticalBarEdge ?? "right", animated: !entering)
337
- rail.view.isHidden = false
338
- if entering { ShellCrossfade.enter(rail.view, duration: duration) }
339
- } else {
340
- rejectedControls.append(contentsOf: verticalBars.map(\.id))
341
- }
342
- if snapshots.isEmpty {
343
- self.host?.removeFromSuperview()
344
- self.host = nil
345
- call.resolve(["revision": next, "rejectedControls": rejectedControls])
346
- return
347
- }
348
- let host = self.host ?? ShellHost()
349
- self.host = host
350
- host.frame = parent.bounds
351
- host.autoresizingMask = [.flexibleWidth, .flexibleHeight]
352
- host.backgroundColor = .clear
353
- host.isAccessibilityElement = false
354
- UIView.performWithoutAnimation {
355
- if host.superview !== parent { parent.addSubview(host) }
356
- for node in snapshots {
357
- let id = node.id
358
- let previous = self.controls[id]
359
- let previousBounds = previous?.bounds
360
- let previousCenter = previous?.center
361
- let reject = {
362
- // Keep the old cover until JS paints Web and omits this ID.
363
- if let previous, self.controls[id] === previous {
364
- previous.bounds = previousBounds!
365
- previous.center = previousCenter!
366
- previous.layoutIfNeeded()
367
- } else { self.removeControl(id) }
368
- rejectedControls.append(id)
369
- }
370
- // Only native search owns its keyboard; other controls return to Web.
371
- if self.keyboardVisible && self.searchControllers[id]?.ownsKeyboard != true {
372
- reject(); continue
373
- }
374
- let local = node.frame.rect
375
- let bounds = webView.convert(CGRect(x: local.minX * scale, y: local.minY * scale,
376
- width: local.width * scale, height: local.height * scale), to: parent)
377
- if let search = node.search {
378
- guard let owner = self.bridge?.viewController else { rejectedSearches.append(id); continue }
379
- let controller: ShellSearchControlling
380
- let previousCover = self.controls[id]
381
- let replacing = self.searchControllers[id]
382
- if let existing = replacing { controller = existing }
383
- else {
384
- // Keep the ordinary UITabBar cover until the search controller applies.
385
- controller = ShellSearchController()
386
- controller.activate = { [weak self] id in self?.activate(id) }
387
- controller.changed = { [weak self] id, phase, value, composing, valueVersion in
388
- self?.searchChanged(id, phase: phase, value: value, composing: composing, valueVersion: valueVersion) ?? 0
389
- }
390
- controller.attach(to: owner, in: owner.view)
391
- self.searchControllers[id] = controller
392
- self.controls[id] = controller.surface
393
- }
394
- let trigger = search.trigger.frame.rect
395
- let searchBarFrame = webView.convert(CGRect(x: webView.bounds.minX + local.minX * scale,
396
- y: webView.bounds.minY + local.minY * scale, width: local.width * scale, height: local.height * scale), to: owner.view)
397
- let triggerFrame = webView.convert(CGRect(x: webView.bounds.minX + trigger.minX * scale,
398
- y: webView.bounds.minY + trigger.minY * scale, width: trigger.width * scale, height: trigger.height * scale), to: owner.view)
399
- searches.append((controller, node, searchBarFrame, triggerFrame, previousCover, replacing == nil))
400
- continue
401
- } else if self.searchControllers[id] != nil {
402
- self.removeControl(id)
403
- }
404
- if self.fingerprints[id] != node {
405
- if node.kind == ShellFab.kind {
406
- let fab = (self.controls[id] as? ShellFab) ?? ShellFab()
407
- if fab.superview == nil { host.addSubview(fab) }
408
- self.controls[id] = fab
409
- fabs.append((fab, node))
410
- } else if let segment = self.controls[id] as? ShellSegment, node.kind == ShellSegment.kind {
411
- segment.update(node, scale: scale, rendering: self.rendering)
412
- } else if let tabBar = self.controls[id] as? UITabBar, node.kind == ShellTabBar.kind {
413
- self.syncTabBar(tabBar, id: id, node: node)
414
- } else {
415
- guard let control = ShellComponents.make(node, scale: scale, rendering: self.rendering, tabDelegate: self,
416
- activate: { [weak self] id in self?.activate(id) }) else { reject(); continue }
417
- self.controls.removeValue(forKey: id)?.removeFromSuperview()
418
- self.controls[id] = control
419
- host.addSubview(control)
420
- }
421
- self.fingerprints[id] = node
422
- } else if let tabBar = self.controls[id] as? UITabBar, self.pendingTabSelections[id] != nil {
423
- // Resolve an in-flight native tap even when other fingerprint fields are unchanged.
424
- self.syncTabBar(tabBar, id: id, node: node)
425
- }
426
- guard let control = self.controls[id] else { reject(); continue }
427
- if let tabBar = control as? UITabBar {
428
- guard ShellTabBar.fit(tabBar, node: node, bounds: bounds) else { reject(); continue }
429
- } else if control.frame != bounds {
430
- control.frame = bounds
431
- }
432
- control.overrideUserInterfaceStyle = node.dark ? .dark : .light
433
- }
434
- host.isHidden = false
435
- host.layoutIfNeeded()
436
- }
437
- // FABs update in place. Only icon content animates; the glass surface
438
- // and Ionic's staggered child visibility keep their own identities.
439
- for (fab, node) in fabs {
440
- fab.apply(node, scale: scale, rendering: self.rendering, activate: { [weak self] id in self?.activate(id) })
441
- }
442
- for (controller, node, frame, triggerFrame, previousCover, created) in searches {
443
- let id = node.id
444
- let webFrame = webView.convert(webView.bounds, to: controller.surface.superview)
445
- if !controller.apply(node, webFrame: webFrame, barFrame: frame, triggerFrame: triggerFrame, rendering: self.rendering) {
446
- if created {
447
- self.searchControllers.removeValue(forKey: id)
448
- controller.detach()
449
- if let previousCover, previousCover.superview != nil {
450
- self.controls[id] = previousCover
451
- } else {
452
- self.controls.removeValue(forKey: id)?.removeFromSuperview()
453
- self.fingerprints.removeValue(forKey: id)
454
- self.pendingTabSelections[id] = nil
455
- self.pendingTabExpiryWorks.removeValue(forKey: id)?.cancel()
456
- }
457
- } else {
458
- self.removeControl(id)
459
- }
460
- rejectedSearches.append(id)
461
- } else {
462
- if created, let previousCover, previousCover !== controller.surface {
463
- previousCover.removeFromSuperview()
464
- }
465
- controller.surface.isHidden = self.keyboardVisible && !controller.ownsKeyboard
466
- }
467
- }
468
- for (id, control) in self.controls where !existing.contains(id) && self.searchControllers[id] == nil {
469
- ShellCrossfade.enter(control, duration: duration)
470
- }
471
- let complete = { call.resolve(["revision": next, "rejectedSearches": rejectedSearches, "rejectedControls": rejectedControls]) }
472
- if let coordinator = searches.first?.0.transitionCoordinator,
473
- coordinator.animate(alongsideTransition: nil, completion: { _ in complete() }) { return }
474
- complete()
475
-
476
- }
477
- }
478
-
479
- public func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
480
- guard item.isEnabled, let itemId = item.accessibilityIdentifier else { return }
481
- if let controlId = controls.first(where: { $0.value === tabBar })?.key {
482
- let pending = ShellTabBar.PendingSelection.start(itemId)
483
- pendingTabSelections[controlId] = pending
484
- schedulePendingTabExpiry(controlId, until: pending.until)
485
- }
486
- activate(itemId)
487
- }
488
-
489
- private func searchChanged(_ id: String, phase: ShellSearchPhase, value: String,
490
- composing: Bool, valueVersion: Int) -> Int {
491
- sequence += 1
492
- notifyListeners("search", data: ["id": id, "phase": phase.rawValue, "value": value,
493
- "composing": composing, "valueVersion": valueVersion, "sequence": sequence, "revision": revision])
494
- return sequence
495
- }
496
-
497
- private func activate(_ id: String) {
498
- sequence += 1
499
- notifyListeners("activate", data: ["id": id, "revision": revision, "sequence": sequence])
500
-
501
- }
502
- }
@@ -1,119 +0,0 @@
1
- import UIKit
2
- import WebKit
3
-
4
- /// Intercepts only a prepared blank window. Other requests keep Capacitor's behavior.
5
- final class ShellOverlayController: NSObject, WKUIDelegate {
6
- private weak var source: WKWebView?
7
- private weak var owner: UIViewController?
8
- private let original: WKUIDelegate?
9
- private let automaticallyOpensWindows: Bool
10
- private var prepared: String?
11
- private let event: (String, String) -> Void
12
- private var id: String?
13
- private var host: ShellOverlayHost?
14
-
15
- init(source: WKWebView, owner: UIViewController, event: @escaping (String, String) -> Void) {
16
- self.source = source
17
- self.owner = owner
18
- self.event = event
19
- original = source.uiDelegate
20
- automaticallyOpensWindows = source.configuration.preferences.javaScriptCanOpenWindowsAutomatically
21
- super.init()
22
- source.uiDelegate = self
23
- }
24
-
25
- override func responds(to selector: Selector!) -> Bool {
26
- super.responds(to: selector) || original?.responds(to: selector) == true
27
- }
28
-
29
- override func forwardingTarget(for selector: Selector!) -> Any? {
30
- original?.responds(to: selector) == true ? original : super.forwardingTarget(for: selector)
31
- }
32
-
33
- func prepare(_ id: String) -> Bool {
34
- guard prepared == nil, host == nil else { return false }
35
- prepared = id
36
- source?.configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
37
- return true
38
- }
39
-
40
- func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
41
- for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
42
- // WebKit percent-encodes '#' inside about: URLs; compare the decoded form.
43
- guard webView === source, navigationAction.sourceFrame.isMainFrame,
44
- let prepared, navigationAction.targetFrame == nil,
45
- navigationAction.request.url?.absoluteString.removingPercentEncoding == "about:blank#\(prepared)" else {
46
- return original?.webView?(webView, createWebViewWith: configuration,
47
- for: navigationAction, windowFeatures: windowFeatures)
48
- }
49
- self.prepared = nil
50
- source?.configuration.preferences.javaScriptCanOpenWindowsAutomatically = automaticallyOpensWindows
51
- id = prepared
52
- let host = ShellOverlayHost(configuration: configuration, options: nil) { [weak self] action in
53
- self?.event(prepared, action)
54
- }
55
- host.webView.uiDelegate = self
56
- host.webView.frame = source?.bounds ?? .zero
57
- self.host = host
58
- return host.webView
59
- }
60
-
61
- func present(_ id: String, options: [String: Any]?, anchored: (String, ShellOverlayHost, @escaping (Bool) -> Void) -> Bool,
62
- completion: @escaping (Bool) -> Void) {
63
- guard self.id == id, let host, let owner,
64
- owner.presentedViewController == nil else { completion(false); return }
65
- host.apply(options)
66
- var settled = false
67
- var timeout: DispatchWorkItem?
68
- let complete: (Bool) -> Void = { presented in
69
- guard !settled else { return }
70
- settled = true
71
- timeout?.cancel()
72
- timeout = nil
73
- completion(presented)
74
- }
75
- timeout = DispatchWorkItem { [weak host] in
76
- host?.cancelAnchored?()
77
- complete(false)
78
- }
79
- DispatchQueue.main.asyncAfter(deadline: .now() + ShellOverlayHost.completionTimeout, execute: timeout!)
80
- if host.options?["kind"] as? String == "popover",
81
- let anchor = host.options?["anchorId"] as? String,
82
- anchored(anchor, host, complete) { return }
83
- complete(false)
84
- }
85
-
86
- // Keep the WebView alive until JavaScript has restored the adopted nodes and listeners.
87
- func dismiss(_ id: String, animated: Bool, completion: @escaping () -> Void) {
88
- guard self.id == id, let host else { completion(); return }
89
- host.dismissPopover(animated: animated, completion: completion)
90
- }
91
-
92
- func close(_ id: String, completion: @escaping () -> Void) {
93
- if prepared == id {
94
- prepared = nil
95
- source?.configuration.preferences.javaScriptCanOpenWindowsAutomatically = automaticallyOpensWindows
96
- }
97
- guard self.id == id, let host else { completion(); return }
98
- let finish = { [self] in
99
- host.webView.uiDelegate = nil
100
- self.host = nil
101
- self.id = nil
102
- completion()
103
- }
104
- host.dismissPopover(animated: false, completion: finish)
105
- }
106
-
107
- func stop(completion: @escaping () -> Void) {
108
- let finish = { [self] in
109
- prepared = nil
110
- if let source, source.uiDelegate === self {
111
- source.uiDelegate = original
112
- source.configuration.preferences.javaScriptCanOpenWindowsAutomatically = automaticallyOpensWindows
113
- }
114
- completion()
115
- }
116
- if let id { close(id, completion: finish) }
117
- else { finish() }
118
- }
119
- }