@qontinui/ui-bridge-native 0.6.10 → 0.6.11

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