@livx.cc/appwrap 0.61.25 → 0.61.26

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.61.25",
3
+ "version": "0.61.26",
4
4
  "description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
@@ -35,6 +35,11 @@ const KVO_KEYS = ['URL', 'title', 'canGoBack', 'canGoForward', 'loading'];
35
35
  * SF Symbol (iOS, default 'sparkles'), `text` its glyph on Android ('✦'),
36
36
  * `label` = accessibility, `radius` (default round), symbol/text '' = a
37
37
  * plain shape. show:false hides it. Taps emit `webview.fab`.
38
+ * webview.float {on, hit?: [{x,y,w,h}]} — the app's WebView draws OVER the overlay (transparent wherever its own page
39
+ * is), e.g. chat bubbles floating over a shown page without resizing it.
40
+ * Touches inside `hit` (app CSS px) go to the app; everywhere else to the
41
+ * overlay. Call again to move the rects; on:false restores. iOS; Android
42
+ * resolves {floating:false} (not supported — keep a fallback).
38
43
  * Events: `webview.state` {url,title,canGoBack,canGoForward,loading} on every navigation change; `webview.fab` on a tap.
39
44
  *
40
45
  * Swipe back/forward on, target=_blank/window.open loads in place (shared ios-ui-delegate), keyboard +
@@ -66,6 +71,9 @@ const fabOpts = (p?: Fab | null) => ({
66
71
  symbol: p?.symbol === '' ? '' : String(p?.symbol || 'sparkles'), text: p?.text === '' ? '' : String(p?.text || '✦'),
67
72
  radius: p?.radius === undefined ? null : Math.max(0, Number(p.radius) || 0), label: String(p?.label || 'Assistant'),
68
73
  });
74
+ /** webview.float hit rects (app CSS px); junk entries dropped. */
75
+ const floatRects = (l: unknown) => (Array.isArray(l) ? l : []).map((r: any) => ({ x: +r?.x, y: +r?.y, w: +r?.w, h: +r?.h }))
76
+ .filter((r) => [r.x, r.y, r.w, r.h].every(Number.isFinite) && r.w > 0 && r.h > 0);
69
77
  const parsed = (s: unknown) => { try { return JSON.parse(String(s)); } catch { return s ?? null; } };
70
78
  const EVAL_TIMEOUT = 30000;
71
79
 
@@ -75,6 +83,9 @@ const snapOpts = (p?: { width?: number; quality?: number } | null) => ({
75
83
  quality: Math.min(1, Math.max(0.1, Number(p?.quality) || 0.6)),
76
84
  });
77
85
 
