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