rn-backstage 1.4.0 → 1.4.2

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.
@@ -0,0 +1,349 @@
1
+ import React, { useEffect, useMemo, useState } from 'react'
2
+ import {
3
+ Modal,
4
+ SafeAreaView,
5
+ StatusBar,
6
+ StyleSheet,
7
+ Text,
8
+ TouchableOpacity,
9
+ View,
10
+ } from 'react-native'
11
+ import { MonospaceFont, TestIDs } from '../constants'
12
+ import { useBackstageTheme } from '../ThemeContext'
13
+ import { TabBar } from './TabBar'
14
+ import { InfoTab } from './InfoTab'
15
+ import { LogsTab } from './LogsTab'
16
+ import { NetworkTab } from './NetworkTab'
17
+ import { FlagsTab } from './FlagsTab'
18
+ import { StorageTab } from './StorageTab'
19
+ import { BugReportComposer } from './BugReportComposer'
20
+ import type {
21
+ AppInfoItem,
22
+ BackstageStyleOverrides,
23
+ BackstageTab,
24
+ BugReportConfig,
25
+ FeatureFlag,
26
+ LogEntry,
27
+ NetworkEntry,
28
+ QuickAction,
29
+ StorageAdapter,
30
+ } from '../types'
31
+
32
+ // ─── Types ───────────────────────────────────────────────────────────────────
33
+
34
+ interface BackstagePanelProps {
35
+ visible: boolean
36
+ onClose: () => void
37
+
38
+ // Info tab props
39
+ appVersion?: string
40
+ buildNumber?: string
41
+ bundleId?: string
42
+ deviceInfo?: AppInfoItem[]
43
+ state?: Record<string, unknown>
44
+ quickActions?: QuickAction[]
45
+ featureFlags?: FeatureFlag[]
46
+ onToggleFeatureFlag?: (key: string, value: boolean) => void
47
+
48
+ // Logs tab props
49
+ logs: LogEntry[]
50
+ onRefreshLogs: () => void
51
+ onCopyLogs?: (logs: string) => void
52
+
53
+ // Network tab props
54
+ networkEntries: NetworkEntry[]
55
+ onRefreshNetwork: () => void
56
+ onClearNetwork: () => void
57
+ onCopyNetwork?: (text: string) => void
58
+
59
+ // Extensibility
60
+ extraTabs?: BackstageTab[]
61
+
62
+ // JSON tree
63
+ jsonMaxDepth?: number
64
+
65
+ // Storage
66
+ storageAdapter?: StorageAdapter
67
+
68
+ // Bug report
69
+ bugReportConfig?: BugReportConfig
70
+ bugReportOpenerRef?: React.MutableRefObject<(() => void) | null>
71
+
72
+ // Styling
73
+ styles?: BackstageStyleOverrides
74
+ children?: React.ReactNode
75
+ }
76
+
77
+ // ─── Built-in Tab Definitions ────────────────────────────────────────────
78
+
79
+ const ALWAYS_ON_TABS = [
80
+ { key: 'info', title: 'Info', icon: 'ℹ️' },
81
+ { key: 'network', title: 'Network', icon: '🌐' },
82
+ ]
83
+
84
+ const FLAGS_TAB = { key: 'flags', title: 'Flags', icon: '🎚' }
85
+
86
+ const STORAGE_TAB = { key: 'storage', title: 'Storage', icon: '🗄' }
87
+
88
+ const LOGS_TAB = { key: 'logs', title: 'Logs', icon: '📋' }
89
+
90
+ // ─── Component ───────────────────────────────────────────────────────────────
91
+
92
+ export const BackstagePanel: React.FC<BackstagePanelProps> = ({
93
+ visible,
94
+ onClose,
95
+ appVersion,
96
+ buildNumber,
97
+ bundleId,
98
+ deviceInfo,
99
+ state,
100
+ quickActions,
101
+ featureFlags,
102
+ onToggleFeatureFlag,
103
+ logs,
104
+ onRefreshLogs,
105
+ onCopyLogs,
106
+ networkEntries,
107
+ onRefreshNetwork,
108
+ onClearNetwork,
109
+ onCopyNetwork,
110
+ extraTabs = [],
111
+ jsonMaxDepth,
112
+ storageAdapter,
113
+ bugReportConfig,
114
+ bugReportOpenerRef,
115
+ styles: propStyles,
116
+ children,
117
+ }) => {
118
+ const [activeTab, setActiveTab] = useState('info')
119
+ const [bugReportVisible, setBugReportVisible] = useState(false)
120
+ const theme = useBackstageTheme()
121
+ const styles = useMemo(() => createStyles(theme), [theme])
122
+
123
+ // Reset to Info tab when panel closes so indicator and content stay in sync
124
+ useEffect(() => {
125
+ if (!visible) {
126
+ setActiveTab('info')
127
+ }
128
+ }, [visible])
129
+
130
+ // Register opener callback for the parent ref
131
+ useEffect(() => {
132
+ if (!bugReportOpenerRef) return
133
+ bugReportOpenerRef.current = () => setBugReportVisible(true)
134
+ return () => {
135
+ bugReportOpenerRef.current = null
136
+ }
137
+ }, [bugReportOpenerRef])
138
+
139
+ // Compose all tabs: built-in (conditionally including Flags) + extra
140
+ const hasFlags = featureFlags && featureFlags.length > 0
141
+ const hasStorage = !!storageAdapter
142
+ const allTabs = [
143
+ ...ALWAYS_ON_TABS,
144
+ ...(hasFlags ? [FLAGS_TAB] : []),
145
+ ...(hasStorage ? [STORAGE_TAB] : []),
146
+ LOGS_TAB,
147
+ ...extraTabs.map(t => ({ key: t.key, title: t.title, icon: t.icon })),
148
+ ]
149
+
150
+ // Render active tab content
151
+ const renderTabContent = () => {
152
+ switch (activeTab) {
153
+ case 'info':
154
+ return (
155
+ <InfoTab
156
+ appVersion={appVersion}
157
+ buildNumber={buildNumber}
158
+ bundleId={bundleId}
159
+ deviceInfo={deviceInfo}
160
+ state={state}
161
+ quickActions={quickActions}
162
+ jsonMaxDepth={jsonMaxDepth}
163
+ onClosePanel={onClose}
164
+ styles={propStyles}
165
+ >
166
+ {children}
167
+ </InfoTab>
168
+ )
169
+ case 'flags':
170
+ return <FlagsTab flags={featureFlags ?? []} onToggle={onToggleFeatureFlag} />
171
+ case 'storage':
172
+ return storageAdapter ? (
173
+ <StorageTab adapter={storageAdapter} jsonMaxDepth={jsonMaxDepth} />
174
+ ) : null
175
+ case 'network':
176
+ return (
177
+ <NetworkTab
178
+ entries={networkEntries}
179
+ onRefresh={onRefreshNetwork}
180
+ onClear={onClearNetwork}
181
+ onCopy={onCopyNetwork}
182
+ jsonMaxDepth={jsonMaxDepth}
183
+ />
184
+ )
185
+ case 'logs':
186
+ return (
187
+ <LogsTab
188
+ logs={logs}
189
+ onRefresh={onRefreshLogs}
190
+ onCopyLogs={onCopyLogs}
191
+ jsonMaxDepth={jsonMaxDepth}
192
+ styles={propStyles}
193
+ />
194
+ )
195
+ default: {
196
+ // Render extra tab content
197
+ const extraTab = extraTabs.find(t => t.key === activeTab)
198
+ if (extraTab) {
199
+ return <>{extraTab.render()}</>
200
+ }
201
+ return null
202
+ }
203
+ }
204
+ }
205
+
206
+ return (
207
+ <Modal
208
+ testID={TestIDs.panelModal}
209
+ animationType="slide"
210
+ transparent={false}
211
+ visible={visible}
212
+ onRequestClose={onClose}
213
+ statusBarTranslucent
214
+ >
215
+ <StatusBar
216
+ barStyle={theme.background < '#888' ? 'light-content' : 'dark-content'}
217
+ backgroundColor={theme.background}
218
+ />
219
+ <SafeAreaView testID={TestIDs.panel} style={[styles.safeArea, propStyles?.panelStyle]}>
220
+ {/* ── Header ──────────────────────────────────────────── */}
221
+ <View testID={TestIDs.header.container} style={styles.header}>
222
+ <View style={styles.headerLeft}>
223
+ <Text style={styles.headerBrand}>⚙</Text>
224
+ <Text
225
+ testID={TestIDs.header.title}
226
+ style={[styles.headerTitle, propStyles?.headerTitleStyle]}
227
+ >
228
+ Backstage
229
+ </Text>
230
+ </View>
231
+ <View style={styles.headerRight}>
232
+ {bugReportConfig && (
233
+ <TouchableOpacity
234
+ testID={TestIDs.bugReport.triggerButton}
235
+ style={styles.headerButton}
236
+ onPress={() => setBugReportVisible(true)}
237
+ activeOpacity={0.7}
238
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
239
+ >
240
+ <Text style={styles.headerButtonText}>🐛</Text>
241
+ </TouchableOpacity>
242
+ )}
243
+ <TouchableOpacity
244
+ testID={TestIDs.header.closeButton}
245
+ style={styles.closeButton}
246
+ onPress={onClose}
247
+ activeOpacity={0.7}
248
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
249
+ >
250
+ <Text style={styles.closeButtonText}>✕</Text>
251
+ </TouchableOpacity>
252
+ </View>
253
+ </View>
254
+
255
+ {/* ── Tab Bar ─────────────────────────────────────────── */}
256
+ <TabBar tabs={allTabs} activeTab={activeTab} onTabChange={setActiveTab} />
257
+
258
+ {/* ── Tab Content ─────────────────────────────────────── */}
259
+ <View style={styles.content}>{renderTabContent()}</View>
260
+ </SafeAreaView>
261
+
262
+ {/* ── Bug Report Composer ───────────────────────────────── */}
263
+ {bugReportConfig && (
264
+ <BugReportComposer
265
+ visible={bugReportVisible}
266
+ onClose={() => setBugReportVisible(false)}
267
+ config={bugReportConfig}
268
+ logs={logs}
269
+ networkEntries={networkEntries}
270
+ state={state}
271
+ appVersion={appVersion}
272
+ buildNumber={buildNumber}
273
+ bundleId={bundleId}
274
+ deviceInfo={deviceInfo}
275
+ />
276
+ )}
277
+ </Modal>
278
+ )
279
+ }
280
+
281
+ // ─── Styles ──────────────────────────────────────────────────────────────────
282
+
283
+ const createStyles = (t: import('../types').BackstageTheme) =>
284
+ StyleSheet.create({
285
+ safeArea: {
286
+ flex: 1,
287
+ backgroundColor: t.background,
288
+ },
289
+ header: {
290
+ flexDirection: 'row',
291
+ alignItems: 'center',
292
+ justifyContent: 'space-between',
293
+ paddingHorizontal: 16,
294
+ paddingVertical: 12,
295
+ borderBottomWidth: 1,
296
+ borderBottomColor: t.border,
297
+ },
298
+ headerLeft: {
299
+ flexDirection: 'row',
300
+ alignItems: 'center',
301
+ },
302
+ headerBrand: {
303
+ fontSize: 20,
304
+ marginRight: 8,
305
+ },
306
+ headerTitle: {
307
+ fontFamily: MonospaceFont,
308
+ fontSize: 18,
309
+ fontWeight: '800',
310
+ color: t.text,
311
+ letterSpacing: 1,
312
+ },
313
+ closeButton: {
314
+ width: 32,
315
+ height: 32,
316
+ borderRadius: 16,
317
+ backgroundColor: t.surfaceElevated,
318
+ borderWidth: 1,
319
+ borderColor: t.border,
320
+ alignItems: 'center',
321
+ justifyContent: 'center',
322
+ },
323
+ closeButtonText: {
324
+ fontSize: 14,
325
+ color: t.textSecondary,
326
+ fontWeight: '700',
327
+ },
328
+ headerRight: {
329
+ flexDirection: 'row',
330
+ alignItems: 'center',
331
+ gap: 8,
332
+ },
333
+ headerButton: {
334
+ width: 32,
335
+ height: 32,
336
+ borderRadius: 16,
337
+ backgroundColor: t.surfaceElevated,
338
+ borderWidth: 1,
339
+ borderColor: t.border,
340
+ alignItems: 'center',
341
+ justifyContent: 'center',
342
+ },
343
+ headerButtonText: {
344
+ fontSize: 16,
345
+ },
346
+ content: {
347
+ flex: 1,
348
+ },
349
+ })