86
+ /** webview.float is on: the app WebView must stay see-through (keyboard.ts's backdrop paint skips it). */
87
+ export let appFloating = false;
88
+
78
89
  export function registerWebViewHandlers(): void {
79
90
  if (isIOS) registerIos();
80
91
  else if (isAndroid) registerAndroid();
@@ -94,6 +105,17 @@ function registerIos(): void {
94
105
  let fab: UIButton | null = null;
95
106
  let fabTarget: any = null, FabTargetClass: any; // its tap target (an exposedMethods NSObject, built once)
96
107
  let fabBottom: NSLayoutConstraint | null = null, fabRight: NSLayoutConstraint | null = null, fabW: NSLayoutConstraint | null = null, fabH: NSLayoutConstraint | null = null;
108
+ // webview.float: the app WebView renders above the overlay (layer zPosition — draw order only) while the overlay stays
109
+ // first in hit-testing (subview order): it sits in a pass-through wrapper that declines the touches inside `floatHit`
110
+ // (app CSS px), which fall to the app. (A wrapper, not a WKWebView subclass: its super hitTest recursed in NativeScript.)
111
+ let floating = false, floatHit: { x: number; y: number; w: number; h: number }[] = [], hostBg: UIColor | null = null, hostOpaque = true;
112
+ let wrap: UIView | null = null, WrapClass: any;
113
+ const appHit = (view: UIView, pt: CGPoint) => {
114
+ const host = bridge.getWebView()?.ios as UIView | undefined;
115
+ if (!floating || !host) return false;
116
+ const p = view.convertPointToView(pt, host);
117
+ return floatHit.some((r) => p.x >= r.x && p.x <= r.x + r.w && p.y >= r.y && p.y <= r.y + r.h);
118
+ };
97
119
  /** The button stays above the overlay (which comes to the front on open/show). */
98
120
  const fabFront = () => { if (fab && !fab.hidden) fab.superview?.bringSubviewToFront(fab); };
99
121
 
@@ -134,7 +156,16 @@ function registerIos(): void {
134
156
  if ((view as any).inspectable !== undefined) (view as any).inspectable = true; // iOS 16.4+ Safari inspector
135
157
 
136
158
  view.translatesAutoresizingMaskIntoConstraints = false;
137
- container.addSubview(view);
159
+ WrapClass ??= (UIView as any).extend({
160
+ hitTestWithEvent(pt: CGPoint, ev: _UIEvent) {
161
+ if (!wv || appHit(this, pt)) return null;
162
+ return wv.hitTestWithEvent(this.convertPointToView(pt, wv), ev);
163
+ },
164
+ }, { name: 'AppwrapOverlayWrap' });
165
+ wrap = WrapClass.alloc().initWithFrame(CGRectZero);
166
+ wrap!.translatesAutoresizingMaskIntoConstraints = false;
167
+ container.addSubview(wrap!);
168
+ wrap!.addSubview(view);
138
169
  // Anchor to the app WebView's box so `top`/`bottom` are in the page's own CSS px (== points).
139
170
  const ref: any = host ?? container;
140
171
  topConstraint = view.topAnchor.constraintEqualToAnchorConstant(ref.topAnchor, top);
@@ -144,6 +175,8 @@ function registerIos(): void {
144
175
  bottomConstraint,
145
176
  view.leadingAnchor.constraintEqualToAnchor(ref.leadingAnchor),
146
177
  view.trailingAnchor.constraintEqualToAnchor(ref.trailingAnchor),
178
+ wrap!.topAnchor.constraintEqualToAnchor(ref.topAnchor), wrap!.bottomAnchor.constraintEqualToAnchor(ref.bottomAnchor),
179
+ wrap!.leadingAnchor.constraintEqualToAnchor(ref.leadingAnchor), wrap!.trailingAnchor.constraintEqualToAnchor(ref.trailingAnchor),
147
180
  ] as any);
148
181
 
149
182
  ObserverClass ??= (NSObject as any).extend(
@@ -166,7 +199,7 @@ function registerIos(): void {
166
199
  if (!wv) return;
167
200
  for (const k of KVO_KEYS) { try { wv.removeObserverForKeyPath(observer!, k); } catch { /* not observed */ } }
168
201
  wv.stopLoading();
169
- wv.removeFromSuperview();
202
+ wv.removeFromSuperview(); wrap?.removeFromSuperview(); wrap = null;
170
203
  wv = null; observer = null; uiDelegate = null; topConstraint = null; bottomConstraint = null;
171
204
  bridge.emit('webview.closed', {});
172
205
  };
@@ -190,7 +223,7 @@ function registerIos(): void {
190
223
  if (!wv) create(inset(top), inset(bottom));
191
224
  else setInsets(top, bottom);
192
225
  wv!.hidden = false;
193
- wv!.superview?.bringSubviewToFront(wv!);
226
+ wrap?.superview?.bringSubviewToFront(wrap);
194
227
  fabFront();
195
228
  load(target);
196
229
  return state();
@@ -211,7 +244,7 @@ function registerIos(): void {
211
244
 
212
245
  bridge.register('webview.hide', () => onMain(() => { if (wv) wv.hidden = true; return { open: !!wv }; }));
213
246
  bridge.register('webview.show', (p?: { top?: number; bottom?: number } | null) => onMain(() => {
214
- if (wv) { setInsets(p?.top, p?.bottom); wv.hidden = false; wv.superview?.bringSubviewToFront(wv); fabFront(); }
247
+ if (wv) { setInsets(p?.top, p?.bottom); wv.hidden = false; wrap?.superview?.bringSubviewToFront(wrap); fabFront(); }
215
248
  return { open: !!wv };
216
249
  }));
217
250
  bridge.register('webview.close', () => onMain(() => { destroy(); }));
@@ -256,6 +289,21 @@ function registerIos(): void {
256
289
  });
257
290
  });
258
291
 
292
+ bridge.register('webview.float', (p?: { on?: boolean; hit?: unknown } | null) => onMain(() => {
293
+ const host = bridge.getWebView()?.ios as WKWebView | undefined;
294
+ if (!host) throw err('NATIVE_ERROR', 'webview.float: no host view');
295
+ floatHit = floatRects(p?.hit);
296
+ const on = !!p?.on;
297
+ if (on !== floating) {
298
+ floating = appFloating = on;
299
+ if (on) { hostBg = host.backgroundColor; hostOpaque = host.opaque; host.opaque = false; host.backgroundColor = UIColor.clearColor; host.scrollView.backgroundColor = UIColor.clearColor; }
300
+ else { host.opaque = hostOpaque; host.backgroundColor = hostBg; host.scrollView.backgroundColor = hostBg; }
301
+ host.layer.zPosition = on ? 1 : 0;
302
+ if (fab) fab.layer.zPosition = on ? 2 : 0;
303
+ }
304
+ return { floating };
305
+ }));
306
+
259
307
  bridge.register('webview.snapshot', (p?: { width?: number; quality?: number } | null) => {
260
308
  const o = snapOpts(p);
261
309
  return onMain(() => new Promise<{ jpeg: string; width: number; height: number }>((resolve, reject) => {
@@ -537,6 +585,8 @@ function registerAndroid(): void {
537
585
  })();
538
586
  });
539
587
 
588
+ bridge.register('webview.float', () => ({ floating: false })); // (not supported here: callers keep their fallback)
589
+
540
590
  bridge.register('webview.cookies', (p: { cookies?: Cookie[] } | null) => {
541
591
  const list = cookieList(p);
542
592
  return onMain(() => new Promise<{ set: number }>((resolve) => {
@@ -3,6 +3,7 @@ import { bridge } from './bridge';
3
3
  import { SHELL_CONFIG } from './config';
4
4
  import { createKeyboardShowCycle, type KeyboardEventTag } from './keyboard-frame';
5
5
  import { appwrapNativeLog } from './native-log';
6
+ import { appFloating } from './handlers-webview';
6
7
 
7
8
  declare const android: any;
8
9
  // iOS keyboard-notification globals (marshalled from UIKit at runtime).
@@ -231,7 +232,8 @@ function syncBackdropColor(): void {
231
232
  const color = UIColor.colorWithRedGreenBlueAlpha(+m[1] / 255, +m[2] / 255, +m[3] / 255, 1);
232
233
  // The keyboard's rounded corners (iOS 26) show whatever native view is behind the webview — paint
233
234
  // the whole chain up to the window, not only the webview + window (a dark host view showed through).
234
- for (let v: UIView | null = w; v; v = v.superview) v.backgroundColor = color;
235
+ // (floating over a page — webview.float — the webview itself stays clear, or it covers the page)
236
+ for (let v: UIView | null = appFloating ? w.superview : w; v; v = v.superview) v.backgroundColor = color;
235
237
  if (w.window) w.window.backgroundColor = color;
236
238
  if (SHELL_CONFIG.debug) appwrapNativeLog(`[native:keyboard] backdrop ← rgb(${m[1]},${m[2]},${m[3]})`);
237
239
  });