react-native-network-lens 0.1.0 → 0.2.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 (57) hide show
  1. package/README.md +254 -97
  2. package/lib/module/components/BodyViewer.js +31 -19
  3. package/lib/module/components/BodyViewer.js.map +1 -1
  4. package/lib/module/components/JsonTree/JsonTreeView.js +10 -6
  5. package/lib/module/components/JsonTree/JsonTreeView.js.map +1 -1
  6. package/lib/module/components/JsonTree/model.js +2 -0
  7. package/lib/module/components/JsonTree/model.js.map +1 -1
  8. package/lib/module/components/NetworkLens.js +4 -8
  9. package/lib/module/components/NetworkLens.js.map +1 -1
  10. package/lib/module/components/NetworkLensOverlay.js +67 -22
  11. package/lib/module/components/NetworkLensOverlay.js.map +1 -1
  12. package/lib/module/components/RawTextView.js +10 -6
  13. package/lib/module/components/RawTextView.js.map +1 -1
  14. package/lib/module/components/RequestDetails/RequestDetails.js +44 -21
  15. package/lib/module/components/RequestDetails/RequestDetails.js.map +1 -1
  16. package/lib/module/components/RequestList/FilterBar.js +3 -3
  17. package/lib/module/components/RequestList/FilterBar.js.map +1 -1
  18. package/lib/module/components/RequestList/RequestList.js +7 -8
  19. package/lib/module/components/RequestList/RequestList.js.map +1 -1
  20. package/lib/module/components/ui/Button.js +21 -5
  21. package/lib/module/components/ui/Button.js.map +1 -1
  22. package/lib/module/components/ui/Pill.js +13 -6
  23. package/lib/module/components/ui/Pill.js.map +1 -1
  24. package/lib/module/settings.js +48 -0
  25. package/lib/module/settings.js.map +1 -0
  26. package/lib/module/theme.js +4 -4
  27. package/lib/typescript/src/components/BodyViewer.d.ts.map +1 -1
  28. package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts.map +1 -1
  29. package/lib/typescript/src/components/JsonTree/model.d.ts.map +1 -1
  30. package/lib/typescript/src/components/NetworkLens.d.ts +0 -3
  31. package/lib/typescript/src/components/NetworkLens.d.ts.map +1 -1
  32. package/lib/typescript/src/components/NetworkLensOverlay.d.ts +1 -1
  33. package/lib/typescript/src/components/NetworkLensOverlay.d.ts.map +1 -1
  34. package/lib/typescript/src/components/RawTextView.d.ts.map +1 -1
  35. package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts.map +1 -1
  36. package/lib/typescript/src/components/RequestList/RequestList.d.ts +1 -4
  37. package/lib/typescript/src/components/RequestList/RequestList.d.ts.map +1 -1
  38. package/lib/typescript/src/components/ui/Button.d.ts +3 -1
  39. package/lib/typescript/src/components/ui/Button.d.ts.map +1 -1
  40. package/lib/typescript/src/components/ui/Pill.d.ts +1 -0
  41. package/lib/typescript/src/components/ui/Pill.d.ts.map +1 -1
  42. package/lib/typescript/src/settings.d.ts +18 -0
  43. package/lib/typescript/src/settings.d.ts.map +1 -0
  44. package/package.json +3 -3
  45. package/src/components/BodyViewer.tsx +37 -19
  46. package/src/components/JsonTree/JsonTreeView.tsx +11 -5
  47. package/src/components/JsonTree/model.ts +2 -0
  48. package/src/components/NetworkLens.tsx +2 -11
  49. package/src/components/NetworkLensOverlay.tsx +54 -23
  50. package/src/components/RawTextView.tsx +11 -6
  51. package/src/components/RequestDetails/RequestDetails.tsx +31 -19
  52. package/src/components/RequestList/FilterBar.tsx +2 -2
  53. package/src/components/RequestList/RequestList.tsx +3 -7
  54. package/src/components/ui/Button.tsx +23 -7
  55. package/src/components/ui/Pill.tsx +14 -5
  56. package/src/settings.ts +55 -0
  57. package/src/theme.ts +2 -2
