@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/index.js
CHANGED
|
@@ -11,6 +11,179 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
11
11
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
// src/core/vision-fields.ts
|
|
15
|
+
var INTERACTIVE_HANDLER_NAMES = /* @__PURE__ */ new Set([
|
|
16
|
+
"onPress",
|
|
17
|
+
"onPressIn",
|
|
18
|
+
"onPressOut",
|
|
19
|
+
"onLongPress",
|
|
20
|
+
"onChangeText",
|
|
21
|
+
"onValueChange",
|
|
22
|
+
"onChange",
|
|
23
|
+
"onSubmitEditing",
|
|
24
|
+
"onClick",
|
|
25
|
+
"onTouchStart",
|
|
26
|
+
"onTouchEnd"
|
|
27
|
+
]);
|
|
28
|
+
var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
29
|
+
"button",
|
|
30
|
+
"input",
|
|
31
|
+
"switch",
|
|
32
|
+
"checkbox",
|
|
33
|
+
"radio",
|
|
34
|
+
"touchable",
|
|
35
|
+
"pressable",
|
|
36
|
+
"listItem"
|
|
37
|
+
]);
|
|
38
|
+
function roleForType(type) {
|
|
39
|
+
switch (type) {
|
|
40
|
+
case "button":
|
|
41
|
+
case "touchable":
|
|
42
|
+
case "pressable":
|
|
43
|
+
return "button";
|
|
44
|
+
case "input":
|
|
45
|
+
return "textbox";
|
|
46
|
+
case "text":
|
|
47
|
+
return "text";
|
|
48
|
+
case "switch":
|
|
49
|
+
return "switch";
|
|
50
|
+
case "checkbox":
|
|
51
|
+
return "checkbox";
|
|
52
|
+
case "radio":
|
|
53
|
+
return "radio";
|
|
54
|
+
case "image":
|
|
55
|
+
return "img";
|
|
56
|
+
case "list":
|
|
57
|
+
return "list";
|
|
58
|
+
case "listItem":
|
|
59
|
+
return "listitem";
|
|
60
|
+
case "modal":
|
|
61
|
+
return "dialog";
|
|
62
|
+
case "scroll":
|
|
63
|
+
case "view":
|
|
64
|
+
case "custom":
|
|
65
|
+
default:
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function deriveInteractable(type, handlerNames) {
|
|
70
|
+
if (INTERACTIVE_TYPES.has(type)) return true;
|
|
71
|
+
if (handlerNames) {
|
|
72
|
+
for (const name of handlerNames) {
|
|
73
|
+
if (INTERACTIVE_HANDLER_NAMES.has(name)) return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
function deriveText(state, label) {
|
|
79
|
+
const candidates = [state.value, state.textContent, label];
|
|
80
|
+
for (const c of candidates) {
|
|
81
|
+
if (typeof c === "string") {
|
|
82
|
+
const trimmed = c.trim();
|
|
83
|
+
if (trimmed.length > 0) return trimmed;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
var NAMED_COLORS = {
|
|
89
|
+
black: { r: 0, g: 0, b: 0 },
|
|
90
|
+
white: { r: 255, g: 255, b: 255 },
|
|
91
|
+
red: { r: 255, g: 0, b: 0 },
|
|
92
|
+
green: { r: 0, g: 128, b: 0 },
|
|
93
|
+
blue: { r: 0, g: 0, b: 255 },
|
|
94
|
+
gray: { r: 128, g: 128, b: 128 },
|
|
95
|
+
grey: { r: 128, g: 128, b: 128 },
|
|
96
|
+
silver: { r: 192, g: 192, b: 192 },
|
|
97
|
+
transparent: { r: 0, g: 0, b: 0 },
|
|
98
|
+
orange: { r: 255, g: 165, b: 0 },
|
|
99
|
+
yellow: { r: 255, g: 255, b: 0 },
|
|
100
|
+
purple: { r: 128, g: 0, b: 128 },
|
|
101
|
+
navy: { r: 0, g: 0, b: 128 }
|
|
102
|
+
};
|
|
103
|
+
var clamp255 = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
104
|
+
function parseColorToRgb(input) {
|
|
105
|
+
if (typeof input !== "string") return void 0;
|
|
106
|
+
const raw = input.trim().toLowerCase();
|
|
107
|
+
if (raw.length === 0) return void 0;
|
|
108
|
+
if (Object.prototype.hasOwnProperty.call(NAMED_COLORS, raw)) {
|
|
109
|
+
return NAMED_COLORS[raw];
|
|
110
|
+
}
|
|
111
|
+
if (raw.startsWith("#")) {
|
|
112
|
+
const hex = raw.slice(1);
|
|
113
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
114
|
+
const r = parseInt(hex[0] + hex[0], 16);
|
|
115
|
+
const g = parseInt(hex[1] + hex[1], 16);
|
|
116
|
+
const b = parseInt(hex[2] + hex[2], 16);
|
|
117
|
+
if ([r, g, b].every((v) => Number.isFinite(v))) return { r, g, b };
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
121
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
122
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
123
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
124
|
+
if ([r, g, b].every((v) => Number.isFinite(v))) return { r, g, b };
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
return void 0;
|
|
128
|
+
}
|
|
129
|
+
const rgbMatch = raw.match(/^rgba?\(([^)]+)\)$/);
|
|
130
|
+
if (rgbMatch) {
|
|
131
|
+
const parts = rgbMatch[1].split(",").map((p) => p.trim());
|
|
132
|
+
if (parts.length < 3) return void 0;
|
|
133
|
+
const channel = (p) => {
|
|
134
|
+
if (p.endsWith("%")) {
|
|
135
|
+
const pct = parseFloat(p.slice(0, -1));
|
|
136
|
+
if (!Number.isFinite(pct)) return void 0;
|
|
137
|
+
return clamp255(pct / 100 * 255);
|
|
138
|
+
}
|
|
139
|
+
const n = parseFloat(p);
|
|
140
|
+
if (!Number.isFinite(n)) return void 0;
|
|
141
|
+
return clamp255(n);
|
|
142
|
+
};
|
|
143
|
+
const r = channel(parts[0]);
|
|
144
|
+
const g = channel(parts[1]);
|
|
145
|
+
const b = channel(parts[2]);
|
|
146
|
+
if (r === void 0 || g === void 0 || b === void 0) return void 0;
|
|
147
|
+
return { r, g, b };
|
|
148
|
+
}
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
function parseFontSizePx(input) {
|
|
152
|
+
if (typeof input === "number") {
|
|
153
|
+
return Number.isFinite(input) && input > 0 ? input : void 0;
|
|
154
|
+
}
|
|
155
|
+
if (typeof input === "string") {
|
|
156
|
+
const m = input.trim().match(/^(-?\d*\.?\d+)/);
|
|
157
|
+
if (m) {
|
|
158
|
+
const n = parseFloat(m[1]);
|
|
159
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
function projectVisionFields(args) {
|
|
165
|
+
const { type, label, handlerNames, state, flatStyle } = args;
|
|
166
|
+
const fields = {
|
|
167
|
+
interactable: deriveInteractable(type, handlerNames)
|
|
168
|
+
};
|
|
169
|
+
const text = deriveText(state, label);
|
|
170
|
+
if (text !== void 0) fields.text = text;
|
|
171
|
+
const role = roleForType(type);
|
|
172
|
+
if (role !== void 0) fields.role = role;
|
|
173
|
+
if (flatStyle) {
|
|
174
|
+
const fg = parseColorToRgb(flatStyle.color);
|
|
175
|
+
if (fg !== void 0) fields.fg_color = fg;
|
|
176
|
+
const bg = parseColorToRgb(flatStyle.backgroundColor);
|
|
177
|
+
if (bg !== void 0) fields.bg_color = bg;
|
|
178
|
+
const size = parseFontSizePx(flatStyle.fontSize);
|
|
179
|
+
if (size !== void 0) fields.font_size_px = size;
|
|
180
|
+
if (typeof flatStyle.fontFamily === "string" && flatStyle.fontFamily.trim().length > 0) {
|
|
181
|
+
fields.font_family = flatStyle.fontFamily;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return fields;
|
|
185
|
+
}
|
|
186
|
+
|
|
14
187
|
// src/core/registry.ts
|
|
15
188
|
function extractHandlerNames(props) {
|
|
16
189
|
if (!props) return [];
|
|
@@ -20,6 +193,25 @@ function matchesCurrentRoute(registrationRoute, currentRoute) {
|
|
|
20
193
|
if (registrationRoute == null || registrationRoute === "") return true;
|
|
21
194
|
return registrationRoute === currentRoute;
|
|
22
195
|
}
|
|
196
|
+
function projectBbox(state, visibility, pixelRatio = 1) {
|
|
197
|
+
if (visibility !== "visible") return void 0;
|
|
198
|
+
const layout = state.layout;
|
|
199
|
+
if (!layout) return void 0;
|
|
200
|
+
const ratio = pixelRatio > 0 && Number.isFinite(pixelRatio) ? pixelRatio : 1;
|
|
201
|
+
const toPx = (v) => Math.max(0, Math.round(v * ratio));
|
|
202
|
+
const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
|
|
203
|
+
const originX = hasPage ? layout.pageX : layout.x;
|
|
204
|
+
const originY = hasPage ? layout.pageY : layout.y;
|
|
205
|
+
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)) {
|
|
206
|
+
return void 0;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
x: toPx(originX),
|
|
210
|
+
y: toPx(originY),
|
|
211
|
+
w: toPx(layout.width),
|
|
212
|
+
h: toPx(layout.height)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
23
215
|
function inferActions(type) {
|
|
24
216
|
const baseActions = ["focus", "blur"];
|
|
25
217
|
switch (type) {
|
|
@@ -84,6 +276,15 @@ var NativeUIBridgeRegistry = class {
|
|
|
84
276
|
* already installs.
|
|
85
277
|
*/
|
|
86
278
|
this.routeProvider = null;
|
|
279
|
+
/**
|
|
280
|
+
* Device pixel ratio used to project `state.layout` (logical dp) into the
|
|
281
|
+
* physical-pixel `bbox` emitted on each snapshot element. Defaults to `1`
|
|
282
|
+
* (dp ≈ px) so callers/tests that never inject a ratio still get a usable
|
|
283
|
+
* box; the React provider wires the real `PixelRatio.get()` via
|
|
284
|
+
* {@link setPixelRatio}. Read from a non-React core module on purpose — see
|
|
285
|
+
* the `projectBbox` doc comment for why we don't import `react-native` here.
|
|
286
|
+
*/
|
|
287
|
+
this.pixelRatio = 1;
|
|
87
288
|
/**
|
|
88
289
|
* Sticky flag: flips `true` the first time any element is registered and
|
|
89
290
|
* stays `true` even after elements are unregistered. Lets agents distinguish
|
|
@@ -121,6 +322,21 @@ var NativeUIBridgeRegistry = class {
|
|
|
121
322
|
getRouteProvider() {
|
|
122
323
|
return this.routeProvider;
|
|
123
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Set the device pixel ratio used to project per-element `bbox` geometry
|
|
327
|
+
* into physical pixels. The React provider passes `PixelRatio.get()` here
|
|
328
|
+
* once at startup. Ignores non-finite / non-positive values (keeps the
|
|
329
|
+
* previous ratio) so a bad call can't zero out every bbox.
|
|
330
|
+
*/
|
|
331
|
+
setPixelRatio(ratio) {
|
|
332
|
+
if (typeof ratio === "number" && Number.isFinite(ratio) && ratio > 0) {
|
|
333
|
+
this.pixelRatio = ratio;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** Read-only accessor for the device pixel ratio used for bbox projection. */
|
|
337
|
+
getPixelRatio() {
|
|
338
|
+
return this.pixelRatio;
|
|
339
|
+
}
|
|
124
340
|
// ============================================================================
|
|
125
341
|
// Snapshot Enricher Slots
|
|
126
342
|
// ============================================================================
|
|
@@ -346,6 +562,103 @@ var NativeUIBridgeRegistry = class {
|
|
|
346
562
|
console.log(`[ui-bridge-native] Marked ${cleared} elements offscreen for route: ${route}`);
|
|
347
563
|
}
|
|
348
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Re-measure every mounted-visible element's geometry via its stored React
|
|
567
|
+
* ref and write the fresh coordinates into the registry ("measure-on-
|
|
568
|
+
* snapshot").
|
|
569
|
+
*
|
|
570
|
+
* Why: element geometry is otherwise captured ONCE at mount time
|
|
571
|
+
* (`onLayout` + `measureInWindow` in `useUIElement`) and RN never re-fires
|
|
572
|
+
* `onLayout` on pure translation — scrolling a virtualized list
|
|
573
|
+
* (FlashList), recycling a cell, a keyboard shift or an orientation change
|
|
574
|
+
* all move elements WITHOUT updating the stored `state.layout`. Snapshot
|
|
575
|
+
* consumers (vision analyzers hit-testing `bbox`, coord-based tap) then
|
|
576
|
+
* operate on stale positions. Re-measuring at READ time is correct
|
|
577
|
+
* regardless of what moved the element and needs zero per-consumer wiring:
|
|
578
|
+
* every registry entry already stores its ref (see `registerElement`).
|
|
579
|
+
*
|
|
580
|
+
* Per-element outcomes:
|
|
581
|
+
* - non-zero dims → write a fresh full layout (same state shape as the
|
|
582
|
+
* mount-time measure loop in `useUIElement`); counted as `measured`.
|
|
583
|
+
* - zero dims AND the element previously had a non-null `state.layout` →
|
|
584
|
+
* the view collapsed / left the screen; clear to
|
|
585
|
+
* `{ visible: false, layout: null }` (same shape `markRouteOffscreen`
|
|
586
|
+
* writes); counted as `cleared`.
|
|
587
|
+
* - zero dims and it never had a layout → leave state untouched (element
|
|
588
|
+
* in the mount→first-onLayout gap; don't demote `likely-visible`);
|
|
589
|
+
* counted as `skipped`.
|
|
590
|
+
* - no ref / no callable `measureInWindow` (test fixtures, web) → leave
|
|
591
|
+
* untouched; counted as `skipped`.
|
|
592
|
+
*
|
|
593
|
+
* The whole sweep races an overall timeout (default 250 ms) so a dead ref
|
|
594
|
+
* whose `measureInWindow` never calls back can never hang a snapshot: the
|
|
595
|
+
* returned promise resolves at the deadline with whatever counts have
|
|
596
|
+
* accumulated. Late callbacks may still write state afterwards — harmless,
|
|
597
|
+
* freshest data wins. Never throws.
|
|
598
|
+
*
|
|
599
|
+
* Non-React core module on purpose — `measureInWindow` is duck-typed off
|
|
600
|
+
* the stored ref rather than imported from `react-native` (see the
|
|
601
|
+
* `projectBbox` doc comment for why this file must not import RN).
|
|
602
|
+
*/
|
|
603
|
+
async refreshMeasurements(options) {
|
|
604
|
+
const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 250;
|
|
605
|
+
const counts = { measured: 0, cleared: 0, skipped: 0 };
|
|
606
|
+
const pending = [];
|
|
607
|
+
for (const element of this.getMountedVisibleElements()) {
|
|
608
|
+
const node = element.ref?.current;
|
|
609
|
+
if (!node || typeof node.measureInWindow !== "function") {
|
|
610
|
+
counts.skipped++;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
const id = element.id;
|
|
614
|
+
pending.push(
|
|
615
|
+
new Promise((resolve) => {
|
|
616
|
+
let settled = false;
|
|
617
|
+
try {
|
|
618
|
+
node.measureInWindow((pageX, pageY, w, h) => {
|
|
619
|
+
if (settled) return;
|
|
620
|
+
settled = true;
|
|
621
|
+
try {
|
|
622
|
+
if (w > 0 && h > 0) {
|
|
623
|
+
this.updateElementState(id, {
|
|
624
|
+
mounted: true,
|
|
625
|
+
visible: true,
|
|
626
|
+
enabled: true,
|
|
627
|
+
focused: false,
|
|
628
|
+
layout: { x: pageX, y: pageY, width: w, height: h, pageX, pageY }
|
|
629
|
+
});
|
|
630
|
+
counts.measured++;
|
|
631
|
+
} else if (this.elements.get(id)?.getState().layout != null) {
|
|
632
|
+
this.updateElementState(id, { visible: false, layout: null });
|
|
633
|
+
counts.cleared++;
|
|
634
|
+
} else {
|
|
635
|
+
counts.skipped++;
|
|
636
|
+
}
|
|
637
|
+
} finally {
|
|
638
|
+
resolve();
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
} catch {
|
|
642
|
+
if (!settled) {
|
|
643
|
+
settled = true;
|
|
644
|
+
counts.skipped++;
|
|
645
|
+
resolve();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
})
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
if (pending.length > 0) {
|
|
652
|
+
await new Promise((resolve) => {
|
|
653
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
654
|
+
void Promise.all(pending).then(() => {
|
|
655
|
+
clearTimeout(timer);
|
|
656
|
+
resolve();
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
return counts;
|
|
661
|
+
}
|
|
349
662
|
/**
|
|
350
663
|
* Update element state
|
|
351
664
|
*/
|
|
@@ -660,6 +973,18 @@ var NativeUIBridgeRegistry = class {
|
|
|
660
973
|
const handlers = extractHandlerNames(e.props);
|
|
661
974
|
const state = e.getState();
|
|
662
975
|
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
976
|
+
const bbox = projectBbox(state, visibility, this.pixelRatio);
|
|
977
|
+
const vision = projectVisionFields({
|
|
978
|
+
type: e.type,
|
|
979
|
+
label: e.label,
|
|
980
|
+
// Real interactivity signal: the names of props that are actually
|
|
981
|
+
// functions (onPress/onChangeText/...). NOT `e.actions`, which
|
|
982
|
+
// `inferActions` synthesizes press/click onto for every element —
|
|
983
|
+
// using it would mark plain text/view nodes interactable.
|
|
984
|
+
handlerNames: handlers,
|
|
985
|
+
state,
|
|
986
|
+
flatStyle: e.flatStyle
|
|
987
|
+
});
|
|
663
988
|
return {
|
|
664
989
|
id: e.id,
|
|
665
990
|
type: e.type,
|
|
@@ -670,7 +995,9 @@ var NativeUIBridgeRegistry = class {
|
|
|
670
995
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
671
996
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
672
997
|
registrationRoute: e.registrationRoute,
|
|
673
|
-
visibility
|
|
998
|
+
visibility,
|
|
999
|
+
...bbox ? { bbox } : {},
|
|
1000
|
+
...vision
|
|
674
1001
|
};
|
|
675
1002
|
}),
|
|
676
1003
|
components: this.getAllComponents().map((c) => ({
|
|
@@ -1896,7 +2223,7 @@ function createDesignHandlers(registry, viewportProvider) {
|
|
|
1896
2223
|
var GRID_SIZE = 20;
|
|
1897
2224
|
var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
|
|
1898
2225
|
var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
|
|
1899
|
-
var
|
|
2226
|
+
var INTERACTIVE_TYPES2 = /* @__PURE__ */ new Set([
|
|
1900
2227
|
"button",
|
|
1901
2228
|
"input",
|
|
1902
2229
|
"switch",
|
|
@@ -2093,7 +2420,7 @@ function diagnosePageHealth(elements, viewport) {
|
|
|
2093
2420
|
let interactiveTotal = 0;
|
|
2094
2421
|
let interactiveDisabled = 0;
|
|
2095
2422
|
for (const el of elements) {
|
|
2096
|
-
if (!
|
|
2423
|
+
if (!INTERACTIVE_TYPES2.has(el.type)) continue;
|
|
2097
2424
|
interactiveTotal++;
|
|
2098
2425
|
if (el.state.enabled === false) interactiveDisabled++;
|
|
2099
2426
|
}
|
|
@@ -2366,6 +2693,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2366
2693
|
return {
|
|
2367
2694
|
// Elements
|
|
2368
2695
|
getElements: async (ctx) => {
|
|
2696
|
+
await registry.refreshMeasurements();
|
|
2369
2697
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
2370
2698
|
const forRoute = ctx.query?.route || ctx.body?.route;
|
|
2371
2699
|
let allElements = visibleOnly ? registry.getMountedVisibleElements() : registry.getAllElements();
|
|
@@ -2376,6 +2704,16 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2376
2704
|
const handlers = extractHandlerNames(e.props);
|
|
2377
2705
|
const state = e.getState();
|
|
2378
2706
|
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
2707
|
+
const bbox = projectBbox(state, visibility, registry.getPixelRatio());
|
|
2708
|
+
const vision = projectVisionFields({
|
|
2709
|
+
type: e.type,
|
|
2710
|
+
label: e.label,
|
|
2711
|
+
// Real handler names (onPress/...), NOT inferred actions which
|
|
2712
|
+
// synthesize press/click onto every element. See projectVisionFields.
|
|
2713
|
+
handlerNames: handlers,
|
|
2714
|
+
state,
|
|
2715
|
+
flatStyle: e.flatStyle
|
|
2716
|
+
});
|
|
2379
2717
|
return {
|
|
2380
2718
|
id: e.id,
|
|
2381
2719
|
type: e.type,
|
|
@@ -2386,7 +2724,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2386
2724
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
2387
2725
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
2388
2726
|
registrationRoute: e.registrationRoute,
|
|
2389
|
-
visibility
|
|
2727
|
+
visibility,
|
|
2728
|
+
...bbox ? { bbox } : {},
|
|
2729
|
+
...vision
|
|
2390
2730
|
};
|
|
2391
2731
|
});
|
|
2392
2732
|
return success2({ elements });
|
|
@@ -2547,6 +2887,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2547
2887
|
});
|
|
2548
2888
|
},
|
|
2549
2889
|
getSnapshot: async (ctx) => {
|
|
2890
|
+
await registry.refreshMeasurements();
|
|
2550
2891
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
2551
2892
|
const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
|
|
2552
2893
|
const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
|
|
@@ -2862,6 +3203,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2862
3203
|
// that don't measure (the fallback only matches single-rooted screens but
|
|
2863
3204
|
// is good enough for the unit suite).
|
|
2864
3205
|
tapAt: async (ctx) => {
|
|
3206
|
+
await registry.refreshMeasurements();
|
|
2865
3207
|
const body = ctx.body;
|
|
2866
3208
|
const x = body?.x;
|
|
2867
3209
|
const y = body?.y;
|
|
@@ -3069,7 +3411,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3069
3411
|
};
|
|
3070
3412
|
if (config?.appInfo) {
|
|
3071
3413
|
response.uiBridge = {
|
|
3072
|
-
version: "0.6.
|
|
3414
|
+
version: "0.6.12",
|
|
3073
3415
|
...config.appInfo,
|
|
3074
3416
|
capabilities: ["elements", "components", "actions", "design"],
|
|
3075
3417
|
elementCount: stats.elements,
|
|
@@ -3661,6 +4003,14 @@ var NativeUIBridgeServer = class {
|
|
|
3661
4003
|
};
|
|
3662
4004
|
};
|
|
3663
4005
|
}
|
|
4006
|
+
/**
|
|
4007
|
+
* Whether the console/network capture should be installed. The dedicated
|
|
4008
|
+
* `observability` flag wins when set; otherwise falls back to `testHooks`
|
|
4009
|
+
* so consumers predating the flag keep their exact behavior.
|
|
4010
|
+
*/
|
|
4011
|
+
observabilityEnabled() {
|
|
4012
|
+
return this.config.observability ?? this.config.testHooks === true;
|
|
4013
|
+
}
|
|
3664
4014
|
/**
|
|
3665
4015
|
* Set the server adapter
|
|
3666
4016
|
*/
|
|
@@ -3874,7 +4224,7 @@ var NativeUIBridgeServer = class {
|
|
|
3874
4224
|
console.warn("[ui-bridge-native] See documentation for supported adapters.");
|
|
3875
4225
|
return;
|
|
3876
4226
|
}
|
|
3877
|
-
if (this.
|
|
4227
|
+
if (this.observabilityEnabled()) {
|
|
3878
4228
|
this.consoleErrorBuffer.install();
|
|
3879
4229
|
this.networkRequestBuffer.install();
|
|
3880
4230
|
}
|
|
@@ -5376,6 +5726,7 @@ function UIBridgeNativeProvider({
|
|
|
5376
5726
|
verbose: config.verbose,
|
|
5377
5727
|
onEvent
|
|
5378
5728
|
});
|
|
5729
|
+
registryRef.current.setPixelRatio(reactNative.PixelRatio.get());
|
|
5379
5730
|
setGlobalRegistry(registryRef.current);
|
|
5380
5731
|
}
|
|
5381
5732
|
const registry = registryRef.current;
|
|
@@ -5406,6 +5757,10 @@ function UIBridgeNativeProvider({
|
|
|
5406
5757
|
cors: true,
|
|
5407
5758
|
appInfo: config.appInfo,
|
|
5408
5759
|
testHooks: features.testHooks === true,
|
|
5760
|
+
// Dedicated console/network-capture flag; the server defaults it to
|
|
5761
|
+
// the `testHooks` value when undefined, so passing it through verbatim
|
|
5762
|
+
// keeps existing consumers unchanged.
|
|
5763
|
+
observability: features.observability,
|
|
5409
5764
|
// Injected device-viewport getter for POST /control/page-health. The
|
|
5410
5765
|
// server doesn't import `react-native` itself — the require/import
|
|
5411
5766
|
// pattern there crashed the host RN app in 0.6.3/0.6.4 (Metro/Hermes
|
|
@@ -5473,6 +5828,7 @@ function UIBridgeNativeProvider({
|
|
|
5473
5828
|
}, [
|
|
5474
5829
|
features.server,
|
|
5475
5830
|
features.testHooks,
|
|
5831
|
+
features.observability,
|
|
5476
5832
|
config.serverPort,
|
|
5477
5833
|
config.appInfo,
|
|
5478
5834
|
registry,
|
|
@@ -5511,6 +5867,7 @@ function UIBridgeNativeProvider({
|
|
|
5511
5867
|
cors: true,
|
|
5512
5868
|
appInfo: config.appInfo,
|
|
5513
5869
|
testHooks: features.testHooks === true,
|
|
5870
|
+
observability: features.observability,
|
|
5514
5871
|
viewportProvider: () => {
|
|
5515
5872
|
const win = reactNative.Dimensions.get("window");
|
|
5516
5873
|
return { width: win.width, height: win.height };
|
|
@@ -5714,12 +6071,41 @@ function scheduleDeadButtonWarning(bridge, id, type) {
|
|
|
5714
6071
|
}
|
|
5715
6072
|
function useUIElement(options) {
|
|
5716
6073
|
const bridge = useUIBridgeNativeOptional();
|
|
5717
|
-
const ref = react.useRef(null);
|
|
5718
6074
|
const [registered, setRegistered] = react.useState(false);
|
|
5719
6075
|
const [_layout, setLayout] = react.useState(null);
|
|
5720
6076
|
const propsRef = react.useRef({});
|
|
5721
6077
|
const registeredRef = react.useRef(false);
|
|
5722
6078
|
const registeredIdRef = react.useRef(null);
|
|
6079
|
+
const bridgeRef = react.useRef(bridge);
|
|
6080
|
+
bridgeRef.current = bridge;
|
|
6081
|
+
const nodeRef = react.useRef(null);
|
|
6082
|
+
const refInterceptorRef = react.useRef(null);
|
|
6083
|
+
if (refInterceptorRef.current === null) {
|
|
6084
|
+
refInterceptorRef.current = {
|
|
6085
|
+
get current() {
|
|
6086
|
+
return nodeRef.current;
|
|
6087
|
+
},
|
|
6088
|
+
set current(node) {
|
|
6089
|
+
const prev = nodeRef.current;
|
|
6090
|
+
nodeRef.current = node;
|
|
6091
|
+
const liveBridge = bridgeRef.current;
|
|
6092
|
+
const registeredId = registeredIdRef.current;
|
|
6093
|
+
if (!liveBridge || !registeredRef.current || registeredId === null) return;
|
|
6094
|
+
if (node == null && prev != null) {
|
|
6095
|
+
const state = liveBridge.registry.getElement(registeredId)?.getState();
|
|
6096
|
+
if (state && state.visible !== false) {
|
|
6097
|
+
liveBridge.registry.updateElementState(registeredId, { visible: false });
|
|
6098
|
+
}
|
|
6099
|
+
} else if (node != null && prev == null) {
|
|
6100
|
+
const state = liveBridge.registry.getElement(registeredId)?.getState();
|
|
6101
|
+
if (state && state.visible !== true) {
|
|
6102
|
+
liveBridge.registry.updateElementState(registeredId, { visible: true });
|
|
6103
|
+
}
|
|
6104
|
+
}
|
|
6105
|
+
}
|
|
6106
|
+
};
|
|
6107
|
+
}
|
|
6108
|
+
const ref = refInterceptorRef.current;
|
|
5723
6109
|
const {
|
|
5724
6110
|
id,
|
|
5725
6111
|
type = "custom",
|
|
@@ -5763,7 +6149,7 @@ function useUIElement(options) {
|
|
|
5763
6149
|
}
|
|
5764
6150
|
setRegistered(true);
|
|
5765
6151
|
scheduleDeadButtonWarning(bridge, id, type);
|
|
5766
|
-
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
|
|
6152
|
+
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp, ref]);
|
|
5767
6153
|
const unregister = react.useCallback(() => {
|
|
5768
6154
|
if (!bridge || !registeredRef.current) return;
|
|
5769
6155
|
bridge.registry.unregisterElement(registeredIdRef.current ?? id);
|
|
@@ -5799,7 +6185,8 @@ function useUIElement(options) {
|
|
|
5799
6185
|
writeLayout({ x, y, width, height, pageX: x, pageY: y });
|
|
5800
6186
|
}
|
|
5801
6187
|
},
|
|
5802
|
-
|
|
6188
|
+
// `ref` is the once-per-hook interceptor object — identity-stable.
|
|
6189
|
+
[bridge, id, ref]
|
|
5803
6190
|
);
|
|
5804
6191
|
const registerRef = react.useRef(register);
|
|
5805
6192
|
const unregisterRef = react.useRef(unregister);
|
|
@@ -5816,11 +6203,18 @@ function useUIElement(options) {
|
|
|
5816
6203
|
unregisterRef.current();
|
|
5817
6204
|
}
|
|
5818
6205
|
};
|
|
5819
|
-
}, [autoRegister, bridge]);
|
|
6206
|
+
}, [autoRegister, bridge, id]);
|
|
5820
6207
|
react.useEffect(() => {
|
|
5821
6208
|
if (!bridge || !registered) return;
|
|
5822
6209
|
const node = ref.current;
|
|
5823
|
-
if (!node
|
|
6210
|
+
if (!node) {
|
|
6211
|
+
const seeded = bridge.registry.getElement(id)?.getState();
|
|
6212
|
+
if (seeded && seeded.visible !== false) {
|
|
6213
|
+
bridge.registry.updateElementState(id, { visible: false });
|
|
6214
|
+
}
|
|
6215
|
+
return;
|
|
6216
|
+
}
|
|
6217
|
+
if (typeof node.measureInWindow !== "function") return;
|
|
5824
6218
|
let cancelled = false;
|
|
5825
6219
|
let pendingTimer = null;
|
|
5826
6220
|
const backoffMs = [50, 200, 500, 1e3, 2500];
|
|
@@ -5859,7 +6253,7 @@ function useUIElement(options) {
|
|
|
5859
6253
|
cancelled = true;
|
|
5860
6254
|
if (pendingTimer != null) clearTimeout(pendingTimer);
|
|
5861
6255
|
};
|
|
5862
|
-
}, [bridge, registered, id]);
|
|
6256
|
+
}, [bridge, registered, id, ref]);
|
|
5863
6257
|
const registeredHandlerKeysRef = react.useRef([]);
|
|
5864
6258
|
react.useEffect(() => {
|
|
5865
6259
|
if (!bridge || !registered || !handlersProp) return;
|
|
@@ -7273,6 +7667,8 @@ exports.createNativeActionExecutor = createNativeActionExecutor;
|
|
|
7273
7667
|
exports.createNativeElementIdentifier = createNativeElementIdentifier;
|
|
7274
7668
|
exports.createNativeServer = createNativeServer;
|
|
7275
7669
|
exports.createServerHandlers = createServerHandlers;
|
|
7670
|
+
exports.deriveInteractable = deriveInteractable;
|
|
7671
|
+
exports.deriveText = deriveText;
|
|
7276
7672
|
exports.extractHandlerNames = extractHandlerNames;
|
|
7277
7673
|
exports.findAllByPattern = findAllByPattern;
|
|
7278
7674
|
exports.findElementByIdentifier = findElementByIdentifier;
|
|
@@ -7281,8 +7677,12 @@ exports.getNativeElementDesignData = getNativeElementDesignData;
|
|
|
7281
7677
|
exports.isAddrInUseError = isAddrInUseError;
|
|
7282
7678
|
exports.mapNativeStyleToExtended = mapNativeStyleToExtended;
|
|
7283
7679
|
exports.matchesIdentifier = matchesIdentifier;
|
|
7680
|
+
exports.parseColorToRgb = parseColorToRgb;
|
|
7681
|
+
exports.parseFontSizePx = parseFontSizePx;
|
|
7284
7682
|
exports.parseTreePath = parseTreePath;
|
|
7683
|
+
exports.projectVisionFields = projectVisionFields;
|
|
7285
7684
|
exports.resetGlobalRegistry = resetGlobalRegistry;
|
|
7685
|
+
exports.roleForType = roleForType;
|
|
7286
7686
|
exports.setGlobalRegistry = setGlobalRegistry;
|
|
7287
7687
|
exports.useToastRecorder = useToastRecorder;
|
|
7288
7688
|
exports.useUIBridge = useUIBridge;
|