@qontinui/ui-bridge-native 0.6.10 → 0.6.11
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/dist/core/index.js +232 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +232 -2
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/registry.d.ts +46 -1
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/types.d.ts +114 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/vision-fields.d.ts +142 -0
- package/dist/core/vision-fields.d.ts.map +1 -0
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +311 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +307 -13
- package/dist/index.mjs.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +305 -11
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +306 -12
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useUIElement.d.ts.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/http-server.d.ts +9 -1
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.js +217 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +217 -5
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/observability.d.ts +4 -3
- package/dist/server/observability.d.ts.map +1 -1
- package/dist/server/types.d.ts +10 -0
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/react/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useRef, useState, useCallback, useEffect, useMemo, use } from 'react';
|
|
2
|
-
import { Dimensions, StyleSheet, Platform } from 'react-native';
|
|
2
|
+
import { PixelRatio, Dimensions, StyleSheet, Platform } from 'react-native';
|
|
3
3
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
5
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
@@ -9,6 +9,179 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
9
9
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
+
// src/core/vision-fields.ts
|
|
13
|
+
var INTERACTIVE_HANDLER_NAMES = /* @__PURE__ */ new Set([
|
|
14
|
+
"onPress",
|
|
15
|
+
"onPressIn",
|
|
16
|
+
"onPressOut",
|
|
17
|
+
"onLongPress",
|
|
18
|
+
"onChangeText",
|
|
19
|
+
"onValueChange",
|
|
20
|
+
"onChange",
|
|
21
|
+
"onSubmitEditing",
|
|
22
|
+
"onClick",
|
|
23
|
+
"onTouchStart",
|
|
24
|
+
"onTouchEnd"
|
|
25
|
+
]);
|
|
26
|
+
var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
27
|
+
"button",
|
|
28
|
+
"input",
|
|
29
|
+
"switch",
|
|
30
|
+
"checkbox",
|
|
31
|
+
"radio",
|
|
32
|
+
"touchable",
|
|
33
|
+
"pressable",
|
|
34
|
+
"listItem"
|
|
35
|
+
]);
|
|
36
|
+
function roleForType(type) {
|
|
37
|
+
switch (type) {
|
|
38
|
+
case "button":
|
|
39
|
+
case "touchable":
|
|
40
|
+
case "pressable":
|
|
41
|
+
return "button";
|
|
42
|
+
case "input":
|
|
43
|
+
return "textbox";
|
|
44
|
+
case "text":
|
|
45
|
+
return "text";
|
|
46
|
+
case "switch":
|
|
47
|
+
return "switch";
|
|
48
|
+
case "checkbox":
|
|
49
|
+
return "checkbox";
|
|
50
|
+
case "radio":
|
|
51
|
+
return "radio";
|
|
52
|
+
case "image":
|
|
53
|
+
return "img";
|
|
54
|
+
case "list":
|
|
55
|
+
return "list";
|
|
56
|
+
case "listItem":
|
|
57
|
+
return "listitem";
|
|
58
|
+
case "modal":
|
|
59
|
+
return "dialog";
|
|
60
|
+
case "scroll":
|
|
61
|
+
case "view":
|
|
62
|
+
case "custom":
|
|
63
|
+
default:
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function deriveInteractable(type, handlerNames) {
|
|
68
|
+
if (INTERACTIVE_TYPES.has(type)) return true;
|
|
69
|
+
if (handlerNames) {
|
|
70
|
+
for (const name of handlerNames) {
|
|
71
|
+
if (INTERACTIVE_HANDLER_NAMES.has(name)) return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function deriveText(state, label) {
|
|
77
|
+
const candidates = [state.value, state.textContent, label];
|
|
78
|
+
for (const c of candidates) {
|
|
79
|
+
if (typeof c === "string") {
|
|
80
|
+
const trimmed = c.trim();
|
|
81
|
+
if (trimmed.length > 0) return trimmed;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return void 0;
|
|
85
|
+
}
|
|
86
|
+
var NAMED_COLORS = {
|
|
87
|
+
black: { r: 0, g: 0, b: 0 },
|
|
88
|
+
white: { r: 255, g: 255, b: 255 },
|
|
89
|
+
red: { r: 255, g: 0, b: 0 },
|
|
90
|
+
green: { r: 0, g: 128, b: 0 },
|
|
91
|
+
blue: { r: 0, g: 0, b: 255 },
|
|
92
|
+
gray: { r: 128, g: 128, b: 128 },
|
|
93
|
+
grey: { r: 128, g: 128, b: 128 },
|
|
94
|
+
silver: { r: 192, g: 192, b: 192 },
|
|
95
|
+
transparent: { r: 0, g: 0, b: 0 },
|
|
96
|
+
orange: { r: 255, g: 165, b: 0 },
|
|
97
|
+
yellow: { r: 255, g: 255, b: 0 },
|
|
98
|
+
purple: { r: 128, g: 0, b: 128 },
|
|
99
|
+
navy: { r: 0, g: 0, b: 128 }
|
|
100
|
+
};
|
|
101
|
+
var clamp255 = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
102
|
+
function parseColorToRgb(input) {
|
|
103
|
+
if (typeof input !== "string") return void 0;
|
|
104
|
+
const raw = input.trim().toLowerCase();
|
|
105
|
+
if (raw.length === 0) return void 0;
|
|
106
|
+
if (Object.prototype.hasOwnProperty.call(NAMED_COLORS, raw)) {
|
|
107
|
+
return NAMED_COLORS[raw];
|
|
108
|
+
}
|
|
109
|
+
if (raw.startsWith("#")) {
|
|
110
|
+
const hex = raw.slice(1);
|
|
111
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
112
|
+
const r = parseInt(hex[0] + hex[0], 16);
|
|
113
|
+
const g = parseInt(hex[1] + hex[1], 16);
|
|
114
|
+
const b = parseInt(hex[2] + hex[2], 16);
|
|
115
|
+
if ([r, g, b].every((v) => Number.isFinite(v))) return { r, g, b };
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
119
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
120
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
121
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
122
|
+
if ([r, g, b].every((v) => Number.isFinite(v))) return { r, g, b };
|
|
123
|
+
return void 0;
|
|
124
|
+
}
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
const rgbMatch = raw.match(/^rgba?\(([^)]+)\)$/);
|
|
128
|
+
if (rgbMatch) {
|
|
129
|
+
const parts = rgbMatch[1].split(",").map((p) => p.trim());
|
|
130
|
+
if (parts.length < 3) return void 0;
|
|
131
|
+
const channel = (p) => {
|
|
132
|
+
if (p.endsWith("%")) {
|
|
133
|
+
const pct = parseFloat(p.slice(0, -1));
|
|
134
|
+
if (!Number.isFinite(pct)) return void 0;
|
|
135
|
+
return clamp255(pct / 100 * 255);
|
|
136
|
+
}
|
|
137
|
+
const n = parseFloat(p);
|
|
138
|
+
if (!Number.isFinite(n)) return void 0;
|
|
139
|
+
return clamp255(n);
|
|
140
|
+
};
|
|
141
|
+
const r = channel(parts[0]);
|
|
142
|
+
const g = channel(parts[1]);
|
|
143
|
+
const b = channel(parts[2]);
|
|
144
|
+
if (r === void 0 || g === void 0 || b === void 0) return void 0;
|
|
145
|
+
return { r, g, b };
|
|
146
|
+
}
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
149
|
+
function parseFontSizePx(input) {
|
|
150
|
+
if (typeof input === "number") {
|
|
151
|
+
return Number.isFinite(input) && input > 0 ? input : void 0;
|
|
152
|
+
}
|
|
153
|
+
if (typeof input === "string") {
|
|
154
|
+
const m = input.trim().match(/^(-?\d*\.?\d+)/);
|
|
155
|
+
if (m) {
|
|
156
|
+
const n = parseFloat(m[1]);
|
|
157
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return void 0;
|
|
161
|
+
}
|
|
162
|
+
function projectVisionFields(args) {
|
|
163
|
+
const { type, label, handlerNames, state, flatStyle } = args;
|
|
164
|
+
const fields = {
|
|
165
|
+
interactable: deriveInteractable(type, handlerNames)
|
|
166
|
+
};
|
|
167
|
+
const text = deriveText(state, label);
|
|
168
|
+
if (text !== void 0) fields.text = text;
|
|
169
|
+
const role = roleForType(type);
|
|
170
|
+
if (role !== void 0) fields.role = role;
|
|
171
|
+
if (flatStyle) {
|
|
172
|
+
const fg = parseColorToRgb(flatStyle.color);
|
|
173
|
+
if (fg !== void 0) fields.fg_color = fg;
|
|
174
|
+
const bg = parseColorToRgb(flatStyle.backgroundColor);
|
|
175
|
+
if (bg !== void 0) fields.bg_color = bg;
|
|
176
|
+
const size = parseFontSizePx(flatStyle.fontSize);
|
|
177
|
+
if (size !== void 0) fields.font_size_px = size;
|
|
178
|
+
if (typeof flatStyle.fontFamily === "string" && flatStyle.fontFamily.trim().length > 0) {
|
|
179
|
+
fields.font_family = flatStyle.fontFamily;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return fields;
|
|
183
|
+
}
|
|
184
|
+
|
|
12
185
|
// src/core/registry.ts
|
|
13
186
|
function extractHandlerNames(props) {
|
|
14
187
|
if (!props) return [];
|
|
@@ -18,6 +191,25 @@ function matchesCurrentRoute(registrationRoute, currentRoute) {
|
|
|
18
191
|
if (registrationRoute == null || registrationRoute === "") return true;
|
|
19
192
|
return registrationRoute === currentRoute;
|
|
20
193
|
}
|
|
194
|
+
function projectBbox(state, visibility, pixelRatio = 1) {
|
|
195
|
+
if (visibility !== "visible") return void 0;
|
|
196
|
+
const layout = state.layout;
|
|
197
|
+
if (!layout) return void 0;
|
|
198
|
+
const ratio = pixelRatio > 0 && Number.isFinite(pixelRatio) ? pixelRatio : 1;
|
|
199
|
+
const toPx = (v) => Math.max(0, Math.round(v * ratio));
|
|
200
|
+
const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
|
|
201
|
+
const originX = hasPage ? layout.pageX : layout.x;
|
|
202
|
+
const originY = hasPage ? layout.pageY : layout.y;
|
|
203
|
+
if (typeof originX !== "number" || typeof originY !== "number" || typeof layout.width !== "number" || typeof layout.height !== "number" || !Number.isFinite(originX) || !Number.isFinite(originY) || !Number.isFinite(layout.width) || !Number.isFinite(layout.height)) {
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
x: toPx(originX),
|
|
208
|
+
y: toPx(originY),
|
|
209
|
+
w: toPx(layout.width),
|
|
210
|
+
h: toPx(layout.height)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
21
213
|
function inferActions(type) {
|
|
22
214
|
const baseActions = ["focus", "blur"];
|
|
23
215
|
switch (type) {
|
|
@@ -82,6 +274,15 @@ var NativeUIBridgeRegistry = class {
|
|
|
82
274
|
* already installs.
|
|
83
275
|
*/
|
|
84
276
|
this.routeProvider = null;
|
|
277
|
+
/**
|
|
278
|
+
* Device pixel ratio used to project `state.layout` (logical dp) into the
|
|
279
|
+
* physical-pixel `bbox` emitted on each snapshot element. Defaults to `1`
|
|
280
|
+
* (dp ≈ px) so callers/tests that never inject a ratio still get a usable
|
|
281
|
+
* box; the React provider wires the real `PixelRatio.get()` via
|
|
282
|
+
* {@link setPixelRatio}. Read from a non-React core module on purpose — see
|
|
283
|
+
* the `projectBbox` doc comment for why we don't import `react-native` here.
|
|
284
|
+
*/
|
|
285
|
+
this.pixelRatio = 1;
|
|
85
286
|
/**
|
|
86
287
|
* Sticky flag: flips `true` the first time any element is registered and
|
|
87
288
|
* stays `true` even after elements are unregistered. Lets agents distinguish
|
|
@@ -119,6 +320,21 @@ var NativeUIBridgeRegistry = class {
|
|
|
119
320
|
getRouteProvider() {
|
|
120
321
|
return this.routeProvider;
|
|
121
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Set the device pixel ratio used to project per-element `bbox` geometry
|
|
325
|
+
* into physical pixels. The React provider passes `PixelRatio.get()` here
|
|
326
|
+
* once at startup. Ignores non-finite / non-positive values (keeps the
|
|
327
|
+
* previous ratio) so a bad call can't zero out every bbox.
|
|
328
|
+
*/
|
|
329
|
+
setPixelRatio(ratio) {
|
|
330
|
+
if (typeof ratio === "number" && Number.isFinite(ratio) && ratio > 0) {
|
|
331
|
+
this.pixelRatio = ratio;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** Read-only accessor for the device pixel ratio used for bbox projection. */
|
|
335
|
+
getPixelRatio() {
|
|
336
|
+
return this.pixelRatio;
|
|
337
|
+
}
|
|
122
338
|
// ============================================================================
|
|
123
339
|
// Snapshot Enricher Slots
|
|
124
340
|
// ============================================================================
|
|
@@ -658,6 +874,18 @@ var NativeUIBridgeRegistry = class {
|
|
|
658
874
|
const handlers = extractHandlerNames(e.props);
|
|
659
875
|
const state = e.getState();
|
|
660
876
|
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
877
|
+
const bbox = projectBbox(state, visibility, this.pixelRatio);
|
|
878
|
+
const vision = projectVisionFields({
|
|
879
|
+
type: e.type,
|
|
880
|
+
label: e.label,
|
|
881
|
+
// Real interactivity signal: the names of props that are actually
|
|
882
|
+
// functions (onPress/onChangeText/...). NOT `e.actions`, which
|
|
883
|
+
// `inferActions` synthesizes press/click onto for every element —
|
|
884
|
+
// using it would mark plain text/view nodes interactable.
|
|
885
|
+
handlerNames: handlers,
|
|
886
|
+
state,
|
|
887
|
+
flatStyle: e.flatStyle
|
|
888
|
+
});
|
|
661
889
|
return {
|
|
662
890
|
id: e.id,
|
|
663
891
|
type: e.type,
|
|
@@ -668,7 +896,9 @@ var NativeUIBridgeRegistry = class {
|
|
|
668
896
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
669
897
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
670
898
|
registrationRoute: e.registrationRoute,
|
|
671
|
-
visibility
|
|
899
|
+
visibility,
|
|
900
|
+
...bbox ? { bbox } : {},
|
|
901
|
+
...vision
|
|
672
902
|
};
|
|
673
903
|
}),
|
|
674
904
|
components: this.getAllComponents().map((c) => ({
|
|
@@ -1842,7 +2072,7 @@ function createDesignHandlers(registry, viewportProvider) {
|
|
|
1842
2072
|
var GRID_SIZE = 20;
|
|
1843
2073
|
var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
|
|
1844
2074
|
var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
|
|
1845
|
-
var
|
|
2075
|
+
var INTERACTIVE_TYPES2 = /* @__PURE__ */ new Set([
|
|
1846
2076
|
"button",
|
|
1847
2077
|
"input",
|
|
1848
2078
|
"switch",
|
|
@@ -2039,7 +2269,7 @@ function diagnosePageHealth(elements, viewport) {
|
|
|
2039
2269
|
let interactiveTotal = 0;
|
|
2040
2270
|
let interactiveDisabled = 0;
|
|
2041
2271
|
for (const el of elements) {
|
|
2042
|
-
if (!
|
|
2272
|
+
if (!INTERACTIVE_TYPES2.has(el.type)) continue;
|
|
2043
2273
|
interactiveTotal++;
|
|
2044
2274
|
if (el.state.enabled === false) interactiveDisabled++;
|
|
2045
2275
|
}
|
|
@@ -2322,6 +2552,16 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2322
2552
|
const handlers = extractHandlerNames(e.props);
|
|
2323
2553
|
const state = e.getState();
|
|
2324
2554
|
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
2555
|
+
const bbox = projectBbox(state, visibility, registry.getPixelRatio());
|
|
2556
|
+
const vision = projectVisionFields({
|
|
2557
|
+
type: e.type,
|
|
2558
|
+
label: e.label,
|
|
2559
|
+
// Real handler names (onPress/...), NOT inferred actions which
|
|
2560
|
+
// synthesize press/click onto every element. See projectVisionFields.
|
|
2561
|
+
handlerNames: handlers,
|
|
2562
|
+
state,
|
|
2563
|
+
flatStyle: e.flatStyle
|
|
2564
|
+
});
|
|
2325
2565
|
return {
|
|
2326
2566
|
id: e.id,
|
|
2327
2567
|
type: e.type,
|
|
@@ -2332,7 +2572,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2332
2572
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
2333
2573
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
2334
2574
|
registrationRoute: e.registrationRoute,
|
|
2335
|
-
visibility
|
|
2575
|
+
visibility,
|
|
2576
|
+
...bbox ? { bbox } : {},
|
|
2577
|
+
...vision
|
|
2336
2578
|
};
|
|
2337
2579
|
});
|
|
2338
2580
|
return success2({ elements });
|
|
@@ -3015,7 +3257,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3015
3257
|
};
|
|
3016
3258
|
if (config?.appInfo) {
|
|
3017
3259
|
response.uiBridge = {
|
|
3018
|
-
version: "0.6.
|
|
3260
|
+
version: "0.6.11",
|
|
3019
3261
|
...config.appInfo,
|
|
3020
3262
|
capabilities: ["elements", "components", "actions", "design"],
|
|
3021
3263
|
elementCount: stats.elements,
|
|
@@ -3607,6 +3849,14 @@ var NativeUIBridgeServer = class {
|
|
|
3607
3849
|
};
|
|
3608
3850
|
};
|
|
3609
3851
|
}
|
|
3852
|
+
/**
|
|
3853
|
+
* Whether the console/network capture should be installed. The dedicated
|
|
3854
|
+
* `observability` flag wins when set; otherwise falls back to `testHooks`
|
|
3855
|
+
* so consumers predating the flag keep their exact behavior.
|
|
3856
|
+
*/
|
|
3857
|
+
observabilityEnabled() {
|
|
3858
|
+
return this.config.observability ?? this.config.testHooks === true;
|
|
3859
|
+
}
|
|
3610
3860
|
/**
|
|
3611
3861
|
* Set the server adapter
|
|
3612
3862
|
*/
|
|
@@ -3820,7 +4070,7 @@ var NativeUIBridgeServer = class {
|
|
|
3820
4070
|
console.warn("[ui-bridge-native] See documentation for supported adapters.");
|
|
3821
4071
|
return;
|
|
3822
4072
|
}
|
|
3823
|
-
if (this.
|
|
4073
|
+
if (this.observabilityEnabled()) {
|
|
3824
4074
|
this.consoleErrorBuffer.install();
|
|
3825
4075
|
this.networkRequestBuffer.install();
|
|
3826
4076
|
}
|
|
@@ -5322,6 +5572,7 @@ function UIBridgeNativeProvider({
|
|
|
5322
5572
|
verbose: config.verbose,
|
|
5323
5573
|
onEvent
|
|
5324
5574
|
});
|
|
5575
|
+
registryRef.current.setPixelRatio(PixelRatio.get());
|
|
5325
5576
|
setGlobalRegistry(registryRef.current);
|
|
5326
5577
|
}
|
|
5327
5578
|
const registry = registryRef.current;
|
|
@@ -5352,6 +5603,10 @@ function UIBridgeNativeProvider({
|
|
|
5352
5603
|
cors: true,
|
|
5353
5604
|
appInfo: config.appInfo,
|
|
5354
5605
|
testHooks: features.testHooks === true,
|
|
5606
|
+
// Dedicated console/network-capture flag; the server defaults it to
|
|
5607
|
+
// the `testHooks` value when undefined, so passing it through verbatim
|
|
5608
|
+
// keeps existing consumers unchanged.
|
|
5609
|
+
observability: features.observability,
|
|
5355
5610
|
// Injected device-viewport getter for POST /control/page-health. The
|
|
5356
5611
|
// server doesn't import `react-native` itself — the require/import
|
|
5357
5612
|
// pattern there crashed the host RN app in 0.6.3/0.6.4 (Metro/Hermes
|
|
@@ -5419,6 +5674,7 @@ function UIBridgeNativeProvider({
|
|
|
5419
5674
|
}, [
|
|
5420
5675
|
features.server,
|
|
5421
5676
|
features.testHooks,
|
|
5677
|
+
features.observability,
|
|
5422
5678
|
config.serverPort,
|
|
5423
5679
|
config.appInfo,
|
|
5424
5680
|
registry,
|
|
@@ -5457,6 +5713,7 @@ function UIBridgeNativeProvider({
|
|
|
5457
5713
|
cors: true,
|
|
5458
5714
|
appInfo: config.appInfo,
|
|
5459
5715
|
testHooks: features.testHooks === true,
|
|
5716
|
+
observability: features.observability,
|
|
5460
5717
|
viewportProvider: () => {
|
|
5461
5718
|
const win = Dimensions.get("window");
|
|
5462
5719
|
return { width: win.width, height: win.height };
|
|
@@ -5660,12 +5917,41 @@ function scheduleDeadButtonWarning(bridge, id, type) {
|
|
|
5660
5917
|
}
|
|
5661
5918
|
function useUIElement(options) {
|
|
5662
5919
|
const bridge = useUIBridgeNativeOptional();
|
|
5663
|
-
const ref = useRef(null);
|
|
5664
5920
|
const [registered, setRegistered] = useState(false);
|
|
5665
5921
|
const [_layout, setLayout] = useState(null);
|
|
5666
5922
|
const propsRef = useRef({});
|
|
5667
5923
|
const registeredRef = useRef(false);
|
|
5668
5924
|
const registeredIdRef = useRef(null);
|
|
5925
|
+
const bridgeRef = useRef(bridge);
|
|
5926
|
+
bridgeRef.current = bridge;
|
|
5927
|
+
const nodeRef = useRef(null);
|
|
5928
|
+
const refInterceptorRef = useRef(null);
|
|
5929
|
+
if (refInterceptorRef.current === null) {
|
|
5930
|
+
refInterceptorRef.current = {
|
|
5931
|
+
get current() {
|
|
5932
|
+
return nodeRef.current;
|
|
5933
|
+
},
|
|
5934
|
+
set current(node) {
|
|
5935
|
+
const prev = nodeRef.current;
|
|
5936
|
+
nodeRef.current = node;
|
|
5937
|
+
const liveBridge = bridgeRef.current;
|
|
5938
|
+
const registeredId = registeredIdRef.current;
|
|
5939
|
+
if (!liveBridge || !registeredRef.current || registeredId === null) return;
|
|
5940
|
+
if (node == null && prev != null) {
|
|
5941
|
+
const state = liveBridge.registry.getElement(registeredId)?.getState();
|
|
5942
|
+
if (state && state.visible !== false) {
|
|
5943
|
+
liveBridge.registry.updateElementState(registeredId, { visible: false });
|
|
5944
|
+
}
|
|
5945
|
+
} else if (node != null && prev == null) {
|
|
5946
|
+
const state = liveBridge.registry.getElement(registeredId)?.getState();
|
|
5947
|
+
if (state && state.visible !== true) {
|
|
5948
|
+
liveBridge.registry.updateElementState(registeredId, { visible: true });
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5952
|
+
};
|
|
5953
|
+
}
|
|
5954
|
+
const ref = refInterceptorRef.current;
|
|
5669
5955
|
const {
|
|
5670
5956
|
id,
|
|
5671
5957
|
type = "custom",
|
|
@@ -5709,7 +5995,7 @@ function useUIElement(options) {
|
|
|
5709
5995
|
}
|
|
5710
5996
|
setRegistered(true);
|
|
5711
5997
|
scheduleDeadButtonWarning(bridge, id, type);
|
|
5712
|
-
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
|
|
5998
|
+
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp, ref]);
|
|
5713
5999
|
const unregister = useCallback(() => {
|
|
5714
6000
|
if (!bridge || !registeredRef.current) return;
|
|
5715
6001
|
bridge.registry.unregisterElement(registeredIdRef.current ?? id);
|
|
@@ -5745,7 +6031,8 @@ function useUIElement(options) {
|
|
|
5745
6031
|
writeLayout({ x, y, width, height, pageX: x, pageY: y });
|
|
5746
6032
|
}
|
|
5747
6033
|
},
|
|
5748
|
-
|
|
6034
|
+
// `ref` is the once-per-hook interceptor object — identity-stable.
|
|
6035
|
+
[bridge, id, ref]
|
|
5749
6036
|
);
|
|
5750
6037
|
const registerRef = useRef(register);
|
|
5751
6038
|
const unregisterRef = useRef(unregister);
|
|
@@ -5766,7 +6053,14 @@ function useUIElement(options) {
|
|
|
5766
6053
|
useEffect(() => {
|
|
5767
6054
|
if (!bridge || !registered) return;
|
|
5768
6055
|
const node = ref.current;
|
|
5769
|
-
if (!node
|
|
6056
|
+
if (!node) {
|
|
6057
|
+
const seeded = bridge.registry.getElement(id)?.getState();
|
|
6058
|
+
if (seeded && seeded.visible !== false) {
|
|
6059
|
+
bridge.registry.updateElementState(id, { visible: false });
|
|
6060
|
+
}
|
|
6061
|
+
return;
|
|
6062
|
+
}
|
|
6063
|
+
if (typeof node.measureInWindow !== "function") return;
|
|
5770
6064
|
let cancelled = false;
|
|
5771
6065
|
let pendingTimer = null;
|
|
5772
6066
|
const backoffMs = [50, 200, 500, 1e3, 2500];
|
|
@@ -5805,7 +6099,7 @@ function useUIElement(options) {
|
|
|
5805
6099
|
cancelled = true;
|
|
5806
6100
|
if (pendingTimer != null) clearTimeout(pendingTimer);
|
|
5807
6101
|
};
|
|
5808
|
-
}, [bridge, registered, id]);
|
|
6102
|
+
}, [bridge, registered, id, ref]);
|
|
5809
6103
|
const registeredHandlerKeysRef = useRef([]);
|
|
5810
6104
|
useEffect(() => {
|
|
5811
6105
|
if (!bridge || !registered || !handlersProp) return;
|