@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.
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
  // ============================================================================
@@ -344,6 +560,103 @@ var NativeUIBridgeRegistry = class {
344
560
  console.log(`[ui-bridge-native] Marked ${cleared} elements offscreen for route: ${route}`);
345
561
  }
346
562
  }
563
+ /**
564
+ * Re-measure every mounted-visible element's geometry via its stored React
565
+ * ref and write the fresh coordinates into the registry ("measure-on-
566
+ * snapshot").
567
+ *
568
+ * Why: element geometry is otherwise captured ONCE at mount time
569
+ * (`onLayout` + `measureInWindow` in `useUIElement`) and RN never re-fires
570
+ * `onLayout` on pure translation — scrolling a virtualized list
571
+ * (FlashList), recycling a cell, a keyboard shift or an orientation change
572
+ * all move elements WITHOUT updating the stored `state.layout`. Snapshot
573
+ * consumers (vision analyzers hit-testing `bbox`, coord-based tap) then
574
+ * operate on stale positions. Re-measuring at READ time is correct
575
+ * regardless of what moved the element and needs zero per-consumer wiring:
576
+ * every registry entry already stores its ref (see `registerElement`).
577
+ *
578
+ * Per-element outcomes:
579
+ * - non-zero dims → write a fresh full layout (same state shape as the
580
+ * mount-time measure loop in `useUIElement`); counted as `measured`.
581
+ * - zero dims AND the element previously had a non-null `state.layout` →
582
+ * the view collapsed / left the screen; clear to
583
+ * `{ visible: false, layout: null }` (same shape `markRouteOffscreen`
584
+ * writes); counted as `cleared`.
585
+ * - zero dims and it never had a layout → leave state untouched (element
586
+ * in the mount→first-onLayout gap; don't demote `likely-visible`);
587
+ * counted as `skipped`.
588
+ * - no ref / no callable `measureInWindow` (test fixtures, web) → leave
589
+ * untouched; counted as `skipped`.
590
+ *
591
+ * The whole sweep races an overall timeout (default 250 ms) so a dead ref
592
+ * whose `measureInWindow` never calls back can never hang a snapshot: the
593
+ * returned promise resolves at the deadline with whatever counts have
594
+ * accumulated. Late callbacks may still write state afterwards — harmless,
595
+ * freshest data wins. Never throws.
596
+ *
597
+ * Non-React core module on purpose — `measureInWindow` is duck-typed off
598
+ * the stored ref rather than imported from `react-native` (see the
599
+ * `projectBbox` doc comment for why this file must not import RN).
600
+ */
601
+ async refreshMeasurements(options) {
602
+ const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 250;
603
+ const counts = { measured: 0, cleared: 0, skipped: 0 };
604
+ const pending = [];
605
+ for (const element of this.getMountedVisibleElements()) {
606
+ const node = element.ref?.current;
607
+ if (!node || typeof node.measureInWindow !== "function") {
608
+ counts.skipped++;
609
+ continue;
610
+ }
611
+ const id = element.id;
612
+ pending.push(
613
+ new Promise((resolve) => {
614
+ let settled = false;
615
+ try {
616
+ node.measureInWindow((pageX, pageY, w, h) => {
617
+ if (settled) return;
618
+ settled = true;
619
+ try {
620
+ if (w > 0 && h > 0) {
621
+ this.updateElementState(id, {
622
+ mounted: true,
623
+ visible: true,
624
+ enabled: true,
625
+ focused: false,
626
+ layout: { x: pageX, y: pageY, width: w, height: h, pageX, pageY }
627
+ });
628
+ counts.measured++;
629
+ } else if (this.elements.get(id)?.getState().layout != null) {
630
+ this.updateElementState(id, { visible: false, layout: null });
631
+ counts.cleared++;
632
+ } else {
633
+ counts.skipped++;
634
+ }
635
+ } finally {
636
+ resolve();
637
+ }
638
+ });
639
+ } catch {
640
+ if (!settled) {
641
+ settled = true;
642
+ counts.skipped++;
643
+ resolve();
644
+ }
645
+ }
646
+ })
647
+ );
648
+ }
649
+ if (pending.length > 0) {
650
+ await new Promise((resolve) => {
651
+ const timer = setTimeout(resolve, timeoutMs);
652
+ void Promise.all(pending).then(() => {
653
+ clearTimeout(timer);
654
+ resolve();
655
+ });
656
+ });
657
+ }
658
+ return counts;
659
+ }
347
660
  /**
348
661
  * Update element state
349
662
  */
