@webview-native-ui/engine 0.0.0-stage → 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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -2
  3. package/WebViewNativeUIEngine.podspec +17 -0
  4. package/dist/dom/geometry.d.ts +2 -0
  5. package/dist/dom/geometry.js +25 -0
  6. package/dist/dom/overlays.d.ts +42 -0
  7. package/dist/dom/overlays.js +181 -0
  8. package/dist/dom/scroll-bindings.d.ts +11 -0
  9. package/dist/dom/scroll-bindings.js +55 -0
  10. package/dist/dom/scroll-placement.d.ts +5 -0
  11. package/dist/dom/scroll-placement.js +85 -0
  12. package/dist/dom/visibility.d.ts +6 -0
  13. package/dist/dom/visibility.js +29 -0
  14. package/dist/index.d.ts +10 -0
  15. package/dist/index.js +9 -0
  16. package/dist/ionic.d.ts +16 -0
  17. package/dist/ionic.js +31 -0
  18. package/dist/native/bootstrap.d.ts +8 -0
  19. package/dist/native/bootstrap.js +138 -0
  20. package/dist/native/bridge.d.ts +4 -0
  21. package/dist/native/bridge.js +15 -0
  22. package/dist/native/resource-url.d.ts +2 -0
  23. package/dist/native/resource-url.js +11 -0
  24. package/dist/native/session.d.ts +49 -0
  25. package/dist/native/session.js +520 -0
  26. package/dist/projection/events.d.ts +12 -0
  27. package/dist/projection/events.js +44 -0
  28. package/dist/projection/placement.d.ts +14 -0
  29. package/dist/projection/placement.js +18 -0
  30. package/dist/projection/scope.d.ts +6 -0
  31. package/dist/projection/scope.js +8 -0
  32. package/dist/projection/styles.d.ts +2 -0
  33. package/dist/projection/styles.js +14 -0
  34. package/dist/projection/web.d.ts +52 -0
  35. package/dist/projection/web.js +838 -0
  36. package/dist/types.d.ts +88 -0
  37. package/dist/types.js +1 -0
  38. package/docs/assets/hero.png +0 -0
  39. package/docs/capacitor-plugin.md +279 -0
  40. package/docs/cordova-plugin.md +198 -0
  41. package/docs/cordova.md +3 -0
  42. package/docs/maintenance.md +52 -0
  43. package/docs/projection.md +340 -0
  44. package/ios/Sources/WebViewNativeUIEngine/ClippedView.swift +16 -0
  45. package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +256 -0
  46. package/ios/Sources/WebViewNativeUIEngine/Frame.swift +15 -0
  47. package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +314 -0
  48. package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +14 -0
  49. package/ios/Sources/WebViewNativeUIEngine/ScrollBindings.swift +27 -0
  50. package/ios/Sources/WebViewNativeUIEngine/ScrollTracker.swift +117 -0
  51. package/package.json +63 -4
