react-native-element-inspector 0.1.0

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/lib/ElementCycler.d.ts +14 -0
  4. package/lib/ElementCycler.d.ts.map +1 -0
  5. package/lib/ElementCycler.js +33 -0
  6. package/lib/ElementCycler.js.map +1 -0
  7. package/lib/ElementHighlighter.d.ts +21 -0
  8. package/lib/ElementHighlighter.d.ts.map +1 -0
  9. package/lib/ElementHighlighter.js +89 -0
  10. package/lib/ElementHighlighter.js.map +1 -0
  11. package/lib/ElementInspector.d.ts +12 -0
  12. package/lib/ElementInspector.d.ts.map +1 -0
  13. package/lib/ElementInspector.js +74 -0
  14. package/lib/ElementInspector.js.map +1 -0
  15. package/lib/components/Checkbox.d.ts +7 -0
  16. package/lib/components/Checkbox.d.ts.map +1 -0
  17. package/lib/components/Checkbox.js +30 -0
  18. package/lib/components/Checkbox.js.map +1 -0
  19. package/lib/components/index.d.ts +2 -0
  20. package/lib/components/index.d.ts.map +1 -0
  21. package/lib/components/index.js +2 -0
  22. package/lib/components/index.js.map +1 -0
  23. package/lib/constants/colors.d.ts +24 -0
  24. package/lib/constants/colors.d.ts.map +1 -0
  25. package/lib/constants/colors.js +18 -0
  26. package/lib/constants/colors.js.map +1 -0
  27. package/lib/constants/index.d.ts +3 -0
  28. package/lib/constants/index.d.ts.map +1 -0
  29. package/lib/constants/index.js +3 -0
  30. package/lib/constants/index.js.map +1 -0
  31. package/lib/constants/ui.d.ts +56 -0
  32. package/lib/constants/ui.d.ts.map +1 -0
  33. package/lib/constants/ui.js +46 -0
  34. package/lib/constants/ui.js.map +1 -0
  35. package/lib/fiber/FiberAdapter.d.ts +52 -0
  36. package/lib/fiber/FiberAdapter.d.ts.map +1 -0
  37. package/lib/fiber/FiberAdapter.js +112 -0
  38. package/lib/fiber/FiberAdapter.js.map +1 -0
  39. package/lib/fiber/detectVersion.d.ts +9 -0
  40. package/lib/fiber/detectVersion.d.ts.map +1 -0
  41. package/lib/fiber/detectVersion.js +17 -0
  42. package/lib/fiber/detectVersion.js.map +1 -0
  43. package/lib/fiber/index.d.ts +5 -0
  44. package/lib/fiber/index.d.ts.map +1 -0
  45. package/lib/fiber/index.js +4 -0
  46. package/lib/fiber/index.js.map +1 -0
  47. package/lib/fiber/types.d.ts +35 -0
  48. package/lib/fiber/types.d.ts.map +1 -0
  49. package/lib/fiber/types.js +3 -0
  50. package/lib/fiber/types.js.map +1 -0
  51. package/lib/floatingPanel/AddPropertyRow.d.ts +13 -0
  52. package/lib/floatingPanel/AddPropertyRow.d.ts.map +1 -0
  53. package/lib/floatingPanel/AddPropertyRow.js +61 -0
  54. package/lib/floatingPanel/AddPropertyRow.js.map +1 -0
  55. package/lib/floatingPanel/CloseButton.d.ts +7 -0
  56. package/lib/floatingPanel/CloseButton.d.ts.map +1 -0
  57. package/lib/floatingPanel/CloseButton.js +21 -0
  58. package/lib/floatingPanel/CloseButton.js.map +1 -0
  59. package/lib/floatingPanel/EditableValue.d.ts +12 -0
  60. package/lib/floatingPanel/EditableValue.d.ts.map +1 -0
  61. package/lib/floatingPanel/EditableValue.js +67 -0
  62. package/lib/floatingPanel/EditableValue.js.map +1 -0
  63. package/lib/floatingPanel/FloatingPanel.d.ts +7 -0
  64. package/lib/floatingPanel/FloatingPanel.d.ts.map +1 -0
  65. package/lib/floatingPanel/FloatingPanel.js +70 -0
  66. package/lib/floatingPanel/FloatingPanel.js.map +1 -0
  67. package/lib/floatingPanel/HandleContent.d.ts +7 -0
  68. package/lib/floatingPanel/HandleContent.d.ts.map +1 -0
  69. package/lib/floatingPanel/HandleContent.js +35 -0
  70. package/lib/floatingPanel/HandleContent.js.map +1 -0
  71. package/lib/floatingPanel/InspectorBubble.d.ts +6 -0
  72. package/lib/floatingPanel/InspectorBubble.d.ts.map +1 -0
  73. package/lib/floatingPanel/InspectorBubble.js +91 -0
  74. package/lib/floatingPanel/InspectorBubble.js.map +1 -0
  75. package/lib/floatingPanel/PanelBody.d.ts +8 -0
  76. package/lib/floatingPanel/PanelBody.d.ts.map +1 -0
  77. package/lib/floatingPanel/PanelBody.js +72 -0
  78. package/lib/floatingPanel/PanelBody.js.map +1 -0
  79. package/lib/floatingPanel/PanelFooter.d.ts +9 -0
  80. package/lib/floatingPanel/PanelFooter.d.ts.map +1 -0
  81. package/lib/floatingPanel/PanelFooter.js +21 -0
  82. package/lib/floatingPanel/PanelFooter.js.map +1 -0
  83. package/lib/floatingPanel/PanelHeader.d.ts +13 -0
  84. package/lib/floatingPanel/PanelHeader.d.ts.map +1 -0
  85. package/lib/floatingPanel/PanelHeader.js +56 -0
  86. package/lib/floatingPanel/PanelHeader.js.map +1 -0
  87. package/lib/floatingPanel/index.d.ts +12 -0
  88. package/lib/floatingPanel/index.d.ts.map +1 -0
  89. package/lib/floatingPanel/index.js +11 -0
  90. package/lib/floatingPanel/index.js.map +1 -0
  91. package/lib/floatingPanel/panelUtils.d.ts +3 -0
  92. package/lib/floatingPanel/panelUtils.d.ts.map +1 -0
  93. package/lib/floatingPanel/panelUtils.js +12 -0
  94. package/lib/floatingPanel/panelUtils.js.map +1 -0
  95. package/lib/floatingPanel/types.d.ts +23 -0
  96. package/lib/floatingPanel/types.d.ts.map +1 -0
  97. package/lib/floatingPanel/types.js +2 -0
  98. package/lib/floatingPanel/types.js.map +1 -0
  99. package/lib/hooks/index.d.ts +8 -0
  100. package/lib/hooks/index.d.ts.map +1 -0
  101. package/lib/hooks/index.js +8 -0
  102. package/lib/hooks/index.js.map +1 -0
  103. package/lib/hooks/useDebouncedCallback.d.ts +2 -0
  104. package/lib/hooks/useDebouncedCallback.d.ts.map +1 -0
  105. package/lib/hooks/useDebouncedCallback.js +12 -0
  106. package/lib/hooks/useDebouncedCallback.js.map +1 -0
  107. package/lib/hooks/useDebouncedValue.d.ts +2 -0
  108. package/lib/hooks/useDebouncedValue.d.ts.map +1 -0
  109. package/lib/hooks/useDebouncedValue.js +10 -0
  110. package/lib/hooks/useDebouncedValue.js.map +1 -0
  111. package/lib/hooks/useFloatingPanel.d.ts +14 -0
  112. package/lib/hooks/useFloatingPanel.d.ts.map +1 -0
  113. package/lib/hooks/useFloatingPanel.js +132 -0
  114. package/lib/hooks/useFloatingPanel.js.map +1 -0
  115. package/lib/hooks/useLayoutSnapshot.d.ts +14 -0
  116. package/lib/hooks/useLayoutSnapshot.d.ts.map +1 -0
  117. package/lib/hooks/useLayoutSnapshot.js +39 -0
  118. package/lib/hooks/useLayoutSnapshot.js.map +1 -0
  119. package/lib/hooks/useStyleMutation.d.ts +14 -0
  120. package/lib/hooks/useStyleMutation.d.ts.map +1 -0
  121. package/lib/hooks/useStyleMutation.js +22 -0
  122. package/lib/hooks/useStyleMutation.js.map +1 -0
  123. package/lib/hooks/useStyleOverrides.d.ts +24 -0
  124. package/lib/hooks/useStyleOverrides.d.ts.map +1 -0
  125. package/lib/hooks/useStyleOverrides.js +165 -0
  126. package/lib/hooks/useStyleOverrides.js.map +1 -0
  127. package/lib/hooks/useTapToSelect.d.ts +20 -0
  128. package/lib/hooks/useTapToSelect.d.ts.map +1 -0
  129. package/lib/hooks/useTapToSelect.js +58 -0
  130. package/lib/hooks/useTapToSelect.js.map +1 -0
  131. package/lib/index.d.ts +3 -0
  132. package/lib/index.d.ts.map +1 -0
  133. package/lib/index.js +2 -0
  134. package/lib/index.js.map +1 -0
  135. package/lib/utils/clamp.d.ts +2 -0
  136. package/lib/utils/clamp.d.ts.map +1 -0
  137. package/lib/utils/clamp.js +2 -0
  138. package/lib/utils/clamp.js.map +1 -0
  139. package/lib/utils/flattenStyles.d.ts +6 -0
  140. package/lib/utils/flattenStyles.d.ts.map +1 -0
  141. package/lib/utils/flattenStyles.js +11 -0
  142. package/lib/utils/flattenStyles.js.map +1 -0
  143. package/lib/utils/hitTest.d.ts +7 -0
  144. package/lib/utils/hitTest.d.ts.map +1 -0
  145. package/lib/utils/hitTest.js +20 -0
  146. package/lib/utils/hitTest.js.map +1 -0
  147. package/lib/utils/index.d.ts +12 -0
  148. package/lib/utils/index.d.ts.map +1 -0
  149. package/lib/utils/index.js +9 -0
  150. package/lib/utils/index.js.map +1 -0
  151. package/lib/utils/layoutSnapshot.d.ts +7 -0
  152. package/lib/utils/layoutSnapshot.d.ts.map +1 -0
  153. package/lib/utils/layoutSnapshot.js +42 -0
  154. package/lib/utils/layoutSnapshot.js.map +1 -0
  155. package/lib/utils/sourceMapping.d.ts +26 -0
  156. package/lib/utils/sourceMapping.d.ts.map +1 -0
  157. package/lib/utils/sourceMapping.js +53 -0
  158. package/lib/utils/sourceMapping.js.map +1 -0
  159. package/lib/utils/styleFormatting.d.ts +5 -0
  160. package/lib/utils/styleFormatting.d.ts.map +1 -0
  161. package/lib/utils/styleFormatting.js +38 -0
  162. package/lib/utils/styleFormatting.js.map +1 -0
  163. package/lib/utils/styleInputParsing.d.ts +5 -0
  164. package/lib/utils/styleInputParsing.d.ts.map +1 -0
  165. package/lib/utils/styleInputParsing.js +33 -0
  166. package/lib/utils/styleInputParsing.js.map +1 -0
  167. package/lib/utils/yogaLayout.d.ts +33 -0
  168. package/lib/utils/yogaLayout.d.ts.map +1 -0
  169. package/lib/utils/yogaLayout.js +33 -0
  170. package/lib/utils/yogaLayout.js.map +1 -0
  171. package/package.json +74 -0
  172. package/src/ElementCycler.tsx +64 -0
  173. package/src/ElementHighlighter.tsx +122 -0
  174. package/src/ElementInspector.tsx +119 -0
  175. package/src/components/Checkbox.tsx +41 -0
  176. package/src/components/index.ts +1 -0
  177. package/src/constants/colors.ts +18 -0
  178. package/src/constants/index.ts +9 -0
  179. package/src/constants/ui.ts +51 -0
  180. package/src/fiber/FiberAdapter.ts +153 -0
  181. package/src/fiber/detectVersion.ts +19 -0
  182. package/src/fiber/index.ts +4 -0
  183. package/src/fiber/types.ts +36 -0
  184. package/src/floatingPanel/AddPropertyRow.tsx +102 -0
  185. package/src/floatingPanel/CloseButton.tsx +34 -0
  186. package/src/floatingPanel/EditableValue.tsx +109 -0
  187. package/src/floatingPanel/FloatingPanel.tsx +114 -0
  188. package/src/floatingPanel/HandleContent.tsx +45 -0
  189. package/src/floatingPanel/InspectorBubble.tsx +121 -0
  190. package/src/floatingPanel/PanelBody.tsx +162 -0
  191. package/src/floatingPanel/PanelFooter.tsx +36 -0
  192. package/src/floatingPanel/PanelHeader.tsx +111 -0
  193. package/src/floatingPanel/index.ts +11 -0
  194. package/src/floatingPanel/panelUtils.ts +13 -0
  195. package/src/floatingPanel/types.ts +26 -0
  196. package/src/hooks/index.ts +7 -0
  197. package/src/hooks/useDebouncedCallback.ts +18 -0
  198. package/src/hooks/useDebouncedValue.ts +12 -0
  199. package/src/hooks/useFloatingPanel.ts +191 -0
  200. package/src/hooks/useLayoutSnapshot.ts +42 -0
  201. package/src/hooks/useStyleMutation.ts +31 -0
  202. package/src/hooks/useStyleOverrides.ts +176 -0
  203. package/src/hooks/useTapToSelect.ts +76 -0
  204. package/src/index.ts +2 -0
  205. package/src/utils/clamp.ts +2 -0
  206. package/src/utils/flattenStyles.ts +12 -0
  207. package/src/utils/hitTest.ts +29 -0
  208. package/src/utils/index.ts +11 -0
  209. package/src/utils/layoutSnapshot.ts +48 -0
  210. package/src/utils/sourceMapping.ts +67 -0
  211. package/src/utils/styleFormatting.ts +34 -0
  212. package/src/utils/styleInputParsing.ts +33 -0
  213. package/src/utils/yogaLayout.ts +49 -0
