rn-backstage 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,154 +0,0 @@
1
- import React, { useCallback, useMemo, useRef, useEffect } from 'react'
2
- import {
3
- Animated,
4
- LayoutChangeEvent,
5
- ScrollView,
6
- StyleSheet,
7
- Text,
8
- TouchableOpacity,
9
- View,
10
- } from 'react-native'
11
- import { MonospaceFont } from '../constants'
12
- import { useBackstageTheme } from '../ThemeContext'
13
-
14
- // ─── Types ───────────────────────────────────────────────────────────────────
15
-
16
- interface Tab {
17
- key: string
18
- title: string
19
- icon?: string
20
- }
21
-
22
- interface TabBarProps {
23
- tabs: Tab[]
24
- activeTab: string
25
- onTabChange: (key: string) => void
26
- }
27
-
28
- // ─── Component ───────────────────────────────────────────────────────────────
29
-
30
- export const TabBar: React.FC<TabBarProps> = ({ tabs, activeTab, onTabChange }) => {
31
- const theme = useBackstageTheme()
32
- const styles = useMemo(() => createStyles(theme), [theme])
33
- const indicatorAnim = useRef(new Animated.Value(0)).current
34
- const tabWidths = useRef<Record<string, number>>({})
35
- const tabOffsets = useRef<Record<string, number>>({})
36
- const scrollRef = useRef<ScrollView>(null)
37
-
38
- // Animate indicator when active tab changes
39
- useEffect(() => {
40
- const offset = tabOffsets.current[activeTab] || 0
41
- Animated.spring(indicatorAnim, {
42
- toValue: offset,
43
- useNativeDriver: true,
44
- friction: 8,
45
- tension: 60,
46
- }).start()
47
- }, [activeTab, indicatorAnim])
48
-
49
- const handleTabLayout = useCallback(
50
- (key: string) => (e: LayoutChangeEvent) => {
51
- const { x, width } = e.nativeEvent.layout
52
- tabWidths.current[key] = width
53
- tabOffsets.current[key] = x
54
-
55
- // Re-animate if this is the active tab
56
- if (key === activeTab) {
57
- indicatorAnim.setValue(x)
58
- }
59
- },
60
- [activeTab, indicatorAnim],
61
- )
62
-
63
- const activeWidth = tabWidths.current[activeTab] || 80
64
-
65
- return (
66
- <View style={styles.container}>
67
- <ScrollView
68
- ref={scrollRef}
69
- horizontal
70
- showsHorizontalScrollIndicator={false}
71
- contentContainerStyle={styles.scrollContent}
72
- >
73
- {tabs.map(tab => {
74
- const isActive = tab.key === activeTab
75
- return (
76
- <TouchableOpacity
77
- key={tab.key}
78
- onPress={() => onTabChange(tab.key)}
79
- onLayout={handleTabLayout(tab.key)}
80
- style={styles.tab}
81
- activeOpacity={0.7}
82
- testID={`backstage.tab.${tab.key}`}
83
- >
84
- {tab.icon && (
85
- <Text style={[styles.tabIcon, isActive && styles.tabIconActive]}>{tab.icon}</Text>
86
- )}
87
- <Text style={[styles.tabText, isActive && styles.tabTextActive]}>{tab.title}</Text>
88
- </TouchableOpacity>
89
- )
90
- })}
91
-
92
- {/* Animated underline */}
93
- <Animated.View
94
- style={[
95
- styles.indicator,
96
- {
97
- width: activeWidth,
98
- transform: [{ translateX: indicatorAnim }],
99
- },
100
- ]}
101
- />
102
- </ScrollView>
103
- </View>
104
- )
105
- }
106
-
107
- // ─── Styles ──────────────────────────────────────────────────────────────────
108
-
109
- const createStyles = (t: import('../types').BackstageTheme) =>
110
- StyleSheet.create({
111
- container: {
112
- borderBottomWidth: 1,
113
- borderBottomColor: t.border,
114
- },
115
- scrollContent: {
116
- flexDirection: 'row',
117
- position: 'relative',
118
- paddingBottom: 0,
119
- },
120
- tab: {
121
- flexDirection: 'row',
122
- alignItems: 'center',
123
- justifyContent: 'center',
124
- paddingHorizontal: 20,
125
- paddingVertical: 14,
126
- minWidth: 80,
127
- },
128
- tabIcon: {
129
- fontSize: 14,
130
- marginRight: 6,
131
- opacity: 0.5,
132
- },
133
- tabIconActive: {
134
- opacity: 1,
135
- },
136
- tabText: {
137
- fontFamily: MonospaceFont,
138
- fontSize: 13,
139
- fontWeight: '600',
140
- color: t.textMuted,
141
- textTransform: 'uppercase',
142
- letterSpacing: 1,
143
- },
144
- tabTextActive: {
145
- color: t.accent,
146
- },
147
- indicator: {
148
- position: 'absolute',
149
- bottom: 0,
150
- height: 2,
151
- backgroundColor: t.accent,
152
- borderRadius: 1,
153
- },
154
- })
package/src/constants.ts DELETED
@@ -1,207 +0,0 @@
1
- import { Dimensions, Platform } from 'react-native'
2
- import type { BackstageTheme } from './types'
3
-
4
- // ─── Metrics ─────────────────────────────────────────────────────────────────
5
-
6
- const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
7
-
8
- export const Metrics = {
9
- screenWidth,
10
- screenHeight,
11
- isIOS: Platform.OS === 'ios',
12
- pillWidth: 72,
13
- pillHeight: 32,
14
- panelBorderRadius: 20,
15
- tabBarHeight: 48,
16
- headerHeight: 52,
17
- sectionSpacing: 16,
18
- contentPadding: 16,
19
- }
20
-
21
- // ─── Dark Theme ──────────────────────────────────────────────────────────────
22
-
23
- export const DarkTheme: BackstageTheme = {
24
- background: '#0D0D12',
25
- surface: '#16161E',
26
- surfaceElevated: '#1E1E2A',
27
- border: '#2A2A3A',
28
- text: '#E8E8ED',
29
- textSecondary: '#A0A0B0',
30
- textMuted: '#606075',
31
- accent: '#7C5CFC',
32
- accentDim: 'rgba(124, 92, 252, 0.15)',
33
- error: '#FF4D6A',
34
- errorDim: 'rgba(255, 77, 106, 0.12)',
35
- warning: '#FFB224',
36
- warningDim: 'rgba(255, 178, 36, 0.12)',
37
- success: '#34D399',
38
- info: '#38BDF8',
39
- infoDim: 'rgba(56, 189, 248, 0.12)',
40
- debugColor: '#A78BFA',
41
- debugDim: 'rgba(167, 139, 250, 0.12)',
42
- }
43
-
44
- // ─── Light Theme ─────────────────────────────────────────────────────────────
45
-
46
- export const LightTheme: BackstageTheme = {
47
- background: '#F5F5F7',
48
- surface: '#FFFFFF',
49
- surfaceElevated: '#F0F0F4',
50
- border: '#D8D8DC',
51
- text: '#1C1C1E',
52
- textSecondary: '#636366',
53
- textMuted: '#AEAEB2',
54
- accent: '#6A3DE8',
55
- accentDim: 'rgba(106, 61, 232, 0.10)',
56
- error: '#E5325F',
57
- errorDim: 'rgba(229, 50, 95, 0.10)',
58
- warning: '#E09400',
59
- warningDim: 'rgba(224, 148, 0, 0.10)',
60
- success: '#28A770',
61
- info: '#0E8AD6',
62
- infoDim: 'rgba(14, 138, 214, 0.10)',
63
- debugColor: '#7C4DFF',
64
- debugDim: 'rgba(124, 77, 255, 0.10)',
65
- }
66
-
67
- export const DEFAULT_MAX_LOGS = 500
68
-
69
- // ─── Network Defaults ────────────────────────────────────────────────────────
70
-
71
- export const DEFAULT_MAX_NETWORK_ENTRIES = 500
72
- export const DEFAULT_MAX_NETWORK_BODY_SIZE = 65536 // 64 KB
73
-
74
- // ─── Monospace Font ──────────────────────────────────────────────────────────
75
-
76
- export const MonospaceFont = Platform.select({
77
- ios: 'Menlo',
78
- default: 'monospace',
79
- })
80
-
81
- // ─── Test IDs ────────────────────────────────────────────────────────────────
82
-
83
- export const TestIDs = {
84
- // ── Floating Pill ──────────────────────────────
85
- floatingPill: 'backstage.floating-pill',
86
- floatingPillText: 'backstage.floating-pill.text',
87
-
88
- // ── Panel ──────────────────────────────────────
89
- panel: 'backstage.panel',
90
- panelModal: 'backstage.panel.modal',
91
-
92
- // ── Header ─────────────────────────────────────
93
- header: {
94
- container: 'backstage.header',
95
- title: 'backstage.header.title',
96
- closeButton: 'backstage.header.close',
97
- },
98
-
99
- // ── Tab Bar ────────────────────────────────────
100
- tabBar: 'backstage.tab-bar',
101
- tabs: {
102
- info: 'backstage.tab.info',
103
- network: 'backstage.tab.network',
104
- flags: 'backstage.tab.flags',
105
- storage: 'backstage.tab.storage',
106
- logs: 'backstage.tab.logs',
107
- },
108
-
109
- // ── Info Tab ───────────────────────────────────
110
- infoTab: {
111
- container: 'backstage.info',
112
- deviceInfo: 'backstage.info.device',
113
- stateTree: 'backstage.info.state-tree',
114
- quickActions: 'backstage.info.quick-actions',
115
- /** Dynamic: backstage.info.action.{index} */
116
- actionButton: (index: number) => `backstage.info.action.${index}`,
117
- },
118
-
119
- // ── Logs Tab ───────────────────────────────────
120
- logsTab: {
121
- container: 'backstage.logs',
122
- searchInput: 'backstage.logs.search',
123
- copyButton: 'backstage.logs.copy',
124
- list: 'backstage.logs.list',
125
- statsBar: 'backstage.logs.stats',
126
- },
127
-
128
- // ── Log Item ───────────────────────────────────
129
- logItem: {
130
- /** Dynamic: backstage.log-item.{id} */
131
- container: (id: string) => `backstage.log-item.${id}`,
132
- badge: (id: string) => `backstage.log-item.${id}.badge`,
133
- message: (id: string) => `backstage.log-item.${id}.message`,
134
- timestamp: (id: string) => `backstage.log-item.${id}.timestamp`,
135
- dataContainer: (id: string) => `backstage.log-item.${id}.data`,
136
- copyButton: (id: string) => `backstage.log-item.${id}.copy`,
137
- },
138
-
139
- // ── Network Tab ────────────────────────────────
140
- networkTab: {
141
- container: 'backstage.network',
142
- searchInput: 'backstage.network.search',
143
- clearButton: 'backstage.network.clear',
144
- list: 'backstage.network.list',
145
- statsBar: 'backstage.network.stats',
146
- },
147
-
148
- // ── Network Item ───────────────────────────────
149
- networkItem: {
150
- /** Dynamic: backstage.network-item.{id} */
151
- container: (id: string) => `backstage.network-item.${id}`,
152
- methodBadge: (id: string) => `backstage.network-item.${id}.method`,
153
- statusBadge: (id: string) => `backstage.network-item.${id}.status`,
154
- url: (id: string) => `backstage.network-item.${id}.url`,
155
- sectionTab: (id: string, section: string) => `backstage.network-item.${id}.section.${section}`,
156
- curlButton: (id: string) => `backstage.network-item.${id}.curl`,
157
- },
158
-
159
- // ── Flags Tab ──────────────────────────────────
160
- flagsTab: {
161
- container: 'backstage.flags',
162
- searchInput: 'backstage.flags.search',
163
- statsBar: 'backstage.flags.stats',
164
- list: 'backstage.flags.list',
165
- /** Dynamic: backstage.flag.{key} — applied to the Switch */
166
- flagSwitch: (key: string) => `backstage.flag.${key}`,
167
- flagLabel: (key: string) => `backstage.flag.${key}.label`,
168
- flagRow: (key: string) => `backstage.flag.${key}.row`,
169
- },
170
-
171
- // ── Storage Tab ────────────────────────────────
172
- storageTab: {
173
- container: 'backstage.storage',
174
- searchInput: 'backstage.storage.search',
175
- addButton: 'backstage.storage.add-button',
176
- addForm: 'backstage.storage.add-form',
177
- addKeyInput: 'backstage.storage.add-form.key',
178
- addValueInput: 'backstage.storage.add-form.value',
179
- addSubmitButton: 'backstage.storage.add-form.submit',
180
- statsBar: 'backstage.storage.stats',
181
- list: 'backstage.storage.list',
182
- /** Dynamic: backstage.storage.entry.{key} */
183
- entryRow: (key: string) => `backstage.storage.entry.${key}`,
184
- entryEditButton: (key: string) => `backstage.storage.entry.${key}.edit`,
185
- entryDeleteButton: (key: string) => `backstage.storage.entry.${key}.delete`,
186
- entryEditInput: (key: string) => `backstage.storage.entry.${key}.edit-input`,
187
- entrySaveButton: (key: string) => `backstage.storage.entry.${key}.save`,
188
- entryCancelButton: (key: string) => `backstage.storage.entry.${key}.cancel`,
189
- },
190
-
191
- // ── Bug Report ─────────────────────────────────
192
- bugReport: {
193
- triggerButton: 'backstage.bug-report.trigger',
194
- modal: 'backstage.bug-report.modal',
195
- titleInput: 'backstage.bug-report.title',
196
- descriptionInput: 'backstage.bug-report.description',
197
- severityPicker: 'backstage.bug-report.severity',
198
- severityOption: (severity: string) => `backstage.bug-report.severity.${severity}`,
199
- toggleDeviceInfo: 'backstage.bug-report.toggle.device-info',
200
- toggleLogs: 'backstage.bug-report.toggle.logs',
201
- toggleNetwork: 'backstage.bug-report.toggle.network',
202
- toggleState: 'backstage.bug-report.toggle.state',
203
- toggleScreenshot: 'backstage.bug-report.toggle.screenshot',
204
- shareButton: 'backstage.bug-report.share',
205
- cancelButton: 'backstage.bug-report.cancel',
206
- },
207
- }
package/src/index.ts DELETED
@@ -1,37 +0,0 @@
1
- export { Backstage } from './Backstage'
2
-
3
- export type {
4
- BackstageProps,
5
- BackstageRef,
6
- BackstageTab,
7
- BackstageStyleOverrides,
8
- BackstageTheme,
9
- QuickAction,
10
- FeatureFlag,
11
- StorageAdapter,
12
- BugReport,
13
- BugReportConfig,
14
- BugReportSeverity,
15
- AppInfoItem,
16
- LogEntry,
17
- NetworkEntry,
18
- } from './types'
19
-
20
- export { LogLevel, NetworkState } from './types'
21
- export { TestIDs, DarkTheme, LightTheme } from './constants'
22
- export type { ThemePreference } from './ThemeContext'
23
-
24
- // ─── Individual Components ───────────────────────────────────────────────────
25
-
26
- export { BackstagePanel } from './components/BackstagePanel'
27
- export { FloatingPill } from './components/FloatingPill'
28
- export { TabBar } from './components/TabBar'
29
- export { InfoTab } from './components/InfoTab'
30
- export { LogsTab } from './components/LogsTab'
31
- export { LogItem } from './components/LogItem'
32
- export { NetworkTab } from './components/NetworkTab'
33
- export { NetworkItem } from './components/NetworkItem'
34
- export { FlagsTab } from './components/FlagsTab'
35
- export { StorageTab } from './components/StorageTab'
36
- export { BugReportComposer } from './components/BugReportComposer'
37
- export { JsonTreeView } from './components/JsonTreeView'
@@ -1,148 +0,0 @@
1
- import { LogLevel } from './types'
2
- import type { LogEntry } from './types'
3
- import { formatLogMessage } from './utils/stringify'
4
- import { DEFAULT_MAX_LOGS } from './constants'
5
- import { isInsideNetworkCallback } from './network-interceptor'
6
-
7
- // ─── Original Console Methods ────────────────────────────────────────────────
8
-
9
- const originalConsole = {
10
- log: console.log,
11
- debug: console.debug,
12
- info: console.info,
13
- warn: console.warn,
14
- error: console.error,
15
- }
16
-
17
- type LogCallback = (entry: LogEntry) => void
18
-
19
- let activeCallback: LogCallback | null = null
20
- let isInstalled = false
21
- let autoFilterNetwork = false
22
-
23
- // ─── Helpers ─────────────────────────────────────────────────────────────────
24
-
25
- function generateId(): string {
26
- return `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
27
- }
28
-
29
- function createLogEntry(level: LogLevel, message: unknown, optionalParams: unknown[]): LogEntry {
30
- const formattedMessage = formatLogMessage(message, optionalParams)
31
- const hasObjectData =
32
- (typeof message === 'object' && message !== null) ||
33
- optionalParams.some(p => typeof p === 'object' && p !== null)
34
-
35
- return {
36
- id: generateId(),
37
- level,
38
- message: formattedMessage,
39
- data: hasObjectData ? { message, params: optionalParams } : undefined,
40
- timestamp: Date.now(),
41
- }
42
- }
43
-
44
- function shouldLog(message: unknown, filters: string[]): boolean {
45
- if (filters.length === 0) return true
46
-
47
- const messageStr =
48
- typeof message === 'string'
49
- ? message.toLowerCase()
50
- : typeof message === 'object' && message !== null
51
- ? JSON.stringify(message).toLowerCase()
52
- : String(message).toLowerCase()
53
-
54
- return !filters.some(filter => messageStr.includes(filter.toLowerCase()))
55
- }
56
-
57
- // ─── Install / Uninstall ─────────────────────────────────────────────────────
58
-
59
- export function installInterceptor(
60
- callback: LogCallback,
61
- filters: string[] = [],
62
- options: { autoFilterNetworkLogs?: boolean } = {},
63
- ): void {
64
- if (isInstalled) {
65
- // Just update callback if already installed
66
- activeCallback = callback
67
- autoFilterNetwork = options.autoFilterNetworkLogs ?? true
68
- return
69
- }
70
-
71
- activeCallback = callback
72
- autoFilterNetwork = options.autoFilterNetworkLogs ?? true
73
- isInstalled = true
74
-
75
- const levels: Array<[LogLevel, keyof typeof originalConsole]> = [
76
- [LogLevel.log, 'log'],
77
- [LogLevel.debug, 'debug'],
78
- [LogLevel.info, 'info'],
79
- [LogLevel.warn, 'warn'],
80
- [LogLevel.error, 'error'],
81
- ]
82
-
83
- for (const [level, method] of levels) {
84
- console[method] = (message?: unknown, ...optionalParams: unknown[]) => {
85
- // Always forward to original console
86
- originalConsole[method](message, ...optionalParams)
87
-
88
- // Check filters
89
- if (!shouldLog(message, filters)) return
90
-
91
- // Skip logs from inside network callbacks (e.g., Axios interceptors)
92
- if (autoFilterNetwork && isInsideNetworkCallback()) return
93
-
94
- // Create log entry and notify
95
- if (activeCallback) {
96
- const entry = createLogEntry(level, message, optionalParams)
97
- activeCallback(entry)
98
- }
99
- }
100
- }
101
- }
102
-
103
- export function uninstallInterceptor(): void {
104
- if (!isInstalled) return
105
-
106
- console.log = originalConsole.log
107
- console.debug = originalConsole.debug
108
- console.info = originalConsole.info
109
- console.warn = originalConsole.warn
110
- console.error = originalConsole.error
111
-
112
- activeCallback = null
113
- isInstalled = false
114
- }
115
-
116
- // ─── Log Buffer ──────────────────────────────────────────────────────────────
117
-
118
- export class LogBuffer {
119
- private logs: LogEntry[] = []
120
- private maxSize: number
121
-
122
- constructor(maxSize = DEFAULT_MAX_LOGS) {
123
- this.maxSize = maxSize
124
- }
125
-
126
- push(entry: LogEntry): void {
127
- this.logs.unshift(entry)
128
- if (this.logs.length > this.maxSize) {
129
- this.logs = this.logs.slice(0, this.maxSize)
130
- }
131
- }
132
-
133
- getAll(): LogEntry[] {
134
- return [...this.logs]
135
- }
136
-
137
- clear(): void {
138
- this.logs = []
139
- }
140
-
141
- get hasErrors(): boolean {
142
- return this.logs.some(log => log.level === LogLevel.error)
143
- }
144
-
145
- get size(): number {
146
- return this.logs.length
147
- }
148
- }