@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.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useRef, useState, useCallback, useEffect, useMemo, use } from 'react';
|
|
2
|
-
import { Dimensions, StyleSheet, Platform, TouchableOpacity, Text, Modal, View, ScrollView } from 'react-native';
|
|
2
|
+
import { Dimensions, StyleSheet, Platform, PixelRatio, TouchableOpacity, Text, Modal, View, ScrollView } from 'react-native';
|
|
3
3
|
import { jsx, Fragment, jsxs } 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) => ({
|
|
@@ -1894,7 +2221,7 @@ function createDesignHandlers(registry, viewportProvider) {
|
|
|
1894
2221
|
var GRID_SIZE = 20;
|
|
1895
2222
|
var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
|
|
1896
2223
|
var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
|
|
1897
|
-
var
|
|
2224
|
+
var INTERACTIVE_TYPES2 = /* @__PURE__ */ new Set([
|
|
1898
2225
|
"button",
|
|
1899
2226
|
"input",
|
|
1900
2227
|
"switch",
|
|
@@ -2091,7 +2418,7 @@ function diagnosePageHealth(elements, viewport) {
|
|
|
2091
2418
|
let interactiveTotal = 0;
|
|
2092
2419
|
let interactiveDisabled = 0;
|
|
2093
2420
|
for (const el of elements) {
|
|
2094
|
-
if (!
|
|
2421
|
+
if (!INTERACTIVE_TYPES2.has(el.type)) continue;
|
|
2095
2422
|
interactiveTotal++;
|
|
2096
2423
|
if (el.state.enabled === false) interactiveDisabled++;
|
|
2097
2424
|
}
|
|
@@ -2364,6 +2691,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2364
2691
|
return {
|
|
2365
2692
|
// Elements
|
|
2366
2693
|
getElements: async (ctx) => {
|
|
2694
|
+
await registry.refreshMeasurements();
|
|
2367
2695
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
2368
2696
|
const forRoute = ctx.query?.route || ctx.body?.route;
|
|
2369
2697
|
let allElements = visibleOnly ? registry.getMountedVisibleElements() : registry.getAllElements();
|
|
@@ -2374,6 +2702,16 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2374
2702
|
const handlers = extractHandlerNames(e.props);
|
|
2375
2703
|
const state = e.getState();
|
|
2376
2704
|
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
2705
|
+
const bbox = projectBbox(state, visibility, registry.getPixelRatio());
|
|
2706
|
+
const vision = projectVisionFields({
|
|
2707
|
+
type: e.type,
|
|
2708
|
+
label: e.label,
|
|
2709
|
+
// Real handler names (onPress/...), NOT inferred actions which
|
|
2710
|
+
// synthesize press/click onto every element. See projectVisionFields.
|
|
2711
|
+
handlerNames: handlers,
|
|
2712
|
+
state,
|
|
2713
|
+
flatStyle: e.flatStyle
|
|
2714
|
+
});
|
|
2377
2715
|
return {
|
|
2378
2716
|
id: e.id,
|
|
2379
2717
|
type: e.type,
|
|
@@ -2384,7 +2722,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2384
2722
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
2385
2723
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
2386
2724
|
registrationRoute: e.registrationRoute,
|
|
2387
|
-
visibility
|
|
2725
|
+
visibility,
|
|
2726
|
+
...bbox ? { bbox } : {},
|
|
2727
|
+
...vision
|
|
2388
2728
|
};
|
|
2389
2729
|
});
|
|
2390
2730
|
return success2({ elements });
|
|
@@ -2545,6 +2885,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2545
2885
|
});
|
|
2546
2886
|
},
|
|
2547
2887
|
getSnapshot: async (ctx) => {
|
|
2888
|
+
await registry.refreshMeasurements();
|
|
2548
2889
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
2549
2890
|
const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
|
|
2550
2891
|
const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
|
|
@@ -2860,6 +3201,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2860
3201
|
// that don't measure (the fallback only matches single-rooted screens but
|
|
2861
3202
|
// is good enough for the unit suite).
|
|
2862
3203
|
tapAt: async (ctx) => {
|
|
3204
|
+
await registry.refreshMeasurements();
|
|
2863
3205
|
const body = ctx.body;
|
|
2864
3206
|
const x = body?.x;
|
|
2865
3207
|
const y = body?.y;
|
|
@@ -3067,7 +3409,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3067
3409
|
};
|
|
3068
3410
|
if (config?.appInfo) {
|
|
3069
3411
|
response.uiBridge = {
|
|
3070
|
-
version: "0.6.
|
|
3412
|
+
version: "0.6.12",
|
|
3071
3413
|
...config.appInfo,
|
|
3072
3414
|
capabilities: ["elements", "components", "actions", "design"],
|
|
3073
3415
|
elementCount: stats.elements,
|
|
@@ -3659,6 +4001,14 @@ var NativeUIBridgeServer = class {
|
|
|
3659
4001
|
};
|
|
3660
4002
|
};
|
|
3661
4003
|
}
|
|
4004
|
+
/**
|
|
4005
|
+
* Whether the console/network capture should be installed. The dedicated
|
|
4006
|
+
* `observability` flag wins when set; otherwise falls back to `testHooks`
|
|
4007
|
+
* so consumers predating the flag keep their exact behavior.
|
|
4008
|
+
*/
|
|
4009
|
+
observabilityEnabled() {
|
|
4010
|
+
return this.config.observability ?? this.config.testHooks === true;
|
|
4011
|
+
}
|
|
3662
4012
|
/**
|
|
3663
4013
|
* Set the server adapter
|
|
3664
4014
|
*/
|
|
@@ -3872,7 +4222,7 @@ var NativeUIBridgeServer = class {
|
|
|
3872
4222
|
console.warn("[ui-bridge-native] See documentation for supported adapters.");
|
|
3873
4223
|
return;
|
|
3874
4224
|
}
|
|
3875
|
-
if (this.
|
|
4225
|
+
if (this.observabilityEnabled()) {
|
|
3876
4226
|
this.consoleErrorBuffer.install();
|
|
3877
4227
|
this.networkRequestBuffer.install();
|
|
3878
4228
|
}
|
|
@@ -5374,6 +5724,7 @@ function UIBridgeNativeProvider({
|
|
|
5374
5724
|
verbose: config.verbose,
|
|
5375
5725
|
onEvent
|
|
5376
5726
|
});
|
|
5727
|
+
registryRef.current.setPixelRatio(PixelRatio.get());
|
|
5377
5728
|
setGlobalRegistry(registryRef.current);
|
|
5378
5729
|
}
|
|
5379
5730
|
const registry = registryRef.current;
|
|
@@ -5404,6 +5755,10 @@ function UIBridgeNativeProvider({
|
|
|
5404
5755
|
cors: true,
|
|
5405
5756
|
appInfo: config.appInfo,
|
|
5406
5757
|
testHooks: features.testHooks === true,
|
|
5758
|
+
// Dedicated console/network-capture flag; the server defaults it to
|
|
5759
|
+
// the `testHooks` value when undefined, so passing it through verbatim
|
|
5760
|
+
// keeps existing consumers unchanged.
|
|
5761
|
+
observability: features.observability,
|
|
5407
5762
|
// Injected device-viewport getter for POST /control/page-health. The
|
|
5408
5763
|
// server doesn't import `react-native` itself — the require/import
|
|
5409
5764
|
// pattern there crashed the host RN app in 0.6.3/0.6.4 (Metro/Hermes
|
|
@@ -5471,6 +5826,7 @@ function UIBridgeNativeProvider({
|
|
|
5471
5826
|
}, [
|
|
5472
5827
|
features.server,
|
|
5473
5828
|
features.testHooks,
|
|
5829
|
+
features.observability,
|
|
5474
5830
|
config.serverPort,
|
|
5475
5831
|
config.appInfo,
|
|
5476
5832
|
registry,
|
|
@@ -5509,6 +5865,7 @@ function UIBridgeNativeProvider({
|
|
|
5509
5865
|
cors: true,
|
|
5510
5866
|
appInfo: config.appInfo,
|
|
5511
5867
|
testHooks: features.testHooks === true,
|
|
5868
|
+
observability: features.observability,
|
|
5512
5869
|
viewportProvider: () => {
|
|
5513
5870
|
const win = Dimensions.get("window");
|
|
5514
5871
|
return { width: win.width, height: win.height };
|
|
@@ -5712,12 +6069,41 @@ function scheduleDeadButtonWarning(bridge, id, type) {
|
|
|
5712
6069
|
}
|
|
5713
6070
|
function useUIElement(options) {
|
|
5714
6071
|
const bridge = useUIBridgeNativeOptional();
|
|
5715
|
-
const ref = useRef(null);
|
|
5716
6072
|
const [registered, setRegistered] = useState(false);
|
|
5717
6073
|
const [_layout, setLayout] = useState(null);
|
|
5718
6074
|
const propsRef = useRef({});
|
|
5719
6075
|
const registeredRef = useRef(false);
|
|
5720
6076
|
const registeredIdRef = useRef(null);
|
|
6077
|
+
const bridgeRef = useRef(bridge);
|
|
6078
|
+
bridgeRef.current = bridge;
|
|
6079
|
+
const nodeRef = useRef(null);
|
|
6080
|
+
const refInterceptorRef = useRef(null);
|
|
6081
|
+
if (refInterceptorRef.current === null) {
|
|
6082
|
+
refInterceptorRef.current = {
|
|
6083
|
+
get current() {
|
|
6084
|
+
return nodeRef.current;
|
|
6085
|
+
},
|
|
6086
|
+
set current(node) {
|
|
6087
|
+
const prev = nodeRef.current;
|
|
6088
|
+
nodeRef.current = node;
|
|
6089
|
+
const liveBridge = bridgeRef.current;
|
|
6090
|
+
const registeredId = registeredIdRef.current;
|
|
6091
|
+
if (!liveBridge || !registeredRef.current || registeredId === null) return;
|
|
6092
|
+
if (node == null && prev != null) {
|
|
6093
|
+
const state = liveBridge.registry.getElement(registeredId)?.getState();
|
|
6094
|
+
if (state && state.visible !== false) {
|
|
6095
|
+
liveBridge.registry.updateElementState(registeredId, { visible: false });
|
|
6096
|
+
}
|
|
6097
|
+
} else if (node != null && prev == null) {
|
|
6098
|
+
const state = liveBridge.registry.getElement(registeredId)?.getState();
|
|
6099
|
+
if (state && state.visible !== true) {
|
|
6100
|
+
liveBridge.registry.updateElementState(registeredId, { visible: true });
|
|
6101
|
+
}
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
6104
|
+
};
|
|
6105
|
+
}
|
|
6106
|
+
const ref = refInterceptorRef.current;
|
|
5721
6107
|
const {
|
|
5722
6108
|
id,
|
|
5723
6109
|
type = "custom",
|
|
@@ -5761,7 +6147,7 @@ function useUIElement(options) {
|
|
|
5761
6147
|
}
|
|
5762
6148
|
setRegistered(true);
|
|
5763
6149
|
scheduleDeadButtonWarning(bridge, id, type);
|
|
5764
|
-
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
|
|
6150
|
+
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp, ref]);
|
|
5765
6151
|
const unregister = useCallback(() => {
|
|
5766
6152
|
if (!bridge || !registeredRef.current) return;
|
|
5767
6153
|
bridge.registry.unregisterElement(registeredIdRef.current ?? id);
|
|
@@ -5797,7 +6183,8 @@ function useUIElement(options) {
|
|
|
5797
6183
|
writeLayout({ x, y, width, height, pageX: x, pageY: y });
|
|
5798
6184
|
}
|
|
5799
6185
|
},
|
|
5800
|
-
|
|
6186
|
+
// `ref` is the once-per-hook interceptor object — identity-stable.
|
|
6187
|
+
[bridge, id, ref]
|
|
5801
6188
|
);
|
|
5802
6189
|
const registerRef = useRef(register);
|
|
5803
6190
|
const unregisterRef = useRef(unregister);
|
|
@@ -5814,11 +6201,18 @@ function useUIElement(options) {
|
|
|
5814
6201
|
unregisterRef.current();
|
|
5815
6202
|
}
|
|
5816
6203
|
};
|
|
5817
|
-
}, [autoRegister, bridge]);
|
|
6204
|
+
}, [autoRegister, bridge, id]);
|
|
5818
6205
|
useEffect(() => {
|
|
5819
6206
|
if (!bridge || !registered) return;
|
|
5820
6207
|
const node = ref.current;
|
|
5821
|
-
if (!node
|
|
6208
|
+
if (!node) {
|
|
6209
|
+
const seeded = bridge.registry.getElement(id)?.getState();
|
|
6210
|
+
if (seeded && seeded.visible !== false) {
|
|
6211
|
+
bridge.registry.updateElementState(id, { visible: false });
|
|
6212
|
+
}
|
|
6213
|
+
return;
|
|
6214
|
+
}
|
|
6215
|
+
if (typeof node.measureInWindow !== "function") return;
|
|
5822
6216
|
let cancelled = false;
|
|
5823
6217
|
let pendingTimer = null;
|
|
5824
6218
|
const backoffMs = [50, 200, 500, 1e3, 2500];
|
|
@@ -5857,7 +6251,7 @@ function useUIElement(options) {
|
|
|
5857
6251
|
cancelled = true;
|
|
5858
6252
|
if (pendingTimer != null) clearTimeout(pendingTimer);
|
|
5859
6253
|
};
|
|
5860
|
-
}, [bridge, registered, id]);
|
|
6254
|
+
}, [bridge, registered, id, ref]);
|
|
5861
6255
|
const registeredHandlerKeysRef = useRef([]);
|
|
5862
6256
|
useEffect(() => {
|
|
5863
6257
|
if (!bridge || !registered || !handlersProp) return;
|
|
@@ -7245,6 +7639,6 @@ var styles = StyleSheet.create({
|
|
|
7245
7639
|
}
|
|
7246
7640
|
});
|
|
7247
7641
|
|
|
7248
|
-
export { CloudRelayClient, DefaultNativeActionExecutor, DeviceAnnouncer, ModalDetector, NativeUIBridgeRegistry, NativeUIBridgeServer, State, ToastCapture, TransitionTo, UIBridgeInspector, UIBridgeNativeProvider, UI_BRIDGE_NATIVE_ROUTES, UndoTracker, WebSocketConnection, WebSocketEventBridge, bindWithRetry, buildTreePath, buildUpgradeResponse, captureNativeResponsiveSnapshot, captureNativeStateVariations, computeAcceptKey, createDesignHandlers, createNativeActionExecutor, createNativeElementIdentifier, createNativeServer, createServerHandlers, extractHandlerNames, findAllByPattern, findElementByIdentifier, getGlobalRegistry, getNativeElementDesignData, isAddrInUseError, mapNativeStyleToExtended, matchesIdentifier, parseTreePath, resetGlobalRegistry, setGlobalRegistry, useToastRecorder, useUIBridge, useUIBridgeModal, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIBridgeToast, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
|
|
7642
|
+
export { CloudRelayClient, DefaultNativeActionExecutor, DeviceAnnouncer, ModalDetector, NativeUIBridgeRegistry, NativeUIBridgeServer, State, ToastCapture, TransitionTo, UIBridgeInspector, UIBridgeNativeProvider, UI_BRIDGE_NATIVE_ROUTES, UndoTracker, WebSocketConnection, WebSocketEventBridge, bindWithRetry, buildTreePath, buildUpgradeResponse, captureNativeResponsiveSnapshot, captureNativeStateVariations, computeAcceptKey, createDesignHandlers, createNativeActionExecutor, createNativeElementIdentifier, createNativeServer, createServerHandlers, deriveInteractable, deriveText, extractHandlerNames, findAllByPattern, findElementByIdentifier, getGlobalRegistry, getNativeElementDesignData, isAddrInUseError, mapNativeStyleToExtended, matchesIdentifier, parseColorToRgb, parseFontSizePx, parseTreePath, projectVisionFields, resetGlobalRegistry, roleForType, setGlobalRegistry, useToastRecorder, useUIBridge, useUIBridgeModal, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIBridgeToast, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
|
|
7249
7643
|
//# sourceMappingURL=index.mjs.map
|
|
7250
7644
|
//# sourceMappingURL=index.mjs.map
|