@@ -0,0 +1,119 @@
1
+ import type { ReactNode } from 'react';
2
+ import { useCallback, useState } from 'react';
3
+ import { StyleSheet, View } from 'react-native';
4
+ import { FLOATING_PANEL, Z_INDEX } from './constants';
5
+ import { ElementHighlighter } from './ElementHighlighter';
6
+ import { FloatingPanel, type PanelState } from './floatingPanel';
7
+ import { useDebouncedCallback, useLayoutSnapshot, useTapToSelect } from './hooks';
8
+
9
+ export interface ElementInspectorProps {
10
+ /** Only enable in dev mode. Pass `__DEV__` here. */
11
+ enabled?: boolean;
12
+ children: ReactNode;
13
+ }
14
+
15
+ /**
16
+ * Root wrapper component. Wrap your app root with this.
17
+ * In production (enabled=false), renders children with zero overhead.
18
+ */
19
+ export const ElementInspector = ({ enabled = false, children }: ElementInspectorProps) => {
20
+ const [isInspecting, setIsInspecting] = useState(false);
21
+ const [highlightVisible, setHighlightVisible] = useState(false);
22
+ const { snapshot, buildSnapshot } = useLayoutSnapshot();
23
+ const { selected, matches, selectedIndex, handleTap, cycleNext, cyclePrevious, clearSelection } =
24
+ useTapToSelect(snapshot);
25
+
26
+ const hideHighlight = useDebouncedCallback(
27
+ () => setHighlightVisible(false),
28
+ FLOATING_PANEL.HIGHLIGHT_FLASH_MS,
29
+ );
30
+ const flashHighlight = useCallback(() => {
31
+ setHighlightVisible(true);
32
+ hideHighlight();
33
+ }, [hideHighlight]);
34
+
35
+ if (!enabled) {
36
+ return <>{children}</>;
37
+ }
38
+
39
+ // Derive panel state from inspector state
40
+ const panelState: PanelState = isInspecting ? (selected ? 'expanded' : 'handle') : 'bubble';
41
+
42
+ const toggleInspect = async () => {
43
+ if (isInspecting) {
44
+ setIsInspecting(false);
45
+ clearSelection();
46
+ } else {
47
+ try {
48
+ await buildSnapshot();
49
+ setIsInspecting(true);
50
+ } catch (error) {
51
+ // biome-ignore lint/suspicious/noConsole: dev tool — errors should be visible
52
+ console.warn('[ElementInspector] Snapshot failed:', error);
53
+ }
54
+ }
55
+ };
56
+
57
+ const handleClose = () => {
58
+ if (selected) {
59
+ // Expanded → handle (deselect element, stay in inspect mode)
60
+ clearSelection();
61
+ } else {
62
+ // Handle → bubble (exit inspect mode)
63
+ setIsInspecting(false);
64
+ }
65
+ };
66
+
67
+ return (
68
+ <View style={styles.container}>
69
+ {children}
70
+
71
+ {/* Transparent tap-capture overlay — only active in inspect mode */}
72
+ {isInspecting && (
73
+ <View
74
+ style={styles.tapOverlay}
75
+ onStartShouldSetResponder={() => true}
76
+ onResponderRelease={(event) => {
77
+ handleTap(event);
78
+ flashHighlight();
79
+ }}
80
+ />
81
+ )}
82
+
83
+ {/* Persistent outline — always visible while element is selected */}
84
+ {isInspecting && selected && <ElementHighlighter element={selected} outlineOnly />}
85
+
86
+ {/* Full box model highlight — flashes on select/cycle, then auto-hides */}
87
+ {highlightVisible && <ElementHighlighter element={selected} />}
88
+
89
+ {/* Floating panel — bubble / handle / expanded */}
90
+ <FloatingPanel
91
+ panelState={panelState}
92
+ selected={selected}
93
+ matches={matches}
94
+ selectedIndex={selectedIndex}
95
+ onToggleInspect={toggleInspect}
96
+ onCycleNext={() => {
97
+ cycleNext();
98
+ flashHighlight();
99
+ }}
100
+ onCyclePrevious={() => {
101
+ cyclePrevious();
102
+ flashHighlight();
103
+ }}
104
+ onClose={handleClose}
105
+ />
106
+ </View>
107
+ );
108
+ };
109
+
110
+ const styles = StyleSheet.create({
111
+ container: {
112
+ flex: 1,
113
+ },
114
+ tapOverlay: {
115
+ ...StyleSheet.absoluteFillObject,
116
+ backgroundColor: 'transparent',
117
+ zIndex: Z_INDEX.TAP_OVERLAY,
118
+ },
119
+ });
@@ -0,0 +1,41 @@
1
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
2
+
3
+ interface CheckboxProps {
4
+ checked: boolean;
5
+ onToggle: () => void;
6
+ }
7
+
8
+ export const Checkbox = ({ checked, onToggle }: CheckboxProps) => (
9
+ <Pressable onPress={onToggle} hitSlop={6} style={styles.pressable}>
10
+ <View style={[styles.box, checked && styles.checked]}>
11
+ {checked && <Text style={styles.tick}>✓</Text>}
12
+ </View>
13
+ </Pressable>
14
+ );
15
+
16
+ const styles = StyleSheet.create({
17
+ pressable: {
18
+ justifyContent: 'center',
19
+ alignItems: 'center',
20
+ marginRight: 6,
21
+ },
22
+ box: {
23
+ width: 12,
24
+ height: 12,
25
+ borderRadius: 2,
26
+ borderWidth: 1.5,
27
+ borderColor: '#555',
28
+ justifyContent: 'center',
29
+ alignItems: 'center',
30
+ },
31
+ checked: {
32
+ backgroundColor: '#4FC3F7',
33
+ borderColor: '#4FC3F7',
34
+ },
35
+ tick: {
36
+ fontSize: 9,
37
+ lineHeight: 11,
38
+ color: '#fff',
39
+ fontWeight: '700',
40
+ },
41
+ });
@@ -0,0 +1 @@
1
+ export { Checkbox } from './Checkbox';
@@ -0,0 +1,18 @@
1
+ /** Chrome DevTools-style box model colors */
2
+ export const BOX_MODEL_COLORS = {
3
+ margin: 'rgba(246, 178, 107, 0.4)',
4
+ marginLabel: '#F6B26B',
5
+ border: 'rgba(255, 217, 102, 0.3)',
6
+ borderLabel: '#FFD966',
7
+ padding: 'rgba(147, 196, 125, 0.4)',
8
+ paddingLabel: '#6AA84F',
9
+ content: 'rgba(79, 195, 247, 0.3)',
10
+ contentLabel: '#4A90D9',
11
+ outline: '#4FC3F7',
12
+ } as const;
13
+
14
+ /** VS Code-style syntax colors for the editable style property list */
15
+ export const EDITABLE_VALUE_COLORS = {
16
+ key: { text: '#9CDCFE', underline: 'rgba(156, 220, 254, 0.4)' },
17
+ value: { text: '#CE9178', underline: 'rgba(206, 145, 120, 0.4)' },
18
+ } as const;
@@ -0,0 +1,9 @@
1
+ export { BOX_MODEL_COLORS, EDITABLE_VALUE_COLORS } from './colors';
2
+ export {
3
+ EDITABLE_VALUE,
4
+ FLOATING_PANEL,
5
+ INSPECTOR_BUBBLE,
6
+ MEASURE_TIMEOUT_MS,
7
+ MONOSPACE_FONT,
8
+ Z_INDEX,
9
+ } from './ui';
@@ -0,0 +1,51 @@
1
+ import { Platform } from 'react-native';
2
+
3
+ /** Cross-platform monospace font family */
4
+ export const MONOSPACE_FONT = Platform.select({ ios: 'Menlo', default: 'monospace' });
5
+
6
+ /** Z-index layering for inspector UI elements */
7
+ export const Z_INDEX = {
8
+ HIGHLIGHT: 9997,
9
+ TAP_OVERLAY: 9998,
10
+ FLOATING_PANEL: 10000,
11
+ } as const;
12
+
13
+ /** Timeout for fiber measure() calls (ms) */
14
+ export const MEASURE_TIMEOUT_MS = 3000;
15
+
16
+ /** Floating panel dimensions and animation config */
17
+ export const FLOATING_PANEL = {
18
+ BUBBLE_SIZE: 48,
19
+ HANDLE_WIDTH: 180,
20
+ HANDLE_HEIGHT: 44,
21
+ PANEL_WIDTH: 280,
22
+ PANEL_HEIGHT: 320,
23
+ PANEL_BODY_HEIGHT: 220,
24
+ TAP_THRESHOLD: 5,
25
+ EDGE_MARGIN: 12,
26
+ TOP_SAFE_AREA: 50,
27
+ BOTTOM_SAFE_AREA: 40,
28
+ SNAP_FRICTION: 7,
29
+ SNAP_TENSION: 40,
30
+ EXPAND_DURATION: 250,
31
+ HIGHLIGHT_FLASH_MS: 1500,
32
+ } as const;
33
+
34
+ /** Validation constraints for editable style values */
35
+ export const EDITABLE_VALUE = {
36
+ VALID_STYLE_KEY: /^[a-zA-Z][a-zA-Z0-9]*$/,
37
+ MAX_VALUE_LENGTH: 200,
38
+ } as const;
39
+
40
+ /** InspectorBubble sizing and animation trail config */
41
+ export const INSPECTOR_BUBBLE = {
42
+ SIZE: 48,
43
+ SWEEP_WIDTH: 2.5,
44
+ CENTER_DOT_SIZE: 8,
45
+ TRAIL_ARMS: [
46
+ { key: 'sweep', offsetDeg: 0, opacity: 0.9 },
47
+ { key: 'trail-1', offsetDeg: -18, opacity: 0.45 },
48
+ { key: 'trail-2', offsetDeg: -36, opacity: 0.3 },
49
+ { key: 'trail-3', offsetDeg: -54, opacity: 0.2 },
50
+ ],
51
+ } as const;
@@ -0,0 +1,153 @@
1
+ import { MEASURE_TIMEOUT_MS } from '../constants';
2
+ import { flattenStyles } from '../utils/flattenStyles';
3
+ import { type FiberNode, HOST_COMPONENT_TAG } from './types';
4
+
5
+ /**
6
+ * Unified API for all fiber tree access.
7
+ * Abstracts away React version differences.
8
+ */
9
+ export const FiberAdapter = {
10
+ /**
11
+ * Get the fiber root from the React DevTools global hook.
12
+ * Returns null if the hook is not available (production build).
13
+ */
14
+ getFiberRoot: (): FiberNode | null => {
15
+ const hook = (globalThis as Record<string, unknown>).__REACT_DEVTOOLS_GLOBAL_HOOK__ as
16
+ | {
17
+ renderers?: Map<number, unknown>;
18
+ getFiberRoots?: (id: number) => Set<{ current: FiberNode }>;
19
+ }
20
+ | undefined;
21
+
22
+ if (!(hook?.renderers && hook.getFiberRoots)) return null;
23
+
24
+ // Try each renderer ID — the RN renderer isn't always ID 1
25
+ for (const rendererId of hook.renderers.keys()) {
26
+ const roots = hook.getFiberRoots(rendererId);
27
+ if (!roots || roots.size === 0) continue;
28
+
29
+ const root = roots.values().next().value;
30
+ if (root?.current) return root.current;
31
+ }
32
+
33
+ return null;
34
+ },
35
+
36
+ /**
37
+ * Walk the fiber tree and collect all host component fibers with their depth.
38
+ * Depth is needed later for z-ordering in the layout snapshot.
39
+ */
40
+ walkHostFibers: (root: FiberNode): Array<{ fiber: FiberNode; depth: number }> => {
41
+ const hostFibers: Array<{ fiber: FiberNode; depth: number }> = [];
42
+
43
+ const walk = (fiber: FiberNode | null, depth: number) => {
44
+ let current = fiber;
45
+ while (current) {
46
+ if (current.tag === HOST_COMPONENT_TAG) {
47
+ hostFibers.push({ fiber: current, depth });
48
+ }
49
+
50
+ walk(current.child, depth + 1);
51
+ current = current.sibling;
52
+ }
53
+ };
54
+
55
+ walk(root.child, 0);
56
+ return hostFibers;
57
+ },
58
+
59
+ /**
60
+ * Get the flattened style object from a fiber's props.
61
+ */
62
+ getStyle: (fiber: FiberNode): Record<string, unknown> | null => {
63
+ return flattenStyles(fiber.memoizedProps?.style);
64
+ },
65
+
66
+ /**
67
+ * Get the source file location from a fiber's debug info.
68
+ */
69
+ getSource: (fiber: FiberNode): { fileName: string; lineNumber: number } | null => {
70
+ return fiber._debugSource ?? null;
71
+ },
72
+
73
+ /**
74
+ * Get the display name of the component.
75
+ */
76
+ getComponentName: (fiber: FiberNode): string => {
77
+ if (typeof fiber.type === 'string') return fiber.type;
78
+ if (typeof fiber.type === 'function')
79
+ return fiber.type.displayName ?? fiber.type.name ?? 'Unknown';
80
+ return 'Unknown';
81
+ },
82
+
83
+ /**
84
+ * Measure a fiber's native view on screen.
85
+ * Returns a promise with { x, y, width, height }.
86
+ */
87
+ measure: (fiber: FiberNode): Promise<{ x: number; y: number; width: number; height: number }> => {
88
+ type MeasureCallback = (
89
+ x: number,
90
+ y: number,
91
+ width: number,
92
+ height: number,
93
+ pageX: number,
94
+ pageY: number,
95
+ ) => void;
96
+ type Measurable = { measure?: (cb: MeasureCallback) => void };
97
+
98
+ return new Promise((resolve, reject) => {
99
+ const stateNode = fiber.stateNode as
100
+ | (Measurable & {
101
+ canonical?: Measurable & { publicInstance?: Measurable };
102
+ })
103
+ | null;
104
+
105
+ // Old arch: stateNode.measure()
106
+ // Fabric: stateNode.canonical.publicInstance.measure()
107
+ const target = stateNode?.measure
108
+ ? stateNode
109
+ : (stateNode?.canonical?.publicInstance ?? stateNode?.canonical);
110
+
111
+ if (!target?.measure) {
112
+ reject(new Error('Fiber stateNode does not support measure()'));
113
+ return;
114
+ }
115
+
116
+ const timeoutId = setTimeout(() => {
117
+ reject(new Error('measure() timed out'));
118
+ }, MEASURE_TIMEOUT_MS);
119
+
120
+ target.measure((_x, _y, width, height, pageX, pageY) => {
121
+ clearTimeout(timeoutId);
122
+ resolve({ x: pageX, y: pageY, width, height });
123
+ });
124
+ });
125
+ },
126
+
127
+ /**
128
+ * Replace the style on a fiber using the devtools hook.
129
+ * Caller is responsible for building the final style object.
130
+ * Works on both old arch and Fabric.
131
+ */
132
+ setStyle: (fiber: FiberNode, style: unknown): boolean => {
133
+ const hook = (globalThis as Record<string, unknown>).__REACT_DEVTOOLS_GLOBAL_HOOK__ as
134
+ | {
135
+ renderers?: Map<
136
+ number,
137
+ { overrideProps?: (fiber: FiberNode, path: string[], value: unknown) => void }
138
+ >;
139
+ }
140
+ | undefined;
141
+
142
+ if (!hook?.renderers) return false;
143
+
144
+ for (const renderer of hook.renderers.values()) {
145
+ if (renderer?.overrideProps) {
146
+ renderer.overrideProps(fiber, ['style'], style);
147
+ return true;
148
+ }
149
+ }
150
+
151
+ return false;
152
+ },
153
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Detect the React version at runtime from the renderer.
3
+ * Used to load the correct fiber adapter if needed.
4
+ */
5
+ export const detectReactVersion = (): { major: number; minor: number } | null => {
6
+ const hook = (globalThis as Record<string, unknown>).__REACT_DEVTOOLS_GLOBAL_HOOK__ as
7
+ | { renderers?: Map<number, { version?: string }> }
8
+ | undefined;
9
+
10
+ if (!hook?.renderers) return null;
11
+
12
+ for (const renderer of hook.renderers.values()) {
13
+ if (!renderer?.version) continue;
14
+ const [major, minor] = renderer.version.split('.').map(Number);
15
+ return { major: major ?? 0, minor: minor ?? 0 };
16
+ }
17
+
18
+ return null;
19
+ };
@@ -0,0 +1,4 @@
1
+ export { detectReactVersion } from './detectVersion';
2
+ export { FiberAdapter } from './FiberAdapter';
3
+ export type { FiberNode, MeasuredElement } from './types';
4
+ export { HOST_COMPONENT_TAG } from './types';
@@ -0,0 +1,36 @@
1
+ /** React component function with optional display name. */
2
+ type ComponentFunction = ((...args: never[]) => unknown) & {
3
+ displayName?: string;
4
+ name?: string;
5
+ };
6
+
7
+ /** Minimal fiber node type — only the fields we access. */
8
+ export interface FiberNode {
9
+ tag: number;
10
+ type: string | ComponentFunction;
11
+ memoizedProps: Record<string, unknown>;
12
+ stateNode: unknown;
13
+ child: FiberNode | null;
14
+ sibling: FiberNode | null;
15
+ return: FiberNode | null;
16
+ _debugSource?: {
17
+ fileName: string;
18
+ lineNumber: number;
19
+ columnNumber?: number;
20
+ };
21
+ _debugOwner?: FiberNode;
22
+ }
23
+
24
+ /** Host component fiber tag — View, Text, Image, etc. */
25
+ export const HOST_COMPONENT_TAG = 5;
26
+
27
+ export interface MeasuredElement {
28
+ fiber: FiberNode;
29
+ x: number;
30
+ y: number;
31
+ width: number;
32
+ height: number;
33
+ depth: number;
34
+ zIndex: number;
35
+ componentName: string;
36
+ }
@@ -0,0 +1,102 @@
1
+ import { useState } from 'react';
2
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
3
+ import { MONOSPACE_FONT } from '../constants';
4
+ import { EditableValue } from './EditableValue';
5
+
6
+ interface AddPropertyRowProps {
7
+ onAdd: (key: string, value: unknown) => void;
8
+ onCancel: () => void;
9
+ }
10
+
11
+ /** Inline two-step row for adding a new style property: type key → type value → applied. */
12
+ export const AddPropertyRow = ({ onAdd, onCancel }: AddPropertyRowProps) => {
13
+ const [key, setKey] = useState('');
14
+ const [waitingForValue, setWaitingForValue] = useState(false);
15
+
16
+ if (!waitingForValue) {
17
+ return (
18
+ <View style={styles.row}>
19
+ <View style={styles.content}>
20
+ <EditableValue
21
+ value=''
22
+ displayValue=''
23
+ onSubmit={(newKey) => {
24
+ const trimmed = String(newKey).trim();
25
+ if (!trimmed) {
26
+ onCancel();
27
+ return;
28
+ }
29
+ setKey(trimmed);
30
+ setWaitingForValue(true);
31
+ }}
32
+ variant='key'
33
+ initialEditing
34
+ />
35
+ </View>
36
+ </View>
37
+ );
38
+ }
39
+
40
+ return (
41
+ <View style={styles.row}>
42
+ <View style={styles.content}>
43
+ <Text style={styles.pendingKey}>{key}:</Text>
44
+ <EditableValue
45
+ value=''
46
+ displayValue=''
47
+ onSubmit={(newValue) => {
48
+ const trimmed = String(newValue).trim();
49
+ if (!trimmed) {
50
+ onCancel();
51
+ return;
52
+ }
53
+ onAdd(key, newValue);
54
+ }}
55
+ variant='value'
56
+ initialEditing
57
+ />
58
+ </View>
59
+ </View>
60
+ );
61
+ };
62
+
63
+ interface AddPropertyButtonProps {
64
+ onPress: () => void;
65
+ }
66
+
67
+ /** Dim "+ add property" button shown at the bottom of the style list. */
68
+ export const AddPropertyButton = ({ onPress }: AddPropertyButtonProps) => (
69
+ <Pressable style={styles.button} onPress={onPress} hitSlop={6}>
70
+ <Text style={styles.buttonText}>+ add property</Text>
71
+ </Pressable>
72
+ );
73
+
74
+ const styles = StyleSheet.create({
75
+ row: {
76
+ flexDirection: 'row',
77
+ alignItems: 'center',
78
+ paddingVertical: 5,
79
+ paddingHorizontal: 8,
80
+ borderRadius: 4,
81
+ },
82
+ content: {
83
+ flex: 1,
84
+ flexDirection: 'row',
85
+ justifyContent: 'space-between',
86
+ alignItems: 'center',
87
+ },
88
+ pendingKey: {
89
+ color: '#9CDCFE',
90
+ fontSize: 13,
91
+ fontFamily: MONOSPACE_FONT,
92
+ },
93
+ button: {
94
+ paddingVertical: 6,
95
+ paddingHorizontal: 8,
96
+ },
97
+ buttonText: {
98
+ color: '#666',
99
+ fontSize: 12,
100
+ fontFamily: MONOSPACE_FONT,
101
+ },
102
+ });
@@ -0,0 +1,34 @@
1
+ import { StyleSheet, Text, TouchableOpacity } from 'react-native';
2
+
3
+ interface CloseButtonProps {
4
+ onPress: () => void;
5
+ }
6
+
7
+ /** Reusable red-tinted circular close button used across panel states. */
8
+ export const CloseButton = ({ onPress }: CloseButtonProps) => (
9
+ <TouchableOpacity
10
+ onPress={onPress}
11
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
12
+ style={styles.button}
13
+ activeOpacity={0.6}
14
+ >
15
+ <Text style={styles.text}>✕</Text>
16
+ </TouchableOpacity>
17
+ );
18
+
19
+ const styles = StyleSheet.create({
20
+ button: {
21
+ width: 22,
22
+ height: 22,
23
+ borderRadius: 11,
24
+ backgroundColor: 'rgba(255, 59, 48, 0.15)',
25
+ alignItems: 'center',
26
+ justifyContent: 'center',
27
+ },
28
+ text: {
29
+ color: '#FF3B30',
30
+ fontSize: 13,
31
+ fontWeight: '600',
32
+ lineHeight: 14,
33
+ },
34
+ });