rn-backstage 1.2.0 → 1.2.1

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 (100) hide show
  1. package/README.md +2 -0
  2. package/lib/commonjs/Backstage.js +40 -33
  3. package/lib/commonjs/Backstage.js.map +1 -1
  4. package/lib/commonjs/ThemeContext.js +43 -0
  5. package/lib/commonjs/ThemeContext.js.map +1 -0
  6. package/lib/commonjs/components/BackstagePanel.js +17 -10
  7. package/lib/commonjs/components/BackstagePanel.js.map +1 -1
  8. package/lib/commonjs/components/FlagsTab.js +50 -49
  9. package/lib/commonjs/components/FlagsTab.js.map +1 -1
  10. package/lib/commonjs/components/FloatingPill.js +4 -2
  11. package/lib/commonjs/components/FloatingPill.js.map +1 -1
  12. package/lib/commonjs/components/InfoTab.js +73 -107
  13. package/lib/commonjs/components/InfoTab.js.map +1 -1
  14. package/lib/commonjs/components/JsonTreeView.js +39 -34
  15. package/lib/commonjs/components/JsonTreeView.js.map +1 -1
  16. package/lib/commonjs/components/LogItem.js +33 -29
  17. package/lib/commonjs/components/LogItem.js.map +1 -1
  18. package/lib/commonjs/components/LogsTab.js +37 -42
  19. package/lib/commonjs/components/LogsTab.js.map +1 -1
  20. package/lib/commonjs/components/NetworkItem.js +111 -135
  21. package/lib/commonjs/components/NetworkItem.js.map +1 -1
  22. package/lib/commonjs/components/NetworkTab.js +48 -49
  23. package/lib/commonjs/components/NetworkTab.js.map +1 -1
  24. package/lib/commonjs/components/TabBar.js +8 -5
  25. package/lib/commonjs/components/TabBar.js.map +1 -1
  26. package/lib/commonjs/constants.js +24 -1
  27. package/lib/commonjs/constants.js.map +1 -1
  28. package/lib/commonjs/index.js +19 -0
  29. package/lib/commonjs/index.js.map +1 -1
  30. package/lib/commonjs/types.js.map +1 -1
  31. package/lib/module/Backstage.js +40 -33
  32. package/lib/module/Backstage.js.map +1 -1
  33. package/lib/module/ThemeContext.js +37 -0
  34. package/lib/module/ThemeContext.js.map +1 -0
  35. package/lib/module/components/BackstagePanel.js +19 -12
  36. package/lib/module/components/BackstagePanel.js.map +1 -1
  37. package/lib/module/components/FlagsTab.js +51 -50
  38. package/lib/module/components/FlagsTab.js.map +1 -1
  39. package/lib/module/components/FloatingPill.js +5 -3
  40. package/lib/module/components/FloatingPill.js.map +1 -1
  41. package/lib/module/components/InfoTab.js +73 -107
  42. package/lib/module/components/InfoTab.js.map +1 -1
  43. package/lib/module/components/JsonTreeView.js +42 -38
  44. package/lib/module/components/JsonTreeView.js.map +1 -1
  45. package/lib/module/components/LogItem.js +35 -31
  46. package/lib/module/components/LogItem.js.map +1 -1
  47. package/lib/module/components/LogsTab.js +38 -43
  48. package/lib/module/components/LogsTab.js.map +1 -1
  49. package/lib/module/components/NetworkItem.js +113 -137
  50. package/lib/module/components/NetworkItem.js.map +1 -1
  51. package/lib/module/components/NetworkTab.js +49 -50
  52. package/lib/module/components/NetworkTab.js.map +1 -1
  53. package/lib/module/components/TabBar.js +10 -7
  54. package/lib/module/components/TabBar.js.map +1 -1
  55. package/lib/module/constants.js +23 -0
  56. package/lib/module/constants.js.map +1 -1
  57. package/lib/module/index.js +1 -0
  58. package/lib/module/index.js.map +1 -1
  59. package/lib/module/types.js.map +1 -1
  60. package/lib/typescript/Backstage.d.ts.map +1 -1
  61. package/lib/typescript/ThemeContext.d.ts +11 -0
  62. package/lib/typescript/ThemeContext.d.ts.map +1 -0
  63. package/lib/typescript/components/BackstagePanel.d.ts +1 -0
  64. package/lib/typescript/components/BackstagePanel.d.ts.map +1 -1
  65. package/lib/typescript/components/FlagsTab.d.ts.map +1 -1
  66. package/lib/typescript/components/FloatingPill.d.ts.map +1 -1
  67. package/lib/typescript/components/InfoTab.d.ts +1 -0
  68. package/lib/typescript/components/InfoTab.d.ts.map +1 -1
  69. package/lib/typescript/components/JsonTreeView.d.ts.map +1 -1
  70. package/lib/typescript/components/LogItem.d.ts +1 -0
  71. package/lib/typescript/components/LogItem.d.ts.map +1 -1
  72. package/lib/typescript/components/LogsTab.d.ts +1 -0
  73. package/lib/typescript/components/LogsTab.d.ts.map +1 -1
  74. package/lib/typescript/components/NetworkItem.d.ts +1 -0
  75. package/lib/typescript/components/NetworkItem.d.ts.map +1 -1
  76. package/lib/typescript/components/NetworkTab.d.ts +1 -0
  77. package/lib/typescript/components/NetworkTab.d.ts.map +1 -1
  78. package/lib/typescript/components/TabBar.d.ts.map +1 -1
  79. package/lib/typescript/constants.d.ts +1 -0
  80. package/lib/typescript/constants.d.ts.map +1 -1
  81. package/lib/typescript/index.d.ts +2 -0
  82. package/lib/typescript/index.d.ts.map +1 -1
  83. package/lib/typescript/types.d.ts +4 -0
  84. package/lib/typescript/types.d.ts.map +1 -1
  85. package/package.json +1 -1
  86. package/src/Backstage.tsx +39 -33
  87. package/src/ThemeContext.tsx +38 -0
  88. package/src/components/BackstagePanel.tsx +66 -57
  89. package/src/components/FlagsTab.tsx +123 -174
  90. package/src/components/FloatingPill.tsx +6 -3
  91. package/src/components/InfoTab.tsx +152 -186
  92. package/src/components/JsonTreeView.tsx +53 -73
  93. package/src/components/LogItem.tsx +55 -100
  94. package/src/components/LogsTab.tsx +110 -132
  95. package/src/components/NetworkItem.tsx +306 -417
  96. package/src/components/NetworkTab.tsx +129 -163
  97. package/src/components/TabBar.tsx +51 -47
  98. package/src/constants.ts +23 -0
  99. package/src/index.ts +2 -0
  100. package/src/types.ts +6 -0
