@webview-native-ui/engine 0.0.1 → 0.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 (46) hide show
  1. package/LICENSE +17 -14
  2. package/README.md +27 -599
  3. package/WebViewNativeUIEngine.podspec +2 -2
  4. package/dist/{scroll-bindings.d.ts → dom/scroll-bindings.d.ts} +1 -1
  5. package/dist/{scroll-placement.d.ts → dom/scroll-placement.d.ts} +1 -1
  6. package/dist/index.d.ts +9 -9
  7. package/dist/index.js +8 -8
  8. package/dist/ionic.d.ts +3 -3
  9. package/dist/ionic.js +1 -1
  10. package/dist/{native-bootstrap.d.ts → native/bootstrap.d.ts} +1 -1
  11. package/dist/{native-bootstrap.js → native/bootstrap.js} +1 -1
  12. package/dist/{bridge.d.ts → native/bridge.d.ts} +1 -1
  13. package/dist/{native-session.d.ts → native/session.d.ts} +3 -2
  14. package/dist/{native-session.js → native/session.js} +8 -7
  15. package/dist/{projection-events.d.ts → projection/events.d.ts} +1 -1
  16. package/dist/{projection-events.js → projection/events.js} +1 -1
  17. package/dist/{web-projection.d.ts → projection/web.d.ts} +5 -3
  18. package/dist/{web-projection.js → projection/web.js} +9 -6
  19. package/dist/types.d.ts +1 -1
  20. package/docs/assets/hero.png +0 -0
  21. package/docs/capacitor-plugin.md +279 -0
  22. package/docs/cordova-plugin.md +198 -0
  23. package/docs/cordova.md +1 -123
  24. package/docs/maintenance.md +52 -0
  25. package/docs/projection.md +340 -0
  26. package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +6 -3
  27. package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -3
  28. package/package.json +2 -3
  29. package/NOTICE +0 -39
  30. /package/dist/{geometry.d.ts → dom/geometry.d.ts} +0 -0
  31. /package/dist/{geometry.js → dom/geometry.js} +0 -0
  32. /package/dist/{overlays.d.ts → dom/overlays.d.ts} +0 -0
  33. /package/dist/{overlays.js → dom/overlays.js} +0 -0
  34. /package/dist/{scroll-bindings.js → dom/scroll-bindings.js} +0 -0
  35. /package/dist/{scroll-placement.js → dom/scroll-placement.js} +0 -0
  36. /package/dist/{visibility.d.ts → dom/visibility.d.ts} +0 -0
  37. /package/dist/{visibility.js → dom/visibility.js} +0 -0
  38. /package/dist/{bridge.js → native/bridge.js} +0 -0
  39. /package/dist/{runtime.d.ts → native/resource-url.d.ts} +0 -0
  40. /package/dist/{runtime.js → native/resource-url.js} +0 -0
  41. /package/dist/{projection-placement.d.ts → projection/placement.d.ts} +0 -0
  42. /package/dist/{projection-placement.js → projection/placement.js} +0 -0
  43. /package/dist/{web-projection-scope.d.ts → projection/scope.d.ts} +0 -0
  44. /package/dist/{web-projection-scope.js → projection/scope.js} +0 -0
  45. /package/dist/{snapshot-styles.d.ts → projection/styles.d.ts} +0 -0
  46. /package/dist/{snapshot-styles.js → projection/styles.js} +0 -0
package/README.md CHANGED
@@ -1,624 +1,52 @@
1
1
  # WebView Native UI Engine
2
2
 
3
- `@webview-native-ui/engine` is infrastructure **for Capacitor and Cordova plugin maintainers**. It is a library dependency that renders no controls, registers no application-facing Capacitor plugin, and requires no Ionic or Angular runtime. Its native registration owns a scoped internal WebView channel.
3
+ **Bring Web UI above the native layer.**
4
4
 
5
- The engine associates existing DOM and native views, tracks supported scrolling, prepares native images before interaction, and manages image handoff and native restoration. The consuming plugin creates, attaches, positions and sizes its native views, provides the corresponding DOM, and reports presentation events. Registration does not lay out a native view from the DOM or resize it when the DOM changes. Existing renderers can also use the low-level APIs with their own ordered transport.
5
+ _Visually, through seamless native-to-Web projection._
6
6
 
7
- The native libraries support iOS (`UIKit` / `WKWebView`) and Android (`View` / `WebView`). TypeScript geometry and image presentation are framework-independent. APIs are experimental during extraction. Android placement requires API 23+, and composited snapshots require API 26+.
7
+ ![Conceptual view of a Web overlay above a native surface and WebView content](docs/assets/hero.png)
8
8
 
9
- ## Ownership
9
+ _Conceptual view. Native appearance is projected into the Web layer during overlay presentation._
10
10
 
11
- | Engine | Consuming plugin |
12
- | ------------------------------------------------------------------- | -------------------------------------------------------------------- |
13
- | DOM measurement, image geometry and supported native scroll binding | Creating, attaching, positioning and sizing native views |
14
- | Startup, event subscriptions and native draw acknowledgements | Providing the fallback DOM at the correct layer |
15
- | Prepared image cache, decoding, display and retirement | Reporting appearance changes and the intended projection destination |
16
- | Native capture and temporary return images | Providing native views and SDK capture eligibility |
17
- | Image invalidation, token cleanup and observer teardown | Owning business actions and the interactive Web fallback |
11
+ Build WebView apps where native controls and Web content share the same scrolling screens, transitions and overlays.
18
12
 
19
- A DOM slot does not necessarily contain a Web replacement: a UI plugin can restore a button, while an ad plugin may hide its slot. Image handoff is explicit and noninteractive. The engine never forwards clicks through an image or replaces SDK click/impression handling.
13
+ ## Beyond the WebView boundary
20
14
 
21
- ## Projection contract
15
+ WebView apps combine the Web's flexible layouts with access to native platform capabilities. But native UI and the Web page live in separate rendering layers. A native control placed above the WebView does not automatically participate in DOM scrolling, clipping or stacking order. A Web modal cannot simply cover it with a higher `z-index`.
22
16
 
23
- A notification specifies **where the surface belongs**, not how to capture it. The Engine chooses the platform-specific handoff without requiring the plugin to keep another copy of projection state.
17
+ Crossing that boundary takes more than a bridge call. It requires coordinating placement, appearance, input, rendering and restoration across two rendering systems on each platform. Rebuilding that coordination for every component makes otherwise useful designs expensive to deliver and maintain.
24
18
 
