@qontinui/ui-bridge-native 0.6.9 → 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.
Files changed (45) hide show
  1. package/dist/control/action-executor.d.ts.map +1 -1
  2. package/dist/control/index.js +2 -1
  3. package/dist/control/index.js.map +1 -1
  4. package/dist/control/index.mjs +2 -1
  5. package/dist/control/index.mjs.map +1 -1
  6. package/dist/core/index.js +232 -1
  7. package/dist/core/index.js.map +1 -1
  8. package/dist/core/index.mjs +232 -2
  9. package/dist/core/index.mjs.map +1 -1
  10. package/dist/core/registry.d.ts +46 -1
  11. package/dist/core/registry.d.ts.map +1 -1
  12. package/dist/core/types.d.ts +114 -0
  13. package/dist/core/types.d.ts.map +1 -1
  14. package/dist/core/vision-fields.d.ts +142 -0
  15. package/dist/core/vision-fields.d.ts.map +1 -0
  16. package/dist/debug/index.js.map +1 -1
  17. package/dist/debug/index.mjs.map +1 -1
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +371 -20
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +365 -22
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
  25. package/dist/react/index.js +323 -20
  26. package/dist/react/index.js.map +1 -1
  27. package/dist/react/index.mjs +324 -21
  28. package/dist/react/index.mjs.map +1 -1
  29. package/dist/react/useUIElement.d.ts.map +1 -1
  30. package/dist/server/bind-retry.d.ts +64 -0
  31. package/dist/server/bind-retry.d.ts.map +1 -0
  32. package/dist/server/handlers.d.ts.map +1 -1
  33. package/dist/server/http-server.d.ts +9 -1
  34. package/dist/server/http-server.d.ts.map +1 -1
  35. package/dist/server/index.d.ts +2 -0
  36. package/dist/server/index.d.ts.map +1 -1
  37. package/dist/server/index.js +275 -13
  38. package/dist/server/index.js.map +1 -1
  39. package/dist/server/index.mjs +274 -14
  40. package/dist/server/index.mjs.map +1 -1
  41. package/dist/server/observability.d.ts +4 -3
  42. package/dist/server/observability.d.ts.map +1 -1
  43. package/dist/server/types.d.ts +10 -0
  44. package/dist/server/types.d.ts.map +1 -1
  45. 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
  // ============================================================================
@@ -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) => ({
@@ -985,9 +1215,10 @@ var DefaultNativeActionExecutor = class {
985
1215
  requestId: request.requestId,
986
1216
  durationMs: successTime - startTime
987
1217
  });
1218
+ const fresh = this.registry.getElement(registered.id) ?? registered;
988
1219
  return {
989
1220
  success: true,
990
- elementState: registered.getState(),
1221
+ elementState: fresh.getState(),
991
1222
  result,
992
1223
  durationMs: successTime - startTime,
993
1224
  timestamp: successTime,
@@ -1893,7 +2124,7 @@ function createDesignHandlers(registry, viewportProvider) {
1893
2124
  var GRID_SIZE = 20;
1894
2125
  var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
1895
2126
  var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
1896
- var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
2127
+ var INTERACTIVE_TYPES2 = /* @__PURE__ */ new Set([
1897
2128
  "button",
1898
2129
  "input",
1899
2130
  "switch",
@@ -2090,7 +2321,7 @@ function diagnosePageHealth(elements, viewport) {
2090
2321
  let interactiveTotal = 0;
2091
2322
  let interactiveDisabled = 0;
2092
2323
  for (const el of elements) {
2093
- if (!INTERACTIVE_TYPES.has(el.type)) continue;
2324
+ if (!INTERACTIVE_TYPES2.has(el.type)) continue;
2094
2325
  interactiveTotal++;
2095
2326
  if (el.state.enabled === false) interactiveDisabled++;
2096
2327
  }
@@ -2158,6 +2389,13 @@ function matchElementsByText(registry, text, opts) {
2158
2389
  scored.sort((a, b) => a.rank - b.rank);
2159
2390
  return scored.map((s) => s.element);
2160
2391
  }
2392
+ function resolveElementState(element) {
2393
+ const state = element.getState();
2394
+ if (element.type === "input" && state.value === void 0 && typeof element.props?.value === "string") {
2395
+ return { ...state, value: element.props.value };
2396
+ }
2397
+ return state;
2398
+ }
2161
2399
  async function executeWorkflowStep(step, registry, executor) {
2162
2400
  const startTime = Date.now();
2163
2401
  try {
@@ -2366,6 +2604,16 @@ function createServerHandlers(registry, executor, config) {
2366
2604
  const handlers = extractHandlerNames(e.props);
2367
2605
  const state = e.getState();
2368
2606
  const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
2607
+ const bbox = projectBbox(state, visibility, registry.getPixelRatio());
2608
+ const vision = projectVisionFields({
2609
+ type: e.type,
2610
+ label: e.label,
2611
+ // Real handler names (onPress/...), NOT inferred actions which
2612
+ // synthesize press/click onto every element. See projectVisionFields.
2613
+ handlerNames: handlers,
2614
+ state,
2615
+ flatStyle: e.flatStyle
2616
+ });
2369
2617
  return {
2370
2618
  id: e.id,
2371
2619
  type: e.type,
@@ -2376,7 +2624,9 @@ function createServerHandlers(registry, executor, config) {
2376
2624
  customActions: e.customActions ? Object.keys(e.customActions) : void 0,
2377
2625
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
2378
2626
  registrationRoute: e.registrationRoute,
2379
- visibility
2627
+ visibility,
2628
+ ...bbox ? { bbox } : {},
2629
+ ...vision
2380
2630
  };
2381
2631
  });
2382
2632
  return success2({ elements });
@@ -2394,7 +2644,7 @@ function createServerHandlers(registry, executor, config) {
2394
2644
  type: element.type,
2395
2645
  label: element.label,
2396
2646
  identifier: element.getIdentifier(),
2397
- state: element.getState(),
2647
+ state: resolveElementState(element),
2398
2648
  actions: element.actions,
2399
2649
  customActions: element.customActions ? Object.keys(element.customActions) : void 0,
2400
2650
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
@@ -2408,18 +2658,19 @@ function createServerHandlers(registry, executor, config) {
2408
2658
  if (!element) {
2409
2659
  return error2(`Element not found: ${id}`, "ELEMENT_NOT_FOUND");
2410
2660
  }
2411
- return success2({ state: element.getState() });
2661
+ return success2({ state: resolveElementState(element) });
2412
2662
  },
2413
2663
  executeAction: async (ctx) => {
2414
- const { id } = ctx.params;
2415
- const body = ctx.body;
2416
- if (!body?.action) {
2664
+ const rawBody = ctx.body ?? {};
2665
+ const envelope = rawBody.request && typeof rawBody.request === "object" ? rawBody.request : rawBody;
2666
+ const id = ctx.params.id ?? rawBody.id ?? "";
2667
+ if (!envelope?.action) {
2417
2668
  return error2("Action is required", "INVALID_REQUEST");
2418
2669
  }
2419
2670
  const response = await executor.executeAction(id, {
2420
- action: body.action,
2421
- params: body.params,
2422
- waitOptions: body.waitOptions
2671
+ action: envelope.action,
2672
+ params: envelope.params,
2673
+ waitOptions: envelope.waitOptions
2423
2674
  });
2424
2675
  if (!response.success) {
2425
2676
  return error2(response.error || "Action failed", "ACTION_FAILED");
@@ -3058,7 +3309,7 @@ function createServerHandlers(registry, executor, config) {
3058
3309
  };
3059
3310
  if (config?.appInfo) {
3060
3311
  response.uiBridge = {
3061
- version: "0.3.0",
3312
+ version: "0.6.11",
3062
3313
  ...config.appInfo,
3063
3314
  capabilities: ["elements", "components", "actions", "design"],
3064
3315
  elementCount: stats.elements,
@@ -3650,6 +3901,14 @@ var NativeUIBridgeServer = class {
3650
3901
  };
3651
3902
  };
3652
3903
  }
3904
+ /**
3905
+ * Whether the console/network capture should be installed. The dedicated
3906
+ * `observability` flag wins when set; otherwise falls back to `testHooks`
3907
+ * so consumers predating the flag keep their exact behavior.
3908
+ */
3909
+ observabilityEnabled() {
3910
+ return this.config.observability ?? this.config.testHooks === true;
3911
+ }
3653
3912
  /**
3654
3913
  * Set the server adapter
3655
3914
  */
@@ -3863,7 +4122,7 @@ var NativeUIBridgeServer = class {
3863
4122
  console.warn("[ui-bridge-native] See documentation for supported adapters.");
3864
4123
  return;
3865
4124
  }
3866
- if (this.config.testHooks === true) {
4125
+ if (this.observabilityEnabled()) {
3867
4126
  this.consoleErrorBuffer.install();
3868
4127
  this.networkRequestBuffer.install();
3869
4128
  }
@@ -5365,6 +5624,7 @@ function UIBridgeNativeProvider({
5365
5624
  verbose: config.verbose,
5366
5625
  onEvent
5367
5626
  });
5627
+ registryRef.current.setPixelRatio(PixelRatio.get());
5368
5628
  setGlobalRegistry(registryRef.current);
5369
5629
  }
5370
5630
  const registry = registryRef.current;
@@ -5395,6 +5655,10 @@ function UIBridgeNativeProvider({
5395
5655
  cors: true,
5396
5656
  appInfo: config.appInfo,
5397
5657
  testHooks: features.testHooks === true,
5658
+ // Dedicated console/network-capture flag; the server defaults it to
5659
+ // the `testHooks` value when undefined, so passing it through verbatim
5660
+ // keeps existing consumers unchanged.
5661
+ observability: features.observability,
5398
5662
  // Injected device-viewport getter for POST /control/page-health. The
5399
5663
  // server doesn't import `react-native` itself — the require/import
5400
5664
  // pattern there crashed the host RN app in 0.6.3/0.6.4 (Metro/Hermes
@@ -5462,6 +5726,7 @@ function UIBridgeNativeProvider({
5462
5726
  }, [
5463
5727
  features.server,
5464
5728
  features.testHooks,
5729
+ features.observability,
5465
5730
  config.serverPort,
5466
5731
  config.appInfo,
5467
5732
  registry,
@@ -5500,6 +5765,7 @@ function UIBridgeNativeProvider({
5500
5765
  cors: true,
5501
5766
  appInfo: config.appInfo,
5502
5767
  testHooks: features.testHooks === true,
5768
+ observability: features.observability,
5503
5769
  viewportProvider: () => {
5504
5770
  const win = Dimensions.get("window");
5505
5771
  return { width: win.width, height: win.height };
@@ -5703,12 +5969,41 @@ function scheduleDeadButtonWarning(bridge, id, type) {
5703
5969
  }
5704
5970
  function useUIElement(options) {
5705
5971
  const bridge = useUIBridgeNativeOptional();
5706
- const ref = useRef(null);
5707
5972
  const [registered, setRegistered] = useState(false);
5708
5973
  const [_layout, setLayout] = useState(null);
5709
5974
  const propsRef = useRef({});
5710
5975
  const registeredRef = useRef(false);
5711
5976
  const registeredIdRef = useRef(null);
5977
+ const bridgeRef = useRef(bridge);
5978
+ bridgeRef.current = bridge;
5979
+ const nodeRef = useRef(null);
5980
+ const refInterceptorRef = useRef(null);
5981
+ if (refInterceptorRef.current === null) {
5982
+ refInterceptorRef.current = {
5983
+ get current() {
5984
+ return nodeRef.current;
5985
+ },
5986
+ set current(node) {
5987
+ const prev = nodeRef.current;
5988
+ nodeRef.current = node;
5989
+ const liveBridge = bridgeRef.current;
5990
+ const registeredId = registeredIdRef.current;
5991
+ if (!liveBridge || !registeredRef.current || registeredId === null) return;
5992
+ if (node == null && prev != null) {
5993
+ const state = liveBridge.registry.getElement(registeredId)?.getState();
5994
+ if (state && state.visible !== false) {
5995
+ liveBridge.registry.updateElementState(registeredId, { visible: false });
5996
+ }
5997
+ } else if (node != null && prev == null) {
5998
+ const state = liveBridge.registry.getElement(registeredId)?.getState();
5999
+ if (state && state.visible !== true) {
6000
+ liveBridge.registry.updateElementState(registeredId, { visible: true });
6001
+ }
6002
+ }
6003
+ }
6004
+ };
6005
+ }
6006
+ const ref = refInterceptorRef.current;
5712
6007
  const {
5713
6008
  id,
5714
6009
  type = "custom",
@@ -5752,7 +6047,7 @@ function useUIElement(options) {
5752
6047
  }
5753
6048
  setRegistered(true);
5754
6049
  scheduleDeadButtonWarning(bridge, id, type);
5755
- }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
6050
+ }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp, ref]);
5756
6051
  const unregister = useCallback(() => {
5757
6052
  if (!bridge || !registeredRef.current) return;
5758
6053
  bridge.registry.unregisterElement(registeredIdRef.current ?? id);
@@ -5788,7 +6083,8 @@ function useUIElement(options) {
5788
6083
  writeLayout({ x, y, width, height, pageX: x, pageY: y });
5789
6084
  }
5790
6085
  },
5791
- [bridge, id]
6086
+ // `ref` is the once-per-hook interceptor object — identity-stable.
6087
+ [bridge, id, ref]
5792
6088
  );
5793
6089
  const registerRef = useRef(register);
5794
6090
  const unregisterRef = useRef(unregister);
@@ -5809,7 +6105,14 @@ function useUIElement(options) {
5809
6105
  useEffect(() => {
5810
6106
  if (!bridge || !registered) return;
5811
6107
  const node = ref.current;
5812
- if (!node || typeof node.measureInWindow !== "function") return;
6108
+ if (!node) {
6109
+ const seeded = bridge.registry.getElement(id)?.getState();
6110
+ if (seeded && seeded.visible !== false) {
6111
+ bridge.registry.updateElementState(id, { visible: false });
6112
+ }
6113
+ return;
6114
+ }
6115
+ if (typeof node.measureInWindow !== "function") return;
5813
6116
  let cancelled = false;
5814
6117
  let pendingTimer = null;
5815
6118
  const backoffMs = [50, 200, 500, 1e3, 2500];
@@ -5848,7 +6151,7 @@ function useUIElement(options) {
5848
6151
  cancelled = true;
5849
6152
  if (pendingTimer != null) clearTimeout(pendingTimer);
5850
6153
  };
5851
- }, [bridge, registered, id]);
6154
+ }, [bridge, registered, id, ref]);
5852
6155
  const registeredHandlerKeysRef = useRef([]);
5853
6156
  useEffect(() => {
5854
6157
  if (!bridge || !registered || !handlersProp) return;
@@ -6630,6 +6933,46 @@ function decodeTextPayload(payload) {
6630
6933
  return new TextDecoder().decode(payload);
6631
6934
  }
6632
6935
 
6936
+ // src/server/bind-retry.ts
6937
+ function isAddrInUseError(err) {
6938
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
6939
+ const code = err?.code;
6940
+ return message.includes("EADDRINUSE") || message.toLowerCase().includes("address already in use") || code === "EADDRINUSE";
6941
+ }
6942
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6943
+ async function bindWithRetry(attempt, options = {}) {
6944
+ const {
6945
+ minTotalMs = 3e4,
6946
+ initialDelayMs = 500,
6947
+ maxDelayMs = 5e3,
6948
+ backoffFactor = 2,
6949
+ logger = console,
6950
+ sleep: sleep2 = defaultSleep,
6951
+ now = Date.now
6952
+ } = options;
6953
+ const startedAt = now();
6954
+ let delay = initialDelayMs;
6955
+ let attemptIndex = 0;
6956
+ for (; ; ) {
6957
+ try {
6958
+ await attempt(attemptIndex);
6959
+ return;
6960
+ } catch (err) {
6961
+ const elapsed = now() - startedAt;
6962
+ const budgetExhausted = elapsed + delay >= minTotalMs;
6963
+ if (!isAddrInUseError(err) || budgetExhausted) {
6964
+ throw err;
6965
+ }
6966
+ logger.warn(
6967
+ `[ui-bridge-native] bind failed with EADDRINUSE (attempt ${attemptIndex + 1}); retrying in ${delay}ms (elapsed ${elapsed}ms / budget ${minTotalMs === Infinity ? "\u221E" : minTotalMs}ms)`
6968
+ );
6969
+ await sleep2(delay);
6970
+ delay = Math.min(delay * backoffFactor, maxDelayMs);
6971
+ attemptIndex += 1;
6972
+ }
6973
+ }
6974
+ }
6975
+
6633
6976
  // src/server/ws-connection.ts
6634
6977
  var WebSocketConnection = class {
6635
6978
  constructor(id, socket, onMessage, onClose) {
@@ -7196,6 +7539,6 @@ var styles = StyleSheet.create({
7196
7539
  }
7197
7540
  });
7198
7541
 
7199
- export { CloudRelayClient, DefaultNativeActionExecutor, DeviceAnnouncer, ModalDetector, NativeUIBridgeRegistry, NativeUIBridgeServer, State, ToastCapture, TransitionTo, UIBridgeInspector, UIBridgeNativeProvider, UI_BRIDGE_NATIVE_ROUTES, UndoTracker, WebSocketConnection, WebSocketEventBridge, buildTreePath, buildUpgradeResponse, captureNativeResponsiveSnapshot, captureNativeStateVariations, computeAcceptKey, createDesignHandlers, createNativeActionExecutor, createNativeElementIdentifier, createNativeServer, createServerHandlers, extractHandlerNames, findAllByPattern, findElementByIdentifier, getGlobalRegistry, getNativeElementDesignData, mapNativeStyleToExtended, matchesIdentifier, parseTreePath, resetGlobalRegistry, setGlobalRegistry, useToastRecorder, useUIBridge, useUIBridgeModal, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIBridgeToast, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
7542
+ 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 };
7200
7543
  //# sourceMappingURL=index.mjs.map
7201
7544
  //# sourceMappingURL=index.mjs.map