@qontinui/ui-bridge-native 0.6.11 → 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.
@@ -560,6 +560,103 @@ var NativeUIBridgeRegistry = class {
560
560
  console.log(`[ui-bridge-native] Marked ${cleared} elements offscreen for route: ${route}`);
561
561
  }
562
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
+ }
563
660
  /**
564
661
  * Update element state
565
662
  */
@@ -2542,6 +2639,7 @@ function createServerHandlers(registry, executor, config) {
2542
2639
  return {
2543
2640
  // Elements
2544
2641
  getElements: async (ctx) => {
2642
+ await registry.refreshMeasurements();
2545
2643
  const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
2546
2644
  const forRoute = ctx.query?.route || ctx.body?.route;
2547
2645
  let allElements = visibleOnly ? registry.getMountedVisibleElements() : registry.getAllElements();
@@ -2735,6 +2833,7 @@ function createServerHandlers(registry, executor, config) {
2735
2833
  });
2736
2834
  },
2737
2835
  getSnapshot: async (ctx) => {
2836
+ await registry.refreshMeasurements();
2738
2837
  const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
2739
2838
  const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
2740
2839
  const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
@@ -3050,6 +3149,7 @@ function createServerHandlers(registry, executor, config) {
3050
3149
  // that don't measure (the fallback only matches single-rooted screens but
3051
3150
  // is good enough for the unit suite).
3052
3151
  tapAt: async (ctx) => {
3152
+ await registry.refreshMeasurements();
3053
3153
  const body = ctx.body;
3054
3154
  const x = body?.x;
3055
3155
  const y = body?.y;
@@ -3257,7 +3357,7 @@ function createServerHandlers(registry, executor, config) {
3257
3357
  };
3258
3358
  if (config?.appInfo) {
3259
3359
  response.uiBridge = {
3260
- version: "0.6.11",
3360
+ version: "0.6.12",
3261
3361
  ...config.appInfo,
3262
3362
  capabilities: ["elements", "components", "actions", "design"],
3263
3363
  elementCount: stats.elements,
@@ -6049,7 +6149,7 @@ function useUIElement(options) {
6049
6149
  unregisterRef.current();
6050
6150
  }
6051
6151
  };
6052
- }, [autoRegister, bridge]);
6152
+ }, [autoRegister, bridge, id]);
6053
6153
  useEffect(() => {
6054
6154
  if (!bridge || !registered) return;
6055
6155
  const node = ref.current;