@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.
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
  // ============================================================================
@@ -660,6 +876,18 @@ var NativeUIBridgeRegistry = class {
660
876
  const handlers = extractHandlerNames(e.props);
661
877
  const state = e.getState();
662
878
  const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
879
+ const bbox = projectBbox(state, visibility, this.pixelRatio);
880
+ const vision = projectVisionFields({
881
+ type: e.type,
882
+ label: e.label,
883
+ // Real interactivity signal: the names of props that are actually
884
+ // functions (onPress/onChangeText/...). NOT `e.actions`, which
885
+ // `inferActions` synthesizes press/click onto for every element —
886
+ // using it would mark plain text/view nodes interactable.
887
+ handlerNames: handlers,
888
+ state,
889
+ flatStyle: e.flatStyle
890
+ });
663
891
  return {
664
892
  id: e.id,
665
893
  type: e.type,
@@ -670,7 +898,9 @@ var NativeUIBridgeRegistry = class {
670
898
  customActions: e.customActions ? Object.keys(e.customActions) : void 0,
671
899
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
672
900
  registrationRoute: e.registrationRoute,
673
- visibility
901
+ visibility,
902
+ ...bbox ? { bbox } : {},
903
+ ...vision
674
904
  };
675
905
  }),
676
906
  components: this.getAllComponents().map((c) => ({
@@ -1896,7 +2126,7 @@ function createDesignHandlers(registry, viewportProvider) {
1896
2126
  var GRID_SIZE = 20;
1897
2127
  var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
1898
2128
  var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
1899
- var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
2129
+ var INTERACTIVE_TYPES2 = /* @__PURE__ */ new Set([
1900
2130
  "button",
1901
2131
  "input",
1902
2132
  "switch",
@@ -2093,7 +2323,7 @@ function diagnosePageHealth(elements, viewport) {
2093
2323
  let interactiveTotal = 0;
2094
2324
  let interactiveDisabled = 0;
2095
2325
  for (const el of elements) {
2096
- if (!INTERACTIVE_TYPES.has(el.type)) continue;
2326
+ if (!INTERACTIVE_TYPES2.has(el.type)) continue;
2097
2327
  interactiveTotal++;
2098
2328
  if (el.state.enabled === false) interactiveDisabled++;
2099
2329
  }
@@ -2376,6 +2606,16 @@ function createServerHandlers(registry, executor, config) {
2376
2606
  const handlers = extractHandlerNames(e.props);
2377
2607
  const state = e.getState();
2378
2608
  const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
2609
+ const bbox = projectBbox(state, visibility, registry.getPixelRatio());
2610
+ const vision = projectVisionFields({
2611
+ type: e.type,
2612
+ label: e.label,
2613
+ // Real handler names (onPress/...), NOT inferred actions which
2614
+ // synthesize press/click onto every element. See projectVisionFields.
2615
+ handlerNames: handlers,
2616
+ state,
2617
+ flatStyle: e.flatStyle
2618
+ });
2379
2619
  return {
2380
2620
  id: e.id,
2381
2621
  type: e.type,
@@ -2386,7 +2626,9 @@ function createServerHandlers(registry, executor, config) {
2386
2626
  customActions: e.customActions ? Object.keys(e.customActions) : void 0,
2387
2627
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
2388
2628
  registrationRoute: e.registrationRoute,
2389
- visibility
2629
+ visibility,
2630
+ ...bbox ? { bbox } : {},
2631
+ ...vision
2390
2632
  };
2391
2633
  });
2392
2634
  return success2({ elements });
@@ -3069,7 +3311,7 @@ function createServerHandlers(registry, executor, config) {
3069
3311
  };
3070
3312
  if (config?.appInfo) {
3071
3313
  response.uiBridge = {
3072
- version: "0.6.10",
3314
+ version: "0.6.11",
3073
3315
  ...config.appInfo,
3074
3316
  capabilities: ["elements", "components", "actions", "design"],
3075
3317
  elementCount: stats.elements,
@@ -3661,6 +3903,14 @@ var NativeUIBridgeServer = class {
3661
3903
  };
3662
3904
  };
3663
3905
  }
3906
+ /**
3907
+ * Whether the console/network capture should be installed. The dedicated
3908
+ * `observability` flag wins when set; otherwise falls back to `testHooks`
3909
+ * so consumers predating the flag keep their exact behavior.
3910
+ */
3911
+ observabilityEnabled() {
3912
+ return this.config.observability ?? this.config.testHooks === true;
3913
+ }
3664
3914
  /**
3665
3915
  * Set the server adapter
3666
3916
  */
@@ -3874,7 +4124,7 @@ var NativeUIBridgeServer = class {
3874
4124
  console.warn("[ui-bridge-native] See documentation for supported adapters.");
3875
4125
  return;
3876
4126
  }
3877
- if (this.config.testHooks === true) {
4127
+ if (this.observabilityEnabled()) {
3878
4128
  this.consoleErrorBuffer.install();
3879
4129
  this.networkRequestBuffer.install();
3880
4130
  }
@@ -5376,6 +5626,7 @@ function UIBridgeNativeProvider({
5376
5626
  verbose: config.verbose,
5377
5627
  onEvent
5378
5628
  });
5629
+ registryRef.current.setPixelRatio(reactNative.PixelRatio.get());
5379
5630
  setGlobalRegistry(registryRef.current);
5380
5631
  }
5381
5632
  const registry = registryRef.current;
@@ -5406,6 +5657,10 @@ function UIBridgeNativeProvider({
5406
5657
  cors: true,
5407
5658
  appInfo: config.appInfo,
5408
5659
  testHooks: features.testHooks === true,
5660
+ // Dedicated console/network-capture flag; the server defaults it to
5661
+ // the `testHooks` value when undefined, so passing it through verbatim
5662
+ // keeps existing consumers unchanged.
5663
+ observability: features.observability,
5409
5664
  // Injected device-viewport getter for POST /control/page-health. The
5410
5665
  // server doesn't import `react-native` itself — the require/import
5411
5666
  // pattern there crashed the host RN app in 0.6.3/0.6.4 (Metro/Hermes
@@ -5473,6 +5728,7 @@ function UIBridgeNativeProvider({
5473
5728
  }, [
5474
5729
  features.server,
5475
5730
  features.testHooks,
5731
+ features.observability,
5476
5732
  config.serverPort,
5477
5733
  config.appInfo,
5478
5734
  registry,
@@ -5511,6 +5767,7 @@ function UIBridgeNativeProvider({
5511
5767
  cors: true,
5512
5768
  appInfo: config.appInfo,
5513
5769
  testHooks: features.testHooks === true,
5770
+ observability: features.observability,
5514
5771
  viewportProvider: () => {
5515
5772
  const win = reactNative.Dimensions.get("window");
5516
5773
  return { width: win.width, height: win.height };
@@ -5714,12 +5971,41 @@ function scheduleDeadButtonWarning(bridge, id, type) {
5714
5971
  }
5715
5972
  function useUIElement(options) {
5716
5973
  const bridge = useUIBridgeNativeOptional();
5717
- const ref = react.useRef(null);
5718
5974
  const [registered, setRegistered] = react.useState(false);
5719
5975
  const [_layout, setLayout] = react.useState(null);
5720
5976
  const propsRef = react.useRef({});
5721
5977
  const registeredRef = react.useRef(false);
5722
5978
  const registeredIdRef = react.useRef(null);
5979
+ const bridgeRef = react.useRef(bridge);
5980
+ bridgeRef.current = bridge;
5981
+ const nodeRef = react.useRef(null);
5982
+ const refInterceptorRef = react.useRef(null);
5983
+ if (refInterceptorRef.current === null) {
5984
+ refInterceptorRef.current = {
5985
+ get current() {
5986
+ return nodeRef.current;
5987
+ },
5988
+ set current(node) {
5989
+ const prev = nodeRef.current;
5990
+ nodeRef.current = node;
5991
+ const liveBridge = bridgeRef.current;
5992
+ const registeredId = registeredIdRef.current;
5993
+ if (!liveBridge || !registeredRef.current || registeredId === null) return;
5994
+ if (node == null && prev != null) {
5995
+ const state = liveBridge.registry.getElement(registeredId)?.getState();
5996
+ if (state && state.visible !== false) {
5997
+ liveBridge.registry.updateElementState(registeredId, { visible: false });
5998
+ }
5999
+ } else if (node != null && prev == null) {
6000
+ const state = liveBridge.registry.getElement(registeredId)?.getState();
6001
+ if (state && state.visible !== true) {
6002
+ liveBridge.registry.updateElementState(registeredId, { visible: true });
6003
+ }
6004
+ }
6005
+ }
6006
+ };
6007
+ }
6008
+ const ref = refInterceptorRef.current;
5723
6009
  const {
5724
6010
  id,
5725
6011
  type = "custom",
@@ -5763,7 +6049,7 @@ function useUIElement(options) {
5763
6049
  }
5764
6050
  setRegistered(true);
5765
6051
  scheduleDeadButtonWarning(bridge, id, type);
5766
- }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
6052
+ }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp, ref]);
5767
6053
  const unregister = react.useCallback(() => {
5768
6054
  if (!bridge || !registeredRef.current) return;
5769
6055
  bridge.registry.unregisterElement(registeredIdRef.current ?? id);
@@ -5799,7 +6085,8 @@ function useUIElement(options) {
5799
6085
  writeLayout({ x, y, width, height, pageX: x, pageY: y });
5800
6086
  }
5801
6087
  },
5802
- [bridge, id]
6088
+ // `ref` is the once-per-hook interceptor object — identity-stable.
6089
+ [bridge, id, ref]
5803
6090
  );
5804
6091
  const registerRef = react.useRef(register);
5805
6092
  const unregisterRef = react.useRef(unregister);
@@ -5820,7 +6107,14 @@ function useUIElement(options) {
5820
6107
  react.useEffect(() => {
5821
6108
  if (!bridge || !registered) return;
5822
6109
  const node = ref.current;
5823
- if (!node || typeof node.measureInWindow !== "function") return;
6110
+ if (!node) {
6111
+ const seeded = bridge.registry.getElement(id)?.getState();
6112
+ if (seeded && seeded.visible !== false) {
6113
+ bridge.registry.updateElementState(id, { visible: false });
6114
+ }
6115
+ return;
6116
+ }
6117
+ if (typeof node.measureInWindow !== "function") return;
5824
6118
  let cancelled = false;
5825
6119
  let pendingTimer = null;
5826
6120
  const backoffMs = [50, 200, 500, 1e3, 2500];
@@ -5859,7 +6153,7 @@ function useUIElement(options) {
5859
6153
  cancelled = true;
5860
6154
  if (pendingTimer != null) clearTimeout(pendingTimer);
5861
6155
  };
5862
- }, [bridge, registered, id]);
6156
+ }, [bridge, registered, id, ref]);
5863
6157
  const registeredHandlerKeysRef = react.useRef([]);
5864
6158
  react.useEffect(() => {
5865
6159
  if (!bridge || !registered || !handlersProp) return;
@@ -7273,6 +7567,8 @@ exports.createNativeActionExecutor = createNativeActionExecutor;
7273
7567
  exports.createNativeElementIdentifier = createNativeElementIdentifier;
7274
7568
  exports.createNativeServer = createNativeServer;
7275
7569
  exports.createServerHandlers = createServerHandlers;
7570
+ exports.deriveInteractable = deriveInteractable;
7571
+ exports.deriveText = deriveText;
7276
7572
  exports.extractHandlerNames = extractHandlerNames;
7277
7573
  exports.findAllByPattern = findAllByPattern;
7278
7574
  exports.findElementByIdentifier = findElementByIdentifier;
@@ -7281,8 +7577,12 @@ exports.getNativeElementDesignData = getNativeElementDesignData;
7281
7577
  exports.isAddrInUseError = isAddrInUseError;
7282
7578
  exports.mapNativeStyleToExtended = mapNativeStyleToExtended;
7283
7579
  exports.matchesIdentifier = matchesIdentifier;
7580
+ exports.parseColorToRgb = parseColorToRgb;
7581
+ exports.parseFontSizePx = parseFontSizePx;
7284
7582
  exports.parseTreePath = parseTreePath;
7583
+ exports.projectVisionFields = projectVisionFields;
7285
7584
  exports.resetGlobalRegistry = resetGlobalRegistry;
7585
+ exports.roleForType = roleForType;
7286
7586
  exports.setGlobalRegistry = setGlobalRegistry;
7287
7587
  exports.useToastRecorder = useToastRecorder;
7288
7588
  exports.useUIBridge = useUIBridge;