@qontinui/ui-bridge-native 0.6.10 → 0.6.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"UIBridgeNativeProvider.d.ts","sourceRoot":"","sources":["../../src/react/UIBridgeNativeProvider.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"UIBridgeNativeProvider.d.ts","sourceRoot":"","sources":["../../src/react/UIBridgeNativeProvider.tsx"],"names":[],"mappings":"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"}
@@ -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
  // ============================================================================
@@ -346,6 +562,103 @@ var NativeUIBridgeRegistry = class {
346
562
  console.log(`[ui-bridge-native] Marked ${cleared} elements offscreen for route: ${route}`);
347
563
  }
348
564
  }
565
+ /**
566
+ * Re-measure every mounted-visible element's geometry via its stored React
567
+ * ref and write the fresh coordinates into the registry ("measure-on-
568
+ * snapshot").
569
+ *
570
+ * Why: element geometry is otherwise captured ONCE at mount time
571
+ * (`onLayout` + `measureInWindow` in `useUIElement`) and RN never re-fires
572
+ * `onLayout` on pure translation — scrolling a virtualized list
573
+ * (FlashList), recycling a cell, a keyboard shift or an orientation change
574
+ * all move elements WITHOUT updating the stored `state.layout`. Snapshot
575
+ * consumers (vision analyzers hit-testing `bbox`, coord-based tap) then
576
+ * operate on stale positions. Re-measuring at READ time is correct
577
+ * regardless of what moved the element and needs zero per-consumer wiring:
578
+ * every registry entry already stores its ref (see `registerElement`).
579
+ *
580
+ * Per-element outcomes:
581
+ * - non-zero dims → write a fresh full layout (same state shape as the
582
+ * mount-time measure loop in `useUIElement`); counted as `measured`.
583
+ * - zero dims AND the element previously had a non-null `state.layout` →
584
+ * the view collapsed / left the screen; clear to
585
+ * `{ visible: false, layout: null }` (same shape `markRouteOffscreen`
586
+ * writes); counted as `cleared`.
587
+ * - zero dims and it never had a layout → leave state untouched (element
588
+ * in the mount→first-onLayout gap; don't demote `likely-visible`);
589
+ * counted as `skipped`.
590
+ * - no ref / no callable `measureInWindow` (test fixtures, web) → leave
591
+ * untouched; counted as `skipped`.
592
+ *
593
+ * The whole sweep races an overall timeout (default 250 ms) so a dead ref
594
+ * whose `measureInWindow` never calls back can never hang a snapshot: the
595
+ * returned promise resolves at the deadline with whatever counts have
596
+ * accumulated. Late callbacks may still write state afterwards — harmless,
597
+ * freshest data wins. Never throws.
598
+ *
599
+ * Non-React core module on purpose — `measureInWindow` is duck-typed off
600
+ * the stored ref rather than imported from `react-native` (see the
601
+ * `projectBbox` doc comment for why this file must not import RN).
602
+ */
603
+ async refreshMeasurements(options) {
604
+ const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 250;
605
+ const counts = { measured: 0, cleared: 0, skipped: 0 };
606
+ const pending = [];
607
+ for (const element of this.getMountedVisibleElements()) {
608
+ const node = element.ref?.current;
609
+ if (!node || typeof node.measureInWindow !== "function") {
610
+ counts.skipped++;
611
+ continue;
612
+ }
613
+ const id = element.id;
614
+ pending.push(
615
+ new Promise((resolve) => {
616
+ let settled = false;
617
+ try {
618
+ node.measureInWindow((pageX, pageY, w, h) => {
619
+ if (settled) return;
620
+ settled = true;
621
+ try {
622
+ if (w > 0 && h > 0) {
623
+ this.updateElementState(id, {
624
+ mounted: true,
625
+ visible: true,
626
+ enabled: true,
627
+ focused: false,
628
+ layout: { x: pageX, y: pageY, width: w, height: h, pageX, pageY }
629
+ });
630
+ counts.measured++;
631
+ } else if (this.elements.get(id)?.getState().layout != null) {
632
+ this.updateElementState(id, { visible: false, layout: null });
633
+ counts.cleared++;
634
+ } else {
635
+ counts.skipped++;
636
+ }
637
+ } finally {
638
+ resolve();
639
+ }
640
+ });
641
+ } catch {
642
+ if (!settled) {
643
+ settled = true;
644
+ counts.skipped++;
645
+ resolve();
646
+ }
647
+ }
648
+ })
649
+ );
650
+ }
651
+ if (pending.length > 0) {
652
+ await new Promise((resolve) => {
653
+ const timer = setTimeout(resolve, timeoutMs);
654
+ void Promise.all(pending).then(() => {
655
+ clearTimeout(timer);
656
+ resolve();
657
+ });
658
+ });
659
+ }
660
+ return counts;
661
+ }
349
662
  /**
350
663
  * Update element state
351
664
  */