@@ -0,0 +1,138 @@
1
+ import { startNativeSession } from './session.js';
2
+ const sessions = new Map();
3
+ const pending = new Map();
4
+ const interactions = new Map();
5
+ const documentID = Array.from(crypto.getRandomValues(new Uint32Array(4)), (part) => part.toString(16)).join('-');
6
+ const notifications = new Map();
7
+ /** Cordova plugins forward their existing notifications through NativeUIProjection.receive. */
8
+ export const notify = (id, name, data) => notifications.get(id)?.get(name)?.(data);
9
+ let sequence = 0;
10
+ const send = (id, request) => new Promise((resolve, reject) => {
11
+ const key = `${id}:${documentID}:${++sequence}`;
12
+ const timer = setTimeout(() => {
13
+ pending.delete(key);
14
+ reject(new Error('Native UI session timed out'));
15
+ }, 5000);
16
+ pending.set(key, { resolve, reject, timer });
17
+ const message = JSON.stringify({ key, request });
18
+ const host = globalThis;
19
+ const channel = host.webkit?.messageHandlers?.[id] ?? host[id] ?? host.nativeUIEngine;
20
+ if (!channel) {
21
+ clearTimeout(timer);
22
+ pending.delete(key);
23
+ reject(new Error('Native UI session unavailable'));
24
+ return;
25
+ }
26
+ try {
27
+ channel.postMessage(message);
28
+ }
29
+ catch (error) {
30
+ clearTimeout(timer);
31
+ pending.delete(key);
32
+ reject(error);
33
+ }
34
+ });
35
+ export const receive = (id, key, value, error) => {
36
+ if (key === null) {
37
+ interactions.get(id)?.(value);
38
+ return;
39
+ }
40
+ const item = pending.get(key);
41
+ if (!item || !key.startsWith(`${id}:`))
42
+ return;
43
+ pending.delete(key);
44
+ clearTimeout(item.timer);
45
+ if (error)
46
+ item.reject(new Error(error));
47
+ else
48
+ item.resolve(value);
49
+ };
50
+ export const show = (id) => {
51
+ sessions.get(id)?.show();
52
+ };
53
+ export const hide = (id) => {
54
+ sessions.get(id)?.hide();
55
+ return true;
56
+ };
57
+ export const stop = async (id) => {
58
+ const session = sessions.get(id);
59
+ sessions.delete(id);
60
+ notifications.delete(id);
61
+ interactions.delete(id);
62
+ for (const [key, item] of pending) {
63
+ if (!key.startsWith(`${id}:`))
64
+ continue;
65
+ pending.delete(key);
66
+ clearTimeout(item.timer);
67
+ item.reject(new Error('Native UI session closed'));
68
+ }
69
+ await session?.destroy();
70
+ };
71
+ export const start = async (config) => {
72
+ if (sessions.has(config.id))
73
+ return;
74
+ // Reserve before asynchronous initialization, including duplicate Native init callbacks.
75
+ let actual;
76
+ let cancelled = false;
77
+ let pendingDestination;
78
+ const reservation = {
79
+ show: () => {
80
+ pendingDestination = 'native';
81
+ actual?.show();
82
+ },
83
+ hide: () => {
84
+ pendingDestination = 'none';
85
+ actual?.hide();
86
+ },
87
+ destroy: async () => {
88
+ cancelled = true;
89
+ await actual?.destroy();
90
+ },
91
+ };
92
+ sessions.set(config.id, reservation);
93
+ try {
94
+ if (document.readyState === 'loading')
95
+ await new Promise((resolve) => document.addEventListener('DOMContentLoaded', () => resolve(), { once: true }));
96
+ if (cancelled)
97
+ return;
98
+ const listeners = new Map();
99
+ if (config.runtime === 'cordova')
100
+ notifications.set(config.id, listeners);
101
+ actual = await startNativeSession(config, {
102
+ send: (request) => send(config.id, request),
103
+ listen: async (listener) => {
104
+ interactions.set(config.id, listener);
105
+ return {
106
+ remove: () => {
107
+ if (interactions.get(config.id) === listener)
108
+ interactions.delete(config.id);
109
+ },
110
+ };
111
+ },
112
+ }, config.runtime === 'cordova'
113
+ ? {
114
+ addListener: async (name, listener) => {
115
+ listeners.set(name, listener);
116
+ return {
117
+ remove: () => {
118
+ listeners.delete(name);
119
+ },
120
+ };
121
+ },
122
+ }
123
+ : undefined);
124
+ if (pendingDestination === 'none')
125
+ actual.hide();
126
+ else if (pendingDestination === 'native')
127
+ actual.show();
128
+ if (cancelled)
129
+ await actual.destroy();
130
+ }
131
+ catch (error) {
132
+ if (sessions.get(config.id) === reservation) {
133
+ sessions.delete(config.id);
134
+ notifications.delete(config.id);
135
+ }
136
+ throw error;
137
+ }
138
+ };
@@ -0,0 +1,4 @@
1
+ import type { ListenerHandle as PluginListenerHandle } from '../types.js';
2
+ export declare const bounded: <T>(promise: Promise<T>) => Promise<T>;
3
+ /** A timed-out registration still owns a native resource if it eventually succeeds. */
4
+ export declare const boundedListener: (registration: Promise<PluginListenerHandle>) => Promise<PluginListenerHandle>;
@@ -0,0 +1,15 @@
1
+ // A failed bridge must not leave the source inaccessible indefinitely.
2
+ export const bounded = (promise) => new Promise((resolve, reject) => {
3
+ const timer = setTimeout(() => reject(new Error('Native UI bridge timed out')), 5000);
4
+ promise.then(resolve, reject).finally(() => clearTimeout(timer));
5
+ });
6
+ /** A timed-out registration still owns a native resource if it eventually succeeds. */
7
+ export const boundedListener = async (registration) => {
8
+ try {
9
+ return await bounded(registration);
10
+ }
11
+ catch (error) {
12
+ void registration.then((listener) => listener.remove()).catch(() => { });
13
+ throw error;
14
+ }
15
+ };
@@ -0,0 +1,2 @@
1
+ /** Use the host's existing file server; never initialize a framework bridge. */
2
+ export declare const projectionURL: (url: string) => string;
@@ -0,0 +1,11 @@
1
+ /** Use the host's existing file server; never initialize a framework bridge. */
2
+ export const projectionURL = (url) => {
3
+ const host = globalThis;
4
+ if (host.Capacitor?.convertFileSrc)
5
+ return host.Capacitor.convertFileSrc(url);
6
+ if (!url.startsWith('file:'))
7
+ return url;
8
+ if (host.WkWebView?.convertFilePath)
9
+ return host.WkWebView.convertFilePath(url);
10
+ throw new Error('Native UI file URL requires a host resource handler');
11
+ };
@@ -0,0 +1,49 @@
1
+ import { type ProjectionDestination } from '../projection/events.js';
2
+ import type { ScrollPlacement, ListenerHandle, WebProjectionRequest } from '../types.js';
3
+ /** Serialized by the native library. One registration owns one fixed fallback container. */
4
+ export interface NativeSessionConfiguration {
5
+ id: string;
6
+ plugin: string;
7
+ runtime?: 'capacitor' | 'cordova';
8
+ projectionSelector: string;
9
+ captureDelayMs?: number;
10
+ /** Recovery while native is requested. Explicit web/none destinations take precedence. */
11
+ failedFallback?: 'web' | 'native';
12
+ captureMode?: 'composited' | 'view';
13
+ /** Native registration accepts a final placement in its reveal request. */
14
+ placementHandoff?: boolean;
15
+ receiveListeners?: {
16
+ update?: string[];
17
+ projection?: Partial<Record<ProjectionDestination, string[]>>;
18
+ match?: Record<string, string | number | boolean>;
19
+ };
20
+ }
21
+ export interface NativeSessionBridge {
22
+ send(request: WebProjectionRequest | {
23
+ action: 'resume';
24
+ token?: string;
25
+ } | {
26
+ action: 'visibility';
27
+ visible: boolean;
28
+ } | {
29
+ action: 'track';
30
+ viewportWidth: number;
31
+ scroll?: ScrollPlacement;
32
+ } | {
33
+ action: 'connect';
34
+ } | {
35
+ action: 'destination';
36
+ destination: ProjectionDestination;
37
+ }): Promise<any>;
38
+ listen(listener: (event: {
39
+ active: boolean;
40
+ }) => void): Promise<ListenerHandle>;
41
+ }
42
+ /** Native startup calls this; application code does not initialize or wrap the plugin. */
43
+ export declare const startNativeSession: (config: NativeSessionConfiguration, nativeBridge: NativeSessionBridge, plugin?: {
44
+ addListener(name: string, listener: (event: unknown) => void): Promise<ListenerHandle>;
45
+ }) => Promise<{
46
+ destroy: () => Promise<void>;
47
+ show: () => void;
48
+ hide: () => void;
49
+ }>;