@qontinui/ui-bridge-native 0.6.10 → 0.6.12
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 +329 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +329 -2
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/registry.d.ts +91 -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 +412 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +408 -14
- package/dist/index.mjs.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +406 -12
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +407 -13
- 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 +220 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +220 -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
|
// ============================================================================
|
|
@@ -344,6 +560,103 @@ var NativeUIBridgeRegistry = class {
|
|
|
344
560
|
console.log(`[ui-bridge-native] Marked ${cleared} elements offscreen for route: ${route}`);
|
|
345
561
|
}
|
|
346
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* Re-measure every mounted-visible element's geometry via its stored React
|
|
565
|
+
* ref and write the fresh coordinates into the registry ("measure-on-
|
|
566
|
+
* snapshot").
|
|
567
|
+
*
|
|
568
|
+
* Why: element geometry is otherwise captured ONCE at mount time
|
|
569
|
+
* (`onLayout` + `measureInWindow` in `useUIElement`) and RN never re-fires
|
|
570
|
+
* `onLayout` on pure translation — scrolling a virtualized list
|
|
571
|
+
* (FlashList), recycling a cell, a keyboard shift or an orientation change
|
|
572
|
+
* all move elements WITHOUT updating the stored `state.layout`. Snapshot
|
|
573
|
+
* consumers (vision analyzers hit-testing `bbox`, coord-based tap) then
|
|
574
|
+
* operate on stale positions. Re-measuring at READ time is correct
|
|
575
|
+
* regardless of what moved the element and needs zero per-consumer wiring:
|
|
576
|
+
* every registry entry already stores its ref (see `registerElement`).
|
|
577
|
+
*
|
|
578
|
+
* Per-element outcomes:
|
|
579
|
+
* - non-zero dims → write a fresh full layout (same state shape as the
|
|
580
|
+
* mount-time measure loop in `useUIElement`); counted as `measured`.
|
|
581
|
+
* - zero dims AND the element previously had a non-null `state.layout` →
|
|
582
|
+
* the view collapsed / left the screen; clear to
|
|
583
|
+
* `{ visible: false, layout: null }` (same shape `markRouteOffscreen`
|
|
584
|
+
* writes); counted as `cleared`.
|
|
585
|
+
* - zero dims and it never had a layout → leave state untouched (element
|
|
586
|
+
* in the mount→first-onLayout gap; don't demote `likely-visible`);
|
|
587
|
+
* counted as `skipped`.
|
|
588
|
+
* - no ref / no callable `measureInWindow` (test fixtures, web) → leave
|
|
589
|
+
* untouched; counted as `skipped`.
|
|
590
|
+
*
|
|
591
|
+
* The whole sweep races an overall timeout (default 250 ms) so a dead ref
|
|
592
|
+
* whose `measureInWindow` never calls back can never hang a snapshot: the
|
|
593
|
+
* returned promise resolves at the deadline with whatever counts have
|
|
594
|
+
* accumulated. Late callbacks may still write state afterwards — harmless,
|
|
595
|
+
* freshest data wins. Never throws.
|
|
596
|
+
*
|
|
597
|
+
* Non-React core module on purpose — `measureInWindow` is duck-typed off
|
|
598
|
+
* the stored ref rather than imported from `react-native` (see the
|
|
599
|
+
* `projectBbox` doc comment for why this file must not import RN).
|
|
600
|
+
*/
|
|
601
|
+
async refreshMeasurements(options) {
|
|
602
|
+
const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 250;
|
|
603
|
+
const counts = { measured: 0, cleared: 0, skipped: 0 };
|
|
604
|
+
const pending = [];
|
|
605
|
+
for (const element of this.getMountedVisibleElements()) {
|
|
606
|
+
const node = element.ref?.current;
|
|
607
|
+
if (!node || typeof node.measureInWindow !== "function") {
|
|
608
|
+
counts.skipped++;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const id = element.id;
|
|
612
|
+
pending.push(
|
|
613
|
+
new Promise((resolve) => {
|
|
614
|
+
let settled = false;
|
|
615
|
+
try {
|
|
616
|
+
node.measureInWindow((pageX, pageY, w, h) => {
|
|
617
|
+
if (settled) return;
|
|
618
|
+
settled = true;
|
|
619
|
+
try {
|
|
620
|
+
if (w > 0 && h > 0) {
|
|
621
|
+
this.updateElementState(id, {
|
|
622
|
+
mounted: true,
|
|
623
|
+
visible: true,
|
|
624
|
+
enabled: true,
|
|
625
|
+
focused: false,
|
|
626
|
+
layout: { x: pageX, y: pageY, width: w, height: h, pageX, pageY }
|
|
627
|
+
});
|
|
628
|
+
counts.measured++;
|
|
629
|
+
} else if (this.elements.get(id)?.getState().layout != null) {
|
|
630
|
+
this.updateElementState(id, { visible: false, layout: null });
|
|
631
|
+
counts.cleared++;
|
|
632
|
+
} else {
|
|
633
|
+
counts.skipped++;
|
|
634
|
+
}
|
|
635
|
+
} finally {
|
|
636
|
+
resolve();
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
} catch {
|
|
640
|
+
if (!settled) {
|
|
641
|
+
settled = true;
|
|
642
|
+
counts.skipped++;
|
|
643
|
+
resolve();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
})
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
if (pending.length > 0) {
|
|
650
|
+
await new Promise((resolve) => {
|
|
651
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
652
|
+
void Promise.all(pending).then(() => {
|
|
653
|
+
clearTimeout(timer);
|
|
654
|
+
resolve();
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return counts;
|
|
659
|
+
}
|
|
347
660
|
/**
|
|
348
661
|
* Update element state
|
|
349
662
|
*/
|
|
@@ -658,6 +971,18 @@ var NativeUIBridgeRegistry = class {
|
|
|
658
971
|
const handlers = extractHandlerNames(e.props);
|
|
659
972
|
const state = e.getState();
|
|
660
973
|
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
974
|
+
const bbox = projectBbox(state, visibility, this.pixelRatio);
|
|
975
|
+
const vision = projectVisionFields({
|
|
976
|
+
type: e.type,
|
|
977
|
+
label: e.label,
|
|
978
|
+
// Real interactivity signal: the names of props that are actually
|
|
979
|
+
// functions (onPress/onChangeText/...). NOT `e.actions`, which
|
|
980
|
+
// `inferActions` synthesizes press/click onto for every element —
|
|
981
|
+
// using it would mark plain text/view nodes interactable.
|
|
982
|
+
handlerNames: handlers,
|
|
983
|
+
state,
|
|
984
|
+
flatStyle: e.flatStyle
|
|
985
|
+
});
|
|
661
986
|
return {
|
|
662
987
|
id: e.id,
|
|
663
988
|
type: e.type,
|
|
@@ -668,7 +993,9 @@ var NativeUIBridgeRegistry = class {
|
|
|
668
993
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
669
994
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
670
995
|
registrationRoute: e.registrationRoute,
|
|
671
|
-
visibility
|
|
996
|
+
visibility,
|
|
997
|
+
...bbox ? { bbox } : {},
|
|
998
|
+
...vision
|
|
672
999
|
};
|
|
673
1000
|
}),
|
|
674
1001
|
components: this.getAllComponents().map((c) => ({
|
|
@@ -1842,7 +2169,7 @@ function createDesignHandlers(registry, viewportProvider) {
|
|
|
1842
2169
|
var GRID_SIZE = 20;
|
|
1843
2170
|
var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
|
|
1844
2171
|
var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
|
|
1845
|
-
var
|
|
2172
|
+
var INTERACTIVE_TYPES2 = /* @__PURE__ */ new Set([
|
|
1846
2173
|
"button",
|
|
1847
2174
|
"input",
|
|
1848
2175
|
"switch",
|
|
@@ -2039,7 +2366,7 @@ function diagnosePageHealth(elements, viewport) {
|
|
|
2039
2366
|
let interactiveTotal = 0;
|
|
2040
2367
|
let interactiveDisabled = 0;
|
|
2041
2368
|
for (const el of elements) {
|
|
2042
|
-
if (!
|
|
2369
|
+
if (!INTERACTIVE_TYPES2.has(el.type)) continue;
|
|
2043
2370
|
interactiveTotal++;
|
|
2044
2371
|
if (el.state.enabled === false) interactiveDisabled++;
|
|
2045
2372
|
}
|
|
@@ -2312,6 +2639,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2312
2639
|
return {
|
|
2313
2640
|
// Elements
|
|
2314
2641
|
getElements: async (ctx) => {
|
|
2642
|
+
await registry.refreshMeasurements();
|
|
2315
2643
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
2316
2644
|
const forRoute = ctx.query?.route || ctx.body?.route;
|
|
2317
2645
|
let allElements = visibleOnly ? registry.getMountedVisibleElements() : registry.getAllElements();
|
|
@@ -2322,6 +2650,16 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2322
2650
|
const handlers = extractHandlerNames(e.props);
|
|
2323
2651
|
const state = e.getState();
|
|
2324
2652
|
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
2653
|
+
const bbox = projectBbox(state, visibility, registry.getPixelRatio());
|
|
2654
|
+
const vision = projectVisionFields({
|
|
2655
|
+
type: e.type,
|
|
2656
|
+
label: e.label,
|
|
2657
|
+
// Real handler names (onPress/...), NOT inferred actions which
|
|
2658
|
+
// synthesize press/click onto every element. See projectVisionFields.
|
|
2659
|
+
handlerNames: handlers,
|
|
2660
|
+
state,
|
|
2661
|
+
flatStyle: e.flatStyle
|
|
2662
|
+
});
|
|
2325
2663
|
return {
|
|
2326
2664
|
id: e.id,
|
|
2327
2665
|
type: e.type,
|
|
@@ -2332,7 +2670,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2332
2670
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
2333
2671
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
2334
2672
|
registrationRoute: e.registrationRoute,
|
|
2335
|
-
visibility
|
|
2673
|
+
visibility,
|
|
2674
|
+
...bbox ? { bbox } : {},
|
|
2675
|
+
...vision
|
|
2336
2676
|
};
|
|
2337
2677
|
});
|
|
2338
2678
|
return success2({ elements });
|
|
@@ -2493,6 +2833,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2493
2833
|
});
|
|
2494
2834
|
},
|
|
2495
2835
|
getSnapshot: async (ctx) => {
|
|
2836
|
+
await registry.refreshMeasurements();
|
|
2496
2837
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
2497
2838
|
const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
|
|
2498
2839
|
const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
|
|
@@ -2808,6 +3149,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2808
3149
|
// that don't measure (the fallback only matches single-rooted screens but
|
|
2809
3150
|
// is good enough for the unit suite).
|
|
2810
3151
|
tapAt: async (ctx) => {
|
|
3152
|
+
await registry.refreshMeasurements();
|
|
2811
3153
|
const body = ctx.body;
|
|
2812
3154
|
const x = body?.x;
|
|
2813
3155
|
const y = body?.y;
|
|
@@ -3015,7 +3357,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3015
3357
|
};
|
|
3016
3358
|
if (config?.appInfo) {
|
|
3017
3359
|
response.uiBridge = {
|
|
3018
|
-
version: "0.6.
|
|
3360
|
+
version: "0.6.12",
|
|
3019
3361
|
...config.appInfo,
|
|
3020
3362
|
capabilities: ["elements", "components", "actions", "design"],
|
|
3021
3363
|
elementCount: stats.elements,
|
|
@@ -3607,6 +3949,14 @@ var NativeUIBridgeServer = class {
|
|
|
3607
3949
|
};
|
|
3608
3950
|
};
|
|
3609
3951
|
}
|
|
3952
|
+
/**
|
|
3953
|
+
* Whether the console/network capture should be installed. The dedicated
|
|
3954
|
+
* `observability` flag wins when set; otherwise falls back to `testHooks`
|
|
3955
|
+
* so consumers predating the flag keep their exact behavior.
|
|
3956
|
+
*/
|
|
3957
|
+
observabilityEnabled() {
|
|
3958
|
+
return this.config.observability ?? this.config.testHooks === true;
|
|
3959
|
+
}
|
|
3610
3960
|
/**
|
|
3611
3961
|
* Set the server adapter
|
|
3612
3962
|
*/
|
|
@@ -3820,7 +4170,7 @@ var NativeUIBridgeServer = class {
|
|
|
3820
4170
|
console.warn("[ui-bridge-native] See documentation for supported adapters.");
|
|
3821
4171
|
return;
|
|
3822
4172
|
}
|
|
3823
|
-
if (this.
|
|
4173
|
+
if (this.observabilityEnabled()) {
|
|
3824
4174
|
this.consoleErrorBuffer.install();
|
|
3825
4175
|
this.networkRequestBuffer.install();
|
|
3826
4176
|
}
|
|
@@ -5322,6 +5672,7 @@ function UIBridgeNativeProvider({
|
|
|
5322
5672
|
verbose: config.verbose,
|
|
5323
5673
|
onEvent
|
|
5324
5674
|
});
|
|
5675
|
+
registryRef.current.setPixelRatio(PixelRatio.get());
|
|
5325
5676
|
setGlobalRegistry(registryRef.current);
|
|
5326
5677
|
}
|
|
5327
5678
|
const registry = registryRef.current;
|
|
@@ -5352,6 +5703,10 @@ function UIBridgeNativeProvider({
|
|
|
5352
5703
|
cors: true,
|
|
5353
5704
|
appInfo: config.appInfo,
|
|
5354
5705
|
testHooks: features.testHooks === true,
|
|
5706
|
+
// Dedicated console/network-capture flag; the server defaults it to
|
|
5707
|
+
// the `testHooks` value when undefined, so passing it through verbatim
|
|
5708
|
+
// keeps existing consumers unchanged.
|
|
5709
|
+
observability: features.observability,
|
|
5355
5710
|
// Injected device-viewport getter for POST /control/page-health. The
|
|
5356
5711
|
// server doesn't import `react-native` itself — the require/import
|
|
5357
5712
|
// pattern there crashed the host RN app in 0.6.3/0.6.4 (Metro/Hermes
|
|
@@ -5419,6 +5774,7 @@ function UIBridgeNativeProvider({
|
|
|
5419
5774
|
}, [
|
|
5420
5775
|
features.server,
|
|
5421
5776
|
features.testHooks,
|
|
5777
|
+
features.observability,
|
|
5422
5778
|
config.serverPort,
|
|
5423
5779
|
config.appInfo,
|
|
5424
5780
|
registry,
|
|
@@ -5457,6 +5813,7 @@ function UIBridgeNativeProvider({
|
|
|
5457
5813
|
cors: true,
|
|
5458
5814
|
appInfo: config.appInfo,
|
|
5459
5815
|
testHooks: features.testHooks === true,
|
|
5816
|
+
observability: features.observability,
|
|
5460
5817
|
viewportProvider: () => {
|
|
5461
5818
|
const win = Dimensions.get("window");
|
|
5462
5819
|
return { width: win.width, height: win.height };
|
|
@@ -5660,12 +6017,41 @@ function scheduleDeadButtonWarning(bridge, id, type) {
|
|
|
5660
6017
|
}
|
|
5661
6018
|
function useUIElement(options) {
|
|
5662
6019
|
const bridge = useUIBridgeNativeOptional();
|
|
5663
|
-
const ref = useRef(null);
|
|
5664
6020
|
const [registered, setRegistered] = useState(false);
|
|
5665
6021
|
const [_layout, setLayout] = useState(null);
|
|
5666
6022
|
const propsRef = useRef({});
|
|
5667
6023
|
const registeredRef = useRef(false);
|
|
5668
6024
|
const registeredIdRef = useRef(null);
|
|
6025
|
+
const bridgeRef = useRef(bridge);
|
|
6026
|
+
bridgeRef.current = bridge;
|
|
6027
|
+
const nodeRef = useRef(null);
|
|
6028
|
+
const refInterceptorRef = useRef(null);
|
|
6029
|
+
if (refInterceptorRef.current === null) {
|
|
6030
|
+
refInterceptorRef.current = {
|
|
6031
|
+
get current() {
|
|
6032
|
+
return nodeRef.current;
|
|
6033
|
+
},
|
|
6034
|
+
set current(node) {
|
|
6035
|
+
const prev = nodeRef.current;
|
|
6036
|
+
nodeRef.current = node;
|
|
6037
|
+
const liveBridge = bridgeRef.current;
|
|
6038
|
+
const registeredId = registeredIdRef.current;
|
|
6039
|
+
if (!liveBridge || !registeredRef.current || registeredId === null) return;
|
|
6040
|
+
if (node == null && prev != null) {
|
|
6041
|
+
const state = liveBridge.registry.getElement(registeredId)?.getState();
|
|
6042
|
+
if (state && state.visible !== false) {
|
|
6043
|
+
liveBridge.registry.updateElementState(registeredId, { visible: false });
|
|
6044
|
+
}
|
|
6045
|
+
} else if (node != null && prev == null) {
|
|
6046
|
+
const state = liveBridge.registry.getElement(registeredId)?.getState();
|
|
6047
|
+
if (state && state.visible !== true) {
|
|
6048
|
+
liveBridge.registry.updateElementState(registeredId, { visible: true });
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
}
|
|
6052
|
+
};
|
|
6053
|
+
}
|
|
6054
|
+
const ref = refInterceptorRef.current;
|
|
5669
6055
|
const {
|
|
5670
6056
|
id,
|
|
5671
6057
|
type = "custom",
|
|
@@ -5709,7 +6095,7 @@ function useUIElement(options) {
|
|
|
5709
6095
|
}
|
|
5710
6096
|
setRegistered(true);
|
|
5711
6097
|
scheduleDeadButtonWarning(bridge, id, type);
|
|
5712
|
-
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
|
|
6098
|
+
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp, ref]);
|
|
5713
6099
|
const unregister = useCallback(() => {
|
|
5714
6100
|
if (!bridge || !registeredRef.current) return;
|
|
5715
6101
|
bridge.registry.unregisterElement(registeredIdRef.current ?? id);
|
|
@@ -5745,7 +6131,8 @@ function useUIElement(options) {
|
|
|
5745
6131
|
writeLayout({ x, y, width, height, pageX: x, pageY: y });
|
|
5746
6132
|
}
|
|
5747
6133
|
},
|
|
5748
|
-
|
|
6134
|
+
// `ref` is the once-per-hook interceptor object — identity-stable.
|
|
6135
|
+
[bridge, id, ref]
|
|
5749
6136
|
);
|
|
5750
6137
|
const registerRef = useRef(register);
|
|
5751
6138
|
const unregisterRef = useRef(unregister);
|
|
@@ -5762,11 +6149,18 @@ function useUIElement(options) {
|
|
|
5762
6149
|
unregisterRef.current();
|
|
5763
6150
|
}
|
|
5764
6151
|
};
|
|
5765
|
-
}, [autoRegister, bridge]);
|
|
6152
|
+
}, [autoRegister, bridge, id]);
|
|
5766
6153
|
useEffect(() => {
|
|
5767
6154
|
if (!bridge || !registered) return;
|
|
5768
6155
|
const node = ref.current;
|
|
5769
|
-
if (!node
|
|
6156
|
+
if (!node) {
|
|
6157
|
+
const seeded = bridge.registry.getElement(id)?.getState();
|
|
6158
|
+
if (seeded && seeded.visible !== false) {
|
|
6159
|
+
bridge.registry.updateElementState(id, { visible: false });
|
|
6160
|
+
}
|
|
6161
|
+
return;
|
|
6162
|
+
}
|
|
6163
|
+
if (typeof node.measureInWindow !== "function") return;
|
|
5770
6164
|
let cancelled = false;
|
|
5771
6165
|
let pendingTimer = null;
|
|
5772
6166
|
const backoffMs = [50, 200, 500, 1e3, 2500];
|
|
@@ -5805,7 +6199,7 @@ function useUIElement(options) {
|
|
|
5805
6199
|
cancelled = true;
|
|
5806
6200
|
if (pendingTimer != null) clearTimeout(pendingTimer);
|
|
5807
6201
|
};
|
|
5808
|
-
}, [bridge, registered, id]);
|
|
6202
|
+
}, [bridge, registered, id, ref]);
|
|
5809
6203
|
const registeredHandlerKeysRef = useRef([]);
|
|
5810
6204
|
useEffect(() => {
|
|
5811
6205
|
if (!bridge || !registered || !handlersProp) return;
|