@buoy-gg/image-overlay 2.1.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 (46) hide show
  1. package/lib/commonjs/imageOverlay/components/ImageOverlayModal.js +847 -0
  2. package/lib/commonjs/imageOverlay/components/ImageOverlayStandalone.js +455 -0
  3. package/lib/commonjs/imageOverlay/components/OverlayControls.js +253 -0
  4. package/lib/commonjs/imageOverlay/components/TargetList.js +183 -0
  5. package/lib/commonjs/imageOverlay/index.js +33 -0
  6. package/lib/commonjs/imageOverlay/types/index.js +1 -0
  7. package/lib/commonjs/imageOverlay/utils/ImageOverlayController.js +392 -0
  8. package/lib/commonjs/imageOverlay/utils/componentMeasurement.js +106 -0
  9. package/lib/commonjs/imageOverlay/utils/fiberScanner.js +101 -0
  10. package/lib/commonjs/index.js +46 -0
  11. package/lib/commonjs/package.json +1 -0
  12. package/lib/commonjs/preset.js +59 -0
  13. package/lib/module/imageOverlay/components/ImageOverlayModal.js +843 -0
  14. package/lib/module/imageOverlay/components/ImageOverlayStandalone.js +451 -0
  15. package/lib/module/imageOverlay/components/OverlayControls.js +249 -0
  16. package/lib/module/imageOverlay/components/TargetList.js +179 -0
  17. package/lib/module/imageOverlay/index.js +6 -0
  18. package/lib/module/imageOverlay/types/index.js +1 -0
  19. package/lib/module/imageOverlay/utils/ImageOverlayController.js +388 -0
  20. package/lib/module/imageOverlay/utils/componentMeasurement.js +102 -0
  21. package/lib/module/imageOverlay/utils/fiberScanner.js +97 -0
  22. package/lib/module/index.js +34 -0
  23. package/lib/module/preset.js +53 -0
  24. package/lib/typescript/imageOverlay/components/ImageOverlayModal.d.ts +3 -0
  25. package/lib/typescript/imageOverlay/components/ImageOverlayModal.d.ts.map +1 -0
  26. package/lib/typescript/imageOverlay/components/ImageOverlayStandalone.d.ts +12 -0
  27. package/lib/typescript/imageOverlay/components/ImageOverlayStandalone.d.ts.map +1 -0
  28. package/lib/typescript/imageOverlay/components/OverlayControls.d.ts +18 -0
  29. package/lib/typescript/imageOverlay/components/OverlayControls.d.ts.map +1 -0
  30. package/lib/typescript/imageOverlay/components/TargetList.d.ts +12 -0
  31. package/lib/typescript/imageOverlay/components/TargetList.d.ts.map +1 -0
  32. package/lib/typescript/imageOverlay/index.d.ts +6 -0
  33. package/lib/typescript/imageOverlay/index.d.ts.map +1 -0
  34. package/lib/typescript/imageOverlay/types/index.d.ts +53 -0
  35. package/lib/typescript/imageOverlay/types/index.d.ts.map +1 -0
  36. package/lib/typescript/imageOverlay/utils/ImageOverlayController.d.ts +34 -0
  37. package/lib/typescript/imageOverlay/utils/ImageOverlayController.d.ts.map +1 -0
  38. package/lib/typescript/imageOverlay/utils/componentMeasurement.d.ts +13 -0
  39. package/lib/typescript/imageOverlay/utils/componentMeasurement.d.ts.map +1 -0
  40. package/lib/typescript/imageOverlay/utils/fiberScanner.d.ts +13 -0
  41. package/lib/typescript/imageOverlay/utils/fiberScanner.d.ts.map +1 -0
  42. package/lib/typescript/index.d.ts +14 -0
  43. package/lib/typescript/index.d.ts.map +1 -0
  44. package/lib/typescript/preset.d.ts +46 -0
  45. package/lib/typescript/preset.d.ts.map +1 -0
  46. package/package.json +79 -0
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * List of discovered image-overlay target components.
5
+ * Styled to match RenderListItem from highlight-updates.
6
+ */
7
+
8
+ import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from "react-native";
9
+ import { ChevronRight, buoyColors, Hash } from "@buoy-gg/shared-ui";
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ function TargetRow({
12
+ target,
13
+ onSelect
14
+ }) {
15
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
16
+ style: styles.card,
17
+ onPress: onSelect,
18
+ activeOpacity: 0.7,
19
+ children: [/*#__PURE__*/_jsx(View, {
20
+ style: styles.colorIndicator
21
+ }), /*#__PURE__*/_jsxs(View, {
22
+ style: styles.content,
23
+ children: [/*#__PURE__*/_jsx(View, {
24
+ style: styles.topRow,
25
+ children: /*#__PURE__*/_jsx(Text, {
26
+ style: styles.label,
27
+ numberOfLines: 1,
28
+ children: target.label
29
+ })
30
+ }), /*#__PURE__*/_jsxs(View, {
31
+ style: styles.bottomRow,
32
+ children: [/*#__PURE__*/_jsxs(View, {
33
+ style: styles.badgeRow,
34
+ children: [/*#__PURE__*/_jsxs(View, {
35
+ style: styles.testIdBadge,
36
+ children: [/*#__PURE__*/_jsx(Hash, {
37
+ size: 8,
38
+ color: buoyColors.success
39
+ }), /*#__PURE__*/_jsx(Text, {
40
+ style: styles.testIdBadgeText,
41
+ children: "testID"
42
+ })]
43
+ }), /*#__PURE__*/_jsx(Text, {
44
+ style: styles.testIdValue,
45
+ numberOfLines: 1,
46
+ children: target.testID
47
+ })]
48
+ }), target.componentName && /*#__PURE__*/_jsx(Text, {
49
+ style: styles.componentName,
50
+ numberOfLines: 1,
51
+ children: target.componentName
52
+ })]
53
+ })]
54
+ }), /*#__PURE__*/_jsx(ChevronRight, {
55
+ size: 16,
56
+ color: buoyColors.textMuted
57
+ })]
58
+ });
59
+ }
60
+ export function TargetList({
61
+ targets,
62
+ onSelect
63
+ }) {
64
+ if (targets.length === 0) {
65
+ return /*#__PURE__*/_jsxs(View, {
66
+ style: styles.emptyState,
67
+ children: [/*#__PURE__*/_jsx(Text, {
68
+ style: styles.emptyTitle,
69
+ children: "No targets found"
70
+ }), /*#__PURE__*/_jsxs(Text, {
71
+ style: styles.emptyText,
72
+ children: ["Tag components with", "\n", "testID=\"image-target:YourLabel\""]
73
+ })]
74
+ });
75
+ }
76
+ return /*#__PURE__*/_jsx(ScrollView, {
77
+ style: styles.list,
78
+ contentContainerStyle: styles.listContent,
79
+ children: targets.map(target => /*#__PURE__*/_jsx(TargetRow, {
80
+ target: target,
81
+ onSelect: () => onSelect(target)
82
+ }, target.testID))
83
+ });
84
+ }
85
+ const styles = StyleSheet.create({
86
+ list: {
87
+ flex: 1
88
+ },
89
+ listContent: {
90
+ paddingTop: 8,
91
+ paddingBottom: 20
92
+ },
93
+ card: {
94
+ flexDirection: "row",
95
+ alignItems: "center",
96
+ paddingVertical: 10,
97
+ paddingHorizontal: 12,
98
+ marginHorizontal: 12,
99
+ marginBottom: 6,
100
+ backgroundColor: buoyColors.card,
101
+ borderRadius: 8,
102
+ borderWidth: 1,
103
+ borderColor: buoyColors.border
104
+ },
105
+ colorIndicator: {
106
+ width: 4,
107
+ height: 36,
108
+ borderRadius: 2,
109
+ marginRight: 10,
110
+ backgroundColor: buoyColors.primary
111
+ },
112
+ content: {
113
+ flex: 1,
114
+ marginRight: 8
115
+ },
116
+ topRow: {
117
+ flexDirection: "row",
118
+ alignItems: "center",
119
+ marginBottom: 4
120
+ },
121
+ label: {
122
+ fontSize: 13,
123
+ fontWeight: "600",
124
+ color: buoyColors.text,
125
+ flex: 1
126
+ },
127
+ bottomRow: {
128
+ gap: 2
129
+ },
130
+ badgeRow: {
131
+ flexDirection: "row",
132
+ alignItems: "center",
133
+ gap: 4
134
+ },
135
+ testIdBadge: {
136
+ flexDirection: "row",
137
+ alignItems: "center",
138
+ paddingVertical: 2,
139
+ paddingHorizontal: 5,
140
+ borderRadius: 4,
141
+ backgroundColor: buoyColors.success + "20",
142
+ gap: 3
143
+ },
144
+ testIdBadgeText: {
145
+ fontSize: 9,
146
+ fontWeight: "700",
147
+ color: buoyColors.success
148
+ },
149
+ testIdValue: {
150
+ fontSize: 11,
151
+ color: buoyColors.text,
152
+ fontFamily: "monospace",
153
+ flex: 1
154
+ },
155
+ componentName: {
156
+ fontSize: 10,
157
+ color: "#a855f7",
158
+ fontFamily: "monospace",
159
+ marginTop: 1
160
+ },
161
+ emptyState: {
162
+ alignItems: "center",
163
+ paddingVertical: 40,
164
+ paddingHorizontal: 32
165
+ },
166
+ emptyTitle: {
167
+ color: buoyColors.text,
168
+ fontSize: 14,
169
+ fontWeight: "600",
170
+ marginBottom: 6
171
+ },
172
+ emptyText: {
173
+ color: buoyColors.textMuted,
174
+ fontSize: 12,
175
+ textAlign: "center",
176
+ lineHeight: 18,
177
+ fontFamily: "monospace"
178
+ }
179
+ });
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+
3
+ export { ImageOverlayModal } from "./components/ImageOverlayModal";
4
+ export { ImageOverlayStandalone } from "./components/ImageOverlayStandalone";
5
+ export { ImageOverlayController } from "./utils/ImageOverlayController";
6
+ export { scanForImageTargets } from "./utils/fiberScanner";
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,388 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Singleton state manager for the image overlay tool.
5
+ * Bridges the modal (controls) and standalone overlay (rendering).
6
+ * Supports two modes: "component" (match to tagged element) and "free" (drag anywhere).
7
+ */
8
+
9
+ import { Image as RNImage, Dimensions } from "react-native";
10
+ import { measureInstance } from "./componentMeasurement";
11
+ import { scanForImageTargets } from "./fiberScanner";
12
+ const DEFAULT_STATE = {
13
+ mode: null,
14
+ enabled: false,
15
+ imageUri: null,
16
+ imageWidth: null,
17
+ imageHeight: null,
18
+ targetTag: null,
19
+ targetLabel: null,
20
+ targetRect: null,
21
+ showOutline: true,
22
+ opacity: 0.5,
23
+ scale: 1.0,
24
+ offsetX: 0,
25
+ offsetY: 0,
26
+ invertX: false,
27
+ invertY: false,
28
+ locked: false,
29
+ freeX: 0,
30
+ freeY: 0,
31
+ freeWidth: 200,
32
+ freeHeight: 200
33
+ };
34
+ let state = {
35
+ ...DEFAULT_STATE
36
+ };
37
+ const listeners = new Set();
38
+
39
+ // Hold onto references so we can remeasure
40
+ let targetInstance = null;
41
+ let targetLabel = null;
42
+
43
+ // Auto-tracking via React DevTools traceUpdates
44
+ let autoTrackEnabled = false;
45
+ let traceUnsubscribe = null;
46
+ let trackingNativeTag = null;
47
+ function notify() {
48
+ for (const listener of listeners) {
49
+ listener(state);
50
+ }
51
+ }
52
+ function getNativeTag(instance) {
53
+ if (!instance) return null;
54
+ if (typeof instance._nativeTag === "number") return instance._nativeTag;
55
+ if (typeof instance.canonical?._nativeTag === "number") return instance.canonical._nativeTag;
56
+ if (typeof instance.__nativeTag === "number") return instance.__nativeTag;
57
+ return null;
58
+ }
59
+ async function measureWithFallback(instance, label) {
60
+ let rect = await measureInstance(instance);
61
+ if (rect) return {
62
+ rect,
63
+ freshInstance: instance
64
+ };
65
+ const targets = scanForImageTargets();
66
+ const fresh = targets.find(t => t.label === label);
67
+ if (fresh) {
68
+ rect = await measureInstance(fresh.instance);
69
+ if (rect) return {
70
+ rect,
71
+ freshInstance: fresh.instance
72
+ };
73
+ }
74
+ return {
75
+ rect: null,
76
+ freshInstance: instance
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Subscribe to React DevTools traceUpdates to remeasure our target
82
+ * component in real-time whenever ANY component re-renders (which
83
+ * includes scroll events that reposition our target).
84
+ */
85
+ function startTraceTracking() {
86
+ if (traceUnsubscribe) return; // already tracking
87
+
88
+ const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
89
+ if (!hook) return;
90
+
91
+ // Enable trace updates on all renderers (same as highlight-updates)
92
+ if (hook.rendererInterfaces) {
93
+ for (const [, renderer] of hook.rendererInterfaces) {
94
+ if (typeof renderer.setTraceUpdatesEnabled === "function") {
95
+ renderer.setTraceUpdatesEnabled(true);
96
+ }
97
+ }
98
+ }
99
+
100
+ // Subscribe to traceUpdates — fires whenever components render
101
+ if (typeof hook.sub === "function") {
102
+ traceUnsubscribe = hook.sub("traceUpdates", handleTraceForAutoTrack);
103
+ }
104
+ }
105
+ function stopTraceTracking() {
106
+ if (traceUnsubscribe) {
107
+ traceUnsubscribe();
108
+ traceUnsubscribe = null;
109
+ }
110
+ // Note: we don't disable traceUpdates on renderers because
111
+ // highlight-updates may also be using them
112
+ }
113
+
114
+ /** Called on every traceUpdates event — remeasure our target */
115
+ function handleTraceForAutoTrack(_nodes) {
116
+ if (!autoTrackEnabled || !targetInstance) return;
117
+
118
+ // Don't filter by nativeTag — any render could have caused a layout
119
+ // shift (e.g. sibling resize, scroll). Just remeasure cheaply.
120
+ measureInstance(targetInstance).then(rect => {
121
+ if (rect && state.targetRect) {
122
+ // Only notify if position actually changed
123
+ const prev = state.targetRect;
124
+ if (Math.abs(rect.x - prev.x) > 0.5 || Math.abs(rect.y - prev.y) > 0.5 || Math.abs(rect.width - prev.width) > 0.5 || Math.abs(rect.height - prev.height) > 0.5) {
125
+ state = {
126
+ ...state,
127
+ targetRect: rect
128
+ };
129
+ notify();
130
+ }
131
+ }
132
+ });
133
+ }
134
+ function getImageSize(uri) {
135
+ return new Promise(resolve => {
136
+ RNImage.getSize(uri, (width, height) => resolve({
137
+ width,
138
+ height
139
+ }), () => resolve(null));
140
+ });
141
+ }
142
+ export const ImageOverlayController = {
143
+ subscribe(listener) {
144
+ listeners.add(listener);
145
+ listener(state);
146
+ return () => listeners.delete(listener);
147
+ },
148
+ getState() {
149
+ return state;
150
+ },
151
+ // ─── Component Match Mode ───────────────────────────────────────────
152
+
153
+ async setTarget(instance, label, fiber) {
154
+ targetInstance = instance;
155
+ targetLabel = label;
156
+ const instanceToMeasure = fiber?.stateNode ?? instance;
157
+ const {
158
+ rect,
159
+ freshInstance
160
+ } = await measureWithFallback(instanceToMeasure, label);
161
+ targetInstance = freshInstance;
162
+ const tag = getNativeTag(targetInstance);
163
+ state = {
164
+ ...state,
165
+ mode: "component",
166
+ targetTag: tag,
167
+ targetLabel: label,
168
+ targetRect: rect
169
+ };
170
+ notify();
171
+ },
172
+ async setImageUri(uri) {
173
+ const size = await getImageSize(uri);
174
+ let autoScale = 1.0;
175
+ if (size && state.targetRect && size.width > 0) {
176
+ autoScale = state.targetRect.width / size.width;
177
+ }
178
+ state = {
179
+ ...state,
180
+ imageUri: uri,
181
+ imageWidth: size?.width ?? null,
182
+ imageHeight: size?.height ?? null,
183
+ scale: autoScale,
184
+ showOutline: false
185
+ };
186
+ notify();
187
+ },
188
+ setOpacity(value) {
189
+ state = {
190
+ ...state,
191
+ opacity: Math.max(0, Math.min(1, value))
192
+ };
193
+ notify();
194
+ },
195
+ setScale(value) {
196
+ state = {
197
+ ...state,
198
+ scale: Math.max(0.01, Math.min(5, value))
199
+ };
200
+ notify();
201
+ },
202
+ setOffset(x, y) {
203
+ state = {
204
+ ...state,
205
+ offsetX: x,
206
+ offsetY: y
207
+ };
208
+ notify();
209
+ },
210
+ toggleInvertX() {
211
+ state = {
212
+ ...state,
213
+ invertX: !state.invertX
214
+ };
215
+ notify();
216
+ },
217
+ toggleInvertY() {
218
+ state = {
219
+ ...state,
220
+ invertY: !state.invertY
221
+ };
222
+ notify();
223
+ },
224
+ setLocked(locked) {
225
+ state = {
226
+ ...state,
227
+ locked
228
+ };
229
+ notify();
230
+ },
231
+ setAutoTrack(enabled) {
232
+ autoTrackEnabled = enabled;
233
+ if (enabled) {
234
+ startTraceTracking();
235
+ } else {
236
+ stopTraceTracking();
237
+ }
238
+ },
239
+ isAutoTracking() {
240
+ return autoTrackEnabled;
241
+ },
242
+ setShowOutline(show) {
243
+ state = {
244
+ ...state,
245
+ showOutline: show
246
+ };
247
+ notify();
248
+ },
249
+ setEnabled(enabled) {
250
+ state = {
251
+ ...state,
252
+ enabled
253
+ };
254
+ notify();
255
+ },
256
+ toggle() {
257
+ state = {
258
+ ...state,
259
+ enabled: !state.enabled
260
+ };
261
+ notify();
262
+ },
263
+ fitWidth() {
264
+ if (state.imageWidth && state.targetRect && state.imageWidth > 0) {
265
+ state = {
266
+ ...state,
267
+ scale: state.targetRect.width / state.imageWidth
268
+ };
269
+ notify();
270
+ }
271
+ },
272
+ fitHeight() {
273
+ if (state.imageHeight && state.targetRect && state.imageHeight > 0) {
274
+ state = {
275
+ ...state,
276
+ scale: state.targetRect.height / state.imageHeight
277
+ };
278
+ notify();
279
+ }
280
+ },
281
+ resetSettings() {
282
+ if (state.mode === "free") {
283
+ const screen = Dimensions.get("window");
284
+ const w = state.imageWidth ?? 200;
285
+ const h = state.imageHeight ?? 200;
286
+ state = {
287
+ ...state,
288
+ opacity: 0.5,
289
+ freeX: (screen.width - w) / 2,
290
+ freeY: (screen.height - h) / 2,
291
+ freeWidth: w,
292
+ freeHeight: h
293
+ };
294
+ } else {
295
+ let autoScale = 1.0;
296
+ if (state.imageWidth && state.targetRect && state.imageWidth > 0) {
297
+ autoScale = state.targetRect.width / state.imageWidth;
298
+ }
299
+ state = {
300
+ ...state,
301
+ opacity: 0.5,
302
+ scale: autoScale,
303
+ offsetX: 0,
304
+ offsetY: 0
305
+ };
306
+ }
307
+ notify();
308
+ },
309
+ async remeasure() {
310
+ if (!targetLabel) return;
311
+ const {
312
+ rect,
313
+ freshInstance
314
+ } = await measureWithFallback(targetInstance, targetLabel);
315
+ targetInstance = freshInstance;
316
+ if (rect) {
317
+ state = {
318
+ ...state,
319
+ targetRect: rect
320
+ };
321
+ notify();
322
+ }
323
+ },
324
+ // ─── Free Placement Mode ───────────────────────────────────────────
325
+
326
+ async startFreeMode(uri) {
327
+ const size = await getImageSize(uri);
328
+ const screen = Dimensions.get("window");
329
+ const w = size?.width ?? 200;
330
+ const h = size?.height ?? 200;
331
+
332
+ // Scale down if image is larger than 80% of screen
333
+ let displayW = w;
334
+ let displayH = h;
335
+ const maxW = screen.width * 0.8;
336
+ const maxH = screen.height * 0.6;
337
+ if (displayW > maxW || displayH > maxH) {
338
+ const ratio = Math.min(maxW / displayW, maxH / displayH);
339
+ displayW = Math.round(displayW * ratio);
340
+ displayH = Math.round(displayH * ratio);
341
+ }
342
+ state = {
343
+ ...state,
344
+ mode: "free",
345
+ enabled: true,
346
+ imageUri: uri,
347
+ imageWidth: size?.width ?? null,
348
+ imageHeight: size?.height ?? null,
349
+ opacity: 0.5,
350
+ freeX: Math.round((screen.width - displayW) / 2),
351
+ freeY: Math.round((screen.height - displayH) / 2),
352
+ freeWidth: Math.round(displayW),
353
+ freeHeight: Math.round(displayH)
354
+ };
355
+ notify();
356
+ },
357
+ setFreePosition(x, y) {
358
+ state = {
359
+ ...state,
360
+ freeX: x,
361
+ freeY: y
362
+ };
363
+ notify();
364
+ },
365
+ setFreeDimensions(width, height, x, y) {
366
+ state = {
367
+ ...state,
368
+ freeWidth: width,
369
+ freeHeight: height,
370
+ freeX: x,
371
+ freeY: y
372
+ };
373
+ notify();
374
+ },
375
+ // ─── Shared ─────────────────────────────────────────────────────────
376
+
377
+ reset() {
378
+ stopTraceTracking();
379
+ autoTrackEnabled = false;
380
+ trackingNativeTag = null;
381
+ targetInstance = null;
382
+ targetLabel = null;
383
+ state = {
384
+ ...DEFAULT_STATE
385
+ };
386
+ notify();
387
+ }
388
+ };
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Component measurement utilities for image overlay positioning.
5
+ * Adapted from debug-borders/utils/componentMeasurement.js
6
+ *
7
+ * Supports both Fabric (new arch) and Paper (legacy) measurement APIs.
8
+ */
9
+
10
+ import { findNodeHandle, UIManager } from "react-native";
11
+ /**
12
+ * Measures a component instance and returns its screen-space bounding rect.
13
+ * Tries multiple strategies to handle Fabric, Paper, and edge cases.
14
+ */
15
+ export function measureInstance(instance) {
16
+ return new Promise(resolve => {
17
+ try {
18
+ // Build a list of candidates to try measuring
19
+ const candidates = [];
20
+
21
+ // Direct instance (ReactFabricHostComponent has measure/getBoundingClientRect)
22
+ if (instance) candidates.push(instance);
23
+
24
+ // Fabric: instance.canonical.publicInstance
25
+ if (instance?.canonical?.publicInstance) {
26
+ candidates.push(instance.canonical.publicInstance);
27
+ }
28
+ // Fabric fallback: instance.canonical itself
29
+ if (instance?.canonical && instance.canonical !== instance) {
30
+ candidates.push(instance.canonical);
31
+ }
32
+
33
+ // Try getBoundingClientRect first (modern Fabric, synchronous)
34
+ for (const publicInstance of candidates) {
35
+ if (typeof publicInstance.getBoundingClientRect === "function") {
36
+ try {
37
+ const rect = publicInstance.getBoundingClientRect();
38
+ if (rect && typeof rect.x === "number" && typeof rect.y === "number" && typeof rect.width === "number" && typeof rect.height === "number" && rect.width > 0 && rect.height > 0) {
39
+ resolve({
40
+ x: rect.x,
41
+ y: rect.y,
42
+ width: rect.width,
43
+ height: rect.height
44
+ });
45
+ return;
46
+ }
47
+ } catch {
48
+ // Try next candidate
49
+ }
50
+ }
51
+ }
52
+
53
+ // Try measure() callback on all candidates
54
+ for (const publicInstance of candidates) {
55
+ if (typeof publicInstance.measure === "function") {
56
+ const timeout = setTimeout(() => resolve(null), 200);
57
+ publicInstance.measure((_x, _y, width, height, pageX, pageY) => {
58
+ clearTimeout(timeout);
59
+ if (pageX != null && pageY != null && width != null && height != null && width > 0 && height > 0) {
60
+ resolve({
61
+ x: pageX,
62
+ y: pageY,
63
+ width,
64
+ height
65
+ });
66
+ } else {
67
+ resolve(null);
68
+ }
69
+ });
70
+ return;
71
+ }
72
+ }
73
+
74
+ // Last resort: try findNodeHandle + UIManager.measure
75
+ try {
76
+ const nodeHandle = findNodeHandle(instance);
77
+ if (nodeHandle != null) {
78
+ const timeout = setTimeout(() => resolve(null), 200);
79
+ UIManager.measure(nodeHandle, (_x, _y, width, height, pageX, pageY) => {
80
+ clearTimeout(timeout);
81
+ if (pageX != null && pageY != null && width != null && height != null && width > 0 && height > 0) {
82
+ resolve({
83
+ x: pageX,
84
+ y: pageY,
85
+ width,
86
+ height
87
+ });
88
+ } else {
89
+ resolve(null);
90
+ }
91
+ });
92
+ return;
93
+ }
94
+ } catch {
95
+ // findNodeHandle not available or failed
96
+ }
97
+ resolve(null);
98
+ } catch {
99
+ resolve(null);
100
+ }
101
+ });
102
+ }