@@ -658,6 +971,18 @@ var NativeUIBridgeRegistry = class {
658
971
  const handlers = extractHandlerNames(e.props);
659
972
  const state = e.getState();
660
973
  const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
974
+ const bbox = projectBbox(state, visibility, this.pixelRatio);
975
+ const vision = projectVisionFields({
976
+ type: e.type,
977
+ label: e.label,
978
+ // Real interactivity signal: the names of props that are actually
979
+ // functions (onPress/onChangeText/...). NOT `e.actions`, which
980
+ // `inferActions` synthesizes press/click onto for every element —
981
+ // using it would mark plain text/view nodes interactable.
982
+ handlerNames: handlers,
983
+ state,
984
+ flatStyle: e.flatStyle
985
+ });
661
986
  return {
662
987
  id: e.id,
663
988
  type: e.type,
@@ -668,7 +993,9 @@ var NativeUIBridgeRegistry = class {
668
993
  customActions: e.customActions ? Object.keys(e.customActions) : void 0,
669
994
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
670
995
  registrationRoute: e.registrationRoute,
671
- visibility
996
+ visibility,
997
+ ...bbox ? { bbox } : {},
998
+ ...vision
672
999
  };
673
1000
  }),
674
1001
  components: this.getAllComponents().map((c) => ({
@@ -1894,7 +2221,7 @@ function createDesignHandlers(registry, viewportProvider) {
1894
2221
  var GRID_SIZE = 20;
1895
2222
  var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
1896
2223
  var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
1897
- var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
2224
+ var INTERACTIVE_TYPES2 = /* @__PURE__ */ new Set([
1898
2225
  "button",
1899
2226
  "input",
1900
2227
  "switch",
@@ -2091,7 +2418,7 @@ function diagnosePageHealth(elements, viewport) {
2091
2418
  let interactiveTotal = 0;
2092
2419
  let interactiveDisabled = 0;
2093
2420
  for (const el of elements) {
2094
- if (!INTERACTIVE_TYPES.has(el.type)) continue;
2421
+ if (!INTERACTIVE_TYPES2.has(el.type)) continue;
2095
2422
  interactiveTotal++;
2096
2423
  if (el.state.enabled === false) interactiveDisabled++;
2097
2424
  }
@@ -2364,6 +2691,7 @@ function createServerHandlers(registry, executor, config) {
2364
2691
  return {
2365
2692
  // Elements
2366
2693
  getElements: async (ctx) => {
2694
+ await registry.refreshMeasurements();
2367
2695
  const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
2368
2696
  const forRoute = ctx.query?.route || ctx.body?.route;
2369
2697
  let allElements = visibleOnly ? registry.getMountedVisibleElements() : registry.getAllElements();
@@ -2374,6 +2702,16 @@ function createServerHandlers(registry, executor, config) {
2374
2702
  const handlers = extractHandlerNames(e.props);
2375
2703
  const state = e.getState();
2376
2704
  const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
2705
+ const bbox = projectBbox(state, visibility, registry.getPixelRatio());
2706
+ const vision = projectVisionFields({
2707
+ type: e.type,
2708
+ label: e.label,
2709
+ // Real handler names (onPress/...), NOT inferred actions which
2710
+ // synthesize press/click onto every element. See projectVisionFields.
2711
+ handlerNames: handlers,
2712
+ state,
2713
+ flatStyle: e.flatStyle
2714
+ });
2377
2715
  return {
2378
2716
  id: e.id,
2379
2717
  type: e.type,
@@ -2384,7 +2722,9 @@ function createServerHandlers(registry, executor, config) {
2384
2722
  customActions: e.customActions ? Object.keys(e.customActions) : void 0,
2385
2723
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
2386
2724
  registrationRoute: e.registrationRoute,
2387
- visibility
2725
+ visibility,
2726
+ ...bbox ? { bbox } : {},
2727
+ ...vision
2388
2728
  };
2389
2729
  });
2390
2730
  return success2({ elements });
@@ -2545,6 +2885,7 @@ function createServerHandlers(registry, executor, config) {
2545
2885
  });
2546
2886
  },
2547
2887
  getSnapshot: async (ctx) => {
2888
+ await registry.refreshMeasurements();
2548
2889
  const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
2549
2890
  const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
2550
2891
  const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
@@ -2860,6 +3201,7 @@ function createServerHandlers(registry, executor, config) {
2860
3201
  // that don't measure (the fallback only matches single-rooted screens but
2861
3202
  // is good enough for the unit suite).
2862
3203
  tapAt: async (ctx) => {
3204
+ await registry.refreshMeasurements();
2863
3205
  const body = ctx.body;
2864
3206
  const x = body?.x;
2865
3207
  const y = body?.y;
@@ -3067,7 +3409,7 @@ function createServerHandlers(registry, executor, config) {
3067
3409
  };
3068
3410
  if (config?.appInfo) {
3069
3411
  response.uiBridge = {
3070
- version: "0.6.10",
3412
+ version: "0.6.12",
3071
3413
  ...config.appInfo,
3072
3414
  capabilities: ["elements", "components", "actions", "design"],
3073
3415
  elementCount: stats.elements,
@@ -3659,6 +4001,14 @@ var NativeUIBridgeServer = class {
3659
4001
  };
3660
4002
  };
3661
4003
  }
4004
+ /**
4005
+ * Whether the console/network capture should be installed. The dedicated
4006
+ * `observability` flag wins when set; otherwise falls back to `testHooks`
4007
+ * so consumers predating the flag keep their exact behavior.
4008
+ */
4009
+ observabilityEnabled() {
4010
+ return this.config.observability ?? this.config.testHooks === true;
4011
+ }
3662
4012
  /**
3663
4013
  * Set the server adapter
3664
4014
  */
@@ -3872,7 +4222,7 @@ var NativeUIBridgeServer = class {
3872
4222
  console.warn("[ui-bridge-native] See documentation for supported adapters.");
3873
4223
  return;
3874
4224
  }
3875
- if (this.config.testHooks === true) {
4225
+ if (this.observabilityEnabled()) {
3876
4226
  this.consoleErrorBuffer.install();
3877
4227
  this.networkRequestBuffer.install();
3878
4228
  }
@@ -5374,6 +5724,7 @@ function UIBridgeNativeProvider({
5374
5724
  verbose: config.verbose,
5375
5725
  onEvent
5376
5726
  });
5727
+ registryRef.current.setPixelRatio(PixelRatio.get());
5377
5728
  setGlobalRegistry(registryRef.current);
5378
5729
  }
5379
5730
  const registry = registryRef.current;
@@ -5404,6 +5755,10 @@ function UIBridgeNativeProvider({
5404
5755
  cors: true,
5405
5756
  appInfo: config.appInfo,
5406
5757
  testHooks: features.testHooks === true,
5758
+ // Dedicated console/network-capture flag; the server defaults it to
5759
+ // the `testHooks` value when undefined, so passing it through verbatim
5760
+ // keeps existing consumers unchanged.
5761
+ observability: features.observability,
5407
5762
  // Injected device-viewport getter for POST /control/page-health. The
5408
5763
  // server doesn't import `react-native` itself — the require/import
5409
5764
  // pattern there crashed the host RN app in 0.6.3/0.6.4 (Metro/Hermes
@@ -5471,6 +5826,7 @@ function UIBridgeNativeProvider({
5471
5826
  }, [
5472
5827
  features.server,
5473
5828
  features.testHooks,
5829
+ features.observability,
5474
5830
  config.serverPort,
5475
5831
  config.appInfo,
5476
5832
  registry,
@@ -5509,6 +5865,7 @@ function UIBridgeNativeProvider({
5509
5865
  cors: true,
5510
5866
  appInfo: config.appInfo,
5511
5867
  testHooks: features.testHooks === true,
5868
+ observability: features.observability,
5512
5869
  viewportProvider: () => {
5513
5870
  const win = Dimensions.get("window");
5514
5871
  return { width: win.width, height: win.height };
@@ -5712,12 +6069,41 @@ function scheduleDeadButtonWarning(bridge, id, type) {
5712
6069
  }
5713
6070
  function useUIElement(options) {
5714
6071
  const bridge = useUIBridgeNativeOptional();
5715
- const ref = useRef(null);
5716
6072
  const [registered, setRegistered] = useState(false);
5717
6073
  const [_layout, setLayout] = useState(null);
5718
6074
  const propsRef = useRef({});
5719
6075
  const registeredRef = useRef(false);
5720
6076
  const registeredIdRef = useRef(null);
6077
+ const bridgeRef = useRef(bridge);
6078
+ bridgeRef.current = bridge;
6079
+ const nodeRef = useRef(null);
6080
+ const refInterceptorRef = useRef(null);
6081
+ if (refInterceptorRef.current === null) {
6082
+ refInterceptorRef.current = {
6083
+ get current() {
6084
+ return nodeRef.current;
6085
+ },
6086
+ set current(node) {
6087
+ const prev = nodeRef.current;
6088
+ nodeRef.current = node;
6089
+ const liveBridge = bridgeRef.current;
6090
+ const registeredId = registeredIdRef.current;
6091
+ if (!liveBridge || !registeredRef.current || registeredId === null) return;
6092
+ if (node == null && prev != null) {
6093
+ const state = liveBridge.registry.getElement(registeredId)?.getState();
6094
+ if (state && state.visible !== false) {
6095
+ liveBridge.registry.updateElementState(registeredId, { visible: false });
6096
+ }
6097
+ } else if (node != null && prev == null) {
6098
+ const state = liveBridge.registry.getElement(registeredId)?.getState();
6099
+ if (state && state.visible !== true) {
6100
+ liveBridge.registry.updateElementState(registeredId, { visible: true });
6101
+ }
6102
+ }
6103
+ }
6104
+ };
6105
+ }
6106
+ const ref = refInterceptorRef.current;
5721
6107
  const {
5722
6108
  id,
5723
6109
  type = "custom",
@@ -5761,7 +6147,7 @@ function useUIElement(options) {
5761
6147
  }
5762
6148
  setRegistered(true);
5763
6149
  scheduleDeadButtonWarning(bridge, id, type);
5764
- }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
6150
+ }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp, ref]);
5765
6151
  const unregister = useCallback(() => {
5766
6152
  if (!bridge || !registeredRef.current) return;
5767
6153
  bridge.registry.unregisterElement(registeredIdRef.current ?? id);
@@ -5797,7 +6183,8 @@ function useUIElement(options) {
5797
6183
  writeLayout({ x, y, width, height, pageX: x, pageY: y });
5798
6184
  }
5799
6185
  },
5800
- [bridge, id]
6186
+ // `ref` is the once-per-hook interceptor object — identity-stable.
6187
+ [bridge, id, ref]
5801
6188
  );
5802
6189
  const registerRef = useRef(register);
5803
6190
  const unregisterRef = useRef(unregister);
@@ -5814,11 +6201,18 @@ function useUIElement(options) {
5814
6201
  unregisterRef.current();
5815
6202
  }
5816
6203
  };
5817
- }, [autoRegister, bridge]);
6204
+ }, [autoRegister, bridge, id]);
5818
6205
  useEffect(() => {
5819
6206
  if (!bridge || !registered) return;
5820
6207
  const node = ref.current;
5821
- if (!node || typeof node.measureInWindow !== "function") return;
6208
+ if (!node) {
6209
+ const seeded = bridge.registry.getElement(id)?.getState();
6210
+ if (seeded && seeded.visible !== false) {
6211
+ bridge.registry.updateElementState(id, { visible: false });
6212
+ }
6213
+ return;
6214
+ }
6215
+ if (typeof node.measureInWindow !== "function") return;
5822
6216
  let cancelled = false;
5823
6217
  let pendingTimer = null;
5824
6218
  const backoffMs = [50, 200, 500, 1e3, 2500];
@@ -5857,7 +6251,7 @@ function useUIElement(options) {
5857
6251
  cancelled = true;
5858
6252
  if (pendingTimer != null) clearTimeout(pendingTimer);
5859
6253
  };
5860
- }, [bridge, registered, id]);
6254
+ }, [bridge, registered, id, ref]);
5861
6255
  const registeredHandlerKeysRef = useRef([]);
5862
6256
  useEffect(() => {
5863
6257
  if (!bridge || !registered || !handlersProp) return;
@@ -7245,6 +7639,6 @@ var styles = StyleSheet.create({
7245
7639
  }
7246
7640
  });
7247
7641
 
7248
- 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, extractHandlerNames, findAllByPattern, findElementByIdentifier, getGlobalRegistry, getNativeElementDesignData, isAddrInUseError, mapNativeStyleToExtended, matchesIdentifier, parseTreePath, resetGlobalRegistry, setGlobalRegistry, useToastRecorder, useUIBridge, useUIBridgeModal, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIBridgeToast, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
7642
+ 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 };
7249
7643
  //# sourceMappingURL=index.mjs.map
7250
7644
  //# sourceMappingURL=index.mjs.map