react-native-grab 0.0.2 → 0.0.4

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 (99) hide show
  1. package/README.md +6 -5
  2. package/dist/cjs/metro/withReactNativeGrab.js +31 -31
  3. package/dist/cjs/metro/withReactNativeGrab.js.map +1 -1
  4. package/dist/cjs/react-native/__tests__/dom-traversal.test.js +160 -0
  5. package/dist/cjs/react-native/__tests__/dom-traversal.test.js.map +1 -0
  6. package/dist/cjs/react-native/__tests__/mock-dom.js +65 -0
  7. package/dist/cjs/react-native/__tests__/mock-dom.js.map +1 -0
  8. package/dist/cjs/react-native/__tests__/setup.js +43 -0
  9. package/dist/cjs/react-native/__tests__/setup.js.map +1 -0
  10. package/dist/cjs/react-native/__tests__/z-index.test.js +106 -0
  11. package/dist/cjs/react-native/__tests__/z-index.test.js.map +1 -0
  12. package/dist/cjs/react-native/copy-payload.js +65 -69
  13. package/dist/cjs/react-native/copy-payload.js.map +1 -1
  14. package/dist/cjs/react-native/dom-traversal.js +170 -126
  15. package/dist/cjs/react-native/dom-traversal.js.map +1 -1
  16. package/dist/cjs/react-native/fiber.js +16 -16
  17. package/dist/cjs/react-native/fiber.js.map +1 -1
  18. package/dist/cjs/react-native/get-dev-server.js +9 -0
  19. package/dist/cjs/react-native/get-dev-server.js.map +1 -0
  20. package/dist/cjs/react-native/index.js +346 -260
  21. package/dist/cjs/react-native/index.js.map +1 -1
  22. package/dist/cjs/react-native/index.web.js +8 -0
  23. package/dist/cjs/react-native/index.web.js.map +1 -0
  24. package/dist/cjs/react-native/pointer-events.js +25 -28
  25. package/dist/cjs/react-native/pointer-events.js.map +1 -1
  26. package/dist/cjs/react-native/selection-trigger.js +1 -1
  27. package/dist/cjs/react-native/settings.js +7 -10
  28. package/dist/cjs/react-native/settings.js.map +1 -1
  29. package/dist/cjs/react-native/symbolicate.js.map +1 -1
  30. package/dist/cjs/react-native/types.js +0 -4
  31. package/dist/cjs/react-native/types.js.map +1 -1
  32. package/dist/cjs/react-native/z-index.js +23 -5
  33. package/dist/cjs/react-native/z-index.js.map +1 -1
  34. package/dist/esm/metro/index.js +1 -1
  35. package/dist/esm/metro/withReactNativeGrab.js +33 -33
  36. package/dist/esm/metro/withReactNativeGrab.js.map +1 -1
  37. package/dist/esm/react-native/__tests__/dom-traversal.test.js +158 -0
  38. package/dist/esm/react-native/__tests__/dom-traversal.test.js.map +1 -0
  39. package/dist/esm/react-native/__tests__/mock-dom.js +58 -0
  40. package/dist/esm/react-native/__tests__/mock-dom.js.map +1 -0
  41. package/dist/esm/react-native/__tests__/setup.js +41 -0
  42. package/dist/esm/react-native/__tests__/setup.js.map +1 -0
  43. package/dist/esm/react-native/__tests__/z-index.test.js +104 -0
  44. package/dist/esm/react-native/__tests__/z-index.test.js.map +1 -0
  45. package/dist/esm/react-native/copy-payload.js +67 -71
  46. package/dist/esm/react-native/copy-payload.js.map +1 -1
  47. package/dist/esm/react-native/dom-traversal.js +173 -127
  48. package/dist/esm/react-native/dom-traversal.js.map +1 -1
  49. package/dist/esm/react-native/fiber.js +17 -17
  50. package/dist/esm/react-native/fiber.js.map +1 -1
  51. package/dist/esm/react-native/get-dev-server.js +3 -0
  52. package/dist/esm/react-native/get-dev-server.js.map +1 -0
  53. package/dist/esm/react-native/index.js +351 -262
  54. package/dist/esm/react-native/index.js.map +1 -1
  55. package/dist/esm/react-native/index.web.js +4 -0
  56. package/dist/esm/react-native/index.web.js.map +1 -0
  57. package/dist/esm/react-native/pointer-events.js +26 -29
  58. package/dist/esm/react-native/pointer-events.js.map +1 -1
  59. package/dist/esm/react-native/selection-trigger.js +1 -1
  60. package/dist/esm/react-native/settings.js +6 -6
  61. package/dist/esm/react-native/settings.js.map +1 -1
  62. package/dist/esm/react-native/symbolicate.js +2 -2
  63. package/dist/esm/react-native/symbolicate.js.map +1 -1
  64. package/dist/esm/react-native/types.js +0 -4
  65. package/dist/esm/react-native/types.js.map +1 -1
  66. package/dist/esm/react-native/z-index.js +24 -6
  67. package/dist/esm/react-native/z-index.js.map +1 -1
  68. package/dist/types/metro/index.d.ts +1 -1
  69. package/dist/types/metro/withReactNativeGrab.d.ts.map +1 -1
  70. package/dist/types/react-native/__tests__/dom-traversal.test.d.ts +2 -0
  71. package/dist/types/react-native/__tests__/dom-traversal.test.d.ts.map +1 -0
  72. package/dist/types/react-native/__tests__/mock-dom.d.ts +24 -0
  73. package/dist/types/react-native/__tests__/mock-dom.d.ts.map +1 -0
  74. package/dist/types/react-native/__tests__/setup.d.ts +2 -0
  75. package/dist/types/react-native/__tests__/setup.d.ts.map +1 -0
  76. package/dist/types/react-native/__tests__/z-index.test.d.ts +2 -0
  77. package/dist/types/react-native/__tests__/z-index.test.d.ts.map +1 -0
  78. package/dist/types/react-native/copy-payload.d.ts +1 -1
  79. package/dist/types/react-native/copy-payload.d.ts.map +1 -1
  80. package/dist/types/react-native/dom-traversal.d.ts +19 -6
  81. package/dist/types/react-native/dom-traversal.d.ts.map +1 -1
  82. package/dist/types/react-native/fiber.d.ts +1 -1
  83. package/dist/types/react-native/fiber.d.ts.map +1 -1
  84. package/dist/types/react-native/get-dev-server.d.ts +3 -0
  85. package/dist/types/react-native/get-dev-server.d.ts.map +1 -0
  86. package/dist/types/react-native/index.d.ts +1 -1
  87. package/dist/types/react-native/index.d.ts.map +1 -1
  88. package/dist/types/react-native/index.web.d.ts +2 -0
  89. package/dist/types/react-native/index.web.d.ts.map +1 -0
  90. package/dist/types/react-native/pointer-events.d.ts +2 -2
  91. package/dist/types/react-native/pointer-events.d.ts.map +1 -1
  92. package/dist/types/react-native/settings.d.ts.map +1 -1
  93. package/dist/types/react-native/symbolicate.d.ts +1 -1
  94. package/dist/types/react-native/symbolicate.d.ts.map +1 -1
  95. package/dist/types/react-native/types.d.ts +3 -11
  96. package/dist/types/react-native/types.d.ts.map +1 -1
  97. package/dist/types/react-native/z-index.d.ts +1 -1
  98. package/dist/types/react-native/z-index.d.ts.map +1 -1
  99. package/package.json +30 -27
@@ -1,17 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment, useEffect, useRef, useState } from 'react';
3
- import { Animated, DevSettings, Dimensions, PanResponder, Platform, Pressable, StatusBar, StyleSheet, Text, useWindowDimensions, View, } from 'react-native';
4
- import getDevServer from 'react-native/Libraries/Core/Devtools/getDevServer';
5
- import { FullWindowOverlay } from 'react-native-screens';
6
- import { buildInspectorCopyPayload } from './copy-payload';
7
- import { findHitTargetAtPoint } from './dom-traversal';
8
- import { requestInspectorSelection, subscribeInspectorSelectionRequest } from './selection-trigger';
9
- import { getInspectorSettings, initializeInspectorSettings, subscribeInspectorSettings, updateInspectorSettings, } from './settings';
2
+ import { Fragment, useCallback, useEffect, useRef, useState, } from "react";
3
+ import { Animated, DevSettings, Dimensions, PanResponder, Platform, Pressable, StatusBar, StyleSheet, Text, useWindowDimensions, View, } from "react-native";
4
+ import { getDevServer } from "./get-dev-server";
5
+ import { FullWindowOverlay } from "react-native-screens";
6
+ import { buildInspectorCopyPayload } from "./copy-payload";
7
+ import { startSession as startTraversalSession, } from "./dom-traversal";
8
+ import { requestInspectorSelection, subscribeInspectorSelectionRequest } from "./selection-trigger";
9
+ import { getInspectorSettings, initializeInspectorSettings, subscribeInspectorSettings, updateInspectorSettings, } from "./settings";
10
10
  const SETTLE_MS = 80;
11
11
  const MOVEMENT_EPSILON_PX = 8;
12
12
  const MAX_STACK_LINES = 3;
13
13
  const MAX_PREVIEW_TEXT = 120;
14
- const INSPECTOR_COPY_ENDPOINT = '/__react-native-grab/copy';
14
+ const INSPECTOR_COPY_ENDPOINT = "/__react-native-grab/copy";
15
15
  const FAB_SIZE = 56;
16
16
  const FAB_SCREEN_PADDING = 8;
17
17
  const FAB_DEFAULT_RIGHT_OFFSET = 20;
@@ -20,16 +20,10 @@ const getMetroBaseUrl = () => {
20
20
  const devServer = getDevServer();
21
21
  if (!devServer?.url)
22
22
  return null;
23
- return devServer.url.replace(/\/$/, '');
24
- };
25
- export const ElementInspector = () => {
26
- if (Platform.OS === 'web') {
27
- return null;
28
- }
29
- return _jsx(ElementInspectorImpl, {});
23
+ return devServer.url.replace(/\/$/, "");
30
24
  };
31
25
  const getInitialFabPosition = () => {
32
- const { width, height } = Dimensions.get('window');
26
+ const { width, height } = Dimensions.get("window");
33
27
  return {
34
28
  x: Math.max(FAB_SCREEN_PADDING, width - FAB_DEFAULT_RIGHT_OFFSET - FAB_SIZE),
35
29
  y: Math.max(FAB_SCREEN_PADDING, height - FAB_DEFAULT_BOTTOM_OFFSET - FAB_SIZE),
@@ -43,74 +37,227 @@ const clampFabPosition = (x, y, windowWidth, windowHeight) => {
43
37
  y: Math.min(Math.max(y, FAB_SCREEN_PADDING), maxY),
44
38
  };
45
39
  };
46
- const ElementInspectorImpl = () => {
47
- const { width: windowWidth, height: windowHeight } = useWindowDimensions();
48
- const statusBarOffset = Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0;
49
- const [state, setState] = useState({ status: 'idle' });
50
- const [highlightRect, setHighlightRect] = useState(null);
51
- const [toastVisible, setToastVisible] = useState(false);
52
- const [selectionPillVisible, setSelectionPillVisible] = useState(() => getInspectorSettings().selectionPillVisible);
53
- const isClosingRef = useRef(false);
54
- const anchorRef = useRef(null);
55
- const inspectorRootRef = useRef(null);
56
- const overlayRef = useRef(null);
57
- const selectedHitTargetRef = useRef(null);
58
- const lastQueryPositionRef = useRef(null);
59
- const movePositionRef = useRef({ x: 0, y: 0 });
60
- const settleTimeoutRef = useRef(null);
61
- const stateRef = useRef(state);
62
- stateRef.current = state;
63
- const selectingHintAnim = useRef(new Animated.Value(0)).current;
64
- const toastAnim = useRef(new Animated.Value(0)).current;
65
- const fabAppearAnim = useRef(new Animated.Value(1)).current;
66
- const fabPressAnim = useRef(new Animated.Value(1)).current;
67
- const fabPosition = useRef(new Animated.ValueXY(getInitialFabPosition())).current;
68
- const fabPositionRef = useRef(getInitialFabPosition());
69
- const fabDragStartOffsetRef = useRef({ x: 0, y: 0 });
70
- const windowSizeRef = useRef({ width: windowWidth, height: windowHeight });
71
- windowSizeRef.current = { width: windowWidth, height: windowHeight };
72
- const skipRefs = [inspectorRootRef, overlayRef];
73
- const idle = state.status === 'idle';
74
- const selecting = state.status === 'selecting';
75
- const overlayVisible = state.status !== 'idle';
76
- const activeHighlightRect = state.status === 'copying' ? state.selectedRect : highlightRect;
77
- const startSelecting = () => {
78
- selectedHitTargetRef.current = null;
79
- setState({ status: 'selecting' });
80
- };
81
- const performQuery = (x, y, offset) => {
82
- const appY = y - statusBarOffset;
83
- const hitTarget = findHitTargetAtPoint(anchorRef, skipRefs, x, appY, Platform.OS === 'ios'
40
+ const useLatestRef = (value) => {
41
+ const ref = useRef(value);
42
+ ref.current = value;
43
+ return ref;
44
+ };
45
+ const useTraversalSelectionSession = ({ anchorRef, inspectorRootRef, overlayRef, statusBarOffset, setHighlightRect, }) => {
46
+ const traversalSessionRef = useRef(null);
47
+ const skipRefsRef = useRef([inspectorRootRef, overlayRef]);
48
+ const stopSession = useCallback(() => {
49
+ traversalSessionRef.current?.stop();
50
+ traversalSessionRef.current = null;
51
+ }, []);
52
+ const startSession = useCallback(() => {
53
+ const skipPredicate = Platform.OS === "ios"
84
54
  ? (node) => {
85
55
  const inspectorRootNode = inspectorRootRef.current;
86
56
  if (!inspectorRootNode)
87
57
  return false;
88
58
  return node === inspectorRootNode.parentElement;
89
59
  }
90
- : undefined, offset);
60
+ : undefined;
61
+ stopSession();
62
+ traversalSessionRef.current = startTraversalSession({
63
+ anchorRef,
64
+ skipRefs: skipRefsRef.current,
65
+ offset: statusBarOffset,
66
+ skipPredicate,
67
+ });
68
+ setHighlightRect(null);
69
+ }, [anchorRef, inspectorRootRef, setHighlightRect, statusBarOffset, stopSession]);
70
+ const queryAtPoint = useCallback((x, y) => {
71
+ const session = traversalSessionRef.current;
72
+ if (!session) {
73
+ setHighlightRect(null);
74
+ return null;
75
+ }
76
+ const hitTarget = session.move({
77
+ x,
78
+ y: y - statusBarOffset,
79
+ });
91
80
  setHighlightRect(hitTarget?.rect ?? null);
92
81
  return hitTarget;
82
+ }, [setHighlightRect, statusBarOffset]);
83
+ useEffect(() => {
84
+ return () => {
85
+ stopSession();
86
+ };
87
+ }, [stopSession]);
88
+ return {
89
+ startSession,
90
+ queryAtPoint,
91
+ stopSession,
93
92
  };
94
- const performQueryRef = useRef(performQuery);
95
- performQueryRef.current = performQuery;
96
- const closeOverlayImmediately = () => {
93
+ };
94
+ const useSelectionGesture = ({ isSelecting, queryAtPoint, onHitTarget, onCancel, }) => {
95
+ const isSelectingRef = useLatestRef(isSelecting);
96
+ const queryAtPointRef = useLatestRef(queryAtPoint);
97
+ const onHitTargetRef = useLatestRef(onHitTarget);
98
+ const onCancelRef = useLatestRef(onCancel);
99
+ const settleTimeoutRef = useRef(null);
100
+ const lastQueryPositionRef = useRef(null);
101
+ const movePositionRef = useRef({ x: 0, y: 0 });
102
+ const clearSettleTimeout = useCallback(() => {
97
103
  if (settleTimeoutRef.current != null) {
98
104
  clearTimeout(settleTimeoutRef.current);
99
105
  settleTimeoutRef.current = null;
100
106
  }
101
- selectedHitTargetRef.current = null;
107
+ }, []);
108
+ const resetGestureState = useCallback(() => {
109
+ clearSettleTimeout();
102
110
  lastQueryPositionRef.current = null;
103
- setState({ status: 'idle' });
104
- setHighlightRect(null);
111
+ }, [clearSettleTimeout]);
112
+ const panResponder = useRef(PanResponder.create({
113
+ onStartShouldSetPanResponder: () => true,
114
+ onMoveShouldSetPanResponder: () => true,
115
+ onPanResponderGrant: (evt) => {
116
+ if (!isSelectingRef.current)
117
+ return;
118
+ const x = evt.nativeEvent.locationX;
119
+ const y = evt.nativeEvent.locationY;
120
+ queryAtPointRef.current(x, y);
121
+ lastQueryPositionRef.current = { x, y };
122
+ },
123
+ onPanResponderMove: (evt) => {
124
+ if (!isSelectingRef.current)
125
+ return;
126
+ movePositionRef.current = {
127
+ x: evt.nativeEvent.locationX,
128
+ y: evt.nativeEvent.locationY,
129
+ };
130
+ clearSettleTimeout();
131
+ settleTimeoutRef.current = setTimeout(() => {
132
+ settleTimeoutRef.current = null;
133
+ if (!isSelectingRef.current)
134
+ return;
135
+ const pos = movePositionRef.current;
136
+ const last = lastQueryPositionRef.current;
137
+ const distance = last ? Math.hypot(pos.x - last.x, pos.y - last.y) : Infinity;
138
+ if (distance < MOVEMENT_EPSILON_PX)
139
+ return;
140
+ lastQueryPositionRef.current = pos;
141
+ queryAtPointRef.current(pos.x, pos.y);
142
+ }, SETTLE_MS);
143
+ },
144
+ onPanResponderRelease: (evt) => {
145
+ if (!isSelectingRef.current)
146
+ return;
147
+ const hitTarget = queryAtPointRef.current(evt.nativeEvent.locationX, evt.nativeEvent.locationY);
148
+ if (!hitTarget) {
149
+ onCancelRef.current();
150
+ return;
151
+ }
152
+ onHitTargetRef.current(hitTarget);
153
+ },
154
+ onPanResponderTerminate: () => {
155
+ if (isSelectingRef.current) {
156
+ onCancelRef.current();
157
+ }
158
+ },
159
+ })).current;
160
+ useEffect(() => {
161
+ return () => {
162
+ clearSettleTimeout();
163
+ };
164
+ }, [clearSettleTimeout]);
165
+ return {
166
+ panHandlers: panResponder.panHandlers,
167
+ resetGestureState,
168
+ };
169
+ };
170
+ const useFabController = (windowWidth, windowHeight) => {
171
+ const initialFabPosition = useRef(getInitialFabPosition()).current;
172
+ const fabAppearAnim = useRef(new Animated.Value(1)).current;
173
+ const fabPressAnim = useRef(new Animated.Value(1)).current;
174
+ const fabPosition = useRef(new Animated.ValueXY(initialFabPosition)).current;
175
+ const fabPositionRef = useRef(initialFabPosition);
176
+ const fabDragStartOffsetRef = useRef({ x: 0, y: 0 });
177
+ const windowSizeRef = useRef({ width: windowWidth, height: windowHeight });
178
+ windowSizeRef.current = { width: windowWidth, height: windowHeight };
179
+ const animateFabPress = useCallback((toValue) => {
180
+ Animated.spring(fabPressAnim, {
181
+ toValue,
182
+ useNativeDriver: true,
183
+ tension: 220,
184
+ friction: 16,
185
+ }).start();
186
+ }, [fabPressAnim]);
187
+ const fabPanResponder = useRef(PanResponder.create({
188
+ onStartShouldSetPanResponder: () => false,
189
+ onMoveShouldSetPanResponder: (_evt, gestureState) => Math.abs(gestureState.dx) > 2 || Math.abs(gestureState.dy) > 2,
190
+ onPanResponderGrant: (_evt, gestureState) => {
191
+ fabDragStartOffsetRef.current = {
192
+ x: fabPositionRef.current.x - gestureState.x0,
193
+ y: fabPositionRef.current.y - gestureState.y0,
194
+ };
195
+ },
196
+ onPanResponderMove: (_evt, gestureState) => {
197
+ const { width, height } = windowSizeRef.current;
198
+ const nextPosition = clampFabPosition(gestureState.moveX + fabDragStartOffsetRef.current.x, gestureState.moveY + fabDragStartOffsetRef.current.y, width, height);
199
+ fabPositionRef.current = nextPosition;
200
+ fabPosition.setValue(nextPosition);
201
+ },
202
+ onPanResponderTerminationRequest: () => true,
203
+ onPanResponderTerminate: () => {
204
+ fabPressAnim.setValue(1);
205
+ },
206
+ onPanResponderRelease: () => {
207
+ fabPressAnim.setValue(1);
208
+ },
209
+ })).current;
210
+ useEffect(() => {
211
+ const nextPosition = clampFabPosition(fabPositionRef.current.x, fabPositionRef.current.y, windowWidth, windowHeight);
212
+ fabPositionRef.current = nextPosition;
213
+ fabPosition.setValue(nextPosition);
214
+ }, [fabPosition, windowHeight, windowWidth]);
215
+ return {
216
+ animateFabPress,
217
+ fabAppearAnim,
218
+ fabPanHandlers: fabPanResponder.panHandlers,
219
+ fabPosition,
220
+ fabPressAnim,
105
221
  };
106
- const closeOverlay = () => {
222
+ };
223
+ export const ElementInspector = () => {
224
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
225
+ const statusBarOffset = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
226
+ const [state, setState] = useState({ status: "idle" });
227
+ const [highlightRect, setHighlightRect] = useState(null);
228
+ const [toastVisible, setToastVisible] = useState(false);
229
+ const [selectionPillVisible, setSelectionPillVisible] = useState(() => getInspectorSettings().selectionPillVisible);
230
+ const isClosingRef = useRef(false);
231
+ const anchorRef = useRef(null);
232
+ const inspectorRootRef = useRef(null);
233
+ const overlayRef = useRef(null);
234
+ const selectingHintAnim = useRef(new Animated.Value(0)).current;
235
+ const toastAnim = useRef(new Animated.Value(0)).current;
236
+ const idle = state.status === "idle";
237
+ const selecting = state.status === "selecting";
238
+ const overlayVisible = state.status !== "idle";
239
+ const activeHighlightRect = state.status === "copying" ? state.selectedRect : highlightRect;
240
+ const { animateFabPress, fabAppearAnim, fabPanHandlers, fabPosition, fabPressAnim } = useFabController(windowWidth, windowHeight);
241
+ const { queryAtPoint, startSession, stopSession } = useTraversalSelectionSession({
242
+ anchorRef,
243
+ inspectorRootRef,
244
+ overlayRef,
245
+ statusBarOffset,
246
+ setHighlightRect,
247
+ });
248
+ const closeOverlayImmediately = useCallback(() => {
249
+ stopSession();
250
+ setState({ status: "idle" });
251
+ setHighlightRect(null);
252
+ }, [stopSession]);
253
+ const closeOverlay = useCallback(() => {
107
254
  if (isClosingRef.current)
108
255
  return;
109
256
  isClosingRef.current = true;
110
257
  closeOverlayImmediately();
111
258
  isClosingRef.current = false;
112
- };
113
- const showCopiedToast = () => {
259
+ }, [closeOverlayImmediately]);
260
+ const showCopiedToast = useCallback(() => {
114
261
  setToastVisible(true);
115
262
  toastAnim.setValue(0);
116
263
  Animated.sequence([
@@ -126,15 +273,15 @@ const ElementInspectorImpl = () => {
126
273
  useNativeDriver: true,
127
274
  }),
128
275
  ]).start(() => setToastVisible(false));
129
- };
130
- const copyFeedbackToHostClipboard = async (text) => {
276
+ }, [toastAnim]);
277
+ const copyFeedbackToHostClipboard = useCallback(async (text) => {
131
278
  const baseUrl = getMetroBaseUrl();
132
279
  if (!baseUrl)
133
280
  return false;
134
281
  try {
135
282
  const response = await fetch(`${baseUrl}${INSPECTOR_COPY_ENDPOINT}`, {
136
- method: 'POST',
137
- headers: { 'Content-Type': 'application/json' },
283
+ method: "POST",
284
+ headers: { "Content-Type": "application/json" },
138
285
  body: JSON.stringify({ text }),
139
286
  });
140
287
  return response.ok;
@@ -142,7 +289,33 @@ const ElementInspectorImpl = () => {
142
289
  catch {
143
290
  return false;
144
291
  }
145
- };
292
+ }, []);
293
+ const handleSelectionHit = useCallback((hitTarget) => {
294
+ setState({ status: "copying", selectedRect: hitTarget.rect });
295
+ void (async () => {
296
+ const clipboardText = await buildInspectorCopyPayload(hitTarget, {
297
+ maxStackLines: MAX_STACK_LINES,
298
+ maxPreviewText: MAX_PREVIEW_TEXT,
299
+ pathRootHint: null,
300
+ });
301
+ const copied = await copyFeedbackToHostClipboard(clipboardText);
302
+ if (copied) {
303
+ showCopiedToast();
304
+ }
305
+ closeOverlay();
306
+ })();
307
+ }, [closeOverlay, copyFeedbackToHostClipboard, showCopiedToast]);
308
+ const { panHandlers: selectionPanHandlers, resetGestureState } = useSelectionGesture({
309
+ isSelecting: selecting,
310
+ queryAtPoint,
311
+ onHitTarget: handleSelectionHit,
312
+ onCancel: closeOverlay,
313
+ });
314
+ const startSelecting = useCallback(() => {
315
+ resetGestureState();
316
+ startSession();
317
+ setState({ status: "selecting" });
318
+ }, [resetGestureState, startSession]);
146
319
  useEffect(() => {
147
320
  return subscribeInspectorSettings(() => {
148
321
  setSelectionPillVisible(getInspectorSettings().selectionPillVisible);
@@ -150,15 +323,14 @@ const ElementInspectorImpl = () => {
150
323
  }, []);
151
324
  useEffect(() => {
152
325
  return subscribeInspectorSelectionRequest(() => {
153
- setHighlightRect(null);
154
326
  startSelecting();
155
327
  });
156
- }, []);
328
+ }, [startSelecting]);
157
329
  useEffect(() => {
158
330
  void initializeInspectorSettings();
159
331
  }, []);
160
332
  useEffect(() => {
161
- DevSettings.addMenuItem('Toggle React Native Grab', () => {
333
+ DevSettings.addMenuItem("Toggle React Native Grab", () => {
162
334
  void (async () => {
163
335
  await initializeInspectorSettings();
164
336
  const current = getInspectorSettings().selectionPillVisible;
@@ -166,104 +338,11 @@ const ElementInspectorImpl = () => {
166
338
  await updateInspectorSettings({ selectionPillVisible: !current });
167
339
  })();
168
340
  });
169
- DevSettings.addMenuItem('Start React Native Grab Selection', () => {
170
- console.log('[InspectorFeedback] Start selection menu item pressed');
341
+ DevSettings.addMenuItem("Start React Native Grab Selection", () => {
342
+ console.log("[InspectorFeedback] Start selection menu item pressed");
171
343
  requestInspectorSelection();
172
344
  });
173
345
  }, []);
174
- useEffect(() => {
175
- const nextPosition = clampFabPosition(fabPositionRef.current.x, fabPositionRef.current.y, windowWidth, windowHeight);
176
- fabPositionRef.current = nextPosition;
177
- fabPosition.setValue(nextPosition);
178
- }, [fabPosition, windowHeight, windowWidth]);
179
- const panResponder = useRef(PanResponder.create({
180
- onStartShouldSetPanResponder: () => true,
181
- onMoveShouldSetPanResponder: () => true,
182
- onPanResponderGrant: (evt) => {
183
- if (stateRef.current.status !== 'selecting')
184
- return;
185
- const x = evt.nativeEvent.locationX;
186
- const y = evt.nativeEvent.locationY;
187
- performQueryRef.current(x, y, statusBarOffset);
188
- lastQueryPositionRef.current = { x, y };
189
- },
190
- onPanResponderMove: (evt) => {
191
- if (stateRef.current.status !== 'selecting')
192
- return;
193
- const x = evt.nativeEvent.locationX;
194
- const y = evt.nativeEvent.locationY;
195
- movePositionRef.current = { x, y };
196
- if (settleTimeoutRef.current != null) {
197
- clearTimeout(settleTimeoutRef.current);
198
- settleTimeoutRef.current = null;
199
- }
200
- settleTimeoutRef.current = setTimeout(() => {
201
- settleTimeoutRef.current = null;
202
- if (stateRef.current.status !== 'selecting')
203
- return;
204
- const pos = movePositionRef.current;
205
- const last = lastQueryPositionRef.current;
206
- const distance = last
207
- ? Math.hypot(pos.x - last.x, pos.y - last.y)
208
- : Infinity;
209
- if (distance < MOVEMENT_EPSILON_PX)
210
- return;
211
- lastQueryPositionRef.current = pos;
212
- performQueryRef.current(pos.x, pos.y, statusBarOffset);
213
- }, SETTLE_MS);
214
- },
215
- onPanResponderRelease: (evt) => {
216
- if (stateRef.current.status !== 'selecting')
217
- return;
218
- const hitTarget = performQueryRef.current(evt.nativeEvent.locationX, evt.nativeEvent.locationY, statusBarOffset);
219
- if (!hitTarget) {
220
- closeOverlay();
221
- return;
222
- }
223
- selectedHitTargetRef.current = hitTarget;
224
- setState({ status: 'copying', selectedRect: hitTarget.rect });
225
- void (async () => {
226
- const clipboardText = await buildInspectorCopyPayload(hitTarget, {
227
- maxStackLines: MAX_STACK_LINES,
228
- maxPreviewText: MAX_PREVIEW_TEXT,
229
- pathRootHint: null,
230
- });
231
- const copied = await copyFeedbackToHostClipboard(clipboardText);
232
- if (copied) {
233
- showCopiedToast();
234
- }
235
- closeOverlay();
236
- })();
237
- },
238
- onPanResponderTerminate: () => {
239
- if (stateRef.current.status === 'selecting') {
240
- closeOverlay();
241
- }
242
- },
243
- })).current;
244
- const fabPanResponder = useRef(PanResponder.create({
245
- onStartShouldSetPanResponder: () => false,
246
- onMoveShouldSetPanResponder: (_evt, gestureState) => Math.abs(gestureState.dx) > 2 || Math.abs(gestureState.dy) > 2,
247
- onPanResponderGrant: (_evt, gestureState) => {
248
- fabDragStartOffsetRef.current = {
249
- x: fabPositionRef.current.x - gestureState.x0,
250
- y: fabPositionRef.current.y - gestureState.y0,
251
- };
252
- },
253
- onPanResponderMove: (_evt, gestureState) => {
254
- const { width, height } = windowSizeRef.current;
255
- const nextPosition = clampFabPosition(gestureState.moveX + fabDragStartOffsetRef.current.x, gestureState.moveY + fabDragStartOffsetRef.current.y, width, height);
256
- fabPositionRef.current = nextPosition;
257
- fabPosition.setValue(nextPosition);
258
- },
259
- onPanResponderTerminationRequest: () => true,
260
- onPanResponderTerminate: () => {
261
- fabPressAnim.setValue(1);
262
- },
263
- onPanResponderRelease: () => {
264
- fabPressAnim.setValue(1);
265
- },
266
- })).current;
267
346
  useEffect(() => {
268
347
  if (!selecting) {
269
348
  selectingHintAnim.stopAnimation();
@@ -292,76 +371,86 @@ const ElementInspectorImpl = () => {
292
371
  useNativeDriver: true,
293
372
  }).start();
294
373
  }, [fabAppearAnim, idle]);
295
- return (_jsx(InspectorRootContainer, { children: _jsxs(View, { ref: inspectorRootRef, style: StyleSheet.absoluteFill, pointerEvents: "box-none", children: [_jsx(View, { ref: anchorRef, style: [StyleSheet.absoluteFill, styles.anchor], pointerEvents: "none" }), idle && selectionPillVisible && (_jsx(Animated.View, { ...fabPanResponder.panHandlers, style: [
296
- styles.fab,
297
- {
298
- opacity: fabAppearAnim,
299
- transform: [
300
- { translateX: fabPosition.x },
301
- {
302
- translateY: Animated.add(fabPosition.y, fabAppearAnim.interpolate({
303
- inputRange: [0, 1],
304
- outputRange: [8, 0],
305
- })),
306
- },
307
- { scale: fabPressAnim },
308
- ],
309
- },
310
- ], children: _jsx(Pressable, { style: styles.fabInner, onPress: startSelecting, onPressIn: () => {
311
- Animated.spring(fabPressAnim, {
312
- toValue: 0.96,
313
- useNativeDriver: true,
314
- tension: 220,
315
- friction: 16,
316
- }).start();
317
- }, onPressOut: () => {
318
- Animated.spring(fabPressAnim, {
319
- toValue: 1,
320
- useNativeDriver: true,
321
- tension: 220,
322
- friction: 16,
323
- }).start();
324
- }, children: _jsx(Text, { style: styles.fabIcon, allowFontScaling: false, children: "\u2316" }) }) })), overlayVisible && (_jsxs(View, { ref: overlayRef, style: styles.overlay, ...(selecting ? panResponder.panHandlers : {}), children: [activeHighlightRect && (_jsx(View, { pointerEvents: "none", style: [
325
- styles.highlight,
326
- {
327
- left: activeHighlightRect.x,
328
- top: activeHighlightRect.y + statusBarOffset,
329
- width: activeHighlightRect.width,
330
- height: activeHighlightRect.height,
331
- },
332
- ] })), selecting && (_jsx(Animated.View, { pointerEvents: "none", style: [
333
- styles.selectionHint,
334
- {
335
- opacity: selectingHintAnim.interpolate({
336
- inputRange: [0, 1],
337
- outputRange: [0.76, 1],
338
- }),
339
- transform: [
340
- {
341
- translateY: selectingHintAnim.interpolate({
342
- inputRange: [0, 1],
343
- outputRange: [0, -2],
344
- }),
345
- },
346
- ],
347
- },
348
- ], children: _jsx(Text, { style: styles.selectionHintText, children: "Drag or tap to find a source" }) }))] })), toastVisible && (_jsx(Animated.View, { pointerEvents: "none", style: [
349
- styles.toast,
350
- {
351
- opacity: toastAnim,
352
- transform: [
353
- {
354
- translateY: toastAnim.interpolate({
355
- inputRange: [0, 1],
356
- outputRange: [-8, 0],
357
- }),
358
- },
359
- ],
360
- },
361
- ], children: _jsx(Text, { style: styles.toastText, children: "Source location copied" }) }))] }) }));
374
+ useEffect(() => {
375
+ return () => {
376
+ resetGestureState();
377
+ stopSession();
378
+ };
379
+ }, [resetGestureState, stopSession]);
380
+ return (_jsx(InspectorRootContainer, { children: _jsxs(View, { ref: inspectorRootRef, style: StyleSheet.absoluteFill, pointerEvents: "box-none", children: [_jsx(View, { ref: anchorRef, style: [StyleSheet.absoluteFill, styles.anchor], pointerEvents: "none" }), _jsx(SelectionFab, { visible: idle && selectionPillVisible, onPress: startSelecting, onPressIn: () => animateFabPress(0.96), onPressOut: () => animateFabPress(1), panHandlers: fabPanHandlers, fabAppearAnim: fabAppearAnim, fabPosition: fabPosition, fabPressAnim: fabPressAnim }), _jsx(SelectionOverlay, { visible: overlayVisible, selecting: selecting, overlayRef: overlayRef, panHandlers: selectionPanHandlers, highlightRect: activeHighlightRect, statusBarOffset: statusBarOffset, selectingHintAnim: selectingHintAnim }), _jsx(CopiedToast, { visible: toastVisible, toastAnim: toastAnim })] }) }));
381
+ };
382
+ const SelectionFab = ({ visible, onPress, onPressIn, onPressOut, panHandlers, fabAppearAnim, fabPosition, fabPressAnim, }) => {
383
+ if (!visible) {
384
+ return null;
385
+ }
386
+ return (_jsx(Animated.View, { ...panHandlers, style: [
387
+ styles.fab,
388
+ {
389
+ opacity: fabAppearAnim,
390
+ transform: [
391
+ { translateX: fabPosition.x },
392
+ {
393
+ translateY: Animated.add(fabPosition.y, fabAppearAnim.interpolate({
394
+ inputRange: [0, 1],
395
+ outputRange: [8, 0],
396
+ })),
397
+ },
398
+ { scale: fabPressAnim },
399
+ ],
400
+ },
401
+ ], children: _jsx(Pressable, { style: styles.fabInner, onPress: onPress, onPressIn: onPressIn, onPressOut: onPressOut, children: _jsx(Text, { style: styles.fabIcon, allowFontScaling: false, children: "\u2316" }) }) }));
402
+ };
403
+ const SelectionOverlay = ({ visible, selecting, overlayRef, panHandlers, highlightRect, statusBarOffset, selectingHintAnim, }) => {
404
+ if (!visible) {
405
+ return null;
406
+ }
407
+ return (_jsxs(View, { ref: overlayRef, style: styles.overlay, ...(selecting ? panHandlers : {}), children: [highlightRect && (_jsx(View, { pointerEvents: "none", style: [
408
+ styles.highlight,
409
+ {
410
+ left: highlightRect.x,
411
+ top: highlightRect.y + statusBarOffset,
412
+ width: highlightRect.width,
413
+ height: highlightRect.height,
414
+ },
415
+ ] })), selecting && (_jsx(Animated.View, { pointerEvents: "none", style: [
416
+ styles.selectionHint,
417
+ {
418
+ opacity: selectingHintAnim.interpolate({
419
+ inputRange: [0, 1],
420
+ outputRange: [0.76, 1],
421
+ }),
422
+ transform: [
423
+ {
424
+ translateY: selectingHintAnim.interpolate({
425
+ inputRange: [0, 1],
426
+ outputRange: [0, -2],
427
+ }),
428
+ },
429
+ ],
430
+ },
431
+ ], children: _jsx(Text, { style: styles.selectionHintText, children: "Drag or tap to find a source" }) }))] }));
432
+ };
433
+ const CopiedToast = ({ visible, toastAnim }) => {
434
+ if (!visible) {
435
+ return null;
436
+ }
437
+ return (_jsx(Animated.View, { pointerEvents: "none", style: [
438
+ styles.toast,
439
+ {
440
+ opacity: toastAnim,
441
+ transform: [
442
+ {
443
+ translateY: toastAnim.interpolate({
444
+ inputRange: [0, 1],
445
+ outputRange: [-8, 0],
446
+ }),
447
+ },
448
+ ],
449
+ },
450
+ ], children: _jsx(Text, { style: styles.toastText, children: "Source location copied" }) }));
362
451
  };
363
452
  const InspectorRootContainer = ({ children }) => {
364
- if (Platform.OS === 'ios') {
453
+ if (Platform.OS === "ios") {
365
454
  return _jsx(FullWindowOverlay, { children: children });
366
455
  }
367
456
  return _jsx(Fragment, { children: children });
@@ -371,75 +460,75 @@ const styles = StyleSheet.create({
371
460
  opacity: 0,
372
461
  },
373
462
  fab: {
374
- position: 'absolute',
463
+ position: "absolute",
375
464
  left: 0,
376
465
  top: 0,
377
466
  zIndex: 9999,
378
467
  width: FAB_SIZE,
379
468
  height: FAB_SIZE,
380
469
  borderRadius: 9999,
381
- backgroundColor: 'rgba(14,20,26,0.98)',
470
+ backgroundColor: "rgba(14,20,26,0.98)",
382
471
  borderWidth: 1,
383
- borderColor: 'rgba(255,255,255,0.14)',
472
+ borderColor: "rgba(255,255,255,0.14)",
384
473
  elevation: 4,
385
- shadowColor: '#000',
474
+ shadowColor: "#000",
386
475
  shadowOffset: { width: 0, height: 2 },
387
476
  shadowOpacity: 0.28,
388
477
  shadowRadius: 6,
389
478
  },
390
479
  fabInner: {
391
480
  flex: 1,
392
- flexDirection: 'row',
393
- justifyContent: 'center',
394
- alignItems: 'center',
395
- width: '100%',
396
- height: '100%',
481
+ flexDirection: "row",
482
+ justifyContent: "center",
483
+ alignItems: "center",
484
+ width: "100%",
485
+ height: "100%",
397
486
  },
398
487
  fabIcon: {
399
- color: '#F5F7FA',
488
+ color: "#F5F7FA",
400
489
  fontSize: 28,
401
- fontWeight: '500',
490
+ fontWeight: "500",
402
491
  },
403
492
  overlay: {
404
493
  ...StyleSheet.absoluteFillObject,
405
- backgroundColor: 'rgba(0,0,0,0.015)',
494
+ backgroundColor: "rgba(0,0,0,0.015)",
406
495
  },
407
496
  selectionHint: {
408
- position: 'absolute',
497
+ position: "absolute",
409
498
  top: 56,
410
- alignSelf: 'center',
411
- backgroundColor: 'rgba(14,20,26,0.9)',
499
+ alignSelf: "center",
500
+ backgroundColor: "rgba(14,20,26,0.9)",
412
501
  borderRadius: 18,
413
502
  paddingHorizontal: 14,
414
503
  paddingVertical: 8,
415
504
  borderWidth: 1,
416
- borderColor: 'rgba(255,255,255,0.14)',
505
+ borderColor: "rgba(255,255,255,0.14)",
417
506
  },
418
507
  selectionHintText: {
419
- color: '#F5F7FA',
508
+ color: "#F5F7FA",
420
509
  fontSize: 13,
421
- fontWeight: '600',
510
+ fontWeight: "600",
422
511
  },
423
512
  highlight: {
424
- position: 'absolute',
513
+ position: "absolute",
425
514
  borderWidth: 2,
426
- borderColor: '#1264A3',
427
- backgroundColor: 'rgba(18,100,163,0.16)',
515
+ borderColor: "#1264A3",
516
+ backgroundColor: "rgba(18,100,163,0.16)",
428
517
  },
429
518
  toast: {
430
- position: 'absolute',
519
+ position: "absolute",
431
520
  top: 52,
432
521
  left: 16,
433
522
  right: 16,
434
- alignItems: 'center',
523
+ alignItems: "center",
435
524
  paddingVertical: 10,
436
525
  borderRadius: 10,
437
- backgroundColor: 'rgba(18,100,163,0.96)',
526
+ backgroundColor: "rgba(18,100,163,0.96)",
438
527
  },
439
528
  toastText: {
440
- color: '#fff',
529
+ color: "#fff",
441
530
  fontSize: 14,
442
- fontWeight: '700',
531
+ fontWeight: "700",
443
532
  },
444
533
  });
445
534
  //# sourceMappingURL=index.js.map