25
- | Intended destination | Required behavior |
26
- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
27
- | `native` | Show the interactive native view at confirmed geometry. During motion, use supported native tracking or a prepared image. |
28
- | `web` | Keep native content below the covering Web layer by removing its native projection; use a prepared image or the plugin's Web fallback. |
29
- | `none` | Retire native content, projected images and fallback content. Later update/capture callbacks must not show it again. |
19
+ WebView Native UI Engine makes that work reusable. It lets applications combine native UI with Web-driven screens and interactions, while sharing the machinery that moves presentation between the two layers. The goal is to expand what a WebView app can be built to do—and make that practical to ship.
30
20
 
31
- Overlay coverage can temporarily turn `native` into `web`; it never overrides `none`. A capture failure does not change the intended destination. `failedFallback: native` is only valid for consumers that maintain native placement themselves, and never overrides explicit `web`, `none`, or overlay coverage.
21
+ ## What this makes possible
32
22
 
33
- The Engine owns image readiness and restoration acknowledgement. It must not hide native pixels before replacement pixels are ready, nor retire replacement pixels before native restoration succeeds. Position-only motion reuses prepared content; appearance, size and renderer identity changes invalidate it. A missing image uses the declared fallback immediately rather than waiting to capture during a transition.
23
+ - **Native UI within Web-designed screens.** Combine native toolbars, tab bars, player controls and eligible SDK views with the application's DOM layout and navigation.
24
+ - **Web overlays that coexist with native content.** Present modals and other overlays over native surfaces, then return to the interactive native UI when they close.
25
+ - **Scrolling and transitions that retain native appearance.** Carry the native surface's captured appearance through Web-layer movement, without recreating its styling in CSS.
26
+ - **Shared infrastructure across components and platforms.** Reuse the same projection and restoration lifecycle on iOS and Android instead of implementing it independently in every integration.
34
27
 
35
- Registration and notification forwarding retain the latest intent across startup and reconnection; they do not replay business events. Destruction retires the registration and its cached images. Notifications are intent, not completion promises: an API that promises completed hiding must await the explicit retirement acknowledgement described below.
28
+ ## The approach
36
29
 
37
- An existing renderer that checks CSS visibility can call `getUnprojectedVisibility(element)` from the npm package. It reads current authored/inherited visibility without the Engine's hiding rules, and restores projection ownership before returning. Do not inspect or mutate Engine marker attributes in a consuming plugin. This helper performs a style read and can trigger attribute observers; it does not eliminate measurement or transport costs.
30
+ Use the live native view where it belongs, and a prepared visual projection in the DOM when the Web layer needs to own presentation. Restore the interactive native view when it can take over again. An existing DOM container gives the Web projection its place in the application's layout and layer structure.
38
31
 
39
- ## Register from the native plugin
32
+ The Engine coordinates this handoff; the integrating renderer still creates and positions its views. A projected image preserves appearance, not live interaction or video. Capture availability and layout support determine when a projection can be used, with an explicit fallback when it cannot. See [projection behavior](docs/projection.md) for the detailed contract and limits.
40
33
 
41
- Keep the plugin's normal `registerPlugin()` export and public methods unchanged. Create and retain a registration after creating the native renderer. The engine ships its Web runtime with the Swift/Android library, starts it in the current document and future documents, finds the existing fallback DOM, and subscribes to the plugin's notifications. Applications do not call a separate JS initializer.
34
+ ## Integration
42
35
 
43
- The integration has three responsibilities:
36
+ Applications gain these capabilities through plugins that integrate the Engine. Capacitor and Cordova plugin authors add it as a library dependency while retaining their existing native controls and public APIs. The same Engine supports iOS and Android. Ionic and Angular are not required; Ionic overlay coordination is optional.
44
37
 
45
- 1. The consumer provides an existing, laid-out Web fallback container in the correct layer. Pass this projection destination as the required `projectionSelector: string`; there is no `null` / automatic DOM creation mode.
46
- 2. The plugin registers its native views and notification names once on each platform.
47
- 3. The plugin emits notifications after its updates and releases the registration at teardown.
38
+ - [Capacitor plugin guide](docs/capacitor-plugin.md) — install dependencies and add registration, events and teardown to your existing plugin.
39
+ - [Cordova plugin guide](docs/cordova-plugin.md) — integrate the same Engine with Cordova's lifecycle and resource handler, without a Capacitor dependency.
48
40
 
49
- ### iOS: add the registration to your `CAPPlugin` class
41
+ Native registration starts the Web runtime; applications do not need a separate Engine initializer for the base integration. Keep your plugin's normal API and notifications.
50
42
 
51
- In `ios/Sources/ExamplePlugin/ExamplePlugin.swift`, retain the registration as a property and initialize it in `load()`, **after your existing renderer creates and attaches its view**.
43
+ The current API is pre-1.0. Use matching, pinned Engine versions across npm and native dependencies. Android placement requires API 23+, with composited snapshots available from API 26+.
52
44
 
53
- In this diff, `nativeView` is the `UIView` your plugin already owns; its existing rendering code is represented by comments.
45
+ ## Further reading
54
46
 
