@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.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) => ({
@@ -987,9 +1217,10 @@ var DefaultNativeActionExecutor = class {
987
1217
  requestId: request.requestId,
988
1218
  durationMs: successTime - startTime
989
1219
  });
1220
+ const fresh = this.registry.getElement(registered.id) ?? registered;
990
1221
  return {
991
1222
  success: true,
992
- elementState: registered.getState(),
1223
+ elementState: fresh.getState(),
993
1224
  result,
994
1225
  durationMs: successTime - startTime,
995
1226
  timestamp: successTime,
@@ -1895,7 +2126,7 @@ function createDesignHandlers(registry, viewportProvider) {
1895
2126
  var GRID_SIZE = 20;
1896
2127
  var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
1897
2128
  var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
1898
- var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
2129
+ var INTERACTIVE_TYPES2 = /* @__PURE__ */ new Set([
1899
2130
  "button",
1900
2131
  "input",
1901
2132
  "switch",
@@ -2092,7 +2323,7 @@ function diagnosePageHealth(elements, viewport) {
2092
2323
  let interactiveTotal = 0;
2093
2324
  let interactiveDisabled = 0;
2094
2325
  for (const el of elements) {
2095
- if (!INTERACTIVE_TYPES.has(el.type)) continue;
2326
+ if (!INTERACTIVE_TYPES2.has(el.type)) continue;
2096
2327
  interactiveTotal++;
2097
2328
  if (el.state.enabled === false) interactiveDisabled++;
2098
2329
  }
@@ -2160,6 +2391,13 @@ function matchElementsByText(registry, text, opts) {
2160
2391
  scored.sort((a, b) => a.rank - b.rank);
2161
2392
  return scored.map((s) => s.element);
2162
2393
  }
2394
+ function resolveElementState(element) {
2395
+ const state = element.getState();
2396
+ if (element.type === "input" && state.value === void 0 && typeof element.props?.value === "string") {
2397
+ return { ...state, value: element.props.value };
2398
+ }
2399
+ return state;
2400
+ }
2163
2401
  async function executeWorkflowStep(step, registry, executor) {
2164
2402
  const startTime = Date.now();
2165
2403
  try {
@@ -2368,6 +2606,16 @@ function createServerHandlers(registry, executor, config) {
2368
2606
  const handlers = extractHandlerNames(e.props);
2369
2607
  const state = e.getState();
2370
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
+ });
2371
2619
  return {
2372
2620
  id: e.id,
2373
2621
  type: e.type,
@@ -2378,7 +2626,9 @@ function createServerHandlers(registry, executor, config) {
2378
2626
  customActions: e.customActions ? Object.keys(e.customActions) : void 0,
2379
2627
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
2380
2628
  registrationRoute: e.registrationRoute,
2381
- visibility
2629
+ visibility,
2630
+ ...bbox ? { bbox } : {},
2631
+ ...vision
2382
2632
  };
2383
2633
  });
2384
2634
  return success2({ elements });
@@ -2396,7 +2646,7 @@ function createServerHandlers(registry, executor, config) {
2396
2646
  type: element.type,
2397
2647
  label: element.label,
2398
2648
  identifier: element.getIdentifier(),
2399
- state: element.getState(),
2649
+ state: resolveElementState(element),
2400
2650
  actions: element.actions,
2401
2651
  customActions: element.customActions ? Object.keys(element.customActions) : void 0,
2402
2652
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
@@ -2410,18 +2660,19 @@ function createServerHandlers(registry, executor, config) {
2410
2660
  if (!element) {
2411
2661
  return error2(`Element not found: ${id}`, "ELEMENT_NOT_FOUND");
2412
2662
  }
2413
- return success2({ state: element.getState() });
2663
+ return success2({ state: resolveElementState(element) });
2414
2664
  },
2415
2665
  executeAction: async (ctx) => {
2416
- const { id } = ctx.params;
2417
- const body = ctx.body;
2418
- if (!body?.action) {
2666
+ const rawBody = ctx.body ?? {};
2667
+ const envelope = rawBody.request && typeof rawBody.request === "object" ? rawBody.request : rawBody;
2668
+ const id = ctx.params.id ?? rawBody.id ?? "";
2669
+ if (!envelope?.action) {
2419
2670
  return error2("Action is required", "INVALID_REQUEST");
2420
2671
  }
2421
2672
  const response = await executor.executeAction(id, {
2422
- action: body.action,
2423
- params: body.params,
2424
- waitOptions: body.waitOptions
2673
+ action: envelope.action,
2674
+ params: envelope.params,
2675
+ waitOptions: envelope.waitOptions
2425
2676
  });
2426
2677
  if (!response.success) {
2427
2678
  return error2(response.error || "Action failed", "ACTION_FAILED");
@@ -3060,7 +3311,7 @@ function createServerHandlers(registry, executor, config) {
3060
3311
  };
3061
3312
  if (config?.appInfo) {
3062
3313
  response.uiBridge = {
3063
- version: "0.3.0",
3314
+ version: "0.6.11",
3064
3315
  ...config.appInfo,
3065
3316
  capabilities: ["elements", "components", "actions", "design"],
3066
3317
  elementCount: stats.elements,
@@ -3652,6 +3903,14 @@ var NativeUIBridgeServer = class {
3652
3903
  };
3653
3904
  };
3654
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
+ }
3655
3914
  /**
3656
3915
  * Set the server adapter
3657
3916
  */
@@ -3865,7 +4124,7 @@ var NativeUIBridgeServer = class {
3865
4124
  console.warn("[ui-bridge-native] See documentation for supported adapters.");
3866
4125
  return;
3867
4126
  }
3868
- if (this.config.testHooks === true) {
4127
+ if (this.observabilityEnabled()) {
3869
4128
  this.consoleErrorBuffer.install();
3870
4129
  this.networkRequestBuffer.install();
3871
4130
  }
@@ -5367,6 +5626,7 @@ function UIBridgeNativeProvider({
5367
5626
  verbose: config.verbose,
5368
5627
  onEvent
5369
5628
  });
5629
+ registryRef.current.setPixelRatio(reactNative.PixelRatio.get());
5370
5630
  setGlobalRegistry(registryRef.current);
5371
5631
  }
5372
5632
  const registry = registryRef.current;
@@ -5397,6 +5657,10 @@ function UIBridgeNativeProvider({
5397
5657
  cors: true,
5398
5658
  appInfo: config.appInfo,
5399
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,
5400
5664
  // Injected device-viewport getter for POST /control/page-health. The
5401
5665
  // server doesn't import `react-native` itself — the require/import
5402
5666
  // pattern there crashed the host RN app in 0.6.3/0.6.4 (Metro/Hermes
@@ -5464,6 +5728,7 @@ function UIBridgeNativeProvider({
5464
5728
  }, [
5465
5729
  features.server,
5466
5730
  features.testHooks,
5731
+ features.observability,
5467
5732
  config.serverPort,
5468
5733
  config.appInfo,
5469
5734
  registry,
@@ -5502,6 +5767,7 @@ function UIBridgeNativeProvider({
5502
5767
  cors: true,
5503
5768
  appInfo: config.appInfo,
5504
5769
  testHooks: features.testHooks === true,
5770
+ observability: features.observability,
5505
5771
  viewportProvider: () => {
5506
5772
  const win = reactNative.Dimensions.get("window");
5507
5773
  return { width: win.width, height: win.height };
@@ -5705,12 +5971,41 @@ function scheduleDeadButtonWarning(bridge, id, type) {
5705
5971
  }
5706
5972
  function useUIElement(options) {
5707
5973
  const bridge = useUIBridgeNativeOptional();
5708
- const ref = react.useRef(null);
5709
5974
  const [registered, setRegistered] = react.useState(false);
5710
5975
  const [_layout, setLayout] = react.useState(null);
5711
5976
  const propsRef = react.useRef({});
5712
5977
  const registeredRef = react.useRef(false);
5713
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;
5714
6009
  const {
5715
6010
  id,
5716
6011
  type = "custom",
@@ -5754,7 +6049,7 @@ function useUIElement(options) {
5754
6049
  }
5755
6050
  setRegistered(true);
5756
6051
  scheduleDeadButtonWarning(bridge, id, type);
5757
- }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
6052
+ }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp, ref]);
5758
6053
  const unregister = react.useCallback(() => {
5759
6054
  if (!bridge || !registeredRef.current) return;
5760
6055
  bridge.registry.unregisterElement(registeredIdRef.current ?? id);
@@ -5790,7 +6085,8 @@ function useUIElement(options) {
5790
6085
  writeLayout({ x, y, width, height, pageX: x, pageY: y });
5791
6086
  }
5792
6087
  },
5793
- [bridge, id]
6088
+ // `ref` is the once-per-hook interceptor object — identity-stable.
6089
+ [bridge, id, ref]
5794
6090
  );
5795
6091
  const registerRef = react.useRef(register);
5796
6092
  const unregisterRef = react.useRef(unregister);
@@ -5811,7 +6107,14 @@ function useUIElement(options) {
5811
6107
  react.useEffect(() => {
5812
6108
  if (!bridge || !registered) return;
5813
6109
  const node = ref.current;
5814
- 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;
5815
6118
  let cancelled = false;
5816
6119
  let pendingTimer = null;
5817
6120
  const backoffMs = [50, 200, 500, 1e3, 2500];
@@ -5850,7 +6153,7 @@ function useUIElement(options) {
5850
6153
  cancelled = true;
5851
6154
  if (pendingTimer != null) clearTimeout(pendingTimer);
5852
6155
  };
5853
- }, [bridge, registered, id]);
6156
+ }, [bridge, registered, id, ref]);
5854
6157
  const registeredHandlerKeysRef = react.useRef([]);
5855
6158
  react.useEffect(() => {
5856
6159
  if (!bridge || !registered || !handlersProp) return;
@@ -6632,6 +6935,46 @@ function decodeTextPayload(payload) {
6632
6935
  return new TextDecoder().decode(payload);
6633
6936
  }
6634
6937
 
6938
+ // src/server/bind-retry.ts
6939
+ function isAddrInUseError(err) {
6940
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
6941
+ const code = err?.code;
6942
+ return message.includes("EADDRINUSE") || message.toLowerCase().includes("address already in use") || code === "EADDRINUSE";
6943
+ }
6944
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6945
+ async function bindWithRetry(attempt, options = {}) {
6946
+ const {
6947
+ minTotalMs = 3e4,
6948
+ initialDelayMs = 500,
6949
+ maxDelayMs = 5e3,
6950
+ backoffFactor = 2,
6951
+ logger = console,
6952
+ sleep: sleep2 = defaultSleep,
6953
+ now = Date.now
6954
+ } = options;
6955
+ const startedAt = now();
6956
+ let delay = initialDelayMs;
6957
+ let attemptIndex = 0;
6958
+ for (; ; ) {
6959
+ try {
6960
+ await attempt(attemptIndex);
6961
+ return;
6962
+ } catch (err) {
6963
+ const elapsed = now() - startedAt;
6964
+ const budgetExhausted = elapsed + delay >= minTotalMs;
6965
+ if (!isAddrInUseError(err) || budgetExhausted) {
6966
+ throw err;
6967
+ }
6968
+ logger.warn(
6969
+ `[ui-bridge-native] bind failed with EADDRINUSE (attempt ${attemptIndex + 1}); retrying in ${delay}ms (elapsed ${elapsed}ms / budget ${minTotalMs === Infinity ? "\u221E" : minTotalMs}ms)`
6970
+ );
6971
+ await sleep2(delay);
6972
+ delay = Math.min(delay * backoffFactor, maxDelayMs);
6973
+ attemptIndex += 1;
6974
+ }
6975
+ }
6976
+ }
6977
+
6635
6978
  // src/server/ws-connection.ts
6636
6979
  var WebSocketConnection = class {
6637
6980
  constructor(id, socket, onMessage, onClose) {
@@ -7213,6 +7556,7 @@ exports.UI_BRIDGE_NATIVE_ROUTES = UI_BRIDGE_NATIVE_ROUTES;
7213
7556
  exports.UndoTracker = UndoTracker;
7214
7557
  exports.WebSocketConnection = WebSocketConnection;
7215
7558
  exports.WebSocketEventBridge = WebSocketEventBridge;
7559
+ exports.bindWithRetry = bindWithRetry;
7216
7560
  exports.buildTreePath = buildTreePath;
7217
7561
  exports.buildUpgradeResponse = buildUpgradeResponse;
7218
7562
  exports.captureNativeResponsiveSnapshot = captureNativeResponsiveSnapshot;
@@ -7223,15 +7567,22 @@ exports.createNativeActionExecutor = createNativeActionExecutor;
7223
7567
  exports.createNativeElementIdentifier = createNativeElementIdentifier;
7224
7568
  exports.createNativeServer = createNativeServer;
7225
7569
  exports.createServerHandlers = createServerHandlers;
7570
+ exports.deriveInteractable = deriveInteractable;
7571
+ exports.deriveText = deriveText;
7226
7572
  exports.extractHandlerNames = extractHandlerNames;
7227
7573
  exports.findAllByPattern = findAllByPattern;
7228
7574
  exports.findElementByIdentifier = findElementByIdentifier;
7229
7575
  exports.getGlobalRegistry = getGlobalRegistry;
7230
7576
  exports.getNativeElementDesignData = getNativeElementDesignData;
7577
+ exports.isAddrInUseError = isAddrInUseError;
7231
7578
  exports.mapNativeStyleToExtended = mapNativeStyleToExtended;
7232
7579
  exports.matchesIdentifier = matchesIdentifier;
7580
+ exports.parseColorToRgb = parseColorToRgb;
7581
+ exports.parseFontSizePx = parseFontSizePx;
7233
7582
  exports.parseTreePath = parseTreePath;
7583
+ exports.projectVisionFields = projectVisionFields;
7234
7584
  exports.resetGlobalRegistry = resetGlobalRegistry;
7585
+ exports.roleForType = roleForType;
7235
7586
  exports.setGlobalRegistry = setGlobalRegistry;
7236
7587
  exports.useToastRecorder = useToastRecorder;
7237
7588
  exports.useUIBridge = useUIBridge;