@@ -1,8 +1,9 @@
1
- import React, { useCallback, useState } from 'react'
1
+ import React, { useCallback, useMemo, useState } from 'react'
2
2
  import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'
3
- import { DarkTheme, MonospaceFont } from '../constants'
3
+ import { MonospaceFont } from '../constants'
4
+ import { useBackstageTheme } from '../ThemeContext'
4
5
  import { NetworkState } from '../types'
5
- import type { NetworkEntry } from '../types'
6
+ import type { NetworkEntry, BackstageTheme } from '../types'
6
7
  import { formatTimestamp } from '../utils/formatTimestamp'
7
8
  import { JsonTreeView } from './JsonTreeView'
8
9
  import { toCurl } from '../network-interceptor'
@@ -12,32 +13,20 @@ import { toCurl } from '../network-interceptor'
12
13
  interface NetworkItemProps {
13
14
  item: NetworkEntry
14
15
  onCopy?: (text: string) => void
16
+ jsonMaxDepth?: number
15
17
  }
16
18
 
17
19
  // ─── Status Badge Config ─────────────────────────────────────────────────────
18
20
 
19
- function getStatusConfig(entry: NetworkEntry): {
20
- label: string
21
- color: string
22
- bgColor: string
23
- rowBg: string
24
- } {
21
+ function getStatusConfig(
22
+ entry: NetworkEntry,
23
+ t: BackstageTheme,
24
+ ): { label: string; color: string; bgColor: string; rowBg: string } {
25
25
  if (entry.state === NetworkState.pending) {
26
- return {
27
- label: '…',
28
- color: DarkTheme.warning,
29
- bgColor: DarkTheme.warningDim,
30
- rowBg: 'transparent',
31
- }
26
+ return { label: '…', color: t.warning, bgColor: t.warningDim, rowBg: 'transparent' }
32
27
  }
33
-
34
28
  if (entry.state === NetworkState.error) {
35
- return {
36
- label: 'ERR',
37
- color: DarkTheme.error,
38
- bgColor: DarkTheme.errorDim,
39
- rowBg: 'rgba(255, 77, 106, 0.06)',
40
- }
29
+ return { label: 'ERR', color: t.error, bgColor: t.errorDim, rowBg: 'rgba(255, 77, 106, 0.06)' }
41
30
  }
42
31
 
43
32
  const status = entry.status ?? 0
@@ -45,54 +34,46 @@ function getStatusConfig(entry: NetworkEntry): {
45
34
  if (status >= 200 && status < 300) {
46
35
  return {
47
36
  label: String(status),
48
- color: DarkTheme.success,
37
+ color: t.success,
49
38
  bgColor: 'rgba(52, 211, 153, 0.12)',
50
39
  rowBg: 'transparent',
51
40
  }
52
41
  }
53
-
54
42
  if (status >= 300 && status < 400) {
55
- return {
56
- label: String(status),
57
- color: DarkTheme.info,
58
- bgColor: DarkTheme.infoDim,
59
- rowBg: 'transparent',
60
- }
43
+ return { label: String(status), color: t.info, bgColor: t.infoDim, rowBg: 'transparent' }
61
44
  }
62
-
63
45
  if (status >= 400 && status < 500) {
64
46
  return {
65
47
  label: String(status),
66
- color: DarkTheme.warning,
67
- bgColor: DarkTheme.warningDim,
48
+ color: t.warning,
49
+ bgColor: t.warningDim,
68
50
  rowBg: 'rgba(255, 178, 36, 0.06)',
69
51
  }
70
52
  }
71
-
72
53
  // 5xx
73
54
  return {
74
55
  label: String(status),
75
- color: DarkTheme.error,
76
- bgColor: DarkTheme.errorDim,
56
+ color: t.error,
57
+ bgColor: t.errorDim,
77
58
  rowBg: 'rgba(255, 77, 106, 0.06)',
78
59
  }
79
60
  }
80
61
 
81
62
  // ─── Method Badge Colors ─────────────────────────────────────────────────────
82
63
 
83
- function getMethodColor(method: string): string {
64
+ function getMethodColor(method: string, t: BackstageTheme): string {
84
65
  switch (method) {
85
66
  case 'GET':
86
- return DarkTheme.success
67
+ return t.success
87
68
  case 'POST':
88
- return DarkTheme.info
69
+ return t.info
89
70
  case 'PUT':
90
71
  case 'PATCH':
91
- return DarkTheme.warning
72
+ return t.warning
92
73
  case 'DELETE':
93
- return DarkTheme.error
74
+ return t.error
94
75
  default:
95
- return DarkTheme.textSecondary
76
+ return t.textSecondary
96
77
  }
97
78
  }
98
79
 
@@ -144,393 +125,301 @@ function tryParseJSON(text?: string): unknown | null {
144
125
 
145
126
  // ─── Component ───────────────────────────────────────────────────────────────
146
127
 
147
- export const NetworkItem: React.FC<NetworkItemProps> = React.memo(({ item, onCopy }) => {
148
- const [expanded, setExpanded] = useState(false)
149
- const [detailSection, setDetailSection] = useState<'general' | 'request' | 'response'>('general')
150
-
151
- const config = getStatusConfig(item)
152
- const methodColor = getMethodColor(item.method)
153
- const isPending = item.state === NetworkState.pending
154
-
155
- const toggleExpand = useCallback(() => {
156
- setExpanded(prev => !prev)
157
- }, [])
158
-
159
- const handleCopyCurl = useCallback(() => {
160
- if (onCopy) {
161
- onCopy(toCurl(item))
162
- }
163
- }, [item, onCopy])
164
-
165
- return (
166
- <TouchableOpacity
167
- style={[styles.container, { backgroundColor: config.rowBg }]}
168
- onPress={toggleExpand}
169
- onLongPress={handleCopyCurl}
170
- activeOpacity={0.7}
171
- >
172
- {/* ── Collapsed Row ─────────────────────────────────────── */}
173
- <View style={styles.headerRow}>
174
- {/* Method badge */}
175
- <View style={[styles.methodBadge, { borderColor: methodColor }]}>
176
- <Text style={[styles.methodText, { color: methodColor }]}>{item.method}</Text>
177
- </View>
178
-
179
- {/* Status badge */}
180
- <View style={[styles.statusBadge, { backgroundColor: config.bgColor }]}>
181
- <Text style={[styles.statusText, { color: config.color }]}>{config.label}</Text>
128
+ export const NetworkItem: React.FC<NetworkItemProps> = React.memo(
129
+ ({ item, onCopy, jsonMaxDepth }) => {
130
+ const theme = useBackstageTheme()
131
+ const s = useMemo(() => createStyles(theme), [theme])
132
+ const [expanded, setExpanded] = useState(false)
133
+ const [detailSection, setDetailSection] = useState<'general' | 'request' | 'response'>(
134
+ 'general',
135
+ )
136
+
137
+ const config = getStatusConfig(item, theme)
138
+ const methodColor = getMethodColor(item.method, theme)
139
+ const isPending = item.state === NetworkState.pending
140
+
141
+ const toggleExpand = useCallback(() => {
142
+ setExpanded(prev => !prev)
143
+ }, [])
144
+
145
+ const handleCopyCurl = useCallback(() => {
146
+ if (onCopy) {
147
+ onCopy(toCurl(item))
148
+ }
149
+ }, [item, onCopy])
150
+
151
+ // ─── Detail Row (inline) ─────────────────────────────────
152
+ const renderDetailRow = (
153
+ label: string,
154
+ value: string,
155
+ selectable?: boolean,
156
+ isError?: boolean,
157
+ ) => (
158
+ <View style={s.detailRow}>
159
+ <Text style={s.detailLabel}>{label}</Text>
160
+ <Text
161
+ style={[s.detailValue, isError && { color: theme.error }]}
162
+ numberOfLines={3}
163
+ selectable={selectable}
164
+ >
165
+ {value}
166
+ </Text>
167
+ </View>
168
+ )
169
+
170
+ return (
171
+ <TouchableOpacity
172
+ style={[s.container, { backgroundColor: config.rowBg }]}
173
+ onPress={toggleExpand}
174
+ onLongPress={handleCopyCurl}
175
+ activeOpacity={0.7}
176
+ >
177
+ {/* ── Collapsed Row ─────────────────────────────────────── */}
178
+ <View style={s.headerRow}>
179
+ <View style={[s.methodBadge, { borderColor: methodColor }]}>
180
+ <Text style={[s.methodText, { color: methodColor }]}>{item.method}</Text>
181
+ </View>
182
+ <View style={[s.statusBadge, { backgroundColor: config.bgColor }]}>
183
+ <Text style={[s.statusText, { color: config.color }]}>{config.label}</Text>
184
+ </View>
185
+ {!isPending && <Text style={s.duration}>{formatDuration(item.duration)}</Text>}
186
+ {isPending && <Text style={[s.duration, { color: theme.warning }]}>pending</Text>}
187
+ {item.responseSize !== undefined && (
188
+ <Text style={s.size}>{formatSize(item.responseSize)}</Text>
189
+ )}
190
+ <Text style={s.timestamp}>{formatTimestamp(item.startTime)}</Text>
182
191
  </View>
183
192
 
184
- {/* Duration */}
185
- {!isPending && <Text style={styles.duration}>{formatDuration(item.duration)}</Text>}
186
- {isPending && <Text style={[styles.duration, { color: DarkTheme.warning }]}>pending</Text>}
193
+ <Text style={s.path} numberOfLines={expanded ? undefined : 1}>
194
+ {extractPath(item.url)}
195
+ </Text>
196
+ <Text style={s.host} numberOfLines={1}>
197
+ {extractHost(item.url)}
198
+ </Text>
187
199
 
188
- {/* Size */}
189
- {item.responseSize !== undefined && (
190
- <Text style={styles.size}>{formatSize(item.responseSize)}</Text>
200
+ {item.error && (
201
+ <Text style={s.errorText} numberOfLines={expanded ? undefined : 1}>
202
+ {item.error}
203
+ </Text>
191
204
  )}
192
205
 
193
- {/* Timestamp */}
194
- <Text style={styles.timestamp}>{formatTimestamp(item.startTime)}</Text>
195
- </View>
196
-
197
- {/* URL */}
198
- <Text style={styles.path} numberOfLines={expanded ? undefined : 1}>
199
- {extractPath(item.url)}
200
- </Text>
201
- <Text style={styles.host} numberOfLines={1}>
202
- {extractHost(item.url)}
203
- </Text>
204
-
205
- {/* Error message */}
206
- {item.error && (
207
- <Text style={styles.errorText} numberOfLines={expanded ? undefined : 1}>
208
- ✕ {item.error}
209
- </Text>
210
- )}
211
-
212
- {/* ── Expanded Detail ───────────────────────────────────── */}
213
- {expanded && (
214
- <View style={styles.detailContainer}>
215
- {/* Section tabs */}
216
- <View style={styles.sectionTabs}>
217
- {(['general', 'request', 'response'] as const).map(section => (
218
- <TouchableOpacity
219
- key={section}
220
- style={[styles.sectionTab, detailSection === section && styles.sectionTabActive]}
221
- onPress={() => setDetailSection(section)}
222
- >
223
- <Text
224
- style={[
225
- styles.sectionTabText,
226
- detailSection === section && styles.sectionTabTextActive,
227
- ]}
206
+ {/* ── Expanded Detail ───────────────────────────────────── */}
207
+ {expanded && (
208
+ <View style={s.detailContainer}>
209
+ <View style={s.sectionTabs}>
210
+ {(['general', 'request', 'response'] as const).map(section => (
211
+ <TouchableOpacity
212
+ key={section}
213
+ style={[s.sectionTab, detailSection === section && s.sectionTabActive]}
214
+ onPress={() => setDetailSection(section)}
228
215
  >
229
- {section.charAt(0).toUpperCase() + section.slice(1)}
230
- </Text>
231
- </TouchableOpacity>
232
- ))}
233
- </View>
234
-
235
- {/* Section content */}
236
- {detailSection === 'general' && (
237
- <View style={styles.sectionContent}>
238
- <DetailRow label="Method" value={item.method} />
239
- <DetailRow label="URL" value={item.url} selectable />
240
- <DetailRow
241
- label="Status"
242
- value={item.status ? `${item.status} ${item.statusText || ''}` : '—'}
243
- />
244
- <DetailRow label="Duration" value={formatDuration(item.duration)} />
245
- {item.responseSize !== undefined && (
246
- <DetailRow label="Size" value={formatSize(item.responseSize)} />
247
- )}
248
- {item.error && <DetailRow label="Error" value={item.error} isError />}
216
+ <Text
217
+ style={[s.sectionTabText, detailSection === section && s.sectionTabTextActive]}
218
+ >
219
+ {section.charAt(0).toUpperCase() + section.slice(1)}
220
+ </Text>
221
+ </TouchableOpacity>
222
+ ))}
249
223
  </View>
250
- )}
251
224
 
252
- {detailSection === 'request' && (
253
- <View style={styles.sectionContent}>
254
- {item.requestHeaders && Object.keys(item.requestHeaders).length > 0 ? (
255
- <>
256
- <Text style={styles.subsectionTitle}>HEADERS</Text>
257
- <View style={styles.jsonContainer}>
258
- <JsonTreeView data={item.requestHeaders} hideRoot maxDepth={3} />
259
- </View>
260
- </>
261
- ) : (
262
- <Text style={styles.emptyNote}>No request headers captured</Text>
263
- )}
264
- {item.requestBody ? (
265
- <>
266
- <Text style={styles.subsectionTitle}>BODY</Text>
267
- {tryParseJSON(item.requestBody) ? (
268
- <View style={styles.jsonContainer}>
269
- <JsonTreeView data={tryParseJSON(item.requestBody)} hideRoot maxDepth={5} />
225
+ {detailSection === 'general' && (
226
+ <View style={s.sectionContent}>
227
+ {renderDetailRow('Method', item.method)}
228
+ {renderDetailRow('URL', item.url, true)}
229
+ {renderDetailRow(
230
+ 'Status',
231
+ item.status ? `${item.status} ${item.statusText || ''}` : '—',
232
+ )}
233
+ {renderDetailRow('Duration', formatDuration(item.duration))}
234
+ {item.responseSize !== undefined &&
235
+ renderDetailRow('Size', formatSize(item.responseSize))}
236
+ {item.error && renderDetailRow('Error', item.error, false, true)}
237
+ </View>
238
+ )}
239
+
240
+ {detailSection === 'request' && (
241
+ <View style={s.sectionContent}>
242
+ {item.requestHeaders && Object.keys(item.requestHeaders).length > 0 ? (
243
+ <>
244
+ <Text style={s.subsectionTitle}>HEADERS</Text>
245
+ <View style={s.jsonContainer}>
246
+ <JsonTreeView data={item.requestHeaders} hideRoot maxDepth={jsonMaxDepth} />
270
247
  </View>
271
- ) : (
272
- <Text style={styles.bodyText} selectable>
273
- {item.requestBody}
274
- </Text>
275
- )}
276
- </>
277
- ) : null}
278
- </View>
279
- )}
280
-
281
- {detailSection === 'response' && (
282
- <View style={styles.sectionContent}>
283
- {item.responseHeaders && Object.keys(item.responseHeaders).length > 0 ? (
284
- <>
285
- <Text style={styles.subsectionTitle}>HEADERS</Text>
286
- <View style={styles.jsonContainer}>
287
- <JsonTreeView data={item.responseHeaders} hideRoot maxDepth={3} />
288
- </View>
289
- </>
290
- ) : (
291
- <Text style={styles.emptyNote}>No response headers captured</Text>
292
- )}
293
- {item.responseBody ? (
294
- <>
295
- <Text style={styles.subsectionTitle}>BODY</Text>
296
- {tryParseJSON(item.responseBody) ? (
297
- <View style={styles.jsonContainer}>
298
- <JsonTreeView data={tryParseJSON(item.responseBody)} hideRoot maxDepth={5} />
248
+ </>
249
+ ) : (
250
+ <Text style={s.emptyNote}>No request headers captured</Text>
251
+ )}
252
+ {item.requestBody ? (
253
+ <>
254
+ <Text style={s.subsectionTitle}>BODY</Text>
255
+ {tryParseJSON(item.requestBody) ? (
256
+ <View style={s.jsonContainer}>
257
+ <JsonTreeView
258
+ data={tryParseJSON(item.requestBody)}
259
+ hideRoot
260
+ maxDepth={jsonMaxDepth}
261
+ />
262
+ </View>
263
+ ) : (
264
+ <Text style={s.bodyText} selectable>
265
+ {item.requestBody}
266
+ </Text>
267
+ )}
268
+ </>
269
+ ) : null}
270
+ </View>
271
+ )}
272
+
273
+ {detailSection === 'response' && (
274
+ <View style={s.sectionContent}>
275
+ {item.responseHeaders && Object.keys(item.responseHeaders).length > 0 ? (
276
+ <>
277
+ <Text style={s.subsectionTitle}>HEADERS</Text>
278
+ <View style={s.jsonContainer}>
279
+ <JsonTreeView data={item.responseHeaders} hideRoot maxDepth={jsonMaxDepth} />
299
280
  </View>
300
- ) : (
301
- <Text style={styles.bodyText} selectable>
302
- {item.responseBody}
303
- </Text>
304
- )}
305
- </>
306
- ) : (
307
- <Text style={styles.emptyNote}>
308
- {isPending ? 'Awaiting response…' : 'No response body'}
309
- </Text>
310
- )}
311
- </View>
312
- )}
281
+ </>
282
+ ) : (
283
+ <Text style={s.emptyNote}>No response headers captured</Text>
284
+ )}
285
+ {item.responseBody ? (
286
+ <>
287
+ <Text style={s.subsectionTitle}>BODY</Text>
288
+ {tryParseJSON(item.responseBody) ? (
289
+ <View style={s.jsonContainer}>
290
+ <JsonTreeView
291
+ data={tryParseJSON(item.responseBody)}
292
+ hideRoot
293
+ maxDepth={jsonMaxDepth}
294
+ />
295
+ </View>
296
+ ) : (
297
+ <Text style={s.bodyText} selectable>
298
+ {item.responseBody}
299
+ </Text>
300
+ )}
301
+ </>
302
+ ) : (
303
+ <Text style={s.emptyNote}>
304
+ {isPending ? 'Awaiting response…' : 'No response body'}
305
+ </Text>
306
+ )}
307
+ </View>
308
+ )}
309
+
310
+ {onCopy && (
311
+ <TouchableOpacity style={s.curlButton} onPress={handleCopyCurl} activeOpacity={0.7}>
312
+ <Text style={s.curlButtonText}>⧉ Copy as cURL</Text>
313
+ </TouchableOpacity>
314
+ )}
315
+ </View>
316
+ )}
313
317
 
314
- {/* Copy as cURL */}
315
- {onCopy && (
316
- <TouchableOpacity
317
- style={styles.curlButton}
318
- onPress={handleCopyCurl}
319
- activeOpacity={0.7}
320
- >
321
- <Text style={styles.curlButtonText}>⧉ Copy as cURL</Text>
322
- </TouchableOpacity>
323
- )}
324
- </View>
325
- )}
326
-
327
- {/* Expand hint */}
328
- {!expanded && <Text style={styles.expandHint}>tap to inspect ▾</Text>}
329
- </TouchableOpacity>
330
- )
331
- })
332
-
333
- // ─── Detail Row ──────────────────────────────────────────────────────────────
334
-
335
- const DetailRow: React.FC<{
336
- label: string
337
- value: string
338
- selectable?: boolean
339
- isError?: boolean
340
- }> = ({ label, value, selectable, isError }) => (
341
- <View style={styles.detailRow}>
342
- <Text style={styles.detailLabel}>{label}</Text>
343
- <Text
344
- style={[styles.detailValue, isError && { color: DarkTheme.error }]}
345
- numberOfLines={3}
346
- selectable={selectable}
347
- >
348
- {value}
349
- </Text>
350
- </View>
318
+ {!expanded && <Text style={s.expandHint}>tap to inspect ▾</Text>}
319
+ </TouchableOpacity>
320
+ )
321
+ },
351
322
  )
352
323
 
353
324
  // ─── Styles ──────────────────────────────────────────────────────────────────
354
325
 
355
- const styles = StyleSheet.create({
356
- container: {
357
- paddingHorizontal: 14,
358
- paddingVertical: 10,
359
- borderBottomWidth: 1,
360
- borderBottomColor: DarkTheme.border,
361
- },
362
- headerRow: {
363
- flexDirection: 'row',
364
- alignItems: 'center',
365
- marginBottom: 4,
366
- gap: 6,
367
- },
368
- methodBadge: {
369
- borderRadius: 4,
370
- borderWidth: 1,
371
- paddingHorizontal: 6,
372
- paddingVertical: 1,
373
- },
374
- methodText: {
375
- fontFamily: MonospaceFont,
376
- fontSize: 10,
377
- fontWeight: '800',
378
- letterSpacing: 0.5,
379
- },
380
- statusBadge: {
381
- borderRadius: 4,
382
- paddingHorizontal: 6,
383
- paddingVertical: 2,
384
- },
385
- statusText: {
386
- fontFamily: MonospaceFont,
387
- fontSize: 10,
388
- fontWeight: '800',
389
- letterSpacing: 0.5,
390
- },
391
- duration: {
392
- fontFamily: MonospaceFont,
393
- fontSize: 11,
394
- color: DarkTheme.textMuted,
395
- },
396
- size: {
397
- fontFamily: MonospaceFont,
398
- fontSize: 10,
399
- color: DarkTheme.textMuted,
400
- opacity: 0.7,
401
- },
402
- timestamp: {
403
- fontFamily: MonospaceFont,
404
- fontSize: 10,
405
- color: DarkTheme.textMuted,
406
- marginLeft: 'auto',
407
- },
408
- path: {
409
- fontFamily: MonospaceFont,
410
- fontSize: 12,
411
- color: DarkTheme.text,
412
- lineHeight: 18,
413
- },
414
- host: {
415
- fontFamily: MonospaceFont,
416
- fontSize: 10,
417
- color: DarkTheme.textMuted,
418
- marginTop: 1,
419
- },
420
- errorText: {
421
- fontFamily: MonospaceFont,
422
- fontSize: 11,
423
- color: DarkTheme.error,
424
- marginTop: 4,
425
- },
426
- expandHint: {
427
- fontFamily: MonospaceFont,
428
- fontSize: 10,
429
- color: DarkTheme.textMuted,
430
- marginTop: 4,
431
- fontStyle: 'italic',
432
- },
433
- // ── Detail Section ──────────────────────────────
434
- detailContainer: {
435
- marginTop: 10,
436
- paddingTop: 10,
437
- borderTopWidth: 1,
438
- borderTopColor: DarkTheme.border,
439
- },
440
- sectionTabs: {
441
- flexDirection: 'row',
442
- gap: 4,
443
- marginBottom: 10,
444
- },
445
- sectionTab: {
446
- paddingHorizontal: 12,
447
- paddingVertical: 6,
448
- borderRadius: 6,
449
- backgroundColor: DarkTheme.surfaceElevated,
450
- borderWidth: 1,
451
- borderColor: DarkTheme.border,
452
- },
453
- sectionTabActive: {
454
- backgroundColor: DarkTheme.accentDim,
455
- borderColor: DarkTheme.accent,
456
- },
457
- sectionTabText: {
458
- fontFamily: MonospaceFont,
459
- fontSize: 11,
460
- fontWeight: '600',
461
- color: DarkTheme.textMuted,
462
- },
463
- sectionTabTextActive: {
464
- color: DarkTheme.accent,
465
- },
466
- sectionContent: {
467
- marginBottom: 8,
468
- },
469
- subsectionTitle: {
470
- fontFamily: MonospaceFont,
471
- fontSize: 10,
472
- fontWeight: '700',
473
- color: DarkTheme.textMuted,
474
- letterSpacing: 1.2,
475
- marginBottom: 6,
476
- marginTop: 8,
477
- },
478
- jsonContainer: {
479
- backgroundColor: DarkTheme.surfaceElevated,
480
- borderRadius: 8,
481
- borderWidth: 1,
482
- borderColor: DarkTheme.border,
483
- padding: 8,
484
- },
485
- bodyText: {
486
- fontFamily: MonospaceFont,
487
- fontSize: 11,
488
- color: DarkTheme.text,
489
- lineHeight: 16,
490
- backgroundColor: DarkTheme.surfaceElevated,
491
- borderRadius: 8,
492
- borderWidth: 1,
493
- borderColor: DarkTheme.border,
494
- padding: 8,
495
- overflow: 'hidden',
496
- },
497
- emptyNote: {
498
- fontFamily: MonospaceFont,
499
- fontSize: 11,
500
- color: DarkTheme.textMuted,
501
- fontStyle: 'italic',
502
- paddingVertical: 8,
503
- },
504
- detailRow: {
505
- flexDirection: 'row',
506
- paddingVertical: 4,
507
- },
508
- detailLabel: {
509
- fontFamily: MonospaceFont,
510
- fontSize: 11,
511
- color: DarkTheme.textMuted,
512
- width: 70,
513
- },
514
- detailValue: {
515
- fontFamily: MonospaceFont,
516
- fontSize: 11,
517
- color: DarkTheme.text,
518
- flex: 1,
519
- },
520
- curlButton: {
521
- marginTop: 10,
522
- backgroundColor: DarkTheme.accentDim,
523
- borderRadius: 8,
524
- borderWidth: 1,
525
- borderColor: DarkTheme.accent,
526
- paddingVertical: 8,
527
- paddingHorizontal: 14,
528
- alignSelf: 'flex-start',
529
- },
530
- curlButtonText: {
531
- fontFamily: MonospaceFont,
532
- fontSize: 12,
533
- fontWeight: '700',
534
- color: DarkTheme.accent,
535
- },
536
- })
326
+ const createStyles = (t: BackstageTheme) =>
327
+ StyleSheet.create({
328
+ container: {
329
+ paddingHorizontal: 14,
330
+ paddingVertical: 10,
331
+ borderBottomWidth: 1,
332
+ borderBottomColor: t.border,
333
+ },
334
+ headerRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 4, gap: 6 },
335
+ methodBadge: { borderRadius: 4, borderWidth: 1, paddingHorizontal: 6, paddingVertical: 1 },
336
+ methodText: { fontFamily: MonospaceFont, fontSize: 10, fontWeight: '800', letterSpacing: 0.5 },
337
+ statusBadge: { borderRadius: 4, paddingHorizontal: 6, paddingVertical: 2 },
338
+ statusText: { fontFamily: MonospaceFont, fontSize: 10, fontWeight: '800', letterSpacing: 0.5 },
339
+ duration: { fontFamily: MonospaceFont, fontSize: 11, color: t.textMuted },
340
+ size: { fontFamily: MonospaceFont, fontSize: 10, color: t.textMuted, opacity: 0.7 },
341
+ timestamp: { fontFamily: MonospaceFont, fontSize: 10, color: t.textMuted, marginLeft: 'auto' },
342
+ path: { fontFamily: MonospaceFont, fontSize: 12, color: t.text, lineHeight: 18 },
343
+ host: { fontFamily: MonospaceFont, fontSize: 10, color: t.textMuted, marginTop: 1 },
344
+ errorText: { fontFamily: MonospaceFont, fontSize: 11, color: t.error, marginTop: 4 },
345
+ expandHint: {
346
+ fontFamily: MonospaceFont,
347
+ fontSize: 10,
348
+ color: t.textMuted,
349
+ marginTop: 4,
350
+ fontStyle: 'italic',
351
+ },
352
+ detailContainer: {
353
+ marginTop: 10,
354
+ paddingTop: 10,
355
+ borderTopWidth: 1,
356
+ borderTopColor: t.border,
357
+ },
358
+ sectionTabs: { flexDirection: 'row', gap: 4, marginBottom: 10 },
359
+ sectionTab: {
360
+ paddingHorizontal: 12,
361
+ paddingVertical: 6,
362
+ borderRadius: 6,
363
+ backgroundColor: t.surfaceElevated,
364
+ borderWidth: 1,
365
+ borderColor: t.border,
366
+ },
367
+ sectionTabActive: { backgroundColor: t.accentDim, borderColor: t.accent },
368
+ sectionTabText: {
369
+ fontFamily: MonospaceFont,
370
+ fontSize: 11,
371
+ fontWeight: '600',
372
+ color: t.textMuted,
373
+ },
374
+ sectionTabTextActive: { color: t.accent },
375
+ sectionContent: { marginBottom: 8 },
376
+ subsectionTitle: {
377
+ fontFamily: MonospaceFont,
378
+ fontSize: 10,
379
+ fontWeight: '700',
380
+ color: t.textMuted,
381
+ letterSpacing: 1.2,
382
+ marginBottom: 6,
383
+ marginTop: 8,
384
+ },
385
+ jsonContainer: {
386
+ backgroundColor: t.surfaceElevated,
387
+ borderRadius: 8,
388
+ borderWidth: 1,
389
+ borderColor: t.border,
390
+ padding: 8,
391
+ },
392
+ bodyText: {
393
+ fontFamily: MonospaceFont,
394
+ fontSize: 11,
395
+ color: t.text,
396
+ lineHeight: 16,
397
+ backgroundColor: t.surfaceElevated,
398
+ borderRadius: 8,
399
+ borderWidth: 1,
400
+ borderColor: t.border,
401
+ padding: 8,
402
+ overflow: 'hidden',
403
+ },
404
+ emptyNote: {
405
+ fontFamily: MonospaceFont,
406
+ fontSize: 11,
407
+ color: t.textMuted,
408
+ fontStyle: 'italic',
409
+ paddingVertical: 8,
410
+ },
411
+ detailRow: { flexDirection: 'row', paddingVertical: 4 },
412
+ detailLabel: { fontFamily: MonospaceFont, fontSize: 11, color: t.textMuted, width: 70 },
413
+ detailValue: { fontFamily: MonospaceFont, fontSize: 11, color: t.text, flex: 1 },
414
+ curlButton: {
415
+ marginTop: 10,
416
+ backgroundColor: t.accentDim,
417
+ borderRadius: 8,
418
+ borderWidth: 1,
419
+ borderColor: t.accent,
420
+ paddingVertical: 8,
421
+ paddingHorizontal: 14,
422
+ alignSelf: 'flex-start',
423
+ },
424
+ curlButtonText: { fontFamily: MonospaceFont, fontSize: 12, fontWeight: '700', color: t.accent },
425
+ })