react-native-grab 0.0.1 → 0.0.3

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