@@ -1,4 +1,3 @@
1
- import { type ReactNode } from 'react';
2
1
  import type NetworkRequest from '../../core/NetworkRequest.js';
3
2
  import { type ActionItem } from '../ui/index.js';
4
3
  interface Props {
@@ -10,9 +9,7 @@ interface Props {
10
9
  loggingEnabled: boolean;
11
10
  onPressItem: (id: string) => void;
12
11
  menuActions: ActionItem[];
13
- /** Optional row rendered above the search bar (used by the overlay for title + close). */
14
- header?: ReactNode;
15
12
  }
16
- declare const RequestList: ({ requests, sort, compact, maxRows, paused, loggingEnabled, onPressItem, menuActions, header, }: Props) => import("react").JSX.Element;
13
+ declare const RequestList: ({ requests, sort, compact, maxRows, paused, loggingEnabled, onPressItem, menuActions, }: Props) => import("react").JSX.Element;
17
14
  export default RequestList;
18
15
  //# sourceMappingURL=RequestList.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RequestList.d.ts","sourceRoot":"","sources":["../../../../../src/components/RequestList/RequestList.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvE,OAAO,KAAK,cAAc,MAAM,8BAA2B,CAAC;AAG5D,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,gBAAO,CAAC;AAYf,UAAU,KAAK;IACb,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,cAAc,EAAE,OAAO,CAAC;IACxB,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,0FAA0F;IAC1F,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAWD,QAAA,MAAM,WAAW,GAAI,iGAUlB,KAAK,gCAyJP,CAAC;AAiBF,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"RequestList.d.ts","sourceRoot":"","sources":["../../../../../src/components/RequestList/RequestList.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,cAAc,MAAM,8BAA2B,CAAC;AAG5D,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,gBAAO,CAAC;AAYf,UAAU,KAAK;IACb,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,cAAc,EAAE,OAAO,CAAC;IACxB,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAWD,QAAA,MAAM,WAAW,GAAI,yFASlB,KAAK,gCAwJP,CAAC;AAiBF,eAAe,WAAW,CAAC"}
@@ -4,10 +4,12 @@ interface TextButtonProps {
4
4
  onPress?: () => void;
5
5
  disabled?: boolean;
6
6
  active?: boolean;
7
+ /** Text-only button in the primary colour, for navigation actions like Back. */
8
+ plain?: boolean;
7
9
  style?: StyleProp<ViewStyle>;
8
10
  textStyle?: StyleProp<TextStyle>;
9
11
  }
10
- export declare const TextButton: ({ label, onPress, disabled, active, style, textStyle, }: TextButtonProps) => import("react").JSX.Element;
12
+ export declare const TextButton: ({ label, onPress, disabled, active, plain, style, textStyle, }: TextButtonProps) => import("react").JSX.Element;
11
13
  interface IconButtonProps {
12
14
  /** A unicode glyph; the library ships no image assets. */
13
15
  glyph: string;
@@ -1 +1 @@
1
- {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAGtB,UAAU,eAAe;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAClC;AAED,eAAO,MAAM,UAAU,GAAI,yDAOxB,eAAe,gCA8BjB,CAAC;AAEF,UAAU,eAAe;IACvB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,UAAU,GAAI,2DAMxB,eAAe,gCAyBjB,CAAC"}
1
+ {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAOtB,UAAU,eAAe;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gFAAgF;IAChF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAClC;AAED,eAAO,MAAM,UAAU,GAAI,gEAQxB,eAAe,gCAsCjB,CAAC;AAEF,UAAU,eAAe;IACvB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,UAAU,GAAI,2DAMxB,eAAe,gCAyBjB,CAAC"}
@@ -6,6 +6,7 @@ interface Props {
6
6
  small?: boolean;
7
7
  style?: StyleProp<ViewStyle>;
8
8
  }
9
+ /** Fixed-height badge so filled and outlined variants align on one row. */
9
10
  export declare const Pill: ({ label, color, filled, small, style }: Props) => import("react").JSX.Element;
10
11
  export {};
11
12
  //# sourceMappingURL=Pill.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Pill.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/Pill.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAEtB,UAAU,KAAK;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,eAAO,MAAM,IAAI,GAAI,wCAAwC,KAAK,gCAqBjE,CAAC"}
1
+ {"version":3,"file":"Pill.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/Pill.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAEtB,UAAU,KAAK;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,2EAA2E;AAC3E,eAAO,MAAM,IAAI,GAAI,wCAAwC,KAAK,gCAqBjE,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { Theme } from './theme.js';
2
+ /**
3
+ * Tiny in-memory preferences shared by every viewer in the session, e.g. the
4
+ * code font scale the user picks with the "Aa" button.
5
+ */
6
+ export type CodeFontScale = 'small' | 'medium' | 'large';
7
+ export declare const setCodeFontScale: (next: CodeFontScale) => void;
8
+ export declare const cycleCodeFontScale: () => void;
9
+ export declare const useCodeFontScale: () => CodeFontScale;
10
+ /** Font metrics for code views: the theme's base size times the chosen scale. */
11
+ export declare const useCodeFont: (theme: Theme) => {
12
+ scale: CodeFontScale;
13
+ fontSize: number;
14
+ lineHeight: number;
15
+ /** Average glyph advance for monospace fonts; used to size horizontal scroll areas. */
16
+ charWidth: number;
17
+ };
18
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/settings.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAS,CAAC;AAErC;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAoBzD,eAAO,MAAM,gBAAgB,GAAI,MAAM,aAAa,SAInD,CAAC;AAEF,eAAO,MAAM,kBAAkB,YAI9B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAO,aACiB,CAAC;AAEtD,iFAAiF;AACjF,eAAO,MAAM,WAAW,GAAI,OAAO,KAAK;;;;IAQpC,uFAAuF;;CAG1F,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-network-lens",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "In-app HTTP network inspector for React Native with a tabbed request viewer, collapsible JSON tree, search and zero dependencies",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -88,8 +88,8 @@
88
88
  "typescript": "^6.0.3"
89
89
  },
90
90
  "peerDependencies": {
91
- "react": ">=19.0.0",
92
- "react-native": ">=0.83.0"
91
+ "react": ">=18.0.0",
92
+ "react-native": ">=0.69.0"
93
93
  },
94
94
  "workspaces": [
95
95
  "example"
@@ -6,12 +6,13 @@ import { bodySize, stringifyJson } from '../utils/body';
6
6
  import { formatBytes } from '../utils/format';
7
7
  import JsonTreeView, { type JsonTreeHandle } from './JsonTree/JsonTreeView';
8
8
  import RawTextView from './RawTextView';
9
+ import { cycleCodeFontScale, useCodeFontScale } from '../settings';
9
10
  import {
10
11
  ActionSheet,
11
12
  EmptyState,
13
+ IconButton,
12
14
  SearchInput,
13
15
  SegmentedControl,
14
- TextButton,
15
16
  } from './ui';
16
17
 
17
18
  type ViewMode = 'tree' | 'raw';
@@ -60,6 +61,8 @@ const BodyViewer = ({
60
61
  value: unknown;
61
62
  } | null>(null);
62
63
  const treeRef = useRef<JsonTreeHandle>(null);
64
+ const [menuVisible, setMenuVisible] = useState(false);
65
+ const fontScale = useCodeFontScale();
63
66
 
64
67
  const effectiveMode: ViewMode = structured ? mode : 'raw';
65
68
 
@@ -122,22 +125,16 @@ const BodyViewer = ({
122
125
  </Text>
123
126
  )}
124
127
  <View style={styles.spacer} />
125
- {effectiveMode === 'tree' && (
126
- <>
127
- <TextButton
128
- label="Expand"
129
- onPress={() => treeRef.current?.expandAll()}
130
- />
131
- <TextButton
132
- label="Collapse"
133
- onPress={() => treeRef.current?.collapseAll()}
134
- />
135
- </>
136
- )}
137
- {copyToClipboard && body.kind !== 'binary' && (
138
- <TextButton label="Copy" onPress={() => copyToClipboard(body.text)} />
139
- )}
140
- <TextButton label="Share" onPress={() => share(body.text)} />
128
+ <IconButton
129
+ glyph="Aa"
130
+ accessibilityLabel={`Text size: ${fontScale}. Tap to change`}
131
+ onPress={cycleCodeFontScale}
132
+ />
133
+ <IconButton
134
+ glyph="⋮"
135
+ accessibilityLabel="More options"
136
+ onPress={() => setMenuVisible(true)}
137
+ />
141
138
  </View>
142
139
  {body.kind !== 'binary' && (
143
140
  <SearchInput
@@ -183,6 +180,28 @@ const BodyViewer = ({
183
180
  />
184
181
  )}
185
182
  </View>
183
+ <ActionSheet
184
+ visible={menuVisible}
185
+ onClose={() => setMenuVisible(false)}
186
+ actions={[
187
+ ...(effectiveMode === 'tree'
188
+ ? [
189
+ {
190
+ label: 'Expand all',
191
+ onPress: () => treeRef.current?.expandAll(),
192
+ },
193
+ {
194
+ label: 'Collapse all',
195
+ onPress: () => treeRef.current?.collapseAll(),
196
+ },
197
+ ]
198
+ : []),
199
+ ...(copyToClipboard && body.kind !== 'binary'
200
+ ? [{ label: 'Copy', onPress: () => copyToClipboard(body.text) }]
201
+ : []),
202
+ { label: 'Share', onPress: () => share(body.text) },
203
+ ]}
204
+ />
186
205
  <ActionSheet
187
206
  visible={nodeMenu !== null}
188
207
  onClose={() => setNodeMenu(null)}
@@ -227,9 +246,8 @@ const styles = StyleSheet.create({
227
246
  alignItems: 'center',
228
247
  paddingHorizontal: 12,
229
248
  paddingTop: 10,
230
- gap: 6,
231
249
  },
232
- modeSwitch: { width: 130 },
250
+ modeSwitch: { width: 140 },
233
251
  spacer: { flex: 1 },
234
252
  search: { marginHorizontal: 12, marginTop: 8 },
235
253
  meta: { fontSize: 12, flexShrink: 1 },
@@ -8,6 +8,7 @@ import {
8
8
  useState,
9
9
  } from 'react';
10
10
  import { Pressable, StyleSheet, Text } from 'react-native';
11
+ import { useCodeFont } from '../../settings';
11
12
  import { useTheme, type Theme } from '../../theme';
12
13
  import { splitHighlights } from '../../utils/search';
13
14
  import { CodeList } from '../CodeList';
@@ -58,6 +59,7 @@ const JsonTreeView = forwardRef<JsonTreeHandle, Props>(
58
59
  ref
59
60
  ) {
60
61
  const theme = useTheme();
62
+ const font = useCodeFont(theme);
61
63
  const [expanded, setExpanded] = useState<Set<string>>(() =>
62
64
  pathsToDepth(value, initialDepth)
63
65
  );
@@ -118,8 +120,8 @@ const JsonTreeView = forwardRef<JsonTreeHandle, Props>(
118
120
  let maxChars = 0;
119
121
  for (const row of rows)
120
122
  maxChars = Math.max(maxChars, rowCharCount(row, INDENT_CHARS));
121
- return maxChars * theme.sizes.codeFontSize * 0.62 + 40;
122
- }, [rows, theme.sizes.codeFontSize]);
123
+ return maxChars * font.charWidth + 40;
124
+ }, [rows, font.charWidth]);
123
125
 
124
126
  const toggle = useCallback((row: JsonRow) => {
125
127
  setExpanded((prev) => {
@@ -135,13 +137,14 @@ const JsonTreeView = forwardRef<JsonTreeHandle, Props>(
135
137
  });
136
138
  }, []);
137
139
 
138
- const rowHeight = theme.sizes.codeLineHeight + 4;
140
+ const rowHeight = font.lineHeight + 4;
139
141
 
140
142
  const renderItem = useCallback(
141
143
  ({ item }: { item: JsonRow }) => (
142
144
  <TreeRow
143
145
  row={item}
144
146
  theme={theme}
147
+ font={font}
145
148
  query={query}
146
149
  matched={matchedPaths.has(item.path)}
147
150
  active={item.path === activePath}
@@ -152,6 +155,7 @@ const JsonTreeView = forwardRef<JsonTreeHandle, Props>(
152
155
  ),
153
156
  [
154
157
  theme,
158
+ font,
155
159
  query,
156
160
  matchedPaths,
157
161
  activePath,
@@ -180,6 +184,7 @@ const keyExtractor = (row: JsonRow) => row.path;
180
184
  const TreeRow = memo(function TreeRowInner({
181
185
  row,
182
186
  theme,
187
+ font,
183
188
  query,
184
189
  matched,
185
190
  active,
@@ -189,6 +194,7 @@ const TreeRow = memo(function TreeRowInner({
189
194
  }: {
190
195
  row: JsonRow;
191
196
  theme: Theme;
197
+ font: ReturnType<typeof useCodeFont>;
192
198
  query: string;
193
199
  matched: boolean;
194
200
  active: boolean;
@@ -198,8 +204,8 @@ const TreeRow = memo(function TreeRowInner({
198
204
  }) {
199
205
  const codeStyle = {
200
206
  fontFamily: theme.fonts.mono,
201
- fontSize: theme.sizes.codeFontSize,
202
- lineHeight: theme.sizes.codeLineHeight,
207
+ fontSize: font.fontSize,
208
+ lineHeight: font.lineHeight,
203
209
  };
204
210
  const valueColor = (() => {
205
211
  switch (row.type) {
@@ -260,5 +260,7 @@ export const MAX_VALUE_CHARS = 400;
260
260
 
261
261
  export const displayValue = (value: unknown): string => {
262
262
  if (typeof value === 'string') return `"${value}"`;
263
+ if (Array.isArray(value)) return '[]'; // only empty arrays reach here
264
+ if (value && typeof value === 'object') return '{}'; // only empty objects reach here
263
265
  return primitiveText(value);
264
266
  };
@@ -5,7 +5,6 @@ import {
5
5
  useImperativeHandle,
6
6
  useMemo,
7
7
  useState,
8
- type ReactNode,
9
8
  } from 'react';
10
9
  import { BackHandler, Share, StyleSheet, View } from 'react-native';
11
10
  import { setLensBackHandler } from '../backHandler';
@@ -40,8 +39,6 @@ export interface NetworkLensProps {
40
39
  * `copyToClipboard={Clipboard.setString}`.
41
40
  */
42
41
  copyToClipboard?: (text: string) => void;
43
- /** Rendered above the request list's search bar (the overlay uses it for its title bar). */
44
- header?: ReactNode;
45
42
  }
46
43
 
47
44
  export interface NetworkLensHandle {
@@ -58,7 +55,6 @@ const NetworkLens = forwardRef<NetworkLensHandle, NetworkLensProps>(
58
55
  compact = false,
59
56
  maxRows,
60
57
  copyToClipboard,
61
- header,
62
58
  },
63
59
  ref
64
60
  ) {
@@ -71,7 +67,6 @@ const NetworkLens = forwardRef<NetworkLensHandle, NetworkLensProps>(
71
67
  compact={compact}
72
68
  maxRows={maxRows}
73
69
  copyToClipboard={copyToClipboard}
74
- header={header}
75
70
  />
76
71
  </ThemeContext.Provider>
77
72
  );
@@ -79,13 +74,10 @@ const NetworkLens = forwardRef<NetworkLensHandle, NetworkLensProps>(
79
74
  );
80
75
 
81
76
  type InspectorProps = Required<Pick<NetworkLensProps, 'sort' | 'compact'>> &
82
- Pick<NetworkLensProps, 'maxRows' | 'copyToClipboard' | 'header'>;
77
+ Pick<NetworkLensProps, 'maxRows' | 'copyToClipboard'>;
83
78
 
84
79
  const Inspector = forwardRef<NetworkLensHandle, InspectorProps>(
85
- function InspectorInner(
86
- { sort, compact, maxRows, copyToClipboard, header },
87
- ref
88
- ) {
80
+ function InspectorInner({ sort, compact, maxRows, copyToClipboard }, ref) {
89
81
  const theme = useTheme();
90
82
  const requests = useNetworkRequests();
91
83
  const loggingEnabled = useLoggerEnabled();
@@ -163,7 +155,6 @@ const Inspector = forwardRef<NetworkLensHandle, InspectorProps>(
163
155
  loggingEnabled={loggingEnabled}
164
156
  onPressItem={setSelectedId}
165
157
  menuActions={menuActions}
166
- header={header}
167
158
  />
168
159
  )}
169
160
  </View>
@@ -11,15 +11,16 @@ import {
11
11
  } from 'react-native';
12
12
  import { useLoggerEnabled, useNetworkRequests } from '../hooks';
13
13
  import { ThemeContext, useTheme, type ThemeInput } from '../theme';
14
+
15
+ const HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 };
14
16
  import NetworkLens, {
15
17
  type NetworkLensHandle,
16
18
  type NetworkLensProps,
17
19
  } from './NetworkLens';
18
- import { IconButton } from './ui';
19
20
 
20
21
  export interface NetworkLensOverlayProps extends Omit<
21
22
  NetworkLensProps,
22
- 'enabled' | 'theme' | 'header'
23
+ 'enabled' | 'theme'
23
24
  > {
24
25
  /**
25
26
  * Render nothing when `false`, e.g.
@@ -88,17 +89,6 @@ const Overlay = ({
88
89
  [position.endsWith('right') ? 'right' : 'left']: offset?.x ?? 16,
89
90
  };
90
91
 
91
- const header = (
92
- <View style={styles.header}>
93
- <Text style={[styles.title, { color: t.colors.text }]}>{title}</Text>
94
- <IconButton
95
- glyph="✕"
96
- accessibilityLabel="Close network inspector"
97
- onPress={() => setOpen(false)}
98
- />
99
- </View>
100
- );
101
-
102
92
  return (
103
93
  <>
104
94
  {!hideButton && (
@@ -135,12 +125,43 @@ const Overlay = ({
135
125
  }}
136
126
  >
137
127
  <View style={[styles.sheet, { backgroundColor: t.colors.surface }]}>
138
- <NetworkLens
139
- ref={lensRef}
140
- {...lensProps}
141
- theme={theme}
142
- header={header}
143
- />
128
+ <View
129
+ style={[
130
+ styles.header,
131
+ {
132
+ backgroundColor: t.colors.surface,
133
+ borderBottomColor: t.colors.border,
134
+ },
135
+ ]}
136
+ >
137
+ <View style={styles.headerSide} />
138
+ <Text
139
+ style={[styles.title, { color: t.colors.text }]}
140
+ numberOfLines={1}
141
+ >
142
+ {title}
143
+ </Text>
144
+ <View style={styles.headerSide}>
145
+ <Pressable
146
+ accessibilityRole="button"
147
+ accessibilityLabel="Close network inspector"
148
+ onPress={() => setOpen(false)}
149
+ hitSlop={HIT_SLOP}
150
+ style={({ pressed }) => [
151
+ styles.close,
152
+ { backgroundColor: t.colors.surfaceAlt },
153
+ pressed && styles.pressed,
154
+ ]}
155
+ >
156
+ <Text
157
+ style={[styles.closeGlyph, { color: t.colors.textMuted }]}
158
+ >
159
+ ✕
160
+ </Text>
161
+ </Pressable>
162
+ </View>
163
+ </View>
164
+ <NetworkLens ref={lensRef} {...lensProps} theme={theme} />
144
165
  </View>
145
166
  </Modal>
146
167
  </>
@@ -175,13 +196,23 @@ const styles = StyleSheet.create({
175
196
  paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0,
176
197
  },
177
198
  header: {
199
+ height: 52,
178
200
  flexDirection: 'row',
179
201
  alignItems: 'center',
180
- justifyContent: 'space-between',
181
- paddingLeft: 8,
182
- paddingBottom: 6,
202
+ paddingHorizontal: 12,
203
+ borderBottomWidth: StyleSheet.hairlineWidth,
204
+ },
205
+ headerSide: { width: 44, alignItems: 'flex-end', justifyContent: 'center' },
206
+ title: { flex: 1, textAlign: 'center', fontSize: 17, fontWeight: '600' },
207
+ close: {
208
+ width: 30,
209
+ height: 30,
210
+ borderRadius: 15,
211
+ alignItems: 'center',
212
+ justifyContent: 'center',
183
213
  },
184
- title: { fontSize: 17, fontWeight: '700' },
214
+ closeGlyph: { fontSize: 15, fontWeight: '700' },
215
+ pressed: { opacity: 0.6 },
185
216
  });
186
217
 
187
218
  export default NetworkLensOverlay;
@@ -1,5 +1,6 @@
1
1
  import React, { useCallback, useEffect, useMemo } from 'react';
2
2
  import { StyleSheet, Text, View } from 'react-native';
3
+ import { useCodeFont } from '../settings';
3
4
  import { useTheme, type Theme } from '../theme';
4
5
  import { findLineMatches, splitHighlights, toLines } from '../utils/search';
5
6
  import { CodeList } from './CodeList';
@@ -22,6 +23,7 @@ const RawTextView = ({
22
23
  onMatchCountChange,
23
24
  }: Props) => {
24
25
  const theme = useTheme();
26
+ const font = useCodeFont(theme);
25
27
  const lines = useMemo(() => toLines(text), [text]);
26
28
  const matches = useMemo(() => findLineMatches(lines, query), [lines, query]);
27
29
 
@@ -37,14 +39,14 @@ const RawTextView = ({
37
39
  matches[Math.min(activeMatch, Math.max(matches.length - 1, 0))]?.line;
38
40
 
39
41
  const gutterChars = String(lines.length).length;
40
- const charWidth = theme.sizes.codeFontSize * 0.62;
42
+ const charWidth = font.charWidth;
41
43
  const contentWidth = useMemo(() => {
42
44
  let max = 0;
43
45
  for (const line of lines) if (line.length > max) max = line.length;
44
46
  return (max + gutterChars + 3) * charWidth + 32;
45
47
  }, [lines, gutterChars, charWidth]);
46
48
 
47
- const rowHeight = theme.sizes.codeLineHeight;
49
+ const rowHeight = font.lineHeight;
48
50
 
49
51
  const renderItem = useCallback(
50
52
  ({ item, index }: { item: string; index: number }) => (
@@ -53,13 +55,14 @@ const RawTextView = ({
53
55
  index={index}
54
56
  gutterChars={gutterChars}
55
57
  theme={theme}
58
+ font={font}
56
59
  query={query}
57
60
  matched={matchedLines.has(index)}
58
61
  active={index === activeLine}
59
62
  height={rowHeight}
60
63
  />
61
64
  ),
62
- [gutterChars, theme, query, matchedLines, activeLine, rowHeight]
65
+ [gutterChars, theme, font, query, matchedLines, activeLine, rowHeight]
63
66
  );
64
67
 
65
68
  return (
@@ -82,6 +85,7 @@ const Line = React.memo(function LineInner({
82
85
  index,
83
86
  gutterChars,
84
87
  theme,
88
+ font,
85
89
  query,
86
90
  matched,
87
91
  active,
@@ -91,6 +95,7 @@ const Line = React.memo(function LineInner({
91
95
  index: number;
92
96
  gutterChars: number;
93
97
  theme: Theme;
98
+ font: ReturnType<typeof useCodeFont>;
94
99
  query: string;
95
100
  matched: boolean;
96
101
  active: boolean;
@@ -98,8 +103,8 @@ const Line = React.memo(function LineInner({
98
103
  }) {
99
104
  const codeStyle = {
100
105
  fontFamily: theme.fonts.mono,
101
- fontSize: theme.sizes.codeFontSize,
102
- lineHeight: theme.sizes.codeLineHeight,
106
+ fontSize: font.fontSize,
107
+ lineHeight: font.lineHeight,
103
108
  };
104
109
  const segments = matched ? splitHighlights(text, query) : null;
105
110
  return (
@@ -120,7 +125,7 @@ const Line = React.memo(function LineInner({
120
125
  styles.gutter,
121
126
  {
122
127
  color: theme.colors.textMuted,
123
- width: (gutterChars + 1) * theme.sizes.codeFontSize * 0.62 + 8,
128
+ width: (gutterChars + 1) * font.charWidth + 8,
124
129
  },
125
130
  ]}
126
131
  >
@@ -18,7 +18,6 @@ import {
18
18
  } from '../../utils/format';
19
19
  import BodyViewer from '../BodyViewer';
20
20
  import {
21
- IconButton,
22
21
  KeyValueList,
23
22
  Pill,
24
23
  Section,
@@ -76,27 +75,32 @@ const RequestDetails = ({ request, onClose, copyToClipboard }: Props) => {
76
75
  },
77
76
  ]}
78
77
  >
79
- <View style={styles.headerRow}>
78
+ <View style={styles.navRow}>
79
+ <TextButton
80
+ label="‹ Back"
81
+ onPress={onClose}
82
+ plain
83
+ style={styles.back}
84
+ textStyle={styles.backLabel}
85
+ />
86
+ <Text
87
+ style={[styles.summary, { color: theme.colors.textMuted }]}
88
+ numberOfLines={1}
89
+ >
90
+ {summary}
91
+ </Text>
92
+ </View>
93
+ <View style={styles.pillRow}>
80
94
  <Pill
81
95
  label={request.method}
82
96
  color={methodColor(theme, request.method)}
83
97
  filled
98
+ style={styles.headerPill}
84
99
  />
85
100
  <Pill
86
101
  label={statusLabel(request)}
87
102
  color={statusColor(theme, request.status, failed)}
88
103
  />
89
- <Text
90
- style={[styles.summary, { color: theme.colors.textMuted }]}
91
- numberOfLines={1}
92
- >
93
- {summary}
94
- </Text>
95
- <IconButton
96
- glyph="✕"
97
- accessibilityLabel="Back to list"
98
- onPress={onClose}
99
- />
100
104
  </View>
101
105
  <Pressable
102
106
  onPress={() => setUrlExpanded((v) => !v)}
@@ -292,21 +296,29 @@ const styles = StyleSheet.create({
292
296
  paddingVertical: 10,
293
297
  borderBottomWidth: StyleSheet.hairlineWidth,
294
298
  },
295
- headerRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
296
- summary: { flex: 1, fontSize: 12, fontVariant: ['tabular-nums'] },
299
+ navRow: {
300
+ flexDirection: 'row',
301
+ alignItems: 'center',
302
+ justifyContent: 'space-between',
303
+ marginLeft: -8,
304
+ },
305
+ back: { paddingHorizontal: 8, paddingVertical: 4 },
306
+ backLabel: { fontSize: 16 },
307
+ pillRow: { flexDirection: 'row', alignItems: 'center', marginTop: 6 },
308
+ headerPill: { marginRight: 8 },
309
+ summary: { flexShrink: 1, fontSize: 12, fontVariant: ['tabular-nums'] },
297
310
  url: { fontSize: 14, marginTop: 8, lineHeight: 19 },
298
311
  gql: { fontSize: 12, marginTop: 4, fontWeight: '600' },
299
312
  tabs: { marginTop: 10 },
300
313
  overview: { paddingBottom: 24 },
301
- sectionActions: { flexDirection: 'row', gap: 6 },
302
- smallButton: { paddingVertical: 3, paddingHorizontal: 8 },
314
+ sectionActions: { flexDirection: 'row' },
315
+ smallButton: { paddingVertical: 3, paddingHorizontal: 8, marginLeft: 6 },
303
316
  exportRow: {
304
317
  flexDirection: 'row',
305
318
  flexWrap: 'wrap',
306
- gap: 8,
307
319
  paddingVertical: 8,
308
320
  },
309
- exportButton: { paddingVertical: 8 },
321
+ exportButton: { paddingVertical: 8, marginRight: 8, marginBottom: 8 },
310
322
  footer: { textAlign: 'center', fontSize: 11, marginTop: 20 },
311
323
  });
312
324
 
@@ -155,8 +155,8 @@ const styles = StyleSheet.create({
155
155
  paddingLeft: 12,
156
156
  letterSpacing: 0.5,
157
157
  },
158
- chips: { paddingRight: 12, gap: 6 },
159
- chip: { paddingVertical: 4, paddingHorizontal: 10 },
158
+ chips: { paddingRight: 12 },
159
+ chip: { paddingVertical: 4, paddingHorizontal: 10, marginRight: 6 },
160
160
  clear: {
161
161
  alignSelf: 'flex-end',
162
162
  marginRight: 12,