@webview-native-ui/engine 1.0.0 → 1.0.2-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/README.md +48 -35
  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/bootstrap.js +1 -1
  116. package/dist/native/session.js +14 -8
  117. package/dist/projection/pause.d.ts +10 -0
  118. package/dist/projection/pause.js +58 -0
  119. package/dist/projection/styles.d.ts +1 -1
  120. package/dist/projection/styles.js +0 -1
  121. package/dist/projection/surfaces.js +11 -8
  122. package/dist/projection/web.js +48 -13
  123. package/dist/types.d.ts +5 -1
  124. package/docs/README.md +17 -10
  125. package/docs/concepts.md +39 -14
  126. package/docs/configuration/README.md +4 -2
  127. package/docs/configuration/ionic-overlays.md +36 -2
  128. package/docs/configuration/pause-resume.md +51 -0
  129. package/docs/controls.md +39 -0
  130. package/docs/getting-started/capacitor.md +3 -3
  131. package/docs/getting-started/cordova.md +3 -1
  132. package/docs/getting-started/initialization.md +18 -2
  133. package/docs/reference/platforms.md +2 -2
  134. package/docs/reference/projection.md +1 -1
  135. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButton.swift +98 -0
  136. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButtons.swift +24 -0
  137. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIComponents.swift +19 -0
  138. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIFab.swift +80 -0
  139. package/ios/Sources/WebViewNativeUIControls/Components/NativeUISearchController.swift +449 -0
  140. package/ios/Sources/WebViewNativeUIControls/Components/NativeUISegment.swift +91 -0
  141. package/ios/Sources/WebViewNativeUIControls/Components/NativeUITabBar.swift +213 -0
  142. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIToolbarTransition.swift +186 -0
  143. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalBars.swift +696 -0
  144. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalSearch.swift +117 -0
  145. package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUI.swift +180 -0
  146. package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUIRendering.swift +38 -0
  147. package/ios/Sources/WebViewNativeUIControls/NativeUIController.swift +801 -0
  148. package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayController.swift +131 -0
  149. package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayHost.swift +85 -0
  150. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIControlRendering.swift +21 -0
  151. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICoveredSnapshots.swift +14 -0
  152. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICrossfade.swift +35 -0
  153. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIHost.swift +8 -0
  154. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIScrollTracker.swift +2 -0
  155. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUISnapshot.swift +206 -0
  156. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIVerticalBarsGeometry.swift +93 -0
  157. package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
  158. package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +6 -4
  159. package/ios/Sources/WebViewNativeUIEngine/ProjectionOrigins.swift +31 -0
  160. package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -4
  161. package/package.json +30 -5
  162. package/src/controls/activation.ts +427 -0
  163. package/src/controls/components/index.ts +45 -0
  164. package/src/controls/components/ion-back-button.ts +22 -0
  165. package/src/controls/components/ion-button.ts +28 -0
  166. package/src/controls/components/ion-buttons.ts +37 -0
  167. package/src/controls/components/ion-fab.ts +151 -0
  168. package/src/controls/components/ion-menu-button.ts +37 -0
  169. package/src/controls/components/ion-segment.ts +27 -0
  170. package/src/controls/components/ion-tab-bar.ts +55 -0
  171. package/src/controls/components/searchable-tabs.ts +534 -0
  172. package/src/controls/definitions.ts +338 -0
  173. package/src/controls/extensions.ts +198 -0
  174. package/src/controls/foldable/fold-state.ts +10 -0
  175. package/src/controls/foldable/types.ts +4 -0
  176. package/src/controls/html.ts +239 -0
  177. package/src/controls/index.ts +7 -0
  178. package/src/controls/ionic.ts +1 -0
  179. package/src/controls/legacy-layout.ts +21 -0
  180. package/src/controls/overlays/content.ts +51 -0
  181. package/src/controls/overlays/controller.ts +394 -0
  182. package/src/controls/overlays/covered-overlay.ts +11 -0
  183. package/src/controls/overlays/dialog-animation.ts +53 -0
  184. package/src/controls/overlays/dismiss-animation.ts +15 -0
  185. package/src/controls/overlays/focus.ts +64 -0
  186. package/src/controls/overlays/popover.ts +116 -0
  187. package/src/controls/overlays/styles.ts +195 -0
  188. package/src/controls/prehide.ts +285 -0
  189. package/src/controls/runtime.ts +1731 -0
  190. package/src/controls/shared/bridge.ts +18 -0
  191. package/src/controls/shared/candidate.ts +163 -0
  192. package/src/controls/shared/clipping.ts +1 -0
  193. package/src/controls/shared/connection.ts +105 -0
  194. package/src/controls/shared/control-options.ts +20 -0
  195. package/src/controls/shared/covered-snapshot.ts +52 -0
  196. package/src/controls/shared/covered-source.ts +78 -0
  197. package/src/controls/shared/crossfade.ts +70 -0
  198. package/src/controls/shared/diagnostics.ts +13 -0
  199. package/src/controls/shared/dom.ts +310 -0
  200. package/src/controls/shared/events.ts +14 -0
  201. package/src/controls/shared/horizontal-navigation.ts +106 -0
  202. package/src/controls/shared/icons.ts +64 -0
  203. package/src/controls/shared/modal-animation.ts +109 -0
  204. package/src/controls/shared/modal.ts +286 -0
  205. package/src/controls/shared/projection-id.ts +2 -0
  206. package/src/controls/shared/scroll-placement.ts +1 -0
  207. package/src/controls/shared/tab-bar-width.ts +66 -0
  208. package/src/controls/shared/toolbar-options.ts +51 -0
  209. package/src/controls/shared/transition-identity.ts +67 -0
  210. package/src/controls/shared/vertical-bars-geometry.ts +92 -0
  211. package/src/controls/shared/vertical-navigation.ts +34 -0
  212. package/src/controls/startup.ts +74 -0
  213. package/src/controls/transport.ts +11 -0
  214. package/src/controls/vertical-bars-web-search.ts +86 -0
  215. package/src/controls/vertical-bars-web.ts +674 -0
  216. package/src/dom/geometry.ts +24 -0
  217. package/src/dom/overlays.ts +196 -0
  218. package/src/dom/scroll-bindings.ts +66 -0
  219. package/src/dom/scroll-placement.ts +92 -0
  220. package/src/dom/visibility.ts +29 -0
  221. package/src/index.ts +19 -0
  222. package/src/integration/index.ts +64 -0
  223. package/src/ionic.ts +34 -0
  224. package/src/native/bootstrap.ts +132 -0
  225. package/src/native/bridge.ts +18 -0
  226. package/src/native/resource-url.ts +11 -0
  227. package/src/native/session.ts +708 -0
  228. package/src/native/updates.ts +103 -0
  229. package/src/native/values.ts +29 -0
  230. package/src/projection/events.ts +58 -0
  231. package/src/projection/pause.ts +58 -0
  232. package/src/projection/placement.ts +34 -0
  233. package/src/projection/scope.ts +13 -0
  234. package/src/projection/styles.ts +26 -0
  235. package/src/projection/surface.ts +88 -0
  236. package/src/projection/surfaces.ts +119 -0
  237. package/src/projection/web.ts +1013 -0
  238. package/src/types.ts +58 -0
@@ -5,10 +5,17 @@ above Engine-managed native content. Native registration handles capture and
5
5
  restoration, but it cannot infer Ionic's presentation lifecycle from a native
6
6
  view. This shared monitor supplies that coverage decision for all registrations.
7
7
 
8
- Integrate the plugin through the [Capacitor](../getting-started/capacitor.md) or
9
- [Cordova](../getting-started/cordova.md) guide first. Non-Ionic applications use their own
8
+ Applications use this adapter with an Engine-enabled plugin. Plugin authors first
9
+ integrate through the [Capacitor](../getting-started/capacitor.md) or
10
+ [Cordova](../getting-started/cordova.md) guide. Non-Ionic applications use their own
10
11
  lifecycle notifications and do not install this adapter.
11
12
 
13
+ The `/ionic` entry point provides shared framework integration. It observes Ionic
14
+ overlays and determines which registered surfaces they cover. A plugin's explicit
15
+ presentation controls remain part of that plugin's API, connected through
16
+ [projection notifications](events.md) or its existing
17
+ [renderer API](../advanced/renderer.md).
18
+
12
19
  For example, opening an Ionic settings modal should cover the background native
13
20
  tab bar while leaving the modal's own native close button usable. A blanket hide
14
21
  notification cannot express that relationship. The shared monitor evaluates each
@@ -32,6 +39,10 @@ One monitor is shared per document, including between the npm module and the run
32
39
 
33
40
  The default covers Modal, Popover, Alert, Action Sheet, Loading, Picker, Toast and Menu lifecycle events. It compares each projection's DOM with the active overlay stack: content inside the front modal stays native, while covered content temporarily uses its prepared image. Toasts and inactive sheet backdrops do not cover a surface. Nested overlays keep it covered until the remaining stack permits restoration. Missing images retain the normal Web fallback. This temporary coverage never overwrites a plugin's explicit `web` or `none` destination. Projections registered later read the current state.
34
41
 
42
+ Here, Alert means Ionic's `ion-alert`. This adapter does not intercept
43
+ `window.alert()`, detect HTML `<dialog>` automatically, or observe native dialogs.
44
+ Custom Web overlays can supply lifecycle events through `additional`, below.
45
+
35
46
  A popover displayed in a native window is different from a popover rendered in
36
47
  the WebView: its DOM placeholder must not be treated as a covering Web overlay.
37
48
  The `@rdlabo/capacitor-native-ui` plugin reports this native presentation ownership
@@ -42,6 +53,8 @@ explicit integration; it is not inferred from the popover's DOM alone.
42
53
  ## Customize unusual layouts
43
54
 
44
55
  Optional settings are document-wide. Keep the defaults for ordinary Ionic overlays.
56
+ `IonicOverlayOptions`, exported from the same `/ionic` entry point, is the TypeScript
57
+ type for these settings; it is not another initialization function.
45
58
  Use `exclude` for an application overlay that should not cover native content, and
46
59
  `additional` when the same app also uses a custom dialog with its own lifecycle
47
60
  events. Use `covers` only when DOM containment and backdrop state do not describe
@@ -63,3 +76,24 @@ enableSupportIonicOverlays({
63
76
  ```
64
77
 
65
78
  Use `covers(overlay, target)` only when a registered DOM projection needs a custom coverage rule; return `undefined` for the default, or a boolean to override it. Exclusions and explicit native-window ownership take precedence. The standard relationship uses overlay order, DOM containment and backdrop state, rather than attempting to interpret arbitrary CSS stacking contexts. Configure unusual layouts explicitly.
79
+
80
+ ## When a plugin needs to know which modal is behind another
81
+
82
+ For ordinary application setup, use `enableSupportIonicOverlays()` as shown above.
83
+ The Engine then handles Ionic overlay coverage without additional calls.
84
+
85
+ A plugin that places native controls inside Ionic modals may also need to know
86
+ which modal lies behind the one being opened. Two helpers from the same `/ionic`
87
+ entry point let the plugin read the modal order already tracked by the Engine.
88
+
89
+ For example, open modal A, then modal B above it:
90
+
91
+ - `ionicModalStack(document)` returns `[A, B]`, from back to front.
92
+ - `coveredIonicSurface(B)` returns A, the modal beneath B.
93
+ - If a popover opens above B, `coveredIonicSurface(popover)` returns B.
94
+ - `coveredIonicSurface(A)` returns `undefined`: the page behind A is not a modal.
95
+
96
+ A and B here are the actual `ion-modal` DOM elements. These helpers only read the
97
+ tracked modal order; they do not open or close overlays, switch projection, or
98
+ start monitoring. Call them after `enableSupportIonicOverlays()` has enabled the
99
+ monitor and the relevant overlay has entered its presentation lifecycle.
@@ -0,0 +1,51 @@
1
+ # Cover native UI with a custom Web overlay
2
+
3
+ Use `pause()` before presenting a custom modal or `div` over native UI and
4
+ `resume()` when it closes. For Ionic overlays, use the automatic
5
+ [Ionic integration](ionic-overlays.md), which shares this coordination but only
6
+ covers surfaces behind the overlay.
7
+
8
+ ## Plugin authors: re-export the Engine functions
9
+
10
+ ```ts
11
+ export { pause, resume } from '@webview-native-ui/engine';
12
+ ```
13
+
14
+ The functions share one pause count across Engine-enabled plugins and bundles in
15
+ the same document. No wrapper or native plugin method is needed.
16
+
17
+ ## Application authors: pair each pause with a resume
18
+
19
+ ```ts
20
+ import { pause, resume } from 'your-plugin';
21
+
22
+ pause();
23
+ try {
24
+ await showCustomModal();
25
+ } finally {
26
+ resume();
27
+ }
28
+ ```
29
+
30
+ `showCustomModal()` must resolve when the modal closes. With event-driven modals,
31
+ call `resume()` on dismissal, cancellation or failure. Use one pair per overlay,
32
+ even with multiple plugins; nested overlays each need their own pair.
33
+
34
+ A pause replaces visible native surfaces with prepared Web images or their normal
35
+ Web fallback. Hidden surfaces stay hidden. The final `resume()` releases
36
+ the request, preserving each plugin's presentation intent and remaining Ionic
37
+ overlay coverage.
38
+
39
+ The counter does not track callers or pair individual calls. An extra `resume()`
40
+ while paused can release another overlay's request; only a `resume()` at zero has
41
+ no effect. Each owner must release exactly once for each pause it requested.
42
+
43
+ Both functions return immediately, without waiting for native acknowledgments or
44
+ painting. Returning from `pause()` does not mean native UI has already moved
45
+ behind the Web overlay; it may remain in front until the handoff completes.
46
+ The final `resume()` defers native restoration until the second animation frame,
47
+ allowing overlay removal to paint without guaranteeing compositor completion.
48
+ Call it after removing the overlay or completing
49
+ its dismissal animation. A new `pause()` cancels a pending restoration.
50
+ Calling `pause()` just before a blocking `window.alert()` therefore does
51
+ not guarantee a completed handoff. They do not detect custom overlays automatically.
@@ -0,0 +1,39 @@
1
+ # Shared controls for native adapters
2
+
3
+ The existing Engine package also owns the control implementation used by Capacitor Native UI.
4
+ No additional npm package or workspace is required.
5
+
6
+ - `@webview-native-ui/engine`: generic projection API; unchanged dependency boundary.
7
+ - `@webview-native-ui/engine/controls`: control runtime and transport contract.
8
+ - `@webview-native-ui/engine/controls/ionic`: optional Ionic overlay implementation.
9
+ - `@webview-native-ui/engine/integration`: the single shared transition/search integration state.
10
+
11
+ An adapter creates one `createNativeUI({ plugin, getPlatform, isAvailable })` instance and keeps it
12
+ for its lifetime. The plugin implements the `NativeUIPlugin` transport interface. The adapter owns
13
+ host registration, method dispatch, listener delivery, and lifecycle hooks; Engine owns rendering,
14
+ DOM ownership, extension acknowledgement, and teardown sequencing. Cordova can supply this same
15
+ transport without importing Capacitor. The Cordova Native UI plugin itself is a subsequent change.
16
+
17
+ SwiftPM exposes the optional `WebViewNativeUIControls` product, depending on `WebViewNativeUIEngine`.
18
+ CocoaPods exposes `WebViewNativeUIControls` with the same dependency. Android exposes the optional
19
+ `io.github.webview-native-ui:controls` artifact from `android/controls`; it depends on `engine` and
20
+ Compose. Generic Engine consumers keep their existing dependencies and Android minimum SDK 23.
21
+ Controls require Android API 24. Native adapters create `NativeUIController` and forward requests
22
+ and events. iOS receives JSON dictionaries and completion closures; Android receives JSONObjects.
23
+
24
+ The Ionic theme imports Engine integration directly. The Capacitor compatibility integration export
25
+ re-exports that module. Resolve a single Engine version in the application; do not bundle a private
26
+ copy of integration into the theme or adapter. The `/controls` entry does not load Ionic runtime code;
27
+ load `/controls/ionic` only when opting into Ionic overlays.
28
+
29
+ ## Verification and release order
30
+
31
+ Run `npm run test:controls` for the migrated control tests and `npm test` for generic projection.
32
+ SwiftPM's `WebViewNativeUIEngine-Package` scheme covers both native targets. Android's
33
+ `:controls:testReleaseUnitTest` covers the migrated snapshot parser. Existing Capacitor transport
34
+ and hosted lifecycle tests remain in its repository, and the theme retains the real demo E2E suite.
35
+
36
+ Build and verify Engine first, including both signed Maven artifacts in the same Central bundle and
37
+ both npm-distributed podspecs. Release the Engine native artifacts and npm package at the same version
38
+ before refreshing the Capacitor and theme registry lockfiles. Candidate verification uses local npm
39
+ tarballs and a temporary SwiftPM path or local Maven repository; do not commit these temporary paths.
@@ -1,6 +1,6 @@
1
1
  # Capacitor plugin integration
2
2
 
3
- Add Web projection and restoration to an existing Capacitor plugin without changing its public `registerPlugin()` export. The plugin keeps its native renderer and application API; the Engine coordinates its native view with a DOM fallback.
3
+ Let an existing Capacitor plugin's native components coexist with Web layouts, navigation and overlays without changing its public `registerPlugin()` export. The plugin keeps its native renderer and application API; registration connects each native view to its DOM container using the [shared presentation model](../concepts.md).
4
4
 
5
5
  This guide covers dependencies, registration, notifications and teardown. Start with
6
6
  [native initialization](initialization.md) for the minimal call, required inputs
@@ -40,9 +40,9 @@ The primary native distribution is Swift Package Manager. Depend on the reposito
40
40
  .product(name: "WebViewNativeUIEngine", package: "engine")
41
41
  ```
42
42
 
43
- While the API is pre-1.0, pin the same Engine release across npm, SwiftPM, Gradle and CocoaPods, and upgrade deliberately. Applications should retain their dependency lockfiles for reproducible builds.
43
+ Use the same Engine release across npm, SwiftPM, Gradle and CocoaPods, and upgrade deliberately. Applications should retain their dependency lockfiles for reproducible builds.
44
44
 
45
- SwiftPM resolves this repository's version tags such as `v0.0.1`. For unreleased integration tests, use a local checkout or an explicit tested commit.
45
+ SwiftPM resolves this repository's version tags such as `v1.0.0`. For unreleased integration tests, use a local checkout or an explicit tested commit.
46
46
 
47
47
  `import WebViewNativeUIEngine` requires no Capacitor registration. The npm companion supplies DOM measurement and Web image handoff; SwiftPM resolves the native dependency independently of npm hoisting.
48
48
 
@@ -1,5 +1,7 @@
1
1
  # Cordova plugin integration
2
2
 
3
+ Let a Cordova plugin's native components coexist with Web layouts, navigation and overlays using the [same presentation model](../concepts.md) as Capacitor.
4
+
3
5
  The same npm package, Swift library and Android AAR support Capacitor and Cordova. Engine is a library used by plugin maintainers, not a separately registered Cordova plugin. A Cordova application does not need `@capacitor/core` or a Capacitor native SDK.
4
6
 
5
7
  The consuming plugin still owns its native view and the corresponding DOM, including placement, size and stacking order. Supply an existing selector such as `native-ad` or `#player`. The DOM may be added after initialization. Image preparation, scroll handling, overlays and restoration use the common Engine implementation.
@@ -32,7 +34,7 @@ frameworks report their own transition lifecycle through notifications.
32
34
  Replace `<engine-version>` in every dependency below with the same published
33
35
  release. Read that release tag’s documentation so the examples match its APIs.
34
36
 
35
- Use the same Engine release for the Web package and native libraries. The API is pre-1.0; pin versions and retain dependency lockfiles.
37
+ Use the same Engine release for the Web package and native libraries. Pin matching versions and retain dependency lockfiles for reproducible builds.
36
38
 
37
39
  ```sh
38
40
  npm install --save-exact '@webview-native-ui/engine@<engine-version>'
@@ -2,7 +2,10 @@
2
2
 
3
3
  Start with `NativeUIProjection` when a plugin owns a native view and a corresponding
4
4
  Web container. Registration starts the runtime, binds that container and coordinates
5
- image handoff and restoration. You do not need the renderer APIs for this route.
5
+ presentation between the native view and the Web layout. This is the same starting
6
+ point for a component in a scrolling feed or beneath a Web overlay. You do not need
7
+ the renderer APIs for this route. See [the shared model](../concepts.md) for how
8
+ these situations relate and where platform limits apply.
6
9
 
7
10
  Use the [Capacitor](capacitor.md) or [Cordova](cordova.md) guide for
8
11
  dependency installation, plugin lifecycle hooks and notification forwarding.
@@ -46,7 +49,20 @@ projection = NativeUIProjection(
46
49
  )
47
50
  ```
48
51
 
49
- Both snippets belong inside the integration guide's error boundary; retain the
52
+ Java can use the same defaults without supplying every optional argument:
53
+
54
+ ```java
55
+ NativeUIProjection.install(getBridge().getWebView());
56
+ projection = new NativeUIProjection(
57
+ getActivity().getWindow(),
58
+ getBridge().getWebView(),
59
+ getPluginHandle().getId(),
60
+ "#native-ui-fallback",
61
+ () -> nativeView
62
+ );
63
+ ```
64
+
65
+ These snippets belong inside the integration guide's error boundary; retain the
50
66
  existing Web fallback if initialization fails. `projection` and `nativeView` are
51
67
  properties of your plugin. Dispose the registration with `destroy()` on iOS or
52
68
  `close()` on Android when that renderer is retired. All native lifecycle calls
@@ -100,8 +100,8 @@ precision alone does not resolve this difference.
100
100
  Left: **iPhone 15 Pro Max, iOS 27.2**. Right: **iPad mini (6th generation),
101
101
  iPadOS 27.2**. Each device screenshot shows the captured **16-bit Display P3 PNG
102
102
  in WKWebView above** and the **live UIKit tab bar below**, at the same size within
103
- that device. The screenshots were placed side by side without rescaling or changing
104
- their shared color profile. These are physical-device results, not Simulator captures.
103
+ that device. For this comparison, the iPhone screenshot was resized to match the iPad
104
+ screenshot's height; their shared color profile was preserved. These are physical-device results, not Simulator captures.
105
105
 
106
106
  The system screenshot preserves the live appearance, but the application-level
107
107
  capture does not reproduce it completely. On the tested iPhone, the difference was
@@ -125,7 +125,7 @@ Listeners follow host reparenting and slot reassignment and are removed on teard
125
125
 
126
126
  Scrolling is internal: iOS uses a uniquely matched native scroll binding when available; otherwise, including Android, scrolling temporarily uses the prepared image and restores native content after motion settles. Automatic scroll handling never overrides explicit `web` or `none`. Unsupported capture or geometry keeps the normal DOM fallback; it does not wait for a new capture on the transition path. The latest explicit destination survives WebView reload, and old-document replies cannot complete a new document's requests.
127
127
 
128
- By default, the internal channel is limited to the main frame on Capacitor's localhost origins. Pass the application's exact `allowedOrigins` for a different configured origin. Android requires WebView's `WEB_MESSAGE_LISTENER` and `DOCUMENT_START_SCRIPT` features; registration fails explicitly on unsupported WebView implementations. Do not use wildcard origins for remotely navigable WebViews.
128
+ The internal channel is limited to the main frame. Capacitor origins come from the bundled `capacitor.config.json`: `server.url` takes precedence over the local hostname/scheme. To override them, pass exact `allowedOrigins` (on Android, to `install` before navigation; registrations inherit them). Neither `allowNavigation` nor the current WebView URL expands this set. Android requires WebView's `WEB_MESSAGE_LISTENER` and `DOCUMENT_START_SCRIPT` features; registration fails explicitly on unsupported WebView implementations. Do not use wildcard origins for remotely navigable WebViews.
129
129
 
130
130
  For capture modes, translucent surfaces, image color and private storage, see
131
131
  [capture and platform limits](platforms.md). Custom renderer bridge and scroll
@@ -0,0 +1,98 @@
1
+ import UIKit
2
+
3
+ enum NativeUIButton {
4
+ private final class BadgeLabel: UILabel {}
5
+ private static let activationID = UIAction.Identifier("native-ui-activate")
6
+
7
+ /// Reuse an unambiguous presentation slot while keeping action IDs source-specific.
8
+ @MainActor
9
+ static func reassociate(_ nodes: [NativeUIControl], controls: inout [String: UIView],
10
+ fingerprints: inout [String: NativeUIControl]) -> Set<String> {
11
+ func identity(_ node: NativeUIControl) -> String? {
12
+ guard node.placement == nil, kinds.contains(node.kind), node.items.count == 1 else { return nil }
13
+ return node.items[0].content.transitionId
14
+ }
15
+ // swiftlint:disable:next force_unwrapping
16
+ let old = Dictionary(grouping: fingerprints.values.filter { identity($0) != nil }, by: { identity($0)! })
17
+ // swiftlint:disable:next force_unwrapping
18
+ let incoming = Dictionary(grouping: nodes.filter { identity($0) != nil }, by: { identity($0)! })
19
+ let retained = Set(nodes.map(\.id))
20
+ var reused = Set<String>()
21
+ for (key, matches) in incoming {
22
+ guard matches.count == 1, let node = matches.first, controls[node.id] == nil,
23
+ let previous = old[key], previous.count == 1, let source = previous.first,
24
+ !retained.contains(source.id), let button = controls[source.id] as? UIButton else { continue }
25
+ controls.removeValue(forKey: source.id)
26
+ fingerprints.removeValue(forKey: source.id)
27
+ controls[node.id] = button
28
+ reused.insert(node.id)
29
+ }
30
+ return reused
31
+ }
32
+ // These Ionic components share the same native UIButton presentation.
33
+ static let kinds: [NativeUIComponent] = [.button, .backButton, .menuButton]
34
+
35
+ @available(iOS 26.0, *)
36
+ static func make(_ node: NativeUIControl, existing: UIButton? = nil, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UIButton? {
37
+ guard node.items.count == 1, let item = node.items.first else { return nil }
38
+ let control = render(item.content, glass: item.content.buttonFill != .clear, existing: existing, rendering: rendering, activate: activate)
39
+ control.semanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
40
+ return control
41
+ }
42
+
43
+ @available(iOS 26.0, *)
44
+ static func render(_ item: NativeUIItemContent, glass: Bool, existing: UIButton? = nil, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UIButton {
45
+ var configuration: UIButton.Configuration = glass ? .glass() : .plain()
46
+ configuration.title = item.label
47
+ configuration.image = rendering.image(item)
48
+ configuration.imagePadding = item.imagePadding ?? 4
49
+ configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: item.contentInsetLeading ?? 4,
50
+ bottom: 0, trailing: item.contentInsetTrailing ?? 4)
51
+ configuration.imagePlacement = item.iconPosition == .trailing ? .trailing : .leading
52
+ configuration.baseForegroundColor = rendering.color(item.color)
53
+ configuration.titleLineBreakMode = .byTruncatingTail
54
+ let size = item.fontSize
55
+ let weight = item.fontWeight
56
+ configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
57
+ var outgoing = incoming
58
+ outgoing.font = UIFont.systemFont(ofSize: size, weight: weight >= 600 ? .semibold : weight >= 500 ? .medium : .regular)
59
+ return outgoing
60
+ }
61
+ let button = existing ?? UIButton(configuration: configuration)
62
+ if existing != nil { button.configuration = configuration }
63
+ button.titleLabel?.numberOfLines = 1
64
+ // Web and UIKit can differ by a fraction of a point in text measurement.
65
+ // Absorb that rounding without widening the projected button.
66
+ button.titleLabel?.adjustsFontSizeToFitWidth = true
67
+ button.titleLabel?.minimumScaleFactor = 0.99
68
+ button.isEnabled = !item.disabled
69
+ button.accessibilityLabel = item.accessibilityLabel
70
+ button.accessibilityIdentifier = item.id
71
+ if item.selected { button.accessibilityTraits.insert(.selected) } else { button.accessibilityTraits.remove(.selected) }
72
+ if item.disabled { button.accessibilityTraits.insert(.notEnabled) } else { button.accessibilityTraits.remove(.notEnabled) }
73
+ button.subviews.filter { $0 is BadgeLabel }.forEach { $0.removeFromSuperview() }
74
+ button.accessibilityValue = item.badge?.value
75
+ if let badge = item.badge {
76
+ let label = BadgeLabel()
77
+ label.text = badge.value
78
+ label.font = .systemFont(ofSize: 11, weight: .semibold)
79
+ label.textColor = rendering.color(badge.textColor)
80
+ label.backgroundColor = rendering.color(badge.color)
81
+ label.textAlignment = .center
82
+ label.layer.cornerRadius = 8
83
+ label.clipsToBounds = true
84
+ label.isAccessibilityElement = false
85
+ label.translatesAutoresizingMaskIntoConstraints = false
86
+ button.addSubview(label)
87
+ NSLayoutConstraint.activate([
88
+ label.centerXAnchor.constraint(equalTo: button.centerXAnchor, constant: 14),
89
+ label.topAnchor.constraint(equalTo: button.topAnchor, constant: 1),
90
+ label.widthAnchor.constraint(greaterThanOrEqualToConstant: max(16, label.intrinsicContentSize.width + 6)),
91
+ label.heightAnchor.constraint(equalToConstant: 16)
92
+ ])
93
+ }
94
+ button.removeAction(identifiedBy: activationID, for: .touchUpInside)
95
+ button.addAction(UIAction(identifier: activationID) { _ in activate(item.id) }, for: .touchUpInside)
96
+ return button
97
+ }
98
+ }
@@ -0,0 +1,24 @@
1
+ import UIKit
2
+
3
+ enum NativeUIButtons {
4
+ static let kind = NativeUIComponent.buttons
5
+
6
+ @available(iOS 26.0, *)
7
+ static func make(_ node: NativeUIControl, scale: CGFloat, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UIView {
8
+ let items = node.items
9
+ let rtl = node.rtl
10
+ let effect = UIGlassEffect(style: .regular)
11
+ let control = UIVisualEffectView(effect: effect)
12
+ control.layer.cornerRadius = node.frame.height * scale / 2
13
+ control.clipsToBounds = true
14
+ control.accessibilityIdentifier = node.id
15
+ for item in items {
16
+ let button = NativeUIButton.render(item.content, glass: false, rendering: rendering, activate: activate)
17
+ button.semanticContentAttribute = rtl ? .forceRightToLeft : .forceLeftToRight
18
+ let rect = item.frame.rect
19
+ button.frame = CGRect(x: rect.minX * scale, y: rect.minY * scale, width: rect.width * scale, height: rect.height * scale)
20
+ control.contentView.addSubview(button)
21
+ }
22
+ return control
23
+ }
24
+ }
@@ -0,0 +1,19 @@
1
+ import UIKit
2
+
3
+ // Static composition of supported wire kinds. Component files own their names and rendering.
4
+ enum NativeUIComponents {
5
+ static let supported = Set(NativeUIButton.kinds + [NativeUIButtons.kind, NativeUITabBar.kind, NativeUISegment.kind, NativeUIFab.kind])
6
+
7
+ @available(iOS 26.0, *)
8
+ static func make(_ node: NativeUIControl, scale: CGFloat, rendering: NativeUIControlRendering,
9
+ tabDelegate: UITabBarDelegate, activate: @escaping (String) -> Void) -> UIView? {
10
+ switch node.kind {
11
+ case .button, .backButton, .menuButton: return NativeUIButton.make(node, rendering: rendering, activate: activate)
12
+ case .buttons: return NativeUIButtons.make(node, scale: scale, rendering: rendering, activate: activate)
13
+ case .tabBar: return NativeUITabBar.make(node, rendering: rendering, delegate: tabDelegate)
14
+ case .segment: return NativeUISegment.make(node, scale: scale, rendering: rendering, activate: activate)
15
+ case .fab: return NativeUIFab()
16
+ case .extension: return nil // App-local SwiftUI content is hosted by the shell.
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,80 @@
1
+ import UIKit
2
+
3
+ final class NativeUIFab: UIView {
4
+ static let kind = NativeUIComponent.fab
5
+ var buttons: [String: UIButton] = [:]
6
+ var content: [String: NativeUIItemContent] = [:]
7
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
8
+ let hit = super.hitTest(point, with: event)
9
+ return hit === self ? nil : hit
10
+ }
11
+
12
+ @available(iOS 26.0, *)
13
+ func apply(_ node: NativeUIControl, scale: CGFloat, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) {
14
+ let items = node.items
15
+ let retained = Set(items.map(\.id))
16
+ var lostFocus = false
17
+ for id in Array(self.buttons.keys) where !retained.contains(id) {
18
+ lostFocus = lostFocus || self.buttons[id]?.accessibilityElementIsFocused() == true
19
+ self.buttons.removeValue(forKey: id)?.removeFromSuperview()
20
+ self.content.removeValue(forKey: id)
21
+ }
22
+ self.accessibilityIdentifier = node.id
23
+ for item in items {
24
+ let id = item.id
25
+ let artwork = item.content.fabArtwork
26
+ let previous = self.buttons[id]
27
+ let button = previous ?? NativeUIButton.render(artwork, glass: true, rendering: rendering, activate: activate)
28
+ if previous == nil {
29
+ self.buttons[id] = button
30
+ self.addSubview(button)
31
+ } else if self.content[id] != artwork {
32
+ let changedIcon = self.content[id]?.selected != item.content.selected
33
+ let duration = UIAccessibility.isReduceMotionEnabled ? 0 : min(item.content.iconTransition ?? 0, 1)
34
+ if changedIcon, let imageView = button.imageView, duration > 0 {
35
+ UIView.transition(with: imageView, duration: duration,
36
+ options: [.transitionCrossDissolve, .beginFromCurrentState, .allowUserInteraction]) {
37
+ _ = NativeUIButton.render(artwork, glass: true, existing: button, rendering: rendering, activate: activate)
38
+ }
39
+ } else {
40
+ UIView.performWithoutAnimation { _ = NativeUIButton.render(artwork, glass: true, existing: button, rendering: rendering, activate: activate) }
41
+ }
42
+ }
43
+ self.content[id] = artwork
44
+ button.accessibilityTraits.remove(.selected)
45
+ if id == items.first?.id && items.count > 1 {
46
+ button.accessibilityValue = item.content.selected
47
+ ? NSLocalizedString("Expanded", comment: "FAB accessibility state")
48
+ : NSLocalizedString("Collapsed", comment: "FAB accessibility state")
49
+ } else {
50
+ button.accessibilityValue = nil
51
+ }
52
+ let hidden = item.visible == false
53
+ if hidden && !button.isHidden && button.accessibilityElementIsFocused() { lostFocus = true }
54
+ button.isHidden = hidden
55
+ button.isUserInteractionEnabled = button.isEnabled && !hidden
56
+ button.semanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
57
+ let rect = item.frame.rect
58
+ let frame = CGRect(x: rect.minX * scale, y: rect.minY * scale, width: rect.width * scale, height: rect.height * scale)
59
+ if button.frame != frame { button.frame = frame }
60
+ }
61
+ let ordered = items.compactMap { self.buttons[$0.id] }.filter { !$0.isHidden }
62
+ self.accessibilityElements = ordered
63
+ if lostFocus, let main = ordered.first, main.isEnabled, self.window != nil {
64
+ UIAccessibility.post(notification: .layoutChanged, argument: main)
65
+ }
66
+ }
67
+ }
68
+
69
+ extension NativeUIItemContent {
70
+ var fabArtwork: Self {
71
+ var artwork = self
72
+ if selected {
73
+ artwork.icon = closeIcon
74
+ artwork.iconWidth = closeIconWidth
75
+ artwork.iconHeight = closeIconHeight
76
+ artwork.label = ""
77
+ }
78
+ return artwork
79
+ }
80
+ }