55
- Use the existing **`jsName`**, not `identifier`: Capacitor uses `jsName` for JS plugin calls and event subscriptions. Do not repeat the plugin name in the Engine configuration.
56
-
57
- ```diff
58
- import Capacitor
59
- import UIKit
60
- +import WebViewNativeUIEngine
61
-
62
- @objc(ExamplePlugin)
63
- public class ExamplePlugin: CAPPlugin, CAPBridgedPlugin {
64
- public let identifier = "ExamplePlugin"
65
- public let jsName = "Example"
66
- public let pluginMethods: [CAPPluginMethod] = [
67
- CAPPluginMethod(name: "setColors", returnType: CAPPluginReturnPromise)
68
- ]
69
- private var nativeView: UIView?
70
- + private var projection: NativeUIProjection?
71
-
72
- public override func load() {
73
- // Existing code creates and attaches nativeView here.
74
- + do {
75
- + projection = try NativeUIProjection(
76
- + webView: bridge?.webView,
77
- + plugin: jsName,
78
- + projectionSelector: "#native-ui-fallback",
79
- + receiveListeners: .init(
80
- + update: ["appearanceChanged"],
81
- + projection: .init(
82
- + native: ["shown"],
83
- + web: ["transitionStarted"],
84
- + none: ["hidden"]
85
- + )
86
- + ),
87
- + view: { [weak self] in
88
- + self?.nativeView
89
- + }
90
- + )
91
- + } catch {
92
- + CAPLog.print("Native UI projection unavailable: \(error)")
93
- + }
94
- }
95
-
96
- @objc func setColors(_ call: CAPPluginCall) {
97
- DispatchQueue.main.async {
98
- // Existing code applies the requested colors to nativeView.
99
- + self.notifyListeners("appearanceChanged", data: [:])
100
- call.resolve()
101
- }
102
- }
103
- +
104
- + public override func notifyListeners(_ eventName: String, data: [String: Any]?, retainUntilConsumed: Bool) {
105
- + projection?.receive(eventName, data: data)
106
- + super.notifyListeners(eventName, data: data, retainUntilConsumed: retainUntilConsumed)
107
- + }
108
- +
109
- + deinit {
110
- + projection?.destroy()
111
- + }
112
- }
113
- ```
114
-
115
- Pass `bridge?.webView` directly. If the WebView is unavailable, the Engine throws an initialization error into the same `catch`; a separate guard or force unwrap is unnecessary.
116
-
117
- If the view is created later in an existing `initialize` / `show` method, put the registration there after view creation instead of in `load()`. Retain one registration for that renderer's lifetime; do not create another on every update. When replacing a renderer, destroy its old registration before registering the replacement.
118
-
119
- ### Android: add the registration to your `Plugin` class
120
-
121
- Make the corresponding changes in `android/src/main/java/<your-package>/ExamplePlugin.kt`. **`pluginHandle.id`** is Capacitor's registered plugin name, derived from `@CapacitorPlugin`; no second name string is needed.
122
-
123
- ```diff
124
- import android.view.View
125
- import com.getcapacitor.Plugin
126
- import com.getcapacitor.PluginCall
127
- import com.getcapacitor.PluginMethod
128
- import com.getcapacitor.annotation.CapacitorPlugin
129
- +import android.util.Log
130
- +import com.getcapacitor.JSObject
131
- +import io.github.webview_native_ui.engine.NativeUIProjection
132
- +import io.github.webview_native_ui.engine.NativeUIProjection.ReceiveListeners
133
-
134
- @CapacitorPlugin(name = "Example")
135
- class ExamplePlugin : Plugin() {
136
- private var nativeView: View? = null
137
- + private var projection: NativeUIProjection? = null
138
-
139
- override fun load() {
140
- // Existing code creates and attaches nativeView here.
141
- + projection = runCatching {
142
- + NativeUIProjection.install(bridge.webView)
143
- + NativeUIProjection(
144
- + window = activity.window,
145
- + webView = bridge.webView,
146
- + plugin = pluginHandle.id,
147
- + projectionSelector = "#native-ui-fallback",
148
- + receiveListeners = ReceiveListeners(
149
- + update = listOf("appearanceChanged"),
150
- + projection = ReceiveListeners.Projection(
151
- + native = listOf("shown"),
152
- + web = listOf("transitionStarted"),
153
- + none = listOf("hidden")
154
- + )
155
- + ),
156
- + view = { nativeView }
157
- + )
158
- + }.onFailure {
159
- + Log.e(javaClass.simpleName, "Native UI projection unavailable", it)
160
- + }.getOrNull()
161
- }
162
-
163
- @PluginMethod
164
- fun setColors(call: PluginCall) {
165
- activity.runOnUiThread {
166
- // Existing code applies the requested colors to nativeView.
167
- + notifyListeners("appearanceChanged", JSObject())
168
- call.resolve()
169
- }
170
- }
171
- +
172
- + override fun notifyListeners(eventName: String, data: JSObject, retainUntilConsumed: Boolean) {
173
- + projection?.receive(eventName, data)
174
- + super.notifyListeners(eventName, data, retainUntilConsumed)
175
- + }
176
- +
177
- + override fun handleOnDestroy() {
178
- + projection?.close()
179
- + projection = null
180
- + super.handleOnDestroy()
181
- + }
182
- }
183
- ```
184
-
185
- On Android, call `NativeUIProjection.install(bridge.webView)` in plugin `load()`,
186
- before the first navigation, even when SDK views arrive later. Include installation
187
- in the same error boundary as registration; if it fails, keep the existing Web
188
- fallback and do not retry installation after navigation. Registrations then
189
- share that main-frame, origin-restricted channel; they do not install new message
190
- listeners into a loaded WebView. This avoids a WebView native crash reproduced when
191
- registering an asynchronously loaded AdMob view. The registration constructor can
192
- install the channel only on an un-navigated WebView; late installation fails with
193
- an actionable error instead of attempting unsafe injection. All registrations on
194
- the same WebView must use the same allowed origins. Closing a registration removes
195
- only its owner from the channel, so other slots continue working. The shared channel
196
- remains installed for the WebView lifetime; registrations must still be retained
197
- and closed by their consuming plugin.
198
-
199
- Both diffs add a private property, native registration, a notification override, an update notification and teardown. They add no Engine methods to `pluginMethods`, no public JS wrapper, and no application-side Engine initializer.
200
-
201
- Keep your plugin's existing `registerPlugin()` export. Use the same event names in the registration and `notifyListeners`; if the plugin already emits a suitable event (such as `tabSelected`), list that event instead of emitting it twice.
202
-
203
- ### Notifications during startup
204
-
205
- The notification override above lets the Engine retain the latest matching projection destination before the Web runtime subscribes. No separate state variable or initial-state callback is needed. Forward every notification to `super` as usual: the Engine records state without replaying notifications, updates, or user actions. Without a destination notification, it infers the initial destination from native view visibility. Registration must exist before the notifications it should observe.
206
-
207
- If your plugin owns multiple registrations, pass the notification to each registration; `receiveListeners.match` selects the owning surface. Appearance updates do not change the destination. The renderer still owns native placement and visibility before connection.
208
-
209
- With the default Web fallback, scrolling without a prepared image or native scroll binding keeps the Web fallback visible until the renderer confirms current native placement with its native-destination notification. An appearance update alone is not that confirmation. Consumers choosing native failure recovery must keep their own native geometry current.
210
-
211
- ### Which events to emit
212
-
213
- All incoming event declarations belong to `receiveListeners`. Its `update` list refreshes the image, its `projection` lists select the destination, and its optional `match` filters notifications for the owning surface.
214
-
215
- | Notification | Where to emit it in the existing plugin |
216
- | --------------------------------------- | ------------------------------------------------------------------------------------- |
217
- | `appearanceChanged` (an `update` event) | After a successful native appearance update, such as `setColors` |
218
- | `transitionStarted` (`web`) | Before the fallback DOM starts moving or a Web layer covers the native view |
219
- | `shown` (`native`) | After the plugin's existing show operation, or when returning from the Web projection |
220
- | `hidden` (`none`) | After the plugin's existing hide operation |
221
-
222
- The names in the examples are conventions you choose, not reserved Engine methods. An `update` event refreshes the prepared image without changing the requested destination. It does not call `setColors` again.
223
-
224
- Emit `transitionStarted` before the DOM starts moving, `shown` when native projection is requested again, and `hidden` after the plugin's own hide operation. Native capture, URL conversion, image preparation, draw acknowledgement, image retirement and listener cleanup stay inside the engine. The engine never invokes the plugin's `show`, `hide`, selection or ad-loading methods. End the registration with `projection.destroy()` on iOS or `projection.close()` on Android when the renderer is destroyed. Retain the registration for its full lifetime and call native APIs on the UI thread.
225
-
226
- `projectionSelector` is required and must resolve to **one existing container when projection starts**, including one retrieved by the plugin's own `querySelector`. The engine does not create a DOM or choose its size, placement, layer or z-index. Keep the container's normal layout and any interactive Web controls; an empty zero-size placeholder is not a usable fallback. It is a fixed scope, not a discovery root.
227
-
228
- The container does not have to exist at registration time. A missing DOM waits without showing native content; replacing it creates a new binding and retires old images. The engine preserves the container's children, listeners and values. Pass the native view itself through `view`; a group of controls can use their common native container. The engine manages the internal identifier, so no ID list or view dictionary is required. Independent surfaces use separate registrations.
229
-
230
- A Web Component host can be the destination, for example `projectionSelector: 'native-ad[placement="home"]'`. Give the host a layout box (`display: block` with the intended dimensions); the engine observes insertion, removal and selector attribute changes. The selector runs in the document and does not search inside shadow roots. The fallback content, including shadow content, must inherit the host's `visibility`: do not override it with `visibility: visible`, and reserve the host's `::before` for the projection image. These requirements apply to open and closed shadow roots.
231
-
232
- For independently moving AdMob slots, retain one registration per loaded native view/surface, use a slot-specific selector and filter the plugin's notifications through `receiveListeners.match` (`match: ["slotKey": slotKey, "loadId": loadId]` / `match = mapOf("slotKey" to slotKey, "loadId" to loadId)`). Dispose the registration when a loaded ad is replaced, even if its host view is reused. Keep SDK loading, impression and click handling in the ad plugin. Supply `canCapture` with its existing capture eligibility checks (for example, test-ad / opaque-background / no-video restrictions); the engine does not grant permission to snapshot an SDK view.
233
-
234
- For SDK consumers without an equivalent Web fallback, set `failedFallback: .native`
235
- on iOS or `failedFallback = NativeUIProjection.FailedFallback.NATIVE` on Android.
236
- The default is `.web` / `WEB`, preserving existing consumers. If automatic motion
237
- cannot use a prepared image (not ready, unsupported capture, or rejected handoff),
238
- `native` preserves the live native view and keeps fallback DOM hidden. The consumer
239
- must continue positioning that view; this option does not create native scroll
240
- tracking on Android. Explicit `web`, `none`, and overlay coverage take precedence:
241
- this option never raises a native view above an intentionally covering Web layer.
242
-
243
- For opaque, software-renderable Android SDK views, use
244
- `captureMode = NativeUIProjection.CaptureMode.VIEW` together with an explicit
245
- `failedFallback` policy (`WEB` unless the consumer maintains native placement). This draws only the View's
246
- pixels with no surrounding padding, so clipping a partially visible ad cannot bake
247
- in a toolbar or adjacent content. Video, SurfaceView, translucent views, and content
248
- requiring Window composition must keep the default `COMPOSITED` mode. The consumer
249
- must provide a matching, clipped DOM host for the full opaque View; arbitrary native
250
- outline masks, transforms, and custom clipping are not reproduced.
251
-
252
- VIEW pixels are cached by native View identity, dimensions, and content revision.
253
- Ordinary scrolling retains the decoded Web image and updates placement without
254
- capture, PNG encoding, or image decode. The image covers the host border box at
255
- local origin (0, 0), independently of native placement acknowledgment latency.
256
- Opaque images remain painted beneath the live native View between scrolls.
257
- Before hiding that View, the engine waits for WebView visual readiness and a draw;
258
- VIEW creates no native entry or return image copies. Return acknowledgment waits
259
- across the native draw and the following animation frame. A generation check prevents
260
- a delayed cover callback from hiding content after restoration or suspension.
261
-
262
- The native Bitmap is recycled after PNG encoding in VIEW mode; the PNG and decoded
263
- Web image remain cached. Plugin update notifications advance the content revision:
264
- notify an update whenever content changes without replacing the View. Replacing the
265
- registration, WebView navigation, reset, or close retires the cache. Encoding stays
266
- off the UI thread; View drawing and cache ownership stay on it, with the existing
267
- 8-million-pixel cap.
268
-
269
- COMPOSITED retains native handoff copies for translucent content. Entry copies wait
270
- for WebView visual readiness and a draw before retirement. If the 500 ms entry
271
- deadline expires, the native View is restored before the copy is removed, and a late
272
- paint acknowledgment is rejected.
273
-
274
- Android VIEW consumers can read `isProjecting` on the UI thread after a successful
275
- cover to defer same-size native placement work. Keep only the latest accepted placement;
276
- continue validating sequence/generation, and process hide, resize and teardown immediately.
277
- Supply `beforeNativeReveal` to apply that pending placement synchronously before normal
278
- restoration, failure recovery, or active-image release reveals native pixels. The engine
279
- clears `isProjecting` before invoking the callback, so applying placement must not defer
280
- again. The callback may run without pending work and must be idempotent. Clear consumer
281
- pending work on hide/destroy and validate the loaded View identity before applying it.
282
- The callback does not replace content/size invalidation or JavaScript geometry tracking.
283
-
284
- On Android, consumers whose asynchronous hide method promises that all pixels have
285
- disappeared must call `projection.hide { completed -> ... }` and resolve only when
286
- `completed` is true. Native handoff copies disappear immediately; completion waits
287
- for synchronous Web-proxy retirement. A timeout reports failure, not success.
288
- Call `projection.show()` when presenting again, in addition to normal plugin events;
289
- this also preserves presentation intent if event subscription is still starting.
290
-
291
- Scroll observation follows composed-tree ancestors, including assigned slots.
292
- Non-composed scroll events inside an Ionic shadow tree are observed at their source;
293
- applications do not add an extra listener or dispatch synthetic document events.
294
- Listeners follow host reparenting and slot reassignment and are removed on teardown.
295
-
296
- 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.
297
-
298
- 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.
299
-
300
- ## Ionic overlay support (optional)
301
-
302
- For an Ionic application, call this once during application startup. When calling it directly from the application, install `@webview-native-ui/engine` as a direct application dependency:
303
-
304
- ```ts
305
- import { enableSupportIonicOverlays } from '@webview-native-ui/engine/ionic';
306
-
307
- enableSupportIonicOverlays();
308
- ```
309
-
310
- Plugin maintainers can include this snippet in their application setup instructions, or call it inside an existing JS initializer. `@rdlabo/capacitor-native-ui` already calls the same initializer internally; its users need no additional call. Non-Ionic applications omit it. Native registration alone does not install Ionic event listeners.
311
-
312
- One monitor is shared per document, including between the npm module and the runtime injected by Swift/Android. Repeated initialization does not add listeners or erase explicit configuration. Destroying one projection removes only its subscription; it does not stop the monitor used by other plugins. The monitor lives with the document.
313
-
314
- 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.
315
-
316
- Native Popover ownership is supplied by the Native UI vertical-bars renderer. The shared monitor does not treat its source DOM as a covering Web overlay. If native presentation fails, the renderer releases ownership and normal DOM coverage resumes.
317
-
318
- Optional settings are document-wide:
319
-
320
- ```ts
321
- enableSupportIonicOverlays({
322
- exclude: '[data-nonblocking-overlay]',
323
- additional: [
324
- {
325
- selector: 'app-dialog',
326
- events: {
327
- willPresent: 'appDialogWillPresent',
328
- didDismiss: 'appDialogDidDismiss',
329
- },
330
- },
331
- ],
332
- });
333
- ```
334
-
335
- 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.
336
-
337
- ## Cordova
338
-
339
- The same Engine supports Cordova plugins without installing Capacitor. Select `runtime: .cordova` (Swift) or `runtime = Runtime.CORDOVA` (Kotlin) and connect the host lifecycle/resource handler. See the [Cordova plugin integration guide](docs/cordova.md). The projection implementation is shared; there is no separate Cordova package.
340
-
341
- ## Install inside a plugin
342
-
343
- ```sh
344
- npm install --save-exact @webview-native-ui/engine@0.0.1
345
- ```
346
-
347
- The npm archive contains TypeScript declarations, ESM, Swift sources and a podspec. Native dependencies must also be declared by the consuming plugin; an npm dependency alone links neither the Swift library nor the Android AAR.
348
-
349
- The primary native distribution is Swift Package Manager. Depend on the repository-root library from your plugin:
350
-
351
- ```swift
352
- .package(url: "https://github.com/webview-native-ui/engine.git", exact: "0.0.1")
353
- // In the plugin target's dependencies:
354
- .product(name: "WebViewNativeUIEngine", package: "engine")
355
- ```
356
-
357
- The new coordinates start at `0.0.1`. 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.
358
-
359
- SwiftPM resolves this repository's version tags such as `v0.0.1`. A standalone release tag must exist before consumers switch to this URL; old `ios27-v…` theme tags are not Engine versions. For unreleased integration tests, use a local checkout or an explicit tested commit.
360
-
361
- `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.
362
-
363
- ### Android: add the Maven dependency
364
-
365
- In the consuming plugin's `android/build.gradle`, add the Engine version matching its npm dependency:
366
-
367
- ```groovy
368
- dependencies {
369
- implementation 'io.github.webview-native-ui:engine:0.0.1'
370
- }
371
- ```
372
-
373
- Ensure `google()` and `mavenCentral()` are present in the consuming build's dependency repositories. For a build using centralized repositories, merge them into `android/settings.gradle`:
374
-
375
- ```groovy
376
- dependencyResolutionManagement {
377
- repositories {
378
- google()
379
- mavenCentral()
380
- }
381
- }
382
- ```
383
-
384
- If the build uses project-level repositories instead, add them to its existing `repositories` block. Installing the npm package does not add this Gradle dependency. Use a version whose POM and AAR are available from Central; `PUBLISHING` only confirms acceptance and does not yet guarantee downloads.
385
-
386
- ### CocoaPods compatibility through npm
387
-
388
- CocoaPods uses the Swift sources and podspec shipped in npm; no CocoaPods Trunk publication is required. The plugin maintainer declares the matching dependency in the plugin's podspec:
389
-
390
- ```ruby
391
- s.dependency 'WebViewNativeUIEngine', '0.0.1'
392
- ```
393
-
394
- The maintainer must also document this one-time application setup. Install the matching engine version as a direct npm dependency, then add the local pod **inside the application's target, outside `def capacitor_pods`**, which Capacitor regenerates:
395
-
396
- ```sh
397
- npm install --save-exact @webview-native-ui/engine@0.0.1
398
- ```
399
-
400
- ```ruby
401
- # ios/App/Podfile; adjust the path for a different native project layout.
402
- target 'App' do
403
- capacitor_pods
404
- pod 'WebViewNativeUIEngine', :path => '../../node_modules/@webview-native-ui/engine'
405
- end
406
- ```
407
-
408
- Run `npx cap sync ios` and rebuild. Keep the direct engine version aligned with the consuming plugin's dependency when upgrading. With a nonstandard node_modules layout, point `:path` at the installed engine package directory. An npm dependency alone does not register a Pod, and a podspec dependency cannot specify this local path. The engine intentionally has no Capacitor plugin registration or Podfile rewriting hook. SwiftPM consumers do not need this CocoaPods setup.
409
-
410
- ## Low-level Web projection
411
-
412
- A scope binds one existing Web fallback element for its lifetime. Obtain that element from your plugin's DOM; the engine does not create a replacement host or choose a page, stacking order or z-index. A different fallback element requires a new scope. Its Document is derived from `element.ownerDocument`.
413
-
414
- ```ts
415
- import { createWebProjectionScope, createWebProjection } from '@webview-native-ui/engine';
416
-
417
- const fallback = container.querySelector<HTMLElement>('[data-web-fallback]');
418
- if (!fallback) throw new Error('Web fallback is missing');
419
- const scope = createWebProjectionScope(fallback, 'my-plugin');
420
- const projection = await createWebProjection(
421
- scope,
422
- {
423
- send: (request) => plugin.snapshot(request),
424
- listen: (listener) => plugin.addListener('snapshotInteraction', listener),
425
- },
426
- {
427
- wake: () => schedulePluginUpdate(),
428
- blocked: () => overlayIsActive(),
429
- revision: () => lastSentRevision,
430
- },
431
- );
432
- ```
433
-
434
- The container, bridge and update callbacks above belong to the consuming plugin. No `document`, `target`, `layer`, CSS selector or layer resolver is passed to the projection manager.
435
-
436
- 1. After an accepted native update, call `projection.consider(revision, sources)`. Sources associate the native IDs and viewport frames with their source DOM; they need not be inside the fallback element. Images are captured and decoded in advance.
437
- 2. Call `projection.changed()` for appearance changes, or `projection.changed(scrollElement)` to invalidate scroll geometry. Layout updates go through `consider`; use `invalidate()` for other geometry changes.
438
- 3. Before covering Native with a Web overlay, call `projection.begin()`. It displays already prepared pixels at the fixed fallback. No new capture or decode starts here. If capture is unavailable, the plugin keeps its ordinary Web fallback.
439
- 4. Exclude `projection.owners` from normal Native updates while the image owns those sources. `visibleOwners` keeps their return pixels while Native is being restored; `preparedOwners` identifies sources with prepared images. These arrays are read-only snapshots.
440
- 5. Call `projection.end()`, send the normal native update, then call `projection.updated(snapshotToken, acceptedIDs, revision)`. A newer acknowledgement containing all captured IDs retires the image.
441
- 6. Call `projection.destroy()` when disposing the surface. It releases only its own images and listener; the plugin owns final Native reset and its DOM.
442
-
443
- The engine uses a noninteractive, empty CSS `::before` pseudo-element for the prepared image. Supply a container that supports it, such as an existing `div`, `button` or custom element. Replaced/void elements such as `img`, `input` and `video` are rejected when creating the scope; use their existing fallback container. It does not clone, replace, remove or set `inert`/`aria-hidden` on the existing fallback or its children. Removing its temporary image styles restores the same live DOM, including application changes made during projection. Event listeners and form values are preserved. The renderer may move that same element between parents; it must not replace the scope's element or let two managers control it simultaneously.
444
-
445
- ### Notifications from the existing plugin
446
-
447
- The low-level manager can subscribe to the existing plugin's events through `options.events`:
448
-
449
- ```ts
450
- events: {
451
- addListener: (name, listener) => plugin.addListener(name, listener),
452
- update: ['setColors', 'setTabs', 'tabSelected'],
453
- projection: {
454
- native: ['shown'],
455
- web: ['transitionStarted'],
456
- none: ['hidden'],
457
- },
458
- }
459
- ```
460
-
461
- These are **notifications**, not intercepted method names. The plugin emits update events after its native changes succeed. A Web projection event must arrive before movement starts; it uses the previously prepared image. A native event requests restoration, but the image remains until the normal renderer acknowledgement confirms that Native is ready. A hidden event follows the plugin's native hide operation and retires the image; it does not invoke `hide()` or change application-owned fallback DOM. Appearance updates preserve the requested destination, including `none`.
462
-
463
- For plugins with several surfaces, provide `accepts(event)` to select this scope's stable surface identity. Events shared by `update` and a destination are subscribed once and processed together. Conflicting destinations for the same event are rejected before subscription. Destroying the manager removes all its subscriptions, including registrations that resolve late after initialization fails. No public plugin methods are wrapped or replayed.
464
-
465
- The manager converts private native file URLs with Capacitor internally. Plugin authors do not supply a URL resolver.
466
-
467
- This section describes the lower-level manager for renderers that already own batch reconciliation, such as the Ionic adapter. Prefer the native registration above for a new integration; it owns startup, measurements and draw acknowledgements.
468
-
469
- ### Multiple fallbacks in one Native transaction
470
-
471
- Use `createWebProjectionBatch(transport, options)` when several fallback elements must switch together. It uses the same image engine, one listener and one capture/cover/restore token. Pass `{ scope, element, control, ... }` for each entry to `batch.consider(revision, sources)`. Several native IDs may share one scope, such as a button group.
472
-
473
- The batch derives its Document from the first nonempty `consider`; all subsequent scopes and sources must belong to that Document. Before this first update, `begin()` does nothing and `destroy()` is safe. A changed fallback requires a new scope. Scope identity participates in cache invalidation; an active presentation retains its captured scopes until restoration finishes.
474
-
475
- `WebProjectionRequest` is an action-discriminated union. `prepare` requires `revision`, `viewportWidth` and `ids`; `cover` requires `token`; `restore` requires `token`, `viewportWidth` and destination `parts`; `painted`, `restored` and `release` require `token`; `reset` takes no payload. Empty preparation results and `{ shown: false }` mean the plugin should use its normal Web fallback. Use the plugin's existing ordered bridge; the engine does not retry user actions.
476
-
477
- ### Native handoff on both platforms
478
-
479
- Call all view and snapshot lifecycle APIs on the main/UI thread. Native input frames use **points relative to the WebView on iOS** and **physical pixels relative to the reference view on Android**. `viewportWidth` is the Web viewport width in CSS pixels. Prepared parts sent to JavaScript are converted to CSS pixels.
480
-
481
- On both platforms, `cover` installs the prepared entry image **before hiding the supplied live views**, within the same UI turn. Its boolean result reports whether this was possible; failure leaves visibility unchanged. Supply the actual views for every captured component. Do not separately hide views or create another entry copy.
482
-
483
- ```swift
484
- let shown = snapshots.cover(token, in: webView, views: renderedViews, transient: movingSurface)
485
- ```
486
-
487
- ```kotlin
488
- val shown = snapshots.cover(token, activity.window, webView, renderedViews, transient = movingSurface)
489
- ```
490
-
491
- `transient: true` is for a moving foreground surface: it does not replace the background's `coveringToken`. Retire the entry copy with `retireCopy(token)` after Web image readiness. Before applying a normal native update, undo the engine's visibility changes with iOS `restoreControls()` / Android `restoreViews()`. For return, `restore` installs the cached return image, the plugin renders underneath it, and `retireCopy(token, entry: false)` retires it after the corresponding acknowledgement/readiness. These platform-native names remain idiomatic; the lifecycle is the same. `cover` is an entry operation, not a method to repeat on each normal update.
492
-
493
- Token cleanup uses `release(token)`; owner teardown uses `reset()` (and Android `close()` when disposing its encoder). A surface manager must not reset another surface's captures. Web fallback remains the renderer's responsibility when a capture or native placement is unavailable.
494
-
495
- The optional `eligible(source)` and `current(sources)` callbacks keep framework-specific surface selection outside the engine. `transient: true` supports a moving surface whose return image must be rebased. Arbitrary CSS transforms are not supported by native scroll tracking; such movement can use this image path.
496
-
497
- The image transport accepts `WebProjectionRequest` and returns prepared token/parts or `{ shown: false }`. Parts use local `file:` URLs and CSS-pixel viewport coordinates. Requests use the plugin's existing ordered bridge:
498
-
499
- | Action | Native responsibility |
500
- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
501
- | `prepare` | Validate the current revision and IDs, then `NativeUICoveredSnapshots.prepare` using actual rendered frames. |
502
- | `cover` | Validate the token and complete native surface ownership; create the entry copy and hide the associated native views. Return `shown: false` on failure. |
503
- | `painted` | Retire only that token's entry copy. |
504
- | `restore` | Rebase the prepared return copy to supplied frames. Never capture here. |
505
- | `restored` | Retire only that token's return copy after the native update ACK and Web paint boundary. |
506
- | `release` | Release the named token, files and associated temporary views. |
507
- | `reset` | Owner teardown only; release all native capture resources. |
508
-
509
- Use `NativeUICoveredSnapshots.observe` to notify the JS listener when interaction starts/ends. Captures use composited pixels so blur/refraction is preserved; they include the background under the clipped native component. Cache validity therefore includes layout and the plugin's visual-change notifications. The engine does not capture continuously or guarantee every possible renderer can be captured.
510
-
511
- ### Temporary storage and application permissions
512
-
513
- Prepared images stay in the app's private temporary storage. iOS uses `FileManager.default.temporaryDirectory`. On Android, construct `NativeUISnapshots` with a directory inside `context.cacheDir`; do not use shared or external storage. Neither platform needs a photo-library or storage permission prompt for this path. No Capacitor Filesystem plugin is required. See [Apple's sandbox directories](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html) and [Android's app-specific storage](https://developer.android.com/training/data-storage/app-specific).
514
-
515
- `Capacitor.convertFileSrc` maps the returned file URL to the existing WebView local server. It grants no filesystem permission and does not upload the image. If the application sets a Content Security Policy, its `img-src` must allow the local app origin used by that URL; do not enable broad `file:` access or unrelated WebView file-access settings.
516
-
517
- `release(token)` and owner teardown remove owned snapshot files. Abrupt process termination can leave temporary images until the operating system clears the cache or the app is uninstalled; immediate deletion after a crash is not guaranteed. Keep this storage private and treat it as disposable, not persistent application data.
518
-
519
- ## Native scroll tracking
520
-
521
- `readScrollPlacement(element, owner, identify)` measures one untransformed overflow owner. `createScrollBindings()` validates reuse and detects movement while acquisition is awaiting ACK. Keep `reuse` out of layout-deduplication signatures; it describes acknowledgement state, not layout.
522
-
523
- On iOS, `NativeUIScrollBindings.replace` groups sources by owner and binds only an unambiguous matching `UIScrollView`. `update()` applies positions and clipping without rebuilding content. `NativeUIClippedView` preserves full content dimensions while restricting visible pixels and hit testing.
524
-
525
- An established binding rejected by native loses ownership and may be freshly acquired through the **same** update/ACK path. A failed fresh acquisition stays on the fallback until a relevant layout/scroll-end event. Never add an immediate retry loop or replay user actions. A frame/props change does not recalibrate an established coordinate origin.
526
-
527
- This uses WKWebView's internal overflow hierarchy without private class names. Missing/ambiguous views, nested scrolling, document scrolling, unsupported transforms, shaped clipping, or a moving unconfirmed owner cannot be bound. Do not claim support solely because a `UIScrollView` exists. A native view may intercept gestures that begin inside it; routing those gestures and SDK accessibility behavior remain renderer-specific.
528
-
529
- ## Development
530
-
531
- Use Node.js 24, matching CI. From this repository:
532
-
533
- ```sh
534
- npm ci
535
- npm run build
536
- npm run lint
537
- npx playwright install chromium
538
- npm test
539
- xcodebuild -scheme WebViewNativeUIEngine -destination 'platform=iOS Simulator,id=<booted simulator UUID>' test
540
- ```
541
-
542
- Lint combines strict TypeScript checking, the standard ESLint/TypeScript recommended rules, and typed Promise checks for `src`. Scripts and tests receive syntax-level linting; Prettier owns formatting. `npm run format` applies formatting only. Bridge payloads and test doubles may use explicit `any`; production boundary validation remains necessary. An explicit `void` marks background work, but does not handle a rejected Promise. Android uses `lintRelease` with warnings treated as errors in CI; Swift is compiled and tested on an iOS simulator.
543
-
544
- The engine is versioned independently of its consumers. Publish order is the native Maven artifact → npm engine; consumers upgrade separately. npm releases verify that the matching POM and AAR exist on Maven Central before publication. No publication is performed by build or test commands. Tests cover plain DOM, fractional/RTL geometry, stale ACKs, image ownership and native WKWebView scroll tracking; consumer tests remain responsible for Ionic/SDK integration.
545
-
546
- ## Android library
547
-
548
- Android uses a Gradle Android library, distributed as an AAR through Maven rather than Capacitor plugin registration. The publication coordinates are `io.github.webview-native-ui:engine:<version>`. Build and verify the local Maven publication with:
549
-
550
- ```sh
551
- android/gradlew -p android testDebugUnitTest assembleRelease connectedDebugAndroidTest publishReleasePublicationToVerificationRepository
552
- ```
553
-
554
- `NativeUIClippedView` is a low-level helper that applies caller-supplied position and clipping without resetting unchanged dimensions. `NativeUIProjection` manages image handoff and restoration; it does not use this helper to lay out the renderer. The consuming plugin owns normal native placement and size updates. Android does not claim WKWebView-style native overflow binding. High-level registrations share a non-consuming window touch observer: down cancels in-flight capture adoption, up/cancel/focus loss permits preparation again, and teardown removes only that registration. Existing callback dispatch and return values are preserved.
555
-
556
- `NativeUISnapshots` prepares composited PixelCopy images and local PNG files, then covers/restores from that cache. Encoding runs off the UI thread; only one preparation runs per owner. Notify it of visual changes and interaction, and release tokens on teardown. All view/lifecycle calls run on the UI thread. The plugin retains revision, eligibility and fallback policy.
557
-
558
- After prepared Web pixels are shown or retired, `NativeUIWebReadiness.prepare` checks readiness for a subsequent WebView draw. Its `current` predicate must validate the owning session, source generation and image revision. The temporary native copy covers this asynchronous handoff; overlay startup itself does not wait. Neither bridge acknowledgement nor this callback proves physical display presentation.
559
-
560
- [AdMob PR #487](https://github.com/capacitor-community/admob/pull/487) demonstrates a persistent image underlay for opaque test ads. That policy is not enabled globally: translucent UI would be composited twice, and video/SDK interactions require renderer-specific decisions. Never synthesize clicks through an image. The Native UI consumer uses the engine on both iOS and Android. Android acceptance tests exercise the real Capacitor bridge and translucent Compose content. Other SDK consumers still require their own integration and acceptance testing.
561
-
562
- ### Translucent Android surfaces
563
-
564
- Capture the composited window pixels, including the background, instead of drawing the translucent view onto an empty bitmap. Keep the prepared Web image decoded but hidden. At handoff, `cover(token, window, reference, views)` installs a temporary opaque image copy before hiding those native views in the same UI turn. Reveal the Web image, confirm Web drawing readiness, then retire the entry copy. The overlay starts immediately; image encoding is never on this path.
565
-
566
- For return, install the prepared return copy with `restore`, restore the native views underneath it, hide the Web image, and retire the return copy after Web drawing readiness. This avoids compositing live translucent native pixels over their own snapshot. Validate the token and current surface generation at every asynchronous callback. The temporary copy is noninteractive and bounded; teardown releases both copies and cached resources.
567
-
568
- This preserves the captured background, not a continuously changing scene behind the image. Background changes must invalidate the cache through the same visual-change notifications as content updates. Video and unsupported capture surfaces retain the consumer's fallback policy.
569
-
570
- Consumer integration tests belong to their respective repositories. Engine CI exercises its browser, Swift and Android contracts without requiring a sibling renderer checkout.
571
-
572
- ### Final placement when returning from an opaque image (Android)
573
-
574
- A plugin that normally measures and posts native placement on every scroll can opt
575
- into a final placement handoff. Pass `updatePlacement: (JSONObject) -> Unit` to its
576
- Android `NativeUIProjection` registration and bind its Web host with
577
- `bindProjectionPlacement(element, measure, invalidate)`.
578
-
579
- - `binding.projected` becomes true only after native confirms the image handoff.
580
- While true, the plugin can reuse that host's last placement in full placement
581
- batches and skip measuring it. Other hosts continue their ordinary updates.
582
- - `measure()` returns a JSON object synchronously. The engine includes it in the
583
- existing restore request (or an active-image recovery request). Native validates
584
- the active image token and View identity, calls `updatePlacement`, then reveals
585
- native and acknowledges its draw. No extra bridge round trip is introduced.
586
- - The plugin must validate its own identity, generation and per-host sequence in
587
- `updatePlacement`. Throw to reject a stale or invalid placement. A final update
588
- for one host must not cause updates for another host to be dropped.
589
- - `invalidate()` is called after a failed handoff request. Clear optimistic
590
- placement/deduplication caches and schedule ordinary placement reconciliation.
591
- - Dispose the binding when detaching a host. Session ownership prevents an old
592
- registration's delayed acknowledgement from clearing a new registration's
593
- projection state. Pause, author visibility and layout changes must still use
594
- the plugin's normal visibility/invalidation paths.
595
-
596
- This opt-in does not change registrations without `updatePlacement`. Android rejects
597
- `updatePlacement` unless `captureMode = CaptureMode.VIEW`; live video and other ineligible captures
598
- continue native placement tracking under `failedFallback = NATIVE`.
599
-
600
- ## Maven publication
601
-
602
- The `Native UI Engine Maven Release` workflow is manually dispatched on `main` with the exact engine package version. That commit must first pass `Native UI Engine` CI. It signs an AAR, source archive, generated Kotlin API documentation and Maven metadata, uploads once, and completes when Central reports `PUBLISHING` or `PUBLISHED`. The deployment ID and state changes are recorded in the run summary. `PUBLISHING` means validation passed and Central accepted publication; it does not guarantee that artifacts can already be downloaded. This workflow does not wait for distribution. The npm release still requires the matching POM and AAR to be available on Maven Central. Existing coordinates are never overwritten; if an upload times out, inspect the recorded deployment in Central before taking further action.
603
-
604
- Configure the repository's Actions secrets with `CENTRAL_USERNAME`, `CENTRAL_PASSWORD` (a Central user token), `MAVEN_SIGNING_KEY` (ASCII-armored private key) and `MAVEN_SIGNING_PASSWORD`. The signing public key must be available to Central. These are the same credential types used by cf-workers-native; GitHub secrets are scoped to their repository and must be provisioned here separately.
605
-
606
- For local packaging verification, run `python3 scripts/native-ui-engine-release/verify.py` with JDK 21, Android SDK 36 and GnuPG. It creates a disposable key, validates signatures and checksums, and deletes its test ZIP without uploading. Publication always rebuilds from clean outputs with the real signing key. Publish its Maven artifact before the corresponding npm release, including prereleases.
607
-
608
- ## Maintainer release setup
609
-
610
- This repository starts from the merged Engine implementation at [ionic-theme-ios27 `b218d8b`](https://github.com/rdlabo-dev/ionic-theme-ios27/commit/b218d8bfe0c87c02addc668fee9c7aa333489486). Extraction preserves the merged implementation; Cordova host support is developed in this repository. The renamed package starts at version `0.0.1`. Publish the same version for npm, SwiftPM and Maven; do not reuse the old package coordinates.
611
-
612
- Before the first release, create the npm organization `webview-native-ui` and verify the Maven Central namespace `io.github.webview-native-ui`. Configure npm trusted publishing for `webview-native-ui/engine`, workflow `release.yml`, and provision the Maven secrets above in this repository. Credentials are not copied by moving source files.
613
-
614
- 1. Prepare the next version and release notes on `main`; use `npm run release` to create a standard `vX.Y.Z` tag/GitHub release. Keep the GitHub release as a draft until Maven is downloadable.
615
- 2. After that main commit passes CI, dispatch `native-ui-engine-release.yml` with the exact package version.
616
- 3. Central `PUBLISHING` is an accepted upload, not download readiness. Once the POM and AAR are available, publish the prepared GitHub release. `release.yml` verifies the tag, package version, CI and Maven artifacts before npm publication (`next` for prereleases, `latest` otherwise).
617
-
618
- No build or test command publishes a package. Existing npm/Maven versions are never overwritten.
47
+ - [Projection behavior and advanced integration](docs/projection.md) — DOM requirements, lifecycle, fallback policies, optional Ionic support and lower-level APIs.
48
+ - [Development and releases](docs/maintenance.md) — repository structure, validation and publishing.
619
49
 
620
50
  ## License
621
51
 
622
- This engine uses the [Zlib license](LICENSE): commercial use, modification and redistribution are permitted. Do not misrepresent the origin or claim authorship of the original software; identify altered source versions and retain the notice in source distributions. Forking, renaming or removing Git history is not itself prohibited by this license.
623
-
624
- [NOTICE](NOTICE) preserves the upstream MIT notice for code adapted from ionic-theme-ios27 / capacitor-native-ui. Previously granted MIT permissions remain applicable to those versions and portions. The theme and Native UI plugin remain MIT; the engine's Zlib license does not change their licenses or the licenses of applications that depend on it. The npm archive and Android artifacts include the engine notices; SwiftPM users obtain them in this repository alongside the engine sources.
52
+ [MIT](LICENSE). Commercial use, modification and redistribution are permitted with the license's notice and attribution requirements. The Engine's license does not change the licenses of applications or plugins that depend on it.