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