@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/core/index.js
CHANGED
|
@@ -1,5 +1,178 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/core/vision-fields.ts
|
|
4
|
+
var INTERACTIVE_HANDLER_NAMES = /* @__PURE__ */ new Set([
|
|
5
|
+
"onPress",
|
|
6
|
+
"onPressIn",
|
|
7
|
+
"onPressOut",
|
|
8
|
+
"onLongPress",
|
|
9
|
+
"onChangeText",
|
|
10
|
+
"onValueChange",
|
|
11
|
+
"onChange",
|
|
12
|
+
"onSubmitEditing",
|
|
13
|
+
"onClick",
|
|
14
|
+
"onTouchStart",
|
|
15
|
+
"onTouchEnd"
|
|
16
|
+
]);
|
|
17
|
+
var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
18
|
+
"button",
|
|
19
|
+
"input",
|
|
20
|
+
"switch",
|
|
21
|
+
"checkbox",
|
|
22
|
+
"radio",
|
|
23
|
+
"touchable",
|
|
24
|
+
"pressable",
|
|
25
|
+
"listItem"
|
|
26
|
+
]);
|
|
27
|
+
function roleForType(type) {
|
|
28
|
+
switch (type) {
|
|
29
|
+
case "button":
|
|
30
|
+
case "touchable":
|
|
31
|
+
case "pressable":
|
|
32
|
+
return "button";
|
|
33
|
+
case "input":
|
|
34
|
+
return "textbox";
|
|
35
|
+
case "text":
|
|
36
|
+
return "text";
|
|
37
|
+
case "switch":
|
|
38
|
+
return "switch";
|
|
39
|
+
case "checkbox":
|
|
40
|
+
return "checkbox";
|
|
41
|
+
case "radio":
|
|
42
|
+
return "radio";
|
|
43
|
+
case "image":
|
|
44
|
+
return "img";
|
|
45
|
+
case "list":
|
|
46
|
+
return "list";
|
|
47
|
+
case "listItem":
|
|
48
|
+
return "listitem";
|
|
49
|
+
case "modal":
|
|
50
|
+
return "dialog";
|
|
51
|
+
case "scroll":
|
|
52
|
+
case "view":
|
|
53
|
+
case "custom":
|
|
54
|
+
default:
|
|
55
|
+
return void 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function deriveInteractable(type, handlerNames) {
|
|
59
|
+
if (INTERACTIVE_TYPES.has(type)) return true;
|
|
60
|
+
if (handlerNames) {
|
|
61
|
+
for (const name of handlerNames) {
|
|
62
|
+
if (INTERACTIVE_HANDLER_NAMES.has(name)) return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
function deriveText(state, label) {
|
|
68
|
+
const candidates = [state.value, state.textContent, label];
|
|
69
|
+
for (const c of candidates) {
|
|
70
|
+
if (typeof c === "string") {
|
|
71
|
+
const trimmed = c.trim();
|
|
72
|
+
if (trimmed.length > 0) return trimmed;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
var NAMED_COLORS = {
|
|
78
|
+
black: { r: 0, g: 0, b: 0 },
|
|
79
|
+
white: { r: 255, g: 255, b: 255 },
|
|
80
|
+
red: { r: 255, g: 0, b: 0 },
|
|
81
|
+
green: { r: 0, g: 128, b: 0 },
|
|
82
|
+
blue: { r: 0, g: 0, b: 255 },
|
|
83
|
+
gray: { r: 128, g: 128, b: 128 },
|
|
84
|
+
grey: { r: 128, g: 128, b: 128 },
|
|
85
|
+
silver: { r: 192, g: 192, b: 192 },
|
|
86
|
+
transparent: { r: 0, g: 0, b: 0 },
|
|
87
|
+
orange: { r: 255, g: 165, b: 0 },
|
|
88
|
+
yellow: { r: 255, g: 255, b: 0 },
|
|
89
|
+
purple: { r: 128, g: 0, b: 128 },
|
|
90
|
+
navy: { r: 0, g: 0, b: 128 }
|
|
91
|
+
};
|
|
92
|
+
var clamp255 = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
93
|
+
function parseColorToRgb(input) {
|
|
94
|
+
if (typeof input !== "string") return void 0;
|
|
95
|
+
const raw = input.trim().toLowerCase();
|
|
96
|
+
if (raw.length === 0) return void 0;
|
|
97
|
+
if (Object.prototype.hasOwnProperty.call(NAMED_COLORS, raw)) {
|
|
98
|
+
return NAMED_COLORS[raw];
|
|
99
|
+
}
|
|
100
|
+
if (raw.startsWith("#")) {
|
|
101
|
+
const hex = raw.slice(1);
|
|
102
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
103
|
+
const r = parseInt(hex[0] + hex[0], 16);
|
|
104
|
+
const g = parseInt(hex[1] + hex[1], 16);
|
|
105
|
+
const b = parseInt(hex[2] + hex[2], 16);
|
|
106
|
+
if ([r, g, b].every((v) => Number.isFinite(v))) return { r, g, b };
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
110
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
111
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
112
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
113
|
+
if ([r, g, b].every((v) => Number.isFinite(v))) return { r, g, b };
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
const rgbMatch = raw.match(/^rgba?\(([^)]+)\)$/);
|
|
119
|
+
if (rgbMatch) {
|
|
120
|
+
const parts = rgbMatch[1].split(",").map((p) => p.trim());
|
|
121
|
+
if (parts.length < 3) return void 0;
|
|
122
|
+
const channel = (p) => {
|
|
123
|
+
if (p.endsWith("%")) {
|
|
124
|
+
const pct = parseFloat(p.slice(0, -1));
|
|
125
|
+
if (!Number.isFinite(pct)) return void 0;
|
|
126
|
+
return clamp255(pct / 100 * 255);
|
|
127
|
+
}
|
|
128
|
+
const n = parseFloat(p);
|
|
129
|
+
if (!Number.isFinite(n)) return void 0;
|
|
130
|
+
return clamp255(n);
|
|
131
|
+
};
|
|
132
|
+
const r = channel(parts[0]);
|
|
133
|
+
const g = channel(parts[1]);
|
|
134
|
+
const b = channel(parts[2]);
|
|
135
|
+
if (r === void 0 || g === void 0 || b === void 0) return void 0;
|
|
136
|
+
return { r, g, b };
|
|
137
|
+
}
|
|
138
|
+
return void 0;
|
|
139
|
+
}
|
|
140
|
+
function parseFontSizePx(input) {
|
|
141
|
+
if (typeof input === "number") {
|
|
142
|
+
return Number.isFinite(input) && input > 0 ? input : void 0;
|
|
143
|
+
}
|
|
144
|
+
if (typeof input === "string") {
|
|
145
|
+
const m = input.trim().match(/^(-?\d*\.?\d+)/);
|
|
146
|
+
if (m) {
|
|
147
|
+
const n = parseFloat(m[1]);
|
|
148
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
153
|
+
function projectVisionFields(args) {
|
|
154
|
+
const { type, label, handlerNames, state, flatStyle } = args;
|
|
155
|
+
const fields = {
|
|
156
|
+
interactable: deriveInteractable(type, handlerNames)
|
|
157
|
+
};
|
|
158
|
+
const text = deriveText(state, label);
|
|
159
|
+
if (text !== void 0) fields.text = text;
|
|
160
|
+
const role = roleForType(type);
|
|
161
|
+
if (role !== void 0) fields.role = role;
|
|
162
|
+
if (flatStyle) {
|
|
163
|
+
const fg = parseColorToRgb(flatStyle.color);
|
|
164
|
+
if (fg !== void 0) fields.fg_color = fg;
|
|
165
|
+
const bg = parseColorToRgb(flatStyle.backgroundColor);
|
|
166
|
+
if (bg !== void 0) fields.bg_color = bg;
|
|
167
|
+
const size = parseFontSizePx(flatStyle.fontSize);
|
|
168
|
+
if (size !== void 0) fields.font_size_px = size;
|
|
169
|
+
if (typeof flatStyle.fontFamily === "string" && flatStyle.fontFamily.trim().length > 0) {
|
|
170
|
+
fields.font_family = flatStyle.fontFamily;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return fields;
|
|
174
|
+
}
|
|
175
|
+
|
|
3
176
|
// src/core/registry.ts
|
|
4
177
|
function extractHandlerNames(props) {
|
|
5
178
|
if (!props) return [];
|
|
@@ -9,6 +182,25 @@ function matchesCurrentRoute(registrationRoute, currentRoute) {
|
|
|
9
182
|
if (registrationRoute == null || registrationRoute === "") return true;
|
|
10
183
|
return registrationRoute === currentRoute;
|
|
11
184
|
}
|
|
185
|
+
function projectBbox(state, visibility, pixelRatio = 1) {
|
|
186
|
+
if (visibility !== "visible") return void 0;
|
|
187
|
+
const layout = state.layout;
|
|
188
|
+
if (!layout) return void 0;
|
|
189
|
+
const ratio = pixelRatio > 0 && Number.isFinite(pixelRatio) ? pixelRatio : 1;
|
|
190
|
+
const toPx = (v) => Math.max(0, Math.round(v * ratio));
|
|
191
|
+
const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
|
|
192
|
+
const originX = hasPage ? layout.pageX : layout.x;
|
|
193
|
+
const originY = hasPage ? layout.pageY : layout.y;
|
|
194
|
+
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)) {
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
x: toPx(originX),
|
|
199
|
+
y: toPx(originY),
|
|
200
|
+
w: toPx(layout.width),
|
|
201
|
+
h: toPx(layout.height)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
12
204
|
function inferActions(type) {
|
|
13
205
|
const baseActions = ["focus", "blur"];
|
|
14
206
|
switch (type) {
|
|
@@ -73,6 +265,15 @@ var NativeUIBridgeRegistry = class {
|
|
|
73
265
|
* already installs.
|
|
74
266
|
*/
|
|
75
267
|
this.routeProvider = null;
|
|
268
|
+
/**
|
|
269
|
+
* Device pixel ratio used to project `state.layout` (logical dp) into the
|
|
270
|
+
* physical-pixel `bbox` emitted on each snapshot element. Defaults to `1`
|
|
271
|
+
* (dp ≈ px) so callers/tests that never inject a ratio still get a usable
|
|
272
|
+
* box; the React provider wires the real `PixelRatio.get()` via
|
|
273
|
+
* {@link setPixelRatio}. Read from a non-React core module on purpose — see
|
|
274
|
+
* the `projectBbox` doc comment for why we don't import `react-native` here.
|
|
275
|
+
*/
|
|
276
|
+
this.pixelRatio = 1;
|
|
76
277
|
/**
|
|
77
278
|
* Sticky flag: flips `true` the first time any element is registered and
|
|
78
279
|
* stays `true` even after elements are unregistered. Lets agents distinguish
|
|
@@ -110,6 +311,21 @@ var NativeUIBridgeRegistry = class {
|
|
|
110
311
|
getRouteProvider() {
|
|
111
312
|
return this.routeProvider;
|
|
112
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Set the device pixel ratio used to project per-element `bbox` geometry
|
|
316
|
+
* into physical pixels. The React provider passes `PixelRatio.get()` here
|
|
317
|
+
* once at startup. Ignores non-finite / non-positive values (keeps the
|
|
318
|
+
* previous ratio) so a bad call can't zero out every bbox.
|
|
319
|
+
*/
|
|
320
|
+
setPixelRatio(ratio) {
|
|
321
|
+
if (typeof ratio === "number" && Number.isFinite(ratio) && ratio > 0) {
|
|
322
|
+
this.pixelRatio = ratio;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** Read-only accessor for the device pixel ratio used for bbox projection. */
|
|
326
|
+
getPixelRatio() {
|
|
327
|
+
return this.pixelRatio;
|
|
328
|
+
}
|
|
113
329
|
// ============================================================================
|
|
114
330
|
// Snapshot Enricher Slots
|
|
115
331
|
// ============================================================================
|
|
@@ -335,6 +551,103 @@ var NativeUIBridgeRegistry = class {
|
|
|
335
551
|
console.log(`[ui-bridge-native] Marked ${cleared} elements offscreen for route: ${route}`);
|
|
336
552
|
}
|
|
337
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Re-measure every mounted-visible element's geometry via its stored React
|
|
556
|
+
* ref and write the fresh coordinates into the registry ("measure-on-
|
|
557
|
+
* snapshot").
|
|
558
|
+
*
|
|
559
|
+
* Why: element geometry is otherwise captured ONCE at mount time
|
|
560
|
+
* (`onLayout` + `measureInWindow` in `useUIElement`) and RN never re-fires
|
|
561
|
+
* `onLayout` on pure translation — scrolling a virtualized list
|
|
562
|
+
* (FlashList), recycling a cell, a keyboard shift or an orientation change
|
|
563
|
+
* all move elements WITHOUT updating the stored `state.layout`. Snapshot
|
|
564
|
+
* consumers (vision analyzers hit-testing `bbox`, coord-based tap) then
|
|
565
|
+
* operate on stale positions. Re-measuring at READ time is correct
|
|
566
|
+
* regardless of what moved the element and needs zero per-consumer wiring:
|
|
567
|
+
* every registry entry already stores its ref (see `registerElement`).
|
|
568
|
+
*
|
|
569
|
+
* Per-element outcomes:
|
|
570
|
+
* - non-zero dims → write a fresh full layout (same state shape as the
|
|
571
|
+
* mount-time measure loop in `useUIElement`); counted as `measured`.
|
|
572
|
+
* - zero dims AND the element previously had a non-null `state.layout` →
|
|
573
|
+
* the view collapsed / left the screen; clear to
|
|
574
|
+
* `{ visible: false, layout: null }` (same shape `markRouteOffscreen`
|
|
575
|
+
* writes); counted as `cleared`.
|
|
576
|
+
* - zero dims and it never had a layout → leave state untouched (element
|
|
577
|
+
* in the mount→first-onLayout gap; don't demote `likely-visible`);
|
|
578
|
+
* counted as `skipped`.
|
|
579
|
+
* - no ref / no callable `measureInWindow` (test fixtures, web) → leave
|
|
580
|
+
* untouched; counted as `skipped`.
|
|
581
|
+
*
|
|
582
|
+
* The whole sweep races an overall timeout (default 250 ms) so a dead ref
|
|
583
|
+
* whose `measureInWindow` never calls back can never hang a snapshot: the
|
|
584
|
+
* returned promise resolves at the deadline with whatever counts have
|
|
585
|
+
* accumulated. Late callbacks may still write state afterwards — harmless,
|
|
586
|
+
* freshest data wins. Never throws.
|
|
587
|
+
*
|
|
588
|
+
* Non-React core module on purpose — `measureInWindow` is duck-typed off
|
|
589
|
+
* the stored ref rather than imported from `react-native` (see the
|
|
590
|
+
* `projectBbox` doc comment for why this file must not import RN).
|
|
591
|
+
*/
|
|
592
|
+
async refreshMeasurements(options) {
|
|
593
|
+
const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 250;
|
|
594
|
+
const counts = { measured: 0, cleared: 0, skipped: 0 };
|
|
595
|
+
const pending = [];
|
|
596
|
+
for (const element of this.getMountedVisibleElements()) {
|
|
597
|
+
const node = element.ref?.current;
|
|
598
|
+
if (!node || typeof node.measureInWindow !== "function") {
|
|
599
|
+
counts.skipped++;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const id = element.id;
|
|
603
|
+
pending.push(
|
|
604
|
+
new Promise((resolve) => {
|
|
605
|
+
let settled = false;
|
|
606
|
+
try {
|
|
607
|
+
node.measureInWindow((pageX, pageY, w, h) => {
|
|
608
|
+
if (settled) return;
|
|
609
|
+
settled = true;
|
|
610
|
+
try {
|
|
611
|
+
if (w > 0 && h > 0) {
|
|
612
|
+
this.updateElementState(id, {
|
|
613
|
+
mounted: true,
|
|
614
|
+
visible: true,
|
|
615
|
+
enabled: true,
|
|
616
|
+
focused: false,
|
|
617
|
+
layout: { x: pageX, y: pageY, width: w, height: h, pageX, pageY }
|
|
618
|
+
});
|
|
619
|
+
counts.measured++;
|
|
620
|
+
} else if (this.elements.get(id)?.getState().layout != null) {
|
|
621
|
+
this.updateElementState(id, { visible: false, layout: null });
|
|
622
|
+
counts.cleared++;
|
|
623
|
+
} else {
|
|
624
|
+
counts.skipped++;
|
|
625
|
+
}
|
|
626
|
+
} finally {
|
|
627
|
+
resolve();
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
} catch {
|
|
631
|
+
if (!settled) {
|
|
632
|
+
settled = true;
|
|
633
|
+
counts.skipped++;
|
|
634
|
+
resolve();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
})
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
if (pending.length > 0) {
|
|
641
|
+
await new Promise((resolve) => {
|
|
642
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
643
|
+
void Promise.all(pending).then(() => {
|
|
644
|
+
clearTimeout(timer);
|
|
645
|
+
resolve();
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return counts;
|
|
650
|
+
}
|
|
338
651
|
/**
|
|
339
652
|
* Update element state
|
|
340
653
|
*/
|
|
@@ -649,6 +962,18 @@ var NativeUIBridgeRegistry = class {
|
|
|
649
962
|
const handlers = extractHandlerNames(e.props);
|
|
650
963
|
const state = e.getState();
|
|
651
964
|
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
965
|
+
const bbox = projectBbox(state, visibility, this.pixelRatio);
|
|
966
|
+
const vision = projectVisionFields({
|
|
967
|
+
type: e.type,
|
|
968
|
+
label: e.label,
|
|
969
|
+
// Real interactivity signal: the names of props that are actually
|
|
970
|
+
// functions (onPress/onChangeText/...). NOT `e.actions`, which
|
|
971
|
+
// `inferActions` synthesizes press/click onto for every element —
|
|
972
|
+
// using it would mark plain text/view nodes interactable.
|
|
973
|
+
handlerNames: handlers,
|
|
974
|
+
state,
|
|
975
|
+
flatStyle: e.flatStyle
|
|
976
|
+
});
|
|
652
977
|
return {
|
|
653
978
|
id: e.id,
|
|
654
979
|
type: e.type,
|
|
@@ -659,7 +984,9 @@ var NativeUIBridgeRegistry = class {
|
|
|
659
984
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
660
985
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
661
986
|
registrationRoute: e.registrationRoute,
|
|
662
|
-
visibility
|
|
987
|
+
visibility,
|
|
988
|
+
...bbox ? { bbox } : {},
|
|
989
|
+
...vision
|
|
663
990
|
};
|
|
664
991
|
}),
|
|
665
992
|
components: this.getAllComponents().map((c) => ({
|
|
@@ -884,6 +1211,7 @@ exports.getGlobalRegistry = getGlobalRegistry;
|
|
|
884
1211
|
exports.matchesCurrentRoute = matchesCurrentRoute;
|
|
885
1212
|
exports.matchesIdentifier = matchesIdentifier;
|
|
886
1213
|
exports.parseTreePath = parseTreePath;
|
|
1214
|
+
exports.projectBbox = projectBbox;
|
|
887
1215
|
exports.resetGlobalRegistry = resetGlobalRegistry;
|
|
888
1216
|
exports.setGlobalRegistry = setGlobalRegistry;
|
|
889
1217
|
//# sourceMappingURL=index.js.map
|