rn-backstage 1.0.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.
- package/README.md +80 -0
- package/lib/commonjs/Backstage.js +130 -0
- package/lib/commonjs/Backstage.js.map +1 -0
- package/lib/commonjs/components/BackstagePanel.js +192 -0
- package/lib/commonjs/components/BackstagePanel.js.map +1 -0
- package/lib/commonjs/components/FloatingPill.js +147 -0
- package/lib/commonjs/components/FloatingPill.js.map +1 -0
- package/lib/commonjs/components/InfoTab.js +260 -0
- package/lib/commonjs/components/InfoTab.js.map +1 -0
- package/lib/commonjs/components/JsonTreeView.js +245 -0
- package/lib/commonjs/components/JsonTreeView.js.map +1 -0
- package/lib/commonjs/components/LogItem.js +191 -0
- package/lib/commonjs/components/LogItem.js.map +1 -0
- package/lib/commonjs/components/LogsTab.js +235 -0
- package/lib/commonjs/components/LogsTab.js.map +1 -0
- package/lib/commonjs/components/TabBar.js +132 -0
- package/lib/commonjs/components/TabBar.js.map +1 -0
- package/lib/commonjs/constants.js +89 -0
- package/lib/commonjs/constants.js.map +1 -0
- package/lib/commonjs/index.js +20 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/log-interceptor.js +114 -0
- package/lib/commonjs/log-interceptor.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +21 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/utils/formatTimestamp.js +26 -0
- package/lib/commonjs/utils/formatTimestamp.js.map +1 -0
- package/lib/commonjs/utils/stringify.js +78 -0
- package/lib/commonjs/utils/stringify.js.map +1 -0
- package/lib/module/Backstage.js +125 -0
- package/lib/module/Backstage.js.map +1 -0
- package/lib/module/components/BackstagePanel.js +187 -0
- package/lib/module/components/BackstagePanel.js.map +1 -0
- package/lib/module/components/FloatingPill.js +142 -0
- package/lib/module/components/FloatingPill.js.map +1 -0
- package/lib/module/components/InfoTab.js +255 -0
- package/lib/module/components/InfoTab.js.map +1 -0
- package/lib/module/components/JsonTreeView.js +241 -0
- package/lib/module/components/JsonTreeView.js.map +1 -0
- package/lib/module/components/LogItem.js +186 -0
- package/lib/module/components/LogItem.js.map +1 -0
- package/lib/module/components/LogsTab.js +230 -0
- package/lib/module/components/LogsTab.js.map +1 -0
- package/lib/module/components/TabBar.js +127 -0
- package/lib/module/components/TabBar.js.map +1 -0
- package/lib/module/constants.js +85 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/log-interceptor.js +108 -0
- package/lib/module/log-interceptor.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +25 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/formatTimestamp.js +22 -0
- package/lib/module/utils/formatTimestamp.js.map +1 -0
- package/lib/module/utils/stringify.js +73 -0
- package/lib/module/utils/stringify.js.map +1 -0
- package/lib/typescript/Backstage.d.ts +4 -0
- package/lib/typescript/Backstage.d.ts.map +1 -0
- package/lib/typescript/components/BackstagePanel.d.ts +21 -0
- package/lib/typescript/components/BackstagePanel.d.ts.map +1 -0
- package/lib/typescript/components/FloatingPill.d.ts +13 -0
- package/lib/typescript/components/FloatingPill.d.ts.map +1 -0
- package/lib/typescript/components/InfoTab.d.ts +16 -0
- package/lib/typescript/components/InfoTab.d.ts.map +1 -0
- package/lib/typescript/components/JsonTreeView.d.ts +10 -0
- package/lib/typescript/components/JsonTreeView.d.ts.map +1 -0
- package/lib/typescript/components/LogItem.d.ts +10 -0
- package/lib/typescript/components/LogItem.d.ts.map +1 -0
- package/lib/typescript/components/LogsTab.d.ts +11 -0
- package/lib/typescript/components/LogsTab.d.ts.map +1 -0
- package/lib/typescript/components/TabBar.d.ts +14 -0
- package/lib/typescript/components/TabBar.d.ts.map +1 -0
- package/lib/typescript/constants.d.ts +43 -0
- package/lib/typescript/constants.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +4 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/log-interceptor.d.ts +16 -0
- package/lib/typescript/log-interceptor.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +110 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/utils/formatTimestamp.d.ts +7 -0
- package/lib/typescript/utils/formatTimestamp.d.ts.map +1 -0
- package/lib/typescript/utils/stringify.d.ts +9 -0
- package/lib/typescript/utils/stringify.d.ts.map +1 -0
- package/package.json +91 -0
- package/src/Backstage.tsx +145 -0
- package/src/components/BackstagePanel.tsx +208 -0
- package/src/components/FloatingPill.tsx +150 -0
- package/src/components/InfoTab.tsx +264 -0
- package/src/components/JsonTreeView.tsx +259 -0
- package/src/components/LogItem.tsx +189 -0
- package/src/components/LogsTab.tsx +239 -0
- package/src/components/TabBar.tsx +150 -0
- package/src/constants.ts +82 -0
- package/src/index.ts +14 -0
- package/src/log-interceptor.ts +137 -0
- package/src/types.ts +147 -0
- package/src/utils/formatTimestamp.ts +22 -0
- package/src/utils/stringify.ts +90 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { useCallback, 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 { DarkTheme, MonospaceFont } from '../constants'
|
|
12
|
+
|
|
13
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
interface Tab {
|
|
16
|
+
key: string
|
|
17
|
+
title: string
|
|
18
|
+
icon?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface TabBarProps {
|
|
22
|
+
tabs: Tab[]
|
|
23
|
+
activeTab: string
|
|
24
|
+
onTabChange: (key: string) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export const TabBar: React.FC<TabBarProps> = ({ tabs, activeTab, onTabChange }) => {
|
|
30
|
+
const indicatorAnim = useRef(new Animated.Value(0)).current
|
|
31
|
+
const tabWidths = useRef<Record<string, number>>({})
|
|
32
|
+
const tabOffsets = useRef<Record<string, number>>({})
|
|
33
|
+
const scrollRef = useRef<ScrollView>(null)
|
|
34
|
+
|
|
35
|
+
// Animate indicator when active tab changes
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const offset = tabOffsets.current[activeTab] || 0
|
|
38
|
+
Animated.spring(indicatorAnim, {
|
|
39
|
+
toValue: offset,
|
|
40
|
+
useNativeDriver: true,
|
|
41
|
+
friction: 8,
|
|
42
|
+
tension: 60,
|
|
43
|
+
}).start()
|
|
44
|
+
}, [activeTab, indicatorAnim])
|
|
45
|
+
|
|
46
|
+
const handleTabLayout = useCallback(
|
|
47
|
+
(key: string) => (e: LayoutChangeEvent) => {
|
|
48
|
+
const { x, width } = e.nativeEvent.layout
|
|
49
|
+
tabWidths.current[key] = width
|
|
50
|
+
tabOffsets.current[key] = x
|
|
51
|
+
|
|
52
|
+
// Re-animate if this is the active tab
|
|
53
|
+
if (key === activeTab) {
|
|
54
|
+
indicatorAnim.setValue(x)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[activeTab, indicatorAnim],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const activeWidth = tabWidths.current[activeTab] || 80
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<View style={styles.container}>
|
|
64
|
+
<ScrollView
|
|
65
|
+
ref={scrollRef}
|
|
66
|
+
horizontal
|
|
67
|
+
showsHorizontalScrollIndicator={false}
|
|
68
|
+
contentContainerStyle={styles.scrollContent}
|
|
69
|
+
>
|
|
70
|
+
{tabs.map(tab => {
|
|
71
|
+
const isActive = tab.key === activeTab
|
|
72
|
+
return (
|
|
73
|
+
<TouchableOpacity
|
|
74
|
+
key={tab.key}
|
|
75
|
+
onPress={() => onTabChange(tab.key)}
|
|
76
|
+
onLayout={handleTabLayout(tab.key)}
|
|
77
|
+
style={styles.tab}
|
|
78
|
+
activeOpacity={0.7}
|
|
79
|
+
testID={`backstage.tab.${tab.key}`}
|
|
80
|
+
>
|
|
81
|
+
{tab.icon && (
|
|
82
|
+
<Text style={[styles.tabIcon, isActive && styles.tabIconActive]}>{tab.icon}</Text>
|
|
83
|
+
)}
|
|
84
|
+
<Text style={[styles.tabText, isActive && styles.tabTextActive]}>{tab.title}</Text>
|
|
85
|
+
</TouchableOpacity>
|
|
86
|
+
)
|
|
87
|
+
})}
|
|
88
|
+
|
|
89
|
+
{/* Animated underline */}
|
|
90
|
+
<Animated.View
|
|
91
|
+
style={[
|
|
92
|
+
styles.indicator,
|
|
93
|
+
{
|
|
94
|
+
width: activeWidth,
|
|
95
|
+
transform: [{ translateX: indicatorAnim }],
|
|
96
|
+
},
|
|
97
|
+
]}
|
|
98
|
+
/>
|
|
99
|
+
</ScrollView>
|
|
100
|
+
</View>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Styles ──────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const styles = StyleSheet.create({
|
|
107
|
+
container: {
|
|
108
|
+
borderBottomWidth: 1,
|
|
109
|
+
borderBottomColor: DarkTheme.border,
|
|
110
|
+
},
|
|
111
|
+
scrollContent: {
|
|
112
|
+
flexDirection: 'row',
|
|
113
|
+
position: 'relative',
|
|
114
|
+
paddingBottom: 0,
|
|
115
|
+
},
|
|
116
|
+
tab: {
|
|
117
|
+
flexDirection: 'row',
|
|
118
|
+
alignItems: 'center',
|
|
119
|
+
justifyContent: 'center',
|
|
120
|
+
paddingHorizontal: 20,
|
|
121
|
+
paddingVertical: 14,
|
|
122
|
+
minWidth: 80,
|
|
123
|
+
},
|
|
124
|
+
tabIcon: {
|
|
125
|
+
fontSize: 14,
|
|
126
|
+
marginRight: 6,
|
|
127
|
+
opacity: 0.5,
|
|
128
|
+
},
|
|
129
|
+
tabIconActive: {
|
|
130
|
+
opacity: 1,
|
|
131
|
+
},
|
|
132
|
+
tabText: {
|
|
133
|
+
fontFamily: MonospaceFont,
|
|
134
|
+
fontSize: 13,
|
|
135
|
+
fontWeight: '600',
|
|
136
|
+
color: DarkTheme.textMuted,
|
|
137
|
+
textTransform: 'uppercase',
|
|
138
|
+
letterSpacing: 1,
|
|
139
|
+
},
|
|
140
|
+
tabTextActive: {
|
|
141
|
+
color: DarkTheme.accent,
|
|
142
|
+
},
|
|
143
|
+
indicator: {
|
|
144
|
+
position: 'absolute',
|
|
145
|
+
bottom: 0,
|
|
146
|
+
height: 2,
|
|
147
|
+
backgroundColor: DarkTheme.accent,
|
|
148
|
+
borderRadius: 1,
|
|
149
|
+
},
|
|
150
|
+
})
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
// ─── Max Logs ────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_MAX_LOGS = 500
|
|
47
|
+
|
|
48
|
+
// ─── Monospace Font ──────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export const MonospaceFont = Platform.select({
|
|
51
|
+
ios: 'Menlo',
|
|
52
|
+
default: 'monospace',
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// ─── Test IDs ────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export const TestIDs = {
|
|
58
|
+
floatingPill: 'backstage.floating-pill',
|
|
59
|
+
panel: 'backstage.panel',
|
|
60
|
+
header: {
|
|
61
|
+
container: 'backstage.header',
|
|
62
|
+
title: 'backstage.header.title',
|
|
63
|
+
closeButton: 'backstage.header.close',
|
|
64
|
+
},
|
|
65
|
+
tabBar: 'backstage.tab-bar',
|
|
66
|
+
tabs: {
|
|
67
|
+
info: 'backstage.tab.info',
|
|
68
|
+
logs: 'backstage.tab.logs',
|
|
69
|
+
},
|
|
70
|
+
infoTab: {
|
|
71
|
+
section: 'backstage.info',
|
|
72
|
+
deviceInfo: 'backstage.info.device',
|
|
73
|
+
stateTree: 'backstage.info.state-tree',
|
|
74
|
+
quickActions: 'backstage.info.quick-actions',
|
|
75
|
+
},
|
|
76
|
+
logsTab: {
|
|
77
|
+
section: 'backstage.logs',
|
|
78
|
+
searchInput: 'backstage.logs.search',
|
|
79
|
+
copyButton: 'backstage.logs.copy',
|
|
80
|
+
list: 'backstage.logs.list',
|
|
81
|
+
},
|
|
82
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { Backstage } from './Backstage'
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
BackstageProps,
|
|
5
|
+
BackstageRef,
|
|
6
|
+
BackstageTab,
|
|
7
|
+
BackstageStyleOverrides,
|
|
8
|
+
BackstageTheme,
|
|
9
|
+
QuickAction,
|
|
10
|
+
AppInfoItem,
|
|
11
|
+
LogEntry,
|
|
12
|
+
} from './types'
|
|
13
|
+
|
|
14
|
+
export { LogLevel } from './types'
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
|
|
6
|
+
// ─── Original Console Methods ────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const originalConsole = {
|
|
9
|
+
log: console.log,
|
|
10
|
+
debug: console.debug,
|
|
11
|
+
info: console.info,
|
|
12
|
+
warn: console.warn,
|
|
13
|
+
error: console.error,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type LogCallback = (entry: LogEntry) => void
|
|
17
|
+
|
|
18
|
+
let activeCallback: LogCallback | null = null
|
|
19
|
+
let isInstalled = false
|
|
20
|
+
|
|
21
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function generateId(): string {
|
|
24
|
+
return `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createLogEntry(level: LogLevel, message: unknown, optionalParams: unknown[]): LogEntry {
|
|
28
|
+
const formattedMessage = formatLogMessage(message, optionalParams)
|
|
29
|
+
const hasObjectData =
|
|
30
|
+
(typeof message === 'object' && message !== null) ||
|
|
31
|
+
optionalParams.some(p => typeof p === 'object' && p !== null)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
id: generateId(),
|
|
35
|
+
level,
|
|
36
|
+
message: formattedMessage,
|
|
37
|
+
data: hasObjectData ? { message, params: optionalParams } : undefined,
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function shouldLog(message: unknown, filters: string[]): boolean {
|
|
43
|
+
if (filters.length === 0) return true
|
|
44
|
+
|
|
45
|
+
const messageStr =
|
|
46
|
+
typeof message === 'string'
|
|
47
|
+
? message.toLowerCase()
|
|
48
|
+
: typeof message === 'object' && message !== null
|
|
49
|
+
? JSON.stringify(message).toLowerCase()
|
|
50
|
+
: String(message).toLowerCase()
|
|
51
|
+
|
|
52
|
+
return !filters.some(filter => messageStr.includes(filter.toLowerCase()))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Install / Uninstall ─────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export function installInterceptor(callback: LogCallback, filters: string[] = []): void {
|
|
58
|
+
if (isInstalled) {
|
|
59
|
+
// Just update callback if already installed
|
|
60
|
+
activeCallback = callback
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
activeCallback = callback
|
|
65
|
+
isInstalled = true
|
|
66
|
+
|
|
67
|
+
const levels: Array<[LogLevel, keyof typeof originalConsole]> = [
|
|
68
|
+
[LogLevel.log, 'log'],
|
|
69
|
+
[LogLevel.debug, 'debug'],
|
|
70
|
+
[LogLevel.info, 'info'],
|
|
71
|
+
[LogLevel.warn, 'warn'],
|
|
72
|
+
[LogLevel.error, 'error'],
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
for (const [level, method] of levels) {
|
|
76
|
+
console[method] = (message?: unknown, ...optionalParams: unknown[]) => {
|
|
77
|
+
// Always forward to original console
|
|
78
|
+
originalConsole[method](message, ...optionalParams)
|
|
79
|
+
|
|
80
|
+
// Check filters
|
|
81
|
+
if (!shouldLog(message, filters)) return
|
|
82
|
+
|
|
83
|
+
// Create log entry and notify
|
|
84
|
+
if (activeCallback) {
|
|
85
|
+
const entry = createLogEntry(level, message, optionalParams)
|
|
86
|
+
activeCallback(entry)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function uninstallInterceptor(): void {
|
|
93
|
+
if (!isInstalled) return
|
|
94
|
+
|
|
95
|
+
console.log = originalConsole.log
|
|
96
|
+
console.debug = originalConsole.debug
|
|
97
|
+
console.info = originalConsole.info
|
|
98
|
+
console.warn = originalConsole.warn
|
|
99
|
+
console.error = originalConsole.error
|
|
100
|
+
|
|
101
|
+
activeCallback = null
|
|
102
|
+
isInstalled = false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Log Buffer ──────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
export class LogBuffer {
|
|
108
|
+
private logs: LogEntry[] = []
|
|
109
|
+
private maxSize: number
|
|
110
|
+
|
|
111
|
+
constructor(maxSize = DEFAULT_MAX_LOGS) {
|
|
112
|
+
this.maxSize = maxSize
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
push(entry: LogEntry): void {
|
|
116
|
+
this.logs.unshift(entry)
|
|
117
|
+
if (this.logs.length > this.maxSize) {
|
|
118
|
+
this.logs = this.logs.slice(0, this.maxSize)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getAll(): LogEntry[] {
|
|
123
|
+
return [...this.logs]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
clear(): void {
|
|
127
|
+
this.logs = []
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get hasErrors(): boolean {
|
|
131
|
+
return this.logs.some(log => log.level === LogLevel.error)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get size(): number {
|
|
135
|
+
return this.logs.length
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import type { StyleProp, TextStyle, ViewStyle } from 'react-native'
|
|
3
|
+
|
|
4
|
+
// ─── Log Types ───────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export enum LogLevel {
|
|
7
|
+
log = 'log',
|
|
8
|
+
debug = 'debug',
|
|
9
|
+
info = 'info',
|
|
10
|
+
warn = 'warn',
|
|
11
|
+
error = 'error',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LogEntry {
|
|
15
|
+
id: string
|
|
16
|
+
level: LogLevel
|
|
17
|
+
message: string
|
|
18
|
+
data?: unknown
|
|
19
|
+
timestamp: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── App Info ────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export interface AppInfoItem {
|
|
25
|
+
label: string
|
|
26
|
+
value: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Quick Actions ───────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export interface QuickAction {
|
|
32
|
+
title: string
|
|
33
|
+
onPress: () => void
|
|
34
|
+
closeOnPress?: boolean
|
|
35
|
+
icon?: string
|
|
36
|
+
destructive?: boolean
|
|
37
|
+
testID?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Extensible Tabs ─────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export interface BackstageTab {
|
|
43
|
+
key: string
|
|
44
|
+
title: string
|
|
45
|
+
icon?: string
|
|
46
|
+
render: () => ReactNode
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Theme ───────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export interface BackstageTheme {
|
|
52
|
+
background: string
|
|
53
|
+
surface: string
|
|
54
|
+
surfaceElevated: string
|
|
55
|
+
border: string
|
|
56
|
+
text: string
|
|
57
|
+
textSecondary: string
|
|
58
|
+
textMuted: string
|
|
59
|
+
accent: string
|
|
60
|
+
accentDim: string
|
|
61
|
+
error: string
|
|
62
|
+
errorDim: string
|
|
63
|
+
warning: string
|
|
64
|
+
warningDim: string
|
|
65
|
+
success: string
|
|
66
|
+
info: string
|
|
67
|
+
infoDim: string
|
|
68
|
+
debugColor: string
|
|
69
|
+
debugDim: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Main Props ──────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export interface BackstageProps {
|
|
75
|
+
/** Whether the floating pill trigger is visible. Default: true */
|
|
76
|
+
visible?: boolean
|
|
77
|
+
|
|
78
|
+
/** App version string (e.g., "1.2.3") */
|
|
79
|
+
appVersion?: string
|
|
80
|
+
|
|
81
|
+
/** Build number (e.g., "42") */
|
|
82
|
+
buildNumber?: string
|
|
83
|
+
|
|
84
|
+
/** Bundle identifier (e.g., "com.example.app") */
|
|
85
|
+
bundleId?: string
|
|
86
|
+
|
|
87
|
+
/** Additional device/app information rows */
|
|
88
|
+
deviceInfo?: AppInfoItem[]
|
|
89
|
+
|
|
90
|
+
/** State object to display in the tree viewer (Redux store, Zustand, etc.) */
|
|
91
|
+
state?: Record<string, unknown>
|
|
92
|
+
|
|
93
|
+
/** Custom action buttons in the Info tab */
|
|
94
|
+
quickActions?: QuickAction[]
|
|
95
|
+
|
|
96
|
+
/** Maximum number of logs to retain in memory. Default: 500 */
|
|
97
|
+
maxLogs?: number
|
|
98
|
+
|
|
99
|
+
/** Log messages containing these strings will be excluded */
|
|
100
|
+
logFilters?: string[]
|
|
101
|
+
|
|
102
|
+
/** Callback triggered when user copies logs */
|
|
103
|
+
onCopyLogs?: (logs: string) => void
|
|
104
|
+
|
|
105
|
+
/** Additional custom tabs beyond Info and Logs */
|
|
106
|
+
extraTabs?: BackstageTab[]
|
|
107
|
+
|
|
108
|
+
/** Extra content rendered at the bottom of the Info tab */
|
|
109
|
+
children?: ReactNode
|
|
110
|
+
|
|
111
|
+
/** Custom styles overrides */
|
|
112
|
+
styles?: BackstageStyleOverrides
|
|
113
|
+
|
|
114
|
+
/** Initial X position for the floating pill */
|
|
115
|
+
initialX?: number
|
|
116
|
+
|
|
117
|
+
/** Initial Y position for the floating pill */
|
|
118
|
+
initialY?: number
|
|
119
|
+
|
|
120
|
+
/** Text displayed on the floating pill. Defaults to appVersion or "DEV" */
|
|
121
|
+
pillText?: string
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface BackstageStyleOverrides {
|
|
125
|
+
pillStyle?: StyleProp<ViewStyle>
|
|
126
|
+
pillTextStyle?: StyleProp<TextStyle>
|
|
127
|
+
panelStyle?: StyleProp<ViewStyle>
|
|
128
|
+
headerTitleStyle?: StyleProp<TextStyle>
|
|
129
|
+
sectionTitleStyle?: StyleProp<TextStyle>
|
|
130
|
+
infoLabelStyle?: StyleProp<TextStyle>
|
|
131
|
+
infoValueStyle?: StyleProp<TextStyle>
|
|
132
|
+
actionButtonStyle?: StyleProp<ViewStyle>
|
|
133
|
+
actionButtonTitleStyle?: StyleProp<TextStyle>
|
|
134
|
+
logTimestampStyle?: StyleProp<TextStyle>
|
|
135
|
+
logMessageStyle?: StyleProp<TextStyle>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Ref Methods ─────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export interface BackstageRef {
|
|
141
|
+
/** Open the backstage panel */
|
|
142
|
+
open: () => void
|
|
143
|
+
/** Close the backstage panel */
|
|
144
|
+
close: () => void
|
|
145
|
+
/** Clear all captured logs */
|
|
146
|
+
clearLogs: () => void
|
|
147
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a timestamp (Date.now()) into a human-readable local time string.
|
|
3
|
+
* Format: "h:mm:ss.SSS AM/PM"
|
|
4
|
+
* No external dependencies — uses native Date API.
|
|
5
|
+
*/
|
|
6
|
+
export function formatTimestamp(timestamp: number): string {
|
|
7
|
+
const date = new Date(timestamp)
|
|
8
|
+
let hours = date.getHours()
|
|
9
|
+
const minutes = date.getMinutes()
|
|
10
|
+
const seconds = date.getSeconds()
|
|
11
|
+
const milliseconds = date.getMilliseconds()
|
|
12
|
+
const ampm = hours >= 12 ? 'PM' : 'AM'
|
|
13
|
+
|
|
14
|
+
hours = hours % 12
|
|
15
|
+
hours = hours || 12
|
|
16
|
+
|
|
17
|
+
const mm = minutes.toString().padStart(2, '0')
|
|
18
|
+
const ss = seconds.toString().padStart(2, '0')
|
|
19
|
+
const ms = milliseconds.toString().padStart(3, '0')
|
|
20
|
+
|
|
21
|
+
return `${hours}:${mm}:${ss}.${ms} ${ampm}`
|
|
22
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely stringifies a value, handling circular references and Error objects.
|
|
3
|
+
*/
|
|
4
|
+
export function safeStringify(value: unknown, indent = 2): string {
|
|
5
|
+
if (value === undefined) return 'undefined'
|
|
6
|
+
if (value === null) return 'null'
|
|
7
|
+
|
|
8
|
+
if (value instanceof Error) {
|
|
9
|
+
return JSON.stringify(
|
|
10
|
+
{
|
|
11
|
+
name: value.name,
|
|
12
|
+
message: value.message,
|
|
13
|
+
stack: value.stack,
|
|
14
|
+
},
|
|
15
|
+
null,
|
|
16
|
+
indent,
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const seen = new WeakSet()
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return JSON.stringify(
|
|
24
|
+
value,
|
|
25
|
+
(_key, val) => {
|
|
26
|
+
if (typeof val === 'object' && val !== null) {
|
|
27
|
+
if (seen.has(val)) {
|
|
28
|
+
return '[Circular]'
|
|
29
|
+
}
|
|
30
|
+
seen.add(val)
|
|
31
|
+
}
|
|
32
|
+
if (typeof val === 'bigint') {
|
|
33
|
+
return val.toString()
|
|
34
|
+
}
|
|
35
|
+
if (typeof val === 'function') {
|
|
36
|
+
return `[Function: ${val.name || 'anonymous'}]`
|
|
37
|
+
}
|
|
38
|
+
if (typeof val === 'symbol') {
|
|
39
|
+
return val.toString()
|
|
40
|
+
}
|
|
41
|
+
return val
|
|
42
|
+
},
|
|
43
|
+
indent,
|
|
44
|
+
)
|
|
45
|
+
} catch {
|
|
46
|
+
return String(value)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Formats a console message and its optional params into a single string.
|
|
52
|
+
*/
|
|
53
|
+
export function formatLogMessage(message: unknown, optionalParams?: unknown[]): string {
|
|
54
|
+
const parts: string[] = []
|
|
55
|
+
|
|
56
|
+
if (message instanceof Error) {
|
|
57
|
+
parts.push(`${message.name}: ${message.message}`)
|
|
58
|
+
} else if (typeof message === 'object' && message !== null) {
|
|
59
|
+
parts.push(safeStringify(message, 0))
|
|
60
|
+
} else if (typeof message === 'string') {
|
|
61
|
+
// Strip console color formatting (%c)
|
|
62
|
+
parts.push(message.replace(/%c/g, '').trim())
|
|
63
|
+
} else {
|
|
64
|
+
parts.push(String(message))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (optionalParams && optionalParams.length > 0) {
|
|
68
|
+
for (const param of optionalParams) {
|
|
69
|
+
if (typeof param === 'string' && isColorString(param)) {
|
|
70
|
+
continue // skip css color strings from console.log('%c ...', 'color: ...')
|
|
71
|
+
}
|
|
72
|
+
if (typeof param === 'object' && param !== null) {
|
|
73
|
+
parts.push(safeStringify(param, 0))
|
|
74
|
+
} else {
|
|
75
|
+
parts.push(String(param))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return parts.join(' ')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isColorString(str: string): boolean {
|
|
84
|
+
return (
|
|
85
|
+
str.includes('color:') ||
|
|
86
|
+
str.includes('background:') ||
|
|
87
|
+
str.includes('font-') ||
|
|
88
|
+
/^#[0-9A-Fa-f]{3,8}$/.test(str.trim())
|
|
89
|
+
)
|
|
90
|
+
}
|