@@ -660,6 +973,18 @@ var NativeUIBridgeRegistry = class {
660
973
  const handlers = extractHandlerNames(e.props);
661
974
  const state = e.getState();
662
975
  const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
976
+ const bbox = projectBbox(state, visibility, this.pixelRatio);
977
+ const vision = projectVisionFields({
978
+ type: e.type,
979
+ label: e.label,
980
+ // Real interactivity signal: the names of props that are actually
981
+ // functions (onPress/onChangeText/...). NOT `e.actions`, which
982
+ // `inferActions` synthesizes press/click onto for every element —
983
+ // using it would mark plain text/view nodes interactable.
984
+ handlerNames: handlers,
985
+ state,
986
+ flatStyle: e.flatStyle
987
+ });
663
988
  return {
664
989
  id: e.id,
665
990
  type: e.type,
@@ -670,7 +995,9 @@ var NativeUIBridgeRegistry = class {
670
995
  customActions: e.customActions ? Object.keys(e.customActions) : void 0,
671
996
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
672
997
  registrationRoute: e.registrationRoute,
673
- visibility
998
+ visibility,
999
+ ...bbox ? { bbox } : {},
1000
+ ...vision
674
1001
  };
675
1002
  }),
676
1003
  components: this.getAllComponents().map((c) => ({
@@ -1844,7 +2171,7 @@ function createDesignHandlers(registry, viewportProvider) {
1844
2171
  var GRID_SIZE = 20;
1845
2172
  var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
1846
2173
  var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
1847
- var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
2174
+ var INTERACTIVE_TYPES2 = /* @__PURE__ */ new Set([
1848
2175
  "button",
1849
2176
  "input",
1850
2177
  "switch",
@@ -2041,7 +2368,7 @@ function diagnosePageHealth(elements, viewport) {
2041
2368
  let interactiveTotal = 0;
2042
2369
  let interactiveDisabled = 0;
2043
2370
  for (const el of elements) {
2044
- if (!INTERACTIVE_TYPES.has(el.type)) continue;
2371
+ if (!INTERACTIVE_TYPES2.has(el.type)) continue;
2045
2372
  interactiveTotal++;
2046
2373
  if (el.state.enabled === false) interactiveDisabled++;
2047
2374
  }
@@ -2314,6 +2641,7 @@ function createServerHandlers(registry, executor, config) {
2314
2641
  return {
2315
2642
  // Elements
2316
2643
  getElements: async (ctx) => {
2644
+ await registry.refreshMeasurements();
2317
2645
  const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
2318
2646
  const forRoute = ctx.query?.route || ctx.body?.route;
2319
2647
  let allElements = visibleOnly ? registry.getMountedVisibleElements() : registry.getAllElements();
@@ -2324,6 +2652,16 @@ function createServerHandlers(registry, executor, config) {
2324
2652
  const handlers = extractHandlerNames(e.props);
2325
2653
  const state = e.getState();
2326
2654
  const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
2655
+ const bbox = projectBbox(state, visibility, registry.getPixelRatio());
2656
+ const vision = projectVisionFields({
2657
+ type: e.type,
2658
+ label: e.label,
2659
+ // Real handler names (onPress/...), NOT inferred actions which
2660
+ // synthesize press/click onto every element. See projectVisionFields.
2661
+ handlerNames: handlers,
2662
+ state,
2663
+ flatStyle: e.flatStyle
2664
+ });
2327
2665
  return {
2328
2666
  id: e.id,
2329
2667
  type: e.type,
@@ -2334,7 +2672,9 @@ function createServerHandlers(registry, executor, config) {
2334
2672
  customActions: e.customActions ? Object.keys(e.customActions) : void 0,
2335
2673
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
2336
2674
  registrationRoute: e.registrationRoute,
2337
- visibility
2675
+ visibility,
2676
+ ...bbox ? { bbox } : {},
2677
+ ...vision
2338
2678
  };
2339
2679
  });
2340
2680
  return success2({ elements });
@@ -2495,6 +2835,7 @@ function createServerHandlers(registry, executor, config) {
2495
2835
  });
2496
2836
  },
2497
2837
  getSnapshot: async (ctx) => {
2838
+ await registry.refreshMeasurements();
2498
2839
  const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
2499
2840
  const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
2500
2841
  const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
@@ -2810,6 +3151,7 @@ function createServerHandlers(registry, executor, config) {
2810
3151
  // that don't measure (the fallback only matches single-rooted screens but
2811
3152
  // is good enough for the unit suite).
2812
3153
  tapAt: async (ctx) => {
3154
+ await registry.refreshMeasurements();
2813
3155
  const body = ctx.body;
2814
3156
  const x = body?.x;
2815
3157
  const y = body?.y;
@@ -3017,7 +3359,7 @@ function createServerHandlers(registry, executor, config) {
3017
3359
  };
3018
3360
  if (config?.appInfo) {
3019
3361
  response.uiBridge = {
3020
- version: "0.6.10",
3362
+ version: "0.6.12",
3021
3363
  ...config.appInfo,
3022
3364
  capabilities: ["elements", "components", "actions", "design"],
3023
3365
  elementCount: stats.elements,
@@ -3609,6 +3951,14 @@ var NativeUIBridgeServer = class {
3609
3951
  };
3610
3952
  };
3611
3953
  }
3954
+ /**
3955
+ * Whether the console/network capture should be installed. The dedicated
3956
+ * `observability` flag wins when set; otherwise falls back to `testHooks`
3957
+ * so consumers predating the flag keep their exact behavior.
3958
+ */
3959
+ observabilityEnabled() {
3960
+ return this.config.observability ?? this.config.testHooks === true;
3961
+ }
3612
3962
  /**
3613
3963
  * Set the server adapter
3614
3964
  */
@@ -3822,7 +4172,7 @@ var NativeUIBridgeServer = class {
3822
4172
  console.warn("[ui-bridge-native] See documentation for supported adapters.");
3823
4173
  return;
3824
4174
  }
3825
- if (this.config.testHooks === true) {
4175
+ if (this.observabilityEnabled()) {
3826
4176
  this.consoleErrorBuffer.install();
3827
4177
  this.networkRequestBuffer.install();
3828
4178
  }
@@ -5324,6 +5674,7 @@ function UIBridgeNativeProvider({
5324
5674
  verbose: config.verbose,
5325
5675
  onEvent
5326
5676
  });
5677
+ registryRef.current.setPixelRatio(reactNative.PixelRatio.get());
5327
5678
  setGlobalRegistry(registryRef.current);
5328
5679
  }
5329
5680
  const registry = registryRef.current;
@@ -5354,6 +5705,10 @@ function UIBridgeNativeProvider({
5354
5705
  cors: true,
5355
5706
  appInfo: config.appInfo,
5356
5707
  testHooks: features.testHooks === true,
5708
+ // Dedicated console/network-capture flag; the server defaults it to
5709
+ // the `testHooks` value when undefined, so passing it through verbatim
5710
+ // keeps existing consumers unchanged.
5711
+ observability: features.observability,
5357
5712
  // Injected device-viewport getter for POST /control/page-health. The
5358
5713
  // server doesn't import `react-native` itself — the require/import
5359
5714
  // pattern there crashed the host RN app in 0.6.3/0.6.4 (Metro/Hermes
@@ -5421,6 +5776,7 @@ function UIBridgeNativeProvider({
5421
5776
  }, [
5422
5777
  features.server,
5423
5778
  features.testHooks,
5779
+ features.observability,
5424
5780
  config.serverPort,
5425
5781
  config.appInfo,
5426
5782
  registry,
@@ -5459,6 +5815,7 @@ function UIBridgeNativeProvider({
5459
5815
  cors: true,
5460
5816
  appInfo: config.appInfo,
5461
5817
  testHooks: features.testHooks === true,
5818
+ observability: features.observability,
5462
5819
  viewportProvider: () => {
5463
5820
  const win = reactNative.Dimensions.get("window");
5464
5821
  return { width: win.width, height: win.height };
@@ -5662,12 +6019,41 @@ function scheduleDeadButtonWarning(bridge, id, type) {
5662
6019
  }
5663
6020
  function useUIElement(options) {
5664
6021
  const bridge = useUIBridgeNativeOptional();
5665
- const ref = react.useRef(null);
5666
6022
  const [registered, setRegistered] = react.useState(false);
5667
6023
  const [_layout, setLayout] = react.useState(null);
5668
6024
  const propsRef = react.useRef({});
5669
6025
  const registeredRef = react.useRef(false);
5670
6026
  const registeredIdRef = react.useRef(null);
6027
+ const bridgeRef = react.useRef(bridge);
6028
+ bridgeRef.current = bridge;
6029
+ const nodeRef = react.useRef(null);
6030
+ const refInterceptorRef = react.useRef(null);
6031
+ if (refInterceptorRef.current === null) {
6032
+ refInterceptorRef.current = {
6033
+ get current() {
6034
+ return nodeRef.current;
6035
+ },
6036
+ set current(node) {
6037
+ const prev = nodeRef.current;
6038
+ nodeRef.current = node;
6039
+ const liveBridge = bridgeRef.current;
6040
+ const registeredId = registeredIdRef.current;
6041
+ if (!liveBridge || !registeredRef.current || registeredId === null) return;
6042
+ if (node == null && prev != null) {
6043
+ const state = liveBridge.registry.getElement(registeredId)?.getState();
6044
+ if (state && state.visible !== false) {
6045
+ liveBridge.registry.updateElementState(registeredId, { visible: false });
6046
+ }
6047
+ } else if (node != null && prev == null) {
6048
+ const state = liveBridge.registry.getElement(registeredId)?.getState();
6049
+ if (state && state.visible !== true) {
6050
+ liveBridge.registry.updateElementState(registeredId, { visible: true });
6051
+ }
6052
+ }
6053
+ }
6054
+ };
6055
+ }
6056
+ const ref = refInterceptorRef.current;
5671
6057
  const {
5672
6058
  id,
5673
6059
  type = "custom",
@@ -5711,7 +6097,7 @@ function useUIElement(options) {
5711
6097
  }
5712
6098
  setRegistered(true);
5713
6099
  scheduleDeadButtonWarning(bridge, id, type);
5714
- }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
6100
+ }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp, ref]);
5715
6101
  const unregister = react.useCallback(() => {
5716
6102
  if (!bridge || !registeredRef.current) return;
5717
6103
  bridge.registry.unregisterElement(registeredIdRef.current ?? id);
@@ -5747,7 +6133,8 @@ function useUIElement(options) {
5747
6133
  writeLayout({ x, y, width, height, pageX: x, pageY: y });
5748
6134
  }
5749
6135
  },
5750
- [bridge, id]
6136
+ // `ref` is the once-per-hook interceptor object — identity-stable.
6137
+ [bridge, id, ref]
5751
6138
  );
5752
6139
  const registerRef = react.useRef(register);
5753
6140
  const unregisterRef = react.useRef(unregister);
@@ -5764,11 +6151,18 @@ function useUIElement(options) {
5764
6151
  unregisterRef.current();
5765
6152
  }
5766
6153
  };
5767
- }, [autoRegister, bridge]);
6154
+ }, [autoRegister, bridge, id]);
5768
6155
  react.useEffect(() => {
5769
6156
  if (!bridge || !registered) return;
5770
6157
  const node = ref.current;
5771
- if (!node || typeof node.measureInWindow !== "function") return;
6158
+ if (!node) {
6159
+ const seeded = bridge.registry.getElement(id)?.getState();
6160
+ if (seeded && seeded.visible !== false) {
6161
+ bridge.registry.updateElementState(id, { visible: false });
6162
+ }
6163
+ return;
6164
+ }
6165
+ if (typeof node.measureInWindow !== "function") return;
5772
6166
  let cancelled = false;
5773
6167
  let pendingTimer = null;
5774
6168
  const backoffMs = [50, 200, 500, 1e3, 2500];
@@ -5807,7 +6201,7 @@ function useUIElement(options) {
5807
6201
  cancelled = true;
5808
6202
  if (pendingTimer != null) clearTimeout(pendingTimer);
5809
6203
  };
5810
- }, [bridge, registered, id]);
6204
+ }, [bridge, registered, id, ref]);
5811
6205
  const registeredHandlerKeysRef = react.useRef([]);
5812
6206
  react.useEffect(() => {
5813
6207
  if (!bridge || !registered || !